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 <[email protected]>
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 <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../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 ([email protected])"] });
+
+      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 ([email protected])"], "op-b": ["Sales 
([email protected])", "HR ([email protected])"] });
+
+      expect(component.blockingDatasetLabels).toEqual(["Sales ([email protected])", 
"HR ([email protected])"]);
+      expect(component.blockingDatasetSummary).toBe("Sales ([email protected]), HR 
([email protected])");
+    });
+
+    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");
+  });
 });

Reply via email to