openpencil/tests/helpers/canvas.ts
Danila Poyarkov bb8c5c18d9
fix(editor): commit vector drags on pointer release (#586)
* fix(editor): commit vector drags on pointer release

- Commit vertex and Bézier handle edits immediately on release\n- Rebase active vector-edit state after graph commits\n- Use vector-specific input paths and cover Pen-created Bézier drags

* test: reduce visual snapshot scope

- Replace behavior screenshots with scene assertions\n- Capture renderer fixtures from bounded canvas regions\n- Remove obsolete full-viewport snapshots

* fix(vector): harden edit cancellation and endpoint remapping

- Restore temporary vector geometry on cancelled pointer drags\n- Commit Bézier handle releases consistently and guard stale handle state\n- Preserve valid endpoint indices when connecting vector vertices

* fix(vector): cancel bend drags and satisfy formatting

* fix(vector): guard stale handle references

* fix(vector): guard stale overlay handle indices

* fix(vector): validate overlay handle keys

* refactor(vector): use typed handle selection ids

* test(vector): align typed handle selection coverage
2026-08-24 16:02:27 +03:00

197 lines
5.8 KiB
TypeScript

import type { Page, Locator } from '@playwright/test'
export class CanvasHelper {
readonly page: Page
readonly canvas: Locator
readonly errors: string[] = []
constructor(page: Page) {
this.page = page
this.canvas = page.getByTestId('canvas-area')
page.on('pageerror', (err) => this.errors.push(err.message))
page.on('console', (msg) => {
if (msg.type() === 'error') this.errors.push(msg.text())
})
}
assertNoErrors() {
if (this.errors.length > 0) {
const messages = this.errors.join('\n')
this.errors.length = 0
throw new Error(`Browser errors:\n${messages}`)
}
}
async waitForRender() {
await this.page.evaluate(() => new Promise(requestAnimationFrame))
}
async waitForInit() {
await this.page
.getByTestId('canvas-element')
.and(this.page.locator('[data-ready="1"]'))
.waitFor({ timeout: 30000 })
await this.page.getByTestId('canvas-loading').waitFor({ state: 'hidden', timeout: 30000 })
await this.page.locator('#loader').waitFor({ state: 'detached', timeout: 30000 })
}
async clearCanvas() {
await this.selectAll()
await this.pressKey('Backspace')
await this.waitForRender()
}
async screenshotCanvasRegion(width = 900, height = 700) {
const box = await this.canvas.boundingBox()
if (!box) throw new Error('Canvas has no bounding box — is it visible?')
const viewport = this.page.viewportSize()
if (!viewport) throw new Error('Viewport unavailable')
return this.page.screenshot({
clip: {
x: box.x,
y: box.y,
width: Math.min(width, viewport.width - box.x),
height: Math.min(height, viewport.height - box.y)
}
})
}
private async canvasBounds() {
const b = await this.canvas.boundingBox()
if (!b) throw new Error('Canvas has no bounding box — is it visible?')
return b
}
async click(canvasX: number, canvasY: number) {
const box = await this.canvasBounds()
await this.page.mouse.click(box.x + canvasX, box.y + canvasY)
}
async drag(fromX: number, fromY: number, toX: number, toY: number, steps = 10) {
const box = await this.canvasBounds()
await this.page.mouse.move(box.x + fromX, box.y + fromY)
await this.page.mouse.down()
await this.page.mouse.move(box.x + toX, box.y + toY, { steps })
await this.page.mouse.up()
}
async pressKey(key: string) {
await this.page.keyboard.press(key)
}
async drawRect(x: number, y: number, width: number, height: number) {
await this.pressKey('r')
await this.drag(x, y, x + width, y + height)
await this.waitForRender()
}
async drawEllipse(x: number, y: number, width: number, height: number) {
await this.pressKey('o')
await this.drag(x, y, x + width, y + height)
await this.waitForRender()
}
async drawSection(x: number, y: number, width: number, height: number) {
await this.pressKey('s')
await this.drag(x, y, x + width, y + height)
await this.waitForRender()
}
async selectTool(
tool: 'select' | 'frame' | 'section' | 'rectangle' | 'ellipse' | 'text' | 'pen' | 'hand'
) {
const keys: Record<string, string> = {
select: 'v',
frame: 'f',
section: 's',
rectangle: 'r',
ellipse: 'o',
text: 't',
pen: 'p',
hand: 'h'
}
await this.pressKey(keys[tool])
}
async deleteSelection() {
await this.pressKey('Backspace')
await this.waitForRender()
}
async undo() {
await this.pressKey('Meta+z')
await this.waitForRender()
}
async redo() {
await this.pressKey('Meta+Shift+z')
await this.waitForRender()
}
async selectAll() {
await this.pressKey('Meta+a')
}
async duplicate() {
await this.pressKey('Meta+d')
await this.waitForRender()
}
async marquee(x1: number, y1: number, x2: number, y2: number, steps = 10) {
const box = await this.canvasBounds()
await this.page.mouse.move(box.x + x1, box.y + y1)
await this.page.mouse.down()
await this.page.mouse.move(box.x + x2, box.y + y2, { steps })
await this.page.mouse.up()
await this.waitForRender()
}
async hover(x: number, y: number) {
const box = await this.canvasBounds()
await this.page.mouse.move(box.x + x, box.y + y)
await this.waitForRender()
}
/** Point `locator` at the outer NumberField container, not the inner `<input>`. */
async dragNumberField(locator: Locator, deltaX: number) {
await locator.scrollIntoViewIfNeeded()
const box = await locator.boundingBox()
if (!box) throw new Error('dragNumberField: element has no bounding box')
const cx = box.x + box.width / 2
const cy = box.y + box.height / 2
await this.page.mouse.move(cx, cy)
await this.page.mouse.down()
await this.page.mouse.move(cx + deltaX, cy, { steps: 10 })
await this.page.mouse.up()
await this.waitForRender()
}
async altDrag(fromX: number, fromY: number, toX: number, toY: number) {
const box = await this.canvasBounds()
await this.page.keyboard.down('Alt')
await this.page.mouse.move(box.x + fromX, box.y + fromY)
await this.page.mouse.down()
await this.page.mouse.move(box.x + toX, box.y + toY, { steps: 10 })
await this.page.mouse.up()
await this.page.keyboard.up('Alt')
await this.waitForRender()
}
async shiftDrag(fromX: number, fromY: number, toX: number, toY: number) {
const box = await this.canvasBounds()
await this.page.keyboard.down('Shift')
await this.page.mouse.move(box.x + fromX, box.y + fromY)
await this.page.mouse.down()
await this.page.mouse.move(box.x + toX, box.y + toY, { steps: 10 })
await this.page.mouse.up()
await this.page.keyboard.up('Shift')
await this.waitForRender()
}
async dblclick(x: number, y: number) {
const box = await this.canvasBounds()
await this.page.mouse.dblclick(box.x + x, box.y + y)
await this.waitForRender()
}
}