test(e2e): extract shared editor setup fixture

- useEditorSetup() / useEditorSetupWithClear() in tests/e2e/fixtures.ts
- Migrated 17 specs to shared fixture, removing ~300 lines of boilerplate
- Moved getSelectedNode/getSelectedNodes to shared store helpers
- Replaced inline getSelectedNode in 4 specs with shared import
- Test duplication: 329 → 308 clones (10.12% → 9.08%)
This commit is contained in:
Danila Poyarkov 2026-05-16 16:19:03 +03:00
parent e7b610b228
commit 11e38b626a
24 changed files with 573 additions and 859 deletions

View file

@ -193,7 +193,8 @@ const strictTestFilePlacement = createFileRule('open-pencil/strict-test-file-pla
return 'Temporary/profile test files must not be committed. Move exploratory specs to scratch/ or delete them.'
}
if (sourceRel.startsWith('tests/e2e/')) {
return sourceRel.endsWith('.spec.ts') ? null : 'E2E tests must live under tests/e2e/** and use *.spec.ts.'
if (sourceRel.endsWith('.spec.ts') || sourceRel.endsWith('/fixtures.ts')) return null
return 'E2E tests must live under tests/e2e/** and use *.spec.ts.'
}
if (sourceRel.startsWith('tests/figma/')) {
return sourceRel.endsWith('.spec.ts') ? null : 'Figma Playwright tests must live under tests/figma/** and use *.spec.ts.'

View file

@ -1,37 +1,21 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
test('menu bar is visible in browser mode', async () => {
const menubar = page.locator('[role="menubar"]')
const menubar = editor.page.locator('[role="menubar"]')
await expect(menubar).toBeVisible()
})
test('menu bar has all top-level menus', async () => {
const triggers = page.locator('[role="menubar"] [role="menuitem"]')
const triggers = editor.page.locator('[role="menubar"] [role="menuitem"]')
const labels = await triggers.allTextContents()
expect(labels).toEqual(['File', 'Edit', 'View', 'Object', 'Text', 'Arrange'])
})
test('File menu opens and shows items', async () => {
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'File' }).click()
const menu = page.locator('[role="menu"]')
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'File' }).click()
const menu = editor.page.locator('[role="menu"]')
await expect(menu).toBeVisible()
const items = await menu.locator('[role="menuitem"]').allTextContents()
@ -39,12 +23,12 @@ test('File menu opens and shows items', async () => {
expect(items.some((t) => t.includes('Save'))).toBe(true)
expect(items.some((t) => t.includes('Save As'))).toBe(true)
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
test('Edit menu shows Undo/Redo/Delete', async () => {
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
const menu = page.locator('[role="menu"]')
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
const menu = editor.page.locator('[role="menu"]')
await expect(menu).toBeVisible()
const items = await menu.locator('[role="menuitem"]').allTextContents()
@ -53,12 +37,12 @@ test('Edit menu shows Undo/Redo/Delete', async () => {
expect(items.some((t) => t.includes('Delete'))).toBe(true)
expect(items.some((t) => t.includes('Select all'))).toBe(true)
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
test('View menu shows zoom options', async () => {
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
const menu = page.locator('[role="menu"]')
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
const menu = editor.page.locator('[role="menu"]')
await expect(menu).toBeVisible()
const items = await menu.locator('[role="menuitem"]').allTextContents()
@ -66,12 +50,12 @@ test('View menu shows zoom options', async () => {
expect(items.some((t) => t.includes('Zoom In'))).toBe(true)
expect(items.some((t) => t.includes('Zoom Out'))).toBe(true)
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
test('Object menu shows Group/Ungroup/Component', async () => {
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Object' }).click()
const menu = page.locator('[role="menu"]')
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Object' }).click()
const menu = editor.page.locator('[role="menu"]')
await expect(menu).toBeVisible()
const items = await menu.locator('[role="menuitem"]').allTextContents()
@ -81,11 +65,11 @@ test('Object menu shows Group/Ungroup/Component', async () => {
expect(items.some((t) => t.includes('Bring to front'))).toBe(true)
expect(items.some((t) => t.includes('Send to back'))).toBe(true)
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
function getStoreStateNumber(key: 'selectedIds' | 'zoom') {
return page.evaluate((stateKey) => {
return editor.page.evaluate((stateKey) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
if (stateKey === 'selectedIds') return store.state.selectedIds.size
@ -94,32 +78,32 @@ function getStoreStateNumber(key: 'selectedIds' | 'zoom') {
}
test('Undo via Edit menu works', async () => {
await canvas.drawRect(200, 200, 100, 100)
await editor.canvas.drawRect(200, 200, 100, 100)
const beforeUndo = await getStoreStateNumber('selectedIds')
expect(beforeUndo).toBe(1)
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
await page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Undo' }).click()
await canvas.waitForRender()
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
await editor.page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Undo' }).click()
await editor.canvas.waitForRender()
const afterUndo = await getStoreStateNumber('selectedIds')
expect(afterUndo).toBe(0)
})
test('Duplicate via Edit menu works', async () => {
await canvas.drawRect(300, 300, 80, 80)
await editor.canvas.drawRect(300, 300, 80, 80)
const countBefore = await page.evaluate(() => {
const countBefore = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).length
})
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
await page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Duplicate' }).click()
await canvas.waitForRender()
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'Edit' }).click()
await editor.page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Duplicate' }).click()
await editor.canvas.waitForRender()
const countAfter = await page.evaluate(() => {
const countAfter = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).length
@ -129,16 +113,16 @@ test('Duplicate via Edit menu works', async () => {
})
test('Zoom to fit via View menu works', async () => {
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
await page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Zoom in' }).click()
await canvas.waitForRender()
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
await editor.page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Zoom in' }).click()
await editor.canvas.waitForRender()
const zoomBefore = await getStoreStateNumber('zoom')
expect(zoomBefore).toBeGreaterThan(1)
await page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
await page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Zoom to fit' }).click()
await canvas.waitForRender()
await editor.page.locator('[role="menubar"] [role="menuitem"]', { hasText: 'View' }).click()
await editor.page.locator('[role="menu"] [role="menuitem"]', { hasText: 'Zoom to fit' }).click()
await editor.canvas.waitForRender()
const zoomAfter = await getStoreStateNumber('zoom')
expect(zoomAfter).not.toBe(zoomBefore)

View file

@ -1,25 +1,10 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
test('autosave triggers after scene changes with a file handle', async () => {
const writeCount = await page.evaluate(() => {
const writeCount = await editor.page.evaluate(() => {
let writes = 0
const mockWritable = {
write: async () => {
@ -41,7 +26,7 @@ test('autosave triggers after scene changes with a file handle', async () => {
// Inject the file handle into the store's internal state
// We do this by calling a save first to establish the handle
await page.evaluate(() => {
await editor.page.evaluate(() => {
// Directly set the fileHandle via a test hook
// Since fileHandle is a closure variable, we need to trigger the save path
// The cleanest way: mock showSaveFilePicker to return our handle
@ -56,14 +41,14 @@ test('autosave triggers after scene changes with a file handle', async () => {
})
// Trigger Save As to establish the file handle
await page.keyboard.press('Meta+Shift+s')
await page.waitForTimeout(500)
await editor.page.keyboard.press('Meta+Shift+s')
await editor.page.waitForTimeout(500)
// Now draw a shape — this should trigger autosave after 3s
await canvas.drawRect(400, 400, 60, 60)
await editor.canvas.drawRect(400, 400, 60, 60)
// Check that the scene version changed
const versionAfterDraw = await page.evaluate(() => {
const versionAfterDraw = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.sceneVersion
@ -71,10 +56,10 @@ test('autosave triggers after scene changes with a file handle', async () => {
expect(versionAfterDraw).toBeGreaterThan(0)
// Wait for autosave debounce (3s) + buffer
await page.waitForTimeout(4000)
await editor.page.waitForTimeout(4000)
// Verify a write happened by checking the mock was called
const writeHappened = await page.evaluate(() => {
const writeHappened = await editor.page.evaluate(() => {
// The handle's createWritable should have been called
const handle = window.showSaveFilePicker
return handle !== undefined

View file

@ -1,35 +1,15 @@
import { test, expect, type Page } from '@playwright/test'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
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('/?test&no-chrome&no-rulers')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
test.beforeEach(async () => {
await canvas.clearCanvas()
})
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
async function expectCanvas(name: string) {
canvas.assertNoErrors()
const buffer = await canvas.canvas.screenshot()
editor.canvas.assertNoErrors()
const buffer = await editor.canvas.canvas.screenshot()
expect(buffer).toMatchSnapshot(`${name}.png`)
}
async function createOverlayDemo(rotation: number) {
await page.evaluate((frameRotation) => {
await editor.page.evaluate((frameRotation) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -91,7 +71,7 @@ async function createOverlayDemo(rotation: number) {
store.requestRender()
}, rotation)
await canvas.waitForRender()
await editor.canvas.waitForRender()
}
test('rotated frame selection labels render with hovered child', async () => {
@ -102,7 +82,7 @@ test('rotated frame selection labels render with hovered child', async () => {
test('rotation preview updates frame labels before mouse up', async () => {
await createOverlayDemo(0)
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const frameId = [...store.state.selectedIds][0]
@ -110,7 +90,7 @@ test('rotation preview updates frame labels before mouse up', async () => {
store.requestRepaint()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('rotated-frame-selection-labels-preview')
})

View file

@ -1,6 +1,7 @@
import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
import { getSelectedNodes } from '#tests/helpers/store'
let page: Page
let canvas: CanvasHelper
@ -34,26 +35,6 @@ function getSelectedCount() {
})
}
function getSelectedNodes() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds].map((id) => {
const n = store.graph.getNode(id)
if (!n) throw new Error(`Selected node ${id} not found`)
return {
id: n.id,
name: n.name,
type: n.type,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
fills: n.fills
}
})
})
}
test('copy + paste via store duplicates a shape', async () => {
await canvas.drawRect(100, 100, 120, 80)
@ -76,7 +57,7 @@ test('copy + paste via store duplicates a shape', async () => {
})
test('pasted node is offset from original', async () => {
const nodes = await getSelectedNodes()
const nodes = await getSelectedNodes(page)
expect(nodes).toHaveLength(1)
const pasted = nodes[0]
@ -123,7 +104,7 @@ test('duplicate preserves fills', async () => {
await canvas.duplicate()
const nodes = await getSelectedNodes()
const nodes = await getSelectedNodes(page)
expect(nodes[0].fills[0].color.b).toBeCloseTo(1, 1)
})

View file

@ -1,25 +1,9 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
function getNodeChildren(nodeId: string) {
return page.evaluate((id) => {
return editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(id).map((n) => ({
@ -32,7 +16,7 @@ function getNodeChildren(nodeId: string) {
}
function getSelectedParent() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
@ -43,7 +27,7 @@ function getSelectedParent() {
}
function createFrameWithChild() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -57,7 +41,7 @@ function createFrameWithChild() {
}
function selectNode(nodeId: string) {
return page.evaluate((id) => {
return editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
@ -66,7 +50,7 @@ function selectNode(nodeId: string) {
}
function copyAndPaste() {
return page.evaluate(async () => {
return editor.page.evaluate(async () => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const data = new DataTransfer()
@ -77,20 +61,20 @@ function copyAndPaste() {
}
test('paste into selected frame places node as child', async () => {
await canvas.clearCanvas()
await editor.canvas.clearCanvas()
const { frameId } = await createFrameWithChild()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await canvas.drawRect(400, 50, 60, 60)
await canvas.waitForRender()
await editor.canvas.drawRect(400, 50, 60, 60)
await editor.canvas.waitForRender()
await copyAndPaste()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await selectNode(frameId)
await canvas.waitForRender()
await editor.canvas.waitForRender()
await page.evaluate(async () => {
await editor.page.evaluate(async () => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const rect = [...store.graph.nodes.values()].find(
@ -109,7 +93,7 @@ test('paste into selected frame places node as child', async () => {
if (html) await store.pasteFromHTML(html)
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const children = await getNodeChildren(frameId)
const pastedChild = children.find((c) => c.name === 'Rectangle')
@ -118,14 +102,14 @@ test('paste into selected frame places node as child', async () => {
})
test('paste with child selected places node as sibling in parent frame', async () => {
await canvas.clearCanvas()
await editor.canvas.clearCanvas()
const { frameId, childId } = await createFrameWithChild()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await canvas.drawRect(400, 50, 60, 60)
await canvas.waitForRender()
await editor.canvas.drawRect(400, 50, 60, 60)
await editor.canvas.waitForRender()
await page.evaluate(
await editor.page.evaluate(
async ({ childId: cid }) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
@ -144,7 +128,7 @@ test('paste with child selected places node as sibling in parent frame', async (
},
{ childId }
)
await canvas.waitForRender()
await editor.canvas.waitForRender()
const parent = await getSelectedParent()
expect(parent).toBe(frameId)
@ -154,11 +138,11 @@ test('paste with child selected places node as sibling in parent frame', async (
})
test('paste with no selection places on page', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 60, 60)
await canvas.waitForRender()
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 60, 60)
await editor.canvas.waitForRender()
await page.evaluate(async () => {
await editor.page.evaluate(async () => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const data = new DataTransfer()
@ -167,15 +151,15 @@ test('paste with no selection places on page', async () => {
store.clearSelection()
if (html) await store.pasteFromHTML(html)
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const parent = await getSelectedParent()
const pageId = await page.evaluate(() => {
const pageId = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.currentPageId
})
expect(parent).toBe(pageId)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

View file

@ -1,45 +1,29 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
function codeTab() {
return page.getByTestId('properties-tab-code')
return editor.page.getByTestId('properties-tab-code')
}
function designTab() {
return page.getByTestId('properties-tab-design')
return editor.page.getByTestId('properties-tab-design')
}
function codePanel() {
return page.getByTestId('code-panel')
return editor.page.getByTestId('code-panel')
}
function codePanelEmpty() {
return page.getByTestId('code-panel-empty')
return editor.page.getByTestId('code-panel-empty')
}
function formatToggle() {
return page.getByTestId('code-panel-format-toggle')
return editor.page.getByTestId('code-panel-format-toggle')
}
function copyButton() {
return page.getByTestId('code-panel-copy')
return editor.page.getByTestId('code-panel-copy')
}
test('Code tab shows empty state with no selection', async () => {
@ -49,8 +33,8 @@ test('Code tab shows empty state with no selection', async () => {
})
test('selecting a rectangle shows JSX code', async () => {
await canvas.drawRect(100, 100, 200, 150)
await canvas.waitForRender()
await editor.canvas.drawRect(100, 100, 200, 150)
await editor.canvas.waitForRender()
await expect(codePanel()).toBeVisible()
@ -79,26 +63,26 @@ test('copy button works and shows confirmation', async () => {
await expect(copyButton()).toContainText('Copied')
await page.waitForTimeout(2500)
await editor.page.waitForTimeout(2500)
await expect(copyButton()).toContainText('Copy')
})
test('deselecting shows empty state again', async () => {
await page.keyboard.press('Escape')
await canvas.waitForRender()
await editor.page.keyboard.press('Escape')
await editor.canvas.waitForRender()
await expect(codePanelEmpty()).toBeVisible()
})
test('selecting a frame shows Frame in JSX', async () => {
// Create a frame via store to avoid click-targeting issues
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = store.createShape('FRAME', 300, 100, 200, 200)
store.select([id])
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const code = await codePanel().textContent()
expect(code).toContain('Frame')
@ -107,5 +91,5 @@ test('selecting a frame shows Frame in JSX', async () => {
test('switching back to Design tab works', async () => {
await designTab().click()
await expect(page.getByTestId('design-panel-single').or(page.getByTestId('design-panel-empty')).first()).toBeVisible()
await expect(editor.page.getByTestId('design-panel-single').or(editor.page.getByTestId('design-panel-empty')).first()).toBeVisible()
})

View file

@ -1,25 +1,9 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
async function getSelectedFill() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
@ -30,36 +14,36 @@ async function getSelectedFill() {
}
async function openFillPicker() {
const solidTab = page.getByTestId('fill-picker-tab-solid')
const solidTab = editor.page.getByTestId('fill-picker-tab-solid')
if (await solidTab.isVisible().catch(() => false)) return
const swatch = page.getByTestId('fill-picker-swatch').first()
const swatch = editor.page.getByTestId('fill-picker-swatch').first()
await swatch.click()
await expect(solidTab).toBeVisible()
}
async function chooseFormat(label: 'RGB' | 'HSL' | 'HSB' | 'OkHCL') {
await page.getByTestId('color-format-select').click()
await page.getByRole('option', { name: label, exact: true }).click()
await editor.page.getByTestId('color-format-select').click()
await editor.page.getByRole('option', { name: label, exact: true }).click()
}
async function dragSlider(testId: string, ratio: number) {
const slider = page.getByTestId(testId).locator('input[type="range"]')
const slider = editor.page.getByTestId(testId).locator('input[type="range"]')
const box = await slider.boundingBox()
if (!box) throw new Error(`Missing slider: ${testId}`)
const y = box.y + box.height / 2
await page.mouse.move(box.x + 2, y)
await page.mouse.down()
await page.mouse.move(box.x + Math.max(2, Math.min(box.width - 2, box.width * ratio)), y, {
await editor.page.mouse.move(box.x + 2, y)
await editor.page.mouse.down()
await editor.page.mouse.move(box.x + Math.max(2, Math.min(box.width - 2, box.width * ratio)), y, {
steps: 20
})
await page.mouse.up()
await canvas.waitForRender()
await editor.page.mouse.up()
await editor.canvas.waitForRender()
}
test('rgb hue slider updates selected fill color', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 160, 120)
await canvas.waitForRender()
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 160, 120)
await editor.canvas.waitForRender()
await openFillPicker()
const before = await getSelectedFill()

View file

@ -1,26 +1,10 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
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()
})
const editor = useEditorSetup()
function getNodeById(id: string) {
return page.evaluate((nodeId) => {
return editor.page.evaluate((nodeId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const n = store.graph.getNode(nodeId)
@ -30,7 +14,7 @@ function getNodeById(id: string) {
}
function getSelectedIds() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds]
@ -38,7 +22,7 @@ function getSelectedIds() {
}
function getPageChildren() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
@ -53,11 +37,11 @@ function getPageChildren() {
let componentId: string
test('create component from selection (⌘⌥K)', async () => {
await canvas.drawRect(100, 100, 120, 80)
await canvas.waitForRender()
await editor.canvas.drawRect(100, 100, 120, 80)
await editor.canvas.waitForRender()
await page.keyboard.press('Meta+Alt+k')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+Alt+k')
await editor.canvas.waitForRender()
const ids = await getSelectedIds()
expect(ids).toHaveLength(1)
@ -69,16 +53,16 @@ test('create component from selection (⌘⌥K)', async () => {
})
test('component shows purple label in design panel', async () => {
const header = page.getByTestId('design-node-header')
const header = editor.page.getByTestId('design-node-header')
await expect(header).toContainText('COMPONENT')
})
test('component visible in layers panel', async () => {
const layers = page.locator('[data-node-id]')
const layers = editor.page.locator('[data-node-id]')
const count = await layers.count()
expect(count).toBeGreaterThan(0)
const types = await page.evaluate(() => {
const types = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).map((n) => n.type)
@ -92,12 +76,12 @@ test('create instance from component (context menu)', async () => {
expect(comp).toBeTruthy()
// Use store directly to create instance
await page.evaluate((compId) => {
await editor.page.evaluate((compId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.createInstanceFromComponent(compId, 300, 100)
}, expectDefined(comp, 'component').id)
await canvas.waitForRender()
await editor.canvas.waitForRender()
const updated = await getPageChildren()
const instance = updated.find((c) => c.type === 'INSTANCE')
@ -112,38 +96,38 @@ test('instance shows INSTANCE type in design panel', async () => {
'instance node'
)
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, instance.id)
await canvas.waitForRender()
await editor.canvas.waitForRender()
const header = page.getByTestId('design-node-header')
const header = editor.page.getByTestId('design-node-header')
await expect(header).toContainText('INSTANCE')
})
test('instance has "Go to Main Component" button', async () => {
const goToBtn = page.getByTestId('design-go-to-component')
const goToBtn = editor.page.getByTestId('design-go-to-component')
await expect(goToBtn).toBeVisible()
})
test('instance has "Detach" button', async () => {
const detachBtn = page.getByTestId('design-detach-instance')
const detachBtn = editor.page.getByTestId('design-detach-instance')
await expect(detachBtn).toBeVisible()
})
test('modifying component propagates to instance', async () => {
// Select the component
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, componentId)
await canvas.waitForRender()
await editor.canvas.waitForRender()
// Change component fill
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.updateNodeWithUndo(
@ -162,7 +146,7 @@ test('modifying component propagates to instance', async () => {
'Change fill'
)
}, componentId)
await canvas.waitForRender()
await editor.canvas.waitForRender()
// Check instance got the same fill
const children = await getPageChildren()
@ -170,7 +154,7 @@ test('modifying component propagates to instance', async () => {
children.find((c) => c.type === 'INSTANCE'),
'instance node'
)
const instanceNode = await page.evaluate((id) => {
const instanceNode = await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const n = store.graph.getNode(id)
@ -188,23 +172,23 @@ test('detach instance converts to frame', async () => {
'instance node'
)
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, instance.id)
await canvas.waitForRender()
await editor.canvas.waitForRender()
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.detachInstance()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const ids = await getSelectedIds()
const detached = await getNodeById(expectDefined(ids[0], 'detached selected id'))
expect(detached?.type).toBe('FRAME')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

View file

@ -1,26 +1,10 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
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()
})
const editor = useEditorSetup()
function getPageChildren() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
@ -34,7 +18,7 @@ function getPageChildren() {
}
function getSelectedCount() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.selectedIds.size
@ -42,16 +26,16 @@ function getSelectedCount() {
}
async function rightClickShape(x: number, y: number) {
const box = expectDefined(await canvas.canvas.boundingBox(), 'canvas bounds')
await page.mouse.click(box.x + x, box.y + y, { button: 'right' })
const box = expectDefined(await editor.canvas.canvas.boundingBox(), 'canvas bounds')
await editor.page.mouse.click(box.x + x, box.y + y, { button: 'right' })
}
function contextMenu() {
return page.locator('[role="menu"]')
return editor.page.locator('[role="menu"]')
}
function contextItem(testId: string) {
return page.getByTestId(testId)
return editor.page.getByTestId(testId)
}
test('right-click on empty canvas shows context menu without selection items disabled', async () => {
@ -62,16 +46,16 @@ test('right-click on empty canvas shows context menu without selection items dis
const copyItem = contextItem('context-copy')
await expect(copyItem).toBeVisible()
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
test('draw shape and right-click selects it', async () => {
await canvas.drawRect(200, 200, 120, 80)
await canvas.waitForRender()
await editor.canvas.drawRect(200, 200, 120, 80)
await editor.canvas.waitForRender()
// Deselect first
await page.keyboard.press('Escape')
await canvas.waitForRender()
await editor.page.keyboard.press('Escape')
await editor.canvas.waitForRender()
// Right-click the shape
await rightClickShape(250, 230)
@ -91,7 +75,7 @@ test('context menu shows expected items', async () => {
await expect(contextItem('context-toggle-visibility')).toBeVisible()
await expect(contextItem('context-toggle-lock')).toBeVisible()
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})
test('duplicate via context menu works', async () => {
@ -99,17 +83,17 @@ test('duplicate via context menu works', async () => {
await rightClickShape(250, 230)
await contextItem('context-duplicate').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const countAfter = (await getPageChildren()).length
expect(countAfter).toBe(countBefore + 1)
})
test('toggle visibility via context menu', async () => {
await canvas.click(250, 230)
await canvas.waitForRender()
await editor.canvas.click(250, 230)
await editor.canvas.waitForRender()
const nodeId = await page.evaluate(() => {
const nodeId = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds][0]
@ -117,9 +101,9 @@ test('toggle visibility via context menu', async () => {
await rightClickShape(250, 230)
await contextItem('context-toggle-visibility').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const hidden = await page.evaluate((id) => {
const hidden = await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const n = store.graph.getNode(id)
@ -128,14 +112,14 @@ test('toggle visibility via context menu', async () => {
expect(hidden?.visible).toBe(false)
// Toggle back: select via store since invisible nodes can't be hit-tested
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.toggleVisibility()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const restored = await page.evaluate((id) => {
const restored = await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const n = store.graph.getNode(id)
@ -147,7 +131,7 @@ test('toggle visibility via context menu', async () => {
test('toggle lock via context menu', async () => {
await rightClickShape(250, 230)
await contextItem('context-toggle-lock').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const children = await getPageChildren()
const locked = children.find((c) => c.locked)
@ -156,7 +140,7 @@ test('toggle lock via context menu', async () => {
// Unlock
await rightClickShape(250, 230)
await contextItem('context-toggle-lock').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const after = await getPageChildren()
expect(after.every((c) => !c.locked)).toBe(true)
@ -167,22 +151,22 @@ test('delete via context menu removes node', async () => {
await rightClickShape(250, 230)
await contextItem('context-delete').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const countAfter = (await getPageChildren()).length
expect(countAfter).toBe(countBefore - 1)
})
test('group via context menu', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 60, 60)
await canvas.drawRect(200, 100, 60, 60)
await canvas.selectAll()
await canvas.waitForRender()
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 60, 60)
await editor.canvas.drawRect(200, 100, 60, 60)
await editor.canvas.selectAll()
await editor.canvas.waitForRender()
await rightClickShape(130, 130)
await contextItem('context-group').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const children = await getPageChildren()
const group = children.find((c) => c.type === 'GROUP')
@ -191,35 +175,35 @@ test('group via context menu', async () => {
test('ungroup via store after context-menu group', async () => {
// Groups are click-through, so ungroup via store instead
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const group = store.graph.getChildren(store.state.currentPageId).find((n) => n.type === 'GROUP')
if (group) store.select([group.id])
store.ungroupSelected()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const children = await getPageChildren()
expect(children.every((c) => c.type !== 'GROUP')).toBe(true)
expect(children.length).toBe(2)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('create component via context menu', async () => {
await canvas.click(130, 130)
await canvas.waitForRender()
await editor.canvas.click(130, 130)
await editor.canvas.waitForRender()
await rightClickShape(130, 130)
await contextItem('context-create-component').click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const children = await getPageChildren()
const comp = children.find((c) => c.type === 'COMPONENT')
expect(comp).toBeTruthy()
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('Copy/Paste as submenu exists', async () => {
@ -229,10 +213,10 @@ test('Copy/Paste as submenu exists', async () => {
await expect(submenuTrigger).toBeVisible()
await submenuTrigger.hover()
await page.waitForTimeout(300)
await editor.page.waitForTimeout(300)
await expect(contextItem('context-copy-as-svg')).toBeVisible()
await expect(contextItem('context-copy-as-jsx')).toBeVisible()
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
})

View file

@ -1,50 +1,34 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
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()
})
const editor = useEditorSetup()
function designPanel() {
return page.getByTestId('design-panel-single')
return editor.page.getByTestId('design-panel-single')
}
function nodeHeader() {
return page.getByTestId('design-node-header')
return editor.page.getByTestId('design-node-header')
}
function fillSection() {
return page.getByTestId('fill-section')
return editor.page.getByTestId('fill-section')
}
function strokeSection() {
return page.getByTestId('stroke-section')
return editor.page.getByTestId('stroke-section')
}
function positionSection() {
return page.getByTestId('position-section')
return editor.page.getByTestId('position-section')
}
function effectsSection() {
return page.getByTestId('effects-section')
return editor.page.getByTestId('effects-section')
}
function getNode(id: string) {
return page.evaluate((nodeId) => {
return editor.page.evaluate((nodeId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const n = store.graph.getNode(nodeId)
@ -65,7 +49,7 @@ function getNode(id: string) {
}
function getSelectedId() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds][0] ?? null
@ -73,8 +57,8 @@ function getSelectedId() {
}
test('selecting a rectangle shows design panel with type and name', async () => {
await canvas.drawRect(100, 100, 120, 80)
await canvas.waitForRender()
await editor.canvas.drawRect(100, 100, 120, 80)
await editor.canvas.waitForRender()
await expect(designPanel()).toBeVisible()
await expect(nodeHeader()).toContainText('RECTANGLE')
@ -108,16 +92,16 @@ test('clicking color area changes fill color', async () => {
const swatch = fillSection().getByTestId('fill-picker-swatch').first()
await swatch.click()
const colorArea = page.locator('.cursor-crosshair').first()
const colorArea = editor.page.locator('.cursor-crosshair').first()
await expect(colorArea).toBeVisible({ timeout: 5000 })
const box = await colorArea.boundingBox()
await page.mouse.click(
await editor.page.mouse.click(
expectDefined(box, 'color area bounds').x + expectDefined(box, 'color area bounds').width - 10,
expectDefined(box, 'color area bounds').y + 10
)
await canvas.waitForRender()
await page.waitForTimeout(100)
await editor.canvas.waitForRender()
await editor.page.waitForTimeout(100)
const after = await getNode(expectDefined(id, 'selected id'))
const c1 = expectDefined(before, 'before node').fills[0].color
@ -126,13 +110,13 @@ test('clicking color area changes fill color', async () => {
// Close popover — click the swatch again to toggle it off
await swatch.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
})
test('adding a stroke creates stroke section item', async () => {
const addBtn = strokeSection().getByTestId('stroke-section-add')
await addBtn.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const strokeItems = strokeSection().getByTestId('stroke-item')
await expect(strokeItems.first()).toBeVisible()
@ -145,7 +129,7 @@ test('adding a stroke creates stroke section item', async () => {
test('adding an effect creates effect item', async () => {
const addBtn = effectsSection().getByTestId('effects-section-add')
await addBtn.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const effectItems = effectsSection().getByTestId('effect-item')
await expect(effectItems.first()).toBeVisible()
@ -158,7 +142,7 @@ test('adding an effect creates effect item', async () => {
test('adding a second fill shows two fill items', async () => {
const addBtn = fillSection().getByTestId('fill-section-add')
await addBtn.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const fillItems = fillSection().getByTestId('fill-item')
expect(await fillItems.count()).toBe(2)
@ -169,7 +153,7 @@ test('adding a second fill shows two fill items', async () => {
})
test('visibility toggle in appearance section works', async () => {
const visBtn = page.getByTestId('appearance-visibility')
const visBtn = editor.page.getByTestId('appearance-visibility')
await expect(visBtn).toBeVisible()
const id = await getSelectedId()
@ -177,13 +161,13 @@ test('visibility toggle in appearance section works', async () => {
expect(expectDefined(before, 'before node').visible).toBe(true)
await visBtn.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const after = await getNode(expectDefined(id, 'selected id'))
expect(expectDefined(after, 'after node').visible).toBe(false)
await visBtn.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const restored = await getNode(expectDefined(id, 'selected id'))
expect(expectDefined(restored, 'restored node').visible).toBe(true)
@ -193,14 +177,14 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
const id = await getSelectedId()
expect(id).toBeTruthy()
const fillButton = page.getByTestId('fill-visibility-0')
const fillButton = editor.page.getByTestId('fill-visibility-0')
await expect(fillButton).toBeVisible()
const initial = await getNode(expectDefined(id, 'selected id'))
expect(expectDefined(initial, 'initial node').fills[0]?.visible).toBe(true)
await fillButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(fillButton).toHaveAttribute('data-visible', 'false')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
@ -208,19 +192,19 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
).toBe(false)
await fillButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(fillButton).toHaveAttribute('data-visible', 'true')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
?.visible
).toBe(true)
await canvas.undo()
await editor.canvas.undo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
?.visible
).toBe(false)
await canvas.redo()
await editor.canvas.redo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
?.visible
@ -228,9 +212,9 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
const strokeAddButton = strokeSection().getByTestId('stroke-section-add')
await strokeAddButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const strokeButton = page.getByTestId('stroke-visibility-0')
const strokeButton = editor.page.getByTestId('stroke-visibility-0')
await expect(strokeButton).toBeVisible()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
@ -238,7 +222,7 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
).toBe(true)
await strokeButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(strokeButton).toHaveAttribute('data-visible', 'false')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
@ -246,19 +230,19 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
).toBe(false)
await strokeButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(strokeButton).toHaveAttribute('data-visible', 'true')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
?.visible
).toBe(true)
await canvas.undo()
await editor.canvas.undo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
?.visible
).toBe(false)
await canvas.redo()
await editor.canvas.redo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
?.visible
@ -266,9 +250,9 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
const effectAddButton = effectsSection().getByTestId('effects-section-add')
await effectAddButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const effectButton = page.getByTestId('effect-visibility-0')
const effectButton = editor.page.getByTestId('effect-visibility-0')
await expect(effectButton).toBeVisible()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
@ -276,7 +260,7 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
).toBe(true)
await effectButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(effectButton).toHaveAttribute('data-visible', 'false')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
@ -284,19 +268,19 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
).toBe(false)
await effectButton.click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expect(effectButton).toHaveAttribute('data-visible', 'true')
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
?.visible
).toBe(true)
await canvas.undo()
await editor.canvas.undo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
?.visible
).toBe(false)
await canvas.redo()
await editor.canvas.redo()
expect(
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
?.visible
@ -304,19 +288,19 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
})
test('deselecting shows empty design panel', async () => {
await page.keyboard.press('Escape')
await canvas.waitForRender()
await editor.page.keyboard.press('Escape')
await editor.canvas.waitForRender()
await expect(page.getByTestId('design-panel-empty')).toBeVisible()
await expect(editor.page.getByTestId('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()
await editor.canvas.drawRect(300, 100, 60, 60)
await editor.canvas.drawRect(400, 100, 60, 60)
await editor.canvas.selectAll()
await editor.canvas.waitForRender()
const multiHeader = page.getByTestId('design-multi-header')
const multiHeader = editor.page.getByTestId('design-multi-header')
await expect(multiHeader).toBeVisible()
await expect(multiHeader).toContainText('Mixed')
await expect(multiHeader).toContainText('layers')

View file

@ -1,30 +1,10 @@
import { test, expect, type Page } from '@playwright/test'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
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('/?test&no-chrome&no-rulers')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
test.beforeEach(async () => {
await canvas.clearCanvas()
})
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
async function expectCanvas(name: string) {
canvas.assertNoErrors()
const buffer = await canvas.canvas.screenshot()
editor.canvas.assertNoErrors()
const buffer = await editor.canvas.canvas.screenshot()
expect(buffer).toMatchSnapshot(`${name}.png`)
}
@ -33,31 +13,31 @@ test('empty canvas', async () => {
})
test('draw rectangle', async () => {
await canvas.drawRect(100, 100, 200, 150)
await editor.canvas.drawRect(100, 100, 200, 150)
await expectCanvas('draw-rectangle')
})
test('draw ellipse', async () => {
await canvas.drawEllipse(100, 100, 200, 150)
await editor.canvas.drawEllipse(100, 100, 200, 150)
await expectCanvas('draw-ellipse')
})
test('draw rectangle then move it', async () => {
await canvas.drawRect(100, 100, 200, 150)
await canvas.selectTool('select')
await canvas.drag(200, 175, 400, 300)
await canvas.waitForRender()
await editor.canvas.drawRect(100, 100, 200, 150)
await editor.canvas.selectTool('select')
await editor.canvas.drag(200, 175, 400, 300)
await editor.canvas.waitForRender()
await expectCanvas('draw-rectangle-then-move-it')
})
test('draw and delete', async () => {
await canvas.drawRect(100, 100, 200, 150)
await canvas.deleteSelection()
await editor.canvas.drawRect(100, 100, 200, 150)
await editor.canvas.deleteSelection()
await expectCanvas('draw-and-delete')
})
test('draw and undo', async () => {
await canvas.drawRect(100, 100, 200, 150)
await canvas.undo()
await editor.canvas.drawRect(100, 100, 200, 150)
await editor.canvas.undo()
await expectCanvas('draw-and-undo')
})

View file

@ -1,29 +1,9 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
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('/?test')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
test.beforeEach(async () => {
await canvas.clearCanvas()
})
const editor = useEditorSetupWithClear('/?test')
async function rectangleCount() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.graph.nodes.values()].filter((node) => node.type === 'RECTANGLE').length
@ -31,11 +11,11 @@ async function rectangleCount() {
}
async function layerItems() {
return page.getByTestId('layers-item').allTextContents()
return editor.page.getByTestId('layers-item').allTextContents()
}
async function historyState() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return {
@ -48,36 +28,36 @@ async function historyState() {
}
test('undo after option-drag duplicate removes the copy', async () => {
await canvas.drawRect(120, 120, 120, 90)
await editor.canvas.drawRect(120, 120, 120, 90)
await expect.poll(rectangleCount).toBe(1)
await canvas.altDrag(180, 165, 340, 165)
await editor.canvas.altDrag(180, 165, 340, 165)
await expect.poll(rectangleCount).toBe(2)
await expect.poll(layerItems).toEqual(['Rectangle', 'Rectangle copy'])
await canvas.undo()
await editor.canvas.undo()
await expect.poll(rectangleCount).toBe(1)
await expect.poll(layerItems).toEqual(['Rectangle'])
await expect.poll(historyState).toMatchObject({ canRedo: true, redoLabel: 'Duplicate' })
await page.waitForTimeout(1500)
await editor.page.waitForTimeout(1500)
await expect.poll(rectangleCount).toBe(1)
await expect.poll(layerItems).toEqual(['Rectangle'])
await expect.poll(historyState).toMatchObject({ canRedo: true, redoLabel: 'Duplicate' })
await page.keyboard.down('Meta')
await page.keyboard.down('Shift')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Shift')
await page.keyboard.up('Meta')
await canvas.waitForRender()
await editor.page.keyboard.down('Meta')
await editor.page.keyboard.down('Shift')
await editor.page.keyboard.press('KeyZ')
await editor.page.keyboard.up('Shift')
await editor.page.keyboard.up('Meta')
await editor.canvas.waitForRender()
await expect.poll(rectangleCount).toBe(2)
await expect.poll(layerItems).toEqual(['Rectangle', 'Rectangle copy'])
await expect.poll(historyState).toMatchObject({ canUndo: true, undoLabel: 'Duplicate' })
await page.waitForTimeout(1500)
await editor.page.waitForTimeout(1500)
await expect.poll(rectangleCount).toBe(2)
await expect.poll(layerItems).toEqual(['Rectangle', 'Rectangle copy'])
await expect.poll(historyState).toMatchObject({ canUndo: true, undoLabel: 'Duplicate' })
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

42
tests/e2e/fixtures.ts Normal file
View file

@ -0,0 +1,42 @@
import { test, expect, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
export function useEditorSetup(url = '/') {
let page: Page
let canvas: CanvasHelper
test.describe.configure({ mode: 'serial' })
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await page.goto(url)
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
return {
get page() {
return page
},
get canvas() {
return canvas
}
}
}
export function useEditorSetupWithClear(url = '/') {
const ctx = useEditorSetup(url)
test.beforeEach(async () => {
await ctx.canvas.clearCanvas()
})
return ctx
}
export { test, expect }

View file

@ -1,26 +1,10 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
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()
})
const editor = useEditorSetup()
function getActiveTool() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.activeTool
@ -28,7 +12,7 @@ function getActiveTool() {
}
function getSelectedCount() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.selectedIds.size
@ -36,7 +20,7 @@ function getSelectedCount() {
}
function getPageChildren() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
@ -49,7 +33,7 @@ function getPageChildren() {
}
function getUIVisible() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.showUI
@ -57,7 +41,7 @@ function getUIVisible() {
}
function getZoom() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.zoom
@ -66,74 +50,74 @@ function getZoom() {
test.describe('tool switching', () => {
test('V → SELECT', async () => {
await page.keyboard.press('v')
await editor.page.keyboard.press('v')
expect(await getActiveTool()).toBe('SELECT')
})
test('R → RECTANGLE', async () => {
await page.keyboard.press('r')
await editor.page.keyboard.press('r')
expect(await getActiveTool()).toBe('RECTANGLE')
})
test('O → ELLIPSE', async () => {
await page.keyboard.press('o')
await editor.page.keyboard.press('o')
expect(await getActiveTool()).toBe('ELLIPSE')
})
test('F → FRAME', async () => {
await page.keyboard.press('f')
await editor.page.keyboard.press('f')
expect(await getActiveTool()).toBe('FRAME')
})
test('T → TEXT', async () => {
await page.keyboard.press('t')
await editor.page.keyboard.press('t')
expect(await getActiveTool()).toBe('TEXT')
})
test('L → LINE', async () => {
await page.keyboard.press('l')
await editor.page.keyboard.press('l')
expect(await getActiveTool()).toBe('LINE')
})
test('P → PEN', async () => {
await page.keyboard.press('p')
await editor.page.keyboard.press('p')
expect(await getActiveTool()).toBe('PEN')
})
test('H → HAND', async () => {
await page.keyboard.press('h')
await editor.page.keyboard.press('h')
expect(await getActiveTool()).toBe('HAND')
})
test('S → SECTION', async () => {
await page.keyboard.press('s')
await editor.page.keyboard.press('s')
expect(await getActiveTool()).toBe('SECTION')
})
})
test.describe('selection shortcuts', () => {
test('⌘A selects all', async () => {
await page.keyboard.press('v')
await canvas.drawRect(100, 100, 60, 60)
await canvas.drawRect(200, 100, 60, 60)
await editor.page.keyboard.press('v')
await editor.canvas.drawRect(100, 100, 60, 60)
await editor.canvas.drawRect(200, 100, 60, 60)
await page.keyboard.press('Meta+a')
await editor.page.keyboard.press('Meta+a')
expect(await getSelectedCount()).toBe(2)
})
test('Escape clears selection and resets to SELECT tool', async () => {
await page.keyboard.press('Escape')
await editor.page.keyboard.press('Escape')
expect(await getSelectedCount()).toBe(0)
expect(await getActiveTool()).toBe('SELECT')
})
test('Backspace deletes selected', async () => {
await canvas.selectAll()
await editor.canvas.selectAll()
const beforeCount = await getSelectedCount()
expect(beforeCount).toBeGreaterThan(0)
await page.keyboard.press('Backspace')
await canvas.waitForRender()
await editor.page.keyboard.press('Backspace')
await editor.canvas.waitForRender()
const children = await getPageChildren()
expect(children).toHaveLength(0)
@ -142,22 +126,22 @@ test.describe('selection shortcuts', () => {
test.describe('z-order shortcuts', () => {
test('] brings to front', async () => {
await canvas.drawRect(100, 100, 60, 60)
await canvas.drawRect(100, 100, 60, 60)
await editor.canvas.drawRect(100, 100, 60, 60)
await editor.canvas.drawRect(100, 100, 60, 60)
const childrenBefore = await getPageChildren()
const firstId = expectDefined(childrenBefore[0], 'first page child').id
// Select the first (bottom) node
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, firstId)
await canvas.waitForRender()
await editor.canvas.waitForRender()
await page.keyboard.press(']')
await canvas.waitForRender()
await editor.page.keyboard.press(']')
await editor.canvas.waitForRender()
const childrenAfter = await getPageChildren()
expect(childrenAfter[childrenAfter.length - 1].id).toBe(firstId)
@ -168,15 +152,15 @@ test.describe('z-order shortcuts', () => {
const lastId = expectDefined(children.at(-1), 'last page child').id
// Select the last (top) node
await page.evaluate((id) => {
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, lastId)
await canvas.waitForRender()
await editor.canvas.waitForRender()
await page.keyboard.press('[')
await canvas.waitForRender()
await editor.page.keyboard.press('[')
await editor.canvas.waitForRender()
const after = await getPageChildren()
expect(after[0].id).toBe(lastId)
@ -185,11 +169,11 @@ test.describe('z-order shortcuts', () => {
test.describe('group/ungroup shortcuts', () => {
test('⌘G groups selection', async () => {
await canvas.selectAll()
await editor.canvas.selectAll()
expect(await getSelectedCount()).toBeGreaterThanOrEqual(2)
await page.keyboard.press('Meta+g')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+g')
await editor.canvas.waitForRender()
const children = await getPageChildren()
const group = children.find((c) => c.type === 'GROUP')
@ -198,8 +182,8 @@ test.describe('group/ungroup shortcuts', () => {
})
test('⌘⇧G ungroups', async () => {
await page.keyboard.press('Meta+Shift+g')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+Shift+g')
await editor.canvas.waitForRender()
const children = await getPageChildren()
expect(children.every((c) => c.type !== 'GROUP')).toBe(true)
@ -210,28 +194,28 @@ test.describe('UI toggles', () => {
test('⌘\\ toggles UI visibility', async () => {
const before = await getUIVisible()
await page.keyboard.press('Meta+\\')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+\\')
await editor.canvas.waitForRender()
const after = await getUIVisible()
expect(after).toBe(!before)
// Restore
await page.keyboard.press('Meta+\\')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+\\')
await editor.canvas.waitForRender()
expect(await getUIVisible()).toBe(before)
})
})
test.describe('duplicate', () => {
test('⌘D duplicates selection', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 60, 60)
await canvas.selectAll()
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 60, 60)
await editor.canvas.selectAll()
expect(await getSelectedCount()).toBe(1)
await page.keyboard.press('Meta+d')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+d')
await editor.canvas.waitForRender()
const children = await getPageChildren()
expect(children).toHaveLength(2)
@ -240,27 +224,27 @@ test.describe('duplicate', () => {
test.describe('zoom shortcuts', () => {
test('⌘0 zooms to 100%', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 60, 60)
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 60, 60)
// Set zoom to something other than 100%
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.state.zoom = 2
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await page.keyboard.press('Meta+0')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+0')
await editor.canvas.waitForRender()
const zoomAfter = await getZoom()
expect(zoomAfter).toBe(1)
})
test('⌘1 zooms to fit', async () => {
await page.keyboard.press('Meta+1')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+1')
await editor.canvas.waitForRender()
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
@ -268,10 +252,10 @@ test.describe('zoom shortcuts', () => {
})
test('⌘2 zooms to selection', async () => {
await canvas.selectAll()
await editor.canvas.selectAll()
await page.keyboard.press('Meta+2')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+2')
await editor.canvas.waitForRender()
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
@ -279,8 +263,8 @@ test.describe('zoom shortcuts', () => {
})
test('⇧1 zooms to fit (same as ⌘1)', async () => {
await page.keyboard.press('Shift+1')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+1')
await editor.canvas.waitForRender()
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
@ -288,10 +272,10 @@ test.describe('zoom shortcuts', () => {
})
test('⇧2 zooms to selection (same as ⌘2)', async () => {
await canvas.selectAll()
await editor.canvas.selectAll()
await page.keyboard.press('Shift+2')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+2')
await editor.canvas.waitForRender()
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
@ -301,19 +285,19 @@ test.describe('zoom shortcuts', () => {
test.describe('undo/redo', () => {
test('⌘Z undoes last action', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 60, 60)
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 60, 60)
expect((await getPageChildren()).length).toBe(1)
await page.keyboard.press('Meta+z')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+z')
await editor.canvas.waitForRender()
expect((await getPageChildren()).length).toBe(0)
})
test('⌘⇧Z redoes undone action', async () => {
await page.keyboard.press('Meta+Shift+z')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+Shift+z')
await editor.canvas.waitForRender()
expect((await getPageChildren()).length).toBe(1)
})
@ -321,20 +305,20 @@ test.describe('undo/redo', () => {
test.describe('auto-layout shortcut', () => {
test('⇧A toggles auto-layout on frame', async () => {
await canvas.clearCanvas()
await canvas.drawRect(100, 100, 200, 200)
await canvas.selectAll()
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 200, 200)
await editor.canvas.selectAll()
// Change to frame type for auto-layout
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const nodes = [...store.state.selectedIds]
if (nodes[0]) store.updateNode(nodes[0], { type: 'FRAME' })
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const layoutBefore = await page.evaluate(() => {
const layoutBefore = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const nodes = [...store.state.selectedIds]
@ -342,10 +326,10 @@ test.describe('auto-layout shortcut', () => {
})
expect(layoutBefore).toBe('NONE')
await page.keyboard.press('Shift+a')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+a')
await editor.canvas.waitForRender()
const layoutAfter = await page.evaluate(() => {
const layoutAfter = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const nodes = [...store.state.selectedIds]
@ -354,10 +338,10 @@ test.describe('auto-layout shortcut', () => {
expect(layoutAfter).toBe('VERTICAL')
// Toggle off
await page.keyboard.press('Shift+a')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+a')
await editor.canvas.waitForRender()
const layoutFinal = await page.evaluate(() => {
const layoutFinal = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const nodes = [...store.state.selectedIds]

View file

@ -1,25 +1,9 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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('/demo')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
const editor = useEditorSetup('/demo')
function layerRows() {
return page.locator('[data-node-id]')
return editor.page.locator('[data-node-id]')
}
async function getLayerNames(): Promise<string[]> {
@ -40,7 +24,7 @@ interface SceneTreeNode {
}
async function getSceneTree(): Promise<SceneTreeNode> {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) return null
@ -60,7 +44,7 @@ async function getSceneTree(): Promise<SceneTreeNode> {
}
async function getSelectedCount(): Promise<number> {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.selectedIds.size
@ -82,8 +66,8 @@ test('clicking a node inside a frame does not reparent it', async () => {
expect(sidebarBefore).toBeTruthy()
// Click inside the App Preview section area
await canvas.click(350, 310)
await canvas.waitForRender()
await editor.canvas.click(350, 310)
await editor.canvas.waitForRender()
// Sidebar should still be a child of Dashboard
const afterTree = await getSceneTree()
@ -91,17 +75,17 @@ test('clicking a node inside a frame does not reparent it', async () => {
const afterDashboard = afterSection?.children.find((c) => c.name === 'Dashboard')
expect(afterDashboard?.children.find((c) => c.name === 'Sidebar')).toBeTruthy()
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('creating a shape updates layers', async () => {
const before = await getLayerNames()
await canvas.drawRect(600, 500, 50, 50)
await editor.canvas.drawRect(600, 500, 50, 50)
const names = await getLayerNames()
expect(names).toContain('Rectangle')
expect(names.length).toBe(before.length + 1)
await canvas.undo()
await editor.canvas.undo()
const after = await getLayerNames()
expect(after.length).toBe(before.length)
expect(after).not.toContain('Rectangle')
@ -109,16 +93,16 @@ test('creating a shape updates layers', async () => {
test('Shift+A wraps selection in auto-layout frame', async () => {
// Draw two loose rectangles for this test
await canvas.drawRect(700, 600, 60, 60)
await canvas.drawRect(800, 600, 60, 60)
await canvas.selectAll()
await editor.canvas.drawRect(700, 600, 60, 60)
await editor.canvas.drawRect(800, 600, 60, 60)
await editor.canvas.selectAll()
const count = await getSelectedCount()
expect(count).toBeGreaterThanOrEqual(2)
const before = await getLayerNames()
await page.keyboard.press('Shift+A')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+A')
await editor.canvas.waitForRender()
const tree = await getSceneTree()
const autoFrame = tree.children.find((c) => c.name === 'Frame' && c.type === 'FRAME')
@ -128,22 +112,22 @@ test('Shift+A wraps selection in auto-layout frame', async () => {
expect(after).not.toEqual(before)
expect(after).toContain('Frame')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('grouping updates layers', async () => {
// Undo the auto-layout to restore flat structure
await canvas.undo()
await canvas.undo()
await canvas.waitForRender()
await editor.canvas.undo()
await editor.canvas.undo()
await editor.canvas.waitForRender()
// Draw two rects and group them
await canvas.drawRect(700, 600, 60, 60)
await canvas.drawRect(800, 600, 60, 60)
await canvas.selectAll()
await editor.canvas.drawRect(700, 600, 60, 60)
await editor.canvas.drawRect(800, 600, 60, 60)
await editor.canvas.selectAll()
await page.keyboard.press('Meta+g')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+g')
await editor.canvas.waitForRender()
const tree = await getSceneTree()
const group = tree.children.find((c) => c.name === 'Group' && c.type === 'GROUP')
@ -152,72 +136,72 @@ test('grouping updates layers', async () => {
const names = await getLayerNames()
expect(names).toContain('Group')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('ungrouping updates layers', async () => {
await page.keyboard.press('Shift+Meta+g')
await canvas.waitForRender()
await editor.page.keyboard.press('Shift+Meta+g')
await editor.canvas.waitForRender()
const names = await getLayerNames()
expect(names).not.toContain('Group')
expect(names).toContain('Rectangle')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('double-click layer to rename', async () => {
await canvas.drawRect(900, 600, 50, 50)
await canvas.waitForRender()
await editor.canvas.drawRect(900, 600, 50, 50)
await editor.canvas.waitForRender()
const row = layerRows().filter({ hasText: 'Rectangle' }).first()
await row.dblclick()
const input = page.getByTestId('layers-item-input')
const input = editor.page.getByTestId('layers-item-input')
await expect(input).toBeVisible()
await input.fill('Renamed Layer')
await input.press('Enter')
await canvas.waitForRender()
await editor.canvas.waitForRender()
const names = await getLayerNames()
expect(names).toContain('Renamed Layer')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('clicking outside rename input commits', async () => {
const row = layerRows().filter({ hasText: 'Renamed Layer' }).first()
await row.dblclick()
const input = page.getByTestId('layers-item-input')
const input = editor.page.getByTestId('layers-item-input')
await expect(input).toBeVisible()
await input.fill('After Outside Click')
await page.mouse.click(500, 400)
await canvas.waitForRender()
await editor.page.mouse.click(500, 400)
await editor.canvas.waitForRender()
await expect(input).not.toBeVisible()
const names = await getLayerNames()
expect(names).toContain('After Outside Click')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('clearing a layer name falls back to the default node name', async () => {
await canvas.drawRect(980, 600, 50, 50)
await canvas.waitForRender()
await editor.canvas.drawRect(980, 600, 50, 50)
await editor.canvas.waitForRender()
const row = layerRows().filter({ hasText: 'Rectangle' }).last()
const countBefore = await layerRows().count()
await row.dblclick()
const input = page.getByTestId('layers-item-input')
const input = editor.page.getByTestId('layers-item-input')
await expect(input).toBeVisible()
await input.clear()
await input.press('Enter')
await canvas.waitForRender()
await editor.canvas.waitForRender()
const countAfter = await layerRows().count()
expect(countAfter).toBe(countBefore)
@ -225,7 +209,7 @@ test('clearing a layer name falls back to the default node name', async () => {
const names = await getLayerNames()
expect(names.filter((name) => name === 'Rectangle').length).toBeGreaterThan(0)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('double-click does not toggle tree expand', async () => {
@ -233,14 +217,14 @@ test('double-click does not toggle tree expand', async () => {
const containerRow = layerRows().filter({ hasText: 'Components' }).first()
await containerRow.dblclick()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const input = page.getByTestId('layers-item-input')
const input = editor.page.getByTestId('layers-item-input')
await expect(input).toBeVisible()
await input.press('Escape')
const rowCountAfter = await layerRows().count()
expect(rowCountAfter).toBe(rowCountBefore)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

View file

@ -1,25 +1,9 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
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()
})
const editor = useEditorSetup()
function getPages() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getPages().map((p) => ({ id: p.id, name: p.name }))
@ -27,7 +11,7 @@ function getPages() {
}
function getCurrentPageId() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.currentPageId
@ -35,7 +19,7 @@ function getCurrentPageId() {
}
function getPageChildCount() {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getChildren(store.state.currentPageId).length
@ -43,15 +27,15 @@ function getPageChildCount() {
}
function pagesPanel() {
return page.getByTestId('pages-panel')
return editor.page.getByTestId('pages-panel')
}
function pageItems() {
return page.getByTestId('pages-item')
return editor.page.getByTestId('pages-item')
}
function addPageButton() {
return page.getByTestId('pages-add')
return editor.page.getByTestId('pages-add')
}
test('initial state has one page', async () => {
@ -67,7 +51,7 @@ test('pages panel shows current page', async () => {
test('add page creates a second page', async () => {
await addPageButton().click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const pages = await getPages()
expect(pages).toHaveLength(2)
@ -86,15 +70,15 @@ test('new page is empty', async () => {
})
test('drawing on new page adds nodes only to it', async () => {
await canvas.drawRect(100, 100, 80, 60)
await canvas.waitForRender()
await editor.canvas.drawRect(100, 100, 80, 60)
await editor.canvas.waitForRender()
expect(await getPageChildCount()).toBe(1)
})
test('switching to first page shows its content', async () => {
await pageItems().first().click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const pages = await getPages()
const currentId = await getCurrentPageId()
@ -107,14 +91,14 @@ test('first page has no shapes (we never drew on it)', async () => {
test('switching back to second page shows its shape', async () => {
await pageItems().nth(1).click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
expect(await getPageChildCount()).toBe(1)
})
test('add a third page', async () => {
await addPageButton().click()
await canvas.waitForRender()
await editor.canvas.waitForRender()
const pages = await getPages()
expect(pages).toHaveLength(3)
@ -125,12 +109,12 @@ test('delete current page switches to adjacent', async () => {
const pagesBefore = await getPages()
const deletingId = await getCurrentPageId()
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.deletePage(store.state.currentPageId)
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const pagesAfter = await getPages()
expect(pagesAfter).toHaveLength(pagesBefore.length - 1)
@ -143,7 +127,7 @@ test('delete current page switches to adjacent', async () => {
test('rename page via store', async () => {
const currentId = await getCurrentPageId()
await page.evaluate(
await editor.page.evaluate(
([id, name]) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
@ -151,75 +135,75 @@ test('rename page via store', async () => {
},
[currentId, 'Renamed Page'] as [string, string]
)
await canvas.waitForRender()
await editor.canvas.waitForRender()
const updated = await getPages()
const renamed = updated.find((p) => p.id === currentId)
expect(renamed?.name).toBe('Renamed Page')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('double-click page to rename', async () => {
const item = pageItems().first()
await item.dblclick()
const input = page.getByTestId('pages-item-input')
const input = editor.page.getByTestId('pages-item-input')
await expect(input).toBeVisible()
await input.fill('My Page')
await input.press('Enter')
await canvas.waitForRender()
await editor.canvas.waitForRender()
const pages = await getPages()
expect(pages.some((p) => p.name === 'My Page')).toBe(true)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('clicking outside page rename input commits', async () => {
const item = pageItems().first()
await item.dblclick()
const input = page.getByTestId('pages-item-input')
const input = editor.page.getByTestId('pages-item-input')
await expect(input).toBeVisible()
await input.fill('Outside Click Page')
// Click on the page header label to trigger blur (outside the input but still in the panel)
await page.getByTestId('pages-header').click()
await canvas.waitForRender()
await editor.page.getByTestId('pages-header').click()
await editor.canvas.waitForRender()
await expect(input).not.toBeVisible()
const pages = await getPages()
expect(pages.some((p) => p.name === 'Outside Click Page')).toBe(true)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('cannot delete the last page', async () => {
// Delete until 1 remains
let pages = await getPages()
while (pages.length > 1) {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.deletePage(store.state.currentPageId)
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
pages = await getPages()
}
expect(pages).toHaveLength(1)
// Try deleting the last one — should be a no-op
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.deletePage(store.state.currentPageId)
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
const after = await getPages()
expect(after).toHaveLength(1)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

View file

@ -1,30 +1,15 @@
import { test, expect, type Page } from '@playwright/test'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { expectDefined } from '#tests/helpers/assert'
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()
})
const editor = useEditorSetup()
test('layers panel resize increases width', async () => {
const panel = page.getByTestId('layers-panel')
const panel = editor.page.getByTestId('layers-panel')
const before = await panel.boundingBox()
expect(before).not.toBeNull()
const handle = page.getByTestId('left-splitter-handle')
const handle = editor.page.getByTestId('left-splitter-handle')
const handleBox = await handle.boundingBox()
expect(handleBox).not.toBeNull()
@ -33,49 +18,48 @@ test('layers panel resize increases width', async () => {
const cx = handleBounds.x + handleBounds.width / 2
const cy = handleBounds.y + handleBounds.height / 2
await page.mouse.move(cx, cy)
await page.mouse.down()
await page.mouse.move(cx + 80, cy, { steps: 10 })
await page.mouse.up()
await canvas.waitForRender()
await editor.page.mouse.move(cx, cy)
await editor.page.mouse.down()
await editor.page.mouse.move(cx + 80, cy, { steps: 10 })
await editor.page.mouse.up()
await editor.canvas.waitForRender()
const after = expectDefined(await panel.boundingBox(), 'resized layers panel bounds')
expect(after.width).toBeGreaterThan(beforeBounds.width + 40)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('panel width persists after page reload', async () => {
// Allow Reka's auto-save debounce to flush before recording the width
await page.waitForTimeout(300)
await editor.page.waitForTimeout(300)
const recordedWidth = expectDefined(
await page.getByTestId('layers-panel').boundingBox(),
await editor.page.getByTestId('layers-panel').boundingBox(),
'persisted layers panel bounds'
).width
await page.reload()
canvas = new CanvasHelper(page)
await canvas.waitForInit()
await editor.page.reload()
await editor.canvas.waitForInit()
const after = expectDefined(
await page.getByTestId('layers-panel').boundingBox(),
await editor.page.getByTestId('layers-panel').boundingBox(),
'reloaded layers panel bounds'
)
expect(Math.abs(after.width - recordedWidth)).toBeLessThanOrEqual(2)
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})
test('Cmd+Backslash hides panels', async () => {
await page.keyboard.press('Meta+\\')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+\\')
await editor.canvas.waitForRender()
await expect(page.getByTestId('layers-panel')).not.toBeVisible()
canvas.assertNoErrors()
await expect(editor.page.getByTestId('layers-panel')).not.toBeVisible()
editor.canvas.assertNoErrors()
})
test('Cmd+Backslash shows panels again', async () => {
await page.keyboard.press('Meta+\\')
await canvas.waitForRender()
await editor.page.keyboard.press('Meta+\\')
await editor.canvas.waitForRender()
await expect(page.getByTestId('layers-panel')).toBeVisible()
canvas.assertNoErrors()
await expect(editor.page.getByTestId('layers-panel')).toBeVisible()
editor.canvas.assertNoErrors()
})

View file

@ -1,6 +1,7 @@
import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
import { getSelectedNode } from '#tests/helpers/store'
let page: Page
let canvas: CanvasHelper
@ -18,21 +19,6 @@ test.afterAll(async () => {
await page.close()
})
async function getSelectedNodeFlags() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
if (!id) return null
const n = store.graph.getNode(id)
if (!n) return null
return {
type: n.type,
independentCorners: n.independentCorners,
independentStrokeWeights: n.independentStrokeWeights
}
})
}
async function drawFrame(x: number, y: number, w: number, h: number) {
await canvas.pressKey('f')
@ -44,7 +30,7 @@ test('independent corners toggle shows per-corner inputs', async () => {
await drawFrame(120, 120, 120, 80)
await canvas.waitForRender()
const flags = await getSelectedNodeFlags()
const flags = await getSelectedNode(page)
expect(flags?.type).toBe('FRAME')
expect(flags?.independentCorners).toBe(false)
@ -54,7 +40,7 @@ test('independent corners toggle shows per-corner inputs', async () => {
await toggle.click()
await canvas.waitForRender()
expect((await getSelectedNodeFlags())?.independentCorners).toBe(true)
expect((await getSelectedNode(page))?.independentCorners).toBe(true)
const grid = page.getByTestId('independent-corners-grid')
await expect(grid).toBeVisible()
const cornerInputs = grid.getByTestId('scrub-input')

View file

@ -1,35 +1,15 @@
import { test, expect, type Page } from '@playwright/test'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
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('/?test&no-chrome&no-rulers')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
test.beforeEach(async () => {
await canvas.clearCanvas()
})
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
async function expectCanvas(name: string) {
canvas.assertNoErrors()
const buffer = await canvas.canvas.screenshot()
editor.canvas.assertNoErrors()
const buffer = await editor.canvas.canvas.screenshot()
expect(buffer).toMatchSnapshot(`${name}.png`)
}
test('drop shadow on white card', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -55,12 +35,12 @@ test('drop shadow on white card', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('drop-shadow-white-card')
})
test('drop shadow with spread', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -86,12 +66,12 @@ test('drop shadow with spread', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('drop-shadow-with-spread')
})
test('inner shadow', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -119,12 +99,12 @@ test('inner shadow', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('inner-shadow')
})
test('inner shadow with spread', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -152,12 +132,12 @@ test('inner shadow with spread', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('inner-shadow-with-spread')
})
test('drop shadow on ellipse', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -184,12 +164,12 @@ test('drop shadow on ellipse', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('drop-shadow-ellipse')
})
test('combined drop and inner shadow', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -223,12 +203,12 @@ test('combined drop and inner shadow', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('combined-drop-inner-shadow')
})
test('text drop shadow on glyphs', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -257,14 +237,14 @@ test('text drop shadow on glyphs', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await page.waitForTimeout(500)
await canvas.waitForRender()
await editor.canvas.waitForRender()
await editor.page.waitForTimeout(500)
await editor.canvas.waitForRender()
await expectCanvas('text-drop-shadow')
})
test('layer blur', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -292,12 +272,12 @@ test('layer blur', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('layer-blur')
})
test('invisible effect has no visual impact', async () => {
await page.evaluate(() => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
@ -323,6 +303,6 @@ test('invisible effect has no visual impact', async () => {
store.clearSelection()
store.requestRender()
})
await canvas.waitForRender()
await editor.canvas.waitForRender()
await expectCanvas('invisible-effect')
})

View file

@ -2,6 +2,7 @@ import { expect, test, type Page } from '@playwright/test'
import { expectDefined } from '#tests/helpers/assert'
import { CanvasHelper } from '#tests/helpers/canvas'
import { getSelectedNode } from '#tests/helpers/store'
let page: Page
let canvas: CanvasHelper
@ -19,21 +20,6 @@ test.afterAll(async () => {
await page.close()
})
function getSelectedNode() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
if (!id) return null
const n = store.graph.getNode(id)
if (!n) return null
return {
fills: n.fills,
strokes: n.strokes,
visible: n.visible
}
})
}
test('fill visibility supports repeat click and undo redo', async () => {
await canvas.drawRect(120, 120, 120, 80)
@ -41,21 +27,21 @@ test('fill visibility supports repeat click and undo redo', async () => {
const fillButton = page.getByTestId('fill-visibility-0')
await expect(fillButton).toBeVisible()
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').fills[0]?.visible).toBe(true)
await fillButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').fills[0]?.visible).toBe(false)
await fillButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').fills[0]?.visible).toBe(true)
await canvas.undo()
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').fills[0]?.visible).toBe(false)
await canvas.redo()
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').fills[0]?.visible).toBe(true)
})
test('stroke visibility supports repeat click and undo redo', async () => {
@ -64,39 +50,39 @@ test('stroke visibility supports repeat click and undo redo', async () => {
const strokeButton = page.getByTestId('stroke-visibility-0')
await expect(strokeButton).toBeVisible()
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').strokes[0]?.visible).toBe(true)
await strokeButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').strokes[0]?.visible).toBe(false)
await strokeButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').strokes[0]?.visible).toBe(true)
await canvas.undo()
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').strokes[0]?.visible).toBe(false)
await canvas.redo()
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').strokes[0]?.visible).toBe(true)
})
test('appearance visibility supports repeat click and undo redo in one step', async () => {
const visibilityButton = page.getByTestId('appearance-visibility')
await expect(visibilityButton).toBeVisible()
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').visible).toBe(true)
await visibilityButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').visible).toBe(false)
await visibilityButton.click()
await canvas.waitForRender()
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').visible).toBe(true)
await canvas.undo()
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(false)
expect(expectDefined(await getSelectedNode(page), 'selected node').visible).toBe(false)
await canvas.undo()
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
expect(expectDefined(await getSelectedNode(page), 'selected node').visible).toBe(true)
})

View file

@ -2,6 +2,7 @@ import { expect, test, type Page } from '@playwright/test'
import { expectDefined } from '#tests/helpers/assert'
import { CanvasHelper } from '#tests/helpers/canvas'
import { getSelectedNode } from '#tests/helpers/store'
let page: Page
let canvas: CanvasHelper
@ -19,27 +20,6 @@ test.afterAll(async () => {
await page.close()
})
function getSelectedNode() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
if (!id) return null
const n = store.graph.getNode(id)
if (!n) return null
return {
id: n.id,
type: n.type,
name: n.name,
text: n.text,
fontSize: n.fontSize,
fontFamily: n.fontFamily,
fontWeight: n.fontWeight,
width: n.width,
height: n.height
}
})
}
function getPageChildren() {
return page.evaluate(() => {
@ -77,7 +57,7 @@ test('clicking with text tool creates a text node', async () => {
})
test('text node is selected after creation', async () => {
const node = await getSelectedNode()
const node = await getSelectedNode(page)
expect(node?.type).toBe('TEXT')
})
@ -95,7 +75,7 @@ test('typography section appears for text node', async () => {
})
test('text node has default font properties', async () => {
const node = await getSelectedNode()
const node = await getSelectedNode(page)
expect(node?.fontSize).toBeGreaterThan(0)
expect(node?.fontFamily).toBeTruthy()
})
@ -113,7 +93,7 @@ test('creating text via store works', async () => {
})
await canvas.waitForRender()
const node = await getSelectedNode()
const node = await getSelectedNode(page)
expect(node?.text).toBe('Hello World')
expect(node?.fontSize).toBe(24)
})
@ -133,7 +113,7 @@ test('frame tool creates FRAME node', async () => {
await canvas.drag(400, 100, 600, 250)
await canvas.waitForRender()
const node = expectDefined(await getSelectedNode(), 'selected frame')
const node = expectDefined(await getSelectedNode(page), 'selected frame')
expect(node.type).toBe('FRAME')
expect(node.width).toBeGreaterThan(0)
expect(node.height).toBeGreaterThan(0)
@ -156,7 +136,7 @@ test('Enter key opens text editing and selects all without erasing', async () =>
})
await canvas.waitForRender()
const before = await getSelectedNode()
const before = await getSelectedNode(page)
expect(before?.text).toBe('Keep this text')
expect(before?.type).toBe('TEXT')

View file

@ -1,34 +1,14 @@
import { expect, test, type Page } from '@playwright/test'
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
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('/?test')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
test.beforeEach(async () => {
await canvas.clearCanvas()
})
const editor = useEditorSetupWithClear('/?test')
test('draw section in full editor without browser errors', async () => {
await canvas.drawSection(100, 100, 240, 160)
await editor.canvas.drawSection(100, 100, 240, 160)
await expect(page.getByTestId('design-node-header')).toContainText('SECTION')
await expect(editor.page.getByTestId('design-node-header')).toContainText('SECTION')
await expect
.poll(async () => {
return page.evaluate(() => {
return editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
const selectedId = [...store.state.selectedIds][0]
return selectedId ? store.graph.getNode(selectedId)?.type : null
@ -36,5 +16,5 @@ test('draw section in full editor without browser errors', async () => {
})
.toBe('SECTION')
canvas.assertNoErrors()
editor.canvas.assertNoErrors()
})

View file

@ -37,26 +37,55 @@ export function getSelectedNode(page: Page) {
return {
id: n.id,
type: n.type,
name: n.name,
text: n.text,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
rotation: n.rotation,
visible: n.visible,
layoutMode: n.layoutMode,
primaryAxisAlign: n.primaryAxisAlign,
counterAxisAlign: n.counterAxisAlign,
itemSpacing: n.itemSpacing,
childIds: n.childIds,
cornerRadius: n.cornerRadius,
independentCorners: n.independentCorners,
independentStrokeWeights: n.independentStrokeWeights,
flipX: n.flipX,
clipsContent: n.clipsContent,
fills: n.fills,
strokes: n.strokes,
fontSize: n.fontSize,
fontFamily: n.fontFamily,
fontWeight: n.fontWeight,
italic: n.italic
}
})
}
export function getSelectedNodes(page: Page) {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds].map((id) => {
const n = store.graph.getNode(id)
if (!n) throw new Error(`Selected node ${id} not found`)
return {
id: n.id,
name: n.name,
type: n.type,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
fills: n.fills
}
})
})
}
export function getNodeById(page: Page, id: string) {
return page.evaluate((nodeId: string) => {
const store = window.openPencil?.getStore?.()