From 11e38b626a17d2928ac85a8aa590c9302e22dc32 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 16 May 2026 16:19:03 +0300 Subject: [PATCH] test(e2e): extract shared editor setup fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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%) --- scripts/steiger-rules.ts | 3 +- tests/e2e/app/menu.spec.ts | 82 ++++---- tests/e2e/autosave.spec.ts | 35 +--- tests/e2e/canvas/frame-overlays.spec.ts | 36 +--- tests/e2e/clipboard/copy-paste.spec.ts | 25 +-- .../clipboard/paste-into-container.spec.ts | 72 +++---- tests/e2e/code/panel.spec.ts | 48 ++--- tests/e2e/color-picker/basic.spec.ts | 48 ++--- tests/e2e/components.spec.ts | 74 +++---- tests/e2e/context-menu/basic.spec.ts | 98 ++++------ tests/e2e/design/panel.spec.ts | 112 +++++------ tests/e2e/editor/create-shapes.spec.ts | 48 ++--- tests/e2e/editor/duplicate-undo.spec.ts | 54 ++---- tests/e2e/fixtures.ts | 42 ++++ tests/e2e/keyboard/shortcuts.spec.ts | 180 ++++++++---------- tests/e2e/layers/panel.spec.ts | 106 +++++------ tests/e2e/pages/multi-page.spec.ts | 78 +++----- tests/e2e/panels/basic.spec.ts | 64 +++---- .../properties/corner-stroke-toggles.spec.ts | 20 +- tests/e2e/properties/effects.spec.ts | 68 +++---- tests/e2e/properties/visibility.spec.ts | 46 ++--- tests/e2e/text/editing.spec.ts | 32 +--- tests/e2e/tools/section.spec.ts | 32 +--- tests/helpers/store.ts | 29 +++ 24 files changed, 573 insertions(+), 859 deletions(-) create mode 100644 tests/e2e/fixtures.ts diff --git a/scripts/steiger-rules.ts b/scripts/steiger-rules.ts index 7bf7ec8e1..e6bf49a0a 100644 --- a/scripts/steiger-rules.ts +++ b/scripts/steiger-rules.ts @@ -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.' diff --git a/tests/e2e/app/menu.spec.ts b/tests/e2e/app/menu.spec.ts index ccd148d62..6b3e7630d 100644 --- a/tests/e2e/app/menu.spec.ts +++ b/tests/e2e/app/menu.spec.ts @@ -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) diff --git a/tests/e2e/autosave.spec.ts b/tests/e2e/autosave.spec.ts index 873bd2096..431a3af34 100644 --- a/tests/e2e/autosave.spec.ts +++ b/tests/e2e/autosave.spec.ts @@ -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 diff --git a/tests/e2e/canvas/frame-overlays.spec.ts b/tests/e2e/canvas/frame-overlays.spec.ts index 13f38e986..5095cdeeb 100644 --- a/tests/e2e/canvas/frame-overlays.spec.ts +++ b/tests/e2e/canvas/frame-overlays.spec.ts @@ -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') }) diff --git a/tests/e2e/clipboard/copy-paste.spec.ts b/tests/e2e/clipboard/copy-paste.spec.ts index ee328313c..449f76176 100644 --- a/tests/e2e/clipboard/copy-paste.spec.ts +++ b/tests/e2e/clipboard/copy-paste.spec.ts @@ -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) }) diff --git a/tests/e2e/clipboard/paste-into-container.spec.ts b/tests/e2e/clipboard/paste-into-container.spec.ts index 1cc19a252..0b7d0cf74 100644 --- a/tests/e2e/clipboard/paste-into-container.spec.ts +++ b/tests/e2e/clipboard/paste-into-container.spec.ts @@ -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() }) diff --git a/tests/e2e/code/panel.spec.ts b/tests/e2e/code/panel.spec.ts index 22ce75ce1..cbe4cb6a5 100644 --- a/tests/e2e/code/panel.spec.ts +++ b/tests/e2e/code/panel.spec.ts @@ -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() }) diff --git a/tests/e2e/color-picker/basic.spec.ts b/tests/e2e/color-picker/basic.spec.ts index e5a7628ba..61739d3e4 100644 --- a/tests/e2e/color-picker/basic.spec.ts +++ b/tests/e2e/color-picker/basic.spec.ts @@ -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() diff --git a/tests/e2e/components.spec.ts b/tests/e2e/components.spec.ts index 4c8d1ca64..06fe8ac27 100644 --- a/tests/e2e/components.spec.ts +++ b/tests/e2e/components.spec.ts @@ -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() }) diff --git a/tests/e2e/context-menu/basic.spec.ts b/tests/e2e/context-menu/basic.spec.ts index 2437bf72c..35131dce6 100644 --- a/tests/e2e/context-menu/basic.spec.ts +++ b/tests/e2e/context-menu/basic.spec.ts @@ -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') }) diff --git a/tests/e2e/design/panel.spec.ts b/tests/e2e/design/panel.spec.ts index 11f1ddbed..aab7da392 100644 --- a/tests/e2e/design/panel.spec.ts +++ b/tests/e2e/design/panel.spec.ts @@ -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') diff --git a/tests/e2e/editor/create-shapes.spec.ts b/tests/e2e/editor/create-shapes.spec.ts index 81b03ccc6..0aa339b34 100644 --- a/tests/e2e/editor/create-shapes.spec.ts +++ b/tests/e2e/editor/create-shapes.spec.ts @@ -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') }) diff --git a/tests/e2e/editor/duplicate-undo.spec.ts b/tests/e2e/editor/duplicate-undo.spec.ts index 690cb7c66..82cfbe5eb 100644 --- a/tests/e2e/editor/duplicate-undo.spec.ts +++ b/tests/e2e/editor/duplicate-undo.spec.ts @@ -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() }) diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts new file mode 100644 index 000000000..bb851f358 --- /dev/null +++ b/tests/e2e/fixtures.ts @@ -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 } diff --git a/tests/e2e/keyboard/shortcuts.spec.ts b/tests/e2e/keyboard/shortcuts.spec.ts index 803fff155..328c4185a 100644 --- a/tests/e2e/keyboard/shortcuts.spec.ts +++ b/tests/e2e/keyboard/shortcuts.spec.ts @@ -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] diff --git a/tests/e2e/layers/panel.spec.ts b/tests/e2e/layers/panel.spec.ts index a51d4433f..61a6bc7b1 100644 --- a/tests/e2e/layers/panel.spec.ts +++ b/tests/e2e/layers/panel.spec.ts @@ -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 { @@ -40,7 +24,7 @@ interface SceneTreeNode { } async function getSceneTree(): Promise { - return page.evaluate(() => { + return editor.page.evaluate(() => { const store = window.openPencil?.getStore?.() if (!store) return null @@ -60,7 +44,7 @@ async function getSceneTree(): Promise { } async function getSelectedCount(): Promise { - 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() }) diff --git a/tests/e2e/pages/multi-page.spec.ts b/tests/e2e/pages/multi-page.spec.ts index ca0b932ec..937c9ca11 100644 --- a/tests/e2e/pages/multi-page.spec.ts +++ b/tests/e2e/pages/multi-page.spec.ts @@ -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() }) diff --git a/tests/e2e/panels/basic.spec.ts b/tests/e2e/panels/basic.spec.ts index 2b1b5cc9b..07267e6f5 100644 --- a/tests/e2e/panels/basic.spec.ts +++ b/tests/e2e/panels/basic.spec.ts @@ -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() }) diff --git a/tests/e2e/properties/corner-stroke-toggles.spec.ts b/tests/e2e/properties/corner-stroke-toggles.spec.ts index 9e4b7a173..fdd3de907 100644 --- a/tests/e2e/properties/corner-stroke-toggles.spec.ts +++ b/tests/e2e/properties/corner-stroke-toggles.spec.ts @@ -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') diff --git a/tests/e2e/properties/effects.spec.ts b/tests/e2e/properties/effects.spec.ts index c2eba396f..feed04a8a 100644 --- a/tests/e2e/properties/effects.spec.ts +++ b/tests/e2e/properties/effects.spec.ts @@ -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') }) diff --git a/tests/e2e/properties/visibility.spec.ts b/tests/e2e/properties/visibility.spec.ts index 72f5d5966..a5c2b3920 100644 --- a/tests/e2e/properties/visibility.spec.ts +++ b/tests/e2e/properties/visibility.spec.ts @@ -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) }) diff --git a/tests/e2e/text/editing.spec.ts b/tests/e2e/text/editing.spec.ts index bb16cf9f5..7b093f5e2 100644 --- a/tests/e2e/text/editing.spec.ts +++ b/tests/e2e/text/editing.spec.ts @@ -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') diff --git a/tests/e2e/tools/section.spec.ts b/tests/e2e/tools/section.spec.ts index 66a846cd3..c80ef6ed6 100644 --- a/tests/e2e/tools/section.spec.ts +++ b/tests/e2e/tools/section.spec.ts @@ -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() }) diff --git a/tests/helpers/store.ts b/tests/helpers/store.ts index bc0732203..48af3f89a 100644 --- a/tests/helpers/store.ts +++ b/tests/helpers/store.ts @@ -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?.()