From 6f90d151da9ed01fdd71e13d60052180ab0a25c2 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Thu, 16 Jul 2026 03:24:16 -0700 Subject: [PATCH 1/3] test(frontend): add unit tests for WorkflowSnapshotService --- .../workflow-snapshot.service.spec.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts diff --git a/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts new file mode 100644 index 00000000000..92204fde6bd --- /dev/null +++ b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts @@ -0,0 +1,118 @@ +/** + * 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 { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import html2canvas from "html2canvas"; +import { + WorkflowSnapshotService, + WORKFLOW_SNAPSHOT_API_BASE_URL, + WORKFLOW_SNAPSHOT_UPLOAD_URL, +} from "./workflow-snapshot.service"; + +vi.mock("html2canvas", () => ({ default: vi.fn() })); + +describe("WorkflowSnapshotService", () => { + let service: WorkflowSnapshotService; + let httpMock: HttpTestingController; + + beforeEach(() => { + vi.clearAllMocks(); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [WorkflowSnapshotService], + }); + service = TestBed.inject(WorkflowSnapshotService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("createSnapShotCanvas", () => { + it("scales the target element's rect by the given ratios and returns the canvas", async () => { + const canvas = document.createElement("canvas"); + (html2canvas as unknown as ReturnType).mockResolvedValue(canvas); + + // Force a deterministic rect regardless of the jsdom layout. + vi.spyOn(document.body, "getBoundingClientRect").mockReturnValue({ + height: 200, + width: 100, + } as DOMRect); + + const result = await service.createSnapShotCanvas(0.5, 0.1, 0.5, 0.2); + + expect(result).toBe(canvas); + expect(html2canvas).toHaveBeenCalledTimes(1); + const [, options] = (html2canvas as unknown as ReturnType).mock.calls[0]; + expect(options).toMatchObject({ height: 100, y: 20, width: 50, x: 20 }); + }); + + it("falls back to document.body when the editor element is absent", async () => { + const canvas = document.createElement("canvas"); + (html2canvas as unknown as ReturnType).mockResolvedValue(canvas); + + await service.createSnapShotCanvas(1, 0, 1, 0); + + const [element] = (html2canvas as unknown as ReturnType).mock.calls[0]; + expect(element).toBe(document.body); + }); + }); + + describe("uploadWorkflowSnapshot", () => { + it("PUTs a multipart form with the blob and stringified wid", () => { + const blob = new Blob(["img"], { type: "image/png" }); + service.uploadWorkflowSnapshot(blob, 12).subscribe(); + + const req = httpMock.expectOne(WORKFLOW_SNAPSHOT_UPLOAD_URL); + expect(req.request.method).toEqual("PUT"); + const body = req.request.body as FormData; + expect(body.get("wid")).toEqual("12"); + expect(body.get("SnapshotBlob")).toBeInstanceOf(Blob); + req.flush({}); + }); + + it("sends an empty wid string when wid is undefined", () => { + service.uploadWorkflowSnapshot(new Blob([]), undefined).subscribe(); + + const req = httpMock.expectOne(WORKFLOW_SNAPSHOT_UPLOAD_URL); + expect((req.request.body as FormData).get("wid")).toEqual(""); + req.flush({}); + }); + }); + + describe("retrieveWorkflowSnapshot", () => { + it("GETs the per-snapshot endpoint", () => { + const entry = { sid: 3, snapshot: "data" } as any; + let result: any; + service.retrieveWorkflowSnapshot(3).subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${WORKFLOW_SNAPSHOT_API_BASE_URL}/3`); + expect(req.request.method).toEqual("GET"); + req.flush(entry); + + expect(result).toEqual(entry); + }); + }); +}); From 370a4c358f3896d91e8d720d891d84866624a48b Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Thu, 16 Jul 2026 11:56:02 -0700 Subject: [PATCH 2/3] test(frontend): drop html2canvas-dependent cases (unreliable under shared registry) --- .../workflow-snapshot.service.spec.ts | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts index 92204fde6bd..260e2fe6987 100644 --- a/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts @@ -19,21 +19,20 @@ import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; -import html2canvas from "html2canvas"; import { WorkflowSnapshotService, WORKFLOW_SNAPSHOT_API_BASE_URL, WORKFLOW_SNAPSHOT_UPLOAD_URL, } from "./workflow-snapshot.service"; -vi.mock("html2canvas", () => ({ default: vi.fn() })); - +// createSnapShotCanvas() delegates to html2canvas, whose module-level mock is not +// reliable here (other specs import it unmocked and the builder shares one module +// registry), so it is exercised in the e2e/browser suite rather than pinned here. describe("WorkflowSnapshotService", () => { let service: WorkflowSnapshotService; let httpMock: HttpTestingController; beforeEach(() => { - vi.clearAllMocks(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [WorkflowSnapshotService], @@ -50,36 +49,6 @@ describe("WorkflowSnapshotService", () => { expect(service).toBeTruthy(); }); - describe("createSnapShotCanvas", () => { - it("scales the target element's rect by the given ratios and returns the canvas", async () => { - const canvas = document.createElement("canvas"); - (html2canvas as unknown as ReturnType).mockResolvedValue(canvas); - - // Force a deterministic rect regardless of the jsdom layout. - vi.spyOn(document.body, "getBoundingClientRect").mockReturnValue({ - height: 200, - width: 100, - } as DOMRect); - - const result = await service.createSnapShotCanvas(0.5, 0.1, 0.5, 0.2); - - expect(result).toBe(canvas); - expect(html2canvas).toHaveBeenCalledTimes(1); - const [, options] = (html2canvas as unknown as ReturnType).mock.calls[0]; - expect(options).toMatchObject({ height: 100, y: 20, width: 50, x: 20 }); - }); - - it("falls back to document.body when the editor element is absent", async () => { - const canvas = document.createElement("canvas"); - (html2canvas as unknown as ReturnType).mockResolvedValue(canvas); - - await service.createSnapShotCanvas(1, 0, 1, 0); - - const [element] = (html2canvas as unknown as ReturnType).mock.calls[0]; - expect(element).toBe(document.body); - }); - }); - describe("uploadWorkflowSnapshot", () => { it("PUTs a multipart form with the blob and stringified wid", () => { const blob = new Blob(["img"], { type: "image/png" }); @@ -114,5 +83,17 @@ describe("WorkflowSnapshotService", () => { expect(result).toEqual(entry); }); + + it("propagates a server error to the caller", () => { + let errored = false; + service.retrieveWorkflowSnapshot(9).subscribe({ error: (_e: unknown) => (errored = true) }); + + httpMock.expectOne(`${WORKFLOW_SNAPSHOT_API_BASE_URL}/9`).flush("boom", { + status: 500, + statusText: "Server Error", + }); + + expect(errored).toBe(true); + }); }); }); From c236bfaa6075329e80bfb47c2e97ee3d384bcada Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Tue, 21 Jul 2026 19:39:35 -0700 Subject: [PATCH 3/3] test: address review comments Co-Authored-By: Claude Opus 4.8 --- .../workflow-snapshot.service.spec.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts index 260e2fe6987..0892dd8285c 100644 --- a/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.spec.ts @@ -19,15 +19,16 @@ import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; +import html2canvas from "html2canvas"; import { WorkflowSnapshotService, WORKFLOW_SNAPSHOT_API_BASE_URL, WORKFLOW_SNAPSHOT_UPLOAD_URL, } from "./workflow-snapshot.service"; -// createSnapShotCanvas() delegates to html2canvas, whose module-level mock is not -// reliable here (other specs import it unmocked and the builder shares one module -// registry), so it is exercised in the e2e/browser suite rather than pinned here. +// Stub html2canvas so createSnapShotCanvas() can be unit tested under jsdom. +vi.mock("html2canvas", () => ({ default: vi.fn() })); + describe("WorkflowSnapshotService", () => { let service: WorkflowSnapshotService; let httpMock: HttpTestingController; @@ -69,6 +70,18 @@ describe("WorkflowSnapshotService", () => { expect((req.request.body as FormData).get("wid")).toEqual(""); req.flush({}); }); + + it("propagates a server error to the caller", () => { + let errored = false; + service.uploadWorkflowSnapshot(new Blob([]), 12).subscribe({ error: (_e: unknown) => (errored = true) }); + + httpMock.expectOne(WORKFLOW_SNAPSHOT_UPLOAD_URL).flush("boom", { + status: 500, + statusText: "Server Error", + }); + + expect(errored).toBe(true); + }); }); describe("retrieveWorkflowSnapshot", () => { @@ -96,4 +109,21 @@ describe("WorkflowSnapshotService", () => { expect(errored).toBe(true); }); }); + + describe("createSnapShotCanvas", () => { + it("falls back to document.body and applies the crop ratios", async () => { + const fakeCanvas = document.createElement("canvas"); + vi.mocked(html2canvas).mockResolvedValue(fakeCanvas); + // No #texera-workflow-editor element, so it should fall back to document.body. + vi.spyOn(document, "getElementById").mockReturnValue(null); + vi.spyOn(document.body, "getBoundingClientRect").mockReturnValue({ height: 200, width: 100 } as DOMRect); + + const result = await service.createSnapShotCanvas(0.5, 0.1, 0.25, 0.2); + + expect(result).toBe(fakeCanvas); + const [target, options] = vi.mocked(html2canvas).mock.calls[0]; + expect(target).toBe(document.body); + expect(options).toEqual(expect.objectContaining({ height: 100, y: 20, width: 25, x: 20 })); + }); + }); });