From af91706ff9a4c483e45a784678bcc2653f08d6c7 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 7 Jan 2026 22:36:27 -0500 Subject: [PATCH] Fix various tab bugs and add tests. --- examples/index.css | 4 +- .../{calculate_split.ts => calculate_edge.ts} | 175 +++++------------ src/common/calculate_intersect.ts | 3 + src/common/constants.ts | 46 +++++ src/common/flatten.ts | 1 + src/common/generate_grid.ts | 3 +- src/common/generate_overlay.ts | 13 +- src/common/insert_child.ts | 1 + src/common/layout_config.ts | 30 +-- src/common/redistribute_panel_sizes.ts | 6 +- src/common/remove_child.ts | 1 + src/extensions.ts | 28 +-- src/regular-layout-frame.ts | 127 ++++++++---- src/regular-layout.ts | 149 +++++++++------ tests/helpers/fixtures.ts | 66 ++++--- tests/helpers/integration.ts | 142 +++++++++++--- tests/integration/insert-panel.spec.ts | 23 ++- tests/integration/overlay-absolute.spec.ts | 107 +++++++++++ tests/integration/overlay-edge-detection.ts | 64 +++++++ tests/integration/overlay-grid.spec.ts | 102 ++++++++++ tests/integration/remove-panel.spec.ts | 36 +++- tests/integration/resize.spec.ts | 26 ++- tests/integration/save-restore.spec.ts | 125 ++++++++++-- tests/integration/tabs.spec.ts | 47 ++--- ...e_split.spec.ts => calculate_edge.spec.ts} | 180 ++++++++++++++---- tests/unit/css_grid_layout.spec.ts | 4 +- tests/unit/flatten.spec.ts | 49 +++++ tests/unit/hit_detection.spec.ts | 18 +- tests/unit/insert_child.spec.ts | 43 ++++- tests/unit/redistribute_panel_sizes.spec.ts | 16 +- tests/unit/remove_child.spec.ts | 6 +- 31 files changed, 1197 insertions(+), 444 deletions(-) rename src/common/{calculate_split.ts => calculate_edge.ts} (56%) create mode 100644 src/common/constants.ts create mode 100644 tests/integration/overlay-absolute.spec.ts create mode 100644 tests/integration/overlay-edge-detection.ts create mode 100644 tests/integration/overlay-grid.spec.ts rename tests/unit/{calculate_split.spec.ts => calculate_edge.spec.ts} (73%) diff --git a/examples/index.css b/examples/index.css index 0230498..b529c25 100644 --- a/examples/index.css +++ b/examples/index.css @@ -50,7 +50,7 @@ regular-layout-frame::part(active-tab) { } /* Frame in Overlay Mode */ -regular-layout-frame:not([slot]) { +regular-layout-frame.overlay { background-color: rgba(0, 0, 0, 0.2) !important; border: 1px dashed rgb(0, 0, 0); border-radius: 6px; @@ -67,7 +67,7 @@ regular-layout-frame::part(container) { display: none; } -regular-layout-frame[slot]::part(container) { +regular-layout-frame:not(.overlay)::part(container) { display: revert; } diff --git a/src/common/calculate_split.ts b/src/common/calculate_edge.ts similarity index 56% rename from src/common/calculate_split.ts rename to src/common/calculate_edge.ts index 1ed7ebc..658858e 100644 --- a/src/common/calculate_split.ts +++ b/src/common/calculate_edge.ts @@ -10,12 +10,9 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { calculate_intersection } from "./calculate_intersect"; +import { SPLIT_EDGE_TOLERANCE } from "./constants"; import { insert_child } from "./insert_child"; -import { - SPLIT_EDGE_TOLERANCE, - type Layout, - type LayoutPath, -} from "./layout_config"; +import type { Layout, LayoutPath, Orientation } from "./layout_config"; /** * Calculates an insertion point (which may involve splitting a single @@ -30,146 +27,78 @@ import { * @returns A new `LayoutPath` reflecting the updated (maybe) `"split-panel"`, * which is enough to draw the overlay. */ -function handle_matching_orientation( +export function calculate_edge( col: number, row: number, panel: Layout, slot: string, drop_target: LayoutPath, - is_before: boolean, ): LayoutPath { - if (drop_target.path.length === 0) { - const insert_index = is_before ? 0 : 1; - const new_panel = insert_child(panel, slot, [insert_index]); - if (is_before) { - return calculate_intersection(col, row, new_panel, false); - } else { - const new_drop_target = calculate_intersection( - col, - row, - new_panel, - false, - ); - return { - ...new_drop_target, - path: [0], - }; - } - } else { - const path_without_last = drop_target.path.slice(0, -1); - const last_index = drop_target.path[drop_target.path.length - 1]; - const insert_index = is_before ? last_index : last_index + 1; - const new_panel = insert_child(panel, slot, [ - ...path_without_last, - insert_index, - ]); + const is_column_edge = + drop_target.column_offset < SPLIT_EDGE_TOLERANCE || + drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE; - if (is_before) { - return calculate_intersection(col, row, new_panel, false); - } else { - const new_drop_target = calculate_intersection( - col, - row, - new_panel, - false, - ); - return { - ...new_drop_target, - path: [...path_without_last, last_index], - }; - } + const is_row_edge = + drop_target.row_offset < SPLIT_EDGE_TOLERANCE || + drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE; + + if (is_column_edge) { + return handle_axis( + col, + row, + panel, + slot, + drop_target, + drop_target.column_offset, + "horizontal", + ); + } else if (is_row_edge) { + return handle_axis( + col, + row, + panel, + slot, + drop_target, + drop_target.row_offset, + "vertical", + ); } -} -function handle_cross_orientation( - col: number, - row: number, - panel: Layout, - slot: string, - drop_target: LayoutPath, - insert_index: number, - new_orientation: "horizontal" | "vertical", -): LayoutPath { - const original_path = drop_target.path; - const new_panel = insert_child( - panel, - slot, - [...original_path, insert_index], - new_orientation, - ); - const new_drop_target = calculate_intersection(col, row, new_panel, false); - return { - ...new_drop_target, - slot, - path: [...original_path, insert_index], - }; + return drop_target; } -export function calculate_split( +function handle_axis( col: number, row: number, panel: Layout, slot: string, drop_target: LayoutPath, + axis_offset: number, + axis_orientation: Orientation, ): LayoutPath { - const is_column_edge = - drop_target.column_offset < SPLIT_EDGE_TOLERANCE || - drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE; - const is_row_edge = - drop_target.row_offset < SPLIT_EDGE_TOLERANCE || - drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE; - - if (is_column_edge) { - const is_before = drop_target.column_offset < SPLIT_EDGE_TOLERANCE; - if (drop_target.orientation === "horizontal") { - drop_target = handle_matching_orientation( - col, - row, - panel, - slot, - drop_target, - is_before, - ); - } else { + const is_before = axis_offset < SPLIT_EDGE_TOLERANCE; + if (drop_target.orientation === axis_orientation) { + if (drop_target.path.length === 0) { const insert_index = is_before ? 0 : 1; - drop_target = handle_cross_orientation( - col, - row, - panel, - slot, - drop_target, - insert_index, - "horizontal", - ); - } - - drop_target.is_edge = true; - } else if (is_row_edge) { - const is_before = drop_target.row_offset < SPLIT_EDGE_TOLERANCE; - if (drop_target.orientation === "vertical") { - drop_target = handle_matching_orientation( - col, - row, - panel, - slot, - drop_target, - is_before, - ); + const new_panel = insert_child(panel, slot, [insert_index]); + drop_target = calculate_intersection(col, row, new_panel, false); } else { - const insert_index = is_before ? 0 : 1; - drop_target = handle_cross_orientation( - col, - row, - panel, - slot, - drop_target, + const path_without_last = drop_target.path.slice(0, -1); + const last_index = drop_target.path[drop_target.path.length - 1]; + const insert_index = is_before ? last_index : last_index + 1; + const new_panel = insert_child(panel, slot, [ + ...path_without_last, insert_index, - "vertical", - ); - } + ]); - drop_target.is_edge = true; + drop_target = calculate_intersection(col, row, new_panel, false); + } + } else { + const path = [...drop_target.path, is_before ? 0 : 1]; + const new_panel = insert_child(panel, slot, path, axis_orientation); + drop_target = calculate_intersection(col, row, new_panel, false); } + drop_target.is_edge = true; return drop_target; } diff --git a/src/common/calculate_intersect.ts b/src/common/calculate_intersect.ts index 06c4172..f593f0e 100644 --- a/src/common/calculate_intersect.ts +++ b/src/common/calculate_intersect.ts @@ -86,6 +86,7 @@ function calculate_intersection_recursive( const column_offset = (column - view_window.col_start) / (view_window.col_end - view_window.col_start); + const row_offset = (row - view_window.row_start) / (view_window.row_end - view_window.row_start); @@ -98,6 +99,8 @@ function calculate_intersection_recursive( path: path, view_window: view_window, is_edge: false, + column, + row, column_offset, row_offset, orientation: parent_orientation || "horizontal", diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 0000000..f14717f --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,46 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import type { OverlayMode } from "./layout_config"; + +/** + * The minimum number of pixels the mouse must move to be considered a drag. + */ +export const MIN_DRAG_DISTANCE = 10; + +/** + * Class name to use for child elements in overlay position (dragging). + */ +export const OVERLAY_CLASSNAME = "overlay"; + +/** + * The percentage of the maximum resize distance that will be clamped. + * + */ +export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15; + +/** + * Threshold from panel edge that is considered a split vs drop action. + */ +export const SPLIT_EDGE_TOLERANCE = 0.25; + +/** + * Tolerance threshold for considering two grid track positions as identical. + * + * When collecting and deduplicating track positions, any positions closer than + * this value are treated as the same position to avoid redundant grid tracks. + */ +export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.001; + +/** + * The overlay default behavior. + */ +export const OVERLAY_DEFAULT: OverlayMode = "absolute"; diff --git a/src/common/flatten.ts b/src/common/flatten.ts index 7cc1328..a0e3a71 100644 --- a/src/common/flatten.ts +++ b/src/common/flatten.ts @@ -24,6 +24,7 @@ import type { Layout } from "./layout_config.ts"; */ export function flatten(layout: Layout): Layout { if (layout.type === "child-panel") { + layout.selected = layout.selected || 0; return layout; } diff --git a/src/common/generate_grid.ts b/src/common/generate_grid.ts index 9afa1d7..a6780d2 100644 --- a/src/common/generate_grid.ts +++ b/src/common/generate_grid.ts @@ -9,7 +9,8 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { GRID_TRACK_COLLAPSE_TOLERANCE, type Layout } from "./layout_config.ts"; +import { GRID_TRACK_COLLAPSE_TOLERANCE } from "./constants.ts"; +import type { Layout } from "./layout_config.ts"; import { remove_child } from "./remove_child.ts"; interface GridCell { diff --git a/src/common/generate_overlay.ts b/src/common/generate_overlay.ts index 56f2cf8..2845ae7 100644 --- a/src/common/generate_overlay.ts +++ b/src/common/generate_overlay.ts @@ -11,15 +11,18 @@ import type { LayoutPath } from "./layout_config"; -export function updateOverlaySheet({ - view_window: { row_start, row_end, col_start, col_end }, - box, -}: LayoutPath) { +export function updateOverlaySheet( + slot: string, + { + view_window: { row_start, row_end, col_start, col_end }, + box, + }: LayoutPath, +) { const margin = 0; const top = row_start * box.height + margin / 2; const left = col_start * box.width + margin / 2; const height = (row_end - row_start) * box.height - margin; const width = (col_end - col_start) * box.width - margin; const css = `position:absolute!important;z-index:1;top:${top}px;left:${left}px;height:${height}px;width:${width}px;`; - return `::slotted(:not([slot])){${css}}`; + return `::slotted([slot="${slot}"]){${css}}`; } diff --git a/src/common/insert_child.ts b/src/common/insert_child.ts index 07b0fbe..a6ca2b1 100644 --- a/src/common/insert_child.ts +++ b/src/common/insert_child.ts @@ -77,6 +77,7 @@ export function insert_child( if (restPath.length === 0 || index === panel.children.length) { if (is_edge && panel.children[index]?.type === "child-panel") { panel.children[index].child.unshift(child); + panel.children[index].selected = 0; return panel; } diff --git a/src/common/layout_config.ts b/src/common/layout_config.ts index 5ab633c..11a8e06 100644 --- a/src/common/layout_config.ts +++ b/src/common/layout_config.ts @@ -9,34 +9,10 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -/** - * The percentage of the maximum resize distance that will be clamped. - * - */ -export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15; - -/** - * Threshold from panel edge that is considered a split vs drop action. - */ -export const SPLIT_EDGE_TOLERANCE = 0.25; - -/** - * Tolerance threshold for considering two grid track positions as identical. - * - * When collecting and deduplicating track positions, any positions closer than - * this value are treated as the same position to avoid redundant grid tracks. - */ -export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.001; - -/** - * The overlay default behavior. - */ -export const OVERLAY_DEFAULT: OverlayMode = "absolute"; - /** * The overlay behavior type. */ -export type OverlayMode = "grid" | "absolute" | "interactive"; +export type OverlayMode = "grid" | "absolute"; /** * The representation of a CSS grid, in JSON form. @@ -105,6 +81,8 @@ export interface LayoutPath { panel: TabLayout; path: number[]; view_window: ViewWindow; + column: number; + row: number; column_offset: number; row_offset: number; orientation: Orientation; @@ -125,7 +103,7 @@ export function* iter_panel_children(panel: Layout): Generator { yield* iter_panel_children(child); } } else { - yield* panel.child; + yield panel.child[panel.selected || 0]; } } diff --git a/src/common/redistribute_panel_sizes.ts b/src/common/redistribute_panel_sizes.ts index d458aeb..a39bc09 100644 --- a/src/common/redistribute_panel_sizes.ts +++ b/src/common/redistribute_panel_sizes.ts @@ -9,10 +9,8 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { - MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD, - type Layout, -} from "./layout_config.ts"; +import { MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD } from "./constants.ts"; +import type { Layout } from "./layout_config.ts"; /** * Adjusts panel sizes during a drag operation on a divider. diff --git a/src/common/remove_child.ts b/src/common/remove_child.ts index 61ba3b3..dcf5ec9 100644 --- a/src/common/remove_child.ts +++ b/src/common/remove_child.ts @@ -36,6 +36,7 @@ export function remove_child(panel: Layout, child: string): Layout { child: newChild, }; } + return structuredClone(panel); } diff --git a/src/extensions.ts b/src/extensions.ts index 836b51c..269cb9a 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -11,7 +11,7 @@ import { RegularLayout } from "./regular-layout.ts"; import { RegularLayoutFrame } from "./regular-layout-frame.ts"; -import { Layout } from "./common/layout_config.ts"; +import type { Layout } from "./common/layout_config.ts"; customElements.define("regular-layout", RegularLayout); customElements.define("regular-layout-frame", RegularLayoutFrame); @@ -45,20 +45,24 @@ declare global { options?: { signal: AbortSignal }, ): void; - removeEventListener(name: "regular-layout-update", cb: any): void; + addEventListener( + name: "regular-layout-before-update", + cb: (e: RegularLayoutEvent) => void, + options?: { signal: AbortSignal }, + ): void; + + removeEventListener( + name: "regular-layout-update", + cb: (e: RegularLayoutEvent) => void, + ): void; + + removeEventListener( + name: "regular-layout-before-update", + cb: (e: RegularLayoutEvent) => void, + ): void; } } export interface RegularLayoutEvent extends CustomEvent { detail: Layout; } - -export interface PerspectiveViewerElementExt { - addEventListener( - name: "regular-layout-update", - cb: (e: RegularLayoutEvent) => void, - options?: { signal: AbortSignal }, - ): void; - - removeEventListener(name: "regular-layout-update", cb: any): void; -} diff --git a/src/regular-layout-frame.ts b/src/regular-layout-frame.ts index ffdc4c2..ac8cd0c 100644 --- a/src/regular-layout-frame.ts +++ b/src/regular-layout-frame.ts @@ -9,16 +9,17 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import type { Layout, LayoutPath, TabLayout } from "./common/layout_config.ts"; +import { MIN_DRAG_DISTANCE, OVERLAY_CLASSNAME } from "./common/constants.ts"; +import type { LayoutPath, TabLayout } from "./common/layout_config.ts"; import type { RegularLayoutEvent } from "./extensions.ts"; import type { RegularLayout } from "./regular-layout.ts"; -const CSS = ` +const CSS = (className: string) => ` :host{--titlebar--height:24px;box-sizing:border-box} -:host([slot]){margin-top:calc(var(--titlebar--height) + 3px)!important;} -:host([slot])::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit} -:host([slot])::part(titlebar){height:var(--titlebar--height);margin-top:calc(0px - var(--titlebar--height));user-select: none;} -:host([slot])::part(body){flex:1 1 auto;} +:host(:not(.${className})){margin-top:calc(var(--titlebar--height) + 3px)!important;} +:host(:not(.${className}))::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit} +:host(:not(.${className}))::part(titlebar){height:var(--titlebar--height);margin-top:calc(0px - var(--titlebar--height));user-select: none;} +:host(:not(.${className}))::part(body){flex:1 1 auto;} `; const HTML_TEMPLATE = ``; @@ -56,10 +57,11 @@ export class RegularLayoutFrame extends HTMLElement { private _drag_state: LayoutPath | null = null; private _drag_moved: boolean = false; private _tab_to_index_map: WeakMap = new WeakMap(); + private _tab_panel_state: TabLayout | null = null; constructor() { super(); this._container_sheet = new CSSStyleSheet(); - this._container_sheet.replaceSync(CSS); + this._container_sheet.replaceSync(CSS(OVERLAY_CLASSNAME)); this._shadowRoot = this.attachShadow({ mode: "open" }); this._shadowRoot.adoptedStyleSheets = [this._container_sheet]; } @@ -73,6 +75,10 @@ export class RegularLayoutFrame extends HTMLElement { this._header.addEventListener("pointerup", this.onPointerUp); this._header.addEventListener("lostpointercapture", this.onPointerLost); this._layout.addEventListener("regular-layout-update", this.drawTabs); + this._layout.addEventListener( + "regular-layout-before-update", + this.drawTabs, + ); } disconnectedCallback() { @@ -81,23 +87,12 @@ export class RegularLayoutFrame extends HTMLElement { this._header.removeEventListener("pointerup", this.onPointerUp); this._header.removeEventListener("lostpointercapture", this.onPointerLost); this._layout.removeEventListener("regular-layout-update", this.drawTabs); + this._layout.removeEventListener( + "regular-layout-before-update", + this.drawTabs, + ); } - private drawTabs = (event: RegularLayoutEvent) => { - const slot = this.getAttribute("slot"); - const new_panel = event.detail; - if (slot) { - const result = this._layout.getPanel(slot, new_panel); - this._header.textContent = ""; - if (result) { - for (let e = 0; e < result.child.length; e++) { - const tab = this.createTab(new_panel, result, e); - this._header.appendChild(tab); - } - } - } - }; - private onPointerDown = (event: PointerEvent): void => { const elem = event.target as HTMLDivElement; if (elem.part.contains("tab")) { @@ -107,9 +102,8 @@ export class RegularLayoutFrame extends HTMLElement { ); if (this._drag_state) { - // event.preventDefault(); - // event.stopImmediatePropagation(); this._header.setPointerCapture(event.pointerId); + event.preventDefault(); const last_index = this._drag_state.path.length - 1; const selected = this._tab_to_index_map.get(elem); if (selected) { @@ -121,11 +115,24 @@ export class RegularLayoutFrame extends HTMLElement { private onPointerMove = (event: PointerEvent): void => { if (this._drag_state) { + // Only initiate a drag if the cursor has moved sufficiently. + if (!this._drag_moved) { + const [current_col, current_row, box] = + this._layout.relativeCoordinates(event.clientX, event.clientY); + + const dx = (current_col - this._drag_state.column) * box.width; + const dy = (current_row - this._drag_state.row) * box.height; + if (Math.sqrt(dx * dx + dy * dy) <= MIN_DRAG_DISTANCE) { + return; + } + } + this._drag_moved = true; this._layout.setOverlayState( event.clientX, event.clientY, this._drag_state, + OVERLAY_CLASSNAME, ); } }; @@ -136,6 +143,7 @@ export class RegularLayoutFrame extends HTMLElement { event.clientX, event.clientY, this._drag_state, + OVERLAY_CLASSNAME, ); } @@ -156,25 +164,74 @@ export class RegularLayoutFrame extends HTMLElement { this._drag_moved = false; }; - private createTab = ( - layout: Layout, - result: TabLayout, - index: number, - ): HTMLDivElement => { - const selected = result.selected || 0; + private drawTabs = (event: RegularLayoutEvent) => { + const slot = this.assignedSlot; + if (!slot) { + return; + } + + const new_panel = event.detail; + const new_tab_panel = this._layout.getPanel(slot.name, new_panel); + if (!new_tab_panel) { + return; + } + + for (let i = 0; i < new_tab_panel.child.length; i++) { + if (i >= this._header.children.length) { + const new_tab = this.createTab(new_tab_panel, i); + this._header.appendChild(new_tab); + } else { + const tab_changed = + (i === new_tab_panel.selected) !== + (i === this._tab_panel_state?.selected); + + const tab = this._header.children[i] as HTMLDivElement; + const index_changed = + tab_changed || + this._tab_panel_state?.child[i] !== new_tab_panel.child[i]; + + if (index_changed) { + const new_tab = this.createTab(new_tab_panel, i); + this._header.replaceChild(new_tab, tab); + } + } + } + + const last_index = new_tab_panel.child.length; + for (let j = this._header.children.length - 1; j >= last_index; j--) { + this._header.removeChild(this._header.children[j]); + } + + this._tab_panel_state = new_tab_panel; + }; + + private createTab = (tab_panel: TabLayout, index: number): HTMLDivElement => { + const selected = tab_panel.selected || 0; const tab = document.createElement("div"); this._tab_to_index_map.set(tab, index); - tab.textContent = result.child[index] || ""; + tab.textContent = tab_panel.child[index] || ""; if (index === selected) { tab.setAttribute("part", "tab active-tab"); } else { tab.setAttribute("part", "tab"); - tab.addEventListener("pointerdown", (_: PointerEvent) => { - result.selected = index; - this._layout.restore(layout); - }); + tab.addEventListener("pointerdown", (_) => + this.onTabClick(tab_panel, index), + ); } return tab; }; + + private onTabClick = (tab_panel: TabLayout, index: number) => { + const new_layout = this._layout.save(); + const new_tab_panel = this._layout.getPanel( + tab_panel.child[index], + new_layout, + ); + + if (new_tab_panel) { + new_tab_panel.selected = index; + this._layout.restore(new_layout); + } + }; } diff --git a/src/regular-layout.ts b/src/regular-layout.ts index 80e2268..38e104b 100644 --- a/src/regular-layout.ts +++ b/src/regular-layout.ts @@ -16,25 +16,23 @@ * @packageDocumentation */ -import { - EMPTY_PANEL, - iter_panel_children, - OVERLAY_DEFAULT, -} from "./common/layout_config.ts"; +import { EMPTY_PANEL, iter_panel_children } from "./common/layout_config.ts"; import { create_css_grid_layout } from "./common/generate_grid.ts"; import type { LayoutPath, Layout, LayoutDivider, TabLayout, + OverlayMode, } from "./common/layout_config.ts"; import { calculate_intersection } from "./common/calculate_intersect.ts"; import { remove_child } from "./common/remove_child.ts"; import { insert_child } from "./common/insert_child.ts"; import { redistribute_panel_sizes } from "./common/redistribute_panel_sizes.ts"; import { updateOverlaySheet } from "./common/generate_overlay.ts"; -import { calculate_split } from "./common/calculate_split.ts"; +import { calculate_edge } from "./common/calculate_edge.ts"; import { flatten } from "./common/flatten.ts"; +import { OVERLAY_CLASSNAME, OVERLAY_DEFAULT } from "./common/constants.ts"; /** * A Web Component that provides a resizable panel layout system. @@ -86,9 +84,6 @@ export class RegularLayout extends HTMLElement { this._shadowRoot.adoptedStyleSheets = [this._stylesheet]; this._shadowRoot.appendChild(this._unslotted_slot); this._slots = new Map(); - this.onPointerDown = this.onPointerDown.bind(this); - this.onPointerMove = this.onPointerMove.bind(this); - this.onPointerUp = this.onPointerUp.bind(this); } connectedCallback() { @@ -109,46 +104,50 @@ export class RegularLayout extends HTMLElement { * * @param x - X coordinate in screen pixels. * @param y - Y coordinate in screen pixels. - * @param layoutPath - Layout path containing the slot identifier. - * @param mode - Overlay rendering mode: "grid" highlights the target, - * "absolute" positions the panel absolutely, "interactive" updates the - * actual layout in real-time. Defaults to "absolute". + * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`) + * which points to the drag element in the current layout. + * @param className - The CSS class name to use for the overlay panel + * (defaults to "overlay"). + * @param mode - Overlay rendering mode: "grid" uses CSS grid to position + * the target, "absolute" positions the panel absolutely. Defaults to + * "absolute". */ setOverlayState = ( x: number, y: number, { slot }: LayoutPath, - mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT, + className: string = OVERLAY_CLASSNAME, + mode: OverlayMode = OVERLAY_DEFAULT, ) => { let panel = this._panel; if (mode === "absolute") { panel = remove_child(panel, slot); - this._slots.get(slot)?.assignedElements()[0]?.removeAttribute("slot"); + this.updateSlots(panel, slot); + this._slots.get(slot)?.assignedElements()[0]?.classList.add(className); } const [col, row, box] = this.relativeCoordinates(x, y); let drop_target = calculate_intersection(col, row, panel, false); if (drop_target) { - drop_target = calculate_split(col, row, panel, slot, drop_target); - if (mode === "interactive") { - let new_panel = remove_child(this._panel, slot); - new_panel = flatten(insert_child(new_panel, slot, drop_target.path)); - const css = create_css_grid_layout(new_panel); - this._stylesheet.replaceSync(css); - } else if (mode === "grid") { + drop_target = calculate_edge(col, row, panel, slot, drop_target); + if (mode === "grid") { const path: [string, string] = [slot, drop_target.slot]; const css = create_css_grid_layout(this._panel, false, path); this._stylesheet.replaceSync(css); } else if (mode === "absolute") { - const css = `${create_css_grid_layout(panel)}\n${updateOverlaySheet({ ...drop_target, box })}`; - this._stylesheet.replaceSync(css); + const grid_css = create_css_grid_layout(panel); + const overlay_css = updateOverlaySheet(slot, { ...drop_target, box }); + this._stylesheet.replaceSync([grid_css, overlay_css].join("\n")); } } else { const css = `${create_css_grid_layout(panel)}}`; this._stylesheet.replaceSync(css); } - const event = new CustomEvent("regular-layout-update", { detail: panel }); + const event = new CustomEvent("regular-layout-before-update", { + detail: panel, + }); + this.dispatchEvent(event); }; @@ -157,29 +156,33 @@ export class RegularLayout extends HTMLElement { * * @param x - X coordinate in screen pixels. * @param y - Y coordinate in screen pixels. - * @param layout_path - Layout path containing the slot identifier. + * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`) + * which points to the drag element in the current layout. + * @param className - The CSS class name to use for the overlay panel + * (defaults to "overlay"). * @param mode - Overlay rendering mode that was used, must match the mode - * passed to `setOverlayState`. Defaults to "absolute". + * passed to `setOverlayState`. Defaults to "absolute". */ clearOverlayState = ( x: number, y: number, drag_target: LayoutPath, - mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT, + className: string = OVERLAY_CLASSNAME, + mode: OverlayMode = OVERLAY_DEFAULT, ) => { let panel = this._panel; if (mode === "absolute") { panel = remove_child(panel, drag_target.slot); - this._unslotted_slot - .assignedElements()[0] - ?.setAttribute("slot", drag_target.slot); + this._slots + .get(drag_target.slot) + ?.assignedElements()[0] + ?.classList.remove(className); } const [col, row, _] = this.relativeCoordinates(x, y); let drop_target = calculate_intersection(col, row, panel, false); if (drop_target) { - // TODO I think I only need the new path here? - drop_target = calculate_split( + drop_target = calculate_edge( col, row, panel, @@ -189,7 +192,6 @@ export class RegularLayout extends HTMLElement { } const { path, orientation } = drop_target ? drop_target : drag_target; - this.restore( insert_child( panel, @@ -220,6 +222,13 @@ export class RegularLayout extends HTMLElement { this.restore(remove_child(this._panel, name)); }; + /** + * Retrieves a panel by name from the layout tree. + * + * @param name - Name of the panel to find. + * @param layout - Optional layout tree to search in (defaults to current layout). + * @returns The TabLayout containing the panel if found, null otherwise. + */ getPanel = (name: string, layout: Layout = this._panel): TabLayout | null => { if (layout.type === "child-panel") { if (layout.child.includes(name)) { @@ -240,7 +249,6 @@ export class RegularLayout extends HTMLElement { /** * Determines which panel is at a given screen coordinate. - * Useful for drag-and-drop operations or custom interactions. * * @param column - X coordinate in screen pixels. * @param row - Y coordinate in screen pixels. @@ -260,6 +268,9 @@ export class RegularLayout extends HTMLElement { return null; }; + /** + * Clears the entire layout, unslotting all panels. + */ clear = () => { this.restore(EMPTY_PANEL); }; @@ -278,28 +289,13 @@ export class RegularLayout extends HTMLElement { */ restore = (layout: Layout, _is_flattened: boolean = false) => { this._panel = !_is_flattened ? flatten(layout) : layout; - const css = create_css_grid_layout(layout); + const css = create_css_grid_layout(this._panel); this._stylesheet.replaceSync(css); - const old = new Set(this._slots.keys()); - for (const name of iter_panel_children(layout)) { - old.delete(name); - if (!this._slots.has(name)) { - const slot = document.createElement("slot"); - slot.setAttribute("name", name); - this._shadowRoot.appendChild(slot); - this._slots.set(name, slot); - } - } - - for (const key of old) { - const child = this._slots.get(key); - if (child) { - this._shadowRoot.removeChild(child); - this._slots.delete(key); - } - } + this.updateSlots(this._panel); + const event = new CustomEvent("regular-layout-update", { + detail: this._panel, + }); - const event = new CustomEvent("regular-layout-update", { detail: layout }); this.dispatchEvent(event); }; @@ -319,7 +315,20 @@ export class RegularLayout extends HTMLElement { return structuredClone(this._panel); }; - private relativeCoordinates = ( + /** + * Converts screen coordinates to relative layout coordinates. + * + * Transforms absolute pixel positions into normalized coordinates (0-1 range) + * relative to the layout's bounding box. + * + * @param clientX - X coordinate in screen pixels (client space). + * @param clientY - Y coordinate in screen pixels (client space). + * @returns A tuple containing: + * - col: Normalized X coordinate (0 = left edge, 1 = right edge) + * - row: Normalized Y coordinate (0 = top edge, 1 = bottom edge) + * - box: The layout element's bounding rectangle + */ + relativeCoordinates = ( clientX: number, clientY: number, ): [number, number, DOMRect] => { @@ -329,6 +338,31 @@ export class RegularLayout extends HTMLElement { return [col, row, box]; }; + private updateSlots = (layout: Layout, overlay?: string) => { + const old = new Set(this._slots.keys()); + if (overlay) { + old.delete(overlay); + } + + for (const name of iter_panel_children(layout)) { + old.delete(name); + if (!this._slots.has(name)) { + const slot = document.createElement("slot"); + slot.setAttribute("name", name); + this._shadowRoot.appendChild(slot); + this._slots.set(name, slot); + } + } + + for (const key of old) { + const child = this._slots.get(key); + if (child) { + this._shadowRoot.removeChild(child); + this._slots.delete(key); + } + } + }; + private onPointerDown = (event: PointerEvent) => { if (event.target === this) { const [col, row] = this.relativeCoordinates(event.clientX, event.clientY); @@ -336,8 +370,7 @@ export class RegularLayout extends HTMLElement { if (hit && hit.type !== "layout-path") { this._dragPath = [hit, col, row]; this.setPointerCapture(event.pointerId); - // event.preventDefault(); - // event.stopImmediatePropagation(); + event.preventDefault(); } } }; diff --git a/tests/helpers/fixtures.ts b/tests/helpers/fixtures.ts index 6d80728..9caeb9c 100644 --- a/tests/helpers/fixtures.ts +++ b/tests/helpers/fixtures.ts @@ -1,31 +1,15 @@ -import type { Layout } from "../../src/common/layout_config.ts"; +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -export const TEST_PANEL: Layout = { - type: "split-panel", - children: [ - { - type: "split-panel", - children: [ - { - type: "child-panel", - child: ["AAA"], - }, - { - type: "child-panel", - child: ["BBB"], - }, - ], - sizes: [0.3, 0.7], - orientation: "vertical", - }, - { - type: "child-panel", - child: ["CCC"], - }, - ], - sizes: [0.6, 0.4], - orientation: "horizontal", -}; +import type { Layout } from "../../src/common/layout_config.ts"; /** * Common layout fixtures for testing @@ -47,6 +31,7 @@ export const LAYOUTS = { SINGLE_TABS: { type: "child-panel", child: ["AAA", "BBB", "CCC"], + selected: 0, } as Layout, /** Two panels horizontal split (30/70) */ @@ -105,7 +90,32 @@ export const LAYOUTS = { } as Layout, /** Nested layout: horizontal split with left side vertical split (60/40 outer, 30/70 inner) */ - NESTED_BASIC: TEST_PANEL, + NESTED_BASIC: { + type: "split-panel", + children: [ + { + type: "split-panel", + children: [ + { + type: "child-panel", + child: ["AAA"], + }, + { + type: "child-panel", + child: ["BBB"], + }, + ], + sizes: [0.3, 0.7], + orientation: "vertical", + }, + { + type: "child-panel", + child: ["CCC"], + }, + ], + sizes: [0.6, 0.4], + orientation: "horizontal", + } as Layout, /** Nested layout: vertical split with nested horizontal */ NESTED_VERTICAL_OUTER: { diff --git a/tests/helpers/integration.ts b/tests/helpers/integration.ts index 02835d5..2f84c97 100644 --- a/tests/helpers/integration.ts +++ b/tests/helpers/integration.ts @@ -24,7 +24,6 @@ export async function setupLayout( ): Promise { await page.goto("/examples/index.html"); await page.waitForSelector("regular-layout"); - if (initialLayout) { await restoreLayout(page, initialLayout); } @@ -50,29 +49,6 @@ export async function restoreLayout(page: Page, state: Layout): Promise { }, state); } -/** - * Restores a layout and verifies it matches the expected state. - */ -export async function restoreAndVerify( - page: Page, - state: Layout, -): Promise { - await restoreLayout(page, state); - const restored = await saveLayout(page); - expect(restored).toStrictEqual(state); -} - -/** - * Saves the current layout state and verifies it matches the expected state. - */ -export async function expectLayoutState( - page: Page, - expectedState: Layout, -): Promise { - const currentState = await saveLayout(page); - expect(currentState).toStrictEqual(expectedState); -} - /** * Returns an array of slot names from the layout's shadow DOM. */ @@ -97,7 +73,6 @@ export async function expectSlots( }, ): Promise { const slots = await getSlots(page); - if (options.contains) { for (const slot of options.contains) { expect(slots).toContain(slot); @@ -156,3 +131,120 @@ export async function removePanel( layout?.removePanel(p as string); }, pathOrName); } + +/** + * Calls setOverlayState with the given coordinates and options. + */ +export async function setOverlayState( + page: Page, + x: number, + y: number, + slot: string, + className?: string, + mode?: "grid" | "absolute", +): Promise { + await page.evaluate( + ({ x, y, slot, className, mode }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, { ...layoutPath, slot }, className, mode); + } + }, + { x, y, slot, className, mode }, + ); +} + +/** + * Calls clearOverlayState with the given coordinates and options. + */ +export async function clearOverlayState( + page: Page, + x: number, + y: number, + slot: string, + className?: string, + mode?: "grid" | "absolute", +): Promise { + await page.evaluate( + ({ x, y, slot, className, mode }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.clearOverlayState( + x, + y, + { ...layoutPath, slot }, + className, + mode, + ); + } + }, + { x, y, slot, className, mode }, + ); +} + +/** + * Gets the computed CSS for a slot element. + */ +export async function getSlotCSS( + page: Page, + slotName: string, +): Promise<{ + gridArea?: string; + display?: string; +}> { + return await page.evaluate((name) => { + const layout = document.querySelector("regular-layout"); + const slot = layout?.shadowRoot?.querySelector(`slot[name="${name}"]`); + if (!slot) return {}; + + const computedStyle = window.getComputedStyle(slot); + return { + gridArea: computedStyle.gridArea, + display: computedStyle.display, + }; + }, slotName); +} + +/** + * Checks if an element has a specific CSS class. + */ +export async function hasClass( + page: Page, + slotName: string, + className: string, +): Promise { + return await page.evaluate( + ({ name, className }) => { + const layout = document.querySelector("regular-layout"); + const slot = layout?.shadowRoot?.querySelector( + `slot[name="${name}"]`, + ) as HTMLSlotElement | null; + const element = slot?.assignedElements()[0]; + return element?.classList.contains(className) || false; + }, + { name: slotName, className }, + ); +} + +/** + * Gets the layout bounds for testing overlay coordinates. + */ +export async function getLayoutBounds(page: Page): Promise<{ + x: number; + y: number; + width: number; + height: number; +}> { + return await page.evaluate(() => { + const layout = document.querySelector("regular-layout"); + const rect = layout?.getBoundingClientRect(); + return { + x: rect?.left || 0, + y: rect?.top || 0, + width: rect?.width || 0, + height: rect?.height || 0, + }; + }); +} diff --git a/tests/integration/insert-panel.spec.ts b/tests/integration/insert-panel.spec.ts index 265f4cd..669ecfe 100644 --- a/tests/integration/insert-panel.spec.ts +++ b/tests/integration/insert-panel.spec.ts @@ -14,7 +14,6 @@ import type { Layout } from "../../dist/index.js"; import { setupLayout, saveLayout, - expectLayoutState, expectSlots, insertPanel, } from "../helpers/integration.ts"; @@ -22,11 +21,19 @@ import { LAYOUTS } from "../helpers/fixtures.ts"; test("should insert a single panel into an empty layout", async ({ page }) => { await setupLayout(page, LAYOUTS.SINGLE_AAA); - await expectLayoutState(page, LAYOUTS.SINGLE_AAA); + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ + type: "child-panel", + child: ["AAA"], + selected: 0, + }); + await insertPanel(page, "BBB", []); - await expectLayoutState(page, { + const currentState2 = await saveLayout(page); + expect(currentState2).toStrictEqual({ type: "child-panel", child: ["BBB", "AAA"], + selected: 0, }); await expectSlots(page, { contains: ["BBB"] }); @@ -45,14 +52,17 @@ test("should insert panel at specific path in split panel", async ({ { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, { type: "child-panel", child: ["BBB"], + selected: 0, }, ], sizes: [0.3333333333333333, 0.3333333333333333, 0.3333333333333333], @@ -88,14 +98,17 @@ test("should insert panel into nested split panel", async ({ page }) => { { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "child-panel", child: ["BBB"], + selected: 0, }, { type: "child-panel", child: ["DDD"], + selected: 0, }, ], sizes: [0.3333333333333333, 0.3333333333333333, 0.3333333333333333], @@ -103,6 +116,7 @@ test("should insert panel into nested split panel", async ({ page }) => { { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.6, 0.4], @@ -137,6 +151,7 @@ test("should split existing panel when inserting at deeper path", async ({ { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "split-panel", @@ -145,10 +160,12 @@ test("should split existing panel when inserting at deeper path", async ({ { type: "child-panel", child: ["BBB"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.5, 0.5], diff --git a/tests/integration/overlay-absolute.spec.ts b/tests/integration/overlay-absolute.spec.ts new file mode 100644 index 0000000..3ed251e --- /dev/null +++ b/tests/integration/overlay-absolute.spec.ts @@ -0,0 +1,107 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { expect, test } from "@playwright/test"; +import { + setupLayout, + getLayoutBounds, + hasClass, +} from "../helpers/integration.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; + +test("should apply overlay class to dragged panel in absolute mode", async ({ + page, +}) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + + // Calculate center of AAA panel (left half) + const x = bounds.x + bounds.width * 0.25; + const y = bounds.y + bounds.height * 0.5; + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "absolute"); + } + }, + { x, y }, + ); + + // Verify AAA panel has overlay class + const hasOverlayClass = await hasClass(page, "AAA", "overlay"); + expect(hasOverlayClass).toBe(true); +}); + +test("should dispatch regular-layout-update event in absolute mode", async ({ + page, +}) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + const x = bounds.x + bounds.width * 0.25; + const y = bounds.y + bounds.height * 0.5; + const eventReceived = await page.evaluate( + ({ x, y }) => { + return new Promise((resolve) => { + const layout = document.querySelector("regular-layout"); + layout?.addEventListener( + "regular-layout-before-update", + () => { + resolve(true); + }, + { once: true }, + ); + + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "absolute"); + } else { + resolve(false); + } + }); + }, + { x, y }, + ); + + expect(eventReceived).toBe(true); +}); + +test("should handle custom className in absolute mode", async ({ page }) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + + const x = bounds.x + bounds.width * 0.25; + const y = bounds.y + bounds.height * 0.5; + + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState( + x, + y, + layoutPath, + "custom-drag-class", + "absolute", + ); + } + }, + { x, y }, + ); + + const hasCustomClass = await hasClass(page, "AAA", "custom-drag-class"); + expect(hasCustomClass).toBe(true); + + const hasDefaultClass = await hasClass(page, "AAA", "overlay"); + expect(hasDefaultClass).toBe(false); +}); diff --git a/tests/integration/overlay-edge-detection.ts b/tests/integration/overlay-edge-detection.ts new file mode 100644 index 0000000..565bd65 --- /dev/null +++ b/tests/integration/overlay-edge-detection.ts @@ -0,0 +1,64 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { expect, test } from "@playwright/test"; +import { + getLayoutBounds, + hasClass, + setupLayout, +} from "../helpers/integration.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; + +test("should handle overlay near top edge of panel", async ({ page }) => { + await setupLayout(page, LAYOUTS.TWO_VERTICAL); + const bounds = await getLayoutBounds(page); + + // Near top edge of AAA panel + const x = bounds.x + bounds.width * 0.5; + const y = bounds.y + bounds.height * 0.1; + + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "absolute"); + } + }, + { x, y }, + ); + + const hasOverlayClass = await hasClass(page, "AAA", "overlay"); + expect(hasOverlayClass).toBe(true); +}); + +test("should handle overlay near bottom edge of panel", async ({ page }) => { + await setupLayout(page, LAYOUTS.TWO_VERTICAL); + const bounds = await getLayoutBounds(page); + + // Near bottom edge of BBB panel + const x = bounds.x + bounds.width * 0.5; + const y = bounds.y + bounds.height * 0.9; + + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "absolute"); + } + }, + { x, y }, + ); + + const hasOverlayClass = await hasClass(page, "BBB", "overlay"); + expect(hasOverlayClass).toBe(true); +}); diff --git a/tests/integration/overlay-grid.spec.ts b/tests/integration/overlay-grid.spec.ts new file mode 100644 index 0000000..85ce468 --- /dev/null +++ b/tests/integration/overlay-grid.spec.ts @@ -0,0 +1,102 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { expect, test } from "@playwright/test"; +import { + setupLayout, + getLayoutBounds, + hasClass, +} from "../helpers/integration.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; + +test("should not apply overlay class in grid mode", async ({ page }) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + + const x = bounds.x + bounds.width * 0.25; + const y = bounds.y + bounds.height * 0.5; + + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "grid"); + } + }, + { x, y }, + ); + + const hasOverlayClass = await hasClass(page, "AAA", "overlay"); + expect(hasOverlayClass).toBe(false); +}); + +test("should update CSS with grid preview in grid mode", async ({ page }) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + + const x = bounds.x + bounds.width * 0.75; + const y = bounds.y + bounds.height * 0.5; + + await page.evaluate( + ({ x, y }) => { + const layout = document.querySelector("regular-layout"); + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "grid"); + } + }, + { x, y }, + ); + + const cssRules = await page.evaluate(() => { + const layout = document.querySelector("regular-layout"); + const stylesheet = layout?.shadowRoot?.adoptedStyleSheets[0]; + return stylesheet?.cssRules.length || 0; + }); + + expect(cssRules).toBeGreaterThan(0); +}); + +test("should dispatch regular-layout-update event in grid mode", async ({ + page, +}) => { + await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); + const bounds = await getLayoutBounds(page); + + const x = bounds.x + bounds.width * 0.25; + const y = bounds.y + bounds.height * 0.5; + + const eventReceived = await page.evaluate( + ({ x, y }) => { + return new Promise((resolve) => { + const layout = document.querySelector("regular-layout"); + layout?.addEventListener( + "regular-layout-before-update", + () => { + resolve(true); + }, + { once: true }, + ); + + const layoutPath = layout?.calculateIntersect(x, y); + if (layoutPath) { + layout?.setOverlayState(x, y, layoutPath, "overlay", "grid"); + } else { + resolve(false); + } + }); + }, + { x, y }, + ); + + expect(eventReceived).toBe(true); +}); diff --git a/tests/integration/remove-panel.spec.ts b/tests/integration/remove-panel.spec.ts index 82d2b72..52cce43 100644 --- a/tests/integration/remove-panel.spec.ts +++ b/tests/integration/remove-panel.spec.ts @@ -9,11 +9,10 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { setupLayout, saveLayout, - expectLayoutState, expectSlots, removePanel, } from "../helpers/integration.ts"; @@ -24,7 +23,13 @@ test.describe("removePanel", () => { await setupLayout(page, LAYOUTS.TWO_HORIZONTAL_EQUAL); await removePanel(page, "BBB"); - await expectLayoutState(page, LAYOUTS.SINGLE_AAA); + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ + type: "child-panel", + child: ["AAA"], + selected: 0, + }); + await expectSlots(page, { notContains: ["BBB"], contains: ["AAA"], @@ -34,17 +39,20 @@ test.describe("removePanel", () => { test("should remove panel from 3-panel layout", async ({ page }) => { await setupLayout(page, LAYOUTS.THREE_HORIZONTAL_CUSTOM); await removePanel(page, "BBB"); - await expectLayoutState(page, { + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ type: "split-panel", orientation: "horizontal", children: [ { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.28571428571428575, 0.7142857142857143], @@ -54,17 +62,20 @@ test.describe("removePanel", () => { test("should remove panel from nested layout", async ({ page }) => { await setupLayout(page, LAYOUTS.NESTED_BASIC); await removePanel(page, "AAA"); - await expectLayoutState(page, { + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ type: "split-panel", orientation: "horizontal", children: [ { type: "child-panel", child: ["BBB"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.6, 0.4], @@ -74,7 +85,8 @@ test.describe("removePanel", () => { test("should remove panel from deeply nested layout", async ({ page }) => { await setupLayout(page, LAYOUTS.DEEPLY_NESTED_ALT); await removePanel(page, "BBB"); - await expectLayoutState(page, { + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ type: "split-panel", orientation: "vertical", children: [ @@ -85,10 +97,12 @@ test.describe("removePanel", () => { { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.5, 0.5], @@ -96,6 +110,7 @@ test.describe("removePanel", () => { { type: "child-panel", child: ["DDD"], + selected: 0, }, ], sizes: [0.7, 0.3], @@ -108,7 +123,8 @@ test.describe("removePanel", () => { await setupLayout(page, LAYOUTS.THREE_HORIZONTAL_304030); await removePanel(page, "BBB"); const stateAfterRemove = await saveLayout(page); - await expectLayoutState(page, stateAfterRemove); + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual(stateAfterRemove); }); }); @@ -118,21 +134,25 @@ test.describe("tabs", () => { }) => { await setupLayout(page, LAYOUTS.THREE_HORIZONTAL_WITH_TABS); await removePanel(page, "BBB"); - await expectLayoutState(page, { + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ type: "split-panel", orientation: "horizontal", children: [ { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "child-panel", child: ["DDD", "EEE"], + selected: 0, }, { type: "child-panel", child: ["CCC"], + selected: 0, }, ], sizes: [0.2, 0.3, 0.5], diff --git a/tests/integration/resize.spec.ts b/tests/integration/resize.spec.ts index 324df72..e90437d 100644 --- a/tests/integration/resize.spec.ts +++ b/tests/integration/resize.spec.ts @@ -13,7 +13,7 @@ import { expect, test } from "@playwright/test"; import { setupLayout, saveLayout, - restoreAndVerify, + restoreLayout, dragMouse, } from "../helpers/integration.ts"; @@ -35,8 +35,12 @@ test("should resize panels by dragging dividers and preserve state with save/res expect(resizedState).not.toEqual(initialState); expect(resizedState).toHaveProperty("type", "split-panel"); expect(resizedState).toHaveProperty("sizes"); - await restoreAndVerify(page, initialState); - await restoreAndVerify(page, resizedState); + await restoreLayout(page, initialState); + const restored1 = await saveLayout(page); + expect(restored1).toStrictEqual(initialState); + await restoreLayout(page, resizedState); + const restored2 = await saveLayout(page); + expect(restored2).toStrictEqual(resizedState); }); test("should resize nested panels by dragging horizontal divider", async ({ @@ -59,7 +63,9 @@ test("should resize nested panels by dragging horizontal divider", async ({ const resizedState = await saveLayout(page); expect(resizedState).not.toEqual(initialState); - await restoreAndVerify(page, initialState); + await restoreLayout(page, initialState); + const restored = await saveLayout(page); + expect(restored).toStrictEqual(initialState); }); test("should handle multiple resize operations and save/restore cycles", async ({ @@ -81,7 +87,13 @@ test("should handle multiple resize operations and save/restore cycles", async ( expect(state1).not.toEqual(state2); expect(state2).not.toEqual(state3); expect(state1).not.toEqual(state3); - await restoreAndVerify(page, state1); - await restoreAndVerify(page, state2); - await restoreAndVerify(page, state3); + await restoreLayout(page, state1); + const restored1 = await saveLayout(page); + expect(restored1).toStrictEqual(state1); + await restoreLayout(page, state2); + const restored2 = await saveLayout(page); + expect(restored2).toStrictEqual(state2); + await restoreLayout(page, state3); + const restored3 = await saveLayout(page); + expect(restored3).toStrictEqual(state3); }); diff --git a/tests/integration/save-restore.spec.ts b/tests/integration/save-restore.spec.ts index ee51931..2f3ff58 100644 --- a/tests/integration/save-restore.spec.ts +++ b/tests/integration/save-restore.spec.ts @@ -14,10 +14,8 @@ import { setupLayout, saveLayout, restoreLayout, - expectLayoutState, getSlots, expectSlots, - restoreAndVerify, insertPanel, } from "../helpers/integration.ts"; import { LAYOUTS } from "../helpers/fixtures.ts"; @@ -25,15 +23,66 @@ import { LAYOUTS } from "../helpers/fixtures.ts"; test("should save and restore various layout types", async ({ page }) => { // Test single panel await setupLayout(page, LAYOUTS.SINGLE_AAA); - await expectLayoutState(page, LAYOUTS.SINGLE_AAA); + const currentState = await saveLayout(page); + expect(currentState).toStrictEqual({ + type: "child-panel", + child: ["AAA"], + selected: 0, + }); // Test 2-panel horizontal await restoreLayout(page, LAYOUTS.TWO_HORIZONTAL); - await expectLayoutState(page, LAYOUTS.TWO_HORIZONTAL); + const currentState2 = await saveLayout(page); + expect(currentState2).toStrictEqual({ + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "child-panel", + child: ["AAA"], + selected: 0, + }, + { + type: "child-panel", + child: ["BBB"], + selected: 0, + }, + ], + sizes: [0.3, 0.7], + }); // Test nested layout await restoreLayout(page, LAYOUTS.NESTED_BASIC); - await expectLayoutState(page, LAYOUTS.NESTED_BASIC); + const currentState3 = await saveLayout(page); + expect(currentState3).toStrictEqual({ + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "split-panel", + orientation: "vertical", + children: [ + { + type: "child-panel", + child: ["AAA"], + selected: 0, + }, + { + type: "child-panel", + child: ["BBB"], + selected: 0, + }, + ], + sizes: [0.3, 0.7], + }, + { + type: "child-panel", + child: ["CCC"], + selected: 0, + }, + ], + sizes: [0.6, 0.4], + }); }); test("should save, modify, and revert to saved state", async ({ page }) => { @@ -41,18 +90,17 @@ test("should save, modify, and revert to saved state", async ({ page }) => { await setupLayout(page, LAYOUTS.SINGLE_AAA); const saved1 = await saveLayout(page); await restoreLayout(page, LAYOUTS.SINGLE_BBB); - await restoreAndVerify(page, saved1); + await restoreLayout(page, saved1); + const restored1 = await saveLayout(page); + expect(restored1).toStrictEqual(saved1); // Complex case: nested layout await restoreLayout(page, LAYOUTS.NESTED_BASIC); const saved2 = await saveLayout(page); await restoreLayout(page, LAYOUTS.SINGLE_DDD); - await restoreAndVerify(page, saved2); -}); - -test("should save and restore a deeply nested layout", async ({ page }) => { - await setupLayout(page, LAYOUTS.DEEPLY_NESTED_ALT); - await expectLayoutState(page, LAYOUTS.DEEPLY_NESTED_ALT); + await restoreLayout(page, saved2); + const restored2 = await saveLayout(page); + expect(restored2).toStrictEqual(saved2); }); test("should save returns a deep clone, not a reference", async ({ page }) => { @@ -61,7 +109,23 @@ test("should save returns a deep clone, not a reference", async ({ page }) => { await insertPanel(page, "CCC", []); const afterModification = await saveLayout(page); expect(afterModification).not.toStrictEqual(saved); - expect(saved).toStrictEqual(LAYOUTS.TWO_HORIZONTAL_EQUAL); + expect(saved).toStrictEqual({ + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "child-panel", + child: ["AAA"], + selected: 0, + }, + { + type: "child-panel", + child: ["BBB"], + selected: 0, + }, + ], + sizes: [0.5, 0.5], + }); }); test("should restore updates shadow DOM slots correctly", async ({ page }) => { @@ -83,8 +147,31 @@ test("should restore updates shadow DOM slots correctly", async ({ page }) => { test("should save and restore preserve exact size ratios", async ({ page }) => { await setupLayout(page, LAYOUTS.THREE_HORIZONTAL_PRECISE); const saved = await saveLayout(page); - expect(saved).toStrictEqual(LAYOUTS.THREE_HORIZONTAL_PRECISE); - await restoreAndVerify(page, saved); + expect(saved).toStrictEqual({ + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "child-panel", + child: ["AAA"], + selected: 0, + }, + { + type: "child-panel", + child: ["BBB"], + selected: 0, + }, + { + type: "child-panel", + child: ["CCC"], + selected: 0, + }, + ], + sizes: [0.123456789, 0.456789123, 0.419754088], + }); + await restoreLayout(page, saved); + const restored = await saveLayout(page); + expect(restored).toStrictEqual(saved); }); test("should save and restore handle empty then populated layout", async ({ @@ -95,6 +182,10 @@ test("should save and restore handle empty then populated layout", async ({ await insertPanel(page, "BBB", []); await insertPanel(page, "CCC", []); const saved2 = await saveLayout(page); - await restoreAndVerify(page, saved1); - await restoreAndVerify(page, saved2); + await restoreLayout(page, saved1); + const restored1 = await saveLayout(page); + expect(restored1).toStrictEqual(saved1); + await restoreLayout(page, saved2); + const restored2 = await saveLayout(page); + expect(restored2).toStrictEqual(saved2); }); diff --git a/tests/integration/tabs.spec.ts b/tests/integration/tabs.spec.ts index fd533ec..a81dde0 100644 --- a/tests/integration/tabs.spec.ts +++ b/tests/integration/tabs.spec.ts @@ -21,17 +21,19 @@ test("should switch between tabs by clicking", async ({ page }) => { layoutElement?.restore(layout as Layout); }, LAYOUTS.SINGLE_TABS_WITH_SELECTED); - const getSelectedTab = async () => { - return await page.evaluate(() => { - const frame = document.querySelector('regular-layout-frame[slot="AAA"]'); + const getSelectedTab = async (slot: string) => { + return await page.evaluate((slot) => { + const frame = document.querySelector( + `regular-layout-frame[slot=${slot}]`, + ); const activeTab = frame?.shadowRoot?.querySelector( '[part~="active-tab"]', ); return activeTab?.textContent; - }); + }, slot); }; - const selectedBefore = await getSelectedTab(); + const selectedBefore = await getSelectedTab("AAA"); expect(selectedBefore).toBe("AAA"); const frameBounds = await page.evaluate(() => { const frame = document.querySelector('regular-layout-frame[slot="AAA"]'); @@ -50,8 +52,8 @@ test("should switch between tabs by clicking", async ({ page }) => { await page.mouse.click(frameBounds.x, frameBounds.y); } - await page.waitForTimeout(100); - const selectedAfter = await getSelectedTab(); + // Since tab selection has happened, the visible titlebar is now "BBB"'s + const selectedAfter = await getSelectedTab("BBB"); expect(selectedAfter).toBe("BBB"); const layoutState = await page.evaluate(() => { const layout = document.querySelector("regular-layout"); @@ -73,27 +75,6 @@ test("should move a panel by dragging a selected tab", async ({ page }) => { layoutElement?.restore(layout as Layout); }, LAYOUTS.TWO_HORIZONTAL_WITH_TABS); - const layoutBefore = await page.evaluate(() => { - const layout = document.querySelector("regular-layout"); - return layout?.save(); - }); - - expect(layoutBefore).toMatchObject({ - type: "split-panel", - orientation: "horizontal", - children: [ - { - type: "child-panel", - child: ["AAA", "BBB"], - selected: 0, - }, - { - type: "child-panel", - child: ["CCC"], - }, - ], - }); - const dragCoords = await page.evaluate(() => { const frame = document.querySelector('regular-layout-frame[slot="AAA"]'); const activeTab = frame?.shadowRoot?.querySelector( @@ -121,7 +102,6 @@ test("should move a panel by dragging a selected tab", async ({ page }) => { await page.mouse.up(); } - await page.waitForTimeout(100); const layoutAfter = await page.evaluate(() => { const layout = document.querySelector("regular-layout"); return layout?.save(); @@ -138,11 +118,11 @@ test("should move a panel by dragging a selected tab", async ({ page }) => { }, { type: "child-panel", - child: ["AAA"], + child: ["CCC"], }, { type: "child-panel", - child: ["CCC"], + child: ["AAA"], }, ], }); @@ -206,7 +186,6 @@ test("should move a panel by dragging a deselected tab", async ({ page }) => { await page.mouse.up(); } - await page.waitForTimeout(100); const layoutAfter = await page.evaluate(() => { const layout = document.querySelector("regular-layout"); return layout?.save(); @@ -223,11 +202,11 @@ test("should move a panel by dragging a deselected tab", async ({ page }) => { }, { type: "child-panel", - child: ["BBB"], + child: ["CCC"], }, { type: "child-panel", - child: ["CCC"], + child: ["BBB"], }, ], }); diff --git a/tests/unit/calculate_split.spec.ts b/tests/unit/calculate_edge.spec.ts similarity index 73% rename from tests/unit/calculate_split.spec.ts rename to tests/unit/calculate_edge.spec.ts index a6772be..5e0b11c 100644 --- a/tests/unit/calculate_split.spec.ts +++ b/tests/unit/calculate_edge.spec.ts @@ -10,42 +10,97 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { expect, test } from "@playwright/test"; -import { TEST_PANEL, LAYOUTS } from "../helpers/fixtures.ts"; -import { calculate_split } from "../../src/common/calculate_split.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; +import { calculate_edge } from "../../src/common/calculate_edge.ts"; import { calculate_intersection } from "../../src/common/calculate_intersect.ts"; import type { LayoutPath, TabLayout } from "../../src/common/layout_config.ts"; test("cursor in center of panel - no split", () => { - const drop_target = calculate_intersection(0.3, 0.5, TEST_PANEL, false); - const result = calculate_split(0.3, 0.5, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.3, + 0.5, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.3, + 0.5, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(false); expect(result.slot).toBe("BBB"); }); test("cursor near left edge of vertical split panel", () => { - const drop_target = calculate_intersection(0.05, 0.3, TEST_PANEL, false); - const result = calculate_split(0.05, 0.3, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.05, + 0.3, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.05, + 0.3, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("DDD"); }); test("cursor near right edge of vertical split panel", () => { - const drop_target = calculate_intersection(0.55, 0.3, TEST_PANEL, false); - const result = calculate_split(0.55, 0.3, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.55, + 0.3, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.55, + 0.3, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("DDD"); }); test("cursor near top edge of horizontal split panel", () => { - const drop_target = calculate_intersection(0.7, 0.05, TEST_PANEL, false); - const result = calculate_split(0.7, 0.05, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.7, + 0.05, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.7, + 0.05, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("CCC"); // ??? }); test("cursor near bottom edge of horizontal split panel", () => { - const drop_target = calculate_intersection(0.7, 0.95, TEST_PANEL, false); - const result = calculate_split(0.7, 0.95, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.7, + 0.95, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.7, + 0.95, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("CCC"); // ??? }); @@ -58,7 +113,7 @@ test("cursor near left edge but with horizontal orientation", () => { false, ); - const result = calculate_split( + const result = calculate_edge( 0.05, 0.5, LAYOUTS.SINGLE_SPLIT_HORIZONTAL, @@ -78,7 +133,7 @@ test("cursor near right edge but with horizontal orientation", () => { false, ); - const result = calculate_split( + const result = calculate_edge( 0.95, 0.5, LAYOUTS.SINGLE_SPLIT_HORIZONTAL, @@ -98,7 +153,7 @@ test("cursor near top edge but with vertical orientation", () => { false, ); - const result = calculate_split( + const result = calculate_edge( 0.5, 0.05, LAYOUTS.SINGLE_SPLIT_VERTICAL, @@ -118,7 +173,7 @@ test("cursor near bottom edge but with vertical orientation", () => { false, ); - const result = calculate_split( + const result = calculate_edge( 0.5, 0.95, LAYOUTS.SINGLE_SPLIT_VERTICAL, @@ -135,7 +190,7 @@ test("integrated top edge", () => { const row = 0.002; let drop_target = calculate_intersection(col, row, LAYOUTS.SINGLE_AAA, false); if (drop_target) { - drop_target = calculate_split( + drop_target = calculate_edge( col, row, LAYOUTS.SINGLE_AAA, @@ -157,7 +212,7 @@ test("integrated right edge", () => { const row = 0.53; let drop_target = calculate_intersection(col, row, LAYOUTS.SINGLE_AAA, false); if (drop_target) { - drop_target = calculate_split( + drop_target = calculate_edge( col, row, LAYOUTS.SINGLE_AAA, @@ -166,11 +221,27 @@ test("integrated right edge", () => { ); } - expect(drop_target.view_window).toStrictEqual({ - col_end: 1, - col_start: 0.5, - row_end: 1, - row_start: 0, + expect(drop_target).toStrictEqual({ + box: undefined, + column: 0.998, + column_offset: 0.996, + is_edge: true, + orientation: "horizontal", + panel: { + child: ["BBB"], + type: "child-panel", + }, + path: [1], + row: 0.53, + row_offset: 0.53, + slot: "BBB", + type: "layout-path", + view_window: { + col_end: 1, + col_start: 0.5, + row_end: 1, + row_start: 0, + }, }); }); @@ -182,6 +253,8 @@ test("cursor in top-left corner prioritizes row offset", () => { panel: singlePanel as TabLayout, path: [], view_window: { row_start: 0, row_end: 1, col_start: 0, col_end: 1 }, + column: 0.1, + row: 0.05, column_offset: 0.1, row_offset: 0.05, orientation: "horizontal", @@ -189,7 +262,7 @@ test("cursor in top-left corner prioritizes row offset", () => { box: undefined, }; - const result = calculate_split(0.1, 0.05, singlePanel, "BBB", drop_target); + const result = calculate_edge(0.1, 0.05, singlePanel, "BBB", drop_target); expect(result.is_edge).toBe(true); }); @@ -201,6 +274,8 @@ test("cursor in bottom-right corner prioritizes row offset", () => { panel: singlePanel as TabLayout, path: [], view_window: { row_start: 0, row_end: 1, col_start: 0, col_end: 1 }, + column: 0.9, + row: 0.95, column_offset: 0.9, row_offset: 0.95, orientation: "horizontal", @@ -208,7 +283,7 @@ test("cursor in bottom-right corner prioritizes row offset", () => { box: undefined, }; - const result = calculate_split(0.9, 0.95, singlePanel, "BBB", drop_target); + const result = calculate_edge(0.9, 0.95, singlePanel, "BBB", drop_target); expect(result.is_edge).toBe(true); }); @@ -220,6 +295,8 @@ test("cursor near edge with offset exactly at tolerance threshold", () => { path: [], panel: singlePanel as TabLayout, view_window: { row_start: 0, row_end: 1, col_start: 0, col_end: 1 }, + column: 0.3, + row: 0.5, column_offset: 0.3, row_offset: 0.5, orientation: "horizontal", @@ -227,7 +304,7 @@ test("cursor near edge with offset exactly at tolerance threshold", () => { box: undefined, }; - const result = calculate_split(0.3, 0.5, singlePanel, "BBB", drop_target); + const result = calculate_edge(0.3, 0.5, singlePanel, "BBB", drop_target); expect(result.is_edge).toBe(false); }); @@ -239,6 +316,8 @@ test("cursor near edge with offset just below tolerance threshold", () => { panel: singlePanel as TabLayout, path: [], view_window: { row_start: 0, row_end: 1, col_start: 0, col_end: 1 }, + column: 0.14, + row: 0.5, column_offset: 0.14, row_offset: 0.5, orientation: "horizontal", @@ -246,21 +325,43 @@ test("cursor near edge with offset just below tolerance threshold", () => { box: undefined, }; - const result = calculate_split(0.14, 0.5, singlePanel, "BBB", drop_target); + const result = calculate_edge(0.14, 0.5, singlePanel, "BBB", drop_target); expect(result.is_edge).toBe(true); }); test("nested panel with vertical orientation at left edge", () => { - const drop_target = calculate_intersection(0.02, 0.3, TEST_PANEL, false); - const result = calculate_split(0.02, 0.3, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.02, + 0.3, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.02, + 0.3, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("DDD"); expect(result.path).toEqual([0, 1, 0]); }); test("nested panel with vertical orientation at right edge", () => { - const drop_target = calculate_intersection(0.58, 0.3, TEST_PANEL, false); - const result = calculate_split(0.58, 0.3, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.58, + 0.3, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.58, + 0.3, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.is_edge).toBe(true); expect(result.slot).toBe("DDD"); expect(result.path).toEqual([0, 1, 1]); @@ -274,7 +375,7 @@ test("complex layout with multiple nested panels", () => { false, ); - const result = calculate_split( + const result = calculate_edge( 0.02, 0.3, LAYOUTS.COMPLEX_NESTED_ABC, @@ -287,8 +388,19 @@ test("complex layout with multiple nested panels", () => { }); test("preserves drop_target properties when no split occurs", () => { - const drop_target = calculate_intersection(0.3, 0.3, TEST_PANEL, false); - const result = calculate_split(0.3, 0.3, TEST_PANEL, "DDD", drop_target); + const drop_target = calculate_intersection( + 0.3, + 0.3, + LAYOUTS.NESTED_BASIC, + false, + ); + const result = calculate_edge( + 0.3, + 0.3, + LAYOUTS.NESTED_BASIC, + "DDD", + drop_target, + ); expect(result.view_window).toBeDefined(); expect(result.path).toBeDefined(); expect(result.orientation).toBeDefined(); diff --git a/tests/unit/css_grid_layout.spec.ts b/tests/unit/css_grid_layout.spec.ts index 5126a51..e1fe32e 100644 --- a/tests/unit/css_grid_layout.spec.ts +++ b/tests/unit/css_grid_layout.spec.ts @@ -10,7 +10,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { expect, test } from "@playwright/test"; -import { TEST_PANEL } from "../helpers/fixtures.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; import { create_css_grid_layout } from "../../src/common/generate_grid.ts"; import type { Layout } from "../../src/common/layout_config.ts"; @@ -23,7 +23,7 @@ const RESULT = ` `.trim(); test("simple test", async () => { - const css = create_css_grid_layout(TEST_PANEL, true); + const css = create_css_grid_layout(LAYOUTS.NESTED_BASIC, true); expect(css).toBe(RESULT); }); diff --git a/tests/unit/flatten.spec.ts b/tests/unit/flatten.spec.ts index c094a1f..3703236 100644 --- a/tests/unit/flatten.spec.ts +++ b/tests/unit/flatten.spec.ts @@ -74,6 +74,7 @@ test("Deeply alternating split partial", () => { { type: "child-panel", child: ["AAA"], + selected: 0, }, { type: "split-panel", @@ -82,10 +83,12 @@ test("Deeply alternating split partial", () => { { type: "child-panel", child: ["CCC"], + selected: 0, }, { type: "child-panel", child: ["DDD"], + selected: 0, }, ], sizes: [0.5, 0.5], @@ -93,16 +96,62 @@ test("Deeply alternating split partial", () => { { type: "child-panel", child: ["FFF"], + selected: 0, }, { type: "child-panel", child: ["BBB"], + selected: 0, }, { type: "child-panel", child: ["EEE"], + selected: 0, }, ], sizes: [0.25, 0.25, 0.15, 0.15, 0.2], }); }); + +test("Nested split panels with a single child", () => { + const test: Layout = { + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "split-panel", + orientation: "vertical", + children: [ + { + type: "child-panel", + child: ["AAA", "BBB", "CCC"], + }, + { + type: "child-panel", + child: ["DDD"], + }, + ], + sizes: [0.5, 0.5], + }, + ], + sizes: [1], + }; + + expect(flatten(test)).toStrictEqual({ + type: "split-panel", + orientation: "vertical", + children: [ + { + type: "child-panel", + child: ["AAA", "BBB", "CCC"], + selected: 0, + }, + { + type: "child-panel", + child: ["DDD"], + selected: 0, + }, + ], + sizes: [0.5, 0.5], + }); +}); diff --git a/tests/unit/hit_detection.spec.ts b/tests/unit/hit_detection.spec.ts index f4dd028..9a4a90b 100644 --- a/tests/unit/hit_detection.spec.ts +++ b/tests/unit/hit_detection.spec.ts @@ -11,10 +11,10 @@ import { expect, test } from "@playwright/test"; import { calculate_intersection } from "../../src/common/calculate_intersect.ts"; -import { TEST_PANEL } from "../helpers/fixtures.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; test("AAA", () => { - const result = calculate_intersection(0.1, 0.1, TEST_PANEL); + const result = calculate_intersection(0.1, 0.1, LAYOUTS.NESTED_BASIC); expect(result).toStrictEqual({ slot: "AAA", box: undefined, @@ -28,6 +28,8 @@ test("AAA", () => { row_end: 0.3, row_start: 0, }, + column: 0.1, + row: 0.1, column_offset: 0.16666666666666669, row_offset: 0.33333333333333337, panel: { @@ -38,7 +40,7 @@ test("AAA", () => { }); test("BBB", () => { - const result = calculate_intersection(0.1, 0.4, TEST_PANEL); + const result = calculate_intersection(0.1, 0.4, LAYOUTS.NESTED_BASIC); expect(result).toStrictEqual({ slot: "BBB", path: [0, 1], @@ -52,6 +54,8 @@ test("BBB", () => { row_end: 1, row_start: 0.3, }, + column: 0.1, + row: 0.4, column_offset: 0.16666666666666669, row_offset: 0.1428571428571429, panel: { @@ -62,7 +66,7 @@ test("BBB", () => { }); test("CCC", () => { - const result = calculate_intersection(0.7, 0.1, TEST_PANEL); + const result = calculate_intersection(0.7, 0.1, LAYOUTS.NESTED_BASIC); expect(result).toStrictEqual({ slot: "CCC", path: [1], @@ -80,13 +84,15 @@ test("CCC", () => { col_end: 1, col_start: 0.6, }, + column: 0.7, + row: 0.1, column_offset: 0.24999999999999994, row_offset: 0.1, }); }); test("gap", () => { - const result = calculate_intersection(0.6, 0.1, TEST_PANEL); + const result = calculate_intersection(0.6, 0.1, LAYOUTS.NESTED_BASIC); expect(result).toStrictEqual({ path: [0], type: "horizontal", @@ -100,7 +106,7 @@ test("gap", () => { }); test("nested gap", () => { - const result = calculate_intersection(0.1, 0.3, TEST_PANEL); + const result = calculate_intersection(0.1, 0.3, LAYOUTS.NESTED_BASIC); expect(result).toStrictEqual({ path: [0, 0], type: "vertical", diff --git a/tests/unit/insert_child.spec.ts b/tests/unit/insert_child.spec.ts index ee5416d..40223b6 100644 --- a/tests/unit/insert_child.spec.ts +++ b/tests/unit/insert_child.spec.ts @@ -10,11 +10,11 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { expect, test } from "@playwright/test"; -import { TEST_PANEL, LAYOUTS } from "../helpers/fixtures.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; import { insert_child } from "../../src/common/insert_child.ts"; test("insert into root split panel", () => { - const result = insert_child(TEST_PANEL, "DDD", []); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", []); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -48,7 +48,7 @@ test("insert into root split panel", () => { }); test("append top level split-panel", () => { - const result = insert_child(TEST_PANEL, "DDD", [2]); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", [2]); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -82,7 +82,7 @@ test("append top level split-panel", () => { }); test("insert into top level split-panel", () => { - const result = insert_child(TEST_PANEL, "DDD", [1]); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", [1]); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -116,7 +116,7 @@ test("insert into top level split-panel", () => { }); test("insert at path splitting a child panel", () => { - const result = insert_child(TEST_PANEL, "DDD", [1, 1]); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", [1, 1]); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -157,7 +157,7 @@ test("insert at path splitting a child panel", () => { }); test("insert into nested split panel", () => { - const result = insert_child(TEST_PANEL, "DDD", [0, 2]); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", [0, 2]); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -191,7 +191,7 @@ test("insert into nested split panel", () => { }); test("split a nested child panel", () => { - const result = insert_child(TEST_PANEL, "DDD", [0, 0, 1]); + const result = insert_child(LAYOUTS.NESTED_BASIC, "DDD", [0, 0, 1]); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -333,3 +333,32 @@ test("insert into a child-panel root, on the top edge", () => { sizes: [0.5, 0.5], }); }); + +test("insert with split path into SINGLE_TABS", () => { + const result = insert_child(LAYOUTS.SINGLE_TABS, "DDD", [0, 1], "horizontal"); + expect(result).toStrictEqual({ + type: "split-panel", + orientation: "horizontal", + children: [ + { + type: "split-panel", + orientation: "vertical", + children: [ + { + type: "child-panel", + child: ["AAA", "BBB", "CCC"], + selected: 0, + }, + { + type: "child-panel", + child: ["DDD"], + // TODO this one case does not call flatten internally for performance + // selected: 0, + }, + ], + sizes: [0.5, 0.5], + }, + ], + sizes: [1], + }); +}); diff --git a/tests/unit/redistribute_panel_sizes.spec.ts b/tests/unit/redistribute_panel_sizes.spec.ts index a423860..de4b52a 100644 --- a/tests/unit/redistribute_panel_sizes.spec.ts +++ b/tests/unit/redistribute_panel_sizes.spec.ts @@ -10,11 +10,15 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { expect, test } from "@playwright/test"; -import { TEST_PANEL, LAYOUTS } from "../helpers/fixtures.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; import { redistribute_panel_sizes } from "../../src/common/redistribute_panel_sizes.ts"; test("redistribute depth 1 child", () => { - const clone = redistribute_panel_sizes(structuredClone(TEST_PANEL), [0], 0.1); + const clone = redistribute_panel_sizes( + structuredClone(LAYOUTS.NESTED_BASIC), + [0], + 0.1, + ); expect(clone).toStrictEqual({ type: "split-panel", @@ -45,7 +49,7 @@ test("redistribute depth 1 child", () => { }); test("redistribute depth 2 children", () => { - const clone = redistribute_panel_sizes(TEST_PANEL, [0, 0], 0.1); + const clone = redistribute_panel_sizes(LAYOUTS.NESTED_BASIC, [0, 0], 0.1); expect(clone).toStrictEqual({ type: "split-panel", children: [ @@ -166,7 +170,11 @@ test("redistribute nested spec with 4 children", () => { }); test("nested aligned splitpanels", () => { - const clone = redistribute_panel_sizes(LAYOUTS.NESTED_ALIGNED, [0, 0, 0], 0.1); + const clone = redistribute_panel_sizes( + LAYOUTS.NESTED_ALIGNED, + [0, 0, 0], + 0.1, + ); expect(clone).toStrictEqual({ type: "split-panel", orientation: "horizontal", diff --git a/tests/unit/remove_child.spec.ts b/tests/unit/remove_child.spec.ts index 87c202c..5376d2c 100644 --- a/tests/unit/remove_child.spec.ts +++ b/tests/unit/remove_child.spec.ts @@ -10,11 +10,11 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { expect, test } from "@playwright/test"; -import { TEST_PANEL, LAYOUTS } from "../helpers/fixtures.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; import { remove_child } from "../../src/common/remove_child.ts"; test("remove child from nested split panel", () => { - const result = remove_child(TEST_PANEL, "AAA"); + const result = remove_child(LAYOUTS.NESTED_BASIC, "AAA"); expect(result).toStrictEqual({ type: "split-panel", children: [ @@ -33,7 +33,7 @@ test("remove child from nested split panel", () => { }); test("remove child from top-level split panel", () => { - const result = remove_child(TEST_PANEL, "CCC"); + const result = remove_child(LAYOUTS.NESTED_BASIC, "CCC"); expect(result).toStrictEqual({ type: "split-panel", children: [