This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new bbd5c976d7 test(frontend): render the error frame's grouping and 
operator jump (#7442)
bbd5c976d7 is described below

commit bbd5c976d7512f796251e5d5a8ca1495e5b3756f
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Aug 9 17:59:23 2026 -0700

    test(frontend): render the error frame's grouping and operator jump (#7442)
    
    ### What changes were proposed in this PR?
    
    The suite builds the category map and never renders it, so everything
    the template decides was unpinned.
    
    Adds 10 tests. The one with real teeth is the jump-to-operator shortcut,
    guarded by two conditions: it is withheld for the `unknown operator`
    sentinel, which would navigate the canvas to nothing, and for the
    operator already being shown, where it would be a no-op. Its click also
    stops propagation — the icon sits in the collapse header and would
    otherwise toggle the panel underneath the user.
    
    Also covers the all-operators banner appearing only for an unscoped
    frame, the empty state appearing only when there is nothing to report,
    the grouping into category headings, and the message heading the panel
    with the details in its body.
    
    **Verified by mutation**, all reverted (template diff empty):
    
    | Mutation | Result |
    |---|---|
    | show the all-operators banner always | red |
    | show the empty state always | red |
    | head the category with its size instead of its name | red |
    | head the panel with the details | red |
    | put the message in the body | red |
    | offer the jump for the unknown-operator sentinel | red |
    | offer the jump for the operator already shown | red |
    | jump to the frame's operator instead of the error's | red |
    | drop `stopPropagation` from the jump | red |
    
    The empty-state mutation **survived its first run**: the test asserted
    the message appears with no errors but never that it disappears once
    there are some. It now checks both directions.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7439
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/error-frame.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  17 passed (17)
    ```
    
    10 new on top of the existing 7. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
    
    ---------
    
    Signed-off-by: Xinyuan Lin <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../error-frame/error-frame.component.spec.ts      | 101 +++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git 
a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts
 
b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts
index 28e095e168..67d0a1096d 100644
--- 
a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts
@@ -158,4 +158,105 @@ describe("ErrorFrameComponent", () => {
       expect(highlight).toHaveBeenCalledWith(false, "op-42");
     });
   });
+  /**
+   * The frame's template decides what an error list looks like: the 
all-operators banner, the empty
+   * state, the grouping into categories, and — the part with real teeth — 
whether a "focus operator"
+   * shortcut is offered at all. The suite above builds the category map and 
never renders it.
+   */
+  describe("rendered errors", () => {
+    /** Renders the frame for the given errors, optionally scoped to one 
operator. */
+    function render(errors: WorkflowFatalError[], scopedTo?: string): 
HTMLElement {
+      component.operatorId = scopedTo;
+      component.categoryToErrorMapping = errors.reduce((acc, e) => {
+        const key = e.type.name;
+        acc.set(key, [...(acc.get(key) ?? []), e]);
+        return acc;
+      }, new Map<string, WorkflowFatalError[]>());
+      fixture.detectChanges();
+      return fixture.nativeElement as HTMLElement;
+    }
+
+    function gotoIcons(): HTMLElement[] {
+      return Array.from((fixture.nativeElement as 
HTMLElement).querySelectorAll<HTMLElement>(".goto-operator-icon"));
+    }
+
+    it("announces that it is showing every operator's errors", () => {
+      const el = render([fatalError()]);
+
+      expect(el.querySelector(".all-errors-notification")).not.toBeNull();
+    });
+
+    it("drops that banner once the frame is scoped to one operator", () => {
+      const el = render([fatalError()], "op1");
+
+      expect(el.querySelector(".all-errors-notification")).toBeNull();
+    });
+
+    it("says so when there is nothing to report, and only then", () => {
+      const el = render([]);
+      expect(el.textContent).toContain("No error to display.");
+
+      render([fatalError()]);
+      expect((fixture.nativeElement as 
HTMLElement).textContent).not.toContain("No error to display.");
+    });
+
+    it("groups the errors under their category headings", () => {
+      const el = render([fatalError({ type: { name: "COMPILATION" } }), 
fatalError({ type: { name: "EXECUTION" } })]);
+
+      const headings = 
Array.from(el.querySelectorAll(".error-category")).map(h => 
h.textContent?.trim());
+      expect(headings).toEqual(["COMPILATION:", "EXECUTION:"]);
+    });
+
+    it("shows each error's message as the heading and its details in the 
body", () => {
+      const el = render([fatalError({ message: "boom", details: "stack trace 
here" })]);
+
+      
expect(el.querySelector(".ant-collapse-header")?.textContent).toContain("boom");
+      expect(el.querySelector(".error-message")?.textContent).toContain("stack 
trace here");
+    });
+
+    it("offers a jump to the operator that failed", () => {
+      render([fatalError({ operatorId: "op-broken" })]);
+
+      expect(gotoIcons().length).toBe(1);
+    });
+
+    it("offers no jump for an error with no operator behind it", () => {
+      // "unknown operator" is the sentinel the backend sends for errors that 
belong to no operator;
+      // offering the shortcut would navigate the canvas to nothing.
+      render([fatalError({ operatorId: "unknown operator" })]);
+
+      expect(gotoIcons()).toEqual([]);
+    });
+
+    it("offers no jump to the operator already being shown", () => {
+      // Scoped to op1 and the error is op1's: the shortcut would be a no-op.
+      render([fatalError({ operatorId: "op1" })], "op1");
+
+      expect(gotoIcons()).toEqual([]);
+    });
+
+    it("jumps to the operator the error names", () => {
+      const spy = vi.spyOn(component, 
"onClickGotoButton").mockImplementation(() => {});
+      render([fatalError({ operatorId: "op-broken" })]);
+
+      gotoIcons()[0].click();
+
+      expect(spy).toHaveBeenCalledWith("op-broken");
+    });
+
+    it("does not toggle the panel when the jump is clicked", () => {
+      // The icon lives in the collapse header, so without stopPropagation the 
panel would open or
+      // close underneath the user on the way to another operator.
+      vi.spyOn(component, "onClickGotoButton").mockImplementation(() => {});
+      const el = render([fatalError({ operatorId: "op-broken" })]);
+      const panelBefore = 
el.querySelectorAll(".ant-collapse-item-active").length;
+
+      gotoIcons()[0].click();
+      fixture.detectChanges();
+
+      expect((fixture.nativeElement as 
HTMLElement).querySelectorAll(".ant-collapse-item-active").length).toBe(
+        panelBefore
+      );
+    });
+  });
 });

Reply via email to