- Carry paint bindings through symbol overrides and clone synchronization - Keep explicit instance strokes and resolved variable opacity intact
540 lines
19 KiB
TypeScript
540 lines
19 KiB
TypeScript
import { describe, expect, setDefaultTimeout, test } from 'bun:test'
|
|
|
|
import { importNodeChanges, parseFigFile } from '@open-pencil/core'
|
|
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
import { parseFixture } from '#tests/helpers/fig-fixtures'
|
|
|
|
setDefaultTimeout(60_000)
|
|
|
|
describe('edge cases', () => {
|
|
test('non-fig-kiwi bytes throw meaningful error', async () => {
|
|
const garbage = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
await expect(parseFigFile(garbage.buffer as ArrayBuffer)).rejects.toThrow()
|
|
})
|
|
|
|
test('invalid zip data throws', async () => {
|
|
const garbage = new Uint8Array(50).fill(0xff)
|
|
await expect(parseFigFile(garbage.buffer as ArrayBuffer)).rejects.toThrow()
|
|
})
|
|
|
|
test('REMOVED nodes are skipped', async () => {
|
|
const { importNodeChanges } = await import('#core/kiwi/fig/import')
|
|
const graph = importNodeChanges([
|
|
{
|
|
guid: { sessionID: 0, localID: 0 },
|
|
type: 'DOCUMENT',
|
|
name: 'Document',
|
|
visible: true,
|
|
opacity: 1,
|
|
phase: 'CREATED',
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page',
|
|
visible: true,
|
|
opacity: 1,
|
|
phase: 'CREATED',
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'RECTANGLE',
|
|
name: 'Visible',
|
|
visible: true,
|
|
opacity: 1,
|
|
phase: 'CREATED',
|
|
size: { x: 100, y: 100 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
|
|
type: 'RECTANGLE',
|
|
name: 'Deleted',
|
|
visible: true,
|
|
opacity: 1,
|
|
phase: 'REMOVED',
|
|
size: { x: 100, y: 100 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
}
|
|
])
|
|
const children = graph.getChildren(graph.getPages()[0].id)
|
|
expect(children).toHaveLength(1)
|
|
expect(children[0].name).toBe('Visible')
|
|
})
|
|
|
|
test('symbolOverrides propagate through nested instances', () => {
|
|
// Structure:
|
|
// DOCUMENT (0:0)
|
|
// └─ CANVAS "Page" (0:1)
|
|
// ├─ COMPONENT "Inner" (1:1) ← inner component
|
|
// │ └─ TEXT "Label" (1:2) ← default text "Default"
|
|
// ├─ COMPONENT "Outer" (1:3) ← outer component
|
|
// │ └─ INSTANCE "InnerUse" (1:4) symId=1:1, override: Label→"Changed"
|
|
// └─ INSTANCE "OuterUse" (1:5) ← instance of Outer
|
|
//
|
|
// After import, OuterUse should contain a clone of InnerUse,
|
|
// which should contain a Label text with "Changed" (not "Default")
|
|
const graph = importNodeChanges([
|
|
{
|
|
guid: { sessionID: 0, localID: 0 },
|
|
type: 'DOCUMENT',
|
|
name: 'Document',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
// Inner component
|
|
{
|
|
guid: { sessionID: 1, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Inner',
|
|
phase: 'CREATED',
|
|
size: { x: 100, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
// TEXT child of Inner
|
|
{
|
|
guid: { sessionID: 1, localID: 2 },
|
|
overrideKey: { sessionID: 99, localID: 2 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 1 }, position: '!' },
|
|
type: 'TEXT',
|
|
name: 'Label',
|
|
textData: { characters: 'Default' },
|
|
phase: 'CREATED',
|
|
size: { x: 80, y: 20 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
// Outer component
|
|
{
|
|
guid: { sessionID: 1, localID: 3 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
|
|
type: 'SYMBOL',
|
|
name: 'Outer',
|
|
phase: 'CREATED',
|
|
size: { x: 200, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
// INSTANCE of Inner inside Outer, with symbolOverride on Label
|
|
{
|
|
guid: { sessionID: 1, localID: 4 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'InnerUse',
|
|
phase: 'CREATED',
|
|
size: { x: 100, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 1 },
|
|
symbolOverrides: [
|
|
{
|
|
guidPath: { guids: [{ sessionID: 99, localID: 2 }] },
|
|
textData: { characters: 'Changed' }
|
|
}
|
|
]
|
|
}
|
|
} as NodeChange,
|
|
// Top-level INSTANCE of Outer on the page
|
|
{
|
|
guid: { sessionID: 1, localID: 5 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '#' },
|
|
type: 'INSTANCE',
|
|
name: 'OuterUse',
|
|
phase: 'CREATED',
|
|
size: { x: 200, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 3 }
|
|
}
|
|
} as NodeChange
|
|
])
|
|
|
|
const page = graph.getPages()[0]
|
|
const outerUse = graph.getChildren(page.id).find((n) => n.name === 'OuterUse')
|
|
expect(outerUse).toBeDefined()
|
|
expect(expectDefined(outerUse, 'outerUse').type).toBe('INSTANCE')
|
|
|
|
// Walk down: OuterUse > InnerUse clone > Label clone
|
|
const innerClone = graph.getChildren(outerUse?.id ?? '')[0]
|
|
expect(innerClone).toBeDefined()
|
|
expect(innerClone.name).toBe('InnerUse')
|
|
|
|
const labelClone = graph.getChildren(innerClone.id)[0]
|
|
expect(labelClone).toBeDefined()
|
|
expect(labelClone.name).toBe('Label')
|
|
expect(labelClone.text).toBe('Changed')
|
|
})
|
|
|
|
test('overriddenSymbolID swaps instance component through nested levels', () => {
|
|
// Structure:
|
|
// COMPONENT "IconA" (1:1) → VECTOR "PathA"
|
|
// COMPONENT "IconB" (1:3) → VECTOR "PathB1", VECTOR "PathB2"
|
|
// COMPONENT "Button" (1:5) → INSTANCE "icon" of IconA
|
|
// COMPONENT "Toolbar" (1:7) → INSTANCE "btn" of Button, with override swapping icon to IconB
|
|
// INSTANCE "ToolbarUse" (1:9) of Toolbar on the page
|
|
//
|
|
// After import, ToolbarUse > btn clone > icon clone should have IconB's children
|
|
const graph = importNodeChanges([
|
|
{
|
|
guid: { sessionID: 0, localID: 0 },
|
|
type: 'DOCUMENT',
|
|
name: 'Document',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
// IconA component with 1 child
|
|
{
|
|
guid: { sessionID: 1, localID: 1 },
|
|
overrideKey: { sessionID: 90, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'IconA',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 2 },
|
|
overrideKey: { sessionID: 90, localID: 2 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 1 }, position: '!' },
|
|
type: 'VECTOR',
|
|
name: 'PathA',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
// IconB component with 2 children
|
|
{
|
|
guid: { sessionID: 1, localID: 3 },
|
|
overrideKey: { sessionID: 90, localID: 3 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
|
|
type: 'SYMBOL',
|
|
name: 'IconB',
|
|
phase: 'CREATED',
|
|
size: { x: 40, y: 40 },
|
|
cornerRadius: 12,
|
|
frameMaskDisabled: false,
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 31 },
|
|
overrideKey: { sessionID: 90, localID: 31 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '!' },
|
|
type: 'VECTOR',
|
|
name: 'PathB1',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 12 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 32 },
|
|
overrideKey: { sessionID: 90, localID: 32 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '"' },
|
|
type: 'VECTOR',
|
|
name: 'PathB2',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 12 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 33 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '#' },
|
|
type: 'TEXT',
|
|
name: 'Icon label',
|
|
textData: { characters: 'Default' },
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 12 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
// Button component with instance of IconA
|
|
{
|
|
guid: { sessionID: 1, localID: 5 },
|
|
overrideKey: { sessionID: 90, localID: 5 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '#' },
|
|
type: 'SYMBOL',
|
|
name: 'Button',
|
|
phase: 'CREATED',
|
|
size: { x: 40, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 62 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 5 }, position: '!' },
|
|
type: 'FRAME',
|
|
name: 'first icon wrapper',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 63 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 5 }, position: '"' },
|
|
type: 'FRAME',
|
|
name: 'second icon wrapper',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 24, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 6 },
|
|
overrideKey: { sessionID: 90, localID: 6 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 62 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'icon',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: { symbolID: { sessionID: 1, localID: 1 } }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 61 },
|
|
overrideKey: { sessionID: 90, localID: 61 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 63 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'icon',
|
|
phase: 'CREATED',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 24, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: { symbolID: { sessionID: 1, localID: 1 } }
|
|
} as NodeChange,
|
|
// Toolbar component with instance of Button, swapping icon to IconB
|
|
{
|
|
guid: { sessionID: 1, localID: 7 },
|
|
overrideKey: { sessionID: 90, localID: 7 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '$' },
|
|
type: 'SYMBOL',
|
|
name: 'Toolbar',
|
|
phase: 'CREATED',
|
|
size: { x: 200, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 8 },
|
|
overrideKey: { sessionID: 90, localID: 8 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 7 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'btn',
|
|
phase: 'CREATED',
|
|
size: { x: 40, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 5 },
|
|
symbolOverrides: [
|
|
{
|
|
guidPath: { guids: [{ sessionID: 90, localID: 61 }] },
|
|
overriddenSymbolID: { sessionID: 1, localID: 3 }
|
|
},
|
|
{
|
|
guidPath: {
|
|
guids: [
|
|
{ sessionID: 90, localID: 61 },
|
|
{ sessionID: 90, localID: 31 }
|
|
]
|
|
},
|
|
fillPaints: [
|
|
{
|
|
type: 'SOLID',
|
|
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
colorVar: {
|
|
value: { alias: { guid: { sessionID: 2, localID: 5 } } }
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
guidPath: {
|
|
guids: [
|
|
{ sessionID: 90, localID: 61 },
|
|
{ sessionID: 1, localID: 33 }
|
|
]
|
|
},
|
|
textData: { characters: 'Changed after swap' }
|
|
}
|
|
]
|
|
}
|
|
} as NodeChange,
|
|
// Page-level instance of Toolbar
|
|
{
|
|
guid: { sessionID: 1, localID: 9 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '%' },
|
|
type: 'INSTANCE',
|
|
name: 'ToolbarUse',
|
|
phase: 'CREATED',
|
|
size: { x: 200, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 7 }
|
|
}
|
|
} as NodeChange
|
|
])
|
|
|
|
const page = graph.getPages()[0]
|
|
const toolbarUse = graph.getChildren(page.id).find((n) => n.name === 'ToolbarUse')
|
|
expect(toolbarUse).toBeDefined()
|
|
|
|
// ToolbarUse > btn clone > second wrapped repeated icon clone
|
|
const btnClone = graph.getChildren(toolbarUse?.id ?? '')[0]
|
|
expect(btnClone).toBeDefined()
|
|
expect(btnClone.name).toBe('btn')
|
|
|
|
const buttonChildren = graph.getChildren(btnClone.id)
|
|
expect(buttonChildren.map((child) => child.name)).toEqual([
|
|
'first icon wrapper',
|
|
'second icon wrapper'
|
|
])
|
|
const iconClone = graph.getChildren(buttonChildren[1].id)[0]
|
|
expect(iconClone).toBeDefined()
|
|
expect(iconClone.name).toBe('icon')
|
|
expect(iconClone.width).toBe(24)
|
|
expect(iconClone.height).toBe(24)
|
|
expect(iconClone.cornerRadius).toBe(12)
|
|
expect(iconClone.clipsContent).toBe(true)
|
|
|
|
// Icon should have IconB's children, not IconA's child. The later text
|
|
// override must resolve through the same path prefix after the swap.
|
|
const iconChildren = graph.getChildren(iconClone.id)
|
|
expect(iconChildren).toHaveLength(3)
|
|
expect(iconChildren.map((c) => c.name).sort()).toEqual(['Icon label', 'PathB1', 'PathB2'])
|
|
expect(iconChildren.find((child) => child.name === 'PathB1')?.boundVariables).toMatchObject({
|
|
'fills/0/color': '2:5'
|
|
})
|
|
expect(iconChildren.find((child) => child.name === 'Icon label')?.text).toBe(
|
|
'Changed after swap'
|
|
)
|
|
})
|
|
|
|
test('component swaps use the component set name for variants', () => {
|
|
const graph = importNodeChanges([
|
|
{
|
|
guid: { sessionID: 0, localID: 0 },
|
|
type: 'DOCUMENT',
|
|
name: 'Document',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 1 },
|
|
overrideKey: { sessionID: 90, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Search',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 2 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
|
|
type: 'FRAME',
|
|
name: 'Avatar',
|
|
phase: 'CREATED',
|
|
componentPropDefs: [
|
|
{
|
|
id: { sessionID: 2, localID: 1 },
|
|
name: 'Type',
|
|
type: 'VARIANT',
|
|
initialValue: { textValue: 'Picture' }
|
|
}
|
|
]
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 3 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 2 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Type=Picture',
|
|
phase: 'CREATED'
|
|
} as NodeChange,
|
|
{
|
|
guid: { sessionID: 1, localID: 4 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '#' },
|
|
type: 'INSTANCE',
|
|
name: 'Search',
|
|
phase: 'CREATED',
|
|
size: { x: 50, y: 60 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 1 },
|
|
symbolOverrides: [
|
|
{
|
|
guidPath: { guids: [{ sessionID: 90, localID: 1 }] },
|
|
overriddenSymbolID: { sessionID: 1, localID: 3 },
|
|
size: { x: 200, y: 200 }
|
|
}
|
|
]
|
|
}
|
|
} as NodeChange
|
|
])
|
|
|
|
const instance = graph
|
|
.getChildren(graph.getPages()[0].id)
|
|
.find((node) => node.type === 'INSTANCE')
|
|
expect(instance?.name).toBe('Avatar')
|
|
expect(instance?.width).toBe(50)
|
|
expect(instance?.height).toBe(60)
|
|
})
|
|
|
|
test('DSD propagates through intermediate clones that are also DSD-targeted', async () => {
|
|
const graph = await parseFixture('gold-preview.fig')
|
|
|
|
const thumb = [...graph.getAllNodes()].find((n) => n.name === 'Preview Thumbnail')
|
|
expect(thumb).toBeDefined()
|
|
|
|
let overflows = 0
|
|
function walk(id: string) {
|
|
const node = graph.getNode(id)
|
|
if (!node) return
|
|
if (node.type === 'VECTOR') {
|
|
const parent = graph.getNode(node.parentId)
|
|
if (parent?.type === 'INSTANCE' && parent.width > 0 && parent.height > 0) {
|
|
// Check visibility
|
|
let vis = true
|
|
let cur: typeof node | null = node
|
|
while (cur) {
|
|
if (!cur.visible) {
|
|
vis = false
|
|
break
|
|
}
|
|
cur = cur.parentId ? (graph.getNode(cur.parentId) ?? null) : null
|
|
}
|
|
// Check clipping
|
|
let clipped = false
|
|
cur = graph.getNode(parent.parentId)
|
|
while (cur) {
|
|
if (cur.clipsContent) {
|
|
clipped = true
|
|
break
|
|
}
|
|
cur = cur.parentId ? (graph.getNode(cur.parentId) ?? null) : null
|
|
}
|
|
if (vis && !clipped && node.width > parent.width * 1.2) {
|
|
overflows++
|
|
}
|
|
}
|
|
}
|
|
for (const cid of node.childIds) walk(cid)
|
|
}
|
|
walk(thumb?.id ?? '')
|
|
expect(overflows).toBe(0)
|
|
})
|
|
})
|