···11+import { describe, it, expect } from 'vitest';
22+import { createElement } from 'react';
33+import { renderToStaticMarkup } from 'react-dom/server';
44+import AppBar from './AppBar';
55+import { AuthContext, type AuthContextValue } from '../lib/auth/AuthProvider';
66+import type { AppBarContext } from '../lib/auth/nav';
77+88+/** Render the AppBar under a hand-rolled auth context (bypasses real OAuth). */
99+function renderBar( current: AppBarContext, auth: Partial< AuthContextValue > ): string {
1010+ const value: AuthContextValue = {
1111+ status: 'signed-out',
1212+ agent: null,
1313+ did: null,
1414+ handle: null,
1515+ displayName: null,
1616+ avatar: null,
1717+ pdsUrl: null,
1818+ error: null,
1919+ signIn: async () => {},
2020+ signOut: async () => {},
2121+ ...auth,
2222+ };
2323+ return renderToStaticMarkup(
2424+ createElement( AuthContext.Provider, { value }, createElement( AppBar, { current } ) )
2525+ );
2626+}
2727+2828+describe( 'AppBar contextual nav visibility', () => {
2929+ it( 'hides the Publications link on the editor when signed out', () => {
3030+ const markup = renderBar( 'editor', { status: 'signed-out' } );
3131+ expect( markup ).not.toContain( 'Publications' );
3232+ expect( markup ).not.toContain( 'href="/dashboard"' );
3333+ } );
3434+3535+ it( 'hides the Write link on the dashboard when signed out', () => {
3636+ const markup = renderBar( 'dashboard', { status: 'signed-out' } );
3737+ expect( markup ).not.toContain( 'Write' );
3838+ expect( markup ).not.toContain( 'href="/editor"' );
3939+ } );
4040+4141+ it( 'hides the contextual nav while auth is still loading', () => {
4242+ const markup = renderBar( 'editor', { status: 'loading' } );
4343+ expect( markup ).not.toContain( 'Publications' );
4444+ } );
4545+4646+ it( 'shows the Publications link on the editor when signed in', () => {
4747+ const markup = renderBar( 'editor', {
4848+ status: 'signed-in',
4949+ agent: {} as never,
5050+ did: 'did:plc:writer',
5151+ handle: 'writer.test',
5252+ } );
5353+ expect( markup ).toContain( 'Publications' );
5454+ expect( markup ).toContain( 'href="/dashboard"' );
5555+ } );
5656+5757+ it( 'shows the Write link on the dashboard when signed in', () => {
5858+ const markup = renderBar( 'dashboard', {
5959+ status: 'signed-in',
6060+ agent: {} as never,
6161+ did: 'did:plc:writer',
6262+ handle: 'writer.test',
6363+ } );
6464+ expect( markup ).toContain( 'Write' );
6565+ expect( markup ).toContain( 'href="/editor"' );
6666+ } );
6767+6868+ it( 'always shows the SkyPress home link, regardless of auth state', () => {
6969+ expect( renderBar( 'editor', { status: 'signed-out' } ) ).toContain( 'SkyPress home' );
7070+ } );
7171+} );
+4-3
src/components/AppBar.tsx
···4444/**
4545 * The shared top bar for the editor + dashboard islands. Logo on the left;
4646 * contextual nav + account + sign-out on the right. Rendered inside AuthProvider
4747- * in every auth state: logo-only while loading, + nav when signed out, + account
4848- * and sign-out when signed in.
4747+ * in every auth state: logo-only while loading or signed out, + contextual nav,
4848+ * account, and sign-out once signed in. The cross-link to Publications / Write is
4949+ * gated on auth so signed-out visitors aren't pointed at editor-only routes.
4950 */
5051export default function AppBar( { current }: { current: AppBarContext } ) {
5152 const { status, handle, displayName, avatar, did, signOut } = useAuth();
···67686869 <span className="app-bar__spacer" />
69707070- { status !== 'loading' && (
7171+ { signedIn && (
7172 <a className="app-bar__nav" href={ nav.href }>
7273 <NavIcon name={ nav.icon } />
7374 { nav.label }