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-7365-d72b8be6ce2124c403e0b20dcb63a71546e3e20a in repository https://gitbox.apache.org/repos/asf/texera.git
commit ae17c8fafbb213897579a574519004bf0d66ad20 Author: Meng Wang <[email protected]> AuthorDate: Fri Aug 7 04:24:13 2026 -0700 test(frontend): render ResultExportationComponent template branches for coverage (#7365) ### What changes were proposed in this PR? Extends `ResultExportationComponent`'s spec so the **template** actually renders each of its branches. The class was already fully unit-tested, but the existing tests drive the class directly and never render most of the dialog, leaving its `*ngIf` / `*ngFor` / `(click)` / `[(ngModel)]` constructs unexecuted. No production code was changed. 6 tests, each putting the component in a state the template switches on and then calling `detectChanges()`: - the restricted-export error `nz-alert` (every operator blocked); - the partial-skip warning `nz-alert` (some but not all operators blocked); - the export-type `nz-select` and its output-gated `nz-option` arms (`isTableOutput` / `isVisualizationOutput` / `containsBinaryData`); - the filename input when `exportType === "data"`; - the local **Export** button — driven through the DOM (`triggerEventHandler("click")`) and asserting the export service double was called with the `local` destination; - the dataset destination — the search input's `(input)` handler, the `*ngFor` dataset list, and the **Create New Dataset** button (asserts the modal-service double opens the creator). Per the component's determinism notes: no fake timers, no layout/geometry assertions, and no timezone-sensitive date assertions — the tests assert on rendered text, element presence, and the injected service doubles. ### Any related issues, documentation, discussions? Closes #7361 ### How was this PR tested? Extended unit tests, run locally in `frontend/` (all green; the failure path was verified by breaking an assertion to confirm the suite goes red): ``` ng test --watch=false --include src/app/workspace/component/result-exportation/result-exportation.component.spec.ts # Test Files 1 passed (1) | Tests 26 passed (26) prettier --write <spec> # clean eslint <spec> # clean ``` ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../result-exportation.component.spec.ts | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/frontend/src/app/workspace/component/result-exportation/result-exportation.component.spec.ts b/frontend/src/app/workspace/component/result-exportation/result-exportation.component.spec.ts index 53d0aeb13e..47c84273f4 100644 --- a/frontend/src/app/workspace/component/result-exportation/result-exportation.component.spec.ts +++ b/frontend/src/app/workspace/component/result-exportation/result-exportation.component.spec.ts @@ -18,6 +18,7 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { of } from "rxjs"; import { ResultExportationComponent } from "./result-exportation.component"; import { @@ -367,6 +368,112 @@ describe("ResultExportationComponent", () => { expect(component.userAccessibleDatasets).toBe(before); expect(component.inputDatasetName).toBe(nameBefore); }); + + // Renders the template in each of the states it switches on so the *ngIf / *ngFor / + // (click) / [(ngModel)] constructs actually execute. detectChanges() is the coverage switch. + describe("template rendering", () => { + function setAllOperators(ids: string[]): void { + const graph = TestBed.inject(WorkflowActionService) as unknown as { + getTexeraGraph: ReturnType<typeof vi.fn>; + }; + graph.getTexeraGraph.mockReturnValue({ + getAllOperators: () => ids.map(id => ({ operatorID: id })), + }); + } + + function restrict(entries: Record<string, string[]>): void { + const map = new Map<string, Set<string>>(); + Object.entries(entries).forEach(([op, labels]) => map.set(op, new Set(labels))); + component.downloadability = new WorkflowResultDownloadability(map); + } + + it("renders the restricted-export error alert when every operator is blocked", () => { + setAllOperators(["op-a"]); + restrict({ "op-a": ["Sales ([email protected])"] }); + fixture.detectChanges(); + + expect(component.isExportRestricted).toBe(true); + const alert = fixture.debugElement.query(By.css("nz-alert")); + expect(alert).toBeTruthy(); + expect(alert.nativeElement.textContent).toContain("Export unavailable"); + }); + + it("renders the partial-skip warning alert when only some operators are blocked", () => { + setAllOperators(["op-a", "op-b"]); + restrict({ "op-a": ["Sales ([email protected])"] }); + fixture.detectChanges(); + + expect(component.hasPartialNonDownloadable).toBe(true); + expect(fixture.nativeElement.textContent).toContain("Some operators will be skipped"); + }); + + it("renders the export-type select and its output-gated options when export is allowed", () => { + setAllOperators(["op-a"]); + restrict({}); // nothing blocked -> not restricted + component.exportType = "csv"; // != "data" + component.isTableOutput = true; + component.isVisualizationOutput = true; + component.containsBinaryData = false; + fixture.detectChanges(); + + expect(component.isExportRestricted).toBe(false); + expect(fixture.debugElement.query(By.css("#exportTypeInput"))).toBeTruthy(); + }); + + it("renders the filename input when the export type is 'data'", () => { + setAllOperators(["op-a"]); + restrict({}); + component.exportType = "data"; + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css("#filenameInput"))).toBeTruthy(); + }); + + it("renders the local Export button and exports on click", () => { + setAllOperators(["op-a"]); + restrict({}); + component.destination = "local"; + fixture.detectChanges(); + + const exportBtn = fixture.debugElement + .queryAll(By.css("button")) + .find(btn => btn.nativeElement.textContent.trim() === "Export"); + expect(exportBtn).toBeTruthy(); + + exportBtn!.triggerEventHandler("click", null); + expect(exportWorkflowExecutionResult).toHaveBeenCalledTimes(1); + const args = exportWorkflowExecutionResult.mock.calls[0]; + expect(args[7]).toBe("local"); + }); + + it("renders the dataset destination with its list and create button", () => { + setAllOperators(["op-a"]); + restrict({}); + component.destination = "dataset"; + fixture.detectChanges(); + + // the dataset search input drives the (input) handler + const search = fixture.debugElement.query(By.css("input[name='datasetName']")); + expect(search).toBeTruthy(); + search.triggerEventHandler("input", { target: { value: "" } }); + + // The nz-auto-option list the *ngFor drives is bound to + // `filteredUserAccessibleDatasets`; assert the component fed it exactly the + // WRITE dataset (the READ one is filtered out), so the list branch has real data + // behind it. The option content itself only enters the DOM once the autocomplete + // panel expands, which jsdom does not drive, so it is not asserted here. + expect(component.filteredUserAccessibleDatasets.map(d => d.dataset.name)).toEqual(["writable"]); + + // the create-new-dataset button opens the creator modal + const createBtn = fixture.debugElement + .queryAll(By.css("button")) + .find(btn => btn.nativeElement.textContent.includes("Create New Dataset")); + expect(createBtn).toBeTruthy(); + + createBtn!.triggerEventHandler("click", null); + expect(modalCreate).toHaveBeenCalledTimes(1); + }); + }); }); describe("ResultExportationComponent (context-menu source with default modal data)", () => {
