chore(tests): guard text and keyboard lookups

- Replace non-null assertions in keyboard, text editing, and legacy fig import coverage
- Add explicit store guards and expectDefined for optional test data
- Validate check, legacy fig import, and keyboard E2E coverage
This commit is contained in:
Danila Poyarkov 2026-05-06 10:10:54 +03:00
parent ac5bb4add3
commit eba56fbe38
3 changed files with 80 additions and 38 deletions

View file

@ -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
@ -19,11 +20,19 @@ test.afterAll(async () => {
})
function getActiveTool() {
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.activeTool)
return page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.activeTool
})
}
function getSelectedCount() {
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.selectedIds.size)
return page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.selectedIds.size
})
}
function getPageChildren() {
@ -40,7 +49,19 @@ function getPageChildren() {
}
function getUIVisible() {
return page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.showUI)
return page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.showUI
})
}
function getZoom() {
return page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.zoom
})
}
test.describe('tool switching', () => {
@ -125,11 +146,13 @@ test.describe('z-order shortcuts', () => {
await canvas.drawRect(100, 100, 60, 60)
const childrenBefore = await getPageChildren()
const firstId = childrenBefore[0].id
const firstId = expectDefined(childrenBefore[0], 'first page child').id
// Select the first (bottom) node
await page.evaluate((id) => {
window.__OPEN_PENCIL_STORE__!.select([id])
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, firstId)
await canvas.waitForRender()
@ -142,11 +165,13 @@ test.describe('z-order shortcuts', () => {
test('[ sends to back', async () => {
const children = await getPageChildren()
const lastId = children[children.length - 1].id
const lastId = expectDefined(children.at(-1), 'last page child').id
// Select the last (top) node
await page.evaluate((id) => {
window.__OPEN_PENCIL_STORE__!.select([id])
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
store.select([id])
}, lastId)
await canvas.waitForRender()
@ -169,7 +194,7 @@ test.describe('group/ungroup shortcuts', () => {
const children = await getPageChildren()
const group = children.find((c) => c.type === 'GROUP')
expect(group).toBeTruthy()
expect(group!.childIds.length).toBeGreaterThanOrEqual(2)
expect(group?.childIds.length).toBeGreaterThanOrEqual(2)
})
test('⌘⇧G ungroups', async () => {
@ -229,7 +254,7 @@ test.describe('zoom shortcuts', () => {
await page.keyboard.press('Meta+0')
await canvas.waitForRender()
const zoomAfter = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.zoom)
const zoomAfter = await getZoom()
expect(zoomAfter).toBe(1)
})
@ -237,7 +262,7 @@ test.describe('zoom shortcuts', () => {
await page.keyboard.press('Meta+1')
await canvas.waitForRender()
const zoom = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.zoom)
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
expect(zoom).toBeLessThanOrEqual(1)
})
@ -248,7 +273,7 @@ test.describe('zoom shortcuts', () => {
await page.keyboard.press('Meta+2')
await canvas.waitForRender()
const zoom = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.zoom)
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
expect(zoom).toBeLessThanOrEqual(1)
})
@ -257,7 +282,7 @@ test.describe('zoom shortcuts', () => {
await page.keyboard.press('Shift+1')
await canvas.waitForRender()
const zoom = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.zoom)
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
expect(zoom).toBeLessThanOrEqual(1)
})
@ -268,7 +293,7 @@ test.describe('zoom shortcuts', () => {
await page.keyboard.press('Shift+2')
await canvas.waitForRender()
const zoom = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.zoom)
const zoom = await getZoom()
expect(zoom).toBeGreaterThan(0)
expect(zoom).toBeLessThanOrEqual(1)
})

View file

@ -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
@ -55,7 +56,11 @@ function getPageChildren() {
test('pressing T activates text tool', async () => {
await page.keyboard.press('t')
const tool = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.activeTool)
const tool = await page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.activeTool
})
expect(tool).toBe('TEXT')
})
@ -73,8 +78,7 @@ test('clicking with text tool creates a text node', async () => {
test('text node is selected after creation', async () => {
const node = await getSelectedNode()
expect(node).toBeTruthy()
expect(node!.type).toBe('TEXT')
expect(node?.type).toBe('TEXT')
})
test('typography section appears for text node', async () => {
@ -92,9 +96,8 @@ test('typography section appears for text node', async () => {
test('text node has default font properties', async () => {
const node = await getSelectedNode()
expect(node).toBeTruthy()
expect(node!.fontSize).toBeGreaterThan(0)
expect(node!.fontFamily).toBeTruthy()
expect(node?.fontSize).toBeGreaterThan(0)
expect(node?.fontFamily).toBeTruthy()
})
test('creating text via store works', async () => {
@ -111,8 +114,8 @@ test('creating text via store works', async () => {
await canvas.waitForRender()
const node = await getSelectedNode()
expect(node!.text).toBe('Hello World')
expect(node!.fontSize).toBe(24)
expect(node?.text).toBe('Hello World')
expect(node?.fontSize).toBe(24)
})
test('undo removes text node', async () => {
@ -130,10 +133,10 @@ test('frame tool creates FRAME node', async () => {
await canvas.drag(400, 100, 600, 250)
await canvas.waitForRender()
const node = await getSelectedNode()
expect(node!.type).toBe('FRAME')
expect(node!.width).toBeGreaterThan(0)
expect(node!.height).toBeGreaterThan(0)
const node = expectDefined(await getSelectedNode(), 'selected frame')
expect(node.type).toBe('FRAME')
expect(node.width).toBeGreaterThan(0)
expect(node.height).toBeGreaterThan(0)
canvas.assertNoErrors()
})
@ -154,13 +157,17 @@ test('Enter key opens text editing and selects all without erasing', async () =>
await canvas.waitForRender()
const before = await getSelectedNode()
expect(before!.text).toBe('Keep this text')
expect(before!.type).toBe('TEXT')
expect(before?.text).toBe('Keep this text')
expect(before?.type).toBe('TEXT')
await page.keyboard.press('Enter')
await page.waitForTimeout(200)
const editing = await page.evaluate(() => window.__OPEN_PENCIL_STORE__!.state.editingTextId)
const editing = await page.evaluate(() => {
const store = window.__OPEN_PENCIL_STORE__
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.editingTextId
})
expect(editing).toBe(textId)
const after = await page.evaluate(() => {

View file

@ -2,6 +2,8 @@ import { describe, test, expect } from 'bun:test'
import { importNodeChanges, type NodeChange } from '@open-pencil/core'
import { expectDefined } from '#tests/helpers/assert'
function doc(): NodeChange {
return {
guid: { sessionID: 0, localID: 0 },
@ -125,8 +127,9 @@ describe('fig-import: gradient fills', () => {
expect(n.fills).toHaveLength(1)
expect(n.fills[0].type).toBe('GRADIENT_LINEAR')
expect(n.fills[0].gradientStops).toHaveLength(2)
expect(n.fills[0].gradientStops![0].color.r).toBe(1)
expect(n.fills[0].gradientStops![1].color.b).toBe(1)
const gradientStops = expectDefined(n.fills[0].gradientStops, 'linear gradient stops')
expect(gradientStops[0]?.color.r).toBe(1)
expect(gradientStops[1]?.color.b).toBe(1)
expect(n.fills[0].gradientTransform).toBeDefined()
})
@ -182,7 +185,7 @@ describe('fig-import: image fills', () => {
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.fills[0].type).toBe('IMAGE')
expect(n.fills[0].imageHash).toBeDefined()
expect(n.fills[0].imageHash!.length).toBe(40)
expect(n.fills[0].imageHash?.length).toBe(40)
expect(n.fills[0].imageScaleMode).toBe('FILL')
})
@ -350,9 +353,10 @@ describe('fig-import: arc data', () => {
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.arcData).toBeDefined()
expect(n.arcData!.startingAngle).toBe(0)
expect(n.arcData!.endingAngle).toBeCloseTo(Math.PI)
expect(n.arcData!.innerRadius).toBe(0)
const arcData = expectDefined(n.arcData, 'ellipse arc data')
expect(arcData.startingAngle).toBe(0)
expect(arcData.endingAngle).toBeCloseTo(Math.PI)
expect(arcData.innerRadius).toBe(0)
})
test('donut (inner radius)', () => {
@ -368,7 +372,7 @@ describe('fig-import: arc data', () => {
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.arcData!.innerRadius).toBe(0.5)
expect(n.arcData?.innerRadius).toBe(0.5)
})
})
@ -554,13 +558,19 @@ describe('fig-import: variable asset refs', () => {
})
])
const primary = [...graph.variables.values()].find((v) => v.name === 'Primary')!
const resolved = graph.resolveColorVariable(primary.id)!
const primary = expectDefined(
[...graph.variables.values()].find((v) => v.name === 'Primary'),
'Primary variable'
)
const resolved = expectDefined(graph.resolveColorVariable(primary.id), 'resolved Primary color')
expect(resolved.r).toBeCloseTo(0.14)
expect(resolved.g).toBeCloseTo(0.39)
expect(resolved.b).toBeCloseTo(0.92)
const text = [...graph.getAllNodes()].find((n) => n.type === 'TEXT' && n.text === 'browse')!
const text = expectDefined(
[...graph.getAllNodes()].find((n) => n.type === 'TEXT' && n.text === 'browse'),
'browse text node'
)
expect(text.fills[0].color.r).toBeCloseTo(0.14)
expect(text.fills[0].color.g).toBeCloseTo(0.39)
expect(text.fills[0].color.b).toBeCloseTo(0.92)