chore(lint): remove unsafe test casts
- Enforce explicit-any and TypeScript suppression bans in tests - Remove remaining broad any casts from engine and e2e coverage - Split markdown ambient declarations from typed browser globals
This commit is contained in:
parent
aa07e833cb
commit
fae190a673
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<string, any> {
|
||||
): ToolSet {
|
||||
const { v, valibotSchema, tool } = deps
|
||||
// eslint-disable-next-line typescript-eslint/no-explicit-any -- matches return type
|
||||
const result: Record<string, any> = {}
|
||||
const result: ToolSet = {}
|
||||
|
||||
for (const def of tools) {
|
||||
const shape: Record<string, unknown> = {}
|
||||
|
|
@ -156,8 +154,7 @@ export function toolsToAI(
|
|||
|
||||
const toolOpts: Record<string, unknown> = {
|
||||
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<string, any>)),
|
||||
inputSchema: valibotSchema(v.object(shape as Record<string, never>)),
|
||||
execute: async (args: Record<string, unknown>) => {
|
||||
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
|
||||
|
|
|
|||
83
src/global.d.ts
vendored
83
src/global.d.ts
vendored
|
|
@ -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<string, string[]>
|
||||
}
|
||||
interface GestureEvent extends UIEvent {
|
||||
scale: number
|
||||
rotation: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
}
|
||||
|
||||
interface FilePickerOptions {
|
||||
types?: FilePickerAcceptType[]
|
||||
suggestedName?: string
|
||||
}
|
||||
interface FilePickerAcceptType {
|
||||
description: string
|
||||
accept: Record<string, string[]>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
showOpenFilePicker?(options?: FilePickerOptions): Promise<FileSystemFileHandle[]>
|
||||
showSaveFilePicker?(options?: FilePickerOptions): Promise<FileSystemFileHandle>
|
||||
queryLocalFonts?(): Promise<
|
||||
{
|
||||
family: string
|
||||
fullName: string
|
||||
style: string
|
||||
postscriptName: string
|
||||
blob(): Promise<Blob>
|
||||
}[]
|
||||
>
|
||||
// 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<FileSystemFileHandle[]>
|
||||
showSaveFilePicker?(options?: FilePickerOptions): Promise<FileSystemFileHandle>
|
||||
queryLocalFonts?(): Promise<
|
||||
{
|
||||
family: string
|
||||
fullName: string
|
||||
style: string
|
||||
postscriptName: string
|
||||
blob(): Promise<Blob>
|
||||
}[]
|
||||
>
|
||||
__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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
src/markdown.d.ts
vendored
Normal file
4
src/markdown.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.md' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -33,12 +33,18 @@ async function getLayerNames(): Promise<string[]> {
|
|||
return names
|
||||
}
|
||||
|
||||
async function getSceneTree() {
|
||||
interface SceneTreeNode {
|
||||
name: string
|
||||
type: string
|
||||
children: SceneTreeNode[]
|
||||
}
|
||||
|
||||
async function getSceneTree(): Promise<SceneTreeNode> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<SceneNode & { children?: SceneNode[] }>
|
||||
)
|
||||
|
||||
expect(pastedAll.length).toBe(origAll.length)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<typeof createMockApp>
|
||||
// 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()
|
||||
|
|
|
|||
|
|
@ -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<typeof mock>): unknown[][] {
|
||||
return (fn as { mock: { calls: unknown[][] } }).mock.calls
|
||||
}
|
||||
|
||||
function createMockRenderer(overrides: Partial<SkiaRenderer> = {}): SkiaRenderer {
|
||||
return {
|
||||
|
|
@ -113,7 +118,7 @@ function createMockRenderer(overrides: Partial<SkiaRenderer> = {}): 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<SceneGraph> = {
|
||||
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<SceneGraph> = {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import * as v from 'valibot'
|
|||
|
||||
import { ALL_TOOLS, FigmaAPI, SceneGraph, toolsToAI } from '@open-pencil/core'
|
||||
|
||||
type AdapterTool = { execute(args: Record<string, unknown>): Promise<unknown>; description: string }
|
||||
|
||||
function adapterTool(tools: Record<string, unknown>, 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: '<Frame name="Card" w={200} h={100}><Text>Hello</Text></Frame>'
|
||||
})) 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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<T>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(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<Record<string, unknown>>(stdout)
|
||||
expect(result.page).toBeTruthy()
|
||||
expect(result.pageCount).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: '<Frame name="Card" w={200} h={100} bg="#FFF"><Text>Hello</Text></Frame>'
|
||||
})) as any
|
||||
})) as ToolResult
|
||||
expect(result.name).toBe('Card')
|
||||
expect(result.type).toBe('FRAME')
|
||||
expect(result.children.length).toBeGreaterThan(0)
|
||||
|
|
|
|||
|
|
@ -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<typeof renderer.renderEffects>) => {
|
||||
callOrder.push(`effects:${args[4]}`)
|
||||
origRenderEffects(...args)
|
||||
}
|
||||
renderer.drawNodeFill = (...args: any[]) => {
|
||||
renderer.drawNodeFill = (...args: Parameters<typeof renderer.drawNodeFill>) => {
|
||||
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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<SceneNode>
|
||||
|
||||
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)!
|
||||
|
|
|
|||
|
|
@ -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<SceneNode>
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<SceneNode>
|
||||
|
||||
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)!
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }) => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue