openpencil/tests/e2e/layers/large-tree.spec.ts
Danila Poyarkov 7bfebce0bf feat(editor): scale and theme Layers panel
- Add indexed visible rows, virtual scroll-to-selection, and range selection

- Move Layer Tree state styling into typed Tailwind Variants slots

- Enforce parsed Vue state-class theming and cover 5,000-node documents
2026-07-17 17:23:16 +03:00

148 lines
5.2 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
const NODE_COUNT = 5000
test('large layer trees stay virtualized and scrollable', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test&no-rulers')
await canvas.waitForInit()
const replaceStartedAt = await page.evaluate((count: number) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const Graph = store.graph.constructor as new () => typeof store.graph
const graph = new Graph()
const pageId = graph.getPages()[0]?.id
if (!pageId) throw new Error('Page not initialized')
for (let i = 0; i < count; i++) {
graph.createNode('RECTANGLE', pageId, {
name: `Layer ${String(i + 1).padStart(4, '0')}`,
x: (i % 40) * 24,
y: Math.floor(i / 40) * 24,
width: 16,
height: 16,
fills: [
{
type: 'SOLID',
color: { r: 0.2, g: 0.5, b: 0.9, a: 1 },
visible: true,
opacity: 1
}
]
})
}
const startedAt = performance.now()
store.replaceGraph(graph)
return startedAt
}, NODE_COUNT)
await canvas.waitForRender()
const scroller = page.getByTestId('layers-scroll')
const rows = page.getByTestId('layers-item')
await expect(rows.first()).toContainText('Layer 0001')
await expect.poll(() => rows.count()).toBeLessThan(250)
await expect
.poll(() => page.evaluate((startedAt) => performance.now() - startedAt, replaceStartedAt))
.toBeLessThan(5000)
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageNode = store.graph.getNode(store.state.currentPageId)
const lastId = pageNode?.childIds.at(-1)
if (!lastId) throw new Error('Last layer not found')
store.select([lastId])
})
const lastRow = rows.filter({ hasText: 'Layer 5000' }).first()
await expect(lastRow).toBeVisible()
const scrollBefore = await scroller.evaluate((el) => el.scrollTop)
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageNode = store.graph.getNode(store.state.currentPageId)
const firstId = pageNode?.childIds[0]
if (!firstId) throw new Error('First layer not found')
store.updateNodeWithUndo(firstId, { x: 32 }, 'Move first layer')
})
await expect(lastRow).toBeVisible()
await page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve())
})
)
await expect.poll(() => scroller.evaluate((el) => el.scrollTop)).toBe(scrollBefore)
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageNode = store.graph.getNode(store.state.currentPageId)
const lastId = pageNode?.childIds.at(-1)
if (!lastId) throw new Error('Last layer not found')
store.renameNode(lastId, 'Last layer renamed')
})
const renamedRow = rows.filter({ hasText: 'Last layer renamed' }).first()
await expect(renamedRow).toBeVisible()
await renamedRow.dblclick()
const renameInput = page.getByTestId('layers-item-input')
await expect(renameInput).toBeFocused()
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const firstId = store.graph.getNode(store.state.currentPageId)?.childIds[0]
if (!firstId) throw new Error('First layer not found')
store.updateNodeWithUndo(firstId, { y: 48 }, 'Move first layer again')
})
await expect(renameInput).toBeFocused()
})
test('layer tree supports range and additive selection', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test&no-rulers')
await canvas.waitForInit()
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const Graph = store.graph.constructor as new () => typeof store.graph
const graph = new Graph()
const pageId = graph.getPages()[0]?.id
if (!pageId) throw new Error('Page not initialized')
for (let index = 1; index <= 8; index++) {
graph.createNode('RECTANGLE', pageId, { name: `Layer ${index}` })
}
store.replaceGraph(graph)
})
const rows = page.getByTestId('layers-item')
await rows.filter({ hasText: 'Layer 2' }).click()
await page.keyboard.down('Shift')
await rows.filter({ hasText: 'Layer 5' }).click()
await page.keyboard.up('Shift')
const selectedRows = page.getByTestId('layers-item').and(page.locator('[data-selected]'))
await expect(selectedRows).toHaveCount(4)
await expect(rows.filter({ hasText: 'Layer 2' })).toHaveAttribute('data-selected')
await expect(rows.filter({ hasText: 'Layer 5' })).toHaveAttribute('data-selected')
await page.keyboard.down('Meta')
await rows.filter({ hasText: 'Layer 7' }).click()
await page.keyboard.up('Meta')
await expect(selectedRows).toHaveCount(5)
await expect(rows.filter({ hasText: 'Layer 7' })).toHaveAttribute('data-selected')
})