openpencil/tests/e2e/fonts/picker.spec.ts
2026-09-05 11:25:33 +03:00

185 lines
6.3 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
async function openTypographyForText(page: Page) {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = store.createShape('TEXT', 120, 120, 240, 40)
store.updateNode(id, { characters: 'Font picker smoke' })
store.select([id])
return id
})
}
async function openFontPicker(page: Page) {
await page.getByTestId('font-picker-trigger').click()
}
async function installGoogleFontsMock(page: Page, families = ['Inter', 'OpenPencil Google Font']) {
await page.addInitScript((googleFamilies) => {
const win = window as Window & {
__googleFontsFetchCount?: number
__googleFontPreviewFetchCount?: number
}
win.__googleFontsFetchCount = 0
win.__googleFontPreviewFetchCount = 0
const originalFetch = window.fetch.bind(window)
window.fetch = async (input, init) => {
let url: string
if (typeof input === 'string') url = input
else if (input instanceof URL) url = input.href
else url = input.url
// css2 font discovery is relayed through the same-origin /__font-proxy in
// the browser fork. Decode the upstream target so the mock stays hermetic;
// deliberately leave the metadata proxy untouched so the fetch counter
// below keeps meaning "the google catalog was loaded from a mock".
if (url.startsWith('/__font-proxy?url=')) {
const target = new URL(url, 'http://local').searchParams.get('url')
if (target) {
const decoded = new URL(decodeURIComponent(target), 'http://local').href
if (decoded.startsWith('https://fonts.googleapis.com/css2')) url = decoded
}
}
if (url.startsWith('https://fonts.openpencil.test/')) {
win.__googleFontPreviewFetchCount = (win.__googleFontPreviewFetchCount ?? 0) + 1
return new Response(new ArrayBuffer(8), { status: 200 })
}
if (url.startsWith('https://fonts.google.com/metadata/fonts')) {
win.__googleFontsFetchCount = (win.__googleFontsFetchCount ?? 0) + 1
return new Response(
JSON.stringify({
familyMetadataList: googleFamilies.map((family) => ({
family,
axes: [],
fonts: { '400': {} }
}))
}),
{ status: 200, headers: { 'content-type': 'application/json' } }
)
}
if (url.startsWith('https://fonts.googleapis.com/css2')) {
const family = new URL(url).searchParams.get('family')?.split(':')[0] ?? 'Inter'
return new Response(
`@font-face { font-family: '${family}'; font-style: normal; font-weight: 400; src: url(https://fonts.openpencil.test/${encodeURIComponent(family)}.ttf) format('truetype'); }`,
{ status: 200, headers: { 'content-type': 'text/css' } }
)
}
return originalFetch(input, init)
}
}, families)
}
test('font picker selects local fonts without browser web-font access', async ({ page }) => {
await installGoogleFontsMock(page)
await page.addInitScript(() => {
Object.defineProperty(window, 'queryLocalFonts', {
configurable: true,
value: async () => [
{
family: 'Inter',
fullName: 'Inter Regular',
postscriptName: 'Inter-Regular',
style: 'Regular'
},
{
family: 'OpenPencil Local Font',
fullName: 'OpenPencil Local Font Regular',
postscriptName: 'OpenPencilLocalFont-Regular',
style: 'Regular'
}
]
})
})
const textId = await openTypographyForText(page)
await openFontPicker(page)
await expect(
page.getByTestId('font-picker-item').filter({ hasText: 'OpenPencil Local Font' })
).toBeVisible()
await page.getByTestId('font-picker-item').filter({ hasText: 'OpenPencil Local Font' }).click()
await expect(page.getByTestId('font-picker-trigger')).toContainText('OpenPencil Local Font')
await expect
.poll(async () =>
page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
const node = store?.graph.getNode(id)
return node?.type === 'TEXT' ? node.fontFamily : null
}, textId)
)
.toBe('OpenPencil Local Font')
expect(
await page.evaluate(
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
)
).toBe(0)
})
test('font picker keeps bundled fonts when local and web fonts are unavailable', async ({
page
}) => {
await installGoogleFontsMock(page)
await page.addInitScript(() => {
Reflect.deleteProperty(window, 'queryLocalFonts')
})
await openTypographyForText(page)
await openFontPicker(page)
await expect(page.getByTestId('font-picker-item').filter({ hasText: 'Inter' })).toBeVisible()
await expect(
page.getByTestId('font-picker-item').filter({ hasText: 'OpenPencil Google Font' })
).toHaveCount(0)
expect(
await page.evaluate(
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
)
).toBe(0)
})
test('font picker keeps bundled fonts when local font permission is rejected', async ({ page }) => {
await installGoogleFontsMock(page)
await page.addInitScript(() => {
Object.defineProperty(window, 'queryLocalFonts', {
configurable: true,
value: async () => {
throw new Error('denied')
}
})
})
await openTypographyForText(page)
await openFontPicker(page)
await expect(page.getByTestId('font-picker-item').filter({ hasText: 'Inter' })).toBeVisible()
await expect(
page.getByTestId('font-picker-item').filter({ hasText: 'OpenPencil Google Font' })
).toHaveCount(0)
expect(
await page.evaluate(
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
)
).toBe(0)
})
test('font picker keeps bundled Inter available when local and Google fonts are unavailable', async ({
page
}) => {
await installGoogleFontsMock(page, [])
await page.addInitScript(() => {
Reflect.deleteProperty(window, 'queryLocalFonts')
})
await openTypographyForText(page)
await openFontPicker(page)
await expect(page.getByTestId('font-picker-item').filter({ hasText: 'Inter' })).toBeVisible()
})