- Replace browser store non-null assertions with explicit initialization guards - Use expectDefined for optional test resources and tool results - Clean low-count non-null assertions in font, icon, snap, OKHCL, and visual tests
278 lines
8.5 KiB
TypeScript
278 lines
8.5 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
|
|
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()
|
|
})
|
|
|
|
function designPanel() {
|
|
return page.locator('[data-test-id="design-panel-single"]')
|
|
}
|
|
|
|
function nodeHeader() {
|
|
return page.locator('[data-test-id="design-node-header"]')
|
|
}
|
|
|
|
function fillSection() {
|
|
return page.locator('[data-test-id="fill-section"]')
|
|
}
|
|
|
|
function strokeSection() {
|
|
return page.locator('[data-test-id="stroke-section"]')
|
|
}
|
|
|
|
function positionSection() {
|
|
return page.locator('[data-test-id="position-section"]')
|
|
}
|
|
|
|
function effectsSection() {
|
|
return page.locator('[data-test-id="effects-section"]')
|
|
}
|
|
|
|
function getNode(id: string) {
|
|
return page.evaluate((nodeId) => {
|
|
const store = window.__OPEN_PENCIL_STORE__
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
const n = store.graph.getNode(nodeId)
|
|
if (!n) return null
|
|
return {
|
|
fills: n.fills,
|
|
strokes: n.strokes,
|
|
effects: n.effects,
|
|
opacity: n.opacity,
|
|
visible: n.visible,
|
|
x: n.x,
|
|
y: n.y,
|
|
width: n.width,
|
|
height: n.height,
|
|
rotation: n.rotation
|
|
}
|
|
}, id)
|
|
}
|
|
|
|
function getSelectedId() {
|
|
return page.evaluate(() => {
|
|
const store = window.__OPEN_PENCIL_STORE__
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
return [...store.state.selectedIds][0] ?? null
|
|
})
|
|
}
|
|
|
|
test('selecting a rectangle shows design panel with type and name', async () => {
|
|
await canvas.drawRect(100, 100, 120, 80)
|
|
await canvas.waitForRender()
|
|
|
|
await expect(designPanel()).toBeVisible()
|
|
await expect(nodeHeader()).toContainText('RECTANGLE')
|
|
await expect(nodeHeader()).toContainText('Rectangle')
|
|
})
|
|
|
|
test('position section shows X, Y, rotation inputs', async () => {
|
|
await expect(positionSection()).toBeVisible()
|
|
|
|
const inputs = positionSection().locator('[data-test-id="scrub-input"]')
|
|
const count = await inputs.count()
|
|
expect(count).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
test('fill section appears with default fill', async () => {
|
|
await expect(fillSection()).toBeVisible()
|
|
|
|
const fillItems = fillSection().locator('[data-test-id="fill-item"]')
|
|
await expect(fillItems.first()).toBeVisible()
|
|
})
|
|
|
|
test('fill item shows color swatch', async () => {
|
|
const swatch = fillSection().locator('[data-test-id="fill-picker-swatch"]').first()
|
|
await expect(swatch).toBeVisible()
|
|
})
|
|
|
|
test('clicking color area changes fill color', async () => {
|
|
const id = await getSelectedId()
|
|
const before = await getNode(id!)
|
|
|
|
const swatch = fillSection().locator('[data-test-id="fill-picker-swatch"]').first()
|
|
await swatch.click()
|
|
|
|
const colorArea = page.locator('.cursor-crosshair').first()
|
|
await expect(colorArea).toBeVisible({ timeout: 5000 })
|
|
|
|
const box = await colorArea.boundingBox()
|
|
await page.mouse.click(box!.x + box!.width - 10, box!.y + 10)
|
|
await canvas.waitForRender()
|
|
await page.waitForTimeout(100)
|
|
|
|
const after = await getNode(id!)
|
|
const c1 = before!.fills[0].color
|
|
const c2 = after!.fills[0].color
|
|
expect(c1.r !== c2.r || c1.g !== c2.g || c1.b !== c2.b).toBe(true)
|
|
|
|
// Close popover — click the swatch again to toggle it off
|
|
await swatch.click()
|
|
await canvas.waitForRender()
|
|
})
|
|
|
|
test('adding a stroke creates stroke section item', async () => {
|
|
const addBtn = strokeSection().locator('[data-test-id="stroke-section-add"]')
|
|
await addBtn.click()
|
|
await canvas.waitForRender()
|
|
|
|
const strokeItems = strokeSection().locator('[data-test-id="stroke-item"]')
|
|
await expect(strokeItems.first()).toBeVisible()
|
|
|
|
const id = await getSelectedId()
|
|
const node = await getNode(id!)
|
|
expect(node!.strokes.length).toBe(1)
|
|
})
|
|
|
|
test('adding an effect creates effect item', async () => {
|
|
const addBtn = effectsSection().locator('[data-test-id="effects-section-add"]')
|
|
await addBtn.click()
|
|
await canvas.waitForRender()
|
|
|
|
const effectItems = effectsSection().locator('[data-test-id="effect-item"]')
|
|
await expect(effectItems.first()).toBeVisible()
|
|
|
|
const id = await getSelectedId()
|
|
const node = await getNode(id!)
|
|
expect(node!.effects.length).toBe(1)
|
|
})
|
|
|
|
test('adding a second fill shows two fill items', async () => {
|
|
const addBtn = fillSection().locator('[data-test-id="fill-section-add"]')
|
|
await addBtn.click()
|
|
await canvas.waitForRender()
|
|
|
|
const fillItems = fillSection().locator('[data-test-id="fill-item"]')
|
|
expect(await fillItems.count()).toBe(2)
|
|
|
|
const id = await getSelectedId()
|
|
const node = await getNode(id!)
|
|
expect(node!.fills.length).toBe(2)
|
|
})
|
|
|
|
test('visibility toggle in appearance section works', async () => {
|
|
const visBtn = page.locator('[data-test-id="appearance-visibility"]')
|
|
await expect(visBtn).toBeVisible()
|
|
|
|
const id = await getSelectedId()
|
|
const before = await getNode(id!)
|
|
expect(before!.visible).toBe(true)
|
|
|
|
await visBtn.click()
|
|
await canvas.waitForRender()
|
|
|
|
const after = await getNode(id!)
|
|
expect(after!.visible).toBe(false)
|
|
|
|
await visBtn.click()
|
|
await canvas.waitForRender()
|
|
|
|
const restored = await getNode(id!)
|
|
expect(restored!.visible).toBe(true)
|
|
})
|
|
|
|
test('fill stroke and effect visibility toggles update on repeated clicks and support undo redo', async () => {
|
|
const id = await getSelectedId()
|
|
expect(id).toBeTruthy()
|
|
|
|
const fillButton = page.locator('[data-test-id="fill-visibility-0"]')
|
|
await expect(fillButton).toBeVisible()
|
|
|
|
const initial = await getNode(id!)
|
|
expect(initial!.fills[0]?.visible).toBe(true)
|
|
|
|
await fillButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(fillButton).toHaveAttribute('data-visible', 'false')
|
|
expect((await getNode(id!))!.fills[0]?.visible).toBe(false)
|
|
|
|
await fillButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(fillButton).toHaveAttribute('data-visible', 'true')
|
|
expect((await getNode(id!))!.fills[0]?.visible).toBe(true)
|
|
|
|
await canvas.undo()
|
|
expect((await getNode(id!))!.fills[0]?.visible).toBe(false)
|
|
await canvas.redo()
|
|
expect((await getNode(id!))!.fills[0]?.visible).toBe(true)
|
|
|
|
const strokeAddButton = strokeSection().locator('[data-test-id="stroke-section-add"]')
|
|
await strokeAddButton.click()
|
|
await canvas.waitForRender()
|
|
|
|
const strokeButton = page.locator('[data-test-id="stroke-visibility-0"]')
|
|
await expect(strokeButton).toBeVisible()
|
|
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
|
|
|
await strokeButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(strokeButton).toHaveAttribute('data-visible', 'false')
|
|
expect((await getNode(id!))!.strokes[0]?.visible).toBe(false)
|
|
|
|
await strokeButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(strokeButton).toHaveAttribute('data-visible', 'true')
|
|
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
|
|
|
await canvas.undo()
|
|
expect((await getNode(id!))!.strokes[0]?.visible).toBe(false)
|
|
await canvas.redo()
|
|
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
|
|
|
const effectAddButton = effectsSection().locator('[data-test-id="effects-section-add"]')
|
|
await effectAddButton.click()
|
|
await canvas.waitForRender()
|
|
|
|
const effectButton = page.locator('[data-test-id="effect-visibility-0"]')
|
|
await expect(effectButton).toBeVisible()
|
|
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
|
|
|
await effectButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(effectButton).toHaveAttribute('data-visible', 'false')
|
|
expect((await getNode(id!))!.effects[0]?.visible).toBe(false)
|
|
|
|
await effectButton.click()
|
|
await canvas.waitForRender()
|
|
await expect(effectButton).toHaveAttribute('data-visible', 'true')
|
|
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
|
|
|
await canvas.undo()
|
|
expect((await getNode(id!))!.effects[0]?.visible).toBe(false)
|
|
await canvas.redo()
|
|
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
|
})
|
|
|
|
test('deselecting shows empty design panel', async () => {
|
|
await page.keyboard.press('Escape')
|
|
await canvas.waitForRender()
|
|
|
|
await expect(page.locator('[data-test-id="design-panel-empty"]')).toBeVisible()
|
|
})
|
|
|
|
test('multi-select shows mixed header', async () => {
|
|
await canvas.drawRect(300, 100, 60, 60)
|
|
await canvas.drawRect(400, 100, 60, 60)
|
|
await canvas.selectAll()
|
|
await canvas.waitForRender()
|
|
|
|
const multiHeader = page.locator('[data-test-id="design-multi-header"]')
|
|
await expect(multiHeader).toBeVisible()
|
|
await expect(multiHeader).toContainText('Mixed')
|
|
await expect(multiHeader).toContainText('layers')
|
|
})
|