openpencil/tests/e2e/layers/drop-indicator.spec.ts
Danila Poyarkov d7613f34ec
perf(scene-graph): accelerate axis-aligned hit tests (#534)
* perf(scene-graph): accelerate axis-aligned hit tests

- Reuse cached world positions for untransformed node chains
- Preserve exact matrix hit testing beneath rotated and flipped ancestors
- Profile pointer, hit-test, cached, overlay, and volatile rendering at 500 and 2,000 nodes

* fix(vue): restore layer drop indicators

- Forward reactive drag instructions through virtualized layer item slots
- Render above, below, and child feedback before the drop completes
- Add browser regressions for reorder lines and container highlights

* fix(scene-graph): harden hit-test performance coverage

- Cache transformed ancestry within each hit-test traversal
- Invalidate absolute positions after preview flips and reparenting
- Attach scale-relative browser profiles and correct auto-layout fixture geometry

* fix(scene-graph): reject cyclic layer reorder

- Prevent reorderChild from parenting a node beneath its descendant
- Cover graph preservation after a rejected cyclic reorder
2026-08-16 15:43:25 +03:00

104 lines
3.4 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import type { Vector } from '@open-pencil/scene-graph'
import { CanvasHelper } from '#tests/helpers/canvas'
async function dragLayerAndObserveIndicator(
page: Page,
sourceId: string,
targetId: string,
targetPosition: Vector
) {
await page.evaluate(() => {
const positions: string[] = []
new MutationObserver(() => {
for (const element of document.querySelectorAll<HTMLElement>(
'[data-slot="drop-indicator"]'
)) {
const position = element.dataset.dropPosition
if (position) positions.push(position)
}
}).observe(document.body, { subtree: true, childList: true, attributes: true })
Object.assign(window, { __layerDropPositions: positions })
})
const source = page.locator(`[data-node-id="${sourceId}"]`)
const target = page.locator(`[data-node-id="${targetId}"]`)
const sourceBox = await source.boundingBox()
const targetBox = await target.boundingBox()
if (!sourceBox || !targetBox) throw new Error('Layer row bounds unavailable')
await page.mouse.move(sourceBox.x + 80, sourceBox.y + 12)
await page.mouse.down()
await page.mouse.move(sourceBox.x + 84, sourceBox.y + 8, { steps: 5 })
await page.mouse.move(targetBox.x + targetPosition.x, targetBox.y + targetPosition.y, {
steps: 20
})
await expect(target.locator('[data-slot="drop-indicator"]')).toBeVisible()
await page.mouse.up()
return page.evaluate(
() => (window as typeof window & { __layerDropPositions?: string[] }).__layerDropPositions ?? []
)
}
test('layer reorder exposes a visible drop indicator before dropping', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await canvas.clearCanvas()
const ids = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const first = store.graph.createNode('RECTANGLE', pageId, { name: 'Layer A' })
store.graph.createNode('RECTANGLE', pageId, { name: 'Layer B' })
const third = store.graph.createNode('RECTANGLE', pageId, { name: 'Layer C' })
store.requestRender()
return { first: first.id, third: third.id }
})
await canvas.waitForRender()
const positions = await dragLayerAndObserveIndicator(page, ids.third, ids.first, {
x: 80,
y: 2
})
expect(positions).toContain('above')
canvas.assertNoErrors()
})
test('layer child drop exposes a visible container highlight before dropping', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await canvas.clearCanvas()
const ids = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const frame = store.graph.createNode('FRAME', pageId, {
name: 'Drop Frame',
width: 200,
height: 120
})
const rect = store.graph.createNode('RECTANGLE', pageId, { name: 'Child Candidate' })
store.requestRender()
return { frame: frame.id, rect: rect.id }
})
await canvas.waitForRender()
const positions = await dragLayerAndObserveIndicator(page, ids.rect, ids.frame, {
x: 80,
y: 12
})
expect(positions).toContain('child')
canvas.assertNoErrors()
})