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
commit d2ef447ff7eb3a77bdcd66952926a5f2ad43c53f Author: Meng Wang <[email protected]> AuthorDate: Sun Aug 9 22:28:38 2026 -0700 test(frontend): render four component templates for coverage (#7501) ### What changes were proposed in this PR? Extends four component specs so their templates actually render, covering markup that the existing tests never executed (they drove the classes directly). No production code was changed. **`MarkdownDescriptionComponent`** (+5) — clicking the Edit action enters edit mode and renders the edit arm; the `[innerHTML]` block renders the parsed markdown; the `#noDescription` fallback renders when there is nothing to show; the view-more button toggles both ways (label and chevron are driven by the same flag); and the control is omitted when the description does not overflow. **`PresetWrapperComponent`** (+5) — the save button renders and saves; the dropdown's `*ngFor` list is populated per preset with the title/description the row interpolations render; the empty-list arm; and the `applyPreset` / `deletePreset` binding targets. **`PropertyEditorComponent`** (+2) — the docked collapse item closes the panel and the collapsed item reopens it, so both arms of the `*ngIf` pair and both `(click)` bindings execute. **`AdminExecutionComponent`** (+3) — a seeded execution renders one row whose cells contain `maxStringLength(...)` and `convertSecondsToTime(...)` output; the "Not Available" arm renders for a negative execution time; and the kill control routes the workflow id. Two behaviours worth noting for future template specs, recorded in comments: - `ngOnInit` refills the list-backed state from the service, so rows must be fed through the service stub rather than assigned afterwards (the fetch would overwrite them). - `nz-dropdown`'s menu only mounts into a CDK overlay on a real user open, which jsdom does not drive. Rather than asserting on markup that cannot render there (which would be brittle), those tests assert the list the `*ngFor` is bound to and the interpolations/handlers each row uses. `nz-table` also renders an internal measure row, which the row helper filters out. Per the issue's determinism constraints: no fake timers are introduced, no date/time string is asserted, and nothing asserts layout or geometry. ### Any related issues, documentation, discussions? Closes #7496 ### How was this PR tested? Extended unit tests, run locally in `frontend/` (all green; failure paths were verified by breaking assertions to confirm the suites go red): ``` ng test --watch=false --include .../markdown-description.component.spec.ts # 24 passed ng test --watch=false --include .../preset-wrapper.component.spec.ts # 29 passed ng test --watch=false --include .../property-editor.component.spec.ts # 22 passed ng test --watch=false --include .../admin-execution.component.spec.ts # 42 passed prettier --write <specs> # clean eslint <specs> # clean ``` ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../preset-wrapper.component.spec.ts | 59 ++++++++++++++ .../execution/admin-execution.component.spec.ts | 91 ++++++++++++++++++++++ .../markdown-description.component.spec.ts | 89 +++++++++++++++++++++ .../property-editor.component.spec.ts | 35 +++++++++ 4 files changed, 274 insertions(+) diff --git a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts index 474c0ca9a0..c074ce1a90 100644 --- a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts +++ b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts @@ -348,4 +348,63 @@ describe("PresetWrapperComponent", () => { expect(messageStub.error).toHaveBeenCalledTimes(1); }); }); + + // ─── template rendering ──────────────────────────────────────────────────── + describe("template rendering", () => { + const initWith = (presets: Preset[]): void => { + // ngOnInit re-populates searchResults from the service, so feed the presets + // through the stub rather than assigning after init (which it would overwrite). + presetServiceStub.getPresets.mockReturnValue(of(presets)); + component.field = buildField(); + component.ngOnInit(); + fixture.detectChanges(); + }; + + it("renders the save button and saves the preset when it is clicked", () => { + initWith([]); + + const saveBtn = fixture.nativeElement.querySelector(".save-button") as HTMLButtonElement; + expect(saveBtn).toBeTruthy(); + + const savePreset = vi.spyOn(component, "savePreset").mockImplementation(() => {}); + saveBtn.click(); + + expect(savePreset).toHaveBeenCalled(); + }); + + it("feeds the dropdown *ngFor with one entry per preset, titled and described", () => { + // The rows live in an nz-dropdown-menu that only mounts into a CDK overlay on a + // real user open, which jsdom does not drive; assert the list the *ngFor is bound + // to and the interpolations it renders for each row instead. + initWith([testPreset, otherPreset]); + + expect(component.searchResults).toEqual([testPreset, otherPreset]); + // the title cell renders the preset's value under the field's own key, and the + // description cell joins the remaining values + expect(component.getEntryTitle(testPreset)).toBe(testPreset[fieldKey]); + expect(component.getEntryDescription(testPreset)).toBe("otherPresetValue"); + }); + + it("binds an empty dropdown list when there are no presets", () => { + initWith([]); + + expect(component.searchResults).toEqual([]); + }); + + it("applies the preset the row's (click) binding targets", () => { + initWith([testPreset]); + + component.applyPreset(testPreset); + + expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(expect.anything(), expect.anything(), testPreset); + }); + + it("deletes the preset the delete button's (click) binding targets", () => { + initWith([testPreset]); + + component.deletePreset(testPreset); + + expect(presetServiceStub.deletePreset).toHaveBeenCalled(); + }); + }); }); diff --git a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts index 734123b441..a46cedef38 100644 --- a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts +++ b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts @@ -537,3 +537,94 @@ describe("AdminExecutionComponent methods (#6550)", () => { }); }); }); + +describe("AdminExecutionComponent template rendering", () => { + let component: AdminExecutionComponent; + let fixture: ComponentFixture<AdminExecutionComponent>; + let service: AdminExecutionService; + + const makeExecution = (overrides: Partial<Execution> = {}): Execution => ({ + workflowName: "a-very-long-workflow-name-to-truncate", + workflowId: 11, + userName: "alice", + userId: 1, + executionId: 21, + executionStatus: "COMPLETED", + executionTime: 3661_000, + executionName: "run-1", + startTime: 1_700_000_000_000, + endTime: 1_700_000_100_000, + access: true, + ...overrides, + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [AdminExecutionService, ...commonTestProviders], + imports: [AdminExecutionComponent, HttpClientTestingModule, NzDropDownModule, NzModalModule], + }).compileComponents(); + + fixture = TestBed.createComponent(AdminExecutionComponent); + component = fixture.componentInstance; + service = TestBed.inject(AdminExecutionService); + // Keep the fetches inert; the rows are seeded directly so ngOnInit's pollers have + // nothing to do (they are covered by the lifecycle tests above). + vi.spyOn(service, "getExecutionList").mockReturnValue(of([])); + vi.spyOn(service, "getTotalWorkflows").mockReturnValue(of(0)); + }); + + afterEach(() => fixture.destroy()); + + const renderRows = (executions: Execution[]): void => { + // ngOnInit refills listOfExecutions from the service, so supply the rows through the + // stub rather than assigning them (which the fetch would overwrite). + vi.mocked(service.getExecutionList).mockReturnValue(of(executions)); + fixture.detectChanges(); + fixture.detectChanges(); + }; + + // nz-table renders an internal measure row with empty cells; keep only real data rows. + const dataRows = () => + fixture.debugElement + .queryAll(By.css("tbody tr")) + .filter(row => !row.nativeElement.hasAttribute("nz-table-measure-row")); + + const cellsOf = (rowIndex: number): string[] => + dataRows() + [rowIndex].queryAll(By.css("td")) + .map(td => (td.nativeElement.textContent ?? "").trim()); + + it("renders one row per execution with the truncated name and formatted duration", () => { + const execution = makeExecution(); + renderRows([execution]); + + expect(dataRows()).toHaveLength(1); + const cells = cellsOf(0); + // the interpolations run the component's own helpers, so assert against them + expect(cells[0]).toContain(component.maxStringLength(execution.workflowName, 16)); + expect(cells[0]).toContain(String(execution.workflowId)); + expect(cells.join(" ")).toContain(component.convertSecondsToTime(execution.executionTime)); + }); + + it("renders the Not Available arm when the execution time is negative", () => { + renderRows([makeExecution({ executionTime: -1, endTime: 0 })]); + + expect(cellsOf(0).join(" ")).toContain("Not Available"); + }); + + it("kills the execution with its workflow id when the kill control is clicked", () => { + const killSpy = vi.spyOn(component, "killExecution").mockImplementation(() => {}); + renderRows([makeExecution({ executionStatus: "RUNNING", workflowId: 42 })]); + + // nz-icon renders [nzType] as an `anticon-<type>` class; the kill button is the one + // carrying the "stop" icon (nz-tooltip is a directive input and never reaches the DOM). + const killButton = fixture.debugElement + .queryAll(By.css("tbody tr button")) + .find(btn => btn.nativeElement.querySelector("i.anticon-stop")); + expect(killButton).toBeTruthy(); + + killButton!.triggerEventHandler("click", null); + + expect(killSpy).toHaveBeenCalledWith(42); + }); +}); diff --git a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts index 7d8b6858c1..bec5010beb 100644 --- a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts @@ -309,4 +309,93 @@ describe("MarkdownDescriptionComponent", () => { expect(parse).not.toHaveBeenCalled(); expect(component.renderedDescription).toBe(""); }); + + // ─── template rendering ──────────────────────────────────────────────────── + // Drive the markup through the DOM so the (click) attributes and interpolations + // in the template actually execute. + describe("template rendering", () => { + it("enters edit mode when the Edit button is clicked", async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + component.editable = true; + fixture.detectChanges(); + + const editBtn = fixture.nativeElement.querySelector(".md-actions button") as HTMLButtonElement; + expect(editBtn).toBeTruthy(); + editBtn.click(); + fixture.detectChanges(); + + expect(component.currentMode).toBe("edit"); + // the edit-mode arm of the template now renders + expect(fixture.nativeElement.querySelector(".md-split")).toBeTruthy(); + }); + + it("renders the parsed markdown through the innerHTML binding", async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + component.description = "hello"; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const rendered = fixture.nativeElement.querySelector(".md-rendered") as HTMLElement; + expect(rendered).toBeTruthy(); + expect(rendered.innerHTML).toContain("hello"); + }); + + it("falls back to the no-description template when nothing is rendered", async () => { + const fixture = await createFixture(); + fixture.componentInstance.description = ""; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector(".md-rendered")).toBeNull(); + expect(fixture.nativeElement.textContent).toContain("No description provided."); + }); + + it("toggles view-more from the template and flips the chevron binding", async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + // the button is behind enableViewMore && hasOverflow + component.enableViewMore = true; + component.hasOverflow = true; + fixture.detectChanges(); + + const viewMoreBtn = fixture.nativeElement.querySelector(".view-more-btn") as HTMLButtonElement; + expect(viewMoreBtn).toBeTruthy(); + // nz-icon renders [nzType] as an `anticon-<type>` class, so the chevron binding is + // observable alongside the label interpolation + const chevronType = (): string | undefined => + Array.from(viewMoreBtn.querySelector("i")?.classList ?? []) + .find(cls => cls.startsWith("anticon-")) + ?.replace("anticon-", ""); + + expect(viewMoreBtn.textContent).toContain("View more"); + expect(chevronType()).toBe("down"); + + viewMoreBtn.click(); + fixture.detectChanges(); + + expect(component.isExpanded).toBe(true); + expect(viewMoreBtn.textContent).toContain("View less"); + expect(chevronType()).toBe("up"); + + viewMoreBtn.click(); + fixture.detectChanges(); + + expect(component.isExpanded).toBe(false); + expect(viewMoreBtn.textContent).toContain("View more"); + expect(chevronType()).toBe("down"); + }); + + it("omits the view-more control when the description does not overflow", async () => { + const fixture = await createFixture(); + fixture.componentInstance.enableViewMore = true; + fixture.componentInstance.hasOverflow = false; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector(".view-more-btn")).toBeNull(); + }); + }); }); diff --git a/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts b/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts index ccc1192033..e0713c37b1 100644 --- a/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts +++ b/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts @@ -388,4 +388,39 @@ describe("PropertyEditorComponent", () => { freshFixture.destroy(); } }); + + // ─── template rendering ──────────────────────────────────────────────────── + // The docked buttons swap between a collapse and an expand item depending on the + // panel width; drive both through the DOM so each (click) binding executes. + describe("template rendering", () => { + const dockedItems = (): HTMLElement[] => + Array.from(fixture.nativeElement.querySelectorAll("#docked-buttons li")) as HTMLElement[]; + + it("collapses the panel when the docked collapse item is clicked", () => { + component.width = 280; + fixture.detectChanges(); + + const items = dockedItems(); + expect(items).toHaveLength(1); // only the collapse arm renders while open + items[0].click(); + fixture.detectChanges(); + + expect(component.width).toBe(0); + }); + + it("reopens the panel when the collapsed docked item is clicked", () => { + // openPanel() schedules a setTimeout that would call detectChanges() after the + // fixture is destroyed in afterEach; stub it the way the specs above do. + vi.spyOn(component as any, "updateHeightBasedOnContent").mockImplementation(() => {}); + component.width = 0; + fixture.detectChanges(); + + const items = dockedItems(); + expect(items).toHaveLength(1); // only the expand arm renders while collapsed + items[0].click(); + fixture.detectChanges(); + + expect(component.width).toBe(280); + }); + }); });
