chore(tests): guard component and cache assertions
- Remove non-null assertions from component, visibility, scene cache, and redo sequence tests - Add explicit node, store, renderer, and canvas bounds guards - Validate check plus affected engine and E2E coverage
This commit is contained in:
parent
f7b20a8f38
commit
86cb3970ff
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
|
|
@ -61,9 +62,10 @@ test('create component from selection (⌘⌥K)', async () => {
|
|||
const ids = await getSelectedIds()
|
||||
expect(ids).toHaveLength(1)
|
||||
|
||||
const node = await getNodeById(ids[0])
|
||||
expect(node!.type).toBe('COMPONENT')
|
||||
componentId = ids[0]
|
||||
const selectedId = expectDefined(ids[0], 'selected component id')
|
||||
const node = await getNodeById(selectedId)
|
||||
expect(node?.type).toBe('COMPONENT')
|
||||
componentId = selectedId
|
||||
})
|
||||
|
||||
test('component shows purple label in design panel', async () => {
|
||||
|
|
@ -94,21 +96,26 @@ test('create instance from component (context menu)', async () => {
|
|||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.createInstanceFromComponent(compId, 300, 100)
|
||||
}, comp!.id)
|
||||
}, expectDefined(comp, 'component').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)
|
||||
expect(instance?.componentId).toBe(expectDefined(comp, 'component').id)
|
||||
})
|
||||
|
||||
test('instance shows INSTANCE type in design panel', async () => {
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
const instance = expectDefined(
|
||||
children.find((c) => c.type === 'INSTANCE'),
|
||||
'instance node'
|
||||
)
|
||||
|
||||
await page.evaluate((id) => {
|
||||
window.openPencil?.store!.select([id])
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.select([id])
|
||||
}, instance.id)
|
||||
await canvas.waitForRender()
|
||||
|
||||
|
|
@ -129,7 +136,9 @@ test('instance has "Detach" button', async () => {
|
|||
test('modifying component propagates to instance', async () => {
|
||||
// Select the component
|
||||
await page.evaluate((id) => {
|
||||
window.openPencil?.store!.select([id])
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.select([id])
|
||||
}, componentId)
|
||||
await canvas.waitForRender()
|
||||
|
||||
|
|
@ -157,7 +166,10 @@ test('modifying component propagates to instance', async () => {
|
|||
|
||||
// Check instance got the same fill
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
const instance = expectDefined(
|
||||
children.find((c) => c.type === 'INSTANCE'),
|
||||
'instance node'
|
||||
)
|
||||
const instanceNode = await page.evaluate((id) => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
|
|
@ -171,21 +183,28 @@ test('modifying component propagates to instance', async () => {
|
|||
|
||||
test('detach instance converts to frame', async () => {
|
||||
const children = await getPageChildren()
|
||||
const instance = children.find((c) => c.type === 'INSTANCE')!
|
||||
const instance = expectDefined(
|
||||
children.find((c) => c.type === 'INSTANCE'),
|
||||
'instance node'
|
||||
)
|
||||
|
||||
await page.evaluate((id) => {
|
||||
window.openPencil?.store!.select([id])
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.select([id])
|
||||
}, instance.id)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.openPencil?.store!.detachInstance()
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.detachInstance()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const ids = await getSelectedIds()
|
||||
const detached = await getNodeById(ids[0])
|
||||
expect(detached!.type).toBe('FRAME')
|
||||
const detached = await getNodeById(expectDefined(ids[0], 'detached selected id'))
|
||||
expect(detached?.type).toBe('FRAME')
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
|
|
@ -40,21 +41,21 @@ test('fill visibility supports repeat click and undo redo', async () => {
|
|||
|
||||
const fillButton = page.locator('[data-test-id="fill-visibility-0"]')
|
||||
await expect(fillButton).toBeVisible()
|
||||
expect((await getSelectedNode())!.fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
|
||||
|
||||
await fillButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.fills[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(false)
|
||||
|
||||
await fillButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getSelectedNode())!.fills[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(false)
|
||||
|
||||
await canvas.redo()
|
||||
expect((await getSelectedNode())!.fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').fills[0]?.visible).toBe(true)
|
||||
})
|
||||
|
||||
test('stroke visibility supports repeat click and undo redo', async () => {
|
||||
|
|
@ -63,39 +64,39 @@ test('stroke visibility supports repeat click and undo redo', async () => {
|
|||
|
||||
const strokeButton = page.locator('[data-test-id="stroke-visibility-0"]')
|
||||
await expect(strokeButton).toBeVisible()
|
||||
expect((await getSelectedNode())!.strokes[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
|
||||
await strokeButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.strokes[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(false)
|
||||
|
||||
await strokeButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.strokes[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getSelectedNode())!.strokes[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(false)
|
||||
|
||||
await canvas.redo()
|
||||
expect((await getSelectedNode())!.strokes[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
})
|
||||
|
||||
test('appearance visibility supports repeat click and undo redo in one step', async () => {
|
||||
const visibilityButton = page.locator('[data-test-id="appearance-visibility"]')
|
||||
await expect(visibilityButton).toBeVisible()
|
||||
expect((await getSelectedNode())!.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
|
||||
|
||||
await visibilityButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(false)
|
||||
|
||||
await visibilityButton.click()
|
||||
await canvas.waitForRender()
|
||||
expect((await getSelectedNode())!.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getSelectedNode())!.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(false)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getSelectedNode())!.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(), 'selected node').visible).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
test.describe('SkPicture scene caching', () => {
|
||||
|
|
@ -78,7 +79,9 @@ test.describe('SkPicture scene caching', () => {
|
|||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
// Simulate what loadFonts() does: invalidate the cached picture
|
||||
store.renderer!.invalidateScenePicture()
|
||||
const renderer = store.renderer
|
||||
if (!renderer) throw new Error('OpenPencil renderer not initialized')
|
||||
renderer.invalidateScenePicture()
|
||||
store.requestRender()
|
||||
})
|
||||
await helper.waitForRender()
|
||||
|
|
@ -98,7 +101,8 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const pg = store.graph.getNode(store.state.currentPageId)!
|
||||
const pg = store.graph.getNode(store.state.currentPageId)
|
||||
if (!pg) throw new Error(`Page ${store.state.currentPageId} not found`)
|
||||
const frame = pg.childIds.find((id: string) => store.graph.getNode(id)?.type === 'FRAME')
|
||||
store.setHoveredNode(frame ?? null)
|
||||
})
|
||||
|
|
@ -137,7 +141,8 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const page = store.graph.getNode(store.state.currentPageId)!
|
||||
const page = store.graph.getNode(store.state.currentPageId)
|
||||
if (!page) throw new Error(`Page ${store.state.currentPageId} not found`)
|
||||
const frame = page.childIds.find((id: string) => store.graph.getNode(id)?.type === 'FRAME')
|
||||
store.setHoveredNode(frame ?? null)
|
||||
})
|
||||
|
|
@ -162,7 +167,8 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const page = store.graph.getNode(store.state.currentPageId)!
|
||||
const page = store.graph.getNode(store.state.currentPageId)
|
||||
if (!page) throw new Error(`Page ${store.state.currentPageId} not found`)
|
||||
const frame = page.childIds.find((id: string) => store.graph.getNode(id)?.type === 'FRAME')
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
|
@ -186,15 +192,19 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.waitForRender()
|
||||
const baseline = await helper.screenshotCanvas()
|
||||
|
||||
const box = await helper.canvas.boundingBox()
|
||||
const box = expectDefined(await helper.canvas.boundingBox(), 'canvas bounds')
|
||||
// Move mouse over the frame (at 200, 150 — center of the 300x200 frame at 50,50)
|
||||
await helper.page.mouse.move(box!.x + 200, box!.y + 150)
|
||||
await helper.page.mouse.move(box.x + 200, box.y + 150)
|
||||
await helper.waitForRender()
|
||||
await helper.page.waitForTimeout(100)
|
||||
|
||||
// Move mouse to empty area (far from any node)
|
||||
await helper.page.mouse.move(box!.x + 800, box!.y + 600)
|
||||
await helper.page.evaluate(() => window.openPencil?.store!.setHoveredNode(null))
|
||||
await helper.page.mouse.move(box.x + 800, box.y + 600)
|
||||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.setHoveredNode(null)
|
||||
})
|
||||
await helper.waitForRender()
|
||||
await helper.page.waitForTimeout(100)
|
||||
|
||||
|
|
@ -208,7 +218,8 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const page = store.graph.getNode(store.state.currentPageId)!
|
||||
const page = store.graph.getNode(store.state.currentPageId)
|
||||
if (!page) throw new Error(`Page ${store.state.currentPageId} not found`)
|
||||
const frame = page.childIds.find((id: string) => store.graph.getNode(id)?.type === 'FRAME')
|
||||
if (frame) store.graph.updateNode(frame, { width: 310 })
|
||||
store.requestRender()
|
||||
|
|
@ -219,7 +230,8 @@ test.describe('SkPicture scene caching', () => {
|
|||
await helper.page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const page = store.graph.getNode(store.state.currentPageId)!
|
||||
const page = store.graph.getNode(store.state.currentPageId)
|
||||
if (!page) throw new Error(`Page ${store.state.currentPageId} not found`)
|
||||
const frame = page.childIds.find((id: string) => store.graph.getNode(id)?.type === 'FRAME')
|
||||
store.setHoveredNode(frame ?? null)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, test, expect } from 'bun:test'
|
||||
|
||||
import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { createHistoryFrame, setupEditorPage } from '#tests/helpers/editor-history'
|
||||
|
||||
describe('undo/redo multi-step sequences', () => {
|
||||
|
|
@ -8,7 +9,7 @@ describe('undo/redo multi-step sequences', () => {
|
|||
|
||||
const frame = createHistoryFrame(editor, pageId, { x: 100, y: 100 })
|
||||
editor.select([frame.id])
|
||||
const createSnapshot = structuredClone(editor.graph.getNode(frame.id)!)
|
||||
const createSnapshot = structuredClone(getNodeOrThrow(editor.graph, frame.id))
|
||||
editor.pushUndoEntry({
|
||||
label: 'Create',
|
||||
forward: () => {
|
||||
|
|
@ -21,13 +22,13 @@ describe('undo/redo multi-step sequences', () => {
|
|||
editor.graph.updateNode(frame.id, { x: 300, y: 50 })
|
||||
editor.commitMove(new Map([[frame.id, { x: 100, y: 100 }]]))
|
||||
|
||||
expect(editor.graph.getNode(frame.id)!.x).toBe(300)
|
||||
expect(editor.graph.getNode(frame.id)!.y).toBe(50)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(300)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).y).toBe(50)
|
||||
|
||||
editor.duplicateSelected()
|
||||
const dupIds = [...editor.state.selectedIds]
|
||||
expect(dupIds).toHaveLength(1)
|
||||
const dupId = dupIds[0]
|
||||
const dupId = expectDefined(dupIds[0], 'duplicate id')
|
||||
expect(dupId).not.toBe(frame.id)
|
||||
|
||||
editor.graph.updateNode(dupId, { x: 500, y: 200 })
|
||||
|
|
@ -35,7 +36,7 @@ describe('undo/redo multi-step sequences', () => {
|
|||
|
||||
// Undo move copy
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(dupId)!.x).toBe(320)
|
||||
expect(getNodeOrThrow(editor.graph, dupId).x).toBe(320)
|
||||
|
||||
// Undo duplicate
|
||||
editor.undo.undo()
|
||||
|
|
@ -43,7 +44,7 @@ describe('undo/redo multi-step sequences', () => {
|
|||
|
||||
// Undo move
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(frame.id)!.x).toBe(100)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(100)
|
||||
|
||||
// Undo create
|
||||
editor.undo.undo()
|
||||
|
|
@ -52,11 +53,11 @@ describe('undo/redo multi-step sequences', () => {
|
|||
// Redo create
|
||||
editor.undo.redo()
|
||||
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(frame.id)!.x).toBe(100)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(100)
|
||||
|
||||
// Redo move
|
||||
editor.undo.redo()
|
||||
expect(editor.graph.getNode(frame.id)!.x).toBe(300)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(300)
|
||||
|
||||
// Redo duplicate — must recreate with SAME ID
|
||||
editor.undo.redo()
|
||||
|
|
@ -64,8 +65,8 @@ describe('undo/redo multi-step sequences', () => {
|
|||
|
||||
// Redo move copy — must find the node by same ID
|
||||
editor.undo.redo()
|
||||
expect(editor.graph.getNode(dupId)!.x).toBe(500)
|
||||
expect(editor.graph.getNode(dupId)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, dupId).x).toBe(500)
|
||||
expect(getNodeOrThrow(editor.graph, dupId).y).toBe(200)
|
||||
})
|
||||
|
||||
test('duplicate with children preserves subtree on redo', () => {
|
||||
|
|
@ -84,11 +85,11 @@ describe('undo/redo multi-step sequences', () => {
|
|||
editor.select([frame.id])
|
||||
editor.duplicateSelected()
|
||||
|
||||
const dupFrameId = [...editor.state.selectedIds][0]
|
||||
const dupChildren = editor.graph.getNode(dupFrameId)!.childIds
|
||||
const dupFrameId = expectDefined([...editor.state.selectedIds][0], 'duplicated frame id')
|
||||
const dupChildren = getNodeOrThrow(editor.graph, dupFrameId).childIds
|
||||
expect(dupChildren).toHaveLength(1)
|
||||
const dupTextId = dupChildren[0]
|
||||
expect(editor.graph.getNode(dupTextId)!.text).toBe('Hello')
|
||||
const dupTextId = expectDefined(dupChildren[0], 'duplicated text id')
|
||||
expect(getNodeOrThrow(editor.graph, dupTextId).text).toBe('Hello')
|
||||
|
||||
// Undo
|
||||
editor.undo.undo()
|
||||
|
|
@ -99,8 +100,8 @@ describe('undo/redo multi-step sequences', () => {
|
|||
editor.undo.redo()
|
||||
expect(editor.graph.getNode(dupFrameId)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(dupTextId)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(dupFrameId)!.childIds).toContain(dupTextId)
|
||||
expect(editor.graph.getNode(dupTextId)!.text).toBe('Hello')
|
||||
expect(getNodeOrThrow(editor.graph, dupFrameId).childIds).toContain(dupTextId)
|
||||
expect(getNodeOrThrow(editor.graph, dupTextId).text).toBe('Hello')
|
||||
})
|
||||
|
||||
test('page snapshot restore preserves node IDs', () => {
|
||||
|
|
@ -118,7 +119,7 @@ describe('undo/redo multi-step sequences', () => {
|
|||
|
||||
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(child.id)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(frame.id)!.childIds).toEqual([child.id])
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).childIds).toEqual([child.id])
|
||||
})
|
||||
|
||||
test('delete frame with children → undo restores subtree', () => {
|
||||
|
|
@ -142,6 +143,6 @@ describe('undo/redo multi-step sequences', () => {
|
|||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(child.id)).not.toBeUndefined()
|
||||
expect(editor.graph.getNode(frame.id)!.childIds).toContain(child.id)
|
||||
expect(getNodeOrThrow(editor.graph, frame.id).childIds).toContain(child.id)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue