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:
parent
51eae14e4b
commit
e9e36d5bf5
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue