refactor: scope browser test instrumentation and global types (#667)
This commit is contained in:
parent
77739c42a4
commit
a779edd08a
|
|
@ -139,7 +139,11 @@ Private tooling belongs under `tools/<domain>/{src,tests}`, with kebab-case doma
|
|||
- Use `@/` for app cross-directory imports. Package aliases are `#vue/*`, `#cli/*`, `#dom-css/*`, `#mcp/*`, and `#core/*`; prefer clear relative imports nearby.
|
||||
- No `any`, non-null assertions, or `Math.random()`; use precise types, guards, and `crypto.getRandomValues()`.
|
||||
- Reuse named types and primitives from `@open-pencil/scene-graph`; do not respell `Color`, `Vector`, `SceneNode`, `Effect`, `Fill`, or `Stroke` shapes.
|
||||
- Window API declarations belong in `src/global.d.ts` or `packages/core/src/global.d.ts`.
|
||||
- Window API augmentations belong in the owning compilation boundary: app declarations in `src/global.d.ts`, package DOM gaps in the owning package's `global.d.ts`, and native-test declarations in `tests/helpers/tauri/native-global.d.ts`. Never put `declare global` in specs or implementation modules. Include canonical declarations through tsconfig instead of duplicating them.
|
||||
- Keep app API contracts named and owned by their implementation domain; declaration files import those types. Derive vendor API types from top-level type imports rather than hand-copying signatures. Optional runtime globals remain optional and require a runtime guard.
|
||||
- Native tests centralize invocation in a guarded test helper using vendor-derived types; do not import packages inside serialized WebView callbacks or repeat direct Tauri-global access in specs. Never expand production Window declarations just to accommodate test fixtures.
|
||||
- Prefer test-runner-owned fixtures and request/route counters over browser globals. For in-page performance instrumentation, return a scoped `JSHandle` from `evaluateHandle()`; restore patched methods/listeners and dispose the handle in `finally`. Handles do not survive navigation. Assert transient DOM state with locators before the interaction ends when possible. Do not create a catch-all test Window interface or add ad-hoc counter properties to window.
|
||||
- In Bun tests, prefer injected dependencies or scoped spies with explicit cleanup. `mock.restore()` restores spies but does not undo `mock.module()` overrides; do not assume module mocks are isolated by cleanup hooks. Read the installed runner's current lifecycle/mocking docs before introducing global or module-level instrumentation.
|
||||
- Use `culori` for color conversion and existing dependencies before custom implementations.
|
||||
- Prefer VueUse for common browser, event, focus, clipboard, storage, and timer behavior, but keep one-shot rAF or explicit service-owned timers when clearer.
|
||||
- Components must not hold module-level mutable state. Share repeated logic/constants rather than copying it.
|
||||
|
|
|
|||
12
packages/vue/src/global.d.ts
vendored
12
packages/vue/src/global.d.ts
vendored
|
|
@ -1,15 +1,3 @@
|
|||
interface FontData {
|
||||
family: string
|
||||
fullName: string
|
||||
postscriptName: string
|
||||
style: string
|
||||
blob(): Promise<Blob>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
queryLocalFonts?(): Promise<FontData[]>
|
||||
}
|
||||
|
||||
interface GestureEvent extends UIEvent {
|
||||
scale: number
|
||||
rotation: number
|
||||
|
|
|
|||
|
|
@ -28,12 +28,6 @@ export interface OpenPencilWindowAPI {
|
|||
test?: OpenPencilTestHooks
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
openPencil?: OpenPencilWindowAPI
|
||||
}
|
||||
}
|
||||
|
||||
let activeStore: EditorStore | null = null
|
||||
|
||||
function windowAPI(): OpenPencilWindowAPI {
|
||||
|
|
|
|||
19
src/global.d.ts
vendored
19
src/global.d.ts
vendored
|
|
@ -1,13 +1,8 @@
|
|||
import type { OpenPencilWindowAPI } from './app/browser-bridge'
|
||||
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface GestureEvent extends UIEvent {
|
||||
scale: number
|
||||
rotation: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
}
|
||||
|
||||
interface FilePickerAcceptType {
|
||||
description: string
|
||||
accept: Record<string, string[]>
|
||||
|
|
@ -20,17 +15,9 @@ declare global {
|
|||
}
|
||||
|
||||
interface Window {
|
||||
openPencil?: OpenPencilWindowAPI
|
||||
showOpenFilePicker?(options?: FilePickerOptions): Promise<FileSystemFileHandle[]>
|
||||
showSaveFilePicker?(options?: FilePickerOptions): Promise<FileSystemFileHandle>
|
||||
queryLocalFonts?(): Promise<
|
||||
{
|
||||
family: string
|
||||
fullName: string
|
||||
style: string
|
||||
postscriptName: string
|
||||
blob(): Promise<Blob>
|
||||
}[]
|
||||
>
|
||||
mockWindowOpen?(url: string): void
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { measureInteraction } from '#tests/helpers/performance/interaction'
|
||||
|
||||
test('dragging a nested card uses repaint-only position previews', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
|
@ -46,48 +47,7 @@ test('dragging a nested card uses repaint-only position previews', async ({ page
|
|||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const originalStoreUpdate = store.updateNode.bind(store)
|
||||
const originalGraphUpdate = store.graph.updateNode.bind(store.graph)
|
||||
let storeUpdateCount = 0
|
||||
let graphUpdateCount = 0
|
||||
let repaintCount = 0
|
||||
store.updateNode = ((id, changes) => {
|
||||
storeUpdateCount++
|
||||
return originalStoreUpdate(id, changes)
|
||||
}) as typeof store.updateNode
|
||||
store.graph.updateNode = ((id, changes) => {
|
||||
graphUpdateCount++
|
||||
return originalGraphUpdate(id, changes)
|
||||
}) as typeof store.graph.updateNode
|
||||
store.onEditorEvent('repaint:requested', () => {
|
||||
repaintCount++
|
||||
})
|
||||
Object.assign(window, {
|
||||
__openPencilDragCounters: () => ({
|
||||
storeUpdateCount,
|
||||
graphUpdateCount,
|
||||
repaintCount
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await canvas.drag(110, 110, 210, 150, 20)
|
||||
|
||||
const counters = await page.evaluate(() => {
|
||||
const getCounters = (
|
||||
window as typeof window & {
|
||||
__openPencilDragCounters?: () => {
|
||||
storeUpdateCount: number
|
||||
graphUpdateCount: number
|
||||
repaintCount: number
|
||||
}
|
||||
}
|
||||
).__openPencilDragCounters
|
||||
return getCounters?.() ?? null
|
||||
})
|
||||
const counters = await measureInteraction(page, () => canvas.drag(110, 110, 210, 150, 20))
|
||||
|
||||
expect(counters?.storeUpdateCount).toBeLessThanOrEqual(1)
|
||||
expect(counters?.graphUpdateCount).toBeLessThanOrEqual(25)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { measureInteraction } from '#tests/helpers/performance/interaction'
|
||||
|
||||
test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
|
@ -39,44 +40,7 @@ test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
|
|||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate((selectedId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const originalStoreUpdate = store.updateNode.bind(store)
|
||||
const originalGraphUpdate = store.graph.updateNode.bind(store.graph)
|
||||
let storeUpdateCount = 0
|
||||
let graphUpdateCount = 0
|
||||
let repaintCount = 0
|
||||
store.updateNode = ((nodeId, changes) => {
|
||||
if (nodeId === selectedId) storeUpdateCount++
|
||||
return originalStoreUpdate(nodeId, changes)
|
||||
}) as typeof store.updateNode
|
||||
store.graph.updateNode = ((nodeId, changes) => {
|
||||
if (nodeId === selectedId) graphUpdateCount++
|
||||
return originalGraphUpdate(nodeId, changes)
|
||||
}) as typeof store.graph.updateNode
|
||||
store.onEditorEvent('repaint:requested', () => {
|
||||
repaintCount++
|
||||
})
|
||||
Object.assign(window, {
|
||||
__openPencilResizeCounters: () => ({ storeUpdateCount, graphUpdateCount, repaintCount })
|
||||
})
|
||||
}, id)
|
||||
|
||||
await canvas.drag(340, 240, 420, 290, 12)
|
||||
|
||||
const counters = await page.evaluate(() => {
|
||||
const getCounters = (
|
||||
window as typeof window & {
|
||||
__openPencilResizeCounters?: () => {
|
||||
storeUpdateCount: number
|
||||
graphUpdateCount: number
|
||||
repaintCount: number
|
||||
}
|
||||
}
|
||||
).__openPencilResizeCounters
|
||||
return getCounters?.() ?? null
|
||||
})
|
||||
const counters = await measureInteraction(page, () => canvas.drag(340, 240, 420, 290, 12), id)
|
||||
const node = await page.evaluate((nodeId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { mockGoogleFonts } from '#tests/helpers/fonts/google'
|
||||
|
||||
async function openTypographyForText(page: Page) {
|
||||
await page.goto('/')
|
||||
|
|
@ -25,51 +26,8 @@ async function searchFonts(page: Page, query: string) {
|
|||
await page.getByRole('combobox', { name: 'Search fonts…' }).fill(query)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
const fonts = await mockGoogleFonts(page)
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window, 'queryLocalFonts', {
|
||||
configurable: true,
|
||||
|
|
@ -109,17 +67,13 @@ test('font picker selects local fonts without browser web-font access', async ({
|
|||
}, textId)
|
||||
)
|
||||
.toBe('OpenPencil Local Font')
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => (window as Window & { __googleFontsFetchCount?: number }).__googleFontsFetchCount
|
||||
)
|
||||
).toBe(0)
|
||||
expect(fonts.counts.metadata).toBe(0)
|
||||
})
|
||||
|
||||
test('font picker keeps bundled fonts when local and web fonts are unavailable', async ({
|
||||
page
|
||||
}) => {
|
||||
await installGoogleFontsMock(page)
|
||||
const fonts = await mockGoogleFonts(page)
|
||||
await page.addInitScript(() => {
|
||||
Reflect.deleteProperty(window, 'queryLocalFonts')
|
||||
})
|
||||
|
|
@ -134,15 +88,11 @@ test('font picker keeps bundled fonts when local and web fonts are unavailable',
|
|||
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)
|
||||
expect(fonts.counts.metadata).toBe(0)
|
||||
})
|
||||
|
||||
test('font picker keeps bundled fonts when local font permission is rejected', async ({ page }) => {
|
||||
await installGoogleFontsMock(page)
|
||||
const fonts = await mockGoogleFonts(page)
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window, 'queryLocalFonts', {
|
||||
configurable: true,
|
||||
|
|
@ -162,17 +112,13 @@ test('font picker keeps bundled fonts when local font permission is rejected', a
|
|||
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)
|
||||
expect(fonts.counts.metadata).toBe(0)
|
||||
})
|
||||
|
||||
test('font picker keeps bundled Inter available when local and Google fonts are unavailable', async ({
|
||||
page
|
||||
}) => {
|
||||
await installGoogleFontsMock(page, [])
|
||||
await mockGoogleFonts(page, [])
|
||||
await page.addInitScript(() => {
|
||||
Reflect.deleteProperty(window, 'queryLocalFonts')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,19 +10,6 @@ async function dragLayerAndObserveIndicator(
|
|||
targetId: string,
|
||||
targetPosition: Vector
|
||||
) {
|
||||
await page.evaluate(() => {
|
||||
const positions: string[] = []
|
||||
new MutationObserver(() => {
|
||||
for (const element of document.querySelectorAll<HTMLElement>(
|
||||
'[data-slot="drop-indicator"]'
|
||||
)) {
|
||||
const position = element.dataset.dropPosition
|
||||
if (position) positions.push(position)
|
||||
}
|
||||
}).observe(document.body, { subtree: true, childList: true, attributes: true })
|
||||
Object.assign(window, { __layerDropPositions: positions })
|
||||
})
|
||||
|
||||
const source = page.locator(`[data-node-id="${sourceId}"]`)
|
||||
const target = page.locator(`[data-node-id="${targetId}"]`)
|
||||
const sourceBox = await source.boundingBox()
|
||||
|
|
@ -35,12 +22,11 @@ async function dragLayerAndObserveIndicator(
|
|||
await page.mouse.move(targetBox.x + targetPosition.x, targetBox.y + targetPosition.y, {
|
||||
steps: 20
|
||||
})
|
||||
await expect(target.locator('[data-slot="drop-indicator"]')).toBeVisible()
|
||||
const indicator = target.locator('[data-slot="drop-indicator"]')
|
||||
await expect(indicator).toBeVisible()
|
||||
const position = await indicator.getAttribute('data-drop-position')
|
||||
await page.mouse.up()
|
||||
|
||||
return page.evaluate(
|
||||
() => (window as typeof window & { __layerDropPositions?: string[] }).__layerDropPositions ?? []
|
||||
)
|
||||
return position
|
||||
}
|
||||
|
||||
test('layer reorder exposes a visible drop indicator before dropping', async ({ page }) => {
|
||||
|
|
@ -67,7 +53,7 @@ test('layer reorder exposes a visible drop indicator before dropping', async ({
|
|||
y: 2
|
||||
})
|
||||
|
||||
expect(positions).toContain('above')
|
||||
expect(positions).toBe('above')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -98,6 +84,6 @@ test('layer child drop exposes a visible container highlight before dropping', a
|
|||
y: 12
|
||||
})
|
||||
|
||||
expect(positions).toContain('child')
|
||||
expect(positions).toBe('child')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'
|
|||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { seedLargeDocument } from '#tests/helpers/large-document'
|
||||
import { measureHitTesting } from '#tests/helpers/performance/hit-testing'
|
||||
|
||||
type TimingSummary = {
|
||||
hitTestMissMs: number
|
||||
|
|
@ -31,36 +32,13 @@ test.describe.serial('large-document performance', () => {
|
|||
const fixture = await seedLargeDocument(page, nodeCount)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const originalHitTest = store.graph.hitTest.bind(store.graph)
|
||||
let calls = 0
|
||||
let totalMs = 0
|
||||
store.graph.hitTest = ((...args) => {
|
||||
const startedAt = performance.now()
|
||||
const result = originalHitTest(...args)
|
||||
totalMs += performance.now() - startedAt
|
||||
calls++
|
||||
return result
|
||||
}) as typeof store.graph.hitTest
|
||||
Object.assign(window, {
|
||||
__largeDocumentPointerProfile: () => ({ calls, totalMs })
|
||||
})
|
||||
})
|
||||
const bounds = await canvas.canvas.boundingBox()
|
||||
if (!bounds) throw new Error('Canvas bounds unavailable')
|
||||
await page.mouse.move(bounds.x + 10, bounds.y + 10)
|
||||
await page.mouse.move(bounds.x + bounds.width - 10, bounds.y + bounds.height - 10, {
|
||||
steps: 40
|
||||
})
|
||||
const pointerProfile = await page.evaluate(() => {
|
||||
const profile = (
|
||||
window as typeof window & {
|
||||
__largeDocumentPointerProfile?: () => { calls: number; totalMs: number }
|
||||
}
|
||||
).__largeDocumentPointerProfile
|
||||
return profile?.() ?? { calls: 0, totalMs: 0 }
|
||||
const pointerProfile = await measureHitTesting(page, async () => {
|
||||
await page.mouse.move(bounds.x + 10, bounds.y + 10)
|
||||
await page.mouse.move(bounds.x + bounds.width - 10, bounds.y + bounds.height - 10, {
|
||||
steps: 40
|
||||
})
|
||||
})
|
||||
|
||||
const result = await page.evaluate((profile): Promise<TimingSummary> => {
|
||||
|
|
|
|||
30
tests/helpers/fonts/google.ts
Normal file
30
tests/helpers/fonts/google.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
/** Route-owned counts live in the runner, not on window or in a patched fetch. */
|
||||
export async function mockGoogleFonts(page: Page, families = ['Inter', 'OpenPencil Google Font']) {
|
||||
const counts = { metadata: 0, previews: 0 }
|
||||
const pattern =
|
||||
/^https:\/\/(fonts\.openpencil\.test\/|fonts\.google\.com\/metadata\/fonts|fonts\.googleapis\.com\/css2)/
|
||||
async function handle(route: Route) {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.hostname === 'fonts.openpencil.test') {
|
||||
counts.previews++
|
||||
await route.fulfill({ status: 200, body: Buffer.alloc(8) })
|
||||
} else if (url.hostname === 'fonts.google.com') {
|
||||
counts.metadata++
|
||||
await route.fulfill({
|
||||
json: {
|
||||
familyMetadataList: families.map((family) => ({ family, axes: [], fonts: { '400': {} } }))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const family = url.searchParams.get('family')?.split(':')[0] ?? 'Inter'
|
||||
await route.fulfill({
|
||||
contentType: 'text/css',
|
||||
body: `@font-face { font-family: '${family}'; font-style: normal; font-weight: 400; src: url(https://fonts.openpencil.test/${encodeURIComponent(family)}.ttf) format('truetype'); }`
|
||||
})
|
||||
}
|
||||
}
|
||||
await page.route(pattern, handle)
|
||||
return { counts, dispose: () => page.unroute(pattern, handle) }
|
||||
}
|
||||
34
tests/helpers/performance/hit-testing.ts
Normal file
34
tests/helpers/performance/hit-testing.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
export async function measureHitTesting(page: Page, run: () => Promise<void>) {
|
||||
const probe = await page.evaluateHandle(() => {
|
||||
const graph = window.openPencil?.getStore?.().graph
|
||||
if (!graph) throw new Error('OpenPencil graph not initialized')
|
||||
const original = graph.hitTest
|
||||
let calls = 0
|
||||
let totalMs = 0
|
||||
graph.hitTest = (...args) => {
|
||||
const start = performance.now()
|
||||
const result = original.apply(graph, args)
|
||||
totalMs += performance.now() - start
|
||||
calls++
|
||||
return result
|
||||
}
|
||||
return {
|
||||
read: () => ({ calls, totalMs }),
|
||||
dispose: () => {
|
||||
graph.hitTest = original
|
||||
}
|
||||
}
|
||||
})
|
||||
try {
|
||||
await run()
|
||||
return await probe.evaluate((probe) => probe.read())
|
||||
} finally {
|
||||
try {
|
||||
await probe.evaluate((probe) => probe.dispose())
|
||||
} finally {
|
||||
await probe.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
45
tests/helpers/performance/interaction.ts
Normal file
45
tests/helpers/performance/interaction.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
/** Own the instrumentation and remote handle for exactly one interaction. */
|
||||
export async function measureInteraction(
|
||||
page: Page,
|
||||
run: () => Promise<void>,
|
||||
selectedId?: string
|
||||
) {
|
||||
const probe = await page.evaluateHandle((selectedId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const update = store.updateNode
|
||||
const graphUpdate = store.graph.updateNode
|
||||
const counts = { storeUpdateCount: 0, graphUpdateCount: 0, repaintCount: 0 }
|
||||
store.updateNode = (id, changes) => {
|
||||
if (!selectedId || id === selectedId) counts.storeUpdateCount++
|
||||
return update.call(store, id, changes)
|
||||
}
|
||||
store.graph.updateNode = (id, changes) => {
|
||||
if (!selectedId || id === selectedId) counts.graphUpdateCount++
|
||||
return graphUpdate.call(store.graph, id, changes)
|
||||
}
|
||||
const unsubscribe = store.onEditorEvent('repaint:requested', () => {
|
||||
counts.repaintCount++
|
||||
})
|
||||
return {
|
||||
read: () => ({ ...counts }),
|
||||
dispose() {
|
||||
store.updateNode = update
|
||||
store.graph.updateNode = graphUpdate
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
}, selectedId)
|
||||
try {
|
||||
await run()
|
||||
return await probe.evaluate((probe) => probe.read())
|
||||
} finally {
|
||||
try {
|
||||
await probe.evaluate((probe) => probe.dispose())
|
||||
} finally {
|
||||
await probe.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,12 +57,10 @@ export async function installTauriClipboardMock(page: Page): Promise<TauriClipbo
|
|||
})
|
||||
|
||||
await page.addInitScript((bindingName) => {
|
||||
const invoke = (
|
||||
globalThis as typeof globalThis & {
|
||||
[key: string]: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
|
||||
}
|
||||
)[bindingName]
|
||||
if (!invoke) throw new Error(`Playwright binding not found: ${bindingName}`)
|
||||
const binding: unknown = Reflect.get(globalThis, bindingName)
|
||||
if (typeof binding !== 'function')
|
||||
throw new Error(`Playwright binding not found: ${bindingName}`)
|
||||
const invoke = binding as (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
|
||||
|
||||
let callbackId = 1
|
||||
const callbacks = new Map<number, unknown>()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { mockIPC } from '@tauri-apps/api/mocks'
|
||||
|
||||
const windowLike = globalThis as typeof globalThis & {
|
||||
__TAURI_INTERNALS__?: unknown
|
||||
__TAURI_EVENT_PLUGIN_INTERNALS__?: unknown
|
||||
}
|
||||
Object.assign(globalThis, { window: windowLike })
|
||||
import { installTauriMockWindow } from './mocks'
|
||||
|
||||
installTauriMockWindow()
|
||||
|
||||
mockIPC((cmd, args) => {
|
||||
if (cmd !== 'build_fig_file') throw new Error(`Unexpected command: ${cmd}`)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
export function installTauriMockWindow() {
|
||||
const windowLike = globalThis as typeof globalThis & {
|
||||
__TAURI_INTERNALS__?: unknown
|
||||
__TAURI_EVENT_PLUGIN_INTERNALS__?: unknown
|
||||
}
|
||||
Object.assign(globalThis, { window: windowLike })
|
||||
Object.assign(globalThis, { window: globalThis })
|
||||
}
|
||||
|
||||
export async function mockTauriIPC(
|
||||
|
|
@ -26,5 +22,5 @@ export async function clearTauriMocks() {
|
|||
clearMocks()
|
||||
Reflect.deleteProperty(window, '__TAURI_INTERNALS__')
|
||||
Reflect.deleteProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__')
|
||||
delete (globalThis as typeof globalThis & { window?: unknown }).window
|
||||
Reflect.deleteProperty(globalThis, 'window')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,9 @@
|
|||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue"
|
||||
"src/**/*.vue",
|
||||
"packages/core/src/global.d.ts",
|
||||
"packages/vue/src/global.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue