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-6471-7a6ee7cf9198de7b103bacf685b4237b804a6ef4 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 86dd852085db10e8fe7c2db59a9232ef8347eb67 Author: Matthew B. <[email protected]> AuthorDate: Thu Aug 13 06:15:06 2026 +0000 test(frontend): cover the workflow-snapshot render path in ReportGenerationService (#6471) ### What changes were proposed in this PR? Covers the last uncovered part of `ReportGenerationService`: the render callback of `generateWorkflowSnapshot`, which encodes the canvas html2canvas hands back as a PNG and completes the observable. No production code changed. While this PR sat, main grew its own `report-generation.service.spec.ts` (#7383, #7541), which took over every case this branch originally added and left exactly one gap — lines 93-95 of the service: ```ts .then((canvas: HTMLCanvasElement) => { const dataUrl: string = canvas.toDataURL("image/png"); // 93 observer.next(dataUrl); // 94 observer.complete(); // 95 }) ``` main's suite deliberately leaves the render alone ("it needs a real canvas") and asserts only on the image-inlining step that precedes it. This PR closes that gap, so the merge keeps main's version of the file wholesale and adds one `describe` on top of it. | | statements | uncovered | tests in file | | --- | --- | --- | --- | | main | 126/129 (97.7%) | 93, 94, 95 | 24 | | this PR | **129/129 (100%)** | none | 26 | Branches stay at 25/25 and functions go from one uncovered (the callback at line 92) to none. **Why the original approach could not work.** The first version of this test replaced the renderer with `vi.mock("html2canvas", () => ({ default: vi.fn() }))`. That passes on its own and fails in CI, which is what the `build / frontend` job was reporting: ``` TypeError: Cannot read properties of null (reading 'scale') ❯ new ForeignObjectRenderer node_modules/html2canvas/dist/html2canvas.esm.js:7570:19 ``` `@angular/build`'s unit-test runner sets `isolate: false` ("Default to `false` to align with the Karma/Jasmine experience"), so every spec file shares one module registry. `MenuComponent`'s spec pulls this service in transitively, so whichever spec loads html2canvas first pins it for the whole run — the mock reaches the spec's own import but not the already-instantiated service, which keeps the real module and dies on jsdom's unimplemented `getContext`. Solo, nothing loads the service first, so the mock applies and the test passes; that gap between the two is why this only showed up in CI. So the real renderer is used instead, with jsdom given the three pieces it lacks: a permissive 2D context, an `<img>` that reports a data-URL source as loaded, and a PNG encoder. That also lets the failure path be pinned with a known error instead of jsdom's incidental one. ### Any related issues, documentation, discussions? Closes #6459 ### How was this PR tested? Two new cases, both run in the full-suite configuration that broke the original: ```bash cd frontend && yarn test:ci ``` `200 test files passed`, `4444 passed | 1 skipped`, up from `4442 passed | 1 skipped` on main. Coverage of `report-generation.service.ts` was read out of `coverage/gui/coverage-final.json` from that same full-suite run, on main and on this branch, to get the numbers in the table above. The file on its own: ```bash cd frontend && npx ng test --watch=false --include='**/report-generation.service.spec.ts' ``` `26 tests` pass. `yarn format:ci` is clean. ### Was this PR authored or co-authored using generative AI tooling? Yes, in compliance with ASF policy. The original spec was co-authored with Claude Opus 4.8; the conflict resolution and the CI fix were co-authored with Claude Code. Generated-by: Claude Code (Claude Opus 5) --------- Co-authored-by: Xinyuan Lin <[email protected]> --- .../report-generation.service.spec.ts | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/frontend/src/app/workspace/service/report-generation/report-generation.service.spec.ts b/frontend/src/app/workspace/service/report-generation/report-generation.service.spec.ts index 21617223c8..15c7155b5a 100644 --- a/frontend/src/app/workspace/service/report-generation/report-generation.service.spec.ts +++ b/frontend/src/app/workspace/service/report-generation/report-generation.service.spec.ts @@ -477,5 +477,93 @@ describe("ReportGenerationService", () => { ); }); }); + + /** + * Once the images are inlined, the editor is handed to html2canvas and whatever canvas comes + * back is encoded as a PNG. Substituting the renderer with `vi.mock("html2canvas")` does not + * work here: @angular/build's unit-test runner runs spec files with `isolate: false`, so they + * share one module registry and MenuComponent's spec — which pulls this service in + * transitively — can pin the real html2canvas before this file's mock is ever registered. + * The real renderer is used instead, with jsdom given the three pieces it lacks: a 2D + * context, an <img> that reports a data-URL source as loaded, and a PNG encoder. + */ + describe("rendering the editor to a PNG", () => { + const RENDERED_PNG = "data:image/png;base64,RENDERED"; + + let realImage: typeof globalThis.Image; + let toDataUrl: ReturnType<typeof vi.spyOn>; + let editor: HTMLElement; + + /** + * html2canvas draws the cloned editor onto a canvas; none of those calls affect what the + * service does with the result, so a context that accepts every call stands in for one. + */ + function permissiveContext(): CanvasRenderingContext2D { + return new Proxy({}, { get: () => () => undefined }) as CanvasRenderingContext2D; + } + + /** html2canvas rasterizes the clone through an <img> pointed at a serialized SVG. */ + class InstantImage { + public onload: (() => void) | null = null; + public onerror: (() => void) | null = null; + private source = ""; + get src(): string { + return this.source; + } + set src(value: string) { + this.source = value; + queueMicrotask(() => this.onload?.()); + } + } + + beforeEach(() => { + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation( + () => permissiveContext() as unknown as never + ); + toDataUrl = vi.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue(RENDERED_PNG); + realImage = globalThis.Image; + (globalThis as unknown as { Image: unknown }).Image = InstantImage; + editor = document.createElement("div"); + editor.id = "workflow-editor"; + document.body.appendChild(editor); + }); + + afterEach(() => { + (globalThis as unknown as { Image: unknown }).Image = realImage; + vi.restoreAllMocks(); + editor.remove(); + }); + + it("emits the rendered editor as a PNG data URL and then completes", async () => { + const emitted: string[] = []; + let completed = false; + + await new Promise<void>((resolve, reject) => { + service.generateWorkflowSnapshot("myflow").subscribe({ + next: value => emitted.push(value), + error: reject, + complete: () => { + completed = true; + resolve(); + }, + }); + }); + + expect(emitted).toEqual([RENDERED_PNG]); + expect(completed).toBe(true); + // PNG specifically: the report embeds the snapshot in an <img>, so the format is not + // the encoder's default choice to make. + expect(toDataUrl).toHaveBeenCalledWith("image/png"); + }); + + it("fails when the editor cannot be rendered", async () => { + const failure = new Error("canvas unavailable"); + toDataUrl.mockImplementation(() => { + throw failure; + }); + + await expect(firstValueFrom(service.generateWorkflowSnapshot("myflow"))).rejects.toBe(failure); + }); + }); }); });
