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/07/22 04:00:08 UTC
(texera) 01/01: test(frontend): extend ResultExportationComponent unit test coverage (#6742)
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-6742-fb4a0e2ba17a5fae6989be1b209b3d07d19384e0
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 7f49a83ef590ca0ecfb9a90ff7f9a74c83dfb3c0
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Tue Jul 21 20:59:58 2026 -0700
test(frontend): extend ResultExportationComponent unit test coverage (#6742)
### What changes were proposed in this PR?
Extends `result-exportation.component.spec.ts` covering the export-form
flow (format/destination selection, submit handler, local vs dataset
routing), validation guards, and modal close. Coverage 53% -> high. No
existing tests modified.
### Any related issues, documentation, discussions?
Closes #6735.
### How was this PR tested?
`ng test --include='**/result-exportation.component.spec.ts'` -> 20/20
passing. `yarn format:ci` passes.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---------
Signed-off-by: Xinyuan Lin <xi...@uci.edu>
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
---
.../result-exportation.component.spec.ts | 294 +++++++++++++++++++++
1 file changed, 294 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 866b294e56..53d0aeb13e 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
@@ -113,6 +113,10 @@ describe("ResultExportationComponent", () => {
fixture.detectChanges();
});
+ afterEach(() => {
+ fixture?.destroy();
+ });
+
it("should create and render", () => {
expect(component).toBeTruthy();
});
@@ -165,4 +169,294 @@ describe("ResultExportationComponent", () => {
expect(component.userAccessibleDatasets[0]).toBe(created);
expect(component.inputDatasetName).toBe("brand-new");
});
+
+ describe("operator-id resolution and downloadability getters (menu source)", () => {
+ // Reconfigure the graph mock so the "menu" branch of getOperatorIdsToCheck
+ // returns concrete operator IDs (drives the `.getAllOperators().map(op => op.operatorID)` branch).
+ 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("exportableOperatorIds maps all operators and filters out restricted ones", () => {
+ setAllOperators(["op-a", "op-b", "op-c"]);
+ restrict({ "op-b": ["ds (owner@x.com)"] });
+
+ expect(component.exportableOperatorIds).toEqual(["op-a", "op-c"]);
+ expect(component.blockedOperatorIds).toEqual(["op-b"]);
+ });
+
+ it("getters return [] when downloadability has not been resolved yet", () => {
+ component.downloadability = undefined;
+
+ expect(component.exportableOperatorIds).toEqual([]);
+ expect(component.blockedOperatorIds).toEqual([]);
+ expect(component.blockingDatasetLabels).toEqual([]);
+ });
+
+ it("blockingDatasetLabels and blockingDatasetSummary surface the blocking datasets", () => {
+ setAllOperators(["op-a", "op-b"]);
+ restrict({ "op-a": ["Sales (a@x.com)"], "op-b": ["Sales (a@x.com)", "HR (b@x.com)"] });
+
+ expect(component.blockingDatasetLabels).toEqual(["Sales (a@x.com)", "HR (b@x.com)"]);
+ expect(component.blockingDatasetSummary).toBe("Sales (a@x.com), HR (b@x.com)");
+ });
+
+ it("isExportRestricted is true only when every operator is blocked", () => {
+ setAllOperators(["op-a", "op-b"]);
+ restrict({ "op-a": ["ds1"], "op-b": ["ds2"] });
+
+ expect(component.isExportRestricted).toBe(true);
+ expect(component.hasPartialNonDownloadable).toBe(false);
+ });
+
+ it("hasPartialNonDownloadable is true when some but not all operators are blocked", () => {
+ setAllOperators(["op-a", "op-b"]);
+ restrict({ "op-b": ["ds1"] });
+
+ expect(component.isExportRestricted).toBe(false);
+ expect(component.hasPartialNonDownloadable).toBe(true);
+ });
+ });
+
+ describe("updateOutputType", () => {
+ 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 outputTypes(byOperator: Record<string, unknown>): void {
+ const results = TestBed.inject(WorkflowResultService) as unknown as {
+ determineOutputTypes: ReturnType<typeof vi.fn>;
+ };
+ results.determineOutputTypes.mockImplementation((operatorId: string) => byOperator[operatorId]);
+ }
+
+ it("leaves the output flags untouched when downloadability is not resolved", () => {
+ component.downloadability = undefined;
+ component.isTableOutput = true;
+ component.isVisualizationOutput = true;
+ component.containsBinaryData = true;
+
+ component.updateOutputType();
+
+ // Early return: flags are preserved rather than recomputed/reset.
+ expect(component.isTableOutput).toBe(true);
+ expect(component.isVisualizationOutput).toBe(true);
+ expect(component.containsBinaryData).toBe(true);
+ });
+
+ it("clears every output flag when all selected operators are export-restricted", () => {
+ setAllOperators(["op-a"]);
+ component.downloadability = new WorkflowResultDownloadability(new Map([["op-a", new Set(["ds"])]]));
+ component.isTableOutput = true;
+ component.isVisualizationOutput = true;
+ component.containsBinaryData = true;
+
+ component.updateOutputType();
+
+ expect(component.isTableOutput).toBe(false);
+ expect(component.isVisualizationOutput).toBe(false);
+ expect(component.containsBinaryData).toBe(false);
+ });
+
+ it("aggregates mixed output types across exportable operators", () => {
+ setAllOperators(["skip", "table-only", "viz-binary"]);
+ component.downloadability = new WorkflowResultDownloadability(new Map());
+ outputTypes({
+ // No result at all -> skipped via `continue`.
+ skip: { hasAnyResult: false, isTableOutput: true, isVisualizationOutput: true, containsBinaryData: true },
+ // Pure table output.
+ "table-only": {
+ hasAnyResult: true,
+ isTableOutput: true,
+ isVisualizationOutput: false,
+ containsBinaryData: false,
+ },
+ // Visualization output carrying binary data.
+ "viz-binary": {
+ hasAnyResult: true,
+ isTableOutput: false,
+ isVisualizationOutput: true,
+ containsBinaryData: true,
+ },
+ });
+
+ component.updateOutputType();
+
+ // Not all are tables and not all are visualizations -> both false; one carries binary data.
+ expect(component.isTableOutput).toBe(false);
+ expect(component.isVisualizationOutput).toBe(false);
+ expect(component.containsBinaryData).toBe(true);
+ });
+
+ it("reports table output when every operator with a result is a table", () => {
+ setAllOperators(["t1", "t2"]);
+ component.downloadability = new WorkflowResultDownloadability(new Map());
+ outputTypes({
+ t1: { hasAnyResult: true, isTableOutput: true, isVisualizationOutput: false, containsBinaryData: false },
+ t2: { hasAnyResult: true, isTableOutput: true, isVisualizationOutput: false, containsBinaryData: false },
+ });
+
+ component.updateOutputType();
+
+ expect(component.isTableOutput).toBe(true);
+ expect(component.isVisualizationOutput).toBe(false);
+ expect(component.containsBinaryData).toBe(false);
+ });
+ });
+
+ describe("onUserInputDatasetName", () => {
+ const alpha = {
+ dataset: { did: 1, name: "Alpha" },
+ accessPrivilege: "WRITE",
+ } as unknown as DashboardDataset;
+ const beta = {
+ dataset: { did: 2, name: "Beta" },
+ accessPrivilege: "WRITE",
+ } as unknown as DashboardDataset;
+ const noId = {
+ dataset: { did: undefined, name: "AlphaLike" },
+ accessPrivilege: "WRITE",
+ } as unknown as DashboardDataset;
+
+ it("filters datasets by case-insensitive name match and requires a dataset id", () => {
+ component.userAccessibleDatasets = [alpha, beta, noId];
+ component.inputDatasetName = "alph";
+
+ component.onUserInputDatasetName(new Event("input"));
+
+ // noId matches the name but has no did, so it is excluded; Beta does not match.
+ expect(component.filteredUserAccessibleDatasets).toEqual([alpha]);
+ });
+
+ it("resets to the full list when the input is cleared", () => {
+ component.userAccessibleDatasets = [alpha, beta];
+ component.inputDatasetName = "";
+
+ component.onUserInputDatasetName(new Event("input"));
+
+ expect(component.filteredUserAccessibleDatasets).toEqual([alpha, beta]);
+ // A fresh copy, not the same array reference.
+ expect(component.filteredUserAccessibleDatasets).not.toBe(component.userAccessibleDatasets);
+ });
+ });
+
+ it("onClickCreateNewDataset leaves state unchanged when the creator modal is dismissed", () => {
+ // beforeEach wires modalCreate to emit null on afterClose.
+ const before = component.userAccessibleDatasets;
+ const nameBefore = component.inputDatasetName;
+
+ component.onClickCreateNewDataset();
+
+ expect(modalCreate).toHaveBeenCalledTimes(1);
+ expect(component.userAccessibleDatasets).toBe(before);
+ expect(component.inputDatasetName).toBe(nameBefore);
+ });
+});
+
+describe("ResultExportationComponent (context-menu source with default modal data)", () => {
+ let component: ResultExportationComponent;
+ let fixture: ComponentFixture<ResultExportationComponent>;
+
+ // Modal data intentionally omits defaultFileName / rowIndex / columnIndex / exportType
+ // so the component's `?? default` initializers are exercised, and uses a non-"menu"
+ // trigger so getOperatorIdsToCheck reads the highlighted-operator branch.
+ const CONTEXT_MENU_DATA = {
+ sourceTriggered: "context-menu",
+ workflowName: "ctx-workflow",
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ResultExportationComponent],
+ providers: [
+ { provide: NZ_MODAL_DATA, useValue: CONTEXT_MENU_DATA },
+ { provide: NzModalRef, useValue: { close: vi.fn(), getConfig: () => ({}) } },
+ { provide: NzModalService, useValue: { create: vi.fn().mockReturnValue({ afterClose: of(null) }) } },
+ {
+ provide: WorkflowResultExportService,
+ useValue: {
+ computeRestrictionAnalysis: vi.fn().mockReturnValue(of(new WorkflowResultDownloadability(new Map()))),
+ exportWorkflowExecutionResult: vi.fn(),
+ },
+ },
+ {
+ provide: DatasetService,
+ useValue: { retrieveAccessibleDatasets: vi.fn().mockReturnValue(of([])) },
+ },
+ {
+ provide: WorkflowActionService,
+ useValue: {
+ getTexeraGraph: vi.fn().mockReturnValue({ getAllOperators: vi.fn().mockReturnValue([]) }),
+ getJointGraphWrapper: vi.fn().mockReturnValue({
+ getCurrentHighlightedOperatorIDs: vi.fn().mockReturnValue(["hl-1", "hl-2"]),
+ }),
+ },
+ },
+ {
+ provide: WorkflowResultService,
+ useValue: {
+ determineOutputTypes: vi.fn().mockReturnValue({
+ hasAnyResult: false,
+ isTableOutput: false,
+ isVisualizationOutput: false,
+ containsBinaryData: false,
+ }),
+ },
+ },
+ {
+ provide: ComputingUnitStatusService,
+ useValue: { getSelectedComputingUnit: vi.fn().mockReturnValue(of(null)) },
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ResultExportationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ fixture?.destroy();
+ });
+
+ it("applies fallback defaults for absent modal-data fields", () => {
+ expect(component.inputFileName).toBe("");
+ expect(component.rowIndex).toBe(-1);
+ expect(component.columnIndex).toBe(-1);
+ expect(component.exportType).toBe("");
+ });
+
+ it("resolves operator ids from the highlighted-operator selection", () => {
+ // Empty restriction map => every highlighted operator is exportable.
+ expect(component.exportableOperatorIds).toEqual(["hl-1", "hl-2"]);
+ expect(component.blockedOperatorIds).toEqual([]);
+ });
+
+ it("exports highlighted operators only (exportAll === false) for a context-menu trigger", () => {
+ const exportService = TestBed.inject(WorkflowResultExportService)
+ .exportWorkflowExecutionResult as unknown as ReturnType<typeof vi.fn>;
+
+ component.onClickExportResult("local");
+
+ expect(exportService).toHaveBeenCalledTimes(1);
+ const args = exportService.mock.calls[0];
+ expect(args[6]).toBe(false); // exportAll is false because sourceTriggered !== "menu"
+ expect(args[7]).toBe("local");
+ });
});