From 592a5fc3660539c8f7136dccd5e41a82f0f63993 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Thu, 16 Jul 2026 03:24:16 -0700 Subject: [PATCH 1/5] test(frontend): add unit tests for FileSaverService --- .../user/file/file-saver.service.spec.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts new file mode 100644 index 00000000000..a044935a9aa --- /dev/null +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts @@ -0,0 +1,62 @@ +/** + * 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 { TestBed } from "@angular/core/testing"; +import * as FileSaver from "file-saver"; +import { FileSaverService } from "./file-saver.service"; + +vi.mock("file-saver", () => ({ saveAs: vi.fn() })); + +describe("FileSaverService", () => { + let service: FileSaverService; + + beforeEach(() => { + vi.clearAllMocks(); + TestBed.configureTestingModule({ providers: [FileSaverService] }); + service = TestBed.inject(FileSaverService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("delegates a Blob payload to FileSaver.saveAs with filename and options", () => { + const blob = new Blob(["hello"], { type: "text/plain" }); + const options: FileSaver.FileSaverOptions = { autoBom: true }; + + service.saveAs(blob, "greeting.txt", options); + + expect(FileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(FileSaver.saveAs).toHaveBeenCalledWith(blob, "greeting.txt", options); + }); + + it("forwards a string payload (e.g. a URL) unchanged", () => { + service.saveAs("https://example.com/file.csv", "file.csv"); + + expect(FileSaver.saveAs).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); + }); + + it("passes undefined through when filename and options are omitted", () => { + const blob = new Blob([]); + + service.saveAs(blob); + + expect(FileSaver.saveAs).toHaveBeenCalledWith(blob, undefined, undefined); + }); +}); From 4dbf7382b9e76545778669c15c6c245e35476852 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Thu, 16 Jul 2026 11:56:02 -0700 Subject: [PATCH 2/5] test(frontend): mock file-saver module so the spy survives the full suite --- .../user/file/file-saver.service.spec.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts index a044935a9aa..6896854cb5a 100644 --- a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts @@ -18,16 +18,18 @@ */ import { TestBed } from "@angular/core/testing"; -import * as FileSaver from "file-saver"; +import { saveAs } from "file-saver"; import { FileSaverService } from "./file-saver.service"; +// Replace the file-saver module wholesale (same precedent as menu.component.spec.ts); +// the service is a thin passthrough, so we assert it forwards to saveAs unchanged. vi.mock("file-saver", () => ({ saveAs: vi.fn() })); describe("FileSaverService", () => { let service: FileSaverService; beforeEach(() => { - vi.clearAllMocks(); + vi.mocked(saveAs).mockClear(); TestBed.configureTestingModule({ providers: [FileSaverService] }); service = TestBed.inject(FileSaverService); }); @@ -36,20 +38,20 @@ describe("FileSaverService", () => { expect(service).toBeTruthy(); }); - it("delegates a Blob payload to FileSaver.saveAs with filename and options", () => { + it("delegates a Blob payload to saveAs with filename and options", () => { const blob = new Blob(["hello"], { type: "text/plain" }); - const options: FileSaver.FileSaverOptions = { autoBom: true }; + const options = { autoBom: true }; service.saveAs(blob, "greeting.txt", options); - expect(FileSaver.saveAs).toHaveBeenCalledTimes(1); - expect(FileSaver.saveAs).toHaveBeenCalledWith(blob, "greeting.txt", options); + expect(saveAs).toHaveBeenCalledTimes(1); + expect(saveAs).toHaveBeenCalledWith(blob, "greeting.txt", options); }); it("forwards a string payload (e.g. a URL) unchanged", () => { service.saveAs("https://example.com/file.csv", "file.csv"); - expect(FileSaver.saveAs).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); + expect(saveAs).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); }); it("passes undefined through when filename and options are omitted", () => { @@ -57,6 +59,6 @@ describe("FileSaverService", () => { service.saveAs(blob); - expect(FileSaver.saveAs).toHaveBeenCalledWith(blob, undefined, undefined); + expect(saveAs).toHaveBeenCalledWith(blob, undefined, undefined); }); }); From 8bd6576a79f038e6d645fcb43fcebfc530f1861f Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Tue, 21 Jul 2026 18:25:22 -0700 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Xinyuan Lin --- .../service/user/file/file-saver.service.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts index 6896854cb5a..5d6a173a362 100644 --- a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts @@ -54,6 +54,15 @@ describe("FileSaverService", () => { expect(saveAs).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); }); + it("propagates errors thrown by saveAs", () => { + const err = new Error("boom"); + vi.mocked(saveAs).mockImplementationOnce(() => { + throw err; + }); + + expect(() => service.saveAs(new Blob([]), "x.txt")).toThrow(err); + }); + it("passes undefined through when filename and options are omitted", () => { const blob = new Blob([]); From 036aed393b2ab17d024d78d74d2937d7306fcdc3 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Tue, 21 Jul 2026 19:39:35 -0700 Subject: [PATCH 4/5] test: address review comments Co-Authored-By: Claude Opus 4.8 --- .../service/user/file/file-saver.service.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts index 6896854cb5a..ccdf74a3d82 100644 --- a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts @@ -61,4 +61,13 @@ describe("FileSaverService", () => { expect(saveAs).toHaveBeenCalledWith(blob, undefined, undefined); }); + + it("propagates errors thrown by saveAs instead of swallowing them", () => { + const failure = new Error("save failed"); + vi.mocked(saveAs).mockImplementationOnce(() => { + throw failure; + }); + + expect(() => service.saveAs(new Blob(["oops"]), "oops.txt")).toThrow(failure); + }); }); From f71c43c04697d81466c4c322a6496394edf3bbda Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Wed, 28 Oct 2026 02:22:23 -0700 Subject: [PATCH 5/5] test(frontend): unit-test FileSaverService through a stubbable saveAs delegate FileSaverService holds its reference to file-saver's `saveAs` in a `protected saveAsFn` instance property that defaults to `FileSaver.saveAs`, and its public `saveAs(data, filename?, options?)` calls `this.saveAsFn(data, filename, options)`. Routing the one call-out through an own property gives tests a boundary they can replace per instance, without touching the file-saver module. The spec injects FileSaverService via TestBed, assigns a fresh `vi.fn()` to the instance's `saveAsFn` in `beforeEach`, and drives the public `saveAs`: it checks the service is created, that a Blob payload is forwarded with its filename and options, that a string payload passes through unchanged, that an omitted filename and options arrive as `undefined`, and that an error thrown by `saveAsFn` propagates out of `saveAs` rather than being swallowed. Replacing the instance property is independent of module resolution and bundling, unlike the previous `vi.mock("file-saver")`, which did not reach the copy the service imports once the runner bundled the CommonJS module across the full suite. --- .../user/file/file-saver.service.spec.ts | 29 ++++++------------- .../service/user/file/file-saver.service.ts | 5 +++- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts index e94c0e97c37..3e67c52aaaa 100644 --- a/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.spec.ts @@ -18,20 +18,18 @@ */ import { TestBed } from "@angular/core/testing"; -import { saveAs } from "file-saver"; import { FileSaverService } from "./file-saver.service"; -// Replace the file-saver module wholesale (same precedent as menu.component.spec.ts); -// the service is a thin passthrough, so we assert it forwards to saveAs unchanged. -vi.mock("file-saver", () => ({ saveAs: vi.fn() })); - +// Stub saveAsFn (the call-out to file-saver) and assert saveAs forwards its arguments through. describe("FileSaverService", () => { let service: FileSaverService; + let saveAsFn: ReturnType; beforeEach(() => { - vi.mocked(saveAs).mockClear(); TestBed.configureTestingModule({ providers: [FileSaverService] }); service = TestBed.inject(FileSaverService); + saveAsFn = vi.fn(); + (service as unknown as { saveAsFn: unknown }).saveAsFn = saveAsFn; }); it("should be created", () => { @@ -44,23 +42,14 @@ describe("FileSaverService", () => { service.saveAs(blob, "greeting.txt", options); - expect(saveAs).toHaveBeenCalledTimes(1); - expect(saveAs).toHaveBeenCalledWith(blob, "greeting.txt", options); + expect(saveAsFn).toHaveBeenCalledTimes(1); + expect(saveAsFn).toHaveBeenCalledWith(blob, "greeting.txt", options); }); it("forwards a string payload (e.g. a URL) unchanged", () => { service.saveAs("https://example.com/file.csv", "file.csv"); - expect(saveAs).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); - }); - - it("propagates errors thrown by saveAs", () => { - const err = new Error("boom"); - vi.mocked(saveAs).mockImplementationOnce(() => { - throw err; - }); - - expect(() => service.saveAs(new Blob([]), "x.txt")).toThrow(err); + expect(saveAsFn).toHaveBeenCalledWith("https://example.com/file.csv", "file.csv", undefined); }); it("passes undefined through when filename and options are omitted", () => { @@ -68,12 +57,12 @@ describe("FileSaverService", () => { service.saveAs(blob); - expect(saveAs).toHaveBeenCalledWith(blob, undefined, undefined); + expect(saveAsFn).toHaveBeenCalledWith(blob, undefined, undefined); }); it("propagates errors thrown by saveAs instead of swallowing them", () => { const failure = new Error("save failed"); - vi.mocked(saveAs).mockImplementationOnce(() => { + saveAsFn.mockImplementationOnce(() => { throw failure; }); diff --git a/frontend/src/app/dashboard/service/user/file/file-saver.service.ts b/frontend/src/app/dashboard/service/user/file/file-saver.service.ts index ce3f34b13fa..075d8a57ac4 100644 --- a/frontend/src/app/dashboard/service/user/file/file-saver.service.ts +++ b/frontend/src/app/dashboard/service/user/file/file-saver.service.ts @@ -24,7 +24,10 @@ import * as FileSaver from "file-saver"; providedIn: "root", }) export class FileSaverService { + // Stubbable delegate to file-saver's saveAs. + protected saveAsFn: typeof FileSaver.saveAs = FileSaver.saveAs; + saveAs(data: Blob | string, filename?: string, options?: FileSaver.FileSaverOptions): void { - FileSaver.saveAs(data, filename, options); + this.saveAsFn(data, filename, options); } }