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
|
|
|
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
|
|
|
|
|
|
|
|
|
const editor = useEditorSetup('/?test&no-chrome&no-rulers')
|
|
|
|
|
|
|
|
|
|
test('shows staged and determinate document loading progress in the existing canvas overlay', async () => {
|
|
|
|
|
await editor.page.evaluate(() => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
|
|
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
|
|
|
const load = store.preparationController.begin({
|
|
|
|
|
kind: 'document-open',
|
|
|
|
|
phase: 'resolving-fonts',
|
|
|
|
|
subject: 'example.fig'
|
|
|
|
|
})
|
|
|
|
|
load.update({
|
|
|
|
|
phase: 'resolving-fonts',
|
|
|
|
|
detail: 'Geist SemiBold',
|
|
|
|
|
completed: 7,
|
|
|
|
|
total: 12,
|
|
|
|
|
unit: 'fonts'
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const loader = editor.page.getByTestId('canvas-loading')
|
|
|
|
|
await expect(loader).toBeVisible()
|
|
|
|
|
await expect(loader).toHaveAttribute('role', 'status')
|
|
|
|
|
await expect(loader).toContainText('Resolving fonts')
|
|
|
|
|
await expect(loader).toContainText('Geist SemiBold')
|
|
|
|
|
await expect(loader).toContainText('7 of 12')
|
|
|
|
|
await expect(loader.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '58')
|
2026-09-16 08:54:07 +00:00
|
|
|
for (const appearance of ['light', 'dark'] as const) {
|
|
|
|
|
await editor.page.evaluate(async (appearance) => {
|
|
|
|
|
const path = '/src/app/shell/theme.ts'
|
|
|
|
|
const module = await import(path)
|
|
|
|
|
module.useAppTheme().setTheme(appearance)
|
|
|
|
|
}, appearance)
|
|
|
|
|
const mark = loader.locator('img')
|
|
|
|
|
await expect(mark).toHaveAttribute('src', '/brand/app-icon.svg')
|
|
|
|
|
await expect(mark).toHaveAttribute('alt', '')
|
|
|
|
|
await expect(mark).toHaveScreenshot(`loading-brand-${appearance}.png`, {
|
|
|
|
|
maxDiffPixels: 0,
|
|
|
|
|
threshold: 0.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
|
|
|
|
|
|
|
|
await editor.page.evaluate(() => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
|
|
|
|
if (!store) return
|
|
|
|
|
store.preparationController.dispose()
|
|
|
|
|
})
|
|
|
|
|
await expect(loader).toBeHidden()
|
|
|
|
|
})
|