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-6788-28a2f9f896c41ca639bf75f240b38c1cc6930ee1 in repository https://gitbox.apache.org/repos/asf/texera.git
commit d89107a36598e7b07a16e9d49ecd779ada5ebd1b Author: Xinyuan Lin <[email protected]> AuthorDate: Wed Jul 22 19:38:56 2026 -0700 test(frontend): extend LeftPanelComponent unit test coverage (#6788) ### What changes were proposed in this PR? Extends `left-panel.component.spec.ts` with 17 tests (2 -> 19), covering the `openFrame` collapse/re-open/preserve-width branches, the resize (rAF)/drop/reset/drag handlers, the PanelService close/reset stream subscriptions, `ngAfterViewInit` height summation, the `beforeunload` persistence host-listener, and the constructor/ngOnInit localStorage guard branches (saved-order accept/reject, disabled saved index, style restore). Coverage 79% -> 100%. No existing tests modified. ### Any related issues, documentation, discussions? Closes #6786. ### How was this PR tested? `ng test --include='**/left-panel.component.spec.ts'` -> 19/19 passing. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../left-panel/left-panel.component.spec.ts | 253 +++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts b/frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts index cee8ab6c57..87cd859422 100644 --- a/frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts +++ b/frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts @@ -21,12 +21,24 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testin import { LeftPanelComponent } from "./left-panel.component"; import { mockPoint, mockScanPredicate } from "../../service/workflow-graph/model/mock-workflow-data"; import { VersionsListComponent } from "./versions-list/versions-list.component"; +import { OperatorMenuComponent } from "./operator-menu/operator-menu.component"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { RouterTestingModule } from "@angular/router/testing"; import { commonTestProviders } from "../../../common/testing/test-utils"; +import { PanelService } from "../../service/panel/panel.service"; +import { NzResizeEvent } from "ng-zorro-antd/resizable"; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; + +const PANEL_LOCAL_STORAGE_KEYS = [ + "left-panel-width", + "left-panel-height", + "left-panel-order", + "left-panel-index", + "left-panel-style", +]; describe("LeftPanelComponent", () => { let component: LeftPanelComponent; @@ -34,6 +46,11 @@ describe("LeftPanelComponent", () => { let workflowActionService: WorkflowActionService; let fixture: ComponentFixture<LeftPanelComponent>; + const clearPanelStorage = () => PANEL_LOCAL_STORAGE_KEYS.forEach(key => localStorage.removeItem(key)); + + // Ensure the constructor reads a clean slate so order/index/width defaults are deterministic. + beforeEach(() => clearPanelStorage()); + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [LeftPanelComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([])], @@ -54,6 +71,11 @@ describe("LeftPanelComponent", () => { fixture.detectChanges(); }); + afterEach(() => { + fixture.destroy(); + clearPanelStorage(); + }); + it("should create", () => { expect(component).toBeTruthy(); }); @@ -81,4 +103,235 @@ describe("LeftPanelComponent", () => { // the component should switch to versions display expect(component.currentComponent).toBe(VersionsListComponent); })); + + it("openFrame(0) collapses the panel to the docked bar", () => { + // simulate an already-open panel + component.width = 250; + component.height = 500; + + component.openFrame(0); + + expect(component.width).toBe(0); + expect(component.height).toBe(65); + expect(component.currentIndex).toBe(0); + expect(component.currentComponent).toBeNull(); + expect(component.title).toBe(""); + }); + + it("openFrame re-opens a collapsed panel using MIN_PANEL_WIDTH and minPanelHeight", () => { + // start collapsed (width 0) + component.openFrame(0); + expect(component.width).toBe(0); + + component.minPanelHeight = 333; + component.openFrame(1); + + // the collapsed -> open branch restores default width and uses minPanelHeight + expect(component.width).toBe(230); + expect(component.height).toBe(333); + expect(component.currentIndex).toBe(1); + expect(component.currentComponent).toBe(component.items[1].component); + expect(component.title).toBe("Operators"); + }); + + it("openFrame preserves the current width when switching frames on an already-open panel", () => { + // open the panel, then simulate a user-resized width + component.openFrame(1); + component.width = 400; + + component.openFrame(3); + + // width must be left untouched because the panel is already open + expect(component.width).toBe(400); + expect(component.currentIndex).toBe(3); + expect(component.currentComponent).toBe(component.items[3].component); + expect(component.title).toBe(component.items[3].title); + }); + + it("constructor falls back to the Operators frame when the saved index points to a disabled tab", () => { + // index 4 (Execution History) is disabled in the mock GUI config + localStorage.setItem("left-panel-index", "4"); + + const freshFixture = TestBed.createComponent(LeftPanelComponent); + const fresh = freshFixture.componentInstance; + + expect(fresh.currentIndex).toBe(1); + expect(fresh.currentComponent).toBe(fresh.items[1].component); + + freshFixture.destroy(); + }); + + it("onDrop reorders the tab order array in place", () => { + // default order is [1, 2, 3, 4, 5] + expect(component.order).toEqual([1, 2, 3, 4, 5]); + + component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<string[]>); + + expect(component.order).toEqual([2, 3, 1, 4, 5]); + }); + + it("onResize applies the new dimensions through requestAnimationFrame", () => { + const rafSpy = vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback): number => { + cb(0); + return 1; + }); + const cafSpy = vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {}); + try { + component.onResize({ width: 321, height: 654 } as NzResizeEvent); + + expect(component.width).toBe(321); + expect(component.height).toBe(654); + } finally { + rafSpy.mockRestore(); + cafSpy.mockRestore(); + } + }); + + it("resetPanelPosition docks the panel back onto its return position", () => { + component.returnPosition = { x: 12, y: 34 }; + component.dragPosition = { x: 99, y: 99 }; + component.isDocked = false; + + component.resetPanelPosition(); + + expect(component.dragPosition).toEqual({ x: 12, y: 34 }); + expect(component.isDocked).toBe(true); + }); + + it("handleDragStart marks the panel as undocked", () => { + component.isDocked = true; + + component.handleDragStart(); + + expect(component.isDocked).toBe(false); + }); + + it("closePanelStream collapses the panel via openFrame(0)", () => { + const panelService = TestBed.inject(PanelService); + component.openFrame(1); + expect(component.width).toBeGreaterThan(0); + + panelService.closePanels(); + + expect(component.width).toBe(0); + expect(component.currentIndex).toBe(0); + expect(component.currentComponent).toBeNull(); + }); + + it("resetPanelStream resets the position and re-opens the Operators frame", () => { + const panelService = TestBed.inject(PanelService); + component.returnPosition = { x: 5, y: 6 }; + component.dragPosition = { x: 50, y: 60 }; + component.isDocked = false; + + panelService.resetPanels(); + + expect(component.isDocked).toBe(true); + expect(component.dragPosition).toEqual({ x: 5, y: 6 }); + expect(component.currentIndex).toBe(1); + expect(component.currentComponent).toBe(OperatorMenuComponent); + }); + + it("ngAfterViewInit sizes minPanelHeight/height from the top-level operator categories", fakeAsync(() => { + const contentEl = component.content.nativeElement; + // inject two top-level category panels with a known clientHeight + for (let i = 0; i < 2; i++) { + const panel = document.createElement("nz-collapse-panel"); + panel.classList.add("operator-group"); + panel.setAttribute("data-depth", "0"); + Object.defineProperty(panel, "clientHeight", { value: 100, configurable: true }); + contentEl.appendChild(panel); + } + + component.ngAfterViewInit(); + tick(); // flush the setTimeout(..., 0) + + // 100 + 100 = 200 measured height, + 90 padding + expect(component.minPanelHeight).toBe(290); + expect(component.height).toBe(290); + })); + + it("ngAfterViewInit leaves the height untouched when there are no top-level categories", fakeAsync(() => { + const originalMin = component.minPanelHeight; + const originalHeight = component.height; + + // content has no matching nz-collapse-panel[data-depth="0"] elements + component.ngAfterViewInit(); + tick(); + + expect(component.minPanelHeight).toBe(originalMin); + expect(component.height).toBe(originalHeight); + })); + + it("persists panel state to localStorage on the beforeunload host listener", () => { + component.width = 111; + component.height = 222; + component.currentIndex = 2; + + window.dispatchEvent(new Event("beforeunload")); + + expect(localStorage.getItem("left-panel-width")).toBe("111"); + expect(localStorage.getItem("left-panel-height")).toBe("222"); + expect(localStorage.getItem("left-panel-index")).toBe("2"); + expect(localStorage.getItem("left-panel-order")).toBe(String(component.order)); + }); + + it("ngOnDestroy skips style persistence when the left-container element is absent", () => { + localStorage.removeItem("left-panel-style"); + const getByIdSpy = vi.spyOn(document, "getElementById").mockReturnValue(null); + + component.ngOnDestroy(); + + // style is only written when the container exists + expect(localStorage.getItem("left-panel-style")).toBeNull(); + // the remaining keys are still persisted unconditionally + expect(localStorage.getItem("left-panel-width")).not.toBeNull(); + expect(localStorage.getItem("left-panel-index")).not.toBeNull(); + + getByIdSpy.mockRestore(); + }); + + it("constructor restores a valid saved tab order from localStorage", () => { + // a permutation whose value-set matches the default order's set is accepted + localStorage.setItem("left-panel-order", "5,4,3,2,1"); + + const freshFixture = TestBed.createComponent(LeftPanelComponent); + const fresh = freshFixture.componentInstance; + + expect(fresh.order).toEqual([5, 4, 3, 2, 1]); + + freshFixture.destroy(); + }); + + it("constructor ignores a saved tab order whose unique-entry count differs from the default", () => { + // the guard compares Set sizes only: a duplicate/short set has a different unique count -> fall back to the default order + localStorage.setItem("left-panel-order", "1,1,1"); + + const freshFixture = TestBed.createComponent(LeftPanelComponent); + const fresh = freshFixture.componentInstance; + + expect(fresh.order).toEqual([1, 2, 3, 4, 5]); + + freshFixture.destroy(); + }); + + it("restores the saved left-container style on its first ngOnInit", () => { + // Destroy the default fixture so a single #left-container lives in the document, set the + // saved style, then create a fresh component and attach it before its FIRST ngOnInit runs + // (mirroring real usage where the value exists before init). Assert the immediate restore: + // a subsequent change-detection pass re-applies the template's [style.width]/[style.height] + // bindings, which would overwrite this cssText, so the restore is observed at ngOnInit time. + fixture.destroy(); + localStorage.setItem("left-panel-style", "width: 123px;"); + + const freshFixture = TestBed.createComponent(LeftPanelComponent); + document.body.appendChild(freshFixture.nativeElement); + try { + freshFixture.componentInstance.ngOnInit(); + expect(document.getElementById("left-container")!.style.cssText).toContain("width: 123px"); + } finally { + document.body.removeChild(freshFixture.nativeElement); + freshFixture.destroy(); + } + }); });
