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