diff --git a/tests/e2e/components.spec.ts b/tests/e2e/components.spec.ts index fb2ddfeb3..85e6bbc0e 100644 --- a/tests/e2e/components.spec.ts +++ b/tests/e2e/components.spec.ts @@ -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() }) diff --git a/tests/e2e/properties/visibility.spec.ts b/tests/e2e/properties/visibility.spec.ts index ee265b47e..4b3fc4de4 100644 --- a/tests/e2e/properties/visibility.spec.ts +++ b/tests/e2e/properties/visibility.spec.ts @@ -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) }) diff --git a/tests/e2e/scene/cache.spec.ts b/tests/e2e/scene/cache.spec.ts index 8e489dc3e..1859bcd66 100644 --- a/tests/e2e/scene/cache.spec.ts +++ b/tests/e2e/scene/cache.spec.ts @@ -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) }) diff --git a/tests/engine/editor/undo/redo-sequence.test.ts b/tests/engine/editor/undo/redo-sequence.test.ts index cb4ed3b13..687b19e1f 100644 --- a/tests/engine/editor/undo/redo-sequence.test.ts +++ b/tests/engine/editor/undo/redo-sequence.test.ts @@ -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) }) })