chore(tests): expand non-null assertion cleanup

- Remove non-null assertions from copy, font export, MCP stdio, and plugin data tests
- Reuse shared assertion helpers for required fixtures, proxy nodes, and text tool output
- Enforce the non-null assertion ban for the newly cleaned test files
This commit is contained in:
Danila Poyarkov 2026-05-06 01:18:31 +03:00
parent 51eae14e4b
commit e9e36d5bf5
5 changed files with 67 additions and 33 deletions

View file

@ -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"
],

View file

@ -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)

View file

@ -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)
})
})

View file

@ -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<typeof createMockApp>
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()

View file

@ -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('')
})
})