You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/08 11:52:40 UTC
(texera) branch gh-readonly-queue/main/pr-7418-96ed553760a299f2031f4cd83c024e9327cb5346 created (now 35346629ce)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-7418-96ed553760a299f2031f4cd83c024e9327cb5346
in repository https://gitbox.apache.org/repos/asf/texera.git
at 35346629ce test(frontend): extend UserProjectListItemComponent template coverage (#7418)
This branch includes the following new commits:
new 35346629ce test(frontend): extend UserProjectListItemComponent template coverage (#7418)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): extend UserProjectListItemComponent template coverage (#7418)
Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-7418-96ed553760a299f2031f4cd83c024e9327cb5346
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 35346629ce00493927556df92fc839cb79b78dfa
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sat Aug 8 04:52:20 2026 -0700
test(frontend): extend UserProjectListItemComponent template coverage (#7418)
### What changes were proposed in this PR?
Rebased onto `main` after #7415 landed on the same file, and reduced to
what that
PR left uncovered. #7415 pins what each template branch *looks like* by
putting the
component into the state directly; this PR pins the wiring that produces
those
states, so a control losing its handler fails here. Together they take
`user-project-list-item.component.html` to 100% (110/110 statements, no
uncovered
branches — it was 77/110 after #7415).
8 new tests:
- **colour panel** — the `[(colorPicker)]` / `(colorPickerSelect)`
outputs, and the
`cpExtraTemplate` menu, whose markup exists only while the picker is
open: its
Save action, and its Delete action in both states (disabled while the
project has
no colour, enabled and wired once one is set). None of this was
previously
rendered — it is the bulk of the gap #7410 describes.
- **name** — the edit button opens the input, `keyup.enter` saves,
`focusout` closes.
- **description** — the expand/collapse controls, the edit button,
`focusout`
saving, and the suffix save icon closing the editor.
- **actions** — the share button and the delete popconfirm's
`nzOnConfirm`.
- three class-level gaps: the `entry` getter's guard, `ngOnInit`
adopting a stored
colour, and `updateProjectColor` skipping the service when the colour is
unchanged (class 63/67 -> 66/67).
This block queries with `By.css` + `triggerEventHandler` rather than
`querySelector`: `nzOnConfirm`, `keyup.enter` and `colorPickerSelect`
are directive
outputs, not DOM events, so a native dispatch cannot reach them. #7415's
`MarkdownModule.forRoot()` is kept as-is. No production code was
changed.
Tests that #7415 already covers (the creation date, the `editable`
gating, the
read-only delete branch) were dropped from this PR rather than
duplicated.
One statement stays uncovered: the `if (!this.entry) throw` guard inside
`saveProjectName`'s subscribe. The `entry` getter already throws when no
entry was
provided, so that branch cannot be reached — it is dead code rather than
a coverage
gap, and removing it felt out of scope for a test-only PR.
### Any related issues, documentation, discussions?
Closes #7410. Builds on #7415 (#7412), which covers the same template
from the
rendering side.
### How was this PR tested?
`ng test --watch=false --include
src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts`
— 28 passed (20 existing + 8 new), run 3x for determinism. Coverage
(`--coverage`)
confirms `user-project-list-item.component.html` at 110/110 statements
with no
uncovered branches, and the class at 66/67. The failure path was
verified by
breaking an assertion (red, non-zero exit); eslint and prettier are
clean.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---
.../user-project-list-item.component.spec.ts | 167 +++++++++++++++++++++
1 file changed, 167 insertions(+)
diff --git a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
index f2198854b1..58c57b18af 100644
--- a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
@@ -32,6 +32,8 @@ import { UserService } from "../../../../../common/service/user/user.service";
import { commonTestProviders } from "../../../../../common/testing/test-utils";
import { ShareAccessComponent } from "../../share-access/share-access.component";
import { DatePipe } from "@angular/common";
+import { By } from "@angular/platform-browser";
+import { ColorPickerDirective } from "ngx-color-picker";
import { of } from "rxjs";
import { MarkdownModule } from "ngx-markdown";
@@ -131,6 +133,16 @@ describe("UserProjectListItemComponent", () => {
expect(spy).not.toHaveBeenCalled();
});
+ it("does not call the service when the color is unchanged", () => {
+ const spy = vi.spyOn(userProjectService, "updateProjectColor");
+ component.entry = { ...component.entry, color: "123456" };
+ component.color = "#123456";
+
+ component.updateProjectColor();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
it("is a no-op when the item is not editable", () => {
const spy = vi.spyOn(userProjectService, "updateProjectColor");
component.editable = false;
@@ -332,4 +344,159 @@ describe("UserProjectListItemComponent", () => {
expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).not.toBeNull();
});
});
+
+ /**
+ * The block above pins what each branch looks like by putting the component into that state
+ * directly. What is left unpinned is the wiring in between: the controls that produce those
+ * states, the outputs the parent listens to, and the colour menu, whose markup only exists
+ * while the picker is open. These go through the DOM so a control losing its handler fails
+ * here. Events that ng-zorro / the colour picker raise as directive outputs rather than DOM
+ * events (`nzOnConfirm`, `keyup.enter`, `colorPickerSelect`) need DebugElement, so this block
+ * queries with `By.css` instead of `querySelector`.
+ */
+ describe("rendered controls", () => {
+ // Name what was missing, so a markup change fails with the element it could not find rather
+ // than "Cannot read properties of undefined".
+ const required = <T>(value: T | null | undefined, what: string): T => {
+ if (value === null || value === undefined) {
+ throw new Error(`expected ${what} to be rendered`);
+ }
+ return value;
+ };
+ const q = (selector: string) => hostFixture.debugElement.query(By.css(selector));
+ const buttonLabelled = (label: string) =>
+ required(
+ hostFixture.debugElement
+ .queryAll(By.css("button"))
+ .find(button => (button.nativeElement.textContent ?? "").trim() === label),
+ `a button labelled "${label}"`
+ );
+
+ // The colour menu lives in `cpExtraTemplate`, so it exists only once the picker is open,
+ // which `[(cpToggle)]="editingColor"` drives.
+ const openColorPanel = () => {
+ component.editingColor = true;
+ hostFixture.detectChanges();
+ };
+
+ afterEach(() => {
+ // An open colour picker keeps document-level listeners; destroying the view tears them
+ // down so a panel cannot outlive its test.
+ hostFixture.destroy();
+ });
+
+ it("wires the colour-picker outputs to the component", () => {
+ const updateSpy = vi.spyOn(component, "updateProjectColor").mockImplementation(() => {});
+ const picker = hostFixture.debugElement.query(By.directive(ColorPickerDirective));
+
+ picker.triggerEventHandler("colorPickerChange", "#abcdef");
+ picker.triggerEventHandler("colorPickerSelect", "#abcdef");
+
+ expect(component.color).toBe("#abcdef");
+ expect(updateSpy).toHaveBeenCalled();
+ });
+
+ it("wires the colour menu's Save action", () => {
+ const updateSpy = vi.spyOn(component, "updateProjectColor").mockImplementation(() => {});
+
+ openColorPanel();
+ buttonLabelled("Save").triggerEventHandler("click", null);
+
+ expect(updateSpy).toHaveBeenCalled();
+ });
+
+ it("enables the colour menu's Delete action only once a colour is set", () => {
+ const removeSpy = vi.spyOn(component, "removeProjectColor").mockImplementation(() => {});
+
+ // testProject starts with color: null, so there is nothing to delete yet.
+ openColorPanel();
+ expect(buttonLabelled("Delete").nativeElement.disabled).toBe(true);
+
+ component.entry = { ...component.entry, color: "123456" };
+ hostFixture.detectChanges();
+ const deleteButton = buttonLabelled("Delete");
+ expect(deleteButton.nativeElement.disabled).toBe(false);
+
+ deleteButton.triggerEventHandler("click", null);
+
+ expect(removeSpy).toHaveBeenCalled();
+ });
+
+ it("opens name editing, saves on enter, and closes on focusout", () => {
+ const saveSpy = vi.spyOn(component, "saveProjectName").mockImplementation(() => {});
+
+ required(q(".edit-name-icon").parent, "the edit-name button").triggerEventHandler("click", null);
+ hostFixture.detectChanges();
+ expect(component.editingName).toBe(true);
+
+ const input = q("input");
+ input.nativeElement.value = "renamed";
+ input.triggerEventHandler("keyup.enter", {});
+ expect(saveSpy).toHaveBeenCalledWith("renamed");
+
+ input.triggerEventHandler("focusout", {});
+ expect(component.editingName).toBe(false);
+ });
+
+ it("expands and re-collapses the description through its controls", () => {
+ q('[nz-tooltip="Expand Description"]').triggerEventHandler("click", null);
+ hostFixture.detectChanges();
+ expect(component.descriptionCollapsed).toBe(false);
+
+ q('[nz-tooltip="Collapse Description"]').triggerEventHandler("click", null);
+ hostFixture.detectChanges();
+ expect(component.descriptionCollapsed).toBe(true);
+ });
+
+ it("opens description editing, saves on focusout, and closes through the save icon", () => {
+ const saveSpy = vi.spyOn(component, "saveProjectDescription").mockImplementation(() => {});
+
+ required(q(".edit-description-icon").parent, "the edit-description button").triggerEventHandler("click", null);
+ hostFixture.detectChanges();
+ expect(component.editingDescription).toBe(true);
+
+ const textarea = q("textarea");
+ textarea.nativeElement.value = "a new description";
+ hostFixture.detectChanges();
+
+ textarea.triggerEventHandler("focusout", {});
+ expect(saveSpy).toHaveBeenCalledWith("a new description");
+
+ q(".ant-input-clear-icon").triggerEventHandler("click", null);
+ hostFixture.detectChanges();
+ expect(component.editingDescription).toBe(false);
+ });
+
+ it("wires the share and delete actions", () => {
+ const shareSpy = vi.spyOn(component, "onClickOpenShareAccess").mockImplementation(() => {});
+ const deletedSpy = vi.spyOn(component.deleted, "emit");
+ const [share, remove] = hostFixture.debugElement.queryAll(By.css("ul[nz-list-item-actions] button"));
+
+ share.triggerEventHandler("click", null);
+ remove.triggerEventHandler("nzOnConfirm", null);
+
+ expect(shareSpy).toHaveBeenCalled();
+ expect(deletedSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe("entry input", () => {
+ it("throws when read before an entry is provided", () => {
+ component.entry = undefined as unknown as DashboardProject;
+
+ expect(() => component.entry).toThrowError("entry property must be provided");
+ });
+ });
+
+ describe("ngOnInit", () => {
+ it("adopts the project's stored colour", () => {
+ // A fresh fixture (rather than the shared one) so ngOnInit sees the colour.
+ const fixture = TestBed.createComponent(TestHostComponent);
+ fixture.componentInstance.entry = { ...testProject, color: "123456" };
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.inner.color).toBe("123456");
+ fixture.destroy();
+ });
+ });
});