openpencil/tests/e2e/canvas/nested-transform-interaction.spec.ts
Danila Poyarkov d16400b3e6 feat(editor): checkpoint live interaction improvements
Unify preview-aware transforms across scene drawing, labels, selection and input. Give live property and creation edits explicit preview ownership, preserve cancellation and one-step undo, and reuse bounded text preparation resources.

Include retained device-grid handling, worktree HMR coverage, and nested/reflected interaction regressions in this cohesive progress checkpoint.

Validation: full check passes; 734 scoped unit tests and 53 targeted browser tests pass. This is NOT merge-ready: the unchanged exact nested filled-section held/released regression still fails with 63 differing pixels (maximum channel delta 5/255). The browser pass count excludes that separately run failure and the previously classified paint-field-width baseline. Raster-origin investigation and broader acceptance remain outstanding.
2026-09-15 11:36:17 +03:00

145 lines
5.9 KiB
TypeScript

import { ROTATION_HANDLE_DISTANCE } from '@open-pencil/core/constants'
import type { Vector } from '@open-pencil/scene-graph/primitives'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
import { applyFixtureTransform, nestedGeometryData } from '#tests/helpers/geometry'
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
for (const flipX of [false, true]) {
for (const flipY of [false, true]) {
test(`nested rotation, labels and cancellation (flipX=${flipX}, flipY=${flipY})`, async () => {
const data = nestedGeometryData(flipX, flipY)
const id = await editor.page.evaluate((fixture) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('Editor unavailable')
const s = fixture.section
const sectionId = store.createShape('SECTION', s.x, s.y, s.width, s.height)
store.updateNode(sectionId, s)
const frame = store.graph.createNode('FRAME', sectionId, {
...fixture.frame,
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }]
})
store.graph.createNode('RECTANGLE', frame.id, {
name: 'Asymmetric content',
x: 15,
y: 14,
width: 70,
height: 30,
cornerRadius: 6,
fills: [
{ type: 'SOLID', color: { r: 0.85, g: 0.25, b: 0.3, a: 1 }, opacity: 1, visible: true }
]
})
store.select([frame.id])
store.requestRender()
return frame.id
}, data)
await editor.canvas.waitForRender()
const bounds = expectDefined(
await editor.page.getByTestId('canvas-element').boundingBox(),
'canvas bounds'
)
const viewport = await editor.page.evaluate(() => {
const state = window.openPencil?.getStore?.().state
if (!state) throw new Error('Editor unavailable')
return { zoom: state.zoom, panX: state.panX, panY: state.panY }
})
const world = (point: Vector) =>
applyFixtureTransform(data.section, applyFixtureTransform(data.frame, point))
const center = world({ x: data.frame.width / 2, y: data.frame.height / 2 })
const stalkOffset = ROTATION_HANDLE_DISTANCE / viewport.zoom
const reflectedY = data.section.flipY !== data.frame.flipY
const start = world({
x: data.frame.width / 2,
y: reflectedY ? data.frame.height + stalkOffset : -stalkOffset
})
const radians = Math.PI / 6
const dx = start.x - center.x
const dy = start.y - center.y
const end = {
x: center.x + dx * Math.cos(radians) - dy * Math.sin(radians),
y: center.y + dx * Math.sin(radians) + dy * Math.cos(radians)
}
const move = async (point: Vector, steps = 1) =>
editor.page.mouse.move(
bounds.x + viewport.panX + point.x * viewport.zoom,
bounds.y + viewport.panY + point.y * viewport.zoom,
{ steps }
)
await move(start)
await editor.canvas.waitForRender()
const original = await editor.canvas.screenshotCanvasRegion()
await editor.page.mouse.down()
try {
await move(end, 8)
await editor.canvas.waitForRender()
const held = await editor.page.evaluate((nodeId) => {
const store = window.openPencil?.getStore?.()
return {
rotation: store?.graph.getNode(nodeId)?.rotation,
preview: store?.state.rotationPreview
}
}, id)
expect(held.rotation).toBe(data.frame.rotation)
const preview = expectDefined(held.preview, 'held rotation')
expect(preview.nodeId).toBe(id)
expect(Math.sign(preview.angle - data.frame.rotation)).toBe(flipX === flipY ? 1 : -1)
expect(await editor.canvas.screenshotCanvasRegion()).toMatchSnapshot(
`nested-transform-${flipX}-${flipY}-held.png`,
{ maxDiffPixelRatio: 0, threshold: 0 }
)
await editor.page.keyboard.press('Escape')
await editor.canvas.waitForRender()
expect(
await editor.page.evaluate(() => window.openPencil?.getStore?.().state.rotationPreview)
).toBeNull()
expect(
await editor.page.evaluate(() => [
...(window.openPencil?.getStore?.().state.selectedIds ?? [])
])
).toEqual([id])
await editor.page.mouse.up()
// Compare the restored scene with the pointer (and therefore hover) back in the same place.
await move(start)
await editor.canvas.waitForRender()
expect((await editor.canvas.screenshotCanvasRegion()).equals(original)).toBe(true)
await editor.page.mouse.down()
await move(end, 8)
const committedAngle = expectDefined(
await editor.page.evaluate(
() => window.openPencil?.getStore?.().state.rotationPreview?.angle
),
'preview angle'
)
await editor.page.mouse.up()
await editor.canvas.waitForRender()
expect(
await editor.page.evaluate(
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
id
)
).toBe(committedAngle)
await editor.page.evaluate(() => window.openPencil?.getStore?.().undoAction())
expect(
await editor.page.evaluate(
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
id
)
).toBe(data.frame.rotation)
await editor.page.evaluate(() => window.openPencil?.getStore?.().redoAction())
expect(
await editor.page.evaluate(
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
id
)
).toBe(committedAngle)
editor.canvas.assertNoErrors()
} finally {
await editor.page.mouse.up()
}
})
}
}