1c681df5d3
Fixes the three DESIGN-SPEC Part 4 + #17 audit findings. 1. React #418 hydration mismatch (landing → dashboard entry). The `maybeSignedIn` value was computed in a useState INITIALIZER that reads localStorage during render: server (no window) → false → emits the marketing tree; a signed-in visitor's first CLIENT render → true → emits the loading placeholder. Whole-subtree server/client mismatch → React discarded and re-rendered the page. Deferred behind a mounted flag so the first client render matches the server; the stored-session check flips post-mount. SSR HTML is no longer discarded. 2. Loading walls → skeletons. New tokenized Skeleton primitive (.vyndr-skeleton, reduced-motion-safe via the global rule). Swapped into every text-wall loader: dashboard slate load ("Loading the slate…"), /desk ("Assembling the pack…"), /ledger ("Loading…"), scan ("Loading the model…"), and the landing redirect placeholder. No bare text loader remains. 3. scan→ledger persistence. Root cause: the scan page read its bearer token from localStorage['sb-token'] — a key written ONLY by the OAuth callback — so email/password users posted /api/scan anonymously and the ledger write (gated on an authed user) was silently skipped. Now uses the authoritative session.access_token (matching the ledger read path). Extracted the row builder to web/src/lib/ledgerRow.js (shared, testable). Tests: +17 (scanLedgerPersistence write→mine round-trip + scope + idempotency; ds1SpeedTrust hydration/skeleton/persistence source invariants). Full suite 233 suites / 2793 green; web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.9 KiB
JavaScript
97 lines
3.9 KiB
JavaScript
/**
|
|
* DS1 (DESIGN-SPEC §4 + §17) — source invariants for the Speed + Trust bugs.
|
|
* - React #418 hydration: no client-only value rendered in the SSR path
|
|
* without a mounted guard.
|
|
* - Loading walls: every named loader is a skeleton, not a text wall.
|
|
* - Persistence: the scan write uses the authoritative session token.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
|
|
describe('DS1 — React #418 hydration safety (§4, audit #5)', () => {
|
|
const landing = read('app/page.tsx');
|
|
|
|
test('landing no longer reads localStorage inside a useState initializer', () => {
|
|
// The exact antipattern that caused #418: the client-only stored-session
|
|
// check ran during render, so server (false) and client (true) diverged.
|
|
expect(landing).not.toMatch(/useState<boolean>\(\s*\(\)\s*=>\s*hasStoredSession\(\)\s*\)/);
|
|
expect(landing).not.toMatch(/useState\(\s*\(\)\s*=>\s*hasStoredSession/);
|
|
});
|
|
|
|
test('the client-only value is deferred behind a mounted flag', () => {
|
|
// Server + first client render both compute maybeSignedIn from mounted=false.
|
|
expect(landing).toMatch(/const\s+\[mounted,\s*setMounted\]\s*=\s*useState\(false\)/);
|
|
expect(landing).toMatch(/setMounted\(true\)/);
|
|
expect(landing).toMatch(/maybeSignedIn\s*=\s*mounted\s*&&\s*hasStoredSession\(\)/);
|
|
});
|
|
|
|
test('hasStoredSession still guards against server-side window access', () => {
|
|
expect(landing).toMatch(/typeof window === 'undefined'/);
|
|
});
|
|
});
|
|
|
|
describe('DS1 — loading walls are skeletons, never text (§4, audit #5)', () => {
|
|
const files = {
|
|
'dashboard slate load': 'app/dashboard/page.tsx',
|
|
'/desk assembling': 'app/desk/page.tsx',
|
|
'/ledger loading': 'app/ledger/page.tsx',
|
|
'scan loading': 'app/scan/page.tsx',
|
|
'landing redirect': 'app/page.tsx',
|
|
};
|
|
const BANNED = [
|
|
'Loading the slate',
|
|
'Assembling the pack',
|
|
'Loading the model',
|
|
/<p[^>]*>\s*Loading…\s*<\/p>/, // the bare ledger text loader
|
|
];
|
|
|
|
for (const [name, rel] of Object.entries(files)) {
|
|
test(`${name} contains no text-wall loader`, () => {
|
|
const src = read(rel);
|
|
for (const b of BANNED) {
|
|
if (typeof b === 'string') expect(src).not.toContain(b);
|
|
else expect(src).not.toMatch(b);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('each loader path renders the shared Skeleton', () => {
|
|
for (const rel of ['app/dashboard/page.tsx', 'app/desk/page.tsx', 'app/ledger/page.tsx', 'app/scan/page.tsx', 'app/page.tsx']) {
|
|
const src = read(rel);
|
|
expect(src).toMatch(/Skeleton/);
|
|
expect(src).toMatch(/from '@\/components\/vyndr'/);
|
|
}
|
|
});
|
|
|
|
test('the Skeleton primitive is tokenized (no raw hex) and reduced-motion-safe', () => {
|
|
const skel = read('components/vyndr/Skeleton.tsx');
|
|
// Uses the CSS class, no inline colors.
|
|
expect(skel).toMatch(/vyndr-skeleton/);
|
|
expect(skel).not.toMatch(/#[0-9a-fA-F]{3,8}/);
|
|
const css = fs.readFileSync(path.join(WEB, 'app', 'globals.css'), 'utf8');
|
|
expect(css).toMatch(/\.vyndr-skeleton\s*\{/);
|
|
expect(css).toMatch(/var\(--bg-surface\)/);
|
|
// The global prefers-reduced-motion rule freezes every animation.
|
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/);
|
|
});
|
|
});
|
|
|
|
describe('DS1 — scan→ledger persistence uses the authoritative token (§17)', () => {
|
|
const scan = read('app/scan/page.tsx');
|
|
|
|
test('scan pulls the bearer token from the live Supabase session', () => {
|
|
expect(scan).toMatch(/session,/); // destructured from useAuth
|
|
expect(scan).toMatch(/session\?\.access_token/);
|
|
});
|
|
|
|
test('scan no longer relies solely on the OAuth-only legacy localStorage key', () => {
|
|
// The stale key may remain only as a FALLBACK after the session token.
|
|
const m = scan.match(/session\?\.access_token\s*\|\|[\s\S]{0,120}sb-token/);
|
|
expect(m).toBeTruthy();
|
|
});
|
|
});
|