diff --git a/oxlint.structure.json b/oxlint.structure.json index 27a7430bb..6bf1b0efd 100644 --- a/oxlint.structure.json +++ b/oxlint.structure.json @@ -20,8 +20,12 @@ }, { "files": [ + "tests/engine/copy.test.ts", "tests/engine/fig-roundtrip.test.ts", + "tests/engine/font-normalize-export.test.ts", + "tests/engine/mcp-stdio.test.ts", "tests/engine/nudge.test.ts", + "tests/engine/plugin-data.test.ts", "tests/engine/render.test.ts", "tests/engine/text-edit-undo.test.ts" ], diff --git a/tests/engine/copy.test.ts b/tests/engine/copy.test.ts index d7d7a2e27..583163671 100644 --- a/tests/engine/copy.test.ts +++ b/tests/engine/copy.test.ts @@ -9,6 +9,8 @@ import { copyGeometryPaths } from '#core/scene-graph/copy' +import { expectDefined } from '../helpers/assert' + import type { Fill, Stroke, Effect, StyleRun, GeometryPath } from '@open-pencil/core' describe('copy helpers — mutation isolation', () => { @@ -39,10 +41,10 @@ describe('copy helpers — mutation isolation', () => { gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } const copy = copyFill(original) - copy.gradientStops![0].color.r = 0 - copy.gradientTransform!.m00 = 99 - expect(original.gradientStops![0].color.r).toBe(1) - expect(original.gradientTransform!.m00).toBe(1) + expectDefined(copy.gradientStops?.[0], 'copied gradient stop').color.r = 0 + expectDefined(copy.gradientTransform, 'copied gradient transform').m00 = 99 + expect(expectDefined(original.gradientStops?.[0], 'original gradient stop').color.r).toBe(1) + expect(expectDefined(original.gradientTransform, 'original gradient transform').m00).toBe(1) }) test('copyStroke: dash pattern is independent', () => { @@ -55,7 +57,7 @@ describe('copy helpers — mutation isolation', () => { dashPattern: [5, 3] } const copy = copyStroke(original) - copy.dashPattern!.push(99) + expectDefined(copy.dashPattern, 'copied dash pattern').push(99) copy.color.g = 1 expect(original.dashPattern).toEqual([5, 3]) expect(original.color.g).toBe(0) diff --git a/tests/engine/font-normalize-export.test.ts b/tests/engine/font-normalize-export.test.ts index 5e7901d40..4e9a39b68 100644 --- a/tests/engine/font-normalize-export.test.ts +++ b/tests/engine/font-normalize-export.test.ts @@ -2,6 +2,8 @@ import { describe, test, expect, beforeAll } from 'bun:test' import { exportFigFile, parseFigFile, initCodec, SceneGraph } from '@open-pencil/core' +import { expectDefined, getNodeOrThrow } from '../helpers/assert' + beforeAll(async () => { await initCodec() }) @@ -29,7 +31,10 @@ describe('Font family normalization on .fig export', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const textNode = nodes.find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + nodes.find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('DM Sans') }) @@ -51,7 +56,10 @@ describe('Font family normalization on .fig export', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const textNode = nodes.find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + nodes.find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('Inter') }) @@ -73,7 +81,10 @@ describe('Font family normalization on .fig export', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const textNode = nodes.find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + nodes.find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fontFamily).toBe('Roboto') }) }) @@ -97,7 +108,10 @@ describe('TEXT node default fill', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const textNode = nodes.find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + nodes.find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fills.length).toBe(1) expect(textNode.fills[0].type).toBe('SOLID') expect(textNode.fills[0].color.r).toBe(0) @@ -125,7 +139,10 @@ describe('TEXT node default fill', () => { const reimported = await parseFigFile(exported.buffer as ArrayBuffer) const nodes = [...reimported.nodes.values()] - const textNode = nodes.find((n) => n.type === 'TEXT')! + const textNode = expectDefined( + nodes.find((n) => n.type === 'TEXT'), + 'text node' + ) expect(textNode.fills.length).toBe(1) expect(textNode.fills[0].color.r).toBeCloseTo(0.96, 1) expect(textNode.fills[0].color.g).toBeCloseTo(0.72, 1) @@ -141,6 +158,6 @@ describe('TEXT node default fill', () => { width: 100, height: 100 }) - expect(graph.getNode(node.id)!.fills.length).toBe(0) + expect(getNodeOrThrow(graph, node.id).fills.length).toBe(0) }) }) diff --git a/tests/engine/mcp-stdio.test.ts b/tests/engine/mcp-stdio.test.ts index e28bc7c5d..cac326c9e 100644 --- a/tests/engine/mcp-stdio.test.ts +++ b/tests/engine/mcp-stdio.test.ts @@ -10,6 +10,8 @@ import { executeRpcCommand } from '@open-pencil/core' +import { expectDefined, getNodeOrThrow } from '../helpers/assert' + import type { Client } from '@modelcontextprotocol/sdk/client/index.js' import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import type { AddressInfo } from 'node:net' @@ -35,8 +37,9 @@ function createMockApp() { try { let result: unknown if (msg.command === 'tool' && msg.args?.name) { - const def = ALL_TOOLS.find((t) => t.name === msg.args!.name) - if (!def) throw new Error(`Unknown tool: ${msg.args.name}`) + const toolName = msg.args.name + const def = ALL_TOOLS.find((t) => t.name === toolName) + if (!def) throw new Error(`Unknown tool: ${toolName}`) const api = new FigmaAPI(graph) api.currentPage = api.wrapNode(graph.getPages()[0].id) result = await def.execute(api, msg.args.args ?? {}) @@ -109,6 +112,14 @@ async function createStdioClient(wsPort: number) { return { client, transport } } +function textContent(content: unknown): string { + const items = content as { type: string; text: string }[] + return expectDefined( + items.find((c) => c.type === 'text'), + 'text content' + ).text +} + describe('MCP stdio transport', () => { let app: ReturnType let client: Client @@ -143,32 +154,29 @@ describe('MCP stdio transport', () => { arguments: { type: 'FRAME', x: 10, y: 20, width: 200, height: 100, name: 'StdioFrame' } }) expect(result.isError).not.toBe(true) - const data = JSON.parse( - (result.content as { type: string; text: string }[]).find((c) => c.type === 'text')!.text - ) as { id: string; name: string; type: string } + const data = JSON.parse(textContent(result.content)) as { + id: string + name: string + type: string + } expect(data.type).toBe('FRAME') expect(data.name).toBe('StdioFrame') const node = app.graph.getNode(data.id) - expect(node).toBeDefined() - expect(node!.width).toBe(200) + expect(getNodeOrThrow(app.graph, data.id).width).toBe(200) }) test('save_file via stdio succeeds', async () => { const result = await client.callTool({ name: 'save_file', arguments: {} }) expect(result.isError).not.toBe(true) - const data = JSON.parse( - (result.content as { type: string; text: string }[]).find((c) => c.type === 'text')!.text - ) as { saved: boolean } + const data = JSON.parse(textContent(result.content)) as { saved: boolean } expect(data.saved).toBe(true) }) test('get_codegen_prompt via stdio returns prompt', async () => { const result = await client.callTool({ name: 'get_codegen_prompt', arguments: {} }) expect(result.isError).not.toBe(true) - const data = JSON.parse( - (result.content as { type: string; text: string }[]).find((c) => c.type === 'text')!.text - ) as { prompt: string } + const data = JSON.parse(textContent(result.content)) as { prompt: string } expect(data.prompt.length).toBeGreaterThan(100) }) @@ -177,9 +185,7 @@ describe('MCP stdio transport', () => { name: 'create_shape', arguments: { type: 'RECTANGLE', x: 0, y: 0, width: 50, height: 50 } }) - const { id } = JSON.parse( - (create.content as { type: string; text: string }[]).find((c) => c.type === 'text')!.text - ) as { id: string } + const { id } = JSON.parse(textContent(create.content)) as { id: string } expect(app.graph.getNode(id)).toBeDefined() diff --git a/tests/engine/plugin-data.test.ts b/tests/engine/plugin-data.test.ts index 5c6be0c31..31ce04bcf 100644 --- a/tests/engine/plugin-data.test.ts +++ b/tests/engine/plugin-data.test.ts @@ -12,6 +12,8 @@ import { type NodeChange } from '@open-pencil/core' +import { expectDefined } from '../helpers/assert' + function doc(): NodeChange { return { guid: { sessionID: 0, localID: 0 }, @@ -154,9 +156,12 @@ describe('plugin data deduplication', () => { const changes: NodeChange[] = [doc(), canvas(), node('FRAME', 10, 1, { pluginData: entries })] deduplicateNodeChangePluginData(changes) - const frameChange = changes.find((nc) => nc.type === 'FRAME')! + const frameChange = expectDefined( + changes.find((nc) => nc.type === 'FRAME'), + 'frame change' + ) expect(frameChange.pluginData).toHaveLength(1) - expect(frameChange.pluginData![0]).toEqual({ + expect(expectDefined(frameChange.pluginData?.[0], 'plugin data entry')).toEqual({ pluginID: 'open-pencil', key: 'textDirection', value: 'RTL' @@ -304,7 +309,7 @@ describe('FigmaNodeProxy plugin data split-brain regression', () => { const page = graph.getPages()[0] const frame = graph.getChildren(page.id)[0] const api = new FigmaAPI(graph) - const proxy = api.getNodeById(frame.id)! + const proxy = expectDefined(api.getNodeById(frame.id), 'frame proxy') // Update: old pluginData entry must be replaced, not kept alongside the new one proxy.setSharedPluginData('tokens', 'accent', 'v2') @@ -322,7 +327,7 @@ describe('FigmaNodeProxy plugin data split-brain regression', () => { expect(accentEntries[0].value).toBe('v2') const parsedApi = new FigmaAPI(parsed) - const parsedProxy = parsedApi.getNodeById(parsedFrame.id)! + const parsedProxy = expectDefined(parsedApi.getNodeById(parsedFrame.id), 'parsed frame proxy') expect(parsedProxy.getSharedPluginData('tokens', 'accent')).toBe('v2') }) @@ -340,7 +345,7 @@ describe('FigmaNodeProxy plugin data split-brain regression', () => { const page = graph.getPages()[0] const frame = graph.getChildren(page.id)[0] const api = new FigmaAPI(graph) - const proxy = api.getNodeById(frame.id)! + const proxy = expectDefined(api.getNodeById(frame.id), 'frame proxy') // Delete by setting empty string — must purge from pluginData proxy.setSharedPluginData('tokens', 'accent', '') @@ -356,7 +361,7 @@ describe('FigmaNodeProxy plugin data split-brain regression', () => { expect(parsedFrame.pluginData.some((e) => e.key === 'tokens/accent')).toBe(false) const parsedApi = new FigmaAPI(parsed) - const parsedProxy = parsedApi.getNodeById(parsedFrame.id)! + const parsedProxy = expectDefined(parsedApi.getNodeById(parsedFrame.id), 'parsed frame proxy') expect(parsedProxy.getSharedPluginData('tokens', 'accent')).toBe('') }) })