Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions apps/ui/src/components/settings-view/account-section.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ vi.mock( '@/hooks/use-color-scheme', () => ( {
useColorScheme: () => 'light',
} ) );

vi.mock( './usage-panel', () => ( {
AiCreditsSection: () => <div data-testid="ai-credits-section" />,
PreviewUsageSection: () => <div data-testid="preview-usage-section" />,
} ) );

const useConnectorMock = vi.mocked( useConnector );
const useAuthUserMock = vi.mocked( useAuthUser );
const useLoginMock = vi.mocked( useLogin );
Expand Down Expand Up @@ -90,27 +95,37 @@ describe( 'AccountSection', () => {
expect( logoutMutate ).toHaveBeenCalledTimes( 1 );
} );

it( 'prompts signed-out users to log in', () => {
it( 'shows preview-site usage only for a signed-in user', () => {
const { rerender } = render( <AccountSection /> );
expect( screen.getByTestId( 'preview-usage-section' ) ).toBeInTheDocument();

useAuthUserMock.mockReturnValue( { data: null, isLoading: false } as never );
rerender( <AccountSection /> );
expect( screen.queryByTestId( 'preview-usage-section' ) ).not.toBeInTheDocument();
} );

it( 'prompts signed-out users with the overview sign-in pitch', () => {
useAuthUserMock.mockReturnValue( { data: null, isLoading: false } as never );

render( <AccountSection /> );

expect( screen.getByText( 'WordPress.com account' ) ).toBeInTheDocument();
expect(
screen.getByText( 'Log in to use AI features and synchronize with live and preview sites.' )
screen.getByRole( 'heading', { name: 'Let Studio code it for you' } )
).toBeInTheDocument();
expect( screen.getByText( /An AI powered WordPress expert/ ) ).toBeInTheDocument();
expect( screen.queryByTestId( 'gravatar' ) ).not.toBeInTheDocument();
expect( screen.queryByTestId( 'ai-credits-section' ) ).not.toBeInTheDocument();
expect( screen.queryByRole( 'button', { name: 'Log out' } ) ).not.toBeInTheDocument();

fireEvent.click( screen.getByRole( 'button', { name: 'Log in' } ) );
fireEvent.click( screen.getByRole( 'button', { name: 'Log in with WordPress.com' } ) );

expect( loginMutate ).toHaveBeenCalledTimes( 1 );
} );

it( 'opens docs and issue links through the connector', () => {
render( <AccountSection /> );

fireEvent.click( screen.getByRole( 'button', { name: 'Docs' } ) );
fireEvent.click( screen.getByRole( 'button', { name: 'Documentation' } ) );

expect( openExternalUrl ).toHaveBeenCalledWith(
'https://developer.wordpress.com/docs/developer-tools/studio/'
Expand All @@ -128,7 +143,7 @@ describe( 'AccountSection', () => {

render( <AccountSection /> );

fireEvent.click( screen.getByRole( 'button', { name: 'Docs' } ) );
fireEvent.click( screen.getByRole( 'button', { name: 'Documentation' } ) );

expect( openExternalUrl ).toHaveBeenCalledWith(
'https://developer.wordpress.com/es/docs/herramientas-para-desarrolladores/studio/'
Expand Down
146 changes: 88 additions & 58 deletions apps/ui/src/components/settings-view/account-section.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { __ } from '@wordpress/i18n';
import { caution, Icon, page } from '@wordpress/icons';
import { Button } from '@wordpress/ui';
import { clsx } from 'clsx';
import { Gravatar } from '@/components/gravatar';
import { useConnector } from '@/data/core';
import { useAuthUser, useLogin, useLogout } from '@/data/queries/use-auth-user';
import { useLogin, useLogout } from '@/data/queries/use-auth-user';
import { useUserLocale } from '@/data/queries/use-user-locale';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { getLocalizedLink, REPORT_ISSUE_URL } from '@/lib/docs-links';
import { usePreviewAuthUser } from './settings-preview';
import styles from './style.module.css';
import { AiCreditsSection, PreviewUsageSection } from './usage-panel';

function AccountHelpActions() {
function AccountHelpLinks() {
const connector = useConnector();
const locale = useUserLocale();

Expand All @@ -18,86 +21,113 @@ function AccountHelpActions() {
};

return (
<div className={ styles.accountActions }>
<Button
<div className={ styles.helpLinks }>
<button
type="button"
variant="minimal"
tone="neutral"
size="small"
className={ styles.helpLink }
aria-label={ __( 'Documentation' ) }
onClick={ () => openLink( getLocalizedLink( locale, 'docsStudio' ) ) }
>
{ __( 'Docs' ) }
</Button>
<Button
<Icon icon={ page } size={ 20 } className={ styles.helpLinkIcon } />
<span
className={ clsx( styles.helpLinkLabel, styles.helpLinkLabelFull ) }
aria-hidden="true"
>
{ __( 'Documentation' ) }
</span>
<span
className={ clsx( styles.helpLinkLabel, styles.helpLinkLabelShort ) }
aria-hidden="true"
>
{ __( 'Docs' ) }
</span>
</button>
<button
type="button"
variant="minimal"
tone="neutral"
size="small"
className={ styles.helpLink }
onClick={ () => openLink( REPORT_ISSUE_URL ) }
>
{ __( 'Report an issue' ) }
</Button>
<Icon icon={ caution } size={ 20 } className={ styles.helpLinkIcon } />
<span className={ styles.helpLinkLabel }>{ __( 'Report an issue' ) }</span>
</button>
</div>
);
}

export function AccountSection() {
const { data: user, isLoading } = useAuthUser();
const { data: user, isLoading } = usePreviewAuthUser();
const login = useLogin();
const logout = useLogout();
const themeIsDark = useColorScheme() === 'dark';

return (
<section className={ styles.preferenceSectionGroup }>
<div className={ styles.accountSectionHeader }>
<h2 className={ clsx( styles.preferenceSectionHeading, styles.accountHeading ) }>
{ __( 'Account' ) }
</h2>
<AccountHelpActions />
</div>
<div className={ styles.accountSummary }>
<div className={ styles.accountIdentity }>
{ user ? (
<Gravatar
email={ user.email }
isDark={ themeIsDark }
className={ styles.accountAvatar }
/>
) : null }
<div className={ styles.accountDetails }>
<h2>{ user ? user.displayName : __( 'WordPress.com account' ) }</h2>
<p>
{ user
? user.email
: __( 'Log in to use AI features and synchronize with live and preview sites.' ) }
</p>
<div className={ clsx( styles.accountAside, user && styles.accountAsideGrid ) }>
{ user ? (
<>
<section className={ clsx( styles.asideSection, styles.accountBlock ) }>
<h2 className={ clsx( styles.asideHeading, styles.visuallyHidden ) }>
{ __( 'Account' ) }
</h2>
<div className={ styles.accountIdentity }>
<Gravatar
email={ user.email }
isDark={ themeIsDark }
className={ styles.accountAvatar }
/>
<div className={ styles.accountDetails }>
<span className={ styles.accountName }>{ user.displayName }</span>
<p className={ styles.accountEmail }>{ user.email }</p>
</div>
</div>
</section>

<div className={ styles.usageGroup }>
<AiCreditsSection />
<PreviewUsageSection userId={ user.id } />
</div>
</div>
{ user ? (
<Button
type="button"
variant="outline"
tone="neutral"
loading={ logout.isPending }
loadingAnnouncement={ __( 'Logging out' ) }
onClick={ () => logout.mutate() }
>
{ __( 'Log out' ) }
</Button>
) : (
</>
) : (
<section className={ styles.asideSection }>
<h2 className={ styles.asideHeading }>{ __( 'Let Studio code it for you' ) }</h2>
<p className={ styles.signinCopy }>
{ __(
'An AI powered WordPress expert that can build a site, theme, or plugin, and help you share and publish.'
) }
</p>
<Button
type="button"
variant="outline"
tone="neutral"
className={ styles.signinButton }
variant="solid"
tone="brand"
size="small"
disabled={ isLoading }
loading={ login.isPending }
loadingAnnouncement={ __( 'Logging in' ) }
onClick={ () => login.mutate() }
>
{ __( 'Log in' ) }
{ __( 'Log in with WordPress.com' ) }
</Button>
) }
</div>
</section>
</section>
) }

{ user ? (
<Button
type="button"
className={ styles.logoutButton }
variant="solid"
size="small"
loading={ logout.isPending }
loadingAnnouncement={ __( 'Logging out' ) }
onClick={ () => logout.mutate() }
>
{ __( 'Log out' ) }
</Button>
) : null }

<section className={ clsx( styles.asideSection, styles.accountHelp ) }>
<h2 className={ styles.asideHeading }>{ __( 'Help' ) }</h2>
<AccountHelpLinks />
</section>
</div>
);
}
57 changes: 55 additions & 2 deletions apps/ui/src/components/settings-view/ai-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useConnector } from '@/data/core';
import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
import { useSaveUserPreferences, useUserPreferences } from '@/data/queries/use-user-preferences';
import { AiPanel } from './ai-panel';

Expand All @@ -26,16 +27,37 @@ vi.mock( '@/data/core', () => ( {
useConnector: vi.fn(),
} ) );

vi.mock( '@/data/queries/use-agentic-features', () => ( {
useAgenticFeatures: vi.fn(),
} ) );

vi.mock( '@/data/queries/use-user-preferences', () => ( {
useUserPreferences: vi.fn(),
useSaveUserPreferences: vi.fn(),
} ) );

vi.mock( './account-section', () => ( {
AccountSection: () => <div data-testid="account-section" />,
} ) );

vi.mock( './studio-code-panel', () => ( {
StudioCodePanel: () => <div data-testid="studio-code-panel" />,
} ) );

vi.mock( './skills-panel', () => ( {
SkillsPanel: () => <div data-testid="skills-panel" />,
} ) );

vi.mock( '@/components/agentic-signin-banner', () => ( {
SigninNotice: () => <div aria-label="Sign in to Studio" />,
} ) );

vi.mock( '@/components/offline-banner', () => ( {
OfflineNotice: () => <div role="status">You&apos;re offline</div>,
} ) );

const useConnectorMock = vi.mocked( useConnector );
const useAgenticFeaturesMock = vi.mocked( useAgenticFeatures );
const useUserPreferencesMock = vi.mocked( useUserPreferences );
const useSaveUserPreferencesMock = vi.mocked( useSaveUserPreferences );

Expand All @@ -59,6 +81,7 @@ describe( 'AiPanel', () => {

beforeEach( () => {
vi.clearAllMocks();
useAgenticFeaturesMock.mockReturnValue( { reason: null } as never );
useSaveUserPreferencesMock.mockReturnValue( { mutate } as never );
mockPreferences( true );
} );
Expand Down Expand Up @@ -89,11 +112,15 @@ describe( 'AiPanel', () => {
expect( mutate ).toHaveBeenCalledWith( { agenticFeaturesEnabled: true } );
} );

it( 'shows the toggle even when the host cannot switch back to the classic UI', () => {
it( 'locks the agentic features toggle and prompts sign-in when signed out', () => {
mockConnector();
useAgenticFeaturesMock.mockReturnValue( { reason: 'signed-out' } as never );
render( <AiPanel /> );

expect( screen.getByRole( 'checkbox', { name: 'Agentic features' } ) ).toBeInTheDocument();
const toggle = screen.getByRole( 'checkbox', { name: 'Agentic features' } );
expect( toggle ).toBeDisabled();
expect( toggle ).not.toBeChecked();
expect( screen.getByText( 'You must log in for agentic features.' ) ).toBeInTheDocument();
} );

it( 'shows the global instructions editor alongside the agentic features toggle', () => {
Expand All @@ -110,4 +137,30 @@ describe( 'AiPanel', () => {

expect( screen.queryByTestId( 'studio-code-panel' ) ).not.toBeInTheDocument();
} );

it( 'shows no sign-in banner when signed out — the pitch lives in the account sidebar', () => {
mockConnector();
useAgenticFeaturesMock.mockReturnValue( { reason: 'signed-out' } as never );
render( <AiPanel /> );

expect( screen.queryByLabelText( 'Sign in to Studio' ) ).not.toBeInTheDocument();
expect( screen.queryByRole( 'status' ) ).not.toBeInTheDocument();
} );

it( 'shows the offline notice when offline', () => {
mockConnector();
useAgenticFeaturesMock.mockReturnValue( { reason: 'offline' } as never );
render( <AiPanel /> );

expect( screen.getByRole( 'status' ) ).toHaveTextContent( "You're offline" );
expect( screen.queryByLabelText( 'Sign in to Studio' ) ).not.toBeInTheDocument();
} );

it( 'shows neither banner when signed in and online', () => {
mockConnector();
render( <AiPanel /> );

expect( screen.queryByLabelText( 'Sign in to Studio' ) ).not.toBeInTheDocument();
expect( screen.queryByRole( 'status' ) ).not.toBeInTheDocument();
} );
} );
Loading
Loading