openpencil/tests/e2e/properties/stroke-geometry.spec.ts
Danila Poyarkov 2f8d799a5f feat(editor): add stroke geometry controls
- Add mixed-selection cap, join, and miter-limit controls with one-step undo
- Apply node miter limits across CanvasKit stroke paths and vector outline caches
- Preserve imported and edited stroke geometry through Plugin API and .fig roundtrips
2026-07-17 19:01:42 +03:00

104 lines
3.4 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
import { propertySection } from '#tests/helpers/properties'
let page: Page
let canvas: CanvasHelper
test.describe.configure({ mode: 'serial' })
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await page.goto('/')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
async function drawStrokedRectangle(x: number, y: number) {
await canvas.pressKey('r')
await canvas.drag(x, y, x + 120, y + 80)
await canvas.waitForRender()
await propertySection(page, 'Stroke').getByRole('button', { name: 'Add stroke' }).click()
await canvas.waitForRender()
}
async function selectedStrokeGeometry() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds].map((id) => {
const node = store.graph.getNode(id)
return node
? {
cap: node.strokeCap,
join: node.strokeJoin,
miterLimit: node.strokeMiterLimit,
paintCaps: node.strokes.map((stroke) => stroke.cap),
paintJoins: node.strokes.map((stroke) => stroke.join)
}
: null
})
})
}
test('shows geometry controls only after a stroke is added', async () => {
await canvas.pressKey('r')
await canvas.drag(100, 100, 220, 180)
await canvas.waitForRender()
await expect(page.locator('[data-property="stroke-cap"]')).not.toBeVisible()
await propertySection(page, 'Stroke').getByRole('button', { name: 'Add stroke' }).click()
await canvas.waitForRender()
await expect(page.locator('[data-property="stroke-cap"]')).toBeVisible()
await expect(page.locator('[data-property="stroke-join"]')).toBeVisible()
await expect(page.locator('[data-property="stroke-miter-limit"]')).toBeVisible()
})
test('updates cap, join, and miter state from compact controls', async () => {
const section = propertySection(page, 'Stroke')
await section.getByRole('button', { name: 'Round cap' }).click()
await section.getByRole('button', { name: 'Bevel join' }).click()
await section.getByRole('spinbutton', { name: 'Miter limit' }).focus()
const miter = section.getByRole('spinbutton', { name: 'Miter limit' })
await miter.fill('12')
await miter.press('Enter')
await canvas.waitForRender()
expect(await selectedStrokeGeometry()).toEqual([
{
cap: 'ROUND',
join: 'BEVEL',
miterLimit: 12,
paintCaps: ['ROUND'],
paintJoins: ['BEVEL']
}
])
await canvas.pressKey('Meta+z')
await canvas.waitForRender()
expect((await selectedStrokeGeometry())[0]?.miterLimit).toBe(4)
})
test('applies mixed multi-selection joins in one undo step', async () => {
await canvas.clearCanvas()
await drawStrokedRectangle(80, 80)
await drawStrokedRectangle(260, 80)
await canvas.pressKey('Meta+a')
await canvas.waitForRender()
const roundJoin = propertySection(page, 'Stroke').getByRole('button', { name: 'Round join' })
await expect(roundJoin).toBeVisible()
await roundJoin.click()
await canvas.waitForRender()
expect((await selectedStrokeGeometry()).map((value) => value?.join)).toEqual(['ROUND', 'ROUND'])
await canvas.pressKey('Meta+z')
await canvas.waitForRender()
expect((await selectedStrokeGeometry()).map((value) => value?.join)).toEqual(['MITER', 'MITER'])
})