Files
cms-onpageeditor/test/rail-sections.test.tsx
T
mats b7849c0359 Repair outline rows and add split panels for drag & drop
Outline chevron buttons were matched by a generic `.cms-outline li
button { width: 100% }` rule, so each expand/collapse arrow took a full
row and the tree looked stacked and broken. Scope row styling to
`.cms-orow__main`, fix the chevron to 22px, add rotating chevrons to the
section headers, and turn the blocks palette from centered narrow cards
into left-aligned rows.

Add a split toggle to the panel header: outline stays visible in a left
column while Blocks/Nodes switch as tabs in a right column, so a drag
source and its outline drop gaps are on screen together. Choice and
library tab persist per profile; stacked mode is unchanged.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-11-07 13:37:45 +01:00

163 lines
6.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { act } from 'react'
import { setup } from './harness'
import type { Harness } from './harness'
let h: Harness
beforeEach(() => localStorage.clear())
afterEach(() => {
h?.cleanup()
delete (navigator as any).gpu
})
const q = (sel: string) => h.shadow.querySelector<HTMLElement>(sel)!
const qa = (sel: string) => [...h.shadow.querySelectorAll<HTMLElement>(sel)]
describe('rail sections', () => {
it('outline, blocks and nodes collapse via their headers', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.settle()
await h.settle()
expect(qa('.cms-outline').length).toBeGreaterThan(0)
// collapse outline: the tree leaves the DOM, palette stays
// (NB: the section itself shares the aria-label, so target the toggle button)
await act(async () => void q('button[aria-label="Outline"]').click())
await h.settle()
expect(qa('.cms-outline').length).toBe(0)
expect(q('[aria-label="Search blocks"]')).not.toBeNull()
// collapse blocks too
await act(async () => void q('button[aria-label="Blocks"]').click())
await h.settle()
expect(h.shadow.querySelector('[aria-label="Search blocks"]')).toBeNull()
// and back open
await act(async () => void q('button[aria-label="Blocks"]').click())
await h.settle()
expect(q('[aria-label="Search blocks"]')).not.toBeNull()
await act(async () => void q('button[aria-label="Outline"]').click())
await h.settle()
expect(qa('.cms-outline').length).toBeGreaterThan(0)
})
it('rail open state and collapsed sections survive a reload', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.settle()
await act(async () => void q('button[aria-label="Blocks"]').click()) // collapse blocks
await h.settle()
expect(localStorage.getItem('cms-onpage.rail')).toBe('true')
expect(localStorage.getItem('cms-onpage.sec.blocks')).toBe('false')
// reload with the same profile: rail reopens, blocks stays collapsed
h.cleanup()
h = await setup({ keepStorage: true })
await h.settle()
await h.settle()
expect(q('.cms-rail')).not.toBeNull()
expect(h.shadow.querySelector('[aria-label="Search blocks"]')).toBeNull()
expect(qa('.cms-outline').length).toBeGreaterThan(0)
})
it('dragging the rail edge resizes it and remembers the width', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.settle()
const grip = q('.cms-rail__resize')
const down = new Event('pointerdown', { bubbles: true, cancelable: true }) as Event & { clientX: number; button: number }
down.clientX = 332
down.button = 0
await act(async () => void grip.dispatchEvent(down))
const move = new Event('pointermove', { bubbles: true, cancelable: true }) as Event & { clientX: number }
move.clientX = 420
await act(async () => void document.dispatchEvent(move))
const up = new Event('pointerup', { bubbles: true, cancelable: true })
await act(async () => void document.dispatchEvent(up))
await h.settle()
// jsdom reports rect.left as 0, so width == clientX, clamped to [240, 640]
expect((q('.cms-rail') as HTMLElement).style.width).toBe('420px')
expect(localStorage.getItem('cms-onpage.railwidth')).toBe('420')
h.cleanup()
h = await setup({ keepStorage: true })
await h.settle()
await h.settle()
expect((q('.cms-rail') as HTMLElement).style.width).toBe('420px')
})
it('Escape closes the rail when nothing else is open', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.settle()
expect(q('.cms-rail')).not.toBeNull()
await act(async () => void document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true, composed: true })))
await h.settle()
expect(h.shadow.querySelector('.cms-rail')).toBeNull()
expect(localStorage.getItem('cms-onpage.rail')).toBe('false')
})
it('dev panel remembers its tab', async () => {
h = await setup()
await h.settle()
await act(async () => void h.editor.ctrl.store.set({ dev: true }))
await h.settle()
const registry = qa('.cms-dev header button').find((b) => b.textContent === 'registry')!
await act(async () => void registry.click())
await h.settle()
expect(localStorage.getItem('cms-onpage.devtab')).toBe('registry')
h.cleanup()
h = await setup({ keepStorage: true })
await h.settle()
await act(async () => void h.editor.ctrl.store.set({ dev: true }))
await h.settle()
expect(q('.cms-dev__body').textContent).toContain('blockEditors')
})
it('split mode shows the outline beside blocks/nodes tabs for drag & drop', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.settle()
// stacked by default: single column, no tabs
expect(h.shadow.querySelector('.cms-rail.is-split')).toBeNull()
await act(async () => void q('button[aria-label="Split panels side by side"]').click())
await h.settle()
expect(q('.cms-rail.is-split')).not.toBeNull()
// outline and blocks visible at the same time: a drag source can reach a gap
expect(qa('.cms-outline').length).toBeGreaterThan(0)
expect(q('[aria-label="Search blocks"]')).not.toBeNull()
// switch the library tab to nodes: blocks hide, nodes show, outline stays
await act(async () => void q('button[aria-label="Show nodes panel"]').click())
await h.settle()
expect(h.shadow.querySelector('[aria-label="Search blocks"]')).toBeNull()
expect(q('[aria-label="Search nodes"]')).not.toBeNull()
expect(qa('.cms-outline').length).toBeGreaterThan(0)
// starting a palette drag reveals outline gaps while both columns stay visible
h.editor.ctrl.beginPaletteDrag({ type: 'text', label: 'text' })
await h.settle()
expect(qa('[data-ogap]').length).toBeGreaterThan(0)
expect(q('[aria-label="Search nodes"]')).not.toBeNull()
await act(async () => void h.editor.ctrl.cancelDrag())
await h.settle()
// stacking collapses back to one column and remembers the choice
await act(async () => void q('button[aria-label="Stack panels"]').click())
await h.settle()
expect(h.shadow.querySelector('.cms-rail.is-split')).toBeNull()
expect(localStorage.getItem('cms-onpage.railsplit')).toBe('false')
})
it('assistant sits beside the dev panel instead of stacking under it', async () => {
;(navigator as any).gpu = {} // aiSupported() checks for a WebGPU API
h = await setup()
await h.settle()
await act(async () => void h.editor.ctrl.store.set({ assistant: true, dev: true }))
await h.settle()
expect(q('.cms-dev')).not.toBeNull()
expect(q('.cms-assistant.cms-assistant--shifted')).not.toBeNull()
await act(async () => void h.editor.ctrl.store.set({ dev: false }))
await h.settle()
expect(h.shadow.querySelector('.cms-assistant--shifted')).toBeNull()
})
})