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);
+      });
+    });
   });
 });

Reply via email to