You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/13 06:18:15 UTC
(texera) 01/01: test(frontend): cover the workflow-snapshot render path in ReportGenerationService (#6471)
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. <mg...@uci.edu>
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 <xi...@uci.edu>
---
.../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);
+ });
+ });
});
});