openpencil/tests/engine/vue/canvas/render-loop.test.ts

311 lines
8 KiB
TypeScript
Raw Normal View History

import { describe, expect, test } from 'bun:test'
import type { Editor, EditorEvents } from '@open-pencil/core/editor'
2026-05-24 09:15:29 +00:00
import { createCanvasRenderLoop } from '#vue/canvas/surface/render-loop'
type EditorEventName = keyof EditorEvents
type TestEditor = Pick<Editor, 'state' | 'onEditorEvent'>
function createFrameScheduler() {
let nextId = 1
const callbacks = new Map<number, FrameRequestCallback>()
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
const id = nextId++
callbacks.set(id, callback)
return id
}) as typeof requestAnimationFrame
globalThis.cancelAnimationFrame = ((id: number) => {
callbacks.delete(id)
}) as typeof cancelAnimationFrame
return {
get pendingCount() {
return callbacks.size
},
flush() {
const pending = [...callbacks]
callbacks.clear()
for (const [id, callback] of pending) callback(id)
},
restore() {
globalThis.requestAnimationFrame = originalRequestAnimationFrame
globalThis.cancelAnimationFrame = originalCancelAnimationFrame
}
}
}
function createEditor() {
const handlers = new Map<EditorEventName, Set<(...args: never[]) => void>>()
const editor: TestEditor = {
state: {
renderVersion: 0,
selectedIds: new Set<string>()
} as Editor['state'],
onEditorEvent(event, handler) {
const listeners = handlers.get(event) ?? new Set()
listeners.add(handler as (...args: never[]) => void)
handlers.set(event, listeners)
return () => listeners.delete(handler as (...args: never[]) => void)
}
}
return {
editor: editor as Editor,
emit(event: EditorEventName) {
for (const handler of handlers.get(event) ?? []) handler()
}
}
}
describe('canvas render loop', () => {
test('waits for editor events before scheduling renders', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let renders = 0
createCanvasRenderLoop(editor, () => {
renders++
})
expect(scheduler.pendingCount).toBe(0)
emit('repaint:requested')
expect(scheduler.pendingCount).toBe(1)
scheduler.flush()
expect(renders).toBe(1)
expect(scheduler.pendingCount).toBe(0)
} finally {
scheduler.restore()
}
})
test('coalesces multiple editor events into one animation frame', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let renders = 0
createCanvasRenderLoop(editor, () => {
renders++
})
emit('render:requested')
emit('repaint:requested')
emit('selection:changed')
emit('viewport:changed')
expect(scheduler.pendingCount).toBe(1)
scheduler.flush()
expect(renders).toBe(1)
} finally {
scheduler.restore()
}
})
test('scene layers render on repaint but ignore selection events', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let renders = 0
createCanvasRenderLoop(
editor,
() => {
renders++
},
{ layer: 'scene' }
)
emit('selection:changed')
expect(scheduler.pendingCount).toBe(0)
emit('repaint:requested')
expect(scheduler.pendingCount).toBe(1)
scheduler.flush()
expect(renders).toBe(1)
} finally {
scheduler.restore()
}
})
test('overlay layers render on repaint and selection events', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let renders = 0
createCanvasRenderLoop(
editor,
() => {
renders++
},
{ layer: 'overlays' }
)
emit('repaint:requested')
emit('selection:changed')
expect(scheduler.pendingCount).toBe(1)
scheduler.flush()
expect(renders).toBe(1)
} finally {
scheduler.restore()
}
})
test('coalesces multiple canvas surfaces into one animation frame', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let sceneRenders = 0
let overlayRenders = 0
createCanvasRenderLoop(
editor,
() => {
sceneRenders++
},
{ layer: 'scene' }
)
createCanvasRenderLoop(
editor,
() => {
overlayRenders++
},
{ layer: 'overlays' }
)
emit('viewport:changed')
expect(scheduler.pendingCount).toBe(1)
scheduler.flush()
expect(sceneRenders).toBe(1)
expect(overlayRenders).toBe(1)
} finally {
scheduler.restore()
}
})
feat(editor): model split canvas panes (#519) * refactor(editor): separate canvas view state - Classify shared and view-local editor state explicitly - Let canvas surfaces render supplied view state and report their viewport - Preserve the existing one-canvas behavior by default * fix(canvas): preserve loading render state * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * refactor(editor): group state ownership modules - Move shared and view state into the editor state namespace - Model the partition with explicit interfaces and default factories - Derive runtime view keys from the default view object * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * feat(editor): add split canvas views - Render recursive pane layouts with Reka UI splitters and pane-local headers - Route canvas input, selection, viewport state, and close actions to the active pane - Repaint every canvas during shared document previews and cover split lifecycle in tests * fix(editor): clean up inactive pane interactions - Cancel drag, padding preview, and text editing state when pane focus changes - Remove duplicate changelog entries introduced while updating master
2026-08-15 07:31:52 +00:00
test('repaint events dirty every canvas view even when its local version is unchanged', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
const firstView = { ...editor.state, selectedIds: new Set<string>() }
const secondView = { ...editor.state, selectedIds: new Set<string>() }
let firstRenders = 0
let secondRenders = 0
const firstLoop = createCanvasRenderLoop(editor, () => firstRenders++, {
layer: 'scene',
getRenderState: () => firstView
})
const secondLoop = createCanvasRenderLoop(editor, () => secondRenders++, {
layer: 'scene',
getRenderState: () => secondView
})
emit('repaint:requested')
scheduler.flush()
firstLoop.markRendered()
secondLoop.markRendered()
expect([firstRenders, secondRenders]).toEqual([1, 1])
emit('repaint:requested')
scheduler.flush()
expect([firstRenders, secondRenders]).toEqual([2, 2])
} finally {
scheduler.restore()
}
})
test('reads versions and selection from the supplied canvas view state', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
const viewState = {
...editor.state,
renderVersion: 4,
sceneVersion: 2,
selectedIds: new Set(['pane-node'])
}
let renders = 0
const loop = createCanvasRenderLoop(
editor,
() => {
renders++
},
{ getRenderState: () => viewState }
)
emit('repaint:requested')
scheduler.flush()
loop.markRendered()
expect(renders).toBe(1)
emit('repaint:requested')
scheduler.flush()
feat(editor): model split canvas panes (#519) * refactor(editor): separate canvas view state - Classify shared and view-local editor state explicitly - Let canvas surfaces render supplied view state and report their viewport - Preserve the existing one-canvas behavior by default * fix(canvas): preserve loading render state * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * refactor(editor): group state ownership modules - Move shared and view state into the editor state namespace - Model the partition with explicit interfaces and default factories - Derive runtime view keys from the default view object * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * feat(editor): add split canvas views - Render recursive pane layouts with Reka UI splitters and pane-local headers - Route canvas input, selection, viewport state, and close actions to the active pane - Repaint every canvas during shared document previews and cover split lifecycle in tests * fix(editor): clean up inactive pane interactions - Cancel drag, padding preview, and text editing state when pane focus changes - Remove duplicate changelog entries introduced while updating master
2026-08-15 07:31:52 +00:00
expect(renders).toBe(2)
viewState.renderVersion++
emit('repaint:requested')
scheduler.flush()
feat(editor): model split canvas panes (#519) * refactor(editor): separate canvas view state - Classify shared and view-local editor state explicitly - Let canvas surfaces render supplied view state and report their viewport - Preserve the existing one-canvas behavior by default * fix(canvas): preserve loading render state * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * refactor(editor): group state ownership modules - Move shared and view state into the editor state namespace - Model the partition with explicit interfaces and default factories - Derive runtime view keys from the default view object * feat(editor): model split canvas panes - Add pure recursive split-tree operations with validated sizes - Add explicit pane registry and independent view-state cloning - Cap visible panes and cover close and split behavior * feat(editor): add split canvas views - Render recursive pane layouts with Reka UI splitters and pane-local headers - Route canvas input, selection, viewport state, and close actions to the active pane - Repaint every canvas during shared document previews and cover split lifecycle in tests * fix(editor): clean up inactive pane interactions - Cancel drag, padding preview, and text editing state when pane focus changes - Remove duplicate changelog entries introduced while updating master
2026-08-15 07:31:52 +00:00
expect(renders).toBe(3)
} finally {
scheduler.restore()
}
})
feat(app): prepare documents atomically per tab (#592) * feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
2026-08-30 09:21:49 +00:00
test('renders after an injected suspension finishes without a version change', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
feat(app): prepare documents atomically per tab (#592) * feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
2026-08-30 09:21:49 +00:00
const viewState = { ...editor.state }
let suspended = false
let renders = 0
const loop = createCanvasRenderLoop(
editor,
() => {
renders++
},
feat(app): prepare documents atomically per tab (#592) * feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
2026-08-30 09:21:49 +00:00
{
getRenderState: () => viewState,
shouldSuspendRender: () => suspended
}
)
emit('repaint:requested')
scheduler.flush()
loop.markRendered()
feat(app): prepare documents atomically per tab (#592) * feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
2026-08-30 09:21:49 +00:00
suspended = true
emit('repaint:requested')
scheduler.flush()
expect(renders).toBe(1)
feat(app): prepare documents atomically per tab (#592) * feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
2026-08-30 09:21:49 +00:00
suspended = false
scheduler.flush()
expect(renders).toBe(2)
} finally {
scheduler.restore()
}
})
test('cancels pending renders when paused', () => {
const scheduler = createFrameScheduler()
try {
const { editor, emit } = createEditor()
let renders = 0
const loop = createCanvasRenderLoop(editor, () => {
renders++
})
emit('render:requested')
expect(scheduler.pendingCount).toBe(1)
loop.pause()
expect(scheduler.pendingCount).toBe(0)
emit('render:requested')
scheduler.flush()
expect(renders).toBe(0)
} finally {
scheduler.restore()
}
})
})