fix(app): restore release validation coverage
- Keep the development MCP bridge alive until Vite shuts down - Ignore empty host-font responses and cover current browser font policy - Update interaction selectors, menu labels, resize accounting, and reviewed UI snapshots
|
|
@ -8,7 +8,7 @@ export function automationPlugin(authToken: string | null, corsOrigin: string):
|
|||
|
||||
return {
|
||||
name: 'open-pencil-automation',
|
||||
configureServer() {
|
||||
configureServer(server) {
|
||||
if (child) return
|
||||
|
||||
child = spawn('bun', ['run', 'packages/mcp/src/index.ts'], {
|
||||
|
|
@ -41,10 +41,11 @@ export function automationPlugin(authToken: string | null, corsOrigin: string):
|
|||
}
|
||||
child = null
|
||||
})
|
||||
},
|
||||
buildEnd() {
|
||||
child?.kill()
|
||||
child = null
|
||||
|
||||
server.httpServer?.once('close', () => {
|
||||
child?.kill()
|
||||
child = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,7 +207,8 @@ async function loadSystemFont(family: string, style = 'Regular'): Promise<ArrayB
|
|||
if (!isTauri()) return null
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const data = await invoke<number[]>('load_system_font', { family, style })
|
||||
const data = await invoke<number[] | null>('load_system_font', { family, style })
|
||||
if (!data?.length) return null
|
||||
return new Uint8Array(data).buffer
|
||||
} catch {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ test('File menu opens and shows items', async () => {
|
|||
const items = await menu.locator('[role="menuitem"]').allTextContents()
|
||||
expect(items.some((t) => t.includes('Open'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Save'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Save As'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Save as'))).toBe(true)
|
||||
|
||||
await editor.page.keyboard.press('Escape')
|
||||
})
|
||||
|
|
@ -47,8 +47,8 @@ test('View menu shows zoom options', async () => {
|
|||
|
||||
const items = await menu.locator('[role="menuitem"]').allTextContents()
|
||||
expect(items.some((t) => t.includes('Zoom to fit'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Zoom In'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Zoom Out'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Zoom in'))).toBe(true)
|
||||
expect(items.some((t) => t.includes('Zoom out'))).toBe(true)
|
||||
|
||||
await editor.page.keyboard.press('Escape')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ async function dragSlider(
|
|||
testId: string,
|
||||
ratio: number
|
||||
) {
|
||||
const slider = page.getByTestId(testId).locator('input[type="range"]')
|
||||
const slider = page.getByTestId(testId).getByRole('slider')
|
||||
const box = await slider.boundingBox()
|
||||
if (!box) throw new Error(`Missing slider: ${testId}`)
|
||||
const y = box.y + box.height / 2
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -39,7 +39,7 @@ test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
|
|||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
await page.evaluate((selectedId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const originalStoreUpdate = store.updateNode.bind(store)
|
||||
|
|
@ -48,11 +48,11 @@ test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
|
|||
let graphUpdateCount = 0
|
||||
let repaintCount = 0
|
||||
store.updateNode = ((nodeId, changes) => {
|
||||
storeUpdateCount++
|
||||
if (nodeId === selectedId) storeUpdateCount++
|
||||
return originalStoreUpdate(nodeId, changes)
|
||||
}) as typeof store.updateNode
|
||||
store.graph.updateNode = ((nodeId, changes) => {
|
||||
graphUpdateCount++
|
||||
if (nodeId === selectedId) graphUpdateCount++
|
||||
return originalGraphUpdate(nodeId, changes)
|
||||
}) as typeof store.graph.updateNode
|
||||
store.onEditorEvent('repaint:requested', () => {
|
||||
|
|
@ -61,7 +61,7 @@ test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
|
|||
Object.assign(window, {
|
||||
__openPencilResizeCounters: () => ({ storeUpdateCount, graphUpdateCount, repaintCount })
|
||||
})
|
||||
})
|
||||
}, id)
|
||||
|
||||
await canvas.drag(340, 240, 420, 290, 12)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,27 +39,32 @@ async function installGoogleFontsMock(page: Page, families = ['Inter', 'OpenPenc
|
|||
win.__googleFontPreviewFetchCount = (win.__googleFontPreviewFetchCount ?? 0) + 1
|
||||
return new Response(new ArrayBuffer(8), { status: 200 })
|
||||
}
|
||||
if (url.startsWith('https://www.googleapis.com/webfonts/v1/webfonts')) {
|
||||
if (!url.includes('family='))
|
||||
win.__googleFontsFetchCount = (win.__googleFontsFetchCount ?? 0) + 1
|
||||
if (url.startsWith('https://fonts.google.com/metadata/fonts')) {
|
||||
win.__googleFontsFetchCount = (win.__googleFontsFetchCount ?? 0) + 1
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
items: googleFamilies.map((family) => ({
|
||||
familyMetadataList: googleFamilies.map((family) => ({
|
||||
family,
|
||||
files: { regular: `https://fonts.openpencil.test/${encodeURIComponent(family)}.ttf` }
|
||||
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 preloads Google fonts and selects local fonts after first-open access', async ({
|
||||
page
|
||||
}) => {
|
||||
test('font picker selects local fonts without browser web-font access', async ({ page }) => {
|
||||
await installGoogleFontsMock(page)
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window, 'queryLocalFonts', {
|
||||
|
|
@ -82,13 +87,6 @@ test('font picker preloads Google fonts and selects local fonts after first-open
|
|||
})
|
||||
|
||||
const textId = await openTypographyForText(page)
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
|
||||
)
|
||||
)
|
||||
.toBe(1)
|
||||
await openFontPicker(page)
|
||||
|
||||
await expect(
|
||||
|
|
@ -106,9 +104,16 @@ test('font picker preloads Google fonts and selects local fonts after first-open
|
|||
}, textId)
|
||||
)
|
||||
.toBe('OpenPencil Local Font')
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
|
||||
)
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
test('font picker lists Google fonts when local font API is unavailable', async ({ page }) => {
|
||||
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')
|
||||
|
|
@ -117,24 +122,18 @@ test('font picker lists Google fonts when local font API is unavailable', async
|
|||
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' })
|
||||
).toBeVisible()
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(window as Window & { __googleFontPreviewFetchCount?: number })
|
||||
.__googleFontPreviewFetchCount
|
||||
)
|
||||
).toHaveCount(0)
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
await expect(page.getByText('Local fonts are not available in this browser.')).toHaveCount(0)
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
test('font picker still lists Google fonts when local font permission is rejected', async ({
|
||||
page
|
||||
}) => {
|
||||
test('font picker keeps bundled fonts when local font permission is rejected', async ({ page }) => {
|
||||
await installGoogleFontsMock(page)
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window, 'queryLocalFonts', {
|
||||
|
|
@ -148,10 +147,15 @@ test('font picker still lists Google fonts when local font permission is rejecte
|
|||
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' })
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('Local font access is blocked for this site.')).toHaveCount(0)
|
||||
).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 ({
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 29 KiB |