This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 35346629ce test(frontend): extend UserProjectListItemComponent
template coverage (#7418)
35346629ce is described below
commit 35346629ce00493927556df92fc839cb79b78dfa
Author: Meng Wang <[email protected]>
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();
+ });
+ });
});