chore(tests): enforce non-null assertion rule
- Remove remaining test non-null assertions and delete the broad test override - Add explicit guards for canvas, design panel, auto-layout, SVG, Kiwi, hit-test, scene graph, text editor, and clipboard tests - Validate check plus affected engine and E2E coverage
This commit is contained in:
parent
17857f912f
commit
657188b097
|
|
@ -162,12 +162,6 @@
|
|||
"open-pencil/no-useless-pass-through-wrappers": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "tests/**/*.tsx"],
|
||||
"rules": {
|
||||
"typescript/no-non-null-assertion": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/kiwi/kiwi-schema/**"],
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect, type Page } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { getSelectedIds, getPageChildren, getSelectedNode, getNodeById } from '#tests/helpers/store'
|
||||
|
||||
|
|
@ -106,8 +107,8 @@ test('resize corner handle drag increases node dimensions', async () => {
|
|||
const box = await page.locator('[data-test-id="canvas-element"]').boundingBox()
|
||||
if (!box) throw new Error('No canvas')
|
||||
|
||||
const hx = box.x + viewport!.handleX
|
||||
const hy = box.y + viewport!.handleY
|
||||
const hx = box.x + expectDefined(viewport, 'viewport state').handleX
|
||||
const hy = box.y + expectDefined(viewport, 'viewport state').handleY
|
||||
|
||||
await page.mouse.move(hx, hy)
|
||||
await page.mouse.down()
|
||||
|
|
@ -116,8 +117,12 @@ test('resize corner handle drag increases node dimensions', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const after = await getSelectedNode(page)
|
||||
expect(after!.width).toBeGreaterThan(before!.width + 20)
|
||||
expect(after!.height).toBeGreaterThan(before!.height + 20)
|
||||
expect(expectDefined(after, 'selected node after').width).toBeGreaterThan(
|
||||
expectDefined(before, 'selected node before').width + 20
|
||||
)
|
||||
expect(expectDefined(after, 'selected node after').height).toBeGreaterThan(
|
||||
expectDefined(before, 'selected node before').height + 20
|
||||
)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -129,7 +134,7 @@ test('rotation handle drag rotates node', async () => {
|
|||
|
||||
const before = await getSelectedNode(page)
|
||||
expect(before).not.toBeNull()
|
||||
const initialRotation = before!.rotation ?? 0
|
||||
const initialRotation = expectDefined(before, 'selected node before').rotation ?? 0
|
||||
|
||||
const viewport = await page.evaluate(() => {
|
||||
const store = window.openPencil?.store
|
||||
|
|
@ -150,8 +155,8 @@ test('rotation handle drag rotates node', async () => {
|
|||
const box = await page.locator('[data-test-id="canvas-element"]').boundingBox()
|
||||
if (!box) throw new Error('No canvas')
|
||||
|
||||
const rx = box.x + viewport!.cx
|
||||
const ry = box.y + viewport!.topMidY - 24
|
||||
const rx = box.x + expectDefined(viewport, 'viewport state').cx
|
||||
const ry = box.y + expectDefined(viewport, 'viewport state').topMidY - 24
|
||||
|
||||
await page.mouse.move(rx, ry)
|
||||
await canvas.waitForRender()
|
||||
|
|
@ -160,8 +165,8 @@ test('rotation handle drag rotates node', async () => {
|
|||
await page.mouse.up()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getNodeById(page, before!.id)
|
||||
expect(after!.rotation ?? 0).not.toBe(initialRotation)
|
||||
const after = await getNodeById(page, expectDefined(before, 'selected node before').id)
|
||||
expect(expectDefined(after, 'selected node after').rotation ?? 0).not.toBe(initialRotation)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -236,14 +241,20 @@ async function setupFrameChild(rotation: number) {
|
|||
})
|
||||
|
||||
expect(state).not.toBeNull()
|
||||
return state!
|
||||
return expectDefined(state, 'frame child state')
|
||||
}
|
||||
|
||||
test('frame children keep correct hover and click hit area without rotation', async () => {
|
||||
const state = await setupFrameChild(0)
|
||||
|
||||
await canvas.hover(state.hitX, state.hitY)
|
||||
const hoveredId = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId)
|
||||
const hoveredId = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.hoveredNodeId
|
||||
})()
|
||||
)
|
||||
expect(hoveredId).toBe(state.childId)
|
||||
|
||||
await canvas.click(state.hitX, state.hitY)
|
||||
|
|
@ -252,7 +263,13 @@ test('frame children keep correct hover and click hit area without rotation', as
|
|||
expect(selected?.id).toBe(state.childId)
|
||||
|
||||
await canvas.hover(state.missX, state.missY)
|
||||
const hoveredMiss = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId)
|
||||
const hoveredMiss = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.hoveredNodeId
|
||||
})()
|
||||
)
|
||||
expect(hoveredMiss).not.toBe(state.childId)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
@ -261,7 +278,13 @@ test('rotated frame children keep correct hover and click hit area', async () =>
|
|||
const state = await setupFrameChild(35)
|
||||
|
||||
await canvas.hover(state.hitX, state.hitY)
|
||||
const hoveredId = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId)
|
||||
const hoveredId = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.hoveredNodeId
|
||||
})()
|
||||
)
|
||||
expect(hoveredId).toBe(state.childId)
|
||||
|
||||
await canvas.click(state.hitX, state.hitY)
|
||||
|
|
@ -270,7 +293,13 @@ test('rotated frame children keep correct hover and click hit area', async () =>
|
|||
expect(selected?.id).toBe(state.childId)
|
||||
|
||||
await canvas.hover(state.missX, state.missY)
|
||||
const hoveredMiss = await page.evaluate(() => window.openPencil?.store!.state.hoveredNodeId)
|
||||
const hoveredMiss = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.hoveredNodeId
|
||||
})()
|
||||
)
|
||||
expect(hoveredMiss).not.toBe(state.childId)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
@ -300,8 +329,8 @@ test('rotation drag exposes live rotation preview state', async () => {
|
|||
const box = await page.locator('[data-test-id="canvas-element"]').boundingBox()
|
||||
if (!box) throw new Error('No canvas')
|
||||
|
||||
const rx = box.x + viewport!.cx
|
||||
const ry = box.y + viewport!.topMidY - 24
|
||||
const rx = box.x + expectDefined(viewport, 'viewport state').cx
|
||||
const ry = box.y + expectDefined(viewport, 'viewport state').topMidY - 24
|
||||
|
||||
await page.mouse.move(rx, ry)
|
||||
await canvas.waitForRender()
|
||||
|
|
@ -309,13 +338,25 @@ test('rotation drag exposes live rotation preview state', async () => {
|
|||
await page.mouse.move(rx + 60, ry + 60, { steps: 15 })
|
||||
await canvas.waitForRender()
|
||||
|
||||
const preview = await page.evaluate(() => window.openPencil?.store!.state.rotationPreview)
|
||||
const preview = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.rotationPreview
|
||||
})()
|
||||
)
|
||||
expect(preview).not.toBeNull()
|
||||
|
||||
await page.mouse.up()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const clearedPreview = await page.evaluate(() => window.openPencil?.store!.state.rotationPreview)
|
||||
const clearedPreview = await page.evaluate(() =>
|
||||
(() => {
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
return store.state.rotationPreview
|
||||
})()
|
||||
)
|
||||
expect(clearedPreview).toBeNull()
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
|
|
@ -102,7 +103,7 @@ test('fill item shows color swatch', async () => {
|
|||
|
||||
test('clicking color area changes fill color', async () => {
|
||||
const id = await getSelectedId()
|
||||
const before = await getNode(id!)
|
||||
const before = await getNode(expectDefined(id, 'selected id'))
|
||||
|
||||
const swatch = fillSection().locator('[data-test-id="fill-picker-swatch"]').first()
|
||||
await swatch.click()
|
||||
|
|
@ -111,13 +112,16 @@ test('clicking color area changes fill color', async () => {
|
|||
await expect(colorArea).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const box = await colorArea.boundingBox()
|
||||
await page.mouse.click(box!.x + box!.width - 10, box!.y + 10)
|
||||
await page.mouse.click(
|
||||
expectDefined(box, 'color area bounds').x + expectDefined(box, 'color area bounds').width - 10,
|
||||
expectDefined(box, 'color area bounds').y + 10
|
||||
)
|
||||
await canvas.waitForRender()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const after = await getNode(id!)
|
||||
const c1 = before!.fills[0].color
|
||||
const c2 = after!.fills[0].color
|
||||
const after = await getNode(expectDefined(id, 'selected id'))
|
||||
const c1 = expectDefined(before, 'before node').fills[0].color
|
||||
const c2 = expectDefined(after, 'after node').fills[0].color
|
||||
expect(c1.r !== c2.r || c1.g !== c2.g || c1.b !== c2.b).toBe(true)
|
||||
|
||||
// Close popover — click the swatch again to toggle it off
|
||||
|
|
@ -134,8 +138,8 @@ test('adding a stroke creates stroke section item', async () => {
|
|||
await expect(strokeItems.first()).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.strokes.length).toBe(1)
|
||||
const node = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(node, 'node node').strokes.length).toBe(1)
|
||||
})
|
||||
|
||||
test('adding an effect creates effect item', async () => {
|
||||
|
|
@ -147,8 +151,8 @@ test('adding an effect creates effect item', async () => {
|
|||
await expect(effectItems.first()).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.effects.length).toBe(1)
|
||||
const node = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(node, 'node node').effects.length).toBe(1)
|
||||
})
|
||||
|
||||
test('adding a second fill shows two fill items', async () => {
|
||||
|
|
@ -160,8 +164,8 @@ test('adding a second fill shows two fill items', async () => {
|
|||
expect(await fillItems.count()).toBe(2)
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(id!)
|
||||
expect(node!.fills.length).toBe(2)
|
||||
const node = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(node, 'node node').fills.length).toBe(2)
|
||||
})
|
||||
|
||||
test('visibility toggle in appearance section works', async () => {
|
||||
|
|
@ -169,20 +173,20 @@ test('visibility toggle in appearance section works', async () => {
|
|||
await expect(visBtn).toBeVisible()
|
||||
|
||||
const id = await getSelectedId()
|
||||
const before = await getNode(id!)
|
||||
expect(before!.visible).toBe(true)
|
||||
const before = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(before, 'before node').visible).toBe(true)
|
||||
|
||||
await visBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const after = await getNode(id!)
|
||||
expect(after!.visible).toBe(false)
|
||||
const after = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(after, 'after node').visible).toBe(false)
|
||||
|
||||
await visBtn.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const restored = await getNode(id!)
|
||||
expect(restored!.visible).toBe(true)
|
||||
const restored = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(restored, 'restored node').visible).toBe(true)
|
||||
})
|
||||
|
||||
test('fill stroke and effect visibility toggles update on repeated clicks and support undo redo', async () => {
|
||||
|
|
@ -192,23 +196,35 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
|
|||
const fillButton = page.locator('[data-test-id="fill-visibility-0"]')
|
||||
await expect(fillButton).toBeVisible()
|
||||
|
||||
const initial = await getNode(id!)
|
||||
expect(initial!.fills[0]?.visible).toBe(true)
|
||||
const initial = await getNode(expectDefined(id, 'selected id'))
|
||||
expect(expectDefined(initial, 'initial node').fills[0]?.visible).toBe(true)
|
||||
|
||||
await fillButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(fillButton).toHaveAttribute('data-visible', 'false')
|
||||
expect((await getNode(id!))!.fills[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
|
||||
await fillButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(fillButton).toHaveAttribute('data-visible', 'true')
|
||||
expect((await getNode(id!))!.fills[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getNode(id!))!.fills[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
await canvas.redo()
|
||||
expect((await getNode(id!))!.fills[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').fills[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
const strokeAddButton = strokeSection().locator('[data-test-id="stroke-section-add"]')
|
||||
await strokeAddButton.click()
|
||||
|
|
@ -216,22 +232,37 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
|
|||
|
||||
const strokeButton = page.locator('[data-test-id="stroke-visibility-0"]')
|
||||
await expect(strokeButton).toBeVisible()
|
||||
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
await strokeButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(strokeButton).toHaveAttribute('data-visible', 'false')
|
||||
expect((await getNode(id!))!.strokes[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
|
||||
await strokeButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(strokeButton).toHaveAttribute('data-visible', 'true')
|
||||
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getNode(id!))!.strokes[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
await canvas.redo()
|
||||
expect((await getNode(id!))!.strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').strokes[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
const effectAddButton = effectsSection().locator('[data-test-id="effects-section-add"]')
|
||||
await effectAddButton.click()
|
||||
|
|
@ -239,22 +270,37 @@ test('fill stroke and effect visibility toggles update on repeated clicks and su
|
|||
|
||||
const effectButton = page.locator('[data-test-id="effect-visibility-0"]')
|
||||
await expect(effectButton).toBeVisible()
|
||||
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
await effectButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(effectButton).toHaveAttribute('data-visible', 'false')
|
||||
expect((await getNode(id!))!.effects[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
|
||||
await effectButton.click()
|
||||
await canvas.waitForRender()
|
||||
await expect(effectButton).toHaveAttribute('data-visible', 'true')
|
||||
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
|
||||
await canvas.undo()
|
||||
expect((await getNode(id!))!.effects[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
|
||||
?.visible
|
||||
).toBe(false)
|
||||
await canvas.redo()
|
||||
expect((await getNode(id!))!.effects[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getNode(expectDefined(id, 'selected id')), 'selected node').effects[0]
|
||||
?.visible
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('deselecting shows empty design panel', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect, type Page } from '@playwright/test'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { getSelectedNode, getNodeById } from '#tests/helpers/store'
|
||||
|
||||
|
|
@ -24,7 +25,9 @@ test.afterAll(async () => {
|
|||
async function selectFrame() {
|
||||
expect(frameId, 'frameId must be set — did the Shift+A test run?').toBeTruthy()
|
||||
await page.evaluate((id: string) => {
|
||||
window.openPencil?.store!.select([id])
|
||||
const store = window.openPencil?.store
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.select([id])
|
||||
}, frameId)
|
||||
await canvas.waitForRender()
|
||||
}
|
||||
|
|
@ -41,11 +44,11 @@ test('Shift+A wraps selection in auto-layout frame', async () => {
|
|||
|
||||
const node = await getSelectedNode(page)
|
||||
expect(node).not.toBeNull()
|
||||
expect(node!.type).toBe('FRAME')
|
||||
expect(node!.layoutMode).not.toBe('NONE')
|
||||
expect(node!.childIds.length).toBe(2)
|
||||
expect(expectDefined(node, 'node').type).toBe('FRAME')
|
||||
expect(expectDefined(node, 'node').layoutMode).not.toBe('NONE')
|
||||
expect(expectDefined(node, 'node').childIds.length).toBe(2)
|
||||
|
||||
frameId = node!.id
|
||||
frameId = expectDefined(node, 'node').id
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -56,19 +59,19 @@ test('direction button toggles to VERTICAL', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
expect(frame!.layoutMode).toBe('VERTICAL')
|
||||
expect(expectDefined(frame, 'frame').layoutMode).toBe('VERTICAL')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('gap ScrubInput sets itemSpacing', async () => {
|
||||
await selectFrame()
|
||||
const before = await getNodeById(page, frameId)
|
||||
const initialSpacing = before!.itemSpacing
|
||||
const initialSpacing = expectDefined(before, 'before').itemSpacing
|
||||
|
||||
await canvas.dragScrubInput(page.locator('[data-test-id="layout-gap-input"]'), 40)
|
||||
|
||||
const after = await getNodeById(page, frameId)
|
||||
expect(after!.itemSpacing).toBeGreaterThan(initialSpacing + 5)
|
||||
expect(expectDefined(after, 'after').itemSpacing).toBeGreaterThan(initialSpacing + 5)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -79,15 +82,17 @@ test('gap menu sets auto space-between alignment', async () => {
|
|||
await page.getByRole('option', { name: 'Auto' }).click()
|
||||
await canvas.waitForRender()
|
||||
let frame = await getNodeById(page, frameId)
|
||||
expect(frame!.primaryAxisAlign).toBe('SPACE_BETWEEN')
|
||||
expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('SPACE_BETWEEN')
|
||||
await expect(page.locator('[data-test-id="layout-alignment-grid"] button')).toHaveCount(9)
|
||||
|
||||
await page.locator('[data-test-id="layout-gap-menu"]').click()
|
||||
await page.getByRole('option', { name: String(Math.round(frame!.itemSpacing)) }).click()
|
||||
await page
|
||||
.getByRole('option', { name: String(Math.round(expectDefined(frame, 'frame').itemSpacing)) })
|
||||
.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
frame = await getNodeById(page, frameId)
|
||||
expect(frame!.primaryAxisAlign).toBe('MIN')
|
||||
expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('MIN')
|
||||
await expect(page.locator('[data-test-id="layout-alignment-grid"] button')).toHaveCount(9)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
@ -99,12 +104,12 @@ test('wrap mode exposes cross-axis gap control', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const before = await getNodeById(page, frameId)
|
||||
const initialSpacing = before!.counterAxisSpacing
|
||||
const initialSpacing = expectDefined(before, 'before').counterAxisSpacing
|
||||
await canvas.dragScrubInput(page.locator('[data-test-id="layout-cross-gap-input"]'), 40)
|
||||
|
||||
const after = await getNodeById(page, frameId)
|
||||
expect(after!.layoutWrap).toBe('WRAP')
|
||||
expect(after!.counterAxisSpacing).toBeGreaterThan(initialSpacing + 5)
|
||||
expect(expectDefined(after, 'after').layoutWrap).toBe('WRAP')
|
||||
expect(expectDefined(after, 'after').counterAxisSpacing).toBeGreaterThan(initialSpacing + 5)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -130,10 +135,10 @@ test('padding controls set horizontal and vertical padding pairs', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
expect(frame!.paddingTop).toBe(16)
|
||||
expect(frame!.paddingRight).toBe(24)
|
||||
expect(frame!.paddingBottom).toBe(16)
|
||||
expect(frame!.paddingLeft).toBe(24)
|
||||
expect(expectDefined(frame, 'frame').paddingTop).toBe(16)
|
||||
expect(expectDefined(frame, 'frame').paddingRight).toBe(24)
|
||||
expect(expectDefined(frame, 'frame').paddingBottom).toBe(16)
|
||||
expect(expectDefined(frame, 'frame').paddingLeft).toBe(24)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -145,7 +150,9 @@ test('size dropdown adds and removes min width', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
let frame = await getNodeById(page, frameId)
|
||||
expect(frame!.minWidth).toBe(Math.round(frame!.width))
|
||||
expect(expectDefined(frame, 'frame').minWidth).toBe(
|
||||
Math.round(expectDefined(frame, 'frame').width)
|
||||
)
|
||||
await expect(page.locator('[data-test-id="layout-min-width-input"]')).toBeVisible()
|
||||
|
||||
await page.locator('[data-test-id="layout-width-sizing-menu"]').click()
|
||||
|
|
@ -153,7 +160,7 @@ test('size dropdown adds and removes min width', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
frame = await getNodeById(page, frameId)
|
||||
expect(frame!.minWidth).toBeNull()
|
||||
expect(expectDefined(frame, 'frame').minWidth).toBeNull()
|
||||
await expect(page.locator('[data-test-id="layout-min-width-input"]')).toHaveCount(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
@ -166,8 +173,8 @@ test('alignment grid center sets CENTER alignment', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
expect(frame!.primaryAxisAlign).toBe('CENTER')
|
||||
expect(frame!.counterAxisAlign).toBe('CENTER')
|
||||
expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('CENTER')
|
||||
expect(expectDefined(frame, 'frame').counterAxisAlign).toBe('CENTER')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -178,6 +185,6 @@ test('remove auto-layout sets layoutMode to NONE', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
expect(frame!.layoutMode).toBe('NONE')
|
||||
expect(expectDefined(frame, 'frame').layoutMode).toBe('NONE')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { describe, expect, it } from 'bun:test'
|
|||
import { importClipboardNodes } from '@open-pencil/core'
|
||||
import type { NodeChange, SceneNode } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { createClipboardGraph } from '#tests/helpers/clipboard'
|
||||
|
||||
describe('importClipboardNodes', () => {
|
||||
|
|
@ -52,7 +53,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const card = graph.getNode(created[0])!
|
||||
const card = getNodeOrThrow(graph, created[0])
|
||||
expect(card.type).toBe('FRAME')
|
||||
expect(card.name).toBe('Card')
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ describe('importClipboardNodes', () => {
|
|||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
expect(graph.getNode(created[0])!.name).toBe('RealShape')
|
||||
expect(getNodeOrThrow(graph, created[0]).name).toBe('RealShape')
|
||||
})
|
||||
|
||||
it('imports nested frames with children', () => {
|
||||
|
|
@ -154,7 +155,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const outer = graph.getNode(created[0])!
|
||||
const outer = getNodeOrThrow(graph, created[0])
|
||||
expect(outer.name).toBe('Outer')
|
||||
|
||||
const innerList = graph.getChildren(outer.id)
|
||||
|
|
@ -196,7 +197,7 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
const node = graph.getNode(created[0])!
|
||||
const node = getNodeOrThrow(graph, created[0])
|
||||
expect(node.fills).toHaveLength(1)
|
||||
expect(node.fills[0].color.r).toBe(1)
|
||||
expect(node.strokes).toHaveLength(1)
|
||||
|
|
@ -244,7 +245,7 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
const row = graph.getNode(created[0])!
|
||||
const row = getNodeOrThrow(graph, created[0])
|
||||
const children = graph.getChildren(row.id)
|
||||
expect(children[0].layoutAlignSelf).toBe('STRETCH')
|
||||
expect(children[1].layoutAlignSelf).toBe('AUTO')
|
||||
|
|
@ -290,9 +291,9 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(graph.getNode(created[0])!.clipsContent).toBe(true)
|
||||
expect(graph.getNode(created[1])!.clipsContent).toBe(false)
|
||||
expect(graph.getNode(created[2])!.clipsContent).toBe(false)
|
||||
expect(getNodeOrThrow(graph, created[0]).clipsContent).toBe(true)
|
||||
expect(getNodeOrThrow(graph, created[1]).clipsContent).toBe(false)
|
||||
expect(getNodeOrThrow(graph, created[2]).clipsContent).toBe(false)
|
||||
})
|
||||
|
||||
it('imports fontWeight from fontName.style via styleToWeight', () => {
|
||||
|
|
@ -342,10 +343,10 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(graph.getNode(created[0])!.fontWeight).toBe(500)
|
||||
expect(graph.getNode(created[1])!.fontWeight).toBe(700)
|
||||
expect(graph.getNode(created[2])!.fontWeight).toBe(700)
|
||||
expect(graph.getNode(created[2])!.italic).toBe(true)
|
||||
expect(getNodeOrThrow(graph, created[0]).fontWeight).toBe(500)
|
||||
expect(getNodeOrThrow(graph, created[1]).fontWeight).toBe(700)
|
||||
expect(getNodeOrThrow(graph, created[2]).fontWeight).toBe(700)
|
||||
expect(getNodeOrThrow(graph, created[2]).italic).toBe(true)
|
||||
})
|
||||
|
||||
it('converts letterSpacing object to pixels', () => {
|
||||
|
|
@ -394,9 +395,9 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(graph.getNode(created[0])!.letterSpacing).toBe(2)
|
||||
expect(graph.getNode(created[1])!.letterSpacing).toBe(2) // 10% of 20px
|
||||
expect(graph.getNode(created[2])!.letterSpacing).toBe(0)
|
||||
expect(getNodeOrThrow(graph, created[0]).letterSpacing).toBe(2)
|
||||
expect(getNodeOrThrow(graph, created[1]).letterSpacing).toBe(2) // 10% of 20px
|
||||
expect(getNodeOrThrow(graph, created[2]).letterSpacing).toBe(0)
|
||||
})
|
||||
|
||||
it('converts RAW lineHeight to pixels', () => {
|
||||
|
|
@ -446,9 +447,9 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(graph.getNode(created[0])!.lineHeight).toBe(36) // 24 * 1.5
|
||||
expect(graph.getNode(created[1])!.lineHeight).toBe(20)
|
||||
expect(graph.getNode(created[2])!.lineHeight).toBe(24) // 120% of 20
|
||||
expect(getNodeOrThrow(graph, created[0]).lineHeight).toBe(36) // 24 * 1.5
|
||||
expect(getNodeOrThrow(graph, created[1]).lineHeight).toBe(20)
|
||||
expect(getNodeOrThrow(graph, created[2]).lineHeight).toBe(24) // 120% of 20
|
||||
})
|
||||
|
||||
it('converts letterSpacing and lineHeight in style overrides', () => {
|
||||
|
|
@ -487,7 +488,7 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
const node = graph.getNode(created[0])!
|
||||
const node = getNodeOrThrow(graph, created[0])
|
||||
expect(node.styleRuns).toHaveLength(1)
|
||||
expect(node.styleRuns[0].style.lineHeight).toBe(30) // 20 * 1.5
|
||||
expect(node.styleRuns[0].style.letterSpacing).toBeCloseTo(-0.4) // 20 * -2/100
|
||||
|
|
@ -542,7 +543,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const component = graph.getNode(created[0])!
|
||||
const component = getNodeOrThrow(graph, created[0])
|
||||
expect(component.type).toBe('COMPONENT')
|
||||
expect(component.layoutMode).toBe('VERTICAL')
|
||||
expect(component.itemSpacing).toBe(16)
|
||||
|
|
@ -598,11 +599,11 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(2)
|
||||
|
||||
const component = graph.getNode(created[0])!
|
||||
const component = getNodeOrThrow(graph, created[0])
|
||||
expect(component.type).toBe('COMPONENT')
|
||||
expect(graph.getChildren(component.id)).toHaveLength(1)
|
||||
|
||||
const instance = graph.getNode(created[1])!
|
||||
const instance = getNodeOrThrow(graph, created[1])
|
||||
expect(instance.type).toBe('INSTANCE')
|
||||
expect(instance.componentId).toBe(component.id)
|
||||
|
||||
|
|
@ -662,7 +663,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const instance = graph.getNode(created[0])!
|
||||
const instance = getNodeOrThrow(graph, created[0])
|
||||
expect(instance.type).toBe('INSTANCE')
|
||||
expect(instance.name).toBe('Icon')
|
||||
|
||||
|
|
@ -717,7 +718,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const card = graph.getNode(created[0])!
|
||||
const card = getNodeOrThrow(graph, created[0])
|
||||
const children = graph.getChildren(card.id)
|
||||
expect(children).toHaveLength(1)
|
||||
|
||||
|
|
@ -792,7 +793,7 @@ describe('importClipboardNodes', () => {
|
|||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(created).toHaveLength(1)
|
||||
|
||||
const instance = graph.getNode(created[0])!
|
||||
const instance = getNodeOrThrow(graph, created[0])
|
||||
expect(instance.type).toBe('INSTANCE')
|
||||
const children = graph.getChildren(instance.id)
|
||||
expect(children).toHaveLength(1)
|
||||
|
|
@ -845,9 +846,9 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
expect(graph.getNode(created[0])!.textAutoResize).toBe('HEIGHT')
|
||||
expect(graph.getNode(created[1])!.textAutoResize).toBe('WIDTH_AND_HEIGHT')
|
||||
expect(graph.getNode(created[2])!.textAutoResize).toBe('NONE')
|
||||
expect(getNodeOrThrow(graph, created[0]).textAutoResize).toBe('HEIGHT')
|
||||
expect(getNodeOrThrow(graph, created[1]).textAutoResize).toBe('WIDTH_AND_HEIGHT')
|
||||
expect(getNodeOrThrow(graph, created[2]).textAutoResize).toBe('NONE')
|
||||
})
|
||||
|
||||
it('undo removes all imported nodes including children', () => {
|
||||
|
|
@ -940,11 +941,11 @@ describe('importClipboardNodes', () => {
|
|||
|
||||
const childrenBefore = graph.getChildren(pageId).length
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
const parent = graph.getNode(created[0])!
|
||||
const parent = getNodeOrThrow(graph, created[0])
|
||||
|
||||
const allSnapshots: SceneNode[] = []
|
||||
function walk(id: string) {
|
||||
const n = graph.getNode(id)!
|
||||
const n = getNodeOrThrow(graph, id)
|
||||
allSnapshots.push({ ...n })
|
||||
for (const cid of n.childIds) walk(cid)
|
||||
}
|
||||
|
|
@ -962,7 +963,7 @@ describe('importClipboardNodes', () => {
|
|||
})
|
||||
}
|
||||
|
||||
const restored = graph.getNode(parent.id)!
|
||||
const restored = getNodeOrThrow(graph, parent.id)
|
||||
expect(restored).toBeTruthy()
|
||||
expect(restored.name).toBe('Parent')
|
||||
expect(restored.childIds).toHaveLength(2)
|
||||
|
|
@ -1003,11 +1004,11 @@ describe('importClipboardNodes', () => {
|
|||
] as NodeChange[]
|
||||
|
||||
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
||||
const parent = graph.getNode(created[0])!
|
||||
const parent = getNodeOrThrow(graph, created[0])
|
||||
|
||||
const allSnapshots: SceneNode[] = []
|
||||
function walk(id: string) {
|
||||
const n = graph.getNode(id)!
|
||||
const n = getNodeOrThrow(graph, id)
|
||||
allSnapshots.push({ ...n })
|
||||
for (const cid of n.childIds) walk(cid)
|
||||
}
|
||||
|
|
@ -1020,7 +1021,7 @@ describe('importClipboardNodes', () => {
|
|||
graph.createNode(snapshot.type, snapshot.parentId ?? pageId, snapshot)
|
||||
}
|
||||
|
||||
const restored = graph.getNode(parent.id)!
|
||||
const restored = getNodeOrThrow(graph, parent.id)
|
||||
// Bug: parent has duplicated childIds because snapshot already had [childId]
|
||||
// and createNode appends childId again
|
||||
expect(restored.childIds.length).toBeGreaterThan(1)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test'
|
|||
|
||||
import { SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
|
||||
|
||||
function pageId(graph: SceneGraph) {
|
||||
return graph.getPages()[0].id
|
||||
}
|
||||
|
|
@ -31,21 +33,21 @@ describe('hitTest — group behavior', () => {
|
|||
const { graph, page } = setup()
|
||||
const hit = graph.hitTest(110, 110, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.type).toBe('GROUP')
|
||||
expect(expectDefined(hit, 'hit node').type).toBe('GROUP')
|
||||
})
|
||||
|
||||
test('hitTestDeep on child returns child directly', () => {
|
||||
const { graph, page, child } = setup()
|
||||
const hit = graph.hitTestDeep(110, 110, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(child.id)
|
||||
})
|
||||
|
||||
test('hitTest with scope=group returns child', () => {
|
||||
const { graph, group } = setup()
|
||||
const hit = graph.hitTest(110, 110, group.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('Rect')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('Rect')
|
||||
})
|
||||
|
||||
test('hitTest miss inside group returns null', () => {
|
||||
|
|
@ -93,28 +95,28 @@ describe('hitTest — nested groups', () => {
|
|||
const { graph, page } = setup()
|
||||
const hit = graph.hitTest(100, 100, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('GroupA')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('GroupA')
|
||||
})
|
||||
|
||||
test('click with scope=GroupA returns GroupB', () => {
|
||||
const { graph, groupA } = setup()
|
||||
const hit = graph.hitTest(150, 150, groupA.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('GroupB')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('GroupB')
|
||||
})
|
||||
|
||||
test('click with scope=GroupB returns DeepRect', () => {
|
||||
const { graph, groupB } = setup()
|
||||
const hit = graph.hitTest(160, 160, groupB.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('DeepRect')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('DeepRect')
|
||||
})
|
||||
|
||||
test('hitTestDeep from page returns deepest child', () => {
|
||||
const { graph, page, rect } = setup()
|
||||
const hit = graph.hitTestDeep(160, 160, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(rect.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(rect.id)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -140,7 +142,7 @@ describe('hitTest — locked nodes', () => {
|
|||
|
||||
const hit = graph.hitTestDeep(10, 10, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('LockedFrame')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('LockedFrame')
|
||||
})
|
||||
|
||||
test('locked leaf node is still clickable', () => {
|
||||
|
|
@ -157,7 +159,7 @@ describe('hitTest — locked nodes', () => {
|
|||
|
||||
const hit = graph.hitTest(25, 25, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('LockedRect')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('LockedRect')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -194,7 +196,7 @@ describe('scene graph — locked node operations', () => {
|
|||
|
||||
const node = graph.getNode(rect.id)
|
||||
expect(node).not.toBeNull()
|
||||
expect(node!.locked).toBe(true)
|
||||
expect(expectDefined(node, 'node').locked).toBe(true)
|
||||
})
|
||||
|
||||
test('lock can be toggled', () => {
|
||||
|
|
@ -208,11 +210,11 @@ describe('scene graph — locked node operations', () => {
|
|||
height: 50
|
||||
})
|
||||
|
||||
expect(graph.getNode(rect.id)!.locked).toBe(false)
|
||||
expect(getNodeOrThrow(graph, rect.id).locked).toBe(false)
|
||||
graph.updateNode(rect.id, { locked: true })
|
||||
expect(graph.getNode(rect.id)!.locked).toBe(true)
|
||||
expect(getNodeOrThrow(graph, rect.id).locked).toBe(true)
|
||||
graph.updateNode(rect.id, { locked: false })
|
||||
expect(graph.getNode(rect.id)!.locked).toBe(false)
|
||||
expect(getNodeOrThrow(graph, rect.id).locked).toBe(false)
|
||||
})
|
||||
|
||||
test('visibility can be toggled', () => {
|
||||
|
|
@ -226,11 +228,11 @@ describe('scene graph — locked node operations', () => {
|
|||
height: 50
|
||||
})
|
||||
|
||||
expect(graph.getNode(rect.id)!.visible).toBe(true)
|
||||
expect(getNodeOrThrow(graph, rect.id).visible).toBe(true)
|
||||
graph.updateNode(rect.id, { visible: false })
|
||||
expect(graph.getNode(rect.id)!.visible).toBe(false)
|
||||
expect(getNodeOrThrow(graph, rect.id).visible).toBe(false)
|
||||
graph.updateNode(rect.id, { visible: true })
|
||||
expect(graph.getNode(rect.id)!.visible).toBe(true)
|
||||
expect(getNodeOrThrow(graph, rect.id).visible).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -255,7 +257,7 @@ describe('hitTest — frame with children', () => {
|
|||
|
||||
const hit = graph.hitTest(70, 70, frame.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(child.id)
|
||||
})
|
||||
|
||||
test('rotated frame scope hit test finds children using rotated local bounds', () => {
|
||||
|
|
@ -280,7 +282,7 @@ describe('hitTest — frame with children', () => {
|
|||
// point that is inside rotated frame bounds and child bounds
|
||||
const hitInside = graph.hitTest(193, 111, frame.id)
|
||||
expect(hitInside).not.toBeNull()
|
||||
expect(hitInside!.id).toBe(child.id)
|
||||
expect(expectDefined(hitInside, 'inside hit node').id).toBe(child.id)
|
||||
|
||||
// point that would be inside if frame were not rotated
|
||||
const hitOutside = graph.hitTest(160, 130, frame.id)
|
||||
|
|
@ -309,7 +311,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => {
|
|||
|
||||
const hit = graph.hitTest(10, 10, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('MyComp')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('MyComp')
|
||||
})
|
||||
|
||||
test('hitTestDeep inside COMPONENT scope finds child', () => {
|
||||
|
|
@ -332,7 +334,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => {
|
|||
|
||||
const hit = graph.hitTestDeep(10, 10, comp.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(child.id)
|
||||
})
|
||||
|
||||
test('hitTest on INSTANCE returns instance itself', () => {
|
||||
|
|
@ -355,7 +357,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => {
|
|||
|
||||
const hit = graph.hitTest(55, 55, page)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.name).toBe('MyInstance')
|
||||
expect(expectDefined(hit, 'hit node').name).toBe('MyInstance')
|
||||
})
|
||||
|
||||
test('hitTestDeep inside INSTANCE scope finds child', () => {
|
||||
|
|
@ -378,7 +380,7 @@ describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => {
|
|||
|
||||
const hit = graph.hitTestDeep(55, 55, inst.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(child.id)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -410,7 +412,7 @@ describe('hitTest — absolute position and scope offset', () => {
|
|||
|
||||
const hit = graph.hitTest(250, 360, frame.id)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
expect(expectDefined(hit, 'hit node').id).toBe(child.id)
|
||||
|
||||
const missHit = graph.hitTest(50, 60, frame.id)
|
||||
expect(missHit).toBeNull()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
sceneNodeToKiwi
|
||||
} from '@open-pencil/core'
|
||||
|
||||
import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCodec()
|
||||
})
|
||||
|
|
@ -55,7 +57,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
const blobs: Uint8Array[] = []
|
||||
// Serialize the parent — children are serialized recursively
|
||||
const changes = sceneNodeToKiwi(
|
||||
graph.getNode(parent.id)!,
|
||||
getNodeOrThrow(graph, parent.id),
|
||||
ROOT_GUID,
|
||||
0,
|
||||
{ value: 100 },
|
||||
|
|
@ -64,7 +66,10 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
) as Record<string, unknown>[]
|
||||
|
||||
// changes[0] = parent, changes[1] = child
|
||||
const childNc = changes.find((nc) => nc.name === 'Child')!
|
||||
const childNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'Child'),
|
||||
'child node change'
|
||||
)
|
||||
expect(childNc).toBeDefined()
|
||||
expect(childNc.transform.m02).toBe(0)
|
||||
expect(childNc.transform.m12).toBe(0)
|
||||
|
|
@ -93,7 +98,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
|
||||
const blobs: Uint8Array[] = []
|
||||
const changes = sceneNodeToKiwi(
|
||||
graph.getNode(parent.id)!,
|
||||
getNodeOrThrow(graph, parent.id),
|
||||
ROOT_GUID,
|
||||
0,
|
||||
{ value: 100 },
|
||||
|
|
@ -101,7 +106,10 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
blobs
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const absNc = changes.find((nc) => nc.name === 'AbsChild')!
|
||||
const absNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'AbsChild'),
|
||||
'absolute child node change'
|
||||
)
|
||||
expect(absNc).toBeDefined()
|
||||
expect(absNc.transform.m02).toBe(75)
|
||||
expect(absNc.transform.m12).toBe(120)
|
||||
|
|
@ -128,7 +136,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
|
||||
const blobs: Uint8Array[] = []
|
||||
const changes = sceneNodeToKiwi(
|
||||
graph.getNode(parent.id)!,
|
||||
getNodeOrThrow(graph, parent.id),
|
||||
ROOT_GUID,
|
||||
0,
|
||||
{ value: 100 },
|
||||
|
|
@ -136,7 +144,10 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
blobs
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const childNc = changes.find((nc) => nc.name === 'Child')!
|
||||
const childNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'Child'),
|
||||
'child node change'
|
||||
)
|
||||
expect(childNc).toBeDefined()
|
||||
expect(childNc.transform.m02).toBe(30)
|
||||
expect(childNc.transform.m12).toBe(45)
|
||||
|
|
@ -164,7 +175,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
|
||||
const blobs: Uint8Array[] = []
|
||||
const changes = sceneNodeToKiwi(
|
||||
graph.getNode(parent.id)!,
|
||||
getNodeOrThrow(graph, parent.id),
|
||||
ROOT_GUID,
|
||||
0,
|
||||
{ value: 100 },
|
||||
|
|
@ -172,7 +183,10 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
blobs
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const itemNc = changes.find((nc) => nc.name === 'Item')!
|
||||
const itemNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'Item'),
|
||||
'item node change'
|
||||
)
|
||||
expect(itemNc.transform.m02).toBe(0)
|
||||
expect(itemNc.transform.m12).toBe(0)
|
||||
})
|
||||
|
|
@ -241,7 +255,10 @@ describe('Fix 2: frameMaskDisabled is inverse of clipsContent', () => {
|
|||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const frame = [...reimported.nodes.values()].find((n) => n.name === 'ClipFrame')!
|
||||
const frame = expectDefined(
|
||||
[...reimported.nodes.values()].find((n) => n.name === 'ClipFrame'),
|
||||
'ClipFrame'
|
||||
)
|
||||
expect(frame.clipsContent).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -382,7 +399,10 @@ describe('Fix 4: text lineHeight serialization', () => {
|
|||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')!
|
||||
const textNode = expectDefined(
|
||||
[...reimported.nodes.values()].find((n) => n.type === 'TEXT'),
|
||||
'text node'
|
||||
)
|
||||
expect(textNode.lineHeight).toBe(28)
|
||||
})
|
||||
})
|
||||
|
|
@ -409,7 +429,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => {
|
|||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')!
|
||||
const textNode = expectDefined(
|
||||
[...reimported.nodes.values()].find((n) => n.type === 'TEXT'),
|
||||
'text node'
|
||||
)
|
||||
expect(textNode.fontFamily).toBe('DM Sans')
|
||||
})
|
||||
|
||||
|
|
@ -430,7 +453,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => {
|
|||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')!
|
||||
const textNode = expectDefined(
|
||||
[...reimported.nodes.values()].find((n) => n.type === 'TEXT'),
|
||||
'text node'
|
||||
)
|
||||
expect(textNode.fontFamily).toBe('Roboto')
|
||||
})
|
||||
|
||||
|
|
@ -451,7 +477,10 @@ describe('Fix 5: font family normalization in derivedTextData', () => {
|
|||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const textNode = [...reimported.nodes.values()].find((n) => n.type === 'TEXT')!
|
||||
const textNode = expectDefined(
|
||||
[...reimported.nodes.values()].find((n) => n.type === 'TEXT'),
|
||||
'text node'
|
||||
)
|
||||
expect(textNode.fontFamily).toBe('Inter')
|
||||
})
|
||||
|
||||
|
|
@ -533,7 +562,7 @@ describe('Integration: auto-layout component with all fixes', () => {
|
|||
// Verify kiwi output directly
|
||||
const blobs: Uint8Array[] = []
|
||||
const changes = sceneNodeToKiwi(
|
||||
graph.getNode(card.id)!,
|
||||
getNodeOrThrow(graph, card.id),
|
||||
ROOT_GUID,
|
||||
0,
|
||||
{ value: 100 },
|
||||
|
|
@ -542,8 +571,14 @@ describe('Integration: auto-layout component with all fixes', () => {
|
|||
) as Record<string, unknown>[]
|
||||
|
||||
const cardNc = changes[0]
|
||||
const titleNc = changes.find((nc) => nc.name === 'Title')!
|
||||
const valueNc = changes.find((nc) => nc.name === 'Value')!
|
||||
const titleNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'Title'),
|
||||
'Title node change'
|
||||
)
|
||||
const valueNc = expectDefined(
|
||||
changes.find((nc) => nc.name === 'Value'),
|
||||
'Value node change'
|
||||
)
|
||||
|
||||
// Fix 1: children have zero transforms
|
||||
expect(titleNc.transform.m02).toBe(0)
|
||||
|
|
@ -571,8 +606,14 @@ describe('Integration: auto-layout component with all fixes', () => {
|
|||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const nodes = [...reimported.nodes.values()]
|
||||
const cardNode = nodes.find((n) => n.name === 'StatCard')!
|
||||
const titleNode = nodes.find((n) => n.name === 'Title')!
|
||||
const cardNode = expectDefined(
|
||||
nodes.find((n) => n.name === 'StatCard'),
|
||||
'StatCard node'
|
||||
)
|
||||
const titleNode = expectDefined(
|
||||
nodes.find((n) => n.name === 'Title'),
|
||||
'Title node'
|
||||
)
|
||||
|
||||
expect(cardNode.layoutMode).toBe('VERTICAL')
|
||||
expect(cardNode.clipsContent).toBe(true)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
|
||||
import { createEditorStore } from '@/app/editor/session'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, loadFixtureGraph, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('Auto Layout', () => {
|
||||
|
|
@ -394,7 +395,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(130)
|
||||
})
|
||||
|
||||
|
|
@ -413,7 +414,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.height).toBe(110)
|
||||
})
|
||||
|
||||
|
|
@ -433,7 +434,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(160)
|
||||
expect(f.height).toBe(90)
|
||||
})
|
||||
|
|
@ -591,7 +592,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(50)
|
||||
expect(f.height).toBe(30)
|
||||
})
|
||||
|
|
@ -616,7 +617,7 @@ describe('Auto Layout', () => {
|
|||
computeLayout(graph, outer.id)
|
||||
|
||||
const children = graph.getChildren(outer.id)
|
||||
const innerNode = graph.getNode(inner.id)!
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(50)
|
||||
expect(innerNode.height).toBe(50)
|
||||
expect(children[1].x).toBe(0)
|
||||
|
|
@ -663,7 +664,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = graph.getNode(inner.id)!
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(105)
|
||||
expect(innerNode.x).toBe(0)
|
||||
|
||||
|
|
@ -692,7 +693,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = graph.getNode(inner.id)!
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.height).toBe(130)
|
||||
|
||||
const outerChildren = graph.getChildren(outer.id)
|
||||
|
|
@ -723,7 +724,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const middleNode = graph.getNode(middle.id)!
|
||||
const middleNode = getNodeOrThrow(graph, middle.id)
|
||||
expect(middleNode.width).toBe(105)
|
||||
expect(middleNode.height).toBe(30)
|
||||
|
||||
|
|
@ -834,7 +835,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const c = graph.getNode(child.id)!
|
||||
const c = getNodeOrThrow(graph, child.id)
|
||||
expect(c.x).toBe(100)
|
||||
expect(c.y).toBe(100)
|
||||
})
|
||||
|
|
@ -854,7 +855,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(60)
|
||||
expect(f.height).toBe(40)
|
||||
})
|
||||
|
|
@ -890,7 +891,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(10)
|
||||
expect(f.height).toBe(10)
|
||||
})
|
||||
|
|
@ -906,7 +907,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(50)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
|
|
@ -929,7 +930,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(400)
|
||||
expect(f.height).toBe(120)
|
||||
})
|
||||
|
|
@ -949,7 +950,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.height).toBe(110)
|
||||
expect(f.width).toBe(200)
|
||||
})
|
||||
|
|
@ -975,7 +976,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = graph.getNode(inner.id)!
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
// 400 - 100 - 10 = 290
|
||||
expect(innerNode.width).toBe(290)
|
||||
})
|
||||
|
|
@ -1155,9 +1156,9 @@ describe('Auto Layout', () => {
|
|||
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = graph.getNode(text.id)!
|
||||
const updatedArrow1 = graph.getNode(arrow1.id)!
|
||||
const updatedArrow2 = graph.getNode(arrow2.id)!
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
const updatedArrow1 = getNodeOrThrow(graph, arrow1.id)
|
||||
const updatedArrow2 = getNodeOrThrow(graph, arrow2.id)
|
||||
|
||||
expect(updatedText.width).toBe(60)
|
||||
|
||||
|
|
@ -1320,7 +1321,7 @@ describe('Auto Layout', () => {
|
|||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = graph.getNode(text.id)!
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
// Should stretch to 300 - 20 - 20 = 260, NOT stay at 100
|
||||
expect(updatedText.width).toBe(260)
|
||||
})
|
||||
|
|
@ -1354,7 +1355,7 @@ describe('Auto Layout', () => {
|
|||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = graph.getNode(text.id)!
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(300)
|
||||
expect(updatedText.height).toBe(20)
|
||||
})
|
||||
|
|
@ -1395,7 +1396,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
// 400 - 100 - 10 = 290 available for the fill text
|
||||
expect(receivedWidths.length).toBeGreaterThan(0)
|
||||
const updatedText = graph.getNode(text.id)!
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(290)
|
||||
})
|
||||
|
||||
|
|
@ -1429,7 +1430,7 @@ describe('Auto Layout', () => {
|
|||
setTextMeasurer(null)
|
||||
|
||||
expect(measureCalled).toBe(false)
|
||||
const updatedText = graph.getNode(text.id)!
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(150)
|
||||
expect(updatedText.height).toBe(40)
|
||||
})
|
||||
|
|
@ -1512,7 +1513,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = graph.getNode(inner.id)!
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(250)
|
||||
})
|
||||
})
|
||||
|
|
@ -1711,17 +1712,17 @@ describe('Auto Layout', () => {
|
|||
const child = rect(graph, frame.id, 80, 60)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
expect(graph.getNode(child.id)!.width).toBe(80)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
|
||||
graph.updateNode(child.id, { visible: false })
|
||||
computeLayout(graph, frame.id)
|
||||
expect(graph.getNode(child.id)!.width).toBe(80)
|
||||
expect(graph.getNode(child.id)!.height).toBe(60)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
expect(getNodeOrThrow(graph, child.id).height).toBe(60)
|
||||
|
||||
graph.updateNode(child.id, { visible: true })
|
||||
computeLayout(graph, frame.id)
|
||||
expect(graph.getNode(child.id)!.width).toBe(80)
|
||||
expect(graph.getNode(child.id)!.height).toBe(60)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
expect(getNodeOrThrow(graph, child.id).height).toBe(60)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1740,7 +1741,7 @@ describe('Auto Layout', () => {
|
|||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = graph.getNode(frame.id)!
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(100)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { SkiaRenderer } from '#core/canvas'
|
|||
import { SceneGraph } from '#core/scene-graph'
|
||||
import { fontManager } from '#core/text'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { coreSourcePath, publicPath, testPath as repoTestPath } from '#tests/helpers/paths'
|
||||
|
||||
// === CLAIM EXTRACTION ===
|
||||
|
|
@ -75,7 +76,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
// Use a targeted search near the Effect interface definition
|
||||
const effectInterfaceMatch = src.match(/interface Effect\s*\{([^}]+)\}/)
|
||||
expect(effectInterfaceMatch).toBeTruthy()
|
||||
const effectBody = effectInterfaceMatch![1]
|
||||
const effectBody = expectDefined(effectInterfaceMatch, 'effectInterfaceMatch')[1]
|
||||
for (const type of [
|
||||
'DROP_SHADOW',
|
||||
'INNER_SHADOW',
|
||||
|
|
@ -107,7 +108,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
/export function renderShapeUncached[\s\S]*?(?=\nexport function|\n$)/
|
||||
)
|
||||
expect(renderShapeUncachedMatch).toBeTruthy()
|
||||
const body = renderShapeUncachedMatch![0]
|
||||
const body = expectDefined(renderShapeUncachedMatch, 'renderShapeUncachedMatch')[0]
|
||||
|
||||
// Find indices of key calls
|
||||
const behindIdx = body.indexOf("'behind'")
|
||||
|
|
@ -132,7 +133,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
/export function renderNode[\s\S]*?(?=\nexport function|\nexport const)/
|
||||
)
|
||||
expect(renderNodeMatch).toBeTruthy()
|
||||
const body = renderNodeMatch![0]
|
||||
const body = expectDefined(renderNodeMatch, 'renderNodeMatch')[0]
|
||||
|
||||
// Verify opacity saveLayer comes before layerBlur saveLayer
|
||||
const opacityLayerIdx = body.indexOf('opacity < 1')
|
||||
|
|
@ -170,7 +171,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
/function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/
|
||||
)
|
||||
expect(drawTextInnerShadowMatch).toBeTruthy()
|
||||
const body = drawTextInnerShadowMatch![0]
|
||||
const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0]
|
||||
|
||||
// Count saveLayer calls — there are 4 (Master, Restrictive, Blur, DstOut)
|
||||
// plus 2 canvas.save() calls (child transform + offset transform)
|
||||
|
|
@ -199,7 +200,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
/export function renderShape[\s\S]*?(?=\nexport function|\nexport const)/
|
||||
)
|
||||
expect(renderShapeMatch).toBeTruthy()
|
||||
const body = renderShapeMatch![0]
|
||||
const body = expectDefined(renderShapeMatch, 'renderShapeMatch')[0]
|
||||
expect(body).toContain('nodePictureCache')
|
||||
expect(body).toContain('PictureRecorder')
|
||||
})
|
||||
|
|
@ -213,7 +214,7 @@ describe('Doc 01 — The Current Engine: Static Code Claims', () => {
|
|||
/function getShadowShapeChild[\s\S]*?(?=\nfunction |\nexport )/
|
||||
)
|
||||
expect(getShadowShapeChildFn).toBeTruthy()
|
||||
const body = getShadowShapeChildFn![0]
|
||||
const body = expectDefined(getShadowShapeChildFn, 'getShadowShapeChildFn')[0]
|
||||
// No for loop over children
|
||||
expect(body).not.toMatch(/for\s*\(.*childIds/)
|
||||
// Accesses childIds twice: once for .length guard, once for [0] read
|
||||
|
|
@ -243,7 +244,7 @@ describe('Doc 02 — Formula Deconstruction: Static Code Claims', () => {
|
|||
/function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/
|
||||
)
|
||||
expect(drawTextInnerShadowMatch).toBeTruthy()
|
||||
const body = drawTextInnerShadowMatch![0]
|
||||
const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0]
|
||||
|
||||
// 4 saveLayer: Master, Restrictive, Blur, DstOut
|
||||
// 2 save: child transform, offset transform
|
||||
|
|
@ -303,7 +304,7 @@ describe('Doc 03 — Artifact Analysis: Static + Runtime Verification', () => {
|
|||
/function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/
|
||||
)
|
||||
expect(drawTextInnerShadowMatch).toBeTruthy()
|
||||
const body = drawTextInnerShadowMatch![0]
|
||||
const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0]
|
||||
|
||||
// Check the expand calculation
|
||||
expect(body).toContain('const expand')
|
||||
|
|
@ -316,7 +317,7 @@ describe('Doc 03 — Artifact Analysis: Static + Runtime Verification', () => {
|
|||
const src = readSource(scenePath)
|
||||
const match = src.match(/function getShadowShapeChild[\s\S]*?(?=\nfunction |\nexport )/)
|
||||
expect(match).toBeTruthy()
|
||||
const body = match![0]
|
||||
const body = expectDefined(match, 'match')[0]
|
||||
// Verify it returns early after first child
|
||||
expect(body).toContain('return child')
|
||||
// Verify no array accumulation
|
||||
|
|
@ -354,7 +355,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => {
|
|||
void fontPath
|
||||
}
|
||||
|
||||
const surface = ck.MakeSurface(200, 200)!
|
||||
const surface = expectDefined(ck.MakeSurface(200, 200), 'CanvasKit surface')
|
||||
renderer = new SkiaRenderer(ck, surface)
|
||||
renderer.fontProvider = fontProvider
|
||||
renderer.fontsLoaded = true
|
||||
|
|
@ -516,7 +517,7 @@ describe('Doc 01/03 — Runtime Behavior Verification', () => {
|
|||
/function drawTextInnerShadow[\s\S]*?(?=\nfunction |\nexport )/
|
||||
)
|
||||
expect(drawTextInnerShadowMatch).toBeTruthy()
|
||||
const body = drawTextInnerShadowMatch![0]
|
||||
const body = expectDefined(drawTextInnerShadowMatch, 'drawTextInnerShadowMatch')[0]
|
||||
|
||||
// MakeBlend IS called (for tintFilter and solidBlackFilter)
|
||||
expect(body).toContain('MakeBlend')
|
||||
|
|
@ -543,7 +544,7 @@ describe('Doc 01 — LAYER_BLUR Round-Trip Loss Verification', () => {
|
|||
const src = readSource(schemaPath)
|
||||
const effectTypeBlock = src.match(/enum EffectType\s*\{([^}]*)\}/)
|
||||
expect(effectTypeBlock).toBeTruthy()
|
||||
const body = effectTypeBlock![1]
|
||||
const body = expectDefined(effectTypeBlock, 'effectTypeBlock')[1]
|
||||
// Count the values (each is "NAME = N;")
|
||||
const valueCount = (body.match(/=\s*\d+\s*;/g) || []).length
|
||||
expect(valueCount).toBe(4)
|
||||
|
|
@ -576,7 +577,7 @@ describe('Doc 02/03 — Cache Infrastructure Verification', () => {
|
|||
const rendererSrc = readSource(rendererPath)
|
||||
const destroyMatch = rendererSrc.match(/destroy\(\)[\s\S]*?(?=\n \w|\n\})/)
|
||||
expect(destroyMatch).toBeTruthy()
|
||||
const body = destroyMatch![0]
|
||||
const body = expectDefined(destroyMatch, 'destroyMatch')[0]
|
||||
// destroy() delegates to destroyRenderer — verify the lifecycle module cleans caches
|
||||
expect(body).toContain('destroyRenderer')
|
||||
const lifecycleSrc = readSource(lifecyclePath)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test'
|
|||
|
||||
import { SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
function pageId(graph: SceneGraph) {
|
||||
return graph.getPages()[0].id
|
||||
}
|
||||
|
|
@ -16,7 +18,7 @@ describe('SceneGraph', () => {
|
|||
const id = rect(graph, 'Rect', 100, 100, 200, 150)
|
||||
const node = graph.getNode(id)
|
||||
expect(node).toBeDefined()
|
||||
expect(node!.type).toBe('RECTANGLE')
|
||||
expect(expectDefined(node, 'node').type).toBe('RECTANGLE')
|
||||
expect(node.x).toBe(100)
|
||||
expect(node.width).toBe(200)
|
||||
})
|
||||
|
|
@ -146,13 +148,13 @@ describe('SceneGraph', () => {
|
|||
const r = rect(graph, 'R', 100, 200)
|
||||
const node = graph.getNode(r)
|
||||
expect(node).toBeDefined()
|
||||
expect(node!.parentId).toBe(page)
|
||||
expect(expectDefined(node, 'node').parentId).toBe(page)
|
||||
graph.reparentNode(r, page)
|
||||
// Position should not change
|
||||
const after = graph.getNode(r)
|
||||
expect(after).toBeDefined()
|
||||
expect(after!.x).toBe(100)
|
||||
expect(after!.y).toBe(200)
|
||||
expect(expectDefined(after, 'updated node').x).toBe(100)
|
||||
expect(expectDefined(after, 'updated node').y).toBe(200)
|
||||
expect(after.parentId).toBe(page)
|
||||
expect(graph.getChildren(page)).toHaveLength(1)
|
||||
})
|
||||
|
|
@ -176,7 +178,7 @@ describe('SceneGraph', () => {
|
|||
// Now node is inside frame at (200,100), so local coords should be (100, 50)
|
||||
const after = graph.getNode(r)
|
||||
expect(after).toBeDefined()
|
||||
expect(after!.x).toBe(100)
|
||||
expect(expectDefined(after, 'updated node').x).toBe(100)
|
||||
expect(after.y).toBe(50)
|
||||
// Absolute position preserved
|
||||
const absAfter = graph.getAbsolutePosition(r)
|
||||
|
|
@ -245,7 +247,7 @@ describe('SceneGraph', () => {
|
|||
// Inner was at (100,100), frame is at (50,50), so inner's local = (50,50)
|
||||
const innerNode = graph.getNode(inner)
|
||||
expect(innerNode).toBeDefined()
|
||||
expect(innerNode!.x).toBe(50)
|
||||
expect(expectDefined(innerNode, 'inner node').x).toBe(50)
|
||||
expect(innerNode.y).toBe(50)
|
||||
// Child still at (10,10) relative to inner, absolute = (110,110)
|
||||
expect(graph.getAbsolutePosition(child)).toEqual({ x: 110, y: 110 })
|
||||
|
|
@ -326,7 +328,7 @@ describe('SceneGraph', () => {
|
|||
graph.updateNode(id, { x: 200, name: 'Updated' })
|
||||
const node = graph.getNode(id)
|
||||
expect(node).toBeDefined()
|
||||
expect(node!.x).toBe(200)
|
||||
expect(expectDefined(node, 'node').x).toBe(200)
|
||||
expect(node.name).toBe('Updated')
|
||||
})
|
||||
|
||||
|
|
@ -340,7 +342,7 @@ describe('SceneGraph', () => {
|
|||
const child = graph.createNode('RECTANGLE', comp.id, { name: 'BG', width: 100, height: 40 })
|
||||
const instance = graph.createInstance(comp.id, pageId(graph))
|
||||
expect(instance).toBeDefined()
|
||||
expect(instance!.type).toBe('INSTANCE')
|
||||
expect(expectDefined(instance, 'instance').type).toBe('INSTANCE')
|
||||
expect(instance.componentId).toBe(comp.id)
|
||||
const instChildren = graph.getChildren(instance.id)
|
||||
expect(instChildren).toHaveLength(1)
|
||||
|
|
@ -358,7 +360,7 @@ describe('SceneGraph', () => {
|
|||
const label = graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Hello', fontSize: 14 })
|
||||
const instance = graph.createInstance(comp.id, pageId(graph))
|
||||
expect(instance).toBeDefined()
|
||||
const instLabel = graph.getChildren(instance!.id)[0]
|
||||
const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0]
|
||||
expect(instLabel.text).toBe('Hello')
|
||||
|
||||
graph.updateNode(label.id, { text: 'Updated', fontSize: 18 })
|
||||
|
|
@ -378,7 +380,7 @@ describe('SceneGraph', () => {
|
|||
graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Default', fontSize: 14 })
|
||||
const instance = graph.createInstance(comp.id, pageId(graph))
|
||||
expect(instance).toBeDefined()
|
||||
const instLabel = graph.getChildren(instance!.id)[0]
|
||||
const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0]
|
||||
|
||||
// Override the text on the instance child
|
||||
graph.updateNode(instLabel.id, { text: 'Custom' })
|
||||
|
|
@ -403,12 +405,12 @@ describe('SceneGraph', () => {
|
|||
graph.createNode('RECTANGLE', comp.id, { name: 'BG' })
|
||||
const instance = graph.createInstance(comp.id, pageId(graph))
|
||||
expect(instance).toBeDefined()
|
||||
expect(graph.getChildren(instance!.id)).toHaveLength(1)
|
||||
expect(graph.getChildren(expectDefined(instance, 'instance').id)).toHaveLength(1)
|
||||
|
||||
graph.createNode('TEXT', comp.id, { name: 'Label', text: 'New' })
|
||||
graph.syncInstances(comp.id)
|
||||
|
||||
const instChildren = graph.getChildren(instance!.id)
|
||||
const instChildren = graph.getChildren(expectDefined(instance, 'instance').id)
|
||||
expect(instChildren).toHaveLength(2)
|
||||
expect(instChildren[1].name).toBe('Label')
|
||||
expect(instChildren[1].text).toBe('New')
|
||||
|
|
@ -424,10 +426,10 @@ describe('SceneGraph', () => {
|
|||
graph.createNode('RECTANGLE', comp.id, { name: 'BG' })
|
||||
const instance = graph.createInstance(comp.id, pageId(graph))
|
||||
expect(instance).toBeDefined()
|
||||
expect(instance!.type).toBe('INSTANCE')
|
||||
expect(expectDefined(instance, 'instance').type).toBe('INSTANCE')
|
||||
|
||||
graph.detachInstance(instance!.id)
|
||||
expect(instance!.type).toBe('FRAME')
|
||||
graph.detachInstance(expectDefined(instance, 'instance').id)
|
||||
expect(expectDefined(instance, 'instance').type).toBe('FRAME')
|
||||
expect(instance.componentId).toBeNull()
|
||||
expect(graph.getInstances(comp.id)).toHaveLength(0)
|
||||
})
|
||||
|
|
@ -1019,13 +1021,13 @@ describe('updateNode', () => {
|
|||
expect(textNode).toBeDefined()
|
||||
// Simulate a cached textPicture
|
||||
const fakePicture = new Uint8Array([1, 2, 3])
|
||||
textNode!.textPicture = fakePicture
|
||||
expectDefined(textNode, 'text node').textPicture = fakePicture
|
||||
|
||||
// Changing fontSize (a TEXT_PICTURE_KEY) should null the cache
|
||||
graph.updateNode(textId, { fontSize: 24 })
|
||||
const afterUpdate = graph.getNode(textId)
|
||||
expect(afterUpdate).toBeDefined()
|
||||
expect(afterUpdate!.textPicture).toBeNull()
|
||||
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull()
|
||||
})
|
||||
|
||||
test('textPicture survives non-text property change on TEXT node', () => {
|
||||
|
|
@ -1041,13 +1043,13 @@ describe('updateNode', () => {
|
|||
const fakePicture = new Uint8Array([4, 5, 6])
|
||||
const textNode = graph.getNode(textId)
|
||||
expect(textNode).toBeDefined()
|
||||
textNode!.textPicture = fakePicture
|
||||
expectDefined(textNode, 'text node').textPicture = fakePicture
|
||||
|
||||
// Changing opacity (NOT a TEXT_PICTURE_KEY) should preserve textPicture
|
||||
graph.updateNode(textId, { opacity: 0.5 })
|
||||
const afterUpdate = graph.getNode(textId)
|
||||
expect(afterUpdate).toBeDefined()
|
||||
expect(afterUpdate!.textPicture).toBe(fakePicture)
|
||||
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBe(fakePicture)
|
||||
})
|
||||
|
||||
test('textPicture is nulled when textPicture is already null', () => {
|
||||
|
|
@ -1064,7 +1066,7 @@ describe('updateNode', () => {
|
|||
graph.updateNode(textId, { fontSize: 16 })
|
||||
const afterUpdate = graph.getNode(textId)
|
||||
expect(afterUpdate).toBeDefined()
|
||||
expect(afterUpdate!.textPicture).toBeNull()
|
||||
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
vectorNetworkToSVGPaths
|
||||
} from '@open-pencil/core'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
function makeGraph() {
|
||||
const graph = new SceneGraph()
|
||||
graph.createNode('CANVAS', graph.rootId, { name: 'Page 1' })
|
||||
|
|
@ -23,6 +25,19 @@ function exportSVG(graph: SceneGraph, nodeIds: string[], xmlDecl = false): strin
|
|||
return renderNodesToSVG(graph, pageId(graph), nodeIds, { xmlDeclaration: xmlDecl })
|
||||
}
|
||||
|
||||
function exportSVGOrThrow(graph: SceneGraph, nodeIds: string[], xmlDecl = false): string {
|
||||
return expectDefined(exportSVG(graph, nodeIds, xmlDecl), 'SVG output')
|
||||
}
|
||||
|
||||
function renderNodesToSVGOrThrow(
|
||||
graph: SceneGraph,
|
||||
pageId: string,
|
||||
nodeIds: string[],
|
||||
options: Parameters<typeof renderNodesToSVG>[3]
|
||||
): string {
|
||||
return expectDefined(renderNodesToSVG(graph, pageId, nodeIds, options), 'SVG output')
|
||||
}
|
||||
|
||||
// --- SVGNode builder tests ---
|
||||
|
||||
describe('svg() and renderSVGNode()', () => {
|
||||
|
|
@ -210,7 +225,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 50,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('width="100"')
|
||||
expect(result).toContain('height="50"')
|
||||
|
|
@ -226,10 +241,12 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 10,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const withDecl = renderNodesToSVG(graph, pageId(graph), [node.id], { xmlDeclaration: true })!
|
||||
const withoutDecl = renderNodesToSVG(graph, pageId(graph), [node.id], {
|
||||
const withDecl = renderNodesToSVGOrThrow(graph, pageId(graph), [node.id], {
|
||||
xmlDeclaration: true
|
||||
})
|
||||
const withoutDecl = renderNodesToSVGOrThrow(graph, pageId(graph), [node.id], {
|
||||
xmlDeclaration: false
|
||||
})!
|
||||
})
|
||||
expect(withDecl).toStartWith('<?xml')
|
||||
expect(withoutDecl).toStartWith('<svg')
|
||||
})
|
||||
|
|
@ -241,7 +258,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 60,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<ellipse')
|
||||
expect(result).toContain('cx="40"')
|
||||
expect(result).toContain('cy="30"')
|
||||
|
|
@ -264,7 +281,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<line')
|
||||
expect(result).toContain('x2="100"')
|
||||
expect(result).toContain('stroke-width="2"')
|
||||
|
|
@ -279,7 +296,7 @@ describe('renderNodesToSVG()', () => {
|
|||
starInnerRadius: 0.382,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<polygon')
|
||||
expect(result).toContain('points="')
|
||||
})
|
||||
|
|
@ -292,7 +309,7 @@ describe('renderNodesToSVG()', () => {
|
|||
pointCount: 6,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 1, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<polygon')
|
||||
})
|
||||
|
||||
|
|
@ -304,7 +321,7 @@ describe('renderNodesToSVG()', () => {
|
|||
cornerRadius: 16,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('rx="16"')
|
||||
expect(result).toContain('ry="16"')
|
||||
})
|
||||
|
|
@ -322,7 +339,7 @@ describe('renderNodesToSVG()', () => {
|
|||
bottomLeftRadius: 5,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<path')
|
||||
expect(result).toContain('d="M10 0')
|
||||
})
|
||||
|
|
@ -338,7 +355,7 @@ describe('renderNodesToSVG()', () => {
|
|||
fontFamily: 'Inter',
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<text')
|
||||
expect(result).toContain('font-size="18"')
|
||||
expect(result).toContain('font-weight="700"')
|
||||
|
|
@ -357,7 +374,7 @@ describe('renderNodesToSVG()', () => {
|
|||
textAlignHorizontal: 'LEFT',
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('direction="rtl"')
|
||||
expect(result).toContain('text-anchor="end"')
|
||||
expect(result).toContain('x="180"')
|
||||
|
|
@ -377,7 +394,7 @@ describe('renderNodesToSVG()', () => {
|
|||
{ start: 6, length: 4, style: { fontWeight: 700 } }
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<tspan')
|
||||
expect(result).toContain('font-weight="700"')
|
||||
})
|
||||
|
|
@ -390,7 +407,7 @@ describe('renderNodesToSVG()', () => {
|
|||
opacity: 0.5,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('opacity="0.5"')
|
||||
})
|
||||
|
||||
|
|
@ -402,7 +419,7 @@ describe('renderNodesToSVG()', () => {
|
|||
rotation: 45,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('rotate(45,')
|
||||
})
|
||||
|
||||
|
|
@ -422,7 +439,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('stroke-dasharray="5 3"')
|
||||
})
|
||||
|
||||
|
|
@ -443,7 +460,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('stroke-linecap="round"')
|
||||
expect(result).toContain('stroke-linejoin="bevel"')
|
||||
})
|
||||
|
|
@ -463,7 +480,7 @@ describe('renderNodesToSVG()', () => {
|
|||
y: 10,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [frame.id])!
|
||||
const result = exportSVGOrThrow(graph, [frame.id])
|
||||
expect(result).toContain('<g')
|
||||
expect(result).toContain('translate(10, 10)')
|
||||
})
|
||||
|
|
@ -481,7 +498,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 200,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [frame.id])!
|
||||
const result = exportSVGOrThrow(graph, [frame.id])
|
||||
expect(result).toContain('<clipPath')
|
||||
expect(result).toContain('clip-path="url(#clip')
|
||||
})
|
||||
|
|
@ -505,7 +522,7 @@ describe('renderNodesToSVG()', () => {
|
|||
visible: false,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [frame.id])!
|
||||
const result = exportSVGOrThrow(graph, [frame.id])
|
||||
expect(result).toContain('fill="#FF0000"')
|
||||
expect(result).not.toContain('fill="#0000FF"')
|
||||
})
|
||||
|
|
@ -529,7 +546,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<linearGradient')
|
||||
expect(result).toContain('<stop')
|
||||
expect(result).toContain('url(#grad')
|
||||
|
|
@ -554,7 +571,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<radialGradient')
|
||||
expect(result).toContain('url(#grad')
|
||||
})
|
||||
|
|
@ -576,7 +593,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<filter')
|
||||
expect(result).toContain('<feDropShadow')
|
||||
expect(result).toContain('dy="4"')
|
||||
|
|
@ -600,7 +617,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<feGaussianBlur')
|
||||
expect(result).toContain('stdDeviation="5"')
|
||||
})
|
||||
|
|
@ -621,7 +638,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 40,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [a.id, b.id])!
|
||||
const result = exportSVGOrThrow(graph, [a.id, b.id])
|
||||
expect(result).toContain('viewBox="0 0 100 50"')
|
||||
expect(result).toContain('<rect')
|
||||
expect(result).toContain('<ellipse')
|
||||
|
|
@ -635,7 +652,7 @@ describe('renderNodesToSVG()', () => {
|
|||
blendMode: 'MULTIPLY',
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('mix-blend-mode: multiply')
|
||||
})
|
||||
|
||||
|
|
@ -646,7 +663,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 50,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 0.5, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('fill="#FF0000')
|
||||
})
|
||||
|
||||
|
|
@ -664,7 +681,7 @@ describe('renderNodesToSVG()', () => {
|
|||
y: 10,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [comp.id])!
|
||||
const result = exportSVGOrThrow(graph, [comp.id])
|
||||
expect(result).toContain('<rect')
|
||||
expect(result).toContain('translate(10, 10)')
|
||||
})
|
||||
|
|
@ -678,7 +695,7 @@ describe('renderNodesToSVG()', () => {
|
|||
{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.95, a: 1 }, opacity: 1, visible: true }
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [section.id])!
|
||||
const result = exportSVGOrThrow(graph, [section.id])
|
||||
expect(result).toContain('<rect')
|
||||
expect(result).toContain('width="500"')
|
||||
})
|
||||
|
|
@ -691,7 +708,7 @@ describe('renderNodesToSVG()', () => {
|
|||
flipX: true,
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('scale(-1, 1)')
|
||||
})
|
||||
|
||||
|
|
@ -710,7 +727,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('stroke-opacity="0.5"')
|
||||
})
|
||||
|
||||
|
|
@ -735,7 +752,7 @@ describe('renderNodesToSVG()', () => {
|
|||
height: 50,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
})
|
||||
const result = exportSVG(graph, [group.id])!
|
||||
const result = exportSVGOrThrow(graph, [group.id])
|
||||
expect(result).toContain('<rect')
|
||||
expect(result).toContain('fill="#FF0000"')
|
||||
})
|
||||
|
|
@ -757,7 +774,7 @@ describe('renderNodesToSVG()', () => {
|
|||
}
|
||||
]
|
||||
})
|
||||
const result = exportSVG(graph, [node.id])!
|
||||
const result = exportSVGOrThrow(graph, [node.id])
|
||||
expect(result).toContain('<filter')
|
||||
expect(result).toContain('<feGaussianBlur')
|
||||
expect(result).toContain('<feComposite')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { CanvasKit } from 'canvaskit-wasm'
|
|||
|
||||
import { TextEditor, type SceneNode } from '@open-pencil/core'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
const mockCk = {} as CanvasKit
|
||||
|
||||
function createEditor(text = 'Hello World') {
|
||||
|
|
@ -13,6 +15,10 @@ function createEditor(text = 'Hello World') {
|
|||
return { editor, node }
|
||||
}
|
||||
|
||||
function editorState(editor: TextEditor) {
|
||||
return expectDefined(editor.state, 'text editor state')
|
||||
}
|
||||
|
||||
describe('TextEditor', () => {
|
||||
test('start initializes state', () => {
|
||||
const { editor } = createEditor()
|
||||
|
|
@ -45,15 +51,15 @@ describe('TextEditor', () => {
|
|||
|
||||
test('insert at cursor position', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.cursor = 5
|
||||
editorState(editor).cursor = 5
|
||||
editor.insert(' Beautiful', node)
|
||||
expect(editor.state?.text).toBe('Hello Beautiful World')
|
||||
})
|
||||
|
||||
test('insert replaces selection', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.selectionAnchor = 0
|
||||
editor.state!.cursor = 5
|
||||
editorState(editor).selectionAnchor = 0
|
||||
editorState(editor).cursor = 5
|
||||
editor.insert('Goodbye', node)
|
||||
expect(editor.state?.text).toBe('Goodbye World')
|
||||
expect(editor.state?.cursor).toBe(7)
|
||||
|
|
@ -69,15 +75,15 @@ describe('TextEditor', () => {
|
|||
|
||||
test('backspace at start does nothing', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.cursor = 0
|
||||
editorState(editor).cursor = 0
|
||||
editor.backspace(node)
|
||||
expect(editor.state?.text).toBe('Hello World')
|
||||
})
|
||||
|
||||
test('backspace deletes selection', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.selectionAnchor = 6
|
||||
editor.state!.cursor = 11
|
||||
editorState(editor).selectionAnchor = 6
|
||||
editorState(editor).cursor = 11
|
||||
editor.backspace(node)
|
||||
expect(editor.state?.text).toBe('Hello ')
|
||||
expect(editor.state?.cursor).toBe(6)
|
||||
|
|
@ -85,7 +91,7 @@ describe('TextEditor', () => {
|
|||
|
||||
test('delete removes char after cursor', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.cursor = 0
|
||||
editorState(editor).cursor = 0
|
||||
editor.delete(node)
|
||||
expect(editor.state?.text).toBe('ello World')
|
||||
expect(editor.state?.cursor).toBe(0)
|
||||
|
|
@ -99,8 +105,8 @@ describe('TextEditor', () => {
|
|||
|
||||
test('delete removes selection', () => {
|
||||
const { editor, node } = createEditor()
|
||||
editor.state!.selectionAnchor = 0
|
||||
editor.state!.cursor = 5
|
||||
editorState(editor).selectionAnchor = 0
|
||||
editorState(editor).cursor = 5
|
||||
editor.delete(node)
|
||||
expect(editor.state?.text).toBe(' World')
|
||||
})
|
||||
|
|
@ -119,8 +125,8 @@ describe('TextEditor', () => {
|
|||
|
||||
test('moveLeft collapses selection to start', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.selectionAnchor = 3
|
||||
editor.state!.cursor = 8
|
||||
editorState(editor).selectionAnchor = 3
|
||||
editorState(editor).cursor = 8
|
||||
editor.moveLeft()
|
||||
expect(editor.state?.cursor).toBe(3)
|
||||
expect(editor.state?.selectionAnchor).toBeNull()
|
||||
|
|
@ -128,8 +134,8 @@ describe('TextEditor', () => {
|
|||
|
||||
test('moveRight collapses selection to end', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.selectionAnchor = 3
|
||||
editor.state!.cursor = 8
|
||||
editorState(editor).selectionAnchor = 3
|
||||
editorState(editor).cursor = 8
|
||||
editor.moveRight()
|
||||
expect(editor.state?.cursor).toBe(8)
|
||||
expect(editor.state?.selectionAnchor).toBeNull()
|
||||
|
|
@ -145,28 +151,28 @@ describe('TextEditor', () => {
|
|||
test('hasSelection', () => {
|
||||
const { editor } = createEditor()
|
||||
expect(editor.hasSelection()).toBe(false)
|
||||
editor.state!.selectionAnchor = 0
|
||||
editorState(editor).selectionAnchor = 0
|
||||
expect(editor.hasSelection()).toBe(true)
|
||||
})
|
||||
|
||||
test('hasSelection false when anchor equals cursor', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.selectionAnchor = editor.state!.cursor
|
||||
editorState(editor).selectionAnchor = editorState(editor).cursor
|
||||
expect(editor.hasSelection()).toBe(false)
|
||||
})
|
||||
|
||||
test('getSelectionRange', () => {
|
||||
const { editor } = createEditor()
|
||||
expect(editor.getSelectionRange()).toBeNull()
|
||||
editor.state!.selectionAnchor = 8
|
||||
editor.state!.cursor = 3
|
||||
editorState(editor).selectionAnchor = 8
|
||||
editorState(editor).cursor = 3
|
||||
expect(editor.getSelectionRange()).toEqual([3, 8])
|
||||
})
|
||||
|
||||
test('getSelectedText', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.selectionAnchor = 0
|
||||
editor.state!.cursor = 5
|
||||
editorState(editor).selectionAnchor = 0
|
||||
editorState(editor).cursor = 5
|
||||
expect(editor.getSelectedText()).toBe('Hello')
|
||||
})
|
||||
|
||||
|
|
@ -192,14 +198,14 @@ describe('TextEditor', () => {
|
|||
|
||||
test('moveWordRight from start', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.cursor = 0
|
||||
editorState(editor).cursor = 0
|
||||
editor.moveWordRight()
|
||||
expect(editor.state?.cursor).toBe(6)
|
||||
})
|
||||
|
||||
test('moveLeft with extend creates selection', () => {
|
||||
const { editor } = createEditor()
|
||||
editor.state!.cursor = 5
|
||||
editorState(editor).cursor = 5
|
||||
editor.moveLeft(true)
|
||||
expect(editor.state?.selectionAnchor).toBe(5)
|
||||
expect(editor.state?.cursor).toBe(4)
|
||||
|
|
|
|||
Loading…
Reference in a new issue