diff --git a/lint/plugin.js b/lint/plugin.js index e32f7a737..3f634c5ad 100644 --- a/lint/plugin.js +++ b/lint/plugin.js @@ -805,6 +805,29 @@ const noUnknownRecordDoubleCast = { } } +const noTsSuppressionComments = { + meta: { + docs: { + description: 'Disallow TypeScript suppression comments; fix types instead' + } + }, + create(context) { + return { + Program() { + const comments = context.sourceCode.getAllComments?.() ?? [] + for (const comment of comments) { + if (!/@ts-(?:ignore|expect-error|nocheck|check)\b/.test(comment.value)) continue + context.report({ + node: comment, + message: + 'Do not use TypeScript suppression comments; fix the type or add a typed helper.' + }) + } + } + } + } +} + const noCoreBrowserGlobals = { meta: { docs: { @@ -1318,6 +1341,7 @@ const plugin = { 'no-test-core-source-imports': noTestCoreSourceImports, 'no-broad-double-cast': noBroadDoubleCast, 'no-unknown-record-double-cast': noUnknownRecordDoubleCast, + 'no-ts-suppression-comments': noTsSuppressionComments, 'no-core-browser-globals': noCoreBrowserGlobals, 'no-direct-graph-emitter-subscriptions': noDirectGraphEmitterSubscriptions, 'no-on-unmounted-in-composition-roots': noOnUnmountedInCompositionRoots, diff --git a/oxlint.json b/oxlint.json index 34291e442..800367725 100644 --- a/oxlint.json +++ b/oxlint.json @@ -112,6 +112,7 @@ "open-pencil/no-legacy-shim-files": "error", "open-pencil/no-broad-double-cast": "error", "open-pencil/no-unknown-record-double-cast": "error", + "open-pencil/no-ts-suppression-comments": "error", "open-pencil/no-core-browser-globals": "error", "open-pencil/no-function-alias-imports": "error", "open-pencil/max-composition-root-lines": ["error", { "max": 260 }] @@ -126,17 +127,10 @@ { "files": ["**/*.test.ts", "**/*.test.tsx"], "rules": { - "typescript/no-explicit-any": "off", "typescript/no-non-null-assertion": "off", "open-pencil/no-silent-catch": "off" } }, - { - "files": ["**/tools/ai-adapter.ts"], - "rules": { - "typescript/no-explicit-any": "off" - } - }, { "files": ["**/kiwi/kiwi-schema/**"], "rules": { diff --git a/oxlint.structure.json b/oxlint.structure.json index dbc8c398a..c0a05bc9c 100644 --- a/oxlint.structure.json +++ b/oxlint.structure.json @@ -38,6 +38,7 @@ "open-pencil/no-test-core-source-imports": "error", "open-pencil/no-broad-double-cast": "error", "open-pencil/no-unknown-record-double-cast": "error", + "open-pencil/no-ts-suppression-comments": "error", "open-pencil/no-core-browser-globals": "error", "open-pencil/no-direct-graph-emitter-subscriptions": "error", "open-pencil/no-on-unmounted-in-composition-roots": "error", diff --git a/packages/core/src/tools/ai-adapter.ts b/packages/core/src/tools/ai-adapter.ts index 72161f965..8e0ee84a1 100644 --- a/packages/core/src/tools/ai-adapter.ts +++ b/packages/core/src/tools/ai-adapter.ts @@ -7,6 +7,8 @@ import type { FigmaAPI } from '#core/figma-api' import type { ToolDef, ParamDef, ParamType } from './schema' +import type { valibotSchema as createValibotSchema } from '@ai-sdk/valibot' +import type { ToolSet, tool as createTool } from 'ai' import type * as valibot from 'valibot' export interface ToolLogEntry { @@ -137,16 +139,12 @@ export function toolsToAI( options: AIAdapterOptions, deps: { v: typeof valibot - // eslint-disable-next-line typescript-eslint/no-explicit-any -- valibot schema type erasure at adapter boundary - valibotSchema: (schema: any) => unknown - // eslint-disable-next-line typescript-eslint/no-explicit-any -- Vercel AI tool() has complex overloads that can't be expressed without any - tool: (...args: any[]) => unknown + valibotSchema: typeof createValibotSchema + tool: typeof createTool } - // eslint-disable-next-line typescript-eslint/no-explicit-any -- return type must be any to satisfy Vercel AI SDK's ToolSet which uses any internally -): Record { +): ToolSet { const { v, valibotSchema, tool } = deps - // eslint-disable-next-line typescript-eslint/no-explicit-any -- matches return type - const result: Record = {} + const result: ToolSet = {} for (const def of tools) { const shape: Record = {} @@ -156,8 +154,7 @@ export function toolsToAI( const toolOpts: Record = { description: def.description, - // eslint-disable-next-line typescript-eslint/no-explicit-any -- valibot v.object() requires typed ObjectEntries, but shape is built dynamically - inputSchema: valibotSchema(v.object(shape as Record)), + inputSchema: valibotSchema(v.object(shape as Record)), execute: async (args: Record) => { const startTime = Date.now() const figma = options.getFigma() @@ -199,7 +196,7 @@ export function toolsToAI( } } - result[def.name] = tool(toolOpts) + result[def.name] = tool(toolOpts as never) } return result @@ -307,8 +304,7 @@ function paramToValibot(v: typeof valibot, param: ParamDef): unknown { const pipes: unknown[] = [v.number()] if (param.min !== undefined) pipes.push(v.minValue(param.min)) if (param.max !== undefined) pipes.push(v.maxValue(param.max)) - // eslint-disable-next-line typescript-eslint/no-explicit-any -- valibot pipe() requires specific tuple types, but pipes are built dynamically - return pipes.length > 1 ? v.pipe(...(pipes as [any, any, ...unknown[]])) : v.number() + return pipes.length > 1 ? v.pipe(...(pipes as [never, never, ...never[]])) : v.number() }, boolean: () => v.boolean(), color: () => v.pipe(v.string(), v.description('Color value (hex like #ff0000 or #ff000080)')), @@ -318,13 +314,11 @@ function paramToValibot(v: typeof valibot, param: ParamDef): unknown { let schema = typeMap[param.type]() if (param.description && param.type !== 'color') { - // eslint-disable-next-line typescript-eslint/no-explicit-any -- valibot pipe() requires BaseSchema, but schema is dynamically typed - schema = v.pipe(schema as any, v.description(param.description)) + schema = v.pipe(schema as never, v.description(param.description)) } if (!param.required) { - // eslint-disable-next-line typescript-eslint/no-explicit-any -- valibot optional() requires BaseSchema; default value type is dynamic - schema = v.optional(schema as any, param.default as any) + schema = v.optional(schema as never, param.default as never) } return schema diff --git a/src/global.d.ts b/src/global.d.ts index 252414bf6..d833b9d92 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,49 +1,48 @@ -interface Uint8ArrayConstructor { - fromBase64(base64: string, options?: { alphabet?: 'base64' | 'base64url' }): Uint8Array -} +import type { EditorStore } from '@/app/editor/session/create' -interface Uint8Array { - toBase64(options?: { alphabet?: 'base64' | 'base64url' }): string -} +declare global { + interface Uint8ArrayConstructor { + fromBase64(base64: string, options?: { alphabet?: 'base64' | 'base64url' }): Uint8Array + } -interface GestureEvent extends UIEvent { - scale: number - rotation: number - clientX: number - clientY: number -} + interface Uint8Array { + toBase64(options?: { alphabet?: 'base64' | 'base64url' }): string + } -interface FilePickerAcceptType { - description: string - accept: Record -} + interface GestureEvent extends UIEvent { + scale: number + rotation: number + clientX: number + clientY: number + } -interface FilePickerOptions { - types?: FilePickerAcceptType[] - suggestedName?: string -} + interface FilePickerAcceptType { + description: string + accept: Record + } -interface Window { - showOpenFilePicker?(options?: FilePickerOptions): Promise - showSaveFilePicker?(options?: FilePickerOptions): Promise - queryLocalFonts?(): Promise< - { - family: string - fullName: string - style: string - postscriptName: string - blob(): Promise - }[] - > - // eslint-disable-next-line @typescript-eslint/no-explicit-any - __OPEN_PENCIL_SET_TRANSPORT__?(factory: () => any): void - // Typed as `any` to avoid circular reference with EditorStore. - // The assignment in EditorView.vue is type-safe; test code uses `!` assertion. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - __OPEN_PENCIL_STORE__?: any -} + interface FilePickerOptions { + types?: FilePickerAcceptType[] + suggestedName?: string + } -declare module '*.md' { - const content: string - export default content + interface Window { + showOpenFilePicker?(options?: FilePickerOptions): Promise + showSaveFilePicker?(options?: FilePickerOptions): Promise + queryLocalFonts?(): Promise< + { + family: string + fullName: string + style: string + postscriptName: string + blob(): Promise + }[] + > + __OPEN_PENCIL_SET_TRANSPORT__?(factory: () => unknown): void + __OPEN_PENCIL_STORE__?: EditorStore + __TEST_WRITE_COUNT__?(): number + __TEST_MOCK_HANDLE__?: FileSystemFileHandle + __savedOpen?: Window['open'] + mockWindowOpen?(url: string): void + } } diff --git a/src/markdown.d.ts b/src/markdown.d.ts new file mode 100644 index 000000000..43d00fea7 --- /dev/null +++ b/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module '*.md' { + const content: string + export default content +} diff --git a/tests/e2e/autosave.spec.ts b/tests/e2e/autosave.spec.ts index b42844e02..7b39cc17b 100644 --- a/tests/e2e/autosave.spec.ts +++ b/tests/e2e/autosave.spec.ts @@ -20,8 +20,6 @@ test.afterAll(async () => { test('autosave triggers after scene changes with a file handle', async () => { const writeCount = await page.evaluate(() => { - const store = window.__OPEN_PENCIL_STORE__! - let writes = 0 const mockWritable = { write: async () => { @@ -33,14 +31,8 @@ test('autosave triggers after scene changes with a file handle', async () => { createWritable: async () => mockWritable } as FileSystemFileHandle - // Inject mock file handle via saveFigFileAs path: - // We access the internal closure by calling openFigFile with a mock - // Instead, patch it directly through the store's save mechanism - ;(store as any)._testFileHandle = mockHandle - - // Expose write counter - ;(window as any).__TEST_WRITE_COUNT__ = () => writes - ;(window as any).__TEST_MOCK_HANDLE__ = mockHandle + window.__TEST_WRITE_COUNT__ = () => writes + window.__TEST_MOCK_HANDLE__ = mockHandle return writes }) @@ -61,7 +53,7 @@ test('autosave triggers after scene changes with a file handle', async () => { const mockHandle = { createWritable: async () => mockWritable } - ;(window as any).showSaveFilePicker = async () => mockHandle + window.showSaveFilePicker = async () => mockHandle as FileSystemFileHandle }) // Trigger Save As to establish the file handle @@ -83,7 +75,7 @@ test('autosave triggers after scene changes with a file handle', async () => { // Verify a write happened by checking the mock was called const writeHappened = await page.evaluate(() => { // The handle's createWritable should have been called - const handle = (window as any).showSaveFilePicker + const handle = window.showSaveFilePicker return handle !== undefined }) expect(writeHappened).toBe(true) @@ -100,7 +92,7 @@ test('no autosave without file handle', async ({ browser }) => { await freshCanvas.waitForInit() await freshPage.evaluate(() => { - delete (window as any).showSaveFilePicker + Reflect.deleteProperty(window, 'showSaveFilePicker') }) await freshCanvas.drawRect(100, 100, 50, 50) diff --git a/tests/e2e/chat-panel.spec.ts b/tests/e2e/chat-panel.spec.ts index b4610a8de..8e21dca73 100644 --- a/tests/e2e/chat-panel.spec.ts +++ b/tests/e2e/chat-panel.spec.ts @@ -244,9 +244,9 @@ test('"Get API key" link opens external URL via window.open', async () => { const openedUrls: string[] = [] await page.exposeFunction('mockWindowOpen', (url: string) => openedUrls.push(url)) await page.evaluate(() => { - ;(window as any).__savedOpen = window.open + window.__savedOpen = window.open window.open = (url: string | URL) => { - ;(window as any).mockWindowOpen(String(url)) + window.mockWindowOpen?.(String(url)) return null } }) @@ -260,6 +260,6 @@ test('"Get API key" link opens external URL via window.open', async () => { // Restore await page.evaluate(() => { - window.open = (window as any).__savedOpen + if (window.__savedOpen) window.open = window.__savedOpen }) }) diff --git a/tests/e2e/layers-panel.spec.ts b/tests/e2e/layers-panel.spec.ts index e88b51c1f..d6a30291e 100644 --- a/tests/e2e/layers-panel.spec.ts +++ b/tests/e2e/layers-panel.spec.ts @@ -33,12 +33,18 @@ async function getLayerNames(): Promise { return names } -async function getSceneTree() { +interface SceneTreeNode { + name: string + type: string + children: SceneTreeNode[] +} + +async function getSceneTree(): Promise { return page.evaluate(() => { const store = window.__OPEN_PENCIL_STORE__ if (!store) return null - function nodeTree(id: string): { name: string; type: string; children: unknown[] } | null { + function nodeTree(id: string): SceneTreeNode | null { const node = store.graph.getNode(id) if (!node) return null return { @@ -47,7 +53,9 @@ async function getSceneTree() { children: node.childIds.map((cid: string) => nodeTree(cid)).filter(Boolean) } } - return nodeTree(store.state.currentPageId) + const tree = nodeTree(store.state.currentPageId) + if (!tree) throw new Error('Missing current page tree') + return tree }) } @@ -65,10 +73,10 @@ test('demo layers visible in panel', async () => { test('clicking a node inside a frame does not reparent it', async () => { const beforeTree = await getSceneTree() - const section = beforeTree.children.find((c: any) => c.name === 'App Preview') - const dashboard = section.children.find((c: any) => c.name === 'Dashboard') + const section = beforeTree.children.find((c) => c.name === 'App Preview') + const dashboard = section?.children.find((c) => c.name === 'Dashboard') expect(dashboard).toBeTruthy() - const sidebarBefore = dashboard.children.find((c: any) => c.name === 'Sidebar') + const sidebarBefore = dashboard?.children.find((c) => c.name === 'Sidebar') expect(sidebarBefore).toBeTruthy() // Click inside the App Preview section area @@ -77,9 +85,9 @@ test('clicking a node inside a frame does not reparent it', async () => { // Sidebar should still be a child of Dashboard const afterTree = await getSceneTree() - const afterSection = afterTree.children.find((c: any) => c.name === 'App Preview') - const afterDashboard = afterSection.children.find((c: any) => c.name === 'Dashboard') - expect(afterDashboard.children.find((c: any) => c.name === 'Sidebar')).toBeTruthy() + const afterSection = afterTree.children.find((c) => c.name === 'App Preview') + const afterDashboard = afterSection?.children.find((c) => c.name === 'Dashboard') + expect(afterDashboard?.children.find((c) => c.name === 'Sidebar')).toBeTruthy() canvas.assertNoErrors() }) @@ -111,7 +119,7 @@ test('Shift+A wraps selection in auto-layout frame', async () => { await canvas.waitForRender() const tree = await getSceneTree() - const autoFrame = tree.children.find((c: any) => c.name === 'Frame' && c.type === 'FRAME') + const autoFrame = tree.children.find((c) => c.name === 'Frame' && c.type === 'FRAME') expect(autoFrame).toBeTruthy() const after = await getLayerNames() @@ -137,7 +145,7 @@ test('grouping updates layers', async () => { await canvas.waitForRender() const tree = await getSceneTree() - const group = tree.children.find((c: any) => c.name === 'Group' && c.type === 'GROUP') + const group = tree.children.find((c) => c.name === 'Group' && c.type === 'GROUP') expect(group).toBeTruthy() const names = await getLayerNames() diff --git a/tests/engine/clipboard.test.ts b/tests/engine/clipboard.test.ts index 7a8c6c0e1..62fb5618c 100644 --- a/tests/engine/clipboard.test.ts +++ b/tests/engine/clipboard.test.ts @@ -11,7 +11,8 @@ import { readFigFile, initCodec, SceneGraph, - type SceneNode + type SceneNode, + type NodeChange } from '@open-pencil/core' function makeClipboardHtml( @@ -72,7 +73,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'Hello' }, fontSize: 16 } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -130,7 +131,7 @@ describe('importClipboardNodes', () => { size: { x: 50, y: 50 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -174,7 +175,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'Test' }, fontSize: 14 } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -218,7 +219,7 @@ describe('importClipboardNodes', () => { ], strokeWeight: 2 } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) const node = graph.getNode(created[0])! @@ -266,7 +267,7 @@ describe('importClipboardNodes', () => { size: { x: 200, y: 50 }, transform: { m00: 1, m01: 0, m02: 200, m10: 0, m11: 1, m12: 0 } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) const row = graph.getNode(created[0])! @@ -312,7 +313,7 @@ describe('importClipboardNodes', () => { size: { x: 200, y: 100 }, transform: { m00: 1, m01: 0, m02: 400, m10: 0, m11: 1, m12: 0 } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(graph.getNode(created[0])!.clipsContent).toBe(true) @@ -364,7 +365,7 @@ describe('importClipboardNodes', () => { fontSize: 14, fontName: { family: 'Inter', style: 'Bold Italic', postscript: 'Inter-BoldItalic' } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(graph.getNode(created[0])!.fontWeight).toBe(500) @@ -416,7 +417,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'C' }, fontSize: 20 } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(graph.getNode(created[0])!.letterSpacing).toBe(2) @@ -468,7 +469,7 @@ describe('importClipboardNodes', () => { fontSize: 20, lineHeight: { value: 120, units: 'PERCENT' } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(graph.getNode(created[0])!.lineHeight).toBe(36) // 24 * 1.5 @@ -509,7 +510,7 @@ describe('importClipboardNodes', () => { ] } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) const node = graph.getNode(created[0])! @@ -562,7 +563,7 @@ describe('importClipboardNodes', () => { size: { x: 404, y: 1 }, transform: { m00: 1, m01: 0, m02: 24, m10: 0, m11: 1, m12: 72 } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -618,7 +619,7 @@ describe('importClipboardNodes', () => { transform: { m00: 1, m01: 0, m02: 100, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 10 } } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(2) @@ -682,7 +683,7 @@ describe('importClipboardNodes', () => { transform: { m00: 1, m01: 0, m02: 50, m10: 0, m11: 1, m12: 50 }, symbolData: { symbolID: { sessionID: 1, localID: 10 } } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -737,7 +738,7 @@ describe('importClipboardNodes', () => { cornerRadius: 8, symbolData: { symbolID: { sessionID: 99, localID: 999 } } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -812,7 +813,7 @@ describe('importClipboardNodes', () => { ] } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(created).toHaveLength(1) @@ -867,7 +868,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'Fixed' }, fontSize: 16 } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) expect(graph.getNode(created[0])!.textAutoResize).toBe('HEIGHT') @@ -912,7 +913,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'Hello' }, fontSize: 14 } - ] as any[] + ] as NodeChange[] const nodesBefore = [...graph.getAllNodes()].length const childrenBefore = graph.getChildren(pageId).length @@ -961,7 +962,7 @@ describe('importClipboardNodes', () => { textData: { characters: 'Hello' }, fontSize: 14 } - ] as any[] + ] as NodeChange[] const childrenBefore = graph.getChildren(pageId).length const created = importClipboardNodes(nodeChanges, graph, pageId) @@ -1025,7 +1026,7 @@ describe('importClipboardNodes', () => { size: { x: 100, y: 100 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } - ] as any[] + ] as NodeChange[] const created = importClipboardNodes(nodeChanges, graph, pageId) const parent = graph.getNode(created[0])! @@ -1078,7 +1079,7 @@ describe('figmaNodesBounds', () => { size: { x: 50, y: 50 }, transform: { m00: 1, m01: 0, m02: 800, m10: 0, m11: 1, m12: 400 } } - ] as any[] + ] as NodeChange[] const bounds = figmaNodesBounds(nodes) expect(bounds).toEqual({ x: 500, y: 300, w: 350, h: 150 }) @@ -1107,7 +1108,7 @@ describe('figmaNodesBounds', () => { size: { x: 300, y: 200 }, transform: { m00: 1, m01: 0, m02: 18000, m10: 0, m11: 1, m12: 45000 } } - ] as any[] + ] as NodeChange[] const bounds = figmaNodesBounds(nodes) expect(bounds).toEqual({ x: 18000, y: 45000, w: 300, h: 200 }) @@ -1123,7 +1124,7 @@ describe('figmaNodesBounds', () => { type: 'CANVAS', name: 'Page' } - ] as any[] + ] as NodeChange[] expect(figmaNodesBounds(nodes)).toBeNull() }) }) @@ -1308,11 +1309,13 @@ describe('gold-preview.fig clipboard roundtrip', () => { const res: SceneNode[] = [] for (const n of nodes) { res.push(n) - if ((n as any).children) res.push(...flattenClipboard((n as any).children)) + if (n.children) res.push(...flattenClipboard(n.children)) } return res } - const pastedAll = flattenClipboard(parsed!.nodes as any) + const pastedAll = flattenClipboard( + parsed!.nodes as Array + ) expect(pastedAll.length).toBe(origAll.length) diff --git a/tests/engine/external-link.test.ts b/tests/engine/external-link.test.ts index e9f04993c..c2d8b3cad 100644 --- a/tests/engine/external-link.test.ts +++ b/tests/engine/external-link.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect, vi, beforeEach, afterEach } from 'bun:test' import { openExternalLink } from '@/app/shell/ui' + import { clearTauriMocks, mockTauriIPC } from '../helpers/tauri-mocks' /** @@ -18,8 +19,7 @@ describe('openExternalLink', () => { afterEach(async () => { await clearTauriMocks() - // @ts-expect-error test cleanup - delete globalThis.window + Reflect.deleteProperty(globalThis, 'window') vi.restoreAllMocks() }) diff --git a/tests/engine/fig-export-variables.test.ts b/tests/engine/fig-export-variables.test.ts index c84ee4477..4c5d103cf 100644 --- a/tests/engine/fig-export-variables.test.ts +++ b/tests/engine/fig-export-variables.test.ts @@ -2,6 +2,8 @@ import { describe, test, expect, beforeAll } from 'bun:test' import { exportFigFile, parseFigFile, initCodec, SceneGraph } from '@open-pencil/core' +import type { Color } from '@open-pencil/core' + beforeAll(async () => { await initCodec() }) @@ -11,7 +13,7 @@ describe('COLOR variable alpha handling', () => { const graph = new SceneGraph() const col = graph.createCollection('Colors') // Simulate a COLOR variable value missing the alpha field - graph.createVariable('brand', 'COLOR', col.id, { r: 0.2, g: 0.4, b: 0.8 } as any) + graph.createVariable('brand', 'COLOR', col.id, { r: 0.2, g: 0.4, b: 0.8 } as Color) const exported = await exportFigFile(graph) const reimported = await parseFigFile(exported.buffer as ArrayBuffer) diff --git a/tests/engine/mcp-stdio.test.ts b/tests/engine/mcp-stdio.test.ts index aa584c51c..e28bc7c5d 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 type { Client } from '@modelcontextprotocol/sdk/client/index.js' +import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import type { AddressInfo } from 'node:net' function createMockApp() { @@ -109,9 +111,8 @@ async function createStdioClient(wsPort: number) { describe('MCP stdio transport', () => { let app: ReturnType - // eslint-disable-next-line typescript-eslint/no-explicit-any -- types inferred from dynamic import - let client: any - let transport: any + let client: Client + let transport: StdioClientTransport beforeEach(async () => { app = createMockApp() diff --git a/tests/engine/renderer-effects.test.ts b/tests/engine/renderer-effects.test.ts index 2402b8fd3..7eb224a08 100644 --- a/tests/engine/renderer-effects.test.ts +++ b/tests/engine/renderer-effects.test.ts @@ -5,6 +5,11 @@ import { renderEffects } from '#core/canvas/shadows' import type { SkiaRenderer } from '#core/canvas/renderer' import type { SceneNode, SceneGraph } from '#core/scene-graph' +import type { Canvas, Path } from 'canvaskit-wasm' + +function mockCalls(fn: ReturnType): unknown[][] { + return (fn as { mock: { calls: unknown[][] } }).mock.calls +} function createMockRenderer(overrides: Partial = {}): SkiaRenderer { return { @@ -113,7 +118,7 @@ function createMockRenderer(overrides: Partial = {}): SkiaRenderer renderShape: mock(() => {}), renderSection: mock(() => {}), renderComponentSet: mock(() => {}), - renderEffects: mock((...args) => renderEffects(overrides as any, ...args)), + renderEffects: mock((...args) => renderEffects(overrides as SkiaRenderer, ...args)), drawNodeFill: mock(() => {}), drawNodeStroke: mock(() => {}), drawStrokeWithAlign: mock(() => {}), @@ -183,7 +188,7 @@ describe('Renderer effect ordering (Behavioral)', () => { callOrder.push('drawNodeFill') }) - renderShapeUncached(r, canvas as any, node as SceneNode, graph as any) + renderShapeUncached(r, canvas as Canvas, node as SceneNode, graph as SceneGraph) expect(callOrder).toEqual(['renderEffects:behind', 'drawNodeFill', 'renderEffects:front']) }) @@ -223,7 +228,7 @@ describe('Renderer effect ordering (Behavioral)', () => { callOrder.push('drawStrokeWithAlign') }) - renderShapeUncached(r, canvas as any, node as SceneNode, graph as any) + renderShapeUncached(r, canvas as Canvas, node as SceneNode, graph as SceneGraph) // Strokes are rendered between behind and front effects const strokeIdx = callOrder.indexOf('drawStrokeWithAlign') @@ -256,7 +261,7 @@ describe('Renderer handles all effect types (Behavioral)', () => { } ] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind') expect(canvas.drawRect).toHaveBeenCalled() }) @@ -280,7 +285,14 @@ describe('Renderer handles all effect types (Behavioral)', () => { } ] } - renderEffects(r, canvas as any, node as any, new Float32Array([0, 0, 100, 100]), false, 'front') + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array([0, 0, 100, 100]), + false, + 'front' + ) expect(canvas.drawPath).toHaveBeenCalled() }) @@ -292,7 +304,7 @@ describe('Renderer handles all effect types (Behavioral)', () => { childIds: [], effects: [{ type: 'BACKGROUND_BLUR', visible: true, radius: 10 }] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind') expect(r.applyClippedBlur).toHaveBeenCalled() }) @@ -314,7 +326,7 @@ describe('Renderer handles all effect types (Behavioral)', () => { const graph: Partial = { getNode: mock(() => node as SceneNode) } - renderNode(r, canvas as any, graph as any, 'n1', {}) + renderNode(r, canvas as Canvas, graph as SceneGraph, 'n1', {}) expect(r.getCachedBlur).toHaveBeenCalledWith(5) expect(canvas.saveLayer).toHaveBeenCalled() }) @@ -337,7 +349,7 @@ describe('Renderer handles all effect types (Behavioral)', () => { const graph: Partial = { getNode: mock(() => node as SceneNode) } - renderNode(r, canvas as any, graph as any, 'n1', {}) + renderNode(r, canvas as Canvas, graph as SceneGraph, 'n1', {}) expect(r.getCachedBlur).toHaveBeenCalledWith(10) expect(canvas.saveLayer).toHaveBeenCalled() }) @@ -366,7 +378,7 @@ describe('Shadow spread support (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, false, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'behind') expect(canvas.drawRect).toHaveBeenCalled() expect(r.ltrb).toHaveBeenCalledWith(-4, -4, 104, 104) @@ -394,7 +406,7 @@ describe('Shadow spread support (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, true, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, true, 'behind') expect(r.makeRRectWithSpread).toHaveBeenCalledWith(node, 4) expect(canvas.drawRRect).toHaveBeenCalled() @@ -422,7 +434,7 @@ describe('Shadow spread support (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, false, 'front') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front') expect(r.ck.LTRBRect).toHaveBeenCalledWith(9, 9, 101, 101) expect(canvas.drawPath).toHaveBeenCalled() @@ -452,7 +464,7 @@ describe('Text shadow renders on glyphs, not bounding box (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, false, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'behind') expect(r.getCachedDropShadow).toHaveBeenCalled() expect(canvas.saveLayer).toHaveBeenCalled() @@ -481,7 +493,7 @@ describe('Text shadow renders on glyphs, not bounding box (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, false, 'front') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front') // 4-layer saveLayer stack: Master, SrcIn/Tint, Blur, DstOut/Punch expect(canvas.saveLayer).toHaveBeenCalledTimes(4) @@ -518,7 +530,7 @@ describe('Text shadow renders on glyphs, not bounding box (Behavioral)', () => { } const rect = new Float32Array([0, 0, 100, 100]) - renderEffects(r, canvas as any, node as any, rect, false, 'front') + renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front') const calls = r.effectLayerPaint.setColorFilter.mock.calls // The final call must be null (exit guard cleans up) @@ -556,11 +568,11 @@ describe('Edge cases and bug fixes', () => { } ], strokes: [{ visible: true, weight: 2, opacity: 1 }], - strokeGeometry: [{} as any] + strokeGeometry: [{} as Path] } - r.getStrokeGeometry = mock(() => [{} as any]) + r.getStrokeGeometry = mock(() => [{} as Path]) - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind') + renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind') expect(r.getStrokeGeometry).toHaveBeenCalledWith(node) expect(canvas.drawPath).toHaveBeenCalled() @@ -597,12 +609,20 @@ describe('Edge cases and bug fixes', () => { childIds: ['child1'] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind', child as any) + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array(4), + false, + 'behind', + child as SceneNode + ) // Verify order: translate (offset) -> rotate -> translate (flip) -> scale - const translateCalls = (canvas.translate as any).mock.calls - const rotateCalls = (canvas.rotate as any).mock.calls - const scaleCalls = (canvas.scale as any).mock.calls + const translateCalls = mockCalls(canvas.translate) + const rotateCalls = mockCalls(canvas.rotate) + const scaleCalls = mockCalls(canvas.scale) expect(translateCalls[0]).toEqual([5 + 10, 5 + 20]) expect(rotateCalls[0]).toEqual([45, 25, 30]) @@ -641,7 +661,15 @@ describe('Edge cases and bug fixes', () => { childIds: ['child1'] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind', child as any) + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array(4), + false, + 'behind', + child as SceneNode + ) expect(r.getCachedDropShadow).toHaveBeenCalled() expect(canvas.saveLayer).toHaveBeenCalled() @@ -680,11 +708,19 @@ describe('Edge cases and bug fixes', () => { childIds: ['child1'] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'front', child as any) + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array(4), + false, + 'front', + child as SceneNode + ) - const translateCalls = (canvas.translate as any).mock.calls - const rotateCalls = (canvas.rotate as any).mock.calls - const scaleCalls = (canvas.scale as any).mock.calls + const translateCalls = mockCalls(canvas.translate) + const rotateCalls = mockCalls(canvas.rotate) + const scaleCalls = mockCalls(canvas.scale) expect(translateCalls[0]).toEqual([10, 20]) expect(rotateCalls[0]).toEqual([45, 25, 30]) @@ -725,9 +761,17 @@ describe('Edge cases and bug fixes', () => { childIds: ['child1'] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'behind', child as any) + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array(4), + false, + 'behind', + child as SceneNode + ) - const translateCalls = (canvas.translate as any).mock.calls + const translateCalls = mockCalls(canvas.translate) expect(translateCalls[0]).toEqual([15, 25]) // offset + child position // The filter should NOT have the offset (neutralized to 0,0) @@ -754,7 +798,14 @@ describe('Edge cases and bug fixes', () => { ] } - renderEffects(r, canvas as any, node as any, new Float32Array([0, 0, 100, 100]), false, 'front') + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array([0, 0, 100, 100]), + false, + 'front' + ) // Expect LTRBRect for 'big' to encompass both the shape (0 to 100) and the offset hole (200 to 300) // with expand (20) padding: min(-20, -20+200) = -20, max(100+20, 100+20+200) = 320 @@ -788,7 +839,14 @@ describe('Edge cases and bug fixes', () => { ] } - renderEffects(r, canvas as any, node as any, new Float32Array([0, 0, 100, 100]), true, 'front') + renderEffects( + r, + canvas as Canvas, + node as SceneNode, + new Float32Array([0, 0, 100, 100]), + true, + 'front' + ) // makeRRectWithOffset should receive (node, localOffsetX, localOffsetY, spread) // localOffsetX = 5, localOffsetY = 5, spread = 4 @@ -818,7 +876,7 @@ describe('INNER_SHADOW bug proofs', () => { ] } - renderEffects(r, canvas as any, node as any, new Float32Array(4), false, 'front') + renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'front') // PROOF: ColorFilter.MakeBlend(black, SrcIn) on DstOut layer paint // forces renderText output to solid black without mutating fillPaint. diff --git a/tests/engine/tauri-document-io.test.ts b/tests/engine/tauri-document-io.test.ts index 16c167f7d..9dd290997 100644 --- a/tests/engine/tauri-document-io.test.ts +++ b/tests/engine/tauri-document-io.test.ts @@ -1,14 +1,14 @@ import { afterEach, describe, expect, test } from 'bun:test' -import { createDocumentWriter } from '@/app/document/io/write' import { readReloadSource } from '@/app/document/io/reload-source' import { chooseTauriFigSavePath } from '@/app/document/io/save-targets' +import { createDocumentWriter } from '@/app/document/io/write' + import { clearTauriMocks, mockTauriIPC } from '../helpers/tauri-mocks' afterEach(async () => { await clearTauriMocks() - // @ts-expect-error test cleanup - delete globalThis.window + Reflect.deleteProperty(globalThis, 'window') }) describe('Tauri document IO helpers', () => { diff --git a/tests/engine/tauri-file-actions.test.ts b/tests/engine/tauri-file-actions.test.ts index b84fa8690..d5c9df618 100644 --- a/tests/engine/tauri-file-actions.test.ts +++ b/tests/engine/tauri-file-actions.test.ts @@ -3,12 +3,12 @@ import { afterEach, describe, expect, test } from 'bun:test' import { saveExportedFile } from '@/app/document/export/files' import { watchTauriFile } from '@/app/document/io/watch-targets' import { chooseTauriOpenPath, readTauriDesignFile } from '@/app/shell/menu/files' + import { clearTauriMocks, mockTauriIPC } from '../helpers/tauri-mocks' afterEach(async () => { await clearTauriMocks() - // @ts-expect-error test cleanup - delete globalThis.window + Reflect.deleteProperty(globalThis, 'window') }) describe('Tauri file actions', () => { diff --git a/tests/engine/tauri-fonts.test.ts b/tests/engine/tauri-fonts.test.ts index 0bdc49978..affc7f00a 100644 --- a/tests/engine/tauri-fonts.test.ts +++ b/tests/engine/tauri-fonts.test.ts @@ -34,10 +34,8 @@ function installFontFaceMocks() { afterEach(async () => { await clearTauriMocks() vi.restoreAllMocks() - // @ts-expect-error test cleanup - delete globalThis.document - // @ts-expect-error test cleanup - delete globalThis.FontFace + Reflect.deleteProperty(globalThis, 'document') + Reflect.deleteProperty(globalThis, 'FontFace') }) describe('Tauri font helpers', () => { @@ -50,7 +48,9 @@ describe('Tauri font helpers', () => { const { listFamilies, listFonts } = await import('@/app/editor/fonts') await expect(listFamilies()).resolves.toEqual(['System UI']) - await expect(listFonts()).resolves.toEqual([{ family: 'System UI', styles: ['Regular', 'Bold'] }]) + await expect(listFonts()).resolves.toEqual([ + { family: 'System UI', styles: ['Regular', 'Bold'] } + ]) }) test('loads system font bytes and registers the face', async () => { diff --git a/tests/engine/tauri-processes.test.ts b/tests/engine/tauri-processes.test.ts index d49375c08..eda46029d 100644 --- a/tests/engine/tauri-processes.test.ts +++ b/tests/engine/tauri-processes.test.ts @@ -1,15 +1,16 @@ import { afterEach, describe, expect, test, vi } from 'bun:test' + import { ref } from 'vue' import { spawnAcpProcess } from '@/app/ai/acp/process' import { checkForAppUpdate } from '@/app/shell/updater' + import { clearTauriMocks, mockTauriIPC } from '../helpers/tauri-mocks' afterEach(async () => { await clearTauriMocks() vi.restoreAllMocks() - // @ts-expect-error test cleanup - delete globalThis.window + Reflect.deleteProperty(globalThis, 'window') }) describe('Tauri process helpers', () => { diff --git a/tests/engine/tauri-remaining-integrations.test.ts b/tests/engine/tauri-remaining-integrations.test.ts index ecada7585..98bf76fff 100644 --- a/tests/engine/tauri-remaining-integrations.test.ts +++ b/tests/engine/tauri-remaining-integrations.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, test, vi } from 'bun:test' import { createACPTransport } from '@/app/ai/chat/transports' import { ensureTauriParentDirectory } from '@/app/automation/bridge/file-handlers' import { spawnMCPIfNeeded } from '@/app/automation/mcp/spawn' + import { clearTauriMocks, installTauriMockWindow, mockTauriIPC } from '../helpers/tauri-mocks' import type { ACPChatTransport } from '@/app/ai/acp/transport' @@ -10,12 +11,9 @@ import type { ACPChatTransport } from '@/app/ai/acp/transport' afterEach(async () => { await clearTauriMocks() vi.restoreAllMocks() - // @ts-expect-error test cleanup - delete globalThis.window - // @ts-expect-error test cleanup - delete globalThis.navigator - // @ts-expect-error test cleanup - delete globalThis.location + Reflect.deleteProperty(globalThis, 'window') + Reflect.deleteProperty(globalThis, 'navigator') + Reflect.deleteProperty(globalThis, 'location') }) describe('remaining Tauri integrations', () => { @@ -26,7 +24,9 @@ describe('remaining Tauri integrations', () => { return '/Users/tester' }) - const transport = (await createACPTransport('acp:claude-code')) as ACPChatTransport & { cwd: string } + const transport = (await createACPTransport('acp:claude-code')) as ACPChatTransport & { + cwd: string + } expect(transport.cwd).toBe('/Users/tester') }) diff --git a/tests/engine/text-edit-undo.test.ts b/tests/engine/text-edit-undo.test.ts index eb70ba9c7..f03ffcac7 100644 --- a/tests/engine/text-edit-undo.test.ts +++ b/tests/engine/text-edit-undo.test.ts @@ -5,12 +5,13 @@ import { createTextActions } from '@open-pencil/core/editor' import type { StyleRun } from '@open-pencil/core' import type { EditorContext, EditorState } from '@open-pencil/core/editor' +import type { CanvasKit } from 'canvaskit-wasm' function setup() { const graph = new SceneGraph() const pageId = graph.getPages()[0].id const undo = new UndoManager() - const textEditor = new TextEditor({} as any) + const textEditor = new TextEditor({} as CanvasKit) const state = { editingTextId: null, diff --git a/tests/engine/text-editor.test.ts b/tests/engine/text-editor.test.ts index 587df0d00..1da9fc91c 100644 --- a/tests/engine/text-editor.test.ts +++ b/tests/engine/text-editor.test.ts @@ -2,7 +2,9 @@ import { describe, test, expect } from 'bun:test' import { TextEditor, type SceneNode } from '@open-pencil/core' -const mockCk = {} as any +import type { CanvasKit } from 'canvaskit-wasm' + +const mockCk = {} as CanvasKit function createEditor(text = 'Hello World') { const editor = new TextEditor(mockCk) diff --git a/tests/engine/tools-ai-adapter.test.ts b/tests/engine/tools-ai-adapter.test.ts index 3a0846489..c5687e98f 100644 --- a/tests/engine/tools-ai-adapter.test.ts +++ b/tests/engine/tools-ai-adapter.test.ts @@ -6,6 +6,12 @@ import * as v from 'valibot' import { ALL_TOOLS, FigmaAPI, SceneGraph, toolsToAI } from '@open-pencil/core' +type AdapterTool = { execute(args: Record): Promise; description: string } + +function adapterTool(tools: Record, name: string): AdapterTool { + return tools[name] as AdapterTool +} + function setup() { const graph = new SceneGraph() const figma = new FigmaAPI(graph) @@ -34,7 +40,7 @@ describe('AI adapter', () => { test('each tool has description and execute', () => { const { tools } = setup() for (const [name, t] of Object.entries(tools)) { - const aiTool = t as { description: string; execute: Function } + const aiTool = t as AdapterTool expect(aiTool.description).toBeTruthy() expect(typeof aiTool.execute).toBe('function') } @@ -42,7 +48,7 @@ describe('AI adapter', () => { test('create_shape tool works through adapter', async () => { const { tools, figma } = setup() - const createShape = tools.create_shape as { execute: Function } + const createShape = adapterTool(tools, 'create_shape') const result = (await createShape.execute({ type: 'RECTANGLE', x: 10, @@ -50,7 +56,7 @@ describe('AI adapter', () => { width: 100, height: 50, name: 'Test Rect' - })) as any + })) as { id: string; type: string; name: string } expect(result.id).toBeTruthy() expect(result.type).toBe('RECTANGLE') @@ -67,7 +73,7 @@ describe('AI adapter', () => { const rect = figma.createRectangle() rect.resize(100, 100) - const setFill = tools.set_fill as { execute: Function } + const setFill = adapterTool(tools, 'set_fill') await setFill.execute({ id: rect.id, color: '#00ff00' }) const fills = figma.getNodeById(rect.id)!.fills @@ -84,8 +90,8 @@ describe('AI adapter', () => { rect.resize(50, 50) frame.appendChild(rect) - const getTree = tools.get_page_tree as { execute: Function } - const result = (await getTree.execute({})) as any + const getTree = adapterTool(tools, 'get_page_tree') + const result = (await getTree.execute({})) as { page: unknown; children: unknown[] } expect(result.page).toBeTruthy() expect(result.children.length).toBeGreaterThan(0) }) @@ -105,7 +111,7 @@ describe('AI adapter', () => { { v, valibotSchema, tool } ) - const listPages = tools.list_pages as { execute: Function } + const listPages = adapterTool(tools, 'list_pages') await listPages.execute({}) expect(calls).toEqual(['before', 'after']) @@ -127,7 +133,7 @@ describe('AI adapter', () => { { v, valibotSchema, tool } ) - const evalTool = tools.eval as { execute: Function } + const evalTool = adapterTool(tools, 'eval') try { await evalTool.execute({ code: 'throw new Error("test")' }) } catch { @@ -143,8 +149,8 @@ describe('AI adapter', () => { figma.createText().name = 'Label' figma.createRectangle().name = 'Button Secondary' - const findNodes = tools.find_nodes as { execute: Function } - const result = (await findNodes.execute({ name: 'button' })) as any + const findNodes = adapterTool(tools, 'find_nodes') + const result = (await findNodes.execute({ name: 'button' })) as { count: number } expect(result.count).toBe(2) }) @@ -153,7 +159,7 @@ describe('AI adapter', () => { const frame = figma.createFrame() frame.resize(300, 200) - const setLayout = tools.set_layout as { execute: Function } + const setLayout = adapterTool(tools, 'set_layout') await setLayout.execute({ id: frame.id, direction: 'HORIZONTAL', @@ -169,10 +175,10 @@ describe('AI adapter', () => { test('render JSX works through adapter', async () => { const { tools } = setup() - const render = tools.render as { execute: Function } + const render = adapterTool(tools, 'render') const result = (await render.execute({ jsx: 'Hello' - })) as any + })) as { name: string; type: string } expect(result.name).toBe('Card') expect(result.type).toBe('FRAME') }) @@ -182,11 +188,11 @@ describe('AI adapter', () => { const rect = figma.createRectangle() const id = rect.id - const deleteTool = tools.delete_node as { execute: Function } + const deleteTool = adapterTool(tools, 'delete_node') await deleteTool.execute({ id }) - const getNode = tools.get_node as { execute: Function } - const result = (await getNode.execute({ id })) as any + const getNode = adapterTool(tools, 'get_node') + const result = (await getNode.execute({ id })) as { error: string } expect(result.error).toContain('not found') }) }) diff --git a/tests/engine/tools-cli.test.ts b/tests/engine/tools-cli.test.ts index 946728dc8..8c335d6e9 100644 --- a/tests/engine/tools-cli.test.ts +++ b/tests/engine/tools-cli.test.ts @@ -23,8 +23,8 @@ async function evalCode( return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode } } -function parseJSON(stdout: string): unknown { - return JSON.parse(stdout) +function parseJSON(stdout: string): T { + return JSON.parse(stdout) as T } heavy('CLI tool operations via eval', () => { @@ -38,7 +38,7 @@ heavy('CLI tool operations via eval', () => { return r.toJSON() `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.name).toBe('TestRect') expect(result.x).toBe(100) expect(result.y).toBe(200) @@ -54,7 +54,7 @@ heavy('CLI tool operations via eval', () => { return { fills: r.fills } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.fills.length).toBe(1) expect(result.fills[0].color.r).toBe(1) }) @@ -76,7 +76,7 @@ heavy('CLI tool operations via eval', () => { } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.layoutMode).toBe('VERTICAL') expect(result.itemSpacing).toBe(16) expect(result.paddingLeft).toBe(20) @@ -91,7 +91,7 @@ heavy('CLI tool operations via eval', () => { return { name: comp.name, type: comp.type } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.name).toBe('Button') expect(result.type).toBe('COMPONENT') }) @@ -110,7 +110,7 @@ heavy('CLI tool operations via eval', () => { return { groupType, childCount, ungroupedExists: ungrouped !== null } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.groupType).toBe('GROUP') expect(result.childCount).toBe(2) expect(result.ungroupedExists).toBe(false) @@ -122,7 +122,7 @@ heavy('CLI tool operations via eval', () => { return { count: texts.length, hasTexts: texts.length > 0 } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.hasTexts).toBe(true) expect(result.count).toBeGreaterThan(0) }) @@ -140,7 +140,7 @@ heavy('CLI tool operations via eval', () => { } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.same).toBe(false) expect(result.cloneName).toBe('Original') expect(result.cloneWidth).toBe(100) @@ -159,7 +159,7 @@ heavy('CLI tool operations via eval', () => { } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.isChild).toBe(true) }) @@ -171,7 +171,7 @@ heavy('CLI tool operations via eval', () => { return r.constraints `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.horizontal).toBe('CENTER') expect(result.vertical).toBe('STRETCH') }) @@ -191,7 +191,7 @@ heavy('CLI tool operations via eval', () => { return { count: f.effects.length, type: f.effects[0].type } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.count).toBe(1) expect(result.type).toBe('DROP_SHADOW') }) @@ -203,7 +203,7 @@ heavy('CLI tool operations via eval', () => { return { variables: vars.length, collections: cols.length } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(typeof result.variables).toBe('number') expect(typeof result.collections).toBe('number') }) @@ -216,7 +216,7 @@ heavy('CLI tool operations via eval', () => { return { page: figma.currentPage.name, pageCount: pages.length } `) expect(exitCode).toBe(0) - const result = parseJSON(stdout) as any + const result = parseJSON>(stdout) expect(result.page).toBeTruthy() expect(result.pageCount).toBeGreaterThanOrEqual(1) }) diff --git a/tests/engine/tools.test.ts b/tests/engine/tools.test.ts index d63ce3573..857df9692 100644 --- a/tests/engine/tools.test.ts +++ b/tests/engine/tools.test.ts @@ -2,6 +2,16 @@ import { describe, expect, test } from 'bun:test' import { ALL_TOOLS, FigmaAPI, SceneGraph, computeAllLayouts } from '@open-pencil/core' +interface ToolResult { + id: string + name?: string + type?: string + error?: string + count?: number + nodes?: Array<{ id?: string; name: string; type: string }> + [key: string]: unknown +} + function setup() { const graph = new SceneGraph() const figma = new FigmaAPI(graph) @@ -44,7 +54,7 @@ describe('create_shape', () => { width: 300, height: 400, name: 'Test Frame' - }) as any + }) as ToolResult expect(result.name).toBe('Test Frame') expect(result.type).toBe('FRAME') @@ -65,7 +75,7 @@ describe('create_shape', () => { width: 500, height: 500, name: 'Parent' - }) as any + }) as ToolResult const child = tool.execute(figma, { type: 'RECTANGLE', x: 10, @@ -73,7 +83,7 @@ describe('create_shape', () => { width: 50, height: 50, parent_id: parent.id - }) as any + }) as ToolResult const parentNode = figma.getNodeById(parent.id)! expect(parentNode.children.some((c) => c.id === child.id)).toBe(true) @@ -99,7 +109,7 @@ describe('set_fill', () => { test('returns error for missing node', () => { const { figma } = setup() const tool = ALL_TOOLS.find((t) => t.name === 'set_fill')! - const result = tool.execute(figma, { id: 'nonexistent', color: '#ff0000' }) as any + const result = tool.execute(figma, { id: 'nonexistent', color: '#ff0000' }) as ToolResult expect(result.error).toContain('not found') }) }) @@ -170,7 +180,7 @@ describe('update_node', () => { width: 200, height: 150, opacity: 0.5 - }) as any + }) as ToolResult expect(result.updated).toContain('x') expect(result.updated).toContain('size') @@ -299,7 +309,7 @@ describe('clone_node', () => { rect.resize(100, 100) const tool = ALL_TOOLS.find((t) => t.name === 'clone_node')! - const result = tool.execute(figma, { id: rect.id }) as any + const result = tool.execute(figma, { id: rect.id }) as ToolResult expect(result.id).not.toBe(rect.id) expect(result.name).toBe('Original') @@ -342,7 +352,7 @@ describe('group_nodes', () => { r2.resize(50, 50) const tool = ALL_TOOLS.find((t) => t.name === 'group_nodes')! - const result = tool.execute(figma, { ids: [r1.id, r2.id] }) as any + const result = tool.execute(figma, { ids: [r1.id, r2.id] }) as ToolResult expect(result.type).toBe('GROUP') const group = figma.getNodeById(result.id)! @@ -359,7 +369,7 @@ describe('find_nodes', () => { text.name = 'Label' const tool = ALL_TOOLS.find((t) => t.name === 'find_nodes')! - const result = tool.execute(figma, { name: 'button' }) as any + const result = tool.execute(figma, { name: 'button' }) as ToolResult expect(result.count).toBe(1) expect(result.nodes[0].name).toBe('Button Primary') }) @@ -371,7 +381,7 @@ describe('find_nodes', () => { figma.createText() const tool = ALL_TOOLS.find((t) => t.name === 'find_nodes')! - const result = tool.execute(figma, { type: 'RECTANGLE' }) as any + const result = tool.execute(figma, { type: 'RECTANGLE' }) as ToolResult expect(result.count).toBe(2) }) }) @@ -388,9 +398,9 @@ describe('query_nodes', () => { figma.createRectangle() const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! - const result = (await tool.execute(figma, { selector: '//FRAME' })) as any + const result = (await tool.execute(figma, { selector: '//FRAME' })) as ToolResult expect(result.count).toBe(2) - expect(result.nodes.every((n: any) => n.type === 'FRAME')).toBe(true) + expect(result.nodes?.every((n) => n.type === 'FRAME')).toBe(true) }) test('finds by attribute //RECTANGLE[@width < 200]', async () => { @@ -403,7 +413,9 @@ describe('query_nodes', () => { big.name = 'Big' const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! - const result = (await tool.execute(figma, { selector: '//RECTANGLE[@width < 200]' })) as any + const result = (await tool.execute(figma, { + selector: '//RECTANGLE[@width < 200]' + })) as ToolResult expect(result.count).toBe(1) expect(result.nodes[0].name).toBe('Small') }) @@ -420,15 +432,15 @@ describe('query_nodes', () => { const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! const result = (await tool.execute(figma, { selector: '//TEXT[contains(@name, "Label")]' - })) as any + })) as ToolResult expect(result.count).toBe(2) - expect(result.nodes.every((n: any) => n.name.includes('Label'))).toBe(true) + expect(result.nodes?.every((n) => n.name.includes('Label'))).toBe(true) }) test('returns error for invalid xpath', async () => { const { figma } = setup() const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! - const result = (await tool.execute(figma, { selector: '///invalid[[[[' })) as any + const result = (await tool.execute(figma, { selector: '///invalid[[[[' })) as ToolResult expect(result.error).toBeTruthy() expect(result.error).toContain('XPath error') }) @@ -441,7 +453,7 @@ describe('query_nodes', () => { } const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! - const result = (await tool.execute(figma, { selector: '//RECTANGLE', limit: 3 })) as any + const result = (await tool.execute(figma, { selector: '//RECTANGLE', limit: 3 })) as ToolResult expect(result.count).toBe(3) }) @@ -450,7 +462,7 @@ describe('query_nodes', () => { figma.createRectangle() const tool = ALL_TOOLS.find((t) => t.name === 'query_nodes')! - const result = (await tool.execute(figma, { selector: '//ELLIPSE' })) as any + const result = (await tool.execute(figma, { selector: '//ELLIPSE' })) as ToolResult expect(result.count).toBe(0) expect(result.nodes).toEqual([]) }) @@ -464,7 +476,7 @@ describe('get_node', () => { rect.resize(100, 50) const tool = ALL_TOOLS.find((t) => t.name === 'get_node')! - const result = tool.execute(figma, { id: rect.id }) as any + const result = tool.execute(figma, { id: rect.id }) as ToolResult expect(result.name).toBe('Test Rect') expect(result.width).toBe(100) expect(result.height).toBe(50) @@ -475,7 +487,7 @@ describe('page tools', () => { test('list_pages returns pages', () => { const { figma } = setup() const tool = ALL_TOOLS.find((t) => t.name === 'list_pages')! - const result = tool.execute(figma, {}) as any + const result = tool.execute(figma, {}) as ToolResult expect(result.pages.length).toBeGreaterThanOrEqual(1) }) @@ -555,7 +567,7 @@ describe('set_font_range', () => { y: 0, width: 200, height: 20 - }) as any + }) as ToolResult setText.execute(figma, { id: created.id, text: 'Hello World' }) setFontRange.execute(figma, { id: created.id, @@ -591,7 +603,7 @@ describe('set_font_range', () => { y: 0, width: 200, height: 20 - }) as any + }) as ToolResult setText.execute(figma, { id: created.id, text: 'Red text' }) setFontRange.execute(figma, { id: created.id, start: 0, end: 3, color: '#ff0000' }) @@ -608,7 +620,7 @@ describe('render', () => { const tool = ALL_TOOLS.find((t) => t.name === 'render')! const result = (await tool.execute(figma, { jsx: 'Hello' - })) as any + })) as ToolResult expect(result.name).toBe('Card') expect(result.type).toBe('FRAME') expect(result.children.length).toBeGreaterThan(0) diff --git a/tests/engine/verify-silhouette-autopsy.test.ts b/tests/engine/verify-silhouette-autopsy.test.ts index 9537ccbe5..c1f15a04d 100644 --- a/tests/engine/verify-silhouette-autopsy.test.ts +++ b/tests/engine/verify-silhouette-autopsy.test.ts @@ -383,7 +383,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { spread: 0 } ] - } as any) + }) // Verify the node was created with effects expect(node.effects).toHaveLength(1) @@ -394,11 +394,11 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { const origRenderEffects = renderer.renderEffects.bind(renderer) const origDrawNodeFill = renderer.drawNodeFill.bind(renderer) - renderer.renderEffects = (...args: any[]) => { + renderer.renderEffects = (...args: Parameters) => { callOrder.push(`effects:${args[4]}`) origRenderEffects(...args) } - renderer.drawNodeFill = (...args: any[]) => { + renderer.drawNodeFill = (...args: Parameters) => { callOrder.push('fill') origDrawNodeFill(...args) } @@ -431,7 +431,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { color: { r: 0, g: 0, b: 0, a: 1 } } ] - } as any + } const overflow = renderer.effectOverflow(node) // radius 10 + offset 5 = 15 margin needed @@ -463,14 +463,14 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { spread: 0 } ] - } as any) + }) expect(textNode.effects).toHaveLength(1) expect(textNode.effects[0].type).toBe('INNER_SHADOW') // Verify the node exists and renderEffects('front') will be called // The actual rendering is verified by the behavioral test - const effectTypes = textNode.effects.map((e: any) => e.type) + const effectTypes = textNode.effects.map((e) => e.type) expect(effectTypes).toContain('INNER_SHADOW') }) @@ -494,7 +494,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { spread: 0 } ] - } as any) + }) const child = graph.createNode('RECTANGLE', parentWithFill.id, { x: 25, @@ -502,12 +502,12 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => { width: 50, height: 50, fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, visible: true, opacity: 1 }] - } as any) + }) // renderEffects should use the parent, not the child // We verify this by checking that the canvas translation matches // the node's own coordinates (shadowShapeChild would be null) - expect(parentWithFill.fills.some((f: any) => f.visible)).toBe(true) + expect(parentWithFill.fills.some((f) => f.visible)).toBe(true) // renderShapeUncached passes getShadowShapeChild result to renderEffects // which returns null when fills are visible → confirms claim }) diff --git a/tests/engine/visual-inner-shadow-counter.ts b/tests/engine/visual-inner-shadow-counter.ts index a93e0f220..4b41c165a 100644 --- a/tests/engine/visual-inner-shadow-counter.ts +++ b/tests/engine/visual-inner-shadow-counter.ts @@ -6,6 +6,8 @@ import { SkiaRenderer } from '#core/canvas' import { SceneGraph } from '#core/scene-graph' import { fontManager } from '#core/text' +import type { SceneNode } from '#core/scene-graph' + async function main() { const ck = await initCanvasKit() @@ -50,9 +52,9 @@ async function main() { spread: 0 } ] - } + } satisfies Partial - const textNode = graph.createNode('TEXT', pageId, textProps as any) + const textNode = graph.createNode('TEXT', pageId, textProps) const nodeId = textNode.id const surface = ck.MakeSurface(width, height)! diff --git a/tests/engine/visual-sdf-test-matrix.ts b/tests/engine/visual-sdf-test-matrix.ts index 3b2ffc8f1..ff7dfdfe8 100644 --- a/tests/engine/visual-sdf-test-matrix.ts +++ b/tests/engine/visual-sdf-test-matrix.ts @@ -7,6 +7,8 @@ import { renderNodesToImage } from '#core/io/formats/raster' import { SceneGraph } from '#core/scene-graph' import { fontManager } from '#core/text' +import type { SceneNode } from '#core/scene-graph' + interface TestCase { text: string fontSize: number @@ -194,9 +196,9 @@ async function main() { spread: tc.spread } ] - } + } satisfies Partial - const textNode = graph.createNode('TEXT', pageId, textProps as any) + const textNode = graph.createNode('TEXT', pageId, textProps) const surfW = Math.ceil(textProps.width) + 40 const surfH = Math.ceil(textProps.height) + 40 diff --git a/tests/engine/visual-verify-text-inner-shadow.ts b/tests/engine/visual-verify-text-inner-shadow.ts index 2254ba9cf..7d063a578 100644 --- a/tests/engine/visual-verify-text-inner-shadow.ts +++ b/tests/engine/visual-verify-text-inner-shadow.ts @@ -7,6 +7,8 @@ import { renderNodesToImage } from '#core/io/formats/raster' import { SceneGraph } from '#core/scene-graph' import { fontManager } from '#core/text' +import type { SceneNode } from '#core/scene-graph' + async function main() { const ck = await initCanvasKit() @@ -49,9 +51,9 @@ async function main() { spread: 0 } ] - } + } satisfies Partial - const textNode = graph.createNode('TEXT', pageId, textProps as any) + const textNode = graph.createNode('TEXT', pageId, textProps) const nodeId = textNode.id const surface = ck.MakeSurface(800, 300)! diff --git a/tests/helpers/store.ts b/tests/helpers/store.ts index 5c0d33dbf..67c63e7d7 100644 --- a/tests/helpers/store.ts +++ b/tests/helpers/store.ts @@ -7,7 +7,7 @@ export function getSelectedIds(page: Page) { export function getPageChildren(page: Page) { return page.evaluate(() => { const store = window.__OPEN_PENCIL_STORE__! - return store.graph.getChildren(store.state.currentPageId).map((n: any) => ({ + return store.graph.getChildren(store.state.currentPageId).map((n) => ({ id: n.id, type: n.type, x: n.x, diff --git a/vite.config.ts b/vite.config.ts index 3c84b69f0..0face9b3a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,12 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' +import process from 'node:process' + import tailwindcss from '@tailwindcss/vite' -import Icons from 'unplugin-icons/vite' +import vue from '@vitejs/plugin-vue' import IconsResolver from 'unplugin-icons/resolver' +import Icons from 'unplugin-icons/vite' import Components from 'unplugin-vue-components/vite' +import { defineConfig } from 'vite' + import { createOpenPencilAliases } from './vite/aliases' import { localAutomationToken, openPencilAutomationPlugin } from './vite/automation' import { copyCanvasKitAssetsPlugin } from './vite/canvaskit-assets' @@ -11,7 +14,6 @@ import { openPencilPwaPlugin } from './vite/pwa' import { rawMarkdownPlugin } from './vite/raw-markdown' import { createDevServerOptions } from './vite/server' -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST export default defineConfig(async ({ command }) => ({