This is an automated email from the ASF dual-hosted git repository.

aglinxinyuan 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 dcb457e053 test(frontend): expand spec coverage for download / 
code-editor / annotation-suggestion (#5004)
dcb457e053 is described below

commit dcb457e0531272187c97a555370ea9834734c930
Author: Xinyuan Lin <[email protected]>
AuthorDate: Mon May 11 20:54:12 2026 -0700

    test(frontend): expand spec coverage for download / code-editor / 
annotation-suggestion (#5004)
    
    ### What changes were proposed in this PR?
    
    Three areas were under-tested in the frontend spec suite; this PR
    addresses all three as one cohesive change:
    
    - **`DownloadService`** — went from 8 active specs (all `it.skip(...)`)
    to 14 active specs + 1 still-skipped (`createZip`, blocked by an
    unrelated `import * as JSZip` interop bug). New coverage:
    `downloadSingleFile` (incl. default-filename fallback +
    `isLogin=false`), `downloadDataset`, `downloadDatasetVersion`,
    `downloadWorkflow` (JSON shape), `downloadWorkflowsAsZip`, single-file
    `downloadOperatorsResult`, the `No files to download` error path,
    `getWorkflowResultDownloadability` HTTP wiring, plus matching
    error-notification paths.
    - **`CodeEditorComponent`** — went from 1 spec (`should create`) to 9
    specs: operator-type → language detection for the three V2 Python
    operator types and for plain Java / unknown types; the `languageTitle`
    formula; `getCoeditorCursorStyles` for hex and rgba colours.
    - **`AnnotationSuggestionComponent`** — new spec file (6 specs):
    creation, default inputs, accept / decline event emission, independence
    of the two emitters, `@Input` binding.
    
    #### Why one test stays skipped
    
    The multi-file `downloadOperatorsResult` path goes through `new
    JSZip()`, where the production code does `import * as JSZip from
    "jszip"`. The CI build flags this as `Constructing "JSZip" will crash at
    run-time because it's an import namespace object`, and vitest reproduces
    it as `__vite_ssr_import_* is not a constructor`. The spec is left as
    `it.skip(...)` with a comment so it gets re-enabled when the source
    switches to a default import.
    
    ### Any related issues, documentation, discussions?
    
    Closes #5003.
    
    ### How was this PR tested?
    
    Ran the suite on both branches under active development — confirms the
    new specs are stable AND the in-flight v10 refactor on #4997 preserves
    the externally-observable behaviour these specs exercise.
    
    | Branch | Files | Tests |
    |---|---|---|
    | `main` | 66 passed / 2 skipped | 333 passed / 4 skipped / 2 todo |
    | `chore/monaco-lsp-v10` (#4997) | 65 passed / 2 skipped | 297 passed /
    4 skipped / 2 todo |
    
    ```
    yarn test   # on main                     → 333 passed / 4 skipped / 2 todo
    yarn test   # on chore/monaco-lsp-v10     → 297 passed / 4 skipped / 2 todo
    ```
    
    The test-count delta between branches is unrelated PR drift (`main` has
    picked up extra non-monaco specs that haven't been merged into the v10
    branch yet); the spec files this PR adds run green on both.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.7)
---
 .../service/user/download/download.service.spec.ts | 365 +++++++++++----------
 .../annotation-suggestion.component.spec.ts        |  87 +++++
 .../code-editor.component.spec.ts                  | 158 ++++++++-
 3 files changed, 422 insertions(+), 188 deletions(-)

diff --git 
a/frontend/src/app/dashboard/service/user/download/download.service.spec.ts 
b/frontend/src/app/dashboard/service/user/download/download.service.spec.ts
index 4a9e17729a..d0588665c1 100644
--- a/frontend/src/app/dashboard/service/user/download/download.service.spec.ts
+++ b/frontend/src/app/dashboard/service/user/download/download.service.spec.ts
@@ -16,34 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-// TODO: rewrite skipped tests away from Jasmine done/fail callbacks (#4861).
-// These stubs make the it.skip bodies type-check without running.
-declare function done(): void;
-declare function fail(message?: string): never;
-
-// TODO(vitest): done callbacks need rewrite to async/Promise pattern; these 
specs are skipped pending follow-up — tracked in #4861.
 
 import { TestBed } from "@angular/core/testing";
-import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
 import { DownloadService } from "./download.service";
 import { DatasetService } from "../dataset/dataset.service";
 import { FileSaverService } from "../file/file-saver.service";
 import { NotificationService } from 
"../../../../common/service/notification/notification.service";
 import { WorkflowPersistService } from 
"../../../../common/service/workflow-persist/workflow-persist.service";
-import { of, throwError } from "rxjs";
+import { firstValueFrom, lastValueFrom, of, throwError } from "rxjs";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
 import type { Mocked } from "vitest";
+
 describe("DownloadService", () => {
   let downloadService: DownloadService;
   let datasetServiceSpy: Mocked<DatasetService>;
   let fileSaverServiceSpy: Mocked<FileSaverService>;
   let notificationServiceSpy: Mocked<NotificationService>;
+  let workflowPersistServiceSpy: Mocked<WorkflowPersistService>;
+  let httpMock: HttpTestingController;
 
   beforeEach(() => {
     const datasetSpy = { retrieveDatasetVersionSingleFile: vi.fn(), 
retrieveDatasetVersionZip: vi.fn() };
     const fileSaverSpy = { saveAs: vi.fn() };
     const notificationSpy = { info: vi.fn(), success: vi.fn(), error: vi.fn() 
};
-    const workflowPersistSpy = { getWorkflow: vi.fn() };
+    const workflowPersistSpy = { retrieveWorkflow: vi.fn() };
 
     TestBed.configureTestingModule({
       imports: [HttpClientTestingModule],
@@ -61,198 +58,224 @@ describe("DownloadService", () => {
     datasetServiceSpy = TestBed.inject(DatasetService) as unknown as 
Mocked<DatasetService>;
     fileSaverServiceSpy = TestBed.inject(FileSaverService) as unknown as 
Mocked<FileSaverService>;
     notificationServiceSpy = TestBed.inject(NotificationService) as unknown as 
Mocked<NotificationService>;
+    workflowPersistServiceSpy = TestBed.inject(WorkflowPersistService) as 
unknown as Mocked<WorkflowPersistService>;
+    httpMock = TestBed.inject(HttpTestingController);
+  });
+
+  afterEach(() => {
+    // Catch any test that fires an HTTP request without flushing it; keeps
+    // the suite safe as more specs start using HttpTestingController.
+    httpMock.verify();
   });
 
-  it.skip("should download a single file successfully", () => {
-    const filePath = "test/file.txt";
+  // ─── downloadSingleFile ───────────────────────────────────────────────────
+
+  it("downloads a single file and saves it under the basename of the path", 
async () => {
     const mockBlob = new Blob(["test content"], { type: "text/plain" });
+    
datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));
 
+    const result = await 
firstValueFrom(downloadService.downloadSingleFile("test/file.txt", true));
+
+    expect(result).toBe(mockBlob);
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
+    
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith("test/file.txt",
 true);
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"file.txt");
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith("File 
test/file.txt has been downloaded");
+  });
+
+  it("falls back to a default filename when the path has no basename segment", 
async () => {
+    const mockBlob = new Blob(["x"], { type: "text/plain" });
     
datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));
 
-    downloadService.downloadSingleFile(filePath, true).subscribe({
-      next: blob => {
-        expect(blob).toBe(mockBlob);
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
-        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath,
 true);
-        expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"file.txt");
-        expect(notificationServiceSpy.success).toHaveBeenCalledWith("File 
test/file.txt has been downloaded");
-        done();
-      },
-      error: (error: unknown) => {
-        fail("Should not have thrown an error: " + error);
-      },
-    });
+    await firstValueFrom(downloadService.downloadSingleFile("", true));
+
+    // path.split("/").pop() returns "" for "", which falls through to the 
default name
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"download");
   });
 
-  it.skip("should handle download failure correctly", () => {
-    const filePath = "test/file.txt";
-    const errorMessage = "Download failed";
-
-    
datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(throwError(()
 => new Error(errorMessage)));
-
-    downloadService.downloadSingleFile(filePath, true).subscribe({
-      next: () => {
-        fail("Should have thrown an error");
-      },
-      error: (error: unknown) => {
-        expect(error).toBeTruthy();
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
-        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath,
 true);
-        expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
-        expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading file 'test/file.txt'");
-        done();
-      },
-    });
+  it("propagates errors from downloadSingleFile and emits the error 
notification", async () => {
+    
datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(throwError(()
 => new Error("boom")));
+
+    await 
expect(firstValueFrom(downloadService.downloadSingleFile("test/file.txt", 
true))).rejects.toThrow("boom");
+
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
+    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
+    expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading file 'test/file.txt'");
   });
 
-  it.skip("should download a dataset successfully", () => {
-    const datasetId = 1;
-    const datasetName = "TestDataset";
-    const mockBlob = new Blob(["dataset content"], { type: "application/zip" 
});
+  it("passes isLogin=false through to retrieveDatasetVersionSingleFile", async 
() => {
+    const mockBlob = new Blob(["x"], { type: "text/plain" });
+    
datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));
+
+    await 
firstValueFrom(downloadService.downloadSingleFile("public/sample.csv", false));
+
+    
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith("public/sample.csv",
 false);
+  });
 
+  // ─── downloadDataset ──────────────────────────────────────────────────────
+
+  it("downloads the latest dataset version as a zip named after the dataset", 
async () => {
+    const mockBlob = new Blob(["dataset content"], { type: "application/zip" 
});
     datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob));
 
-    downloadService.downloadDataset(datasetId, datasetName).subscribe({
-      next: blob => {
-        expect(blob).toBe(mockBlob);
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith(
-          "Starting to download the latest version of the dataset as ZIP"
-        );
-        
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(datasetId);
-        expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"TestDataset.zip");
-        expect(notificationServiceSpy.success).toHaveBeenCalledWith(
-          "The latest version of the dataset has been downloaded as ZIP"
-        );
-        done();
-      },
-      error: (error: unknown) => {
-        fail("Should not have thrown an error");
-      },
-    });
+    const result = await firstValueFrom(downloadService.downloadDataset(1, 
"TestDataset"));
+
+    expect(result).toBe(mockBlob);
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith(
+      "Starting to download the latest version of the dataset as ZIP"
+    );
+    
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1);
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"TestDataset.zip");
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith(
+      "The latest version of the dataset has been downloaded as ZIP"
+    );
   });
 
-  it.skip("should handle dataset download failure correctly", () => {
-    const datasetId = 1;
-    const datasetName = "TestDataset";
-    const errorMessage = "Dataset download failed";
-
-    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() 
=> new Error(errorMessage)));
-
-    downloadService.downloadDataset(datasetId, datasetName).subscribe({
-      next: () => {
-        fail("Should have thrown an error");
-      },
-      error: (error: unknown) => {
-        expect(error).toBeTruthy();
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith(
-          "Starting to download the latest version of the dataset as ZIP"
-        );
-        
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(datasetId);
-        expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
-        expect(notificationServiceSpy.error).toHaveBeenCalledWith(
-          "Error downloading the latest version of the dataset as ZIP"
-        );
-        done();
-      },
-    });
+  it("emits the dataset error notification and rethrows on retrieve failure", 
async () => {
+    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() 
=> new Error("fail")));
+
+    await expect(firstValueFrom(downloadService.downloadDataset(1, 
"TestDataset"))).rejects.toThrow("fail");
+
+    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
+    expect(notificationServiceSpy.error).toHaveBeenCalledWith(
+      "Error downloading the latest version of the dataset as ZIP"
+    );
   });
 
-  it.skip("should download a dataset version successfully", () => {
-    const datasetId = 1;
-    const datasetVersionId = 1;
-    const datasetName = "TestDataset";
-    const versionName = "v1.0";
-    const mockBlob = new Blob(["version content"], { type: "application/zip" 
});
+  // ─── downloadDatasetVersion ───────────────────────────────────────────────
 
+  it("downloads a specific dataset version with composite zip name", async () 
=> {
+    const mockBlob = new Blob(["v1"], { type: "application/zip" });
     datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob));
 
-    downloadService.downloadDatasetVersion(datasetId, datasetVersionId, 
datasetName, versionName).subscribe({
-      next: blob => {
-        expect(blob).toBe(mockBlob);
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download version v1.0 as ZIP");
-        
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(datasetId,
 datasetVersionId);
-        expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"TestDataset-v1.0.zip");
-        expect(notificationServiceSpy.success).toHaveBeenCalledWith("Version 
v1.0 has been downloaded as ZIP");
-        done();
-      },
-      error: (error: unknown) => {
-        fail("Should not have thrown an error");
-      },
-    });
+    const result = await 
firstValueFrom(downloadService.downloadDatasetVersion(1, 2, "TestDataset", 
"v1.0"));
+
+    expect(result).toBe(mockBlob);
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download version v1.0 as ZIP");
+    
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1, 2);
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"TestDataset-v1.0.zip");
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith("Version v1.0 
has been downloaded as ZIP");
   });
 
-  it.skip("should handle dataset version download failure correctly", () => {
-    const datasetId = 1;
-    const datasetVersionId = 1;
-    const datasetName = "TestDataset";
-    const versionName = "v1.0";
-    const errorMessage = "Dataset version download failed";
-
-    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() 
=> new Error(errorMessage)));
-
-    downloadService.downloadDatasetVersion(datasetId, datasetVersionId, 
datasetName, versionName).subscribe({
-      next: () => {
-        fail("Should have thrown an error");
-      },
-      error: (error: unknown) => {
-        expect(error).toBeTruthy();
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download version v1.0 as ZIP");
-        
expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(datasetId,
 datasetVersionId);
-        expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
-        expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading version 'v1.0' as ZIP");
-        done();
-      },
-    });
+  it("emits the version-specific error notification on retrieve failure", 
async () => {
+    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() 
=> new Error("nope")));
+
+    await expect(firstValueFrom(downloadService.downloadDatasetVersion(1, 2, 
"TestDataset", "v1.0"))).rejects.toThrow(
+      "nope"
+    );
+
+    expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading version 'v1.0' as ZIP");
   });
 
-  it.skip("should download workflows as ZIP successfully", () => {
-    const workflowEntries = [
-      { id: 1, name: "Workflow1" },
-      { id: 2, name: "Workflow2" },
-    ];
-    const mockBlob = new Blob(["zip content"], { type: "application/zip" });
+  // ─── downloadWorkflow ─────────────────────────────────────────────────────
 
-    vi.spyOn(downloadService as any, 
"createWorkflowsZip").mockReturnValue(of(mockBlob));
+  it("downloads a workflow as a JSON blob named after the workflow", async () 
=> {
+    const workflowContent = { hello: "world", operators: [] };
+    workflowPersistServiceSpy.retrieveWorkflow.mockReturnValue(of({ content: 
workflowContent } as any));
 
-    downloadService.downloadWorkflowsAsZip(workflowEntries).subscribe({
-      next: blob => {
-        expect(blob).toBe(mockBlob);
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download workflows as ZIP");
-        expect((downloadService as 
any).createWorkflowsZip).toHaveBeenCalledWith(workflowEntries);
-        expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(
-          mockBlob,
-          expect.stringMatching(/^workflowExports-.*\.zip$/)
-        );
-        expect(notificationServiceSpy.success).toHaveBeenCalledWith("Workflows 
have been downloaded as ZIP");
-        done();
-      },
-      error: (error: unknown) => {
-        fail("Should not have thrown an error");
-      },
-    });
+    const result = await firstValueFrom(downloadService.downloadWorkflow(42, 
"MyWorkflow"));
+
+    expect(result.fileName).toBe("MyWorkflow.json");
+    expect(result.blob).toBeInstanceOf(Blob);
+    expect(result.blob.type).toBe("text/plain;charset=utf-8");
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(result.blob, 
"MyWorkflow.json");
+    // Blob.text() isn't shipped by jsdom, so we don't pin the body content
+    // here; the saveAs assertion above already verifies the path that
+    // produced it.
   });
 
-  it.skip("should handle workflows ZIP download failure correctly", () => {
-    const workflowEntries = [
+  // ─── downloadWorkflowsAsZip ───────────────────────────────────────────────
+
+  it("downloads the workflow ZIP and routes through createWorkflowsZip", async 
() => {
+    const mockBlob = new Blob(["zip"], { type: "application/zip" });
+    const entries = [
       { id: 1, name: "Workflow1" },
       { id: 2, name: "Workflow2" },
     ];
-    const errorMessage = "Workflows ZIP download failed";
-
-    vi.spyOn(downloadService as any, 
"createWorkflowsZip").mockReturnValue(throwError(() => new 
Error(errorMessage)));
-
-    downloadService.downloadWorkflowsAsZip(workflowEntries).subscribe({
-      next: () => {
-        fail("Should have thrown an error");
-      },
-      error: (error: unknown) => {
-        expect(error).toBeTruthy();
-        expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download workflows as ZIP");
-        expect((downloadService as 
any).createWorkflowsZip).toHaveBeenCalledWith(workflowEntries);
-        expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
-        expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading workflows as ZIP");
-        done();
-      },
-    });
+    vi.spyOn(downloadService as any, 
"createWorkflowsZip").mockReturnValue(of(mockBlob));
+
+    const result = await 
firstValueFrom(downloadService.downloadWorkflowsAsZip(entries));
+
+    expect(result).toBe(mockBlob);
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download workflows as ZIP");
+    expect((downloadService as 
any).createWorkflowsZip).toHaveBeenCalledWith(entries);
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(
+      mockBlob,
+      expect.stringMatching(/^workflowExports-.*\.zip$/)
+    );
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith("Workflows 
have been downloaded as ZIP");
+  });
+
+  it("propagates errors from createWorkflowsZip with the expected error 
notification", async () => {
+    vi.spyOn(downloadService as any, 
"createWorkflowsZip").mockReturnValue(throwError(() => new Error("zip fail")));
+
+    await expect(firstValueFrom(downloadService.downloadWorkflowsAsZip([{ id: 
1, name: "W" }]))).rejects.toThrow(
+      "zip fail"
+    );
+
+    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
+    expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading workflows as ZIP");
+  });
+
+  // ─── downloadOperatorsResult ──────────────────────────────────────────────
+
+  it("downloads a single operator file directly when there's exactly one 
file", async () => {
+    const fileBlob = new Blob(["hello"], { type: "text/plain" });
+    const result = await firstValueFrom(
+      downloadService.downloadOperatorsResult([of([{ filename: "out.csv", 
blob: fileBlob }])], {
+        wid: 1,
+        name: "W",
+      } as any)
+    );
+
+    expect(result).toBe(fileBlob);
+    expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download operator result");
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(fileBlob, 
"out.csv");
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator 
result has been downloaded");
+  });
+
+  // The multi-file zip path goes through `new JSZip()` against the
+  // `import * as JSZip from "jszip"` namespace, which the build flags as
+  // `Constructing "JSZip" will crash at run-time because it's an import
+  // namespace object`. Vitest reproduces the failure (`__vite_ssr_import_*
+  // is not a constructor`). Tracked as a separate cleanup in the codebase;
+  // the test is here as a placeholder so we re-enable it once the import
+  // is normalised to a default import.
+  it.skip("zips multiple operator files into a workflow-named archive", async 
() => {
+    const a = new Blob(["a"], { type: "text/plain" });
+    const b = new Blob(["b"], { type: "text/plain" });
+    const result = await firstValueFrom(
+      downloadService.downloadOperatorsResult(
+        [
+          of([
+            { filename: "a.csv", blob: a },
+            { filename: "b.csv", blob: b },
+          ]),
+        ],
+        { wid: 7, name: "TwoFile" } as any
+      )
+    );
+
+    expect(result).toBeInstanceOf(Blob);
+    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(expect.any(Blob), 
"results_7_TwoFile.zip");
+    expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator 
results have been downloaded as ZIP");
+  });
+
+  it("errors out cleanly when no operator result files are provided", async () 
=> {
+    await expect(
+      firstValueFrom(downloadService.downloadOperatorsResult([of([])], { wid: 
1, name: "Empty" } as any))
+    ).rejects.toThrow("No files to download");
+  });
+
+  // ─── getWorkflowResultDownloadability ─────────────────────────────────────
+
+  it("hits the downloadability endpoint and returns the operator → labels 
map", async () => {
+    const promise = 
lastValueFrom(downloadService.getWorkflowResultDownloadability(99));
+    const req = httpMock.expectOne(r => 
r.url.includes("/99/result/downloadability"));
+    expect(req.request.method).toBe("GET");
+    req.flush({ "op-1": ["my-dataset"], "op-2": [] });
+
+    const map = await promise;
+    expect(map).toEqual({ "op-1": ["my-dataset"], "op-2": [] });
   });
 });
diff --git 
a/frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.spec.ts
 
b/frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.spec.ts
new file mode 100644
index 0000000000..cce6b27d4f
--- /dev/null
+++ 
b/frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.spec.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { AnnotationSuggestionComponent } from 
"./annotation-suggestion.component";
+
+describe("AnnotationSuggestionComponent", () => {
+  let component: AnnotationSuggestionComponent;
+  let fixture: ComponentFixture<AnnotationSuggestionComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [AnnotationSuggestionComponent],
+    }).compileComponents();
+    fixture = TestBed.createComponent(AnnotationSuggestionComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it("creates", () => {
+    expect(component).toBeTruthy();
+  });
+
+  it("defaults its inputs to empty / zero", () => {
+    expect(component.code).toBe("");
+    expect(component.suggestion).toBe("");
+    expect(component.top).toBe(0);
+    expect(component.left).toBe(0);
+  });
+
+  it("emits accept when onAccept is called", () => {
+    const spy = vi.fn();
+    component.accept.subscribe(spy);
+    component.onAccept();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
+  it("emits decline when onDecline is called", () => {
+    const spy = vi.fn();
+    component.decline.subscribe(spy);
+    component.onDecline();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
+  it("emits accept and decline independently — onAccept does not fire decline, 
and vice versa", () => {
+    const acceptSpy = vi.fn();
+    const declineSpy = vi.fn();
+    component.accept.subscribe(acceptSpy);
+    component.decline.subscribe(declineSpy);
+
+    component.onAccept();
+    expect(acceptSpy).toHaveBeenCalledTimes(1);
+    expect(declineSpy).not.toHaveBeenCalled();
+
+    component.onDecline();
+    expect(acceptSpy).toHaveBeenCalledTimes(1);
+    expect(declineSpy).toHaveBeenCalledTimes(1);
+  });
+
+  it("respects the latest values bound to its @Input fields", () => {
+    component.code = "x = 1";
+    component.suggestion = ": int";
+    component.top = 50;
+    component.left = 75;
+    fixture.detectChanges();
+    expect(component.code).toBe("x = 1");
+    expect(component.suggestion).toBe(": int");
+    expect(component.top).toBe(50);
+    expect(component.left).toBe(75);
+  });
+});
diff --git 
a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts
 
b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts
index bb8dfcce98..b477ff5979 100644
--- 
a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts
@@ -24,40 +24,164 @@ import { WorkflowActionService } from 
"../../service/workflow-graph/model/workfl
 import { mockJavaUDFPredicate, mockPoint } from 
"../../service/workflow-graph/model/mock-workflow-data";
 import { OperatorMetadataService } from 
"../../service/operator-metadata/operator-metadata.service";
 import { StubOperatorMetadataService } from 
"../../service/operator-metadata/stub-operator-metadata.service";
+import { mockOperatorMetaData } from 
"../../service/operator-metadata/mock-operator-metadata.data";
 import { commonTestProviders } from "../../../common/testing/test-utils";
+import { OperatorPredicate } from "../../types/workflow-common.interface";
+import { OperatorSchema } from "../../types/operator-schema.interface";
+import { of } from "rxjs";
 
-describe("CodeEditorDialogComponent", () => {
-  let component: CodeEditorComponent;
-  let fixture: ComponentFixture<CodeEditorComponent>;
+// Operator types that the constructor's language-detection branch must map
+// to a specific language. `RUDFSource` / `RUDF` -> `r`; the three V2 Python
+// types -> `python`; everything else -> `java`. Local to this spec so we
+// don't perturb the shared mock-workflow-data fixtures.
+const R_OPERATOR_TYPES = ["RUDFSource", "RUDF"];
+const PYTHON_OPERATOR_TYPES = ["PythonUDFV2", "PythonUDFSourceV2", 
"DualInputPortsPythonUDFV2"];
+
+// Augment `mockOperatorMetaData` with synthetic schemas for the V2 operator
+// types and one unknown type so `addOperator` and `JointUIService` accept
+// them. Cloning the existing `PythonUDF` schema and renaming the
+// `operatorType` is the cheapest way to satisfy both `operatorTypeExists`
+// and the schema-driven joint element creation.
+const baseSchema = mockOperatorMetaData.operators.find(op => op.operatorType 
=== "PythonUDF");
+if (!baseSchema) {
+  throw new Error(
+    "CodeEditorComponent spec setup expected a PythonUDF schema in 
mockOperatorMetaData — fixture has drifted."
+  );
+}
+const synthesizeSchema = (operatorType: string): OperatorSchema => ({ 
...baseSchema, operatorType });
+const augmentedSchemas: OperatorSchema[] = [
+  ...mockOperatorMetaData.operators,
+  ...PYTHON_OPERATOR_TYPES.map(synthesizeSchema),
+  ...R_OPERATOR_TYPES.map(synthesizeSchema),
+  synthesizeSchema("SomeUnknownType"),
+];
+class AugmentedStubMetadataService extends StubOperatorMetadataService {
+  // JointUIService snapshots `operatorSchemas` from this stream once on
+  // construction, so we have to feed it the augmented list (overriding only
+  // `getOperatorSchema`/`operatorTypeExists` is not enough).
+  private readonly augmentedMetadata = of({
+    ...mockOperatorMetaData,
+    operators: augmentedSchemas,
+  });
+  override getOperatorMetadata(): typeof this.augmentedMetadata {
+    return this.augmentedMetadata;
+  }
+  override getOperatorSchema(operatorType: string): OperatorSchema {
+    const schema = augmentedSchemas.find(op => op.operatorType === 
operatorType);
+    if (!schema) throw new Error(`unknown operatorType ${operatorType}`);
+    return schema;
+  }
+  override operatorTypeExists(operatorType: string): boolean {
+    return augmentedSchemas.some(op => op.operatorType === operatorType);
+  }
+}
+
+const buildPredicate = (operatorID: string, operatorType: string): 
OperatorPredicate => ({
+  operatorID,
+  operatorType,
+  operatorVersion: "p1",
+  operatorProperties: {},
+  inputPorts: [{ portID: "input-0" }],
+  outputPorts: [{ portID: "output-0" }],
+  showAdvanced: false,
+  isDisabled: false,
+});
+
+describe("CodeEditorComponent", () => {
   let workflowActionService: WorkflowActionService;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       providers: [
         WorkflowActionService,
-        {
-          provide: OperatorMetadataService,
-          useClass: StubOperatorMetadataService,
-        },
+        { provide: OperatorMetadataService, useClass: 
AugmentedStubMetadataService },
         ...commonTestProviders,
       ],
       imports: [CodeEditorComponent, HttpClientTestingModule],
     }).compileComponents();
 
     workflowActionService = TestBed.inject(WorkflowActionService);
-    workflowActionService.addOperator(mockJavaUDFPredicate, mockPoint);
-    
workflowActionService.getJointGraphWrapper().highlightOperators(mockJavaUDFPredicate.operatorID);
-    fixture = TestBed.createComponent(CodeEditorComponent);
-    component = fixture.componentInstance;
+  });
+
+  function makeFixture(predicate: OperatorPredicate): 
ComponentFixture<CodeEditorComponent> {
+    workflowActionService.addOperator(predicate, mockPoint);
+    
workflowActionService.getJointGraphWrapper().highlightOperators(predicate.operatorID);
+    const fixture = TestBed.createComponent(CodeEditorComponent);
     fixture.detectChanges();
+    return fixture;
+  }
+
+  it("creates with the highlighted operator", () => {
+    const fixture = makeFixture(mockJavaUDFPredicate);
+    expect(fixture.componentInstance).toBeTruthy();
+    
expect(fixture.componentInstance.currentOperatorId).toBe(mockJavaUDFPredicate.operatorID);
+  });
+
+  // Language detection — the constructor maps `RUDFSource` / `RUDF` to `r`,
+  // the three V2-era Python operator types to `python`, and anything else
+  // to `java`. The exact branch lives in the constructor; the public
+  // `language` field is what the rest of the editor (LSP wiring, file-
+  // suffix selection) keys off.
+
+  R_OPERATOR_TYPES.forEach((operatorType, index) => {
+    it(`picks language="r" for operatorType=${operatorType}`, () => {
+      const fixture = makeFixture(buildPredicate(`r-${index}`, operatorType));
+      expect(fixture.componentInstance.language).toBe("r");
+      expect(fixture.componentInstance.languageTitle).toBe("R UDF");
+    });
   });
 
-  it("should create", () => {
-    expect(component).toBeTruthy();
+  PYTHON_OPERATOR_TYPES.forEach((operatorType, index) => {
+    it(`picks language="python" for operatorType=${operatorType}`, () => {
+      const fixture = makeFixture(buildPredicate(`p-${index}`, operatorType));
+      expect(fixture.componentInstance.language).toBe("python");
+      expect(fixture.componentInstance.languageTitle).toBe("Python UDF");
+    });
   });
 
-  // it("should create a websocket when the editor is opened", () => {
-  //   let socketInstance = component.getLanguageServerSocket();
-  //   expect(socketInstance).toBeTruthy();
-  // });
+  it('picks language="java" for plain JavaUDF', () => {
+    const fixture = makeFixture(mockJavaUDFPredicate);
+    expect(fixture.componentInstance.language).toBe("java");
+    expect(fixture.componentInstance.languageTitle).toBe("Java UDF");
+  });
+
+  it('picks language="java" for unknown operator types', () => {
+    const fixture = makeFixture(buildPredicate("u-0", "SomeUnknownType"));
+    expect(fixture.componentInstance.language).toBe("java");
+    expect(fixture.componentInstance.languageTitle).toBe("Java UDF");
+  });
+
+  it("derives languageTitle as Capitalized(language) + ' UDF'", () => {
+    const fixture = makeFixture(buildPredicate("p-x", "PythonUDFV2"));
+    const c = fixture.componentInstance;
+    // Independent re-derivation matches whatever the component computed.
+    const expected = `${c.language[0].toUpperCase()}${c.language.slice(1)} 
UDF`;
+    expect(c.languageTitle).toBe(expected);
+  });
+
+  // Coeditor cursor styles — getCoeditorCursorStyles takes the awareness-
+  // sourced clientId + colour and wraps a `<style>` block via
+  // `DomSanitizer.bypassSecurityTrustHtml`, so the return value is a
+  // SafeHtml (consumed via `[innerHTML]` in the template). We assert the
+  // wrapper shape (truthy DomSanitizer-wrapped object) for valid inputs.
+  // Exact CSS contents are sanitizer-internal and differ across builds, so
+  // we don't pin them here.
+
+  it("produces a SafeHtml for a coeditor with a numeric clientId and a hex 
colour", () => {
+    const fixture = makeFixture(mockJavaUDFPredicate);
+    const result = fixture.componentInstance.getCoeditorCursorStyles({
+      clientId: "12345",
+      color: "#ff00aa",
+    } as any);
+    expect(result).toBeTruthy();
+  });
+
+  it("produces a SafeHtml for a coeditor with an rgba colour", () => {
+    const fixture = makeFixture(mockJavaUDFPredicate);
+    const result = fixture.componentInstance.getCoeditorCursorStyles({
+      clientId: "42",
+      color: "rgba(10, 20, 30, 0.8)",
+    } as any);
+    expect(result).toBeTruthy();
+  });
 });


Reply via email to