deckhearth/test/components/Layout.test.js

91 lines
3.7 KiB
JavaScript
Raw Normal View History

fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) * convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 15:31:37 -04:00
// @vitest-environment jsdom
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, cleanup, screen } from '@testing-library/react';
// Mock next/router so useRouter() does not crash without a RouterContext.
// Layout reads `router.pathname` only; the rest of the surface (`prefetch`,
// `events`, `push`) is for next/link's internals — provide stubs so prefetch
// does not throw when <Link> mounts.
vi.mock('next/router', () => ({
useRouter: () => ({
pathname: '/',
asPath: '/',
query: {},
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn().mockResolvedValue(undefined),
events: { on: vi.fn(), off: vi.fn(), emit: vi.fn() },
}),
}));
// Mock next/link to a plain <a>. The real next/link triggers prefetch on
// mount via the router; bypassing it removes a class of jsdom flake without
// changing the rendered DOM that the assertions inspect.
vi.mock('next/link', () => ({
__esModule: true,
default: ({ href, children, ...rest }) => {
return (
<a href={typeof href === 'string' ? href : ''} {...rest}>
{children}
</a>
);
},
}));
// Mock the theme context so useTheme() does not require a ThemeProvider.
vi.mock('../../lib/theme-context', () => ({
useTheme: () => ({ theme: 'light', toggleTheme: vi.fn() }),
}));
import Layout from '../../components/Layout';
describe('Layout — logged-out rendering (regression: P0 #7)', () => {
afterEach(() => cleanup());
it('does NOT render the maintainer email when no user prop is passed', () => {
const { container } = render(<Layout>page body</Layout>);
expect(container.textContent).not.toContain('me@randallstillwell.com');
});
it('does NOT render the maintainer email when user is null', () => {
const { container } = render(<Layout user={null}>page body</Layout>);
expect(container.textContent).not.toContain('me@randallstillwell.com');
});
it('renders a Sign-in link to /login when user is null', () => {
render(<Layout user={null}>page body</Layout>);
const links = screen.getAllByRole('link', { name: /sign in/i });
expect(links.length).toBeGreaterThanOrEqual(1);
// Both desktop sidebar + mobile drawer render UserProfileDropdown,
// so we expect TWO Sign-in links (one per copy).
for (const link of links) {
expect(link.getAttribute('href')).toBe('/login');
}
});
refactor(design-system): redesign-v2 refinements — tone down active/hover states + dedupe profile + seamless header (#109) Operator feedback after the redesign-v2 epic shipped (PRs #102-#108): the bold ember-gradient pill, the left-shifting hover, the duplicate profile dropdown, and the divider below the header all read too heavy. Four targeted refinements in one PR. 1. Move profile from sidebar bottom → TopSearchBar user-menu chip (top-right). The chip already existed (sub-convoy #3, PR #105); the sidebar's UserProfileDropdown was redundant. Removed from BOTH desktop sidebar and mobile drawer. Kept for logged-out visitors only (the top bar renders null when user is null, so the sidebar still surfaces the auth path via the existing Sign-in CTA branch). 2. Active state: bold ember-gradient pill → 1px ember border on transparent background. - styles/globals.css .nav-item-active: dropped the linear-gradient + 3-stop box-shadow glow. Now: transparent bg, accent-ember text color, inset 0 0 0 1px var(--accent-ember). - Dark theme variant uses a slightly hotter ember (rgb(255,138,80)) for eye-perception correction against the deep-navy substrate. AA contrast measured: 5.4:1 on dark navy bg, 4.6:1 on light cream bg — both pass 4.5:1 normal- text threshold. 3. Hover state: left-shifting border + transform → static transparent ember-tinted background. - Removed `border-left: 3px solid var(--accent-flame)` + `padding-left: calc(1rem - 3px)` on .nav-item-hover:hover (and focus-within). These were causing the 3px-width shift the operator called "movement with the left align." - Removed `transform: translateX(4px)` on .nav-item:hover and .nav-item-bottom:hover — the horizontal-jitter the operator also flagged. - Both classes now apply a flat `background-color: rgba(216, 67, 21, 0.08)` (light) / `rgba(255, 138, 80, 0.10)` (dark) on hover/focus-within with zero geometry shift. 4. TopSearchBar bottom divider removed. - styles change in components/ui/TopSearchBar.js: dropped the `0 1px 0 var(--border)` segment from the box-shadow composition. The rim-light-inner top highlight stays so the bar still reads as elevated chrome against the gradient body, but there's no longer a hairline below — page content flows visually seamlessly out of the header. Test fix: - test/components/Layout.test.js test #4 ("renders the supplied user email") asserted the FULL email `foo@bar.com`. The sidebar UserProfileDropdown used to render that; the TopSearchBar chip renders the username (or email's local-part as fallback) — `'foo'` for `foo@bar.com`. The assertion now checks for `'foo'` + retains the maintainer-email negative check. Renamed the test to "flows the supplied user through to the rendered surface (TopSearchBar chip)" with an inline comment explaining the shift; the three other P0 #7 regression-lock cases are unchanged and still pass. Tests: - npm run test:run: 113/113 - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:45:57 -04:00
it('flows the supplied user through to the rendered surface (TopSearchBar chip)', () => {
// Pre-refinements (2026-06-04), the UserProfileDropdown in the
// sidebar displayed the full email. The redesign-v2 refinements
// moved the user-menu to the TopSearchBar's compact chip in the
// top-right, which displays the username (or the email's local
// part as a fallback) rather than the full address. The
// maintainer-email regression-lock from P0 #7 still passes via
// the three "does NOT render me@randallstillwell.com" cases
// above; this case continues to assert that the user prop FLOWS
// through, just against the new render surface.
fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) * convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 15:31:37 -04:00
const { container } = render(
<Layout user={{ email: 'foo@bar.com', role: 'user' }}>page body</Layout>
);
refactor(design-system): redesign-v2 refinements — tone down active/hover states + dedupe profile + seamless header (#109) Operator feedback after the redesign-v2 epic shipped (PRs #102-#108): the bold ember-gradient pill, the left-shifting hover, the duplicate profile dropdown, and the divider below the header all read too heavy. Four targeted refinements in one PR. 1. Move profile from sidebar bottom → TopSearchBar user-menu chip (top-right). The chip already existed (sub-convoy #3, PR #105); the sidebar's UserProfileDropdown was redundant. Removed from BOTH desktop sidebar and mobile drawer. Kept for logged-out visitors only (the top bar renders null when user is null, so the sidebar still surfaces the auth path via the existing Sign-in CTA branch). 2. Active state: bold ember-gradient pill → 1px ember border on transparent background. - styles/globals.css .nav-item-active: dropped the linear-gradient + 3-stop box-shadow glow. Now: transparent bg, accent-ember text color, inset 0 0 0 1px var(--accent-ember). - Dark theme variant uses a slightly hotter ember (rgb(255,138,80)) for eye-perception correction against the deep-navy substrate. AA contrast measured: 5.4:1 on dark navy bg, 4.6:1 on light cream bg — both pass 4.5:1 normal- text threshold. 3. Hover state: left-shifting border + transform → static transparent ember-tinted background. - Removed `border-left: 3px solid var(--accent-flame)` + `padding-left: calc(1rem - 3px)` on .nav-item-hover:hover (and focus-within). These were causing the 3px-width shift the operator called "movement with the left align." - Removed `transform: translateX(4px)` on .nav-item:hover and .nav-item-bottom:hover — the horizontal-jitter the operator also flagged. - Both classes now apply a flat `background-color: rgba(216, 67, 21, 0.08)` (light) / `rgba(255, 138, 80, 0.10)` (dark) on hover/focus-within with zero geometry shift. 4. TopSearchBar bottom divider removed. - styles change in components/ui/TopSearchBar.js: dropped the `0 1px 0 var(--border)` segment from the box-shadow composition. The rim-light-inner top highlight stays so the bar still reads as elevated chrome against the gradient body, but there's no longer a hairline below — page content flows visually seamlessly out of the header. Test fix: - test/components/Layout.test.js test #4 ("renders the supplied user email") asserted the FULL email `foo@bar.com`. The sidebar UserProfileDropdown used to render that; the TopSearchBar chip renders the username (or email's local-part as fallback) — `'foo'` for `foo@bar.com`. The assertion now checks for `'foo'` + retains the maintainer-email negative check. Renamed the test to "flows the supplied user through to the rendered surface (TopSearchBar chip)" with an inline comment explaining the shift; the three other P0 #7 regression-lock cases are unchanged and still pass. Tests: - npm run test:run: 113/113 - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:45:57 -04:00
expect(container.textContent).toContain('foo');
fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) * convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 15:31:37 -04:00
expect(container.textContent).not.toContain('me@randallstillwell.com');
});
it('does NOT render a "Guest" placeholder when logged out', () => {
// Decision A says the logged-out copy is "Sign in", not "Guest".
// This test prevents a future revert that ships "Guest" as the default
// (which would still hide the maintainer email but skip the CTA).
const { container } = render(<Layout user={null}>page body</Layout>);
expect(container.textContent).not.toContain('Guest');
});
});