- Add case, justification, vertical alignment, truncation, max-lines, and OpenType controls - Apply visual text case and measured vertical offsets in CanvasKit rendering - Preserve nullable preview values, shared text styles, font demand, and .fig metadata - Add panel, engine, and canvas visual coverage
206 lines
7 KiB
TypeScript
206 lines
7 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
import { getSelectedNode, getEditingTextId, getNodeById } from '#tests/helpers/store'
|
|
|
|
let page: Page
|
|
let canvas: CanvasHelper
|
|
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
page = await browser.newPage()
|
|
await page.goto('/')
|
|
canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
await canvas.clearCanvas()
|
|
|
|
await page.evaluate(() => {
|
|
const store = window.openPencil?.getStore?.()
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
store.state.zoom = 1
|
|
store.state.panX = 0
|
|
store.state.panY = 0
|
|
const id = store.createShape('TEXT', 200, 200, 150, 30)
|
|
store.graph.updateNode(id, { text: 'Hello World', fontSize: 18 })
|
|
store.select([id])
|
|
})
|
|
await canvas.waitForRender()
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
})
|
|
|
|
test.afterAll(async () => {
|
|
await page.close()
|
|
})
|
|
|
|
test('double-click enters text edit mode', async () => {
|
|
await canvas.dblclick(275, 215)
|
|
await page.waitForTimeout(200)
|
|
|
|
const editingId = await getEditingTextId(page)
|
|
expect(editingId).not.toBeNull()
|
|
canvas.assertNoErrors()
|
|
})
|
|
|
|
test('typography controls use compact labeled anatomy', async () => {
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
await canvas.click(275, 215)
|
|
await canvas.waitForRender()
|
|
|
|
const typography = page.getByRole('region', { name: 'Typography' })
|
|
await expect(typography.getByRole('button', { name: 'Font family' })).toBeVisible()
|
|
await expect(typography.getByRole('combobox', { name: 'Font weight' })).toBeVisible()
|
|
await expect(typography.getByRole('spinbutton', { name: 'Font size' })).toBeVisible()
|
|
await expect(typography.getByRole('spinbutton', { name: 'Line height' })).toBeVisible()
|
|
await expect(typography.getByRole('spinbutton', { name: 'Letter spacing' })).toBeVisible()
|
|
await expect(typography.getByRole('combobox', { name: 'Direction' })).toBeVisible()
|
|
await expect(typography.getByRole('combobox', { name: 'Text case' })).toBeVisible()
|
|
await expect(typography.getByRole('combobox', { name: 'Truncation' })).toBeVisible()
|
|
await expect(typography.getByRole('switch', { name: 'Standard ligatures' })).toBeVisible()
|
|
|
|
const alignment = typography.getByRole('group', { name: 'Text alignment' })
|
|
await expect(alignment.getByRole('button', { name: 'Align left' })).toHaveAttribute(
|
|
'aria-pressed',
|
|
'true'
|
|
)
|
|
await alignment.getByRole('button', { name: 'Align center horizontally' }).click()
|
|
await canvas.waitForRender()
|
|
await expect(
|
|
alignment.getByRole('button', { name: 'Align center horizontally' })
|
|
).toHaveAttribute('aria-pressed', 'true')
|
|
|
|
await expect(typography).toHaveScreenshot('typography-panel.png')
|
|
})
|
|
|
|
test('advanced typography controls update case, alignment, truncation, and features', async () => {
|
|
const typography = page.getByRole('region', { name: 'Typography' })
|
|
const nodeId = expectDefined(await getSelectedNode(page), 'selected text node').id
|
|
|
|
const vertical = typography.getByRole('group', { name: 'Vertical text alignment' })
|
|
await vertical.getByRole('button', { name: 'Align bottom' }).click()
|
|
|
|
await typography.getByRole('combobox', { name: 'Text case' }).click()
|
|
await page.getByRole('option', { name: 'Uppercase' }).click()
|
|
|
|
await typography.getByRole('combobox', { name: 'Truncation' }).click()
|
|
await page.getByRole('option', { name: 'Ending ellipsis' }).click()
|
|
const maxLines = typography.getByRole('spinbutton', { name: 'Maximum lines' })
|
|
await maxLines.click()
|
|
await page.keyboard.press('Meta+a')
|
|
await page.keyboard.type('2')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await typography.getByRole('switch', { name: 'Standard ligatures' }).click()
|
|
await canvas.waitForRender()
|
|
|
|
const node = await page.evaluate((id) => {
|
|
const store = window.openPencil?.getStore?.()
|
|
const current = store?.graph.getNode(id)
|
|
return current
|
|
? {
|
|
textAlignVertical: current.textAlignVertical,
|
|
textCase: current.textCase,
|
|
textTruncation: current.textTruncation,
|
|
maxLines: current.maxLines,
|
|
fontFeatures: current.fontFeatures
|
|
}
|
|
: null
|
|
}, nodeId)
|
|
expect(node).toMatchObject({
|
|
textAlignVertical: 'BOTTOM',
|
|
textCase: 'UPPER',
|
|
textTruncation: 'ENDING',
|
|
maxLines: 2
|
|
})
|
|
expect(node?.fontFeatures).toContainEqual({ tag: 'LIGA', enabled: false })
|
|
})
|
|
|
|
test('bold button toggles fontWeight to 700 then back to 400', async () => {
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
await canvas.click(275, 215)
|
|
await canvas.waitForRender()
|
|
|
|
const nodeId = expectDefined(await getSelectedNode(page), 'selected text node').id
|
|
|
|
// ensure starting weight is 400 via undo-safe store method
|
|
await page.evaluate(async (id: string) => {
|
|
const store = window.openPencil?.getStore?.()
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
store.updateNodeWithUndo(id, { fontWeight: 400 }, 'reset')
|
|
store.state.sceneVersion++
|
|
await new Promise(requestAnimationFrame)
|
|
}, nodeId)
|
|
await canvas.waitForRender()
|
|
|
|
const boldBtn = page.getByRole('toolbar', { name: 'Text formatting' }).getByRole('button', {
|
|
name: /^Bold/
|
|
})
|
|
await expect(boldBtn).toBeVisible({ timeout: 3000 })
|
|
await page.waitForTimeout(200)
|
|
await boldBtn.click()
|
|
await page.waitForTimeout(500)
|
|
await canvas.waitForRender()
|
|
|
|
const bold = await getNodeById(page, nodeId)
|
|
expect(bold?.fontWeight).toBe(700)
|
|
|
|
await boldBtn.click()
|
|
await page.waitForTimeout(500)
|
|
await canvas.waitForRender()
|
|
|
|
const normal = await getNodeById(page, nodeId)
|
|
expect(normal?.fontWeight).toBe(400)
|
|
canvas.assertNoErrors()
|
|
})
|
|
|
|
test('Cmd+I toggles italic', async () => {
|
|
await canvas.click(275, 215)
|
|
await canvas.waitForRender()
|
|
const nodeId = expectDefined(await getSelectedNode(page), 'selected text node').id
|
|
|
|
await canvas.dblclick(275, 215)
|
|
await expect.poll(() => getEditingTextId(page), { timeout: 5000 }).toBeTruthy()
|
|
|
|
await page.keyboard.press('End')
|
|
await page.keyboard.press('Meta+i')
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
|
|
const node = await getNodeById(page, nodeId)
|
|
expect(node?.italic).toBe(true)
|
|
canvas.assertNoErrors()
|
|
})
|
|
|
|
test('double-click word select changes canvas screenshot', async () => {
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
|
|
const baseline = await canvas.screenshotCanvas()
|
|
|
|
await canvas.dblclick(275, 215)
|
|
await page.waitForTimeout(200)
|
|
|
|
const selected = await canvas.screenshotCanvas()
|
|
|
|
expect(Buffer.compare(baseline, selected)).not.toBe(0)
|
|
await canvas.pressKey('Escape')
|
|
await canvas.waitForRender()
|
|
canvas.assertNoErrors()
|
|
})
|
|
|
|
test('Alt+ArrowRight word navigation stays in text edit mode', async () => {
|
|
await canvas.dblclick(275, 215)
|
|
await page.waitForTimeout(200)
|
|
|
|
await page.keyboard.press('Alt+ArrowRight')
|
|
await canvas.waitForRender()
|
|
|
|
const editingId = await getEditingTextId(page)
|
|
expect(editingId).not.toBeNull()
|
|
canvas.assertNoErrors()
|
|
})
|