diff --git a/oxlint.json b/oxlint.json index c5ec31773..bcec1f5d8 100644 --- a/oxlint.json +++ b/oxlint.json @@ -162,12 +162,6 @@ "open-pencil/no-useless-pass-through-wrappers": "error" } }, - { - "files": ["tests/**/*.ts", "tests/**/*.tsx"], - "rules": { - "typescript/no-non-null-assertion": "off" - } - }, { "files": ["**/kiwi/kiwi-schema/**"], "rules": { diff --git a/tests/e2e/canvas/manipulation.spec.ts b/tests/e2e/canvas/manipulation.spec.ts index 6bee1603e..5f9db05a7 100644 --- a/tests/e2e/canvas/manipulation.spec.ts +++ b/tests/e2e/canvas/manipulation.spec.ts @@ -1,5 +1,6 @@ import { test, expect, type Page } from '@playwright/test' +import { expectDefined } from '#tests/helpers/assert' import { CanvasHelper } from '#tests/helpers/canvas' import { getSelectedIds, getPageChildren, getSelectedNode, getNodeById } from '#tests/helpers/store' @@ -106,8 +107,8 @@ test('resize corner handle drag increases node dimensions', async () => { const box = await page.locator('[data-test-id="canvas-element"]').boundingBox() if (!box) throw new Error('No canvas') - const hx = box.x + viewport!.handleX - const hy = box.y + viewport!.handleY + const hx = box.x + expectDefined(viewport, 'viewport state').handleX + const hy = box.y + expectDefined(viewport, 'viewport state').handleY await page.mouse.move(hx, hy) await page.mouse.down() @@ -116,8 +117,12 @@ test('resize corner handle drag increases node dimensions', async () => { await canvas.waitForRender() const after = await getSelectedNode(page) - expect(after!.width).toBeGreaterThan(before!.width + 20) - expect(after!.height).toBeGreaterThan(before!.height + 20) + expect(expectDefined(after, 'selected node after').width).toBeGreaterThan( + expectDefined(before, 'selected node before').width + 20 + ) + expect(expectDefined(after, 'selected node after').height).toBeGreaterThan( + expectDefined(before, 'selected node before').height + 20 + ) canvas.assertNoErrors() }) @@ -129,7 +134,7 @@ test('rotation handle drag rotates node', async () => { const before = await getSelectedNode(page) expect(before).not.toBeNull() - const initialRotation = before!.rotation ?? 0 + const initialRotation = expectDefined(before, 'selected node before').rotation ?? 0 const viewport = await page.evaluate(() => { const store = window.openPencil?.store @@ -150,8 +155,8 @@ test('rotation handle drag rotates node', async () => { const box = await page.locator('[data-test-id="canvas-element"]').boundingBox() if (!box) throw new Error('No canvas') - const rx = box.x + viewport!.cx - const ry = box.y + viewport!.topMidY - 24 + const rx = box.x + expectDefined(viewport, 'viewport state').cx + const ry = box.y + expectDefined(viewport, 'viewport state').topMidY - 24 await page.mouse.move(rx, ry) await canvas.waitForRender() @@ -160,8 +165,8 @@ test('rotation handle drag rotates node', async () => { await page.mouse.up() await canvas.waitForRender() - const after = await getNodeById(page, before!.id) - expect(after!.rotation ?? 0).not.toBe(initialRotation) + const after = await getNodeById(page, expectDefined(before, 'selected node before').id) + expect(expectDefined(after, 'selected node after').rotation ?? 0).not.toBe(initialRotation) canvas.assertNoErrors() }) @@ -236,14 +241,20 @@ async function setupFrameChild(rotation: number) { }) expect(state).not.toBeNull() - return state! + return expectDefined(state, 'frame child state') } test('frame children keep correct hover and click hit area without rotation', async () => { const state = await setupFrameChild(0) await canvas.hover(state.hitX, state.hitY) - const hoveredId = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId) + const hoveredId = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.hoveredNodeId + })() + ) expect(hoveredId).toBe(state.childId) await canvas.click(state.hitX, state.hitY) @@ -252,7 +263,13 @@ test('frame children keep correct hover and click hit area without rotation', as expect(selected?.id).toBe(state.childId) await canvas.hover(state.missX, state.missY) - const hoveredMiss = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId) + const hoveredMiss = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.hoveredNodeId + })() + ) expect(hoveredMiss).not.toBe(state.childId) canvas.assertNoErrors() }) @@ -261,7 +278,13 @@ test('rotated frame children keep correct hover and click hit area', async () => const state = await setupFrameChild(35) await canvas.hover(state.hitX, state.hitY) - const hoveredId = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId) + const hoveredId = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.hoveredNodeId + })() + ) expect(hoveredId).toBe(state.childId) await canvas.click(state.hitX, state.hitY) @@ -270,7 +293,13 @@ test('rotated frame children keep correct hover and click hit area', async () => expect(selected?.id).toBe(state.childId) await canvas.hover(state.missX, state.missY) - const hoveredMiss = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId) + const hoveredMiss = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.hoveredNodeId + })() + ) expect(hoveredMiss).not.toBe(state.childId) canvas.assertNoErrors() }) @@ -300,8 +329,8 @@ test('rotation drag exposes live rotation preview state', async () => { const box = await page.locator('[data-test-id="canvas-element"]').boundingBox() if (!box) throw new Error('No canvas') - const rx = box.x + viewport!.cx - const ry = box.y + viewport!.topMidY - 24 + const rx = box.x + expectDefined(viewport, 'viewport state').cx + const ry = box.y + expectDefined(viewport, 'viewport state').topMidY - 24 await page.mouse.move(rx, ry) await canvas.waitForRender() @@ -309,13 +338,25 @@ test('rotation drag exposes live rotation preview state', async () => { await page.mouse.move(rx + 60, ry + 60, { steps: 15 }) await canvas.waitForRender() - const preview = await page.evaluate(() => window.openPencil?.store!.state.rotationPreview) + const preview = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.rotationPreview + })() + ) expect(preview).not.toBeNull() await page.mouse.up() await canvas.waitForRender() - const clearedPreview = await page.evaluate(() => window.openPencil?.store!.state.rotationPreview) + const clearedPreview = await page.evaluate(() => + (() => { + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + return store.state.rotationPreview + })() + ) expect(clearedPreview).toBeNull() canvas.assertNoErrors() }) diff --git a/tests/e2e/design/panel.spec.ts b/tests/e2e/design/panel.spec.ts index 22a880066..efe8ab95f 100644 --- a/tests/e2e/design/panel.spec.ts +++ b/tests/e2e/design/panel.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 @@ -102,7 +103,7 @@ test('fill item shows color swatch', async () => { test('clicking color area changes fill color', async () => { const id = await getSelectedId() - const before = await getNode(id!) + const before = await getNode(expectDefined(id, 'selected id')) const swatch = fillSection().locator('[data-test-id="fill-picker-swatch"]').first() await swatch.click() @@ -111,13 +112,16 @@ test('clicking color area changes fill color', async () => { await expect(colorArea).toBeVisible({ timeout: 5000 }) const box = await colorArea.boundingBox() - await page.mouse.click(box!.x + box!.width - 10, box!.y + 10) + await 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) - const after = await getNode(id!) - const c1 = before!.fills[0].color - const c2 = after!.fills[0].color + const after = await getNode(expectDefined(id, 'selected id')) + const c1 = expectDefined(before, 'before node').fills[0].color + const c2 = expectDefined(after, 'after node').fills[0].color expect(c1.r !== c2.r || c1.g !== c2.g || c1.b !== c2.b).toBe(true) // Close popover — click the swatch again to toggle it off @@ -134,8 +138,8 @@ test('adding a stroke creates stroke section item', async () => { await expect(strokeItems.first()).toBeVisible() const id = await getSelectedId() - const node = await getNode(id!) - expect(node!.strokes.length).toBe(1) + const node = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(node, 'node node').strokes.length).toBe(1) }) test('adding an effect creates effect item', async () => { @@ -147,8 +151,8 @@ test('adding an effect creates effect item', async () => { await expect(effectItems.first()).toBeVisible() const id = await getSelectedId() - const node = await getNode(id!) - expect(node!.effects.length).toBe(1) + const node = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(node, 'node node').effects.length).toBe(1) }) test('adding a second fill shows two fill items', async () => { @@ -160,8 +164,8 @@ test('adding a second fill shows two fill items', async () => { expect(await fillItems.count()).toBe(2) const id = await getSelectedId() - const node = await getNode(id!) - expect(node!.fills.length).toBe(2) + const node = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(node, 'node node').fills.length).toBe(2) }) test('visibility toggle in appearance section works', async () => { @@ -169,20 +173,20 @@ test('visibility toggle in appearance section works', async () => { await expect(visBtn).toBeVisible() const id = await getSelectedId() - const before = await getNode(id!) - expect(before!.visible).toBe(true) + const before = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(before, 'before node').visible).toBe(true) await visBtn.click() await canvas.waitForRender() - const after = await getNode(id!) - expect(after!.visible).toBe(false) + const after = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(after, 'after node').visible).toBe(false) await visBtn.click() await canvas.waitForRender() - const restored = await getNode(id!) - expect(restored!.visible).toBe(true) + const restored = await getNode(expectDefined(id, 'selected id')) + expect(expectDefined(restored, 'restored node').visible).toBe(true) }) test('fill stroke and effect visibility toggles update on repeated clicks and support undo redo', async () => { @@ -192,23 +196,35 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su const fillButton = page.locator('[data-test-id="fill-visibility-0"]') await expect(fillButton).toBeVisible() - const initial = await getNode(id!) - expect(initial!.fills[0]?.visible).toBe(true) + 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 expect(fillButton).toHaveAttribute('data-visible', 'false') - expect((await getNode(id!))!.fills[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0] + ?.visible + ).toBe(false) await fillButton.click() await canvas.waitForRender() await expect(fillButton).toHaveAttribute('data-visible', 'true') - expect((await getNode(id!))!.fills[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0] + ?.visible + ).toBe(true) await canvas.undo() - expect((await getNode(id!))!.fills[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0] + ?.visible + ).toBe(false) await canvas.redo() - expect((await getNode(id!))!.fills[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0] + ?.visible + ).toBe(true) const strokeAddButton = strokeSection().locator('[data-test-id="stroke-section-add"]') await strokeAddButton.click() @@ -216,22 +232,37 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su const strokeButton = page.locator('[data-test-id="stroke-visibility-0"]') await expect(strokeButton).toBeVisible() - expect((await getNode(id!))!.strokes[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0] + ?.visible + ).toBe(true) await strokeButton.click() await canvas.waitForRender() await expect(strokeButton).toHaveAttribute('data-visible', 'false') - expect((await getNode(id!))!.strokes[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0] + ?.visible + ).toBe(false) await strokeButton.click() await canvas.waitForRender() await expect(strokeButton).toHaveAttribute('data-visible', 'true') - expect((await getNode(id!))!.strokes[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0] + ?.visible + ).toBe(true) await canvas.undo() - expect((await getNode(id!))!.strokes[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0] + ?.visible + ).toBe(false) await canvas.redo() - expect((await getNode(id!))!.strokes[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0] + ?.visible + ).toBe(true) const effectAddButton = effectsSection().locator('[data-test-id="effects-section-add"]') await effectAddButton.click() @@ -239,22 +270,37 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su const effectButton = page.locator('[data-test-id="effect-visibility-0"]') await expect(effectButton).toBeVisible() - expect((await getNode(id!))!.effects[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0] + ?.visible + ).toBe(true) await effectButton.click() await canvas.waitForRender() await expect(effectButton).toHaveAttribute('data-visible', 'false') - expect((await getNode(id!))!.effects[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0] + ?.visible + ).toBe(false) await effectButton.click() await canvas.waitForRender() await expect(effectButton).toHaveAttribute('data-visible', 'true') - expect((await getNode(id!))!.effects[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0] + ?.visible + ).toBe(true) await canvas.undo() - expect((await getNode(id!))!.effects[0]?.visible).toBe(false) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0] + ?.visible + ).toBe(false) await canvas.redo() - expect((await getNode(id!))!.effects[0]?.visible).toBe(true) + expect( + expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0] + ?.visible + ).toBe(true) }) test('deselecting shows empty design panel', async () => { diff --git a/tests/e2e/editor/auto-layout.spec.ts b/tests/e2e/editor/auto-layout.spec.ts index 8ba7874ce..6b17dd247 100644 --- a/tests/e2e/editor/auto-layout.spec.ts +++ b/tests/e2e/editor/auto-layout.spec.ts @@ -1,5 +1,6 @@ import { test, expect, type Page } from '@playwright/test' +import { expectDefined } from '#tests/helpers/assert' import { CanvasHelper } from '#tests/helpers/canvas' import { getSelectedNode, getNodeById } from '#tests/helpers/store' @@ -24,7 +25,9 @@ test.afterAll(async () => { async function selectFrame() { expect(frameId, 'frameId must be set — did the Shift+A test run?').toBeTruthy() await page.evaluate((id: string) => { - window.openPencil?.store!.select([id]) + const store = window.openPencil?.store + if (!store) throw new Error('OpenPencil store not initialized') + store.select([id]) }, frameId) await canvas.waitForRender() } @@ -41,11 +44,11 @@ test('Shift+A wraps selection in auto-layout frame', async () => { const node = await getSelectedNode(page) expect(node).not.toBeNull() - expect(node!.type).toBe('FRAME') - expect(node!.layoutMode).not.toBe('NONE') - expect(node!.childIds.length).toBe(2) + expect(expectDefined(node, 'node').type).toBe('FRAME') + expect(expectDefined(node, 'node').layoutMode).not.toBe('NONE') + expect(expectDefined(node, 'node').childIds.length).toBe(2) - frameId = node!.id + frameId = expectDefined(node, 'node').id canvas.assertNoErrors() }) @@ -56,19 +59,19 @@ test('direction button toggles to VERTICAL', async () => { await canvas.waitForRender() const frame = await getNodeById(page, frameId) - expect(frame!.layoutMode).toBe('VERTICAL') + expect(expectDefined(frame, 'frame').layoutMode).toBe('VERTICAL') canvas.assertNoErrors() }) test('gap ScrubInput sets itemSpacing', async () => { await selectFrame() const before = await getNodeById(page, frameId) - const initialSpacing = before!.itemSpacing + const initialSpacing = expectDefined(before, 'before').itemSpacing await canvas.dragScrubInput(page.locator('[data-test-id="layout-gap-input"]'), 40) const after = await getNodeById(page, frameId) - expect(after!.itemSpacing).toBeGreaterThan(initialSpacing + 5) + expect(expectDefined(after, 'after').itemSpacing).toBeGreaterThan(initialSpacing + 5) canvas.assertNoErrors() }) @@ -79,15 +82,17 @@ test('gap menu sets auto space-between alignment', async () => { await page.getByRole('option', { name: 'Auto' }).click() await canvas.waitForRender() let frame = await getNodeById(page, frameId) - expect(frame!.primaryAxisAlign).toBe('SPACE_BETWEEN') + expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('SPACE_BETWEEN') await expect(page.locator('[data-test-id="layout-alignment-grid"] button')).toHaveCount(9) await page.locator('[data-test-id="layout-gap-menu"]').click() - await page.getByRole('option', { name: String(Math.round(frame!.itemSpacing)) }).click() + await page + .getByRole('option', { name: String(Math.round(expectDefined(frame, 'frame').itemSpacing)) }) + .click() await canvas.waitForRender() frame = await getNodeById(page, frameId) - expect(frame!.primaryAxisAlign).toBe('MIN') + expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('MIN') await expect(page.locator('[data-test-id="layout-alignment-grid"] button')).toHaveCount(9) canvas.assertNoErrors() }) @@ -99,12 +104,12 @@ test('wrap mode exposes cross-axis gap control', async () => { await canvas.waitForRender() const before = await getNodeById(page, frameId) - const initialSpacing = before!.counterAxisSpacing + const initialSpacing = expectDefined(before, 'before').counterAxisSpacing await canvas.dragScrubInput(page.locator('[data-test-id="layout-cross-gap-input"]'), 40) const after = await getNodeById(page, frameId) - expect(after!.layoutWrap).toBe('WRAP') - expect(after!.counterAxisSpacing).toBeGreaterThan(initialSpacing + 5) + expect(expectDefined(after, 'after').layoutWrap).toBe('WRAP') + expect(expectDefined(after, 'after').counterAxisSpacing).toBeGreaterThan(initialSpacing + 5) canvas.assertNoErrors() }) @@ -130,10 +135,10 @@ test('padding controls set horizontal and vertical padding pairs', async () => { await canvas.waitForRender() const frame = await getNodeById(page, frameId) - expect(frame!.paddingTop).toBe(16) - expect(frame!.paddingRight).toBe(24) - expect(frame!.paddingBottom).toBe(16) - expect(frame!.paddingLeft).toBe(24) + expect(expectDefined(frame, 'frame').paddingTop).toBe(16) + expect(expectDefined(frame, 'frame').paddingRight).toBe(24) + expect(expectDefined(frame, 'frame').paddingBottom).toBe(16) + expect(expectDefined(frame, 'frame').paddingLeft).toBe(24) canvas.assertNoErrors() }) @@ -145,7 +150,9 @@ test('size dropdown adds and removes min width', async () => { await canvas.waitForRender() let frame = await getNodeById(page, frameId) - expect(frame!.minWidth).toBe(Math.round(frame!.width)) + expect(expectDefined(frame, 'frame').minWidth).toBe( + Math.round(expectDefined(frame, 'frame').width) + ) await expect(page.locator('[data-test-id="layout-min-width-input"]')).toBeVisible() await page.locator('[data-test-id="layout-width-sizing-menu"]').click() @@ -153,7 +160,7 @@ test('size dropdown adds and removes min width', async () => { await canvas.waitForRender() frame = await getNodeById(page, frameId) - expect(frame!.minWidth).toBeNull() + expect(expectDefined(frame, 'frame').minWidth).toBeNull() await expect(page.locator('[data-test-id="layout-min-width-input"]')).toHaveCount(0) canvas.assertNoErrors() }) @@ -166,8 +173,8 @@ test('alignment grid center sets CENTER alignment', async () => { await canvas.waitForRender() const frame = await getNodeById(page, frameId) - expect(frame!.primaryAxisAlign).toBe('CENTER') - expect(frame!.counterAxisAlign).toBe('CENTER') + expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('CENTER') + expect(expectDefined(frame, 'frame').counterAxisAlign).toBe('CENTER') canvas.assertNoErrors() }) @@ -178,6 +185,6 @@ test('remove auto-layout sets layoutMode to NONE', async () => { await canvas.waitForRender() const frame = await getNodeById(page, frameId) - expect(frame!.layoutMode).toBe('NONE') + expect(expectDefined(frame, 'frame').layoutMode).toBe('NONE') canvas.assertNoErrors() }) diff --git a/tests/engine/editor/clipboard/import-nodes.test.ts b/tests/engine/editor/clipboard/import-nodes.test.ts index 046c5d2bb..5e4a251db 100644 --- a/tests/engine/editor/clipboard/import-nodes.test.ts +++ b/tests/engine/editor/clipboard/import-nodes.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'bun:test' import { importClipboardNodes } from '@open-pencil/core' import type { NodeChange, SceneNode } from '@open-pencil/core' +import { getNodeOrThrow } from '#tests/helpers/assert' import { createClipboardGraph } from '#tests/helpers/clipboard' describe('importClipboardNodes', () => { @@ -52,7 +53,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const card = graph.getNode(created[0])! + const card = getNodeOrThrow(graph, created[0]) expect(card.type).toBe('FRAME') expect(card.name).toBe('Card') @@ -109,7 +110,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - expect(graph.getNode(created[0])!.name).toBe('RealShape') + expect(getNodeOrThrow(graph, created[0]).name).toBe('RealShape') }) it('imports nested frames with children', () => { @@ -154,7 +155,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const outer = graph.getNode(created[0])! + const outer = getNodeOrThrow(graph, created[0]) expect(outer.name).toBe('Outer') const innerList = graph.getChildren(outer.id) @@ -196,7 +197,7 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - const node = graph.getNode(created[0])! + const node = getNodeOrThrow(graph, created[0]) expect(node.fills).toHaveLength(1) expect(node.fills[0].color.r).toBe(1) expect(node.strokes).toHaveLength(1) @@ -244,7 +245,7 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - const row = graph.getNode(created[0])! + const row = getNodeOrThrow(graph, created[0]) const children = graph.getChildren(row.id) expect(children[0].layoutAlignSelf).toBe('STRETCH') expect(children[1].layoutAlignSelf).toBe('AUTO') @@ -290,9 +291,9 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - expect(graph.getNode(created[0])!.clipsContent).toBe(true) - expect(graph.getNode(created[1])!.clipsContent).toBe(false) - expect(graph.getNode(created[2])!.clipsContent).toBe(false) + expect(getNodeOrThrow(graph, created[0]).clipsContent).toBe(true) + expect(getNodeOrThrow(graph, created[1]).clipsContent).toBe(false) + expect(getNodeOrThrow(graph, created[2]).clipsContent).toBe(false) }) it('imports fontWeight from fontName.style via styleToWeight', () => { @@ -342,10 +343,10 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - expect(graph.getNode(created[0])!.fontWeight).toBe(500) - expect(graph.getNode(created[1])!.fontWeight).toBe(700) - expect(graph.getNode(created[2])!.fontWeight).toBe(700) - expect(graph.getNode(created[2])!.italic).toBe(true) + expect(getNodeOrThrow(graph, created[0]).fontWeight).toBe(500) + expect(getNodeOrThrow(graph, created[1]).fontWeight).toBe(700) + expect(getNodeOrThrow(graph, created[2]).fontWeight).toBe(700) + expect(getNodeOrThrow(graph, created[2]).italic).toBe(true) }) it('converts letterSpacing object to pixels', () => { @@ -394,9 +395,9 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - expect(graph.getNode(created[0])!.letterSpacing).toBe(2) - expect(graph.getNode(created[1])!.letterSpacing).toBe(2) // 10% of 20px - expect(graph.getNode(created[2])!.letterSpacing).toBe(0) + expect(getNodeOrThrow(graph, created[0]).letterSpacing).toBe(2) + expect(getNodeOrThrow(graph, created[1]).letterSpacing).toBe(2) // 10% of 20px + expect(getNodeOrThrow(graph, created[2]).letterSpacing).toBe(0) }) it('converts RAW lineHeight to pixels', () => { @@ -446,9 +447,9 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - expect(graph.getNode(created[0])!.lineHeight).toBe(36) // 24 * 1.5 - expect(graph.getNode(created[1])!.lineHeight).toBe(20) - expect(graph.getNode(created[2])!.lineHeight).toBe(24) // 120% of 20 + expect(getNodeOrThrow(graph, created[0]).lineHeight).toBe(36) // 24 * 1.5 + expect(getNodeOrThrow(graph, created[1]).lineHeight).toBe(20) + expect(getNodeOrThrow(graph, created[2]).lineHeight).toBe(24) // 120% of 20 }) it('converts letterSpacing and lineHeight in style overrides', () => { @@ -487,7 +488,7 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - const node = graph.getNode(created[0])! + const node = getNodeOrThrow(graph, created[0]) expect(node.styleRuns).toHaveLength(1) expect(node.styleRuns[0].style.lineHeight).toBe(30) // 20 * 1.5 expect(node.styleRuns[0].style.letterSpacing).toBeCloseTo(-0.4) // 20 * -2/100 @@ -542,7 +543,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const component = graph.getNode(created[0])! + const component = getNodeOrThrow(graph, created[0]) expect(component.type).toBe('COMPONENT') expect(component.layoutMode).toBe('VERTICAL') expect(component.itemSpacing).toBe(16) @@ -598,11 +599,11 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(2) - const component = graph.getNode(created[0])! + const component = getNodeOrThrow(graph, created[0]) expect(component.type).toBe('COMPONENT') expect(graph.getChildren(component.id)).toHaveLength(1) - const instance = graph.getNode(created[1])! + const instance = getNodeOrThrow(graph, created[1]) expect(instance.type).toBe('INSTANCE') expect(instance.componentId).toBe(component.id) @@ -662,7 +663,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const instance = graph.getNode(created[0])! + const instance = getNodeOrThrow(graph, created[0]) expect(instance.type).toBe('INSTANCE') expect(instance.name).toBe('Icon') @@ -717,7 +718,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const card = graph.getNode(created[0])! + const card = getNodeOrThrow(graph, created[0]) const children = graph.getChildren(card.id) expect(children).toHaveLength(1) @@ -792,7 +793,7 @@ describe('importClipboardNodes', () => { const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) - const instance = graph.getNode(created[0])! + const instance = getNodeOrThrow(graph, created[0]) expect(instance.type).toBe('INSTANCE') const children = graph.getChildren(instance.id) expect(children).toHaveLength(1) @@ -845,9 +846,9 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - expect(graph.getNode(created[0])!.textAutoResize).toBe('HEIGHT') - expect(graph.getNode(created[1])!.textAutoResize).toBe('WIDTH_AND_HEIGHT') - expect(graph.getNode(created[2])!.textAutoResize).toBe('NONE') + expect(getNodeOrThrow(graph, created[0]).textAutoResize).toBe('HEIGHT') + expect(getNodeOrThrow(graph, created[1]).textAutoResize).toBe('WIDTH_AND_HEIGHT') + expect(getNodeOrThrow(graph, created[2]).textAutoResize).toBe('NONE') }) it('undo removes all imported nodes including children', () => { @@ -940,11 +941,11 @@ describe('importClipboardNodes', () => { const childrenBefore = graph.getChildren(pageId).length const created = importClipboardNodes(nodeChanges, graph, pageId) - const parent = graph.getNode(created[0])! + const parent = getNodeOrThrow(graph, created[0]) const allSnapshots: SceneNode[] = [] function walk(id: string) { - const n = graph.getNode(id)! + const n = getNodeOrThrow(graph, id) allSnapshots.push({ ...n }) for (const cid of n.childIds) walk(cid) } @@ -962,7 +963,7 @@ describe('importClipboardNodes', () => { }) } - const restored = graph.getNode(parent.id)! + const restored = getNodeOrThrow(graph, parent.id) expect(restored).toBeTruthy() expect(restored.name).toBe('Parent') expect(restored.childIds).toHaveLength(2) @@ -1003,11 +1004,11 @@ describe('importClipboardNodes', () => { ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) - const parent = graph.getNode(created[0])! + const parent = getNodeOrThrow(graph, created[0]) const allSnapshots: SceneNode[] = [] function walk(id: string) { - const n = graph.getNode(id)! + const n = getNodeOrThrow(graph, id) allSnapshots.push({ ...n }) for (const cid of n.childIds) walk(cid) } @@ -1020,7 +1021,7 @@ describe('importClipboardNodes', () => { graph.createNode(snapshot.type, snapshot.parentId ?? pageId, snapshot) } - const restored = graph.getNode(parent.id)! + const restored = getNodeOrThrow(graph, parent.id) // Bug: parent has duplicated childIds because snapshot already had [childId] // and createNode appends childId again expect(restored.childIds.length).toBeGreaterThan(1) diff --git a/tests/engine/hit-test/scope.test.ts b/tests/engine/hit-test/scope.test.ts index 729320586..c959058ad 100644 --- a/tests/engine/hit-test/scope.test.ts +++ b/tests/engine/hit-test/scope.test.ts @@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test' import { SceneGraph } from '@open-pencil/core' +import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert' + function pageId(graph: SceneGraph) { return graph.getPages()[0].id } @@ -31,21 +33,21 @@ describe('hitTest — group behavior', () => { const { graph, page } = setup() const hit = graph.hitTest(110, 110, page) expect(hit).not.toBeNull() - expect(hit!.type).toBe('GROUP') + expect(expectDefined(hit, 'hit node').type).toBe('GROUP') }) test('hitTestDeep on child returns child directly', () => { const { graph, page, child } = setup() const hit = graph.hitTestDeep(110, 110, page) expect(hit).not.toBeNull() - expect(hit!.id).toBe(child.id) + expect(expectDefined(hit, 'hit node').id).toBe(child.id) }) test('hitTest with scope=group returns child', () => { const { graph, group } = setup() const hit = graph.hitTest(110, 110, group.id) expect(hit).not.toBeNull() - expect(hit!.name).toBe('Rect') + expect(expectDefined(hit, 'hit node').name).toBe('Rect') }) test('hitTest miss inside group returns null', () => { @@ -93,28 +95,28 @@ describe('hitTest — nested groups', () => { const { graph, page } = setup() const hit = graph.hitTest(100, 100, page) expect(hit).not.toBeNull() - expect(hit!.name).toBe('GroupA') + expect(expectDefined(hit, 'hit node').name).toBe('GroupA') }) test('click with scope=GroupA returns GroupB', () => { const { graph, groupA } = setup() const hit = graph.hitTest(150, 150, groupA.id) expect(hit).not.toBeNull() - expect(hit!.name).toBe('GroupB') + expect(expectDefined(hit, 'hit node').name).toBe('GroupB') }) test('click with scope=GroupB returns DeepRect', () => { const { graph, groupB } = setup() const hit = graph.hitTest(160, 160, groupB.id) expect(hit).not.toBeNull() - expect(hit!.name).toBe('DeepRect') + expect(expectDefined(hit, 'hit node').name).toBe('DeepRect') }) test('hitTestDeep from page returns deepest child', () => { const { graph, page, rect } = setup() const hit = graph.hitTestDeep(160, 160, page) expect(hit).not.toBeNull() - expect(hit!.id).toBe(rect.id) + expect(expectDefined(hit, 'hit node').id).toBe(rect.id) }) }) @@ -140,7 +142,7 @@ describe('hitTest — locked nodes', () => { const hit = graph.hitTestDeep(10, 10, page) expect(hit).not.toBeNull() - expect(hit!.name).toBe('LockedFrame') + expect(expectDefined(hit, 'hit node').name).toBe('LockedFrame') }) test('locked leaf node is still clickable', () => { @@ -157,7 +159,7 @@ describe('hitTest — locked nodes', () => { const hit = graph.hitTest(25, 25, page) expect(hit).not.toBeNull() - expect(hit!.name).toBe('LockedRect') + expect(expectDefined(hit, 'hit node').name).toBe('LockedRect') }) }) @@ -194,7 +196,7 @@ describe('scene graph — locked node operations', () => { const node = graph.getNode(rect.id) expect(node).not.toBeNull() - expect(node!.locked).toBe(true) + expect(expectDefined(node, 'node').locked).toBe(true) }) test('lock can be toggled', () => { @@ -208,11 +210,11 @@ describe('scene graph — locked node operations', () => { height: 50 }) - expect(graph.getNode(rect.id)!.locked).toBe(false) + expect(getNodeOrThrow(graph, rect.id).locked).toBe(false) graph.updateNode(rect.id, { locked: true }) - expect(graph.getNode(rect.id)!.locked).toBe(true) + expect(getNodeOrThrow(graph, rect.id).locked).toBe(true) graph.updateNode(rect.id, { locked: false }) - expect(graph.getNode(rect.id)!.locked).toBe(false) + expect(getNodeOrThrow(graph, rect.id).locked).toBe(false) }) test('visibility can be toggled', () => { @@ -226,11 +228,11 @@ describe('scene graph — locked node operations', () => { height: 50 }) - expect(graph.getNode(rect.id)!.visible).toBe(true) + expect(getNodeOrThrow(graph, rect.id).visible).toBe(true) graph.updateNode(rect.id, { visible: false }) - expect(graph.getNode(rect.id)!.visible).toBe(false) + expect(getNodeOrThrow(graph, rect.id).visible).toBe(false) graph.updateNode(rect.id, { visible: true }) - expect(graph.getNode(rect.id)!.visible).toBe(true) + expect(getNodeOrThrow(graph, rect.id).visible).toBe(true) }) }) @@ -255,7 +257,7 @@ describe('hitTest — frame with children', () => { const hit = graph.hitTest(70, 70, frame.id) expect(hit).not.toBeNull() - expect(hit!.id).toBe(child.id) + expect(expectDefined(hit, 'hit node').id).toBe(child.id) }) test('rotated frame scope hit test finds children using rotated local bounds', () => { @@ -280,7 +282,7 @@ describe('hitTest — frame with children', () => { // point that is inside rotated frame bounds and child bounds const hitInside = graph.hitTest(193, 111, frame.id) expect(hitInside).not.toBeNull() - expect(hitInside!.id).toBe(child.id) + expect(expectDefined(hitInside, 'inside hit node').id).toBe(child.id) // point that would be inside if frame were not rotated const hitOutside = graph.hitTest(160, 130, frame.id) @@ -309,7 +311,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => { const hit = graph.hitTest(10, 10, page) expect(hit).not.toBeNull() - expect(hit!.name).toBe('MyComp') + expect(expectDefined(hit, 'hit node').name).toBe('MyComp') }) test('hitTestDeep inside COMPONENT scope finds child', () => { @@ -332,7 +334,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => { const hit = graph.hitTestDeep(10, 10, comp.id) expect(hit).not.toBeNull() - expect(hit!.id).toBe(child.id) + expect(expectDefined(hit, 'hit node').id).toBe(child.id) }) test('hitTest on INSTANCE returns instance itself', () => { @@ -355,7 +357,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => { const hit = graph.hitTest(55, 55, page) expect(hit).not.toBeNull() - expect(hit!.name).toBe('MyInstance') + expect(expectDefined(hit, 'hit node').name).toBe('MyInstance') }) test('hitTestDeep inside INSTANCE scope finds child', () => { @@ -378,7 +380,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => { const hit = graph.hitTestDeep(55, 55, inst.id) expect(hit).not.toBeNull() - expect(hit!.id).toBe(child.id) + expect(expectDefined(hit, 'hit node').id).toBe(child.id) }) }) @@ -410,7 +412,7 @@ describe('hitTest — absolute position and scope offset', () => { const hit = graph.hitTest(250, 360, frame.id) expect(hit).not.toBeNull() - expect(hit!.id).toBe(child.id) + expect(expectDefined(hit, 'hit node').id).toBe(child.id) const missHit = graph.hitTest(50, 60, frame.id) expect(missHit).toBeNull() diff --git a/tests/engine/kiwi/serialize-fixes.test.ts b/tests/engine/kiwi/serialize-fixes.test.ts index 3cd6d00ce..d0c5cbeb2 100644 --- a/tests/engine/kiwi/serialize-fixes.test.ts +++ b/tests/engine/kiwi/serialize-fixes.test.ts @@ -8,6 +8,8 @@ import { sceneNodeToKiwi } from '@open-pencil/core' +import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert' + beforeAll(async () => { await initCodec() }) @@ -55,7 +57,7 @@ describe('Fix 1: auto-layout child transforms', () => { const blobs: Uint8Array[] = [] // Serialize the parent — children are serialized recursively const changes = sceneNodeToKiwi( - graph.getNode(parent.id)!, + getNodeOrThrow(graph, parent.id), ROOT_GUID, 0, { value: 100 }, @@ -64,7 +66,10 @@ describe('Fix 1: auto-layout child transforms', () => { ) as Record[] // changes[0] = parent, changes[1] = child - const childNc = changes.find((nc) => nc.name === 'Child')! + const childNc = expectDefined( + changes.find((nc) => nc.name === 'Child'), + 'child node change' + ) expect(childNc).toBeDefined() expect(childNc.transform.m02).toBe(0) expect(childNc.transform.m12).toBe(0) @@ -93,7 +98,7 @@ describe('Fix 1: auto-layout child transforms', () => { const blobs: Uint8Array[] = [] const changes = sceneNodeToKiwi( - graph.getNode(parent.id)!, + getNodeOrThrow(graph, parent.id), ROOT_GUID, 0, { value: 100 }, @@ -101,7 +106,10 @@ describe('Fix 1: auto-layout child transforms', () => { blobs ) as Record[] - const absNc = changes.find((nc) => nc.name === 'AbsChild')! + const absNc = expectDefined( + changes.find((nc) => nc.name === 'AbsChild'), + 'absolute child node change' + ) expect(absNc).toBeDefined() expect(absNc.transform.m02).toBe(75) expect(absNc.transform.m12).toBe(120) @@ -128,7 +136,7 @@ describe('Fix 1: auto-layout child transforms', () => { const blobs: Uint8Array[] = [] const changes = sceneNodeToKiwi( - graph.getNode(parent.id)!, + getNodeOrThrow(graph, parent.id), ROOT_GUID, 0, { value: 100 }, @@ -136,7 +144,10 @@ describe('Fix 1: auto-layout child transforms', () => { blobs ) as Record[] - const childNc = changes.find((nc) => nc.name === 'Child')! + const childNc = expectDefined( + changes.find((nc) => nc.name === 'Child'), + 'child node change' + ) expect(childNc).toBeDefined() expect(childNc.transform.m02).toBe(30) expect(childNc.transform.m12).toBe(45) @@ -164,7 +175,7 @@ describe('Fix 1: auto-layout child transforms', () => { const blobs: Uint8Array[] = [] const changes = sceneNodeToKiwi( - graph.getNode(parent.id)!, + getNodeOrThrow(graph, parent.id), ROOT_GUID, 0, { value: 100 }, @@ -172,7 +183,10 @@ describe('Fix 1: auto-layout child transforms', () => { blobs ) as Record[] - const itemNc = changes.find((nc) => nc.name === 'Item')! + const itemNc = expectDefined( + changes.find((nc) => nc.name === 'Item'), + 'item node change' + ) expect(itemNc.transform.m02).toBe(0) expect(itemNc.transform.m12).toBe(0) }) @@ -241,7 +255,10 @@ describe('Fix 2: frameMaskDisabled is inverse of clipsContent', () => { const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) - const frame = [...reimported.nodes.values()].find((n) => n.name === 'ClipFrame')! + const frame = expectDefined( + [...reimported.nodes.values()].find((n) => n.name === 'ClipFrame'), + 'ClipFrame' + ) expect(frame.clipsContent).toBe(true) }) }) @@ -382,7 +399,10 @@ describe('Fix 4: text lineHeight serialization', () => { const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) - const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + [...reimported.nodes.values()].find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.lineHeight).toBe(28) }) }) @@ -409,7 +429,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => { const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) - const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + [...reimported.nodes.values()].find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('DM Sans') }) @@ -430,7 +453,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => { const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) - const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + [...reimported.nodes.values()].find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('Roboto') }) @@ -451,7 +477,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => { const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) - const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + [...reimported.nodes.values()].find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('Inter') }) @@ -533,7 +562,7 @@ describe('Integration: auto-layout component with all fixes', () => { // Verify kiwi output directly const blobs: Uint8Array[] = [] const changes = sceneNodeToKiwi( - graph.getNode(card.id)!, + getNodeOrThrow(graph, card.id), ROOT_GUID, 0, { value: 100 }, @@ -542,8 +571,14 @@ describe('Integration: auto-layout component with all fixes', () => { ) as Record[] const cardNc = changes[0] - const titleNc = changes.find((nc) => nc.name === 'Title')! - const valueNc = changes.find((nc) => nc.name === 'Value')! + const titleNc = expectDefined( + changes.find((nc) => nc.name === 'Title'), + 'Title node change' + ) + const valueNc = expectDefined( + changes.find((nc) => nc.name === 'Value'), + 'Value node change' + ) // Fix 1: children have zero transforms expect(titleNc.transform.m02).toBe(0) @@ -571,8 +606,14 @@ describe('Integration: auto-layout component with all fixes', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const cardNode = nodes.find((n) => n.name === 'StatCard')! - const titleNode = nodes.find((n) => n.name === 'Title')! + const cardNode = expectDefined( + nodes.find((n) => n.name === 'StatCard'), + 'StatCard node' + ) + const titleNode = expectDefined( + nodes.find((n) => n.name === 'Title'), + 'Title node' + ) expect(cardNode.layoutMode).toBe('VERTICAL') expect(cardNode.clipsContent).toBe(true) diff --git a/tests/engine/layout/auto-layout.test.ts b/tests/engine/layout/auto-layout.test.ts index 6725a2607..00836f47f 100644 --- a/tests/engine/layout/auto-layout.test.ts +++ b/tests/engine/layout/auto-layout.test.ts @@ -10,6 +10,7 @@ import { import { createEditorStore } from '@/app/editor/session' +import { getNodeOrThrow } from '#tests/helpers/assert' import { autoFrame, loadFixtureGraph, pageId, rect } from '#tests/helpers/layout' describe('Auto Layout', () => { @@ -394,7 +395,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(130) }) @@ -413,7 +414,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.height).toBe(110) }) @@ -433,7 +434,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(160) expect(f.height).toBe(90) }) @@ -591,7 +592,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(50) expect(f.height).toBe(30) }) @@ -616,7 +617,7 @@ describe('Auto Layout', () => { computeLayout(graph, outer.id) const children = graph.getChildren(outer.id) - const innerNode = graph.getNode(inner.id)! + const innerNode = getNodeOrThrow(graph, inner.id) expect(innerNode.width).toBe(50) expect(innerNode.height).toBe(50) expect(children[1].x).toBe(0) @@ -663,7 +664,7 @@ describe('Auto Layout', () => { computeLayout(graph, outer.id) - const innerNode = graph.getNode(inner.id)! + const innerNode = getNodeOrThrow(graph, inner.id) expect(innerNode.width).toBe(105) expect(innerNode.x).toBe(0) @@ -692,7 +693,7 @@ describe('Auto Layout', () => { computeLayout(graph, outer.id) - const innerNode = graph.getNode(inner.id)! + const innerNode = getNodeOrThrow(graph, inner.id) expect(innerNode.height).toBe(130) const outerChildren = graph.getChildren(outer.id) @@ -723,7 +724,7 @@ describe('Auto Layout', () => { computeAllLayouts(graph) - const middleNode = graph.getNode(middle.id)! + const middleNode = getNodeOrThrow(graph, middle.id) expect(middleNode.width).toBe(105) expect(middleNode.height).toBe(30) @@ -834,7 +835,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const c = graph.getNode(child.id)! + const c = getNodeOrThrow(graph, child.id) expect(c.x).toBe(100) expect(c.y).toBe(100) }) @@ -854,7 +855,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(60) expect(f.height).toBe(40) }) @@ -890,7 +891,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(10) expect(f.height).toBe(10) }) @@ -906,7 +907,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(50) const children = graph.getChildren(frame.id) @@ -929,7 +930,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(400) expect(f.height).toBe(120) }) @@ -949,7 +950,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.height).toBe(110) expect(f.width).toBe(200) }) @@ -975,7 +976,7 @@ describe('Auto Layout', () => { computeLayout(graph, outer.id) - const innerNode = graph.getNode(inner.id)! + const innerNode = getNodeOrThrow(graph, inner.id) // 400 - 100 - 10 = 290 expect(innerNode.width).toBe(290) }) @@ -1155,9 +1156,9 @@ describe('Auto Layout', () => { setTextMeasurer(null) - const updatedText = graph.getNode(text.id)! - const updatedArrow1 = graph.getNode(arrow1.id)! - const updatedArrow2 = graph.getNode(arrow2.id)! + const updatedText = getNodeOrThrow(graph, text.id) + const updatedArrow1 = getNodeOrThrow(graph, arrow1.id) + const updatedArrow2 = getNodeOrThrow(graph, arrow2.id) expect(updatedText.width).toBe(60) @@ -1320,7 +1321,7 @@ describe('Auto Layout', () => { computeAllLayouts(graph) setTextMeasurer(null) - const updatedText = graph.getNode(text.id)! + const updatedText = getNodeOrThrow(graph, text.id) // Should stretch to 300 - 20 - 20 = 260, NOT stay at 100 expect(updatedText.width).toBe(260) }) @@ -1354,7 +1355,7 @@ describe('Auto Layout', () => { computeAllLayouts(graph) setTextMeasurer(null) - const updatedText = graph.getNode(text.id)! + const updatedText = getNodeOrThrow(graph, text.id) expect(updatedText.width).toBe(300) expect(updatedText.height).toBe(20) }) @@ -1395,7 +1396,7 @@ describe('Auto Layout', () => { // 400 - 100 - 10 = 290 available for the fill text expect(receivedWidths.length).toBeGreaterThan(0) - const updatedText = graph.getNode(text.id)! + const updatedText = getNodeOrThrow(graph, text.id) expect(updatedText.width).toBe(290) }) @@ -1429,7 +1430,7 @@ describe('Auto Layout', () => { setTextMeasurer(null) expect(measureCalled).toBe(false) - const updatedText = graph.getNode(text.id)! + const updatedText = getNodeOrThrow(graph, text.id) expect(updatedText.width).toBe(150) expect(updatedText.height).toBe(40) }) @@ -1512,7 +1513,7 @@ describe('Auto Layout', () => { computeLayout(graph, outer.id) - const innerNode = graph.getNode(inner.id)! + const innerNode = getNodeOrThrow(graph, inner.id) expect(innerNode.width).toBe(250) }) }) @@ -1711,17 +1712,17 @@ describe('Auto Layout', () => { const child = rect(graph, frame.id, 80, 60) computeLayout(graph, frame.id) - expect(graph.getNode(child.id)!.width).toBe(80) + expect(getNodeOrThrow(graph, child.id).width).toBe(80) graph.updateNode(child.id, { visible: false }) computeLayout(graph, frame.id) - expect(graph.getNode(child.id)!.width).toBe(80) - expect(graph.getNode(child.id)!.height).toBe(60) + expect(getNodeOrThrow(graph, child.id).width).toBe(80) + expect(getNodeOrThrow(graph, child.id).height).toBe(60) graph.updateNode(child.id, { visible: true }) computeLayout(graph, frame.id) - expect(graph.getNode(child.id)!.width).toBe(80) - expect(graph.getNode(child.id)!.height).toBe(60) + expect(getNodeOrThrow(graph, child.id).width).toBe(80) + expect(getNodeOrThrow(graph, child.id).height).toBe(60) }) }) @@ -1740,7 +1741,7 @@ describe('Auto Layout', () => { computeLayout(graph, frame.id) - const f = graph.getNode(frame.id)! + const f = getNodeOrThrow(graph, frame.id) expect(f.width).toBe(100) const children = graph.getChildren(frame.id) diff --git a/tests/engine/render/canvas/silhouette-autopsy.test.ts b/tests/engine/render/canvas/silhouette-autopsy.test.ts index e753956fe..097120a84 100644 --- a/tests/engine/render/canvas/silhouette-autopsy.test.ts +++ b/tests/engine/render/canvas/silhouette-autopsy.test.ts @@ -19,6 +19,7 @@ import { SkiaRenderer } from '#core/canvas' import { SceneGraph } from '#core/scene-graph' import { fontManager } from '#core/text' +import { expectDefined } from '#tests/helpers/assert' import { coreSourcePath, publicPath, testPath as repoTestPath } from '#tests/helpers/paths' // === CLAIM EXTRACTION === @@ -75,7 +76,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { // Use a targeted search near the Effect interface definition const effectInterfaceMatch = src.match(/interface Effect\s*\{([^}]+)\}/) expect(effectInterfaceMatch).toBeTruthy() - const effectBody = effectInterfaceMatch![1] + const effectBody = expectDefined(effectInterfaceMatch, 'effectInterfaceMatch')[1] for (const type of [ 'DROP_SHADOW', 'INNER_SHADOW', @@ -107,7 +108,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { /export function renderShapeUncached[\s\S]*?(?=\nexport function|\n$)/ ) expect(renderShapeUncachedMatch).toBeTruthy() - const body = renderShapeUncachedMatch![0] + const body = expectDefined(renderShapeUncachedMatch, 'renderShapeUncachedMatch')[0] // Find indices of key calls const behindIdx = body.indexOf("'behind'") @@ -132,7 +133,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { /export function renderNode[\s\S]*?(?=\nexport function|\nexport const)/ ) expect(renderNodeMatch).toBeTruthy() - const body = renderNodeMatch![0] + const body = expectDefined(renderNodeMatch, 'renderNodeMatch')[0] // Verify opacity saveLayer comes before layerBlur saveLayer const opacityLayerIdx = body.indexOf('opacity < 1') @@ -170,7 +171,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { /function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/ ) expect(drawTextInnerShadowMatch).toBeTruthy() - const body = drawTextInnerShadowMatch![0] + const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0] // Count saveLayer calls — there are 4 (Master, Restrictive, Blur, DstOut) // plus 2 canvas.save() calls (child transform + offset transform) @@ -199,7 +200,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { /export function renderShape[\s\S]*?(?=\nexport function|\nexport const)/ ) expect(renderShapeMatch).toBeTruthy() - const body = renderShapeMatch![0] + const body = expectDefined(renderShapeMatch, 'renderShapeMatch')[0] expect(body).toContain('nodePictureCache') expect(body).toContain('PictureRecorder') }) @@ -213,7 +214,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => { /function getShadowShapeChild[\s\S]*?(?=\nfunction |\nexport )/ ) expect(getShadowShapeChildFn).toBeTruthy() - const body = getShadowShapeChildFn![0] + const body = expectDefined(getShadowShapeChildFn, 'getShadowShapeChildFn')[0] // No for loop over children expect(body).not.toMatch(/for\s*\(.*childIds/) // Accesses childIds twice: once for .length guard, once for [0] read @@ -243,7 +244,7 @@ describe('Doc 02 — Formula Deconstruction: Static Code Claims', () => { /function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/ ) expect(drawTextInnerShadowMatch).toBeTruthy() - const body = drawTextInnerShadowMatch![0] + const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0] // 4 saveLayer: Master, Restrictive, Blur, DstOut // 2 save: child transform, offset transform @@ -303,7 +304,7 @@ describe('Doc 03 — Artifact Analysis: Static + Runtime Verification', () => { /function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/ ) expect(drawTextInnerShadowMatch).toBeTruthy() - const body = drawTextInnerShadowMatch![0] + const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0] // Check the expand calculation expect(body).toContain('const expand') @@ -316,7 +317,7 @@ describe('Doc 03 — Artifact Analysis: Static + Runtime Verification', () => { const src = readSource(scenePath) const match = src.match(/function getShadowShapeChild[\s\S]*?(?=\nfunction |\nexport )/) expect(match).toBeTruthy() - const body = match![0] + const body = expectDefined(match, 'match')[0] // Verify it returns early after first child expect(body).toContain('return child') // Verify no array accumulation @@ -354,7 +355,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { void fontPath } - const surface = ck.MakeSurface(200, 200)! + const surface = expectDefined(ck.MakeSurface(200, 200), 'CanvasKit surface') renderer = new SkiaRenderer(ck, surface) renderer.fontProvider = fontProvider renderer.fontsLoaded = true @@ -516,7 +517,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { /function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/ ) expect(drawTextInnerShadowMatch).toBeTruthy() - const body = drawTextInnerShadowMatch![0] + const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0] // MakeBlend IS called (for tintFilter and solidBlackFilter) expect(body).toContain('MakeBlend') @@ -543,7 +544,7 @@ describe('Doc 01 — LAYER_BLUR Round-Trip Loss Verification', () => { const src = readSource(schemaPath) const effectTypeBlock = src.match(/enum EffectType\s*\{([^}]*)\}/) expect(effectTypeBlock).toBeTruthy() - const body = effectTypeBlock![1] + const body = expectDefined(effectTypeBlock, 'effectTypeBlock')[1] // Count the values (each is "NAME = N;") const valueCount = (body.match(/=\s*\d+\s*;/g) || []).length expect(valueCount).toBe(4) @@ -576,7 +577,7 @@ describe('Doc 02/03 — Cache Infrastructure Verification', () => { const rendererSrc = readSource(rendererPath) const destroyMatch = rendererSrc.match(/destroy\(\)[\s\S]*?(?=\n \w|\n\})/) expect(destroyMatch).toBeTruthy() - const body = destroyMatch![0] + const body = expectDefined(destroyMatch, 'destroyMatch')[0] // destroy() delegates to destroyRenderer — verify the lifecycle module cleans caches expect(body).toContain('destroyRenderer') const lifecycleSrc = readSource(lifecyclePath) diff --git a/tests/engine/scene-graph/basic.test.ts b/tests/engine/scene-graph/basic.test.ts index ecc15404f..217c15351 100644 --- a/tests/engine/scene-graph/basic.test.ts +++ b/tests/engine/scene-graph/basic.test.ts @@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test' import { SceneGraph } from '@open-pencil/core' +import { expectDefined } from '#tests/helpers/assert' + function pageId(graph: SceneGraph) { return graph.getPages()[0].id } @@ -16,7 +18,7 @@ describe('SceneGraph', () => { const id = rect(graph, 'Rect', 100, 100, 200, 150) const node = graph.getNode(id) expect(node).toBeDefined() - expect(node!.type).toBe('RECTANGLE') + expect(expectDefined(node, 'node').type).toBe('RECTANGLE') expect(node.x).toBe(100) expect(node.width).toBe(200) }) @@ -146,13 +148,13 @@ describe('SceneGraph', () => { const r = rect(graph, 'R', 100, 200) const node = graph.getNode(r) expect(node).toBeDefined() - expect(node!.parentId).toBe(page) + expect(expectDefined(node, 'node').parentId).toBe(page) graph.reparentNode(r, page) // Position should not change const after = graph.getNode(r) expect(after).toBeDefined() - expect(after!.x).toBe(100) - expect(after!.y).toBe(200) + expect(expectDefined(after, 'updated node').x).toBe(100) + expect(expectDefined(after, 'updated node').y).toBe(200) expect(after.parentId).toBe(page) expect(graph.getChildren(page)).toHaveLength(1) }) @@ -176,7 +178,7 @@ describe('SceneGraph', () => { // Now node is inside frame at (200,100), so local coords should be (100, 50) const after = graph.getNode(r) expect(after).toBeDefined() - expect(after!.x).toBe(100) + expect(expectDefined(after, 'updated node').x).toBe(100) expect(after.y).toBe(50) // Absolute position preserved const absAfter = graph.getAbsolutePosition(r) @@ -245,7 +247,7 @@ describe('SceneGraph', () => { // Inner was at (100,100), frame is at (50,50), so inner's local = (50,50) const innerNode = graph.getNode(inner) expect(innerNode).toBeDefined() - expect(innerNode!.x).toBe(50) + expect(expectDefined(innerNode, 'inner node').x).toBe(50) expect(innerNode.y).toBe(50) // Child still at (10,10) relative to inner, absolute = (110,110) expect(graph.getAbsolutePosition(child)).toEqual({ x: 110, y: 110 }) @@ -326,7 +328,7 @@ describe('SceneGraph', () => { graph.updateNode(id, { x: 200, name: 'Updated' }) const node = graph.getNode(id) expect(node).toBeDefined() - expect(node!.x).toBe(200) + expect(expectDefined(node, 'node').x).toBe(200) expect(node.name).toBe('Updated') }) @@ -340,7 +342,7 @@ describe('SceneGraph', () => { const child = graph.createNode('RECTANGLE', comp.id, { name: 'BG', width: 100, height: 40 }) const instance = graph.createInstance(comp.id, pageId(graph)) expect(instance).toBeDefined() - expect(instance!.type).toBe('INSTANCE') + expect(expectDefined(instance, 'instance').type).toBe('INSTANCE') expect(instance.componentId).toBe(comp.id) const instChildren = graph.getChildren(instance.id) expect(instChildren).toHaveLength(1) @@ -358,7 +360,7 @@ describe('SceneGraph', () => { const label = graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Hello', fontSize: 14 }) const instance = graph.createInstance(comp.id, pageId(graph)) expect(instance).toBeDefined() - const instLabel = graph.getChildren(instance!.id)[0] + const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0] expect(instLabel.text).toBe('Hello') graph.updateNode(label.id, { text: 'Updated', fontSize: 18 }) @@ -378,7 +380,7 @@ describe('SceneGraph', () => { graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Default', fontSize: 14 }) const instance = graph.createInstance(comp.id, pageId(graph)) expect(instance).toBeDefined() - const instLabel = graph.getChildren(instance!.id)[0] + const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0] // Override the text on the instance child graph.updateNode(instLabel.id, { text: 'Custom' }) @@ -403,12 +405,12 @@ describe('SceneGraph', () => { graph.createNode('RECTANGLE', comp.id, { name: 'BG' }) const instance = graph.createInstance(comp.id, pageId(graph)) expect(instance).toBeDefined() - expect(graph.getChildren(instance!.id)).toHaveLength(1) + expect(graph.getChildren(expectDefined(instance, 'instance').id)).toHaveLength(1) graph.createNode('TEXT', comp.id, { name: 'Label', text: 'New' }) graph.syncInstances(comp.id) - const instChildren = graph.getChildren(instance!.id) + const instChildren = graph.getChildren(expectDefined(instance, 'instance').id) expect(instChildren).toHaveLength(2) expect(instChildren[1].name).toBe('Label') expect(instChildren[1].text).toBe('New') @@ -424,10 +426,10 @@ describe('SceneGraph', () => { graph.createNode('RECTANGLE', comp.id, { name: 'BG' }) const instance = graph.createInstance(comp.id, pageId(graph)) expect(instance).toBeDefined() - expect(instance!.type).toBe('INSTANCE') + expect(expectDefined(instance, 'instance').type).toBe('INSTANCE') - graph.detachInstance(instance!.id) - expect(instance!.type).toBe('FRAME') + graph.detachInstance(expectDefined(instance, 'instance').id) + expect(expectDefined(instance, 'instance').type).toBe('FRAME') expect(instance.componentId).toBeNull() expect(graph.getInstances(comp.id)).toHaveLength(0) }) @@ -1019,13 +1021,13 @@ describe('updateNode', () => { expect(textNode).toBeDefined() // Simulate a cached textPicture const fakePicture = new Uint8Array([1, 2, 3]) - textNode!.textPicture = fakePicture + expectDefined(textNode, 'text node').textPicture = fakePicture // Changing fontSize (a TEXT_PICTURE_KEY) should null the cache graph.updateNode(textId, { fontSize: 24 }) const afterUpdate = graph.getNode(textId) expect(afterUpdate).toBeDefined() - expect(afterUpdate!.textPicture).toBeNull() + expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull() }) test('textPicture survives non-text property change on TEXT node', () => { @@ -1041,13 +1043,13 @@ describe('updateNode', () => { const fakePicture = new Uint8Array([4, 5, 6]) const textNode = graph.getNode(textId) expect(textNode).toBeDefined() - textNode!.textPicture = fakePicture + expectDefined(textNode, 'text node').textPicture = fakePicture // Changing opacity (NOT a TEXT_PICTURE_KEY) should preserve textPicture graph.updateNode(textId, { opacity: 0.5 }) const afterUpdate = graph.getNode(textId) expect(afterUpdate).toBeDefined() - expect(afterUpdate!.textPicture).toBe(fakePicture) + expect(expectDefined(afterUpdate, 'updated node').textPicture).toBe(fakePicture) }) test('textPicture is nulled when textPicture is already null', () => { @@ -1064,7 +1066,7 @@ describe('updateNode', () => { graph.updateNode(textId, { fontSize: 16 }) const afterUpdate = graph.getNode(textId) expect(afterUpdate).toBeDefined() - expect(afterUpdate!.textPicture).toBeNull() + expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull() }) }) diff --git a/tests/engine/svg/export.test.ts b/tests/engine/svg/export.test.ts index bdb7707df..d68c328ce 100644 --- a/tests/engine/svg/export.test.ts +++ b/tests/engine/svg/export.test.ts @@ -9,6 +9,8 @@ import { vectorNetworkToSVGPaths } from '@open-pencil/core' +import { expectDefined } from '#tests/helpers/assert' + function makeGraph() { const graph = new SceneGraph() graph.createNode('CANVAS', graph.rootId, { name: 'Page 1' }) @@ -23,6 +25,19 @@ function exportSVG(graph: SceneGraph, nodeIds: string[], xmlDecl = false): strin return renderNodesToSVG(graph, pageId(graph), nodeIds, { xmlDeclaration: xmlDecl }) } +function exportSVGOrThrow(graph: SceneGraph, nodeIds: string[], xmlDecl = false): string { + return expectDefined(exportSVG(graph, nodeIds, xmlDecl), 'SVG output') +} + +function renderNodesToSVGOrThrow( + graph: SceneGraph, + pageId: string, + nodeIds: string[], + options: Parameters[3] +): string { + return expectDefined(renderNodesToSVG(graph, pageId, nodeIds, options), 'SVG output') +} + // --- SVGNode builder tests --- describe('svg() and renderSVGNode()', () => { @@ -210,7 +225,7 @@ describe('renderNodesToSVG()', () => { height: 50, fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { height: 10, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const withDecl = renderNodesToSVG(graph, pageId(graph), [node.id], { xmlDeclaration: true })! - const withoutDecl = renderNodesToSVG(graph, pageId(graph), [node.id], { + const withDecl = renderNodesToSVGOrThrow(graph, pageId(graph), [node.id], { + xmlDeclaration: true + }) + const withoutDecl = renderNodesToSVGOrThrow(graph, pageId(graph), [node.id], { xmlDeclaration: false - })! + }) expect(withDecl).toStartWith(' { height: 60, fills: [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { starInnerRadius: 0.382, fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { bottomLeftRadius: 5, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { textAlignHorizontal: 'LEFT', fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('direction="rtl"') expect(result).toContain('text-anchor="end"') expect(result).toContain('x="180"') @@ -377,7 +394,7 @@ describe('renderNodesToSVG()', () => { { start: 6, length: 4, style: { fontWeight: 700 } } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { opacity: 0.5, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('opacity="0.5"') }) @@ -402,7 +419,7 @@ describe('renderNodesToSVG()', () => { rotation: 45, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('rotate(45,') }) @@ -422,7 +439,7 @@ describe('renderNodesToSVG()', () => { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('stroke-dasharray="5 3"') }) @@ -443,7 +460,7 @@ describe('renderNodesToSVG()', () => { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('stroke-linecap="round"') expect(result).toContain('stroke-linejoin="bevel"') }) @@ -463,7 +480,7 @@ describe('renderNodesToSVG()', () => { y: 10, fills: [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [frame.id])! + const result = exportSVGOrThrow(graph, [frame.id]) expect(result).toContain(' { height: 200, fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [frame.id])! + const result = exportSVGOrThrow(graph, [frame.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { height: 40, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [a.id, b.id])! + const result = exportSVGOrThrow(graph, [a.id, b.id]) expect(result).toContain('viewBox="0 0 100 50"') expect(result).toContain(' { blendMode: 'MULTIPLY', fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('mix-blend-mode: multiply') }) @@ -646,7 +663,7 @@ describe('renderNodesToSVG()', () => { height: 50, fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 0.5, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('fill="#FF0000') }) @@ -664,7 +681,7 @@ describe('renderNodesToSVG()', () => { y: 10, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [comp.id])! + const result = exportSVGOrThrow(graph, [comp.id]) expect(result).toContain(' { { type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.95, a: 1 }, opacity: 1, visible: true } ] }) - const result = exportSVG(graph, [section.id])! + const result = exportSVGOrThrow(graph, [section.id]) expect(result).toContain(' { flipX: true, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('scale(-1, 1)') }) @@ -710,7 +727,7 @@ describe('renderNodesToSVG()', () => { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain('stroke-opacity="0.5"') }) @@ -735,7 +752,7 @@ describe('renderNodesToSVG()', () => { height: 50, fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }] }) - const result = exportSVG(graph, [group.id])! + const result = exportSVGOrThrow(graph, [group.id]) expect(result).toContain(' { } ] }) - const result = exportSVG(graph, [node.id])! + const result = exportSVGOrThrow(graph, [node.id]) expect(result).toContain(' { test('start initializes state', () => { const { editor } = createEditor() @@ -45,15 +51,15 @@ describe('TextEditor', () => { test('insert at cursor position', () => { const { editor, node } = createEditor() - editor.state!.cursor = 5 + editorState(editor).cursor = 5 editor.insert(' Beautiful', node) expect(editor.state?.text).toBe('Hello Beautiful World') }) test('insert replaces selection', () => { const { editor, node } = createEditor() - editor.state!.selectionAnchor = 0 - editor.state!.cursor = 5 + editorState(editor).selectionAnchor = 0 + editorState(editor).cursor = 5 editor.insert('Goodbye', node) expect(editor.state?.text).toBe('Goodbye World') expect(editor.state?.cursor).toBe(7) @@ -69,15 +75,15 @@ describe('TextEditor', () => { test('backspace at start does nothing', () => { const { editor, node } = createEditor() - editor.state!.cursor = 0 + editorState(editor).cursor = 0 editor.backspace(node) expect(editor.state?.text).toBe('Hello World') }) test('backspace deletes selection', () => { const { editor, node } = createEditor() - editor.state!.selectionAnchor = 6 - editor.state!.cursor = 11 + editorState(editor).selectionAnchor = 6 + editorState(editor).cursor = 11 editor.backspace(node) expect(editor.state?.text).toBe('Hello ') expect(editor.state?.cursor).toBe(6) @@ -85,7 +91,7 @@ describe('TextEditor', () => { test('delete removes char after cursor', () => { const { editor, node } = createEditor() - editor.state!.cursor = 0 + editorState(editor).cursor = 0 editor.delete(node) expect(editor.state?.text).toBe('ello World') expect(editor.state?.cursor).toBe(0) @@ -99,8 +105,8 @@ describe('TextEditor', () => { test('delete removes selection', () => { const { editor, node } = createEditor() - editor.state!.selectionAnchor = 0 - editor.state!.cursor = 5 + editorState(editor).selectionAnchor = 0 + editorState(editor).cursor = 5 editor.delete(node) expect(editor.state?.text).toBe(' World') }) @@ -119,8 +125,8 @@ describe('TextEditor', () => { test('moveLeft collapses selection to start', () => { const { editor } = createEditor() - editor.state!.selectionAnchor = 3 - editor.state!.cursor = 8 + editorState(editor).selectionAnchor = 3 + editorState(editor).cursor = 8 editor.moveLeft() expect(editor.state?.cursor).toBe(3) expect(editor.state?.selectionAnchor).toBeNull() @@ -128,8 +134,8 @@ describe('TextEditor', () => { test('moveRight collapses selection to end', () => { const { editor } = createEditor() - editor.state!.selectionAnchor = 3 - editor.state!.cursor = 8 + editorState(editor).selectionAnchor = 3 + editorState(editor).cursor = 8 editor.moveRight() expect(editor.state?.cursor).toBe(8) expect(editor.state?.selectionAnchor).toBeNull() @@ -145,28 +151,28 @@ describe('TextEditor', () => { test('hasSelection', () => { const { editor } = createEditor() expect(editor.hasSelection()).toBe(false) - editor.state!.selectionAnchor = 0 + editorState(editor).selectionAnchor = 0 expect(editor.hasSelection()).toBe(true) }) test('hasSelection false when anchor equals cursor', () => { const { editor } = createEditor() - editor.state!.selectionAnchor = editor.state!.cursor + editorState(editor).selectionAnchor = editorState(editor).cursor expect(editor.hasSelection()).toBe(false) }) test('getSelectionRange', () => { const { editor } = createEditor() expect(editor.getSelectionRange()).toBeNull() - editor.state!.selectionAnchor = 8 - editor.state!.cursor = 3 + editorState(editor).selectionAnchor = 8 + editorState(editor).cursor = 3 expect(editor.getSelectionRange()).toEqual([3, 8]) }) test('getSelectedText', () => { const { editor } = createEditor() - editor.state!.selectionAnchor = 0 - editor.state!.cursor = 5 + editorState(editor).selectionAnchor = 0 + editorState(editor).cursor = 5 expect(editor.getSelectedText()).toBe('Hello') }) @@ -192,14 +198,14 @@ describe('TextEditor', () => { test('moveWordRight from start', () => { const { editor } = createEditor() - editor.state!.cursor = 0 + editorState(editor).cursor = 0 editor.moveWordRight() expect(editor.state?.cursor).toBe(6) }) test('moveLeft with extend creates selection', () => { const { editor } = createEditor() - editor.state!.cursor = 5 + editorState(editor).cursor = 5 editor.moveLeft(true) expect(editor.state?.selectionAnchor).toBe(5) expect(editor.state?.cursor).toBe(4)