···11+/**
22+ * Source-level guard for the client:only island loading fallback.
33+ *
44+ * `dashboard.astro` and `editor.astro` mount browser-only React islands
55+ * (`client:only`). Until their JS bundle loads, Astro shows the `slot="fallback"`
66+ * markup. That fallback must carry the durable page chrome — a logo-only header
77+ * (matching AppBar's `status === 'loading'` state) plus a content skeleton — so
88+ * the page has shape before hydration instead of a bare, left-aligned "Loading…".
99+ *
1010+ * Rendering the pages through astro/container isn't viable here (the runner is
1111+ * pinned to jsdom for the WordPress block suites), so these assertions pin the
1212+ * wiring at the source level — same approach as `_index.phase.test.ts`.
1313+ */
1414+import { readFileSync } from 'node:fs';
1515+import { fileURLToPath } from 'node:url';
1616+import { describe, expect, it } from 'vitest';
1717+1818+const read = ( rel: string ) =>
1919+ readFileSync( fileURLToPath( new URL( rel, import.meta.url ) ), 'utf8' );
2020+2121+describe( 'LoadingScene fallback', () => {
2222+ const scene = read( './LoadingScene.astro' );
2323+2424+ it( 'renders the durable logo header using the shared app-bar classes', () => {
2525+ expect( scene ).toMatch( /class="app-bar"/ );
2626+ expect( scene ).toMatch( /class="app-bar__home"/ );
2727+ expect( scene ).toMatch( /class="app-bar__word"/ );
2828+ expect( scene ).toMatch( /SkyPress/ );
2929+ } );
3030+3131+ it( 'omits the dynamic nav + identity (auth state is unknown pre-hydration)', () => {
3232+ expect( scene ).not.toMatch( /app-bar__nav/ );
3333+ expect( scene ).not.toMatch( /app-bar__identity/ );
3434+ expect( scene ).not.toMatch( /app-bar__signout/ );
3535+ } );
3636+3737+ it( 'renders shimmering skeleton placeholders', () => {
3838+ expect( scene ).toMatch( /class="sk\b/ );
3939+ expect( scene ).toMatch( /@keyframes/ );
4040+ } );
4141+4242+ it( 'flags the region as busy and keeps an accessible loading label', () => {
4343+ expect( scene ).toMatch( /aria-busy="true"/ );
4444+ expect( scene ).toMatch( /Loading…/ );
4545+ } );
4646+4747+ it( 'stills the shimmer for reduced-motion users', () => {
4848+ expect( scene ).toMatch( /prefers-reduced-motion/ );
4949+ } );
5050+5151+ it( 'offers distinct dashboard and editor variants', () => {
5252+ expect( scene ).toMatch( /'dashboard'\s*\|\s*'editor'/ );
5353+ expect( scene ).toMatch( /variant === 'dashboard'/ );
5454+ } );
5555+} );
5656+5757+describe( 'island fallbacks use LoadingScene', () => {
5858+ const dashboard = read( '../pages/dashboard.astro' );
5959+ const editor = read( '../pages/editor.astro' );
6060+6161+ it( 'dashboard fallback renders LoadingScene, not a bare "Loading…"', () => {
6262+ expect( dashboard ).toMatch( /import LoadingScene from '[^']*LoadingScene.astro'/ );
6363+ expect( dashboard ).toMatch( /<LoadingScene[\s\S]*?slot="fallback"/ );
6464+ expect( dashboard ).toMatch( /variant="dashboard"/ );
6565+ expect( dashboard ).not.toMatch( /class="dash__loading">Loading…/ );
6666+ } );
6767+6868+ it( 'editor fallback renders LoadingScene, not a bare "Loading…"', () => {
6969+ expect( editor ).toMatch( /import LoadingScene from '[^']*LoadingScene.astro'/ );
7070+ expect( editor ).toMatch( /<LoadingScene[\s\S]*?slot="fallback"/ );
7171+ expect( editor ).toMatch( /variant="editor"/ );
7272+ expect( editor ).not.toMatch( /class="editor-shell__loading">Loading…/ );
7373+ } );
7474+} );
+4-2
src/pages/dashboard.astro
···11---
22import Base from '../layouts/Base.astro';
33import Dashboard from '../components/Dashboard.tsx';
44+import LoadingScene from '../components/LoadingScene.astro';
45// Shared top-bar styles (the Dashboard island is client:only, so Astro scoped
56// styles can't reach its DOM). The sign-in form styles are shared with the editor.
67import '../styles/app-bar.css';
···1011<Base title="Dashboard — SkyPress">
1112 <main class="dash-shell">
1213 <!-- client:only — auth runs only in the browser; its bundle never reaches
1313- reading pages (Decisions 0003 & 0004). No `@wordpress/*` here. -->
1414+ reading pages (Decisions 0003 & 0004). No `@wordpress/*` here. The
1515+ fallback shows the durable header + a skeleton until the island loads. -->
1416 <Dashboard client:only="react">
1515- <p slot="fallback" class="dash__loading">Loading…</p>
1717+ <LoadingScene slot="fallback" variant="dashboard" />
1618 </Dashboard>
1719 </main>
1820</Base>
+4-9
src/pages/editor.astro
···11---
22import Base from '../layouts/Base.astro';
33import Studio from '../components/Studio.tsx';
44+import LoadingScene from '../components/LoadingScene.astro';
45// The Studio is a `client:only` React island, so Astro's scoped styles never
56// reach its DOM — its chrome is styled globally from these shared stylesheets.
67import '../styles/app-bar.css';
···1112<Base title="Write — SkyPress">
1213 <main class="editor-shell">
1314 <!-- client:only — auth + editor run only in the browser; their bundle never
1414- reaches reading pages (Decisions 0001 & 0004). -->
1515+ reaches reading pages (Decisions 0001 & 0004). The fallback shows the
1616+ durable header + a skeleton until the island loads. -->
1517 <Studio client:only="react">
1616- <p slot="fallback" class="editor-shell__loading">Loading…</p>
1818+ <LoadingScene slot="fallback" variant="editor" />
1719 </Studio>
1820 </main>
1921</Base>
2020-2121-<style>
2222- .editor-shell__loading {
2323- padding: 2rem 1.25rem;
2424- color: var(--muted);
2525- }
2626-</style>