Add 8 E2E test suites for release sanity coverage
83 new tests across 8 files: - design-panel: selection → properties panel, fill/stroke/effect add, visibility toggle, multi-select - code-panel: JSX output, OpenPencil/Tailwind toggle, copy button, empty state - components: create component (⌘⌥K), create instance, propagation, detach - copy-paste: store copy/paste, ⌘D duplicate, cut, multi-select duplicate - multi-page: add/switch/delete pages, rename, last-page protection - text-editing: text tool, text node creation, typography section, frame tool - keyboard-shortcuts: all 9 tool keys, ⌘A/Escape/Backspace, ]/[, ⌘G/⌘⇧G, ⌘\, ⌘Z/⌘⇧Z - context-menu: right-click items, duplicate/visibility/lock/delete/group/component, Copy as submenu
This commit is contained in:
parent
7fbe21c8e1
commit
14b796a40d
120
tests/e2e/code-panel.spec.ts
Normal file
120
tests/e2e/code-panel.spec.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function codeTab() {
|
||||
return page.locator('[data-test-id="properties-tab-code"]')
|
||||
}
|
||||
|
||||
function designTab() {
|
||||
return page.locator('[data-test-id="properties-tab-design"]')
|
||||
}
|
||||
|
||||
function codePanel() {
|
||||
return page.locator('[data-test-id="code-panel"]')
|
||||
}
|
||||
|
||||
function codePanelEmpty() {
|
||||
return page.locator('[data-test-id="code-panel-empty"]')
|
||||
}
|
||||
|
||||
function formatToggle() {
|
||||
return page.locator('[data-test-id="code-panel-format-toggle"]')
|
||||
}
|
||||
|
||||
function copyButton() {
|
||||
return page.locator('[data-test-id="code-panel-copy"]')
|
||||
}
|
||||
|
||||
test('Code tab shows empty state with no selection', async () => {
|
||||
await codeTab().click()
|
||||
await expect(codePanelEmpty()).toBeVisible()
|
||||
await expect(codePanelEmpty()).toContainText('Select a layer')
|
||||
})
|
||||
|
||||
test('selecting a rectangle shows JSX code', async () => {
|
||||
await canvas.drawRect(100, 100, 200, 150)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(codePanel()).toBeVisible()
|
||||
|
||||
const code = await page.evaluate(() => {
|
||||
const el = document.querySelector('[data-test-id="code-panel"]')
|
||||
return el?.textContent ?? ''
|
||||
})
|
||||
expect(code).toContain('Rectangle')
|
||||
})
|
||||
|
||||
test('format toggle switches between OpenPencil and Tailwind', async () => {
|
||||
await expect(formatToggle()).toBeVisible()
|
||||
|
||||
const initialFormat = await formatToggle().textContent()
|
||||
expect(initialFormat).toContain('OpenPencil')
|
||||
|
||||
await formatToggle().click()
|
||||
await expect(formatToggle()).toContainText('Tailwind')
|
||||
|
||||
const code = await page.evaluate(() => {
|
||||
const el = document.querySelector('[data-test-id="code-panel"]')
|
||||
return el?.textContent ?? ''
|
||||
})
|
||||
expect(code).toContain('div')
|
||||
|
||||
await formatToggle().click()
|
||||
await expect(formatToggle()).toContainText('OpenPencil')
|
||||
})
|
||||
|
||||
test('copy button works and shows confirmation', async () => {
|
||||
await copyButton().click()
|
||||
|
||||
await expect(copyButton()).toContainText('Copied')
|
||||
|
||||
await page.waitForTimeout(2500)
|
||||
await expect(copyButton()).toContainText('Copy')
|
||||
})
|
||||
|
||||
test('deselecting shows empty state again', async () => {
|
||||
await page.keyboard.press('Escape')
|
||||
await 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(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = store.createShape('FRAME', 300, 100, 200, 200)
|
||||
store.select([id])
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const code = await page.evaluate(() => {
|
||||
const el = document.querySelector('[data-test-id="code-panel"]')
|
||||
return el?.textContent ?? ''
|
||||
})
|
||||
expect(code).toContain('Frame')
|
||||
})
|
||||
|
||||
test('switching back to Design tab works', async () => {
|
||||
await designTab().click()
|
||||
|
||||
const panel = page.locator('[data-test-id="design-panel-single"], [data-test-id="design-panel-empty"]')
|
||||
await expect(panel.first()).toBeVisible()
|
||||
})
|
||||
169
tests/e2e/components.spec.ts
Normal file
169
tests/e2e/components.spec.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getNodeById(id: string) {
|
||||
return page.evaluate((nodeId) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const n = store.graph.getNode(nodeId)
|
||||
if (!n) return null
|
||||
return { type: n.type, name: n.name, componentId: n.componentId, childIds: n.childIds }
|
||||
}, id)
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return page.evaluate(() => [...window.__OPEN_PENCIL_STORE__!.state.selectedIds])
|
||||
}
|
||||
|
||||
function getPageChildren() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
name: n.name,
|
||||
componentId: n.componentId
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
let componentId: string
|
||||
|
||||
test('create component from selection (⌘⌥K)', async () => {
|
||||
await canvas.drawRect(100, 100, 120, 80)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.keyboard.press('Meta+Alt+k')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const ids = await getSelectedIds()
|
||||
expect(ids).toHaveLength(1)
|
||||
|
||||
const node = await getNodeById(ids[0])
|
||||
expect(node!.type).toBe('COMPONENT')
|
||||
componentId = ids[0]
|
||||
})
|
||||
|
||||
test('component shows purple label in design panel', async () => {
|
||||
const header = page.locator('[data-test-id="design-node-header"]')
|
||||
await expect(header).toContainText('COMPONENT')
|
||||
})
|
||||
|
||||
test('component visible in layers panel', async () => {
|
||||
const layers = page.locator('[data-node-id]')
|
||||
const count = await layers.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
const types = await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).map((n) => n.type)
|
||||
})
|
||||
expect(types).toContain('COMPONENT')
|
||||
})
|
||||
|
||||
test('create instance from component (context menu)', async () => {
|
||||
const children = await getPageChildren()
|
||||
const comp = children.find((c) => c.type === 'COMPONENT')
|
||||
expect(comp).toBeTruthy()
|
||||
|
||||
// Use store directly to create instance
|
||||
await page.evaluate((compId) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.createInstanceFromComponent(compId, 300, 100)
|
||||
}, comp!.id)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const updated = await getPageChildren()
|
||||
const instance = updated.find((c) => c.type === 'INSTANCE')
|
||||
expect(instance).toBeTruthy()
|
||||
expect(instance!.componentId).toBe(comp!.id)
|
||||
})
|
||||
|
||||
test('instance shows INSTANCE type in design panel', async () => {
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
|
||||
await page.evaluate((id) => {
|
||||
window.__OPEN_PENCIL_STORE__!.select([id])
|
||||
}, instance.id)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const header = page.locator('[data-test-id="design-node-header"]')
|
||||
await expect(header).toContainText('INSTANCE')
|
||||
})
|
||||
|
||||
test('instance has "Go to Main Component" button', async () => {
|
||||
const goToBtn = page.locator('[data-test-id="design-go-to-component"]')
|
||||
await expect(goToBtn).toBeVisible()
|
||||
})
|
||||
|
||||
test('instance has "Detach" button', async () => {
|
||||
const detachBtn = page.locator('[data-test-id="design-detach-instance"]')
|
||||
await expect(detachBtn).toBeVisible()
|
||||
})
|
||||
|
||||
test('modifying component propagates to instance', async () => {
|
||||
// Select the component
|
||||
await page.evaluate((id) => {
|
||||
window.__OPEN_PENCIL_STORE__!.select([id])
|
||||
}, componentId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Change component fill
|
||||
await page.evaluate((id) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.updateNodeWithUndo(id, {
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true, blendMode: 'NORMAL' }]
|
||||
}, 'Change fill')
|
||||
}, componentId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Check instance got the same fill
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
const instanceNode = await page.evaluate((id) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const n = store.graph.getNode(id)
|
||||
const child = store.graph.getChildren(id)[0]
|
||||
return child ? { fills: child.fills } : { fills: n?.fills ?? [] }
|
||||
}, instance.id)
|
||||
|
||||
expect(instanceNode.fills.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('detach instance converts to frame', async () => {
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
|
||||
await page.evaluate((id) => {
|
||||
window.__OPEN_PENCIL_STORE__!.select([id])
|
||||
}, instance.id)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__OPEN_PENCIL_STORE__!.detachInstance()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const ids = await getSelectedIds()
|
||||
const detached = await getNodeById(ids[0])
|
||||
expect(detached!.type).toBe('FRAME')
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
222
tests/e2e/context-menu.spec.ts
Normal file
222
tests/e2e/context-menu.spec.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getPageChildren() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
name: n.name,
|
||||
visible: n.visible,
|
||||
locked: n.locked
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function getSelectedCount() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.selectedIds.size)
|
||||
}
|
||||
|
||||
async function rightClickShape(x: number, y: number) {
|
||||
const box = await canvas.canvas.boundingBox()
|
||||
await page.mouse.click(box!.x + x, box!.y + y, { button: 'right' })
|
||||
}
|
||||
|
||||
function contextMenu() {
|
||||
return page.locator('[role="menu"]')
|
||||
}
|
||||
|
||||
function contextItem(testId: string) {
|
||||
return page.locator(`[data-test-id="${testId}"]`)
|
||||
}
|
||||
|
||||
test('right-click on empty canvas shows context menu without selection items disabled', async () => {
|
||||
await rightClickShape(500, 400)
|
||||
|
||||
await expect(contextMenu()).toBeVisible()
|
||||
|
||||
const copyItem = contextItem('context-copy')
|
||||
await expect(copyItem).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
test('draw shape and right-click selects it', async () => {
|
||||
await canvas.drawRect(200, 200, 120, 80)
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Deselect first
|
||||
await page.keyboard.press('Escape')
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Right-click the shape
|
||||
await rightClickShape(250, 230)
|
||||
|
||||
expect(await getSelectedCount()).toBe(1)
|
||||
await expect(contextMenu()).toBeVisible()
|
||||
})
|
||||
|
||||
test('context menu shows expected items', async () => {
|
||||
await expect(contextItem('context-copy')).toBeVisible()
|
||||
await expect(contextItem('context-cut')).toBeVisible()
|
||||
await expect(contextItem('context-duplicate')).toBeVisible()
|
||||
await expect(contextItem('context-delete')).toBeVisible()
|
||||
await expect(contextItem('context-group')).toBeVisible()
|
||||
await expect(contextItem('context-bring-to-front')).toBeVisible()
|
||||
await expect(contextItem('context-send-to-back')).toBeVisible()
|
||||
await expect(contextItem('context-toggle-visibility')).toBeVisible()
|
||||
await expect(contextItem('context-toggle-lock')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
test('duplicate via context menu works', async () => {
|
||||
const countBefore = (await getPageChildren()).length
|
||||
|
||||
await rightClickShape(250, 230)
|
||||
await contextItem('context-duplicate').click()
|
||||
await 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()
|
||||
|
||||
const nodeId = await page.evaluate(() => [...window.__OPEN_PENCIL_STORE__!.state.selectedIds][0])
|
||||
|
||||
await rightClickShape(250, 230)
|
||||
await contextItem('context-toggle-visibility').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const hidden = await page.evaluate((id) => {
|
||||
const n = window.__OPEN_PENCIL_STORE__!.graph.getNode(id)
|
||||
return n ? { visible: n.visible } : null
|
||||
}, nodeId)
|
||||
expect(hidden!.visible).toBe(false)
|
||||
|
||||
// Toggle back: select via store since invisible nodes can't be hit-tested
|
||||
await page.evaluate((id) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.toggleVisibility()
|
||||
}, nodeId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const restored = await page.evaluate((id) => {
|
||||
const n = window.__OPEN_PENCIL_STORE__!.graph.getNode(id)
|
||||
return n ? { visible: n.visible } : null
|
||||
}, nodeId)
|
||||
expect(restored!.visible).toBe(true)
|
||||
})
|
||||
|
||||
test('toggle lock via context menu', async () => {
|
||||
await rightClickShape(250, 230)
|
||||
await contextItem('context-toggle-lock').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
const locked = children.find((c) => c.locked)
|
||||
expect(locked).toBeTruthy()
|
||||
|
||||
// Unlock
|
||||
await rightClickShape(250, 230)
|
||||
await contextItem('context-toggle-lock').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getPageChildren()
|
||||
expect(after.every((c) => !c.locked)).toBe(true)
|
||||
})
|
||||
|
||||
test('delete via context menu removes node', async () => {
|
||||
const countBefore = (await getPageChildren()).length
|
||||
|
||||
await rightClickShape(250, 230)
|
||||
await contextItem('context-delete').click()
|
||||
await 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 rightClickShape(130, 130)
|
||||
await contextItem('context-group').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
const group = children.find((c) => c.type === 'GROUP')
|
||||
expect(group).toBeTruthy()
|
||||
})
|
||||
|
||||
test('ungroup via store after context-menu group', async () => {
|
||||
// Groups are click-through, so ungroup via store instead
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const group = store.graph.getChildren(store.state.currentPageId).find((n) => n.type === 'GROUP')
|
||||
if (group) store.select([group.id])
|
||||
store.ungroupSelected()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
expect(children.every((c) => c.type !== 'GROUP')).toBe(true)
|
||||
expect(children.length).toBe(2)
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('create component via context menu', async () => {
|
||||
await canvas.click(130, 130)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await rightClickShape(130, 130)
|
||||
await contextItem('context-create-component').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
const comp = children.find((c) => c.type === 'COMPONENT')
|
||||
expect(comp).toBeTruthy()
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('Copy/Paste as submenu exists', async () => {
|
||||
await rightClickShape(130, 130)
|
||||
|
||||
const submenuTrigger = contextItem('context-copy-paste-as')
|
||||
await expect(submenuTrigger).toBeVisible()
|
||||
|
||||
await submenuTrigger.hover()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await expect(contextItem('context-copy-as-svg')).toBeVisible()
|
||||
await expect(contextItem('context-copy-as-jsx')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
135
tests/e2e/copy-paste.spec.ts
Normal file
135
tests/e2e/copy-paste.spec.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getPageChildCount() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).length
|
||||
})
|
||||
}
|
||||
|
||||
function getSelectedCount() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.selectedIds.size)
|
||||
}
|
||||
|
||||
function getSelectedNodes() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return [...store.state.selectedIds].map((id) => {
|
||||
const n = store.graph.getNode(id)!
|
||||
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)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const countBefore = await getPageChildCount()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const data = new DataTransfer()
|
||||
store.writeCopyData(data)
|
||||
const html = data.getData('text/html')
|
||||
if (html) store.pasteFromHTML(html)
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const countAfter = await getPageChildCount()
|
||||
expect(countAfter).toBe(countBefore + 1)
|
||||
})
|
||||
|
||||
test('pasted node is offset from original', async () => {
|
||||
const nodes = await getSelectedNodes()
|
||||
expect(nodes).toHaveLength(1)
|
||||
|
||||
const pasted = nodes[0]
|
||||
expect(pasted.name).toBe('Rectangle')
|
||||
})
|
||||
|
||||
test('⌘D duplicates in place', async () => {
|
||||
// Clear canvas and start fresh
|
||||
await canvas.clearCanvas()
|
||||
await canvas.drawRect(200, 200, 100, 80)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const countBefore = await getPageChildCount()
|
||||
await canvas.duplicate()
|
||||
|
||||
const countAfter = await getPageChildCount()
|
||||
expect(countAfter).toBe(countBefore + 1)
|
||||
expect(await getSelectedCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('duplicate preserves fills', async () => {
|
||||
// Set a custom fill on the selected node
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = [...store.state.selectedIds][0]
|
||||
store.updateNodeWithUndo(id, {
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1, a: 1 }, opacity: 1, visible: true, blendMode: 'NORMAL' }]
|
||||
}, 'Set fill')
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await canvas.duplicate()
|
||||
|
||||
const nodes = await getSelectedNodes()
|
||||
expect(nodes[0].fills[0].color.b).toBeCloseTo(1, 1)
|
||||
})
|
||||
|
||||
test('cut removes original', async () => {
|
||||
await canvas.clearCanvas()
|
||||
await canvas.drawRect(100, 100, 80, 60)
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect(await getPageChildCount()).toBe(1)
|
||||
|
||||
// Cut via store
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const data = new DataTransfer()
|
||||
store.writeCopyData(data)
|
||||
store.deleteSelected()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect(await getPageChildCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('multi-select duplicate creates copies of all', async () => {
|
||||
await canvas.clearCanvas()
|
||||
await canvas.drawRect(100, 100, 60, 60)
|
||||
await canvas.drawRect(200, 100, 60, 60)
|
||||
await canvas.selectAll()
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect(await getSelectedCount()).toBe(2)
|
||||
|
||||
const countBefore = await getPageChildCount()
|
||||
await canvas.duplicate()
|
||||
|
||||
const countAfter = await getPageChildCount()
|
||||
expect(countAfter).toBe(countBefore + 2)
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
167
tests/e2e/design-panel.spec.ts
Normal file
167
tests/e2e/design-panel.spec.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function designPanel() {
|
||||
return page.locator('[data-test-id="design-panel-single"]')
|
||||
}
|
||||
|
||||
function nodeHeader() {
|
||||
return page.locator('[data-test-id="design-node-header"]')
|
||||
}
|
||||
|
||||
function fillSection() {
|
||||
return page.locator('[data-test-id="fill-section"]')
|
||||
}
|
||||
|
||||
function strokeSection() {
|
||||
return page.locator('[data-test-id="stroke-section"]')
|
||||
}
|
||||
|
||||
function positionSection() {
|
||||
return page.locator('[data-test-id="position-section"]')
|
||||
}
|
||||
|
||||
function effectsSection() {
|
||||
return page.locator('[data-test-id="effects-section"]')
|
||||
}
|
||||
|
||||
function getNode(id: string) {
|
||||
return page.evaluate((nodeId) => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const n = store.graph.getNode(nodeId)
|
||||
if (!n) return null
|
||||
return { fills: n.fills, strokes: n.strokes, effects: n.effects, opacity: n.opacity, visible: n.visible, x: n.x, y: n.y, width: n.width, height: n.height, rotation: n.rotation }
|
||||
}, id)
|
||||
}
|
||||
|
||||
function getSelectedId() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return [...store.state.selectedIds][0] ?? null
|
||||
})
|
||||
}
|
||||
|
||||
test('selecting a rectangle shows design panel with type and name', async () => {
|
||||
await canvas.drawRect(100, 100, 120, 80)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(designPanel()).toBeVisible()
|
||||
await expect(nodeHeader()).toContainText('RECTANGLE')
|
||||
await expect(nodeHeader()).toContainText('Rectangle')
|
||||
})
|
||||
|
||||
test('position section shows X, Y, rotation inputs', async () => {
|
||||
await expect(positionSection()).toBeVisible()
|
||||
|
||||
const inputs = positionSection().locator('[data-test-id="scrub-input"]')
|
||||
const count = await inputs.count()
|
||||
expect(count).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
test('fill section appears with default fill', async () => {
|
||||
await expect(fillSection()).toBeVisible()
|
||||
|
||||
const fillItems = fillSection().locator('[data-test-id="fill-item"]')
|
||||
await expect(fillItems.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('fill item shows color swatch', async () => {
|
||||
const swatch = fillSection().locator('[data-test-id="fill-picker-swatch"]').first()
|
||||
await expect(swatch).toBeVisible()
|
||||
})
|
||||
|
||||
test('adding a stroke creates stroke section item', async () => {
|
||||
const addBtn = strokeSection().locator('[data-test-id="stroke-section-add"]')
|
||||
await addBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const strokeItems = strokeSection().locator('[data-test-id="stroke-item"]')
|
||||
await expect(strokeItems.first()).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.strokes.length).toBe(1)
|
||||
})
|
||||
|
||||
test('adding an effect creates effect item', async () => {
|
||||
const addBtn = effectsSection().locator('[data-test-id="effects-section-add"]')
|
||||
await addBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const effectItems = effectsSection().locator('[data-test-id="effects-item"]')
|
||||
await expect(effectItems.first()).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.effects.length).toBe(1)
|
||||
})
|
||||
|
||||
test('adding a second fill shows two fill items', async () => {
|
||||
const addBtn = fillSection().locator('[data-test-id="fill-section-add"]')
|
||||
await addBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const fillItems = fillSection().locator('[data-test-id="fill-item"]')
|
||||
expect(await fillItems.count()).toBe(2)
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.fills.length).toBe(2)
|
||||
})
|
||||
|
||||
test('visibility toggle in appearance section works', async () => {
|
||||
const visBtn = page.locator('[data-test-id="appearance-visibility"]')
|
||||
await expect(visBtn).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const before = await getNode(id!)
|
||||
expect(before!.visible).toBe(true)
|
||||
|
||||
await visBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getNode(id!)
|
||||
expect(after!.visible).toBe(false)
|
||||
|
||||
await visBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const restored = await getNode(id!)
|
||||
expect(restored!.visible).toBe(true)
|
||||
})
|
||||
|
||||
test('deselecting shows empty design panel', async () => {
|
||||
await page.keyboard.press('Escape')
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(page.locator('[data-test-id="design-panel-empty"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('multi-select shows mixed header', async () => {
|
||||
await canvas.drawRect(300, 100, 60, 60)
|
||||
await canvas.drawRect(400, 100, 60, 60)
|
||||
await canvas.selectAll()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const multiHeader = page.locator('[data-test-id="design-multi-header"]')
|
||||
await expect(multiHeader).toBeVisible()
|
||||
await expect(multiHeader).toContainText('Mixed')
|
||||
await expect(multiHeader).toContainText('layers')
|
||||
})
|
||||
218
tests/e2e/keyboard-shortcuts.spec.ts
Normal file
218
tests/e2e/keyboard-shortcuts.spec.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getActiveTool() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.activeTool)
|
||||
}
|
||||
|
||||
function getSelectedCount() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.selectedIds.size)
|
||||
}
|
||||
|
||||
function getPageChildren() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
name: n.name,
|
||||
childIds: n.childIds
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function getUIVisible() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.showUI)
|
||||
}
|
||||
|
||||
test.describe('tool switching', () => {
|
||||
test('V → SELECT', async () => {
|
||||
await page.keyboard.press('v')
|
||||
expect(await getActiveTool()).toBe('SELECT')
|
||||
})
|
||||
|
||||
test('R → RECTANGLE', async () => {
|
||||
await page.keyboard.press('r')
|
||||
expect(await getActiveTool()).toBe('RECTANGLE')
|
||||
})
|
||||
|
||||
test('O → ELLIPSE', async () => {
|
||||
await page.keyboard.press('o')
|
||||
expect(await getActiveTool()).toBe('ELLIPSE')
|
||||
})
|
||||
|
||||
test('F → FRAME', async () => {
|
||||
await page.keyboard.press('f')
|
||||
expect(await getActiveTool()).toBe('FRAME')
|
||||
})
|
||||
|
||||
test('T → TEXT', async () => {
|
||||
await page.keyboard.press('t')
|
||||
expect(await getActiveTool()).toBe('TEXT')
|
||||
})
|
||||
|
||||
test('L → LINE', async () => {
|
||||
await page.keyboard.press('l')
|
||||
expect(await getActiveTool()).toBe('LINE')
|
||||
})
|
||||
|
||||
test('P → PEN', async () => {
|
||||
await page.keyboard.press('p')
|
||||
expect(await getActiveTool()).toBe('PEN')
|
||||
})
|
||||
|
||||
test('H → HAND', async () => {
|
||||
await page.keyboard.press('h')
|
||||
expect(await getActiveTool()).toBe('HAND')
|
||||
})
|
||||
|
||||
test('S → SECTION', async () => {
|
||||
await 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 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')
|
||||
expect(await getSelectedCount()).toBe(0)
|
||||
expect(await getActiveTool()).toBe('SELECT')
|
||||
})
|
||||
|
||||
test('Backspace deletes selected', async () => {
|
||||
await canvas.selectAll()
|
||||
const beforeCount = await getSelectedCount()
|
||||
expect(beforeCount).toBeGreaterThan(0)
|
||||
|
||||
await page.keyboard.press('Backspace')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
expect(children).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('z-order shortcuts', () => {
|
||||
test('] brings to front', async () => {
|
||||
await canvas.drawRect(100, 100, 60, 60)
|
||||
await canvas.drawRect(100, 100, 60, 60)
|
||||
|
||||
const childrenBefore = await getPageChildren()
|
||||
const firstId = childrenBefore[0].id
|
||||
|
||||
// Select the first (bottom) node
|
||||
await page.evaluate((id) => {
|
||||
window.__OPEN_PENCIL_STORE__!.select([id])
|
||||
}, firstId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.keyboard.press(']')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const childrenAfter = await getPageChildren()
|
||||
expect(childrenAfter[childrenAfter.length - 1].id).toBe(firstId)
|
||||
})
|
||||
|
||||
test('[ sends to back', async () => {
|
||||
const children = await getPageChildren()
|
||||
const lastId = children[children.length - 1].id
|
||||
|
||||
// Select the last (top) node
|
||||
await page.evaluate((id) => {
|
||||
window.__OPEN_PENCIL_STORE__!.select([id])
|
||||
}, lastId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.keyboard.press('[')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getPageChildren()
|
||||
expect(after[0].id).toBe(lastId)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('group/ungroup shortcuts', () => {
|
||||
test('⌘G groups selection', async () => {
|
||||
await canvas.selectAll()
|
||||
expect(await getSelectedCount()).toBeGreaterThanOrEqual(2)
|
||||
|
||||
await page.keyboard.press('Meta+g')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
const group = children.find((c) => c.type === 'GROUP')
|
||||
expect(group).toBeTruthy()
|
||||
expect(group!.childIds.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
test('⌘⇧G ungroups', async () => {
|
||||
await page.keyboard.press('Meta+Shift+g')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const children = await getPageChildren()
|
||||
expect(children.every((c) => c.type !== 'GROUP')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('UI toggles', () => {
|
||||
test('⌘\\ toggles UI visibility', async () => {
|
||||
const before = await getUIVisible()
|
||||
|
||||
await page.keyboard.press('Meta+\\')
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getUIVisible()
|
||||
expect(after).toBe(!before)
|
||||
|
||||
// Restore
|
||||
await page.keyboard.press('Meta+\\')
|
||||
await canvas.waitForRender()
|
||||
expect(await getUIVisible()).toBe(before)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('undo/redo', () => {
|
||||
test('⌘Z undoes last action', async () => {
|
||||
await canvas.clearCanvas()
|
||||
await canvas.drawRect(100, 100, 60, 60)
|
||||
expect((await getPageChildren()).length).toBe(1)
|
||||
|
||||
await page.keyboard.press('Meta+z')
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect((await getPageChildren()).length).toBe(0)
|
||||
})
|
||||
|
||||
test('⌘⇧Z redoes undone action', async () => {
|
||||
await page.keyboard.press('Meta+Shift+z')
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect((await getPageChildren()).length).toBe(1)
|
||||
})
|
||||
})
|
||||
180
tests/e2e/multi-page.spec.ts
Normal file
180
tests/e2e/multi-page.spec.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getPages() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getPages().map((p) => ({ id: p.id, name: p.name }))
|
||||
})
|
||||
}
|
||||
|
||||
function getCurrentPageId() {
|
||||
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.currentPageId)
|
||||
}
|
||||
|
||||
function getPageChildCount() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).length
|
||||
})
|
||||
}
|
||||
|
||||
function pagesPanel() {
|
||||
return page.locator('[data-test-id="pages-panel"]')
|
||||
}
|
||||
|
||||
function pageItems() {
|
||||
return page.locator('[data-test-id="pages-item"]')
|
||||
}
|
||||
|
||||
function addPageButton() {
|
||||
return page.locator('[data-test-id="pages-add"]')
|
||||
}
|
||||
|
||||
test('initial state has one page', async () => {
|
||||
const pages = await getPages()
|
||||
expect(pages).toHaveLength(1)
|
||||
expect(pages[0].name).toBe('Page 1')
|
||||
})
|
||||
|
||||
test('pages panel shows current page', async () => {
|
||||
await expect(pagesPanel()).toBeVisible()
|
||||
await expect(pageItems().first()).toContainText('Page 1')
|
||||
})
|
||||
|
||||
test('add page creates a second page', async () => {
|
||||
await addPageButton().click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const pages = await getPages()
|
||||
expect(pages).toHaveLength(2)
|
||||
|
||||
expect(await pageItems().count()).toBe(2)
|
||||
})
|
||||
|
||||
test('new page is auto-selected', async () => {
|
||||
const pages = await getPages()
|
||||
const currentId = await getCurrentPageId()
|
||||
expect(currentId).toBe(pages[1].id)
|
||||
})
|
||||
|
||||
test('new page is empty', async () => {
|
||||
expect(await getPageChildCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('drawing on new page adds nodes only to it', async () => {
|
||||
await canvas.drawRect(100, 100, 80, 60)
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect(await getPageChildCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('switching to first page shows its content', async () => {
|
||||
await pageItems().first().click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const pages = await getPages()
|
||||
const currentId = await getCurrentPageId()
|
||||
expect(currentId).toBe(pages[0].id)
|
||||
})
|
||||
|
||||
test('first page has no shapes (we never drew on it)', async () => {
|
||||
expect(await getPageChildCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('switching back to second page shows its shape', async () => {
|
||||
await pageItems().nth(1).click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
expect(await getPageChildCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('add a third page', async () => {
|
||||
await addPageButton().click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const pages = await getPages()
|
||||
expect(pages).toHaveLength(3)
|
||||
expect(await pageItems().count()).toBe(3)
|
||||
})
|
||||
|
||||
test('delete current page switches to adjacent', async () => {
|
||||
const pagesBefore = await getPages()
|
||||
const deletingId = await getCurrentPageId()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.deletePage(store.state.currentPageId)
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const pagesAfter = await getPages()
|
||||
expect(pagesAfter).toHaveLength(pagesBefore.length - 1)
|
||||
expect(pagesAfter.find((p) => p.id === deletingId)).toBeUndefined()
|
||||
|
||||
const currentId = await getCurrentPageId()
|
||||
expect(pagesAfter.some((p) => p.id === currentId)).toBe(true)
|
||||
})
|
||||
|
||||
test('rename page via store', async () => {
|
||||
const pages = await getPages()
|
||||
const currentId = await getCurrentPageId()
|
||||
|
||||
await page.evaluate(
|
||||
([id, name]) => {
|
||||
window.__OPEN_PENCIL_STORE__!.renamePage(id, name)
|
||||
},
|
||||
[currentId, 'Renamed Page'] as [string, string]
|
||||
)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const updated = await getPages()
|
||||
const renamed = updated.find((p) => p.id === currentId)
|
||||
expect(renamed!.name).toBe('Renamed Page')
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('cannot delete the last page', async () => {
|
||||
// Delete until 1 remains
|
||||
let pages = await getPages()
|
||||
while (pages.length > 1) {
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.deletePage(store.state.currentPageId)
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
pages = await getPages()
|
||||
}
|
||||
|
||||
expect(pages).toHaveLength(1)
|
||||
|
||||
// Try deleting the last one — should be a no-op
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
store.deletePage(store.state.currentPageId)
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getPages()
|
||||
expect(after).toHaveLength(1)
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
136
tests/e2e/text-editing.spec.ts
Normal file
136
tests/e2e/text-editing.spec.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
function getSelectedNode() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
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(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return store.graph.getChildren(store.state.currentPageId).map((n) => ({
|
||||
type: n.type,
|
||||
name: n.name,
|
||||
text: n.text
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
test('pressing T activates text tool', async () => {
|
||||
await page.keyboard.press('t')
|
||||
|
||||
const tool = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.activeTool)
|
||||
expect(tool).toBe('TEXT')
|
||||
})
|
||||
|
||||
test('clicking with text tool creates a text node', async () => {
|
||||
await canvas.click(200, 200)
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Wait a tick for text editing mode to activate
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
const children = await getPageChildren()
|
||||
const textNode = children.find((c) => c.type === 'TEXT')
|
||||
expect(textNode).toBeTruthy()
|
||||
})
|
||||
|
||||
test('text node is selected after creation', async () => {
|
||||
const node = await getSelectedNode()
|
||||
expect(node).toBeTruthy()
|
||||
expect(node!.type).toBe('TEXT')
|
||||
})
|
||||
|
||||
test('typography section appears for text node', async () => {
|
||||
// Exit text editing mode
|
||||
await page.keyboard.press('Escape')
|
||||
await canvas.waitForRender()
|
||||
|
||||
// Re-select the text node
|
||||
await canvas.click(200, 200)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const typoSection = page.locator('[data-test-id="typography-section"]')
|
||||
await expect(typoSection).toBeVisible()
|
||||
})
|
||||
|
||||
test('text node has default font properties', async () => {
|
||||
const node = await getSelectedNode()
|
||||
expect(node).toBeTruthy()
|
||||
expect(node!.fontSize).toBeGreaterThan(0)
|
||||
expect(node!.fontFamily).toBeTruthy()
|
||||
})
|
||||
|
||||
test('creating text via store works', async () => {
|
||||
await page.keyboard.press('Escape')
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = store.createShape('TEXT', 100, 300, 200, 30)
|
||||
store.graph.updateNode(id, { text: 'Hello World', fontSize: 24, fontFamily: 'Inter' })
|
||||
store.select([id])
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const node = await getSelectedNode()
|
||||
expect(node!.text).toBe('Hello World')
|
||||
expect(node!.fontSize).toBe(24)
|
||||
})
|
||||
|
||||
test('undo removes text node', async () => {
|
||||
const beforeCount = (await getPageChildren()).length
|
||||
|
||||
await canvas.undo()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const afterCount = (await getPageChildren()).length
|
||||
expect(afterCount).toBe(beforeCount - 1)
|
||||
})
|
||||
|
||||
test('frame tool creates FRAME node', async () => {
|
||||
await page.keyboard.press('f')
|
||||
await canvas.drag(400, 100, 600, 250)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const node = await getSelectedNode()
|
||||
expect(node!.type).toBe('FRAME')
|
||||
expect(node!.width).toBeGreaterThan(0)
|
||||
expect(node!.height).toBeGreaterThan(0)
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
Loading…
Reference in a new issue