Skip to content
Merged
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
2 changes: 1 addition & 1 deletion frontend/src/v2/__tests__/V2BillingPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('V2BillingPanel', () => {

test('can defer the section heading to Settings without hiding the current tier', () => {
(useAuth as jest.Mock).mockReturnValue({ currentUser: { entitlements: { pro: true } } });
render(<V2BillingPanel showHeading={false} />);
render(<V2BillingPanel />);

expect(screen.queryByRole('heading', { name: 'Plan' })).not.toBeInTheDocument();
expect(screen.getByText('Pro · $12 a month')).toBeInTheDocument();
Expand Down
29 changes: 24 additions & 5 deletions frontend/src/v2/__tests__/V2SettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jest.mock('../components/V2AvatarCropDialog', () => {
});

jest.mock('../components/V2BillingPanel', () => {
const MockV2BillingPanel = ({ showHeading }: { showHeading?: boolean }) => <div data-show-heading={String(showHeading)}>Plan controls</div>;
const MockV2BillingPanel = () => <div>Plan controls</div>;
MockV2BillingPanel.displayName = 'MockV2BillingPanel';
return MockV2BillingPanel;
});
Expand Down Expand Up @@ -105,15 +105,15 @@ describe('V2SettingsPage', () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();

expect(languageRadioLabels()).toEqual(['English', '中文']);
expect(languageRadioLabels()).toEqual(['EN', '中文']);
});

test('builds the language radios from the registry, and only those', async () => {
await i18nReady;
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();

expect(languageRadioLabels()).toEqual(LANGUAGE_REGISTRY.map(({ label }) => label));
expect(languageRadioLabels()).toEqual(LANGUAGE_REGISTRY.map(({ short }) => short));
});

test('Look carries the language as a segment that switches the app language', async () => {
Expand Down Expand Up @@ -151,7 +151,6 @@ describe('V2SettingsPage', () => {
expect(screen.getByText('lily')).toBeInTheDocument();
expect(screen.getByText('Plan controls')).toBeInTheDocument();
expect(screen.getByText('Device controls')).toBeInTheDocument();
expect(screen.getByText('Plan controls')).toHaveAttribute('data-show-heading', 'false');
expect(screen.getByText('Device controls')).toHaveAttribute('data-show-heading', 'false');
// Developer apps stay hidden unless REACT_APP_DEVELOPER_APPS=true (TASK-674).
expect(screen.queryByText('Connected app controls')).not.toBeInTheDocument();
Expand Down Expand Up @@ -222,12 +221,32 @@ describe('V2SettingsPage', () => {
expect(document.getElementById('v2-settings-profile-edit')).toHaveClass('v2-settings__edit--closed');
});

test('Change photo opens the profile editor', () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();

const photo = screen.getByRole('button', { name: 'Change photo' });
expect(photo).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(photo);
expect(photo).toHaveAttribute('aria-expanded', 'true');
expect(document.getElementById('v2-settings-profile-edit')).not.toHaveClass('v2-settings__edit--closed');
});

test('the avatar and token actions render as the primitive keys the 44px phone rule sizes', () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();

for (const name of ['Regenerate', 'Upload photo']) {
expect(screen.getByRole('button', { name })).toHaveClass('v2p-b');
}
});

test('generates and reveals a new API token without leaving Settings', async () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
(axios.post as jest.Mock).mockResolvedValue({ data: { apiToken: 'cm_user_secret', createdAt: TOKEN_CREATED_AT } });
renderSettings();

fireEvent.click(screen.getByRole('button', { name: 'Generate API token' }));
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));

await waitFor(() => expect(axios.post).toHaveBeenCalledWith('/api/auth/api-token/generate', {}));
expect(await screen.findByText('cm_user_secret')).toBeInTheDocument();
Expand Down
81 changes: 40 additions & 41 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { blockAt, blockContaining } from '../lib/cssBlocks';
const read = (rel: string): string =>
fs.readFileSync(path.join(__dirname, rel), 'utf8');

// Settings keeps its own sheet, lifted from the d215 frame (TASK-688).
const ss = read('../components/V2SettingsPage.css');

// The team's phone block: the `@media (max-width: 760px)` block that carries
// `.v2-team__grid`, wherever it sits in the sheet — not the last one.
const teamPhoneBlock = (css: string): string =>
Expand Down Expand Up @@ -1231,11 +1234,11 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(settingsPage).toContain('<AppsManagement variant="settings" showIntegrations={false} />');
expect(settingsPage).toContain('to="/v2/connectors"');
expect(settingsPage).toContain('<summary>Developer apps</summary>');
expect(settingsPage).toContain('<V2BillingPanel showHeading={false} />');
expect(settingsPage).toContain('<V2BillingPanel />');
expect(settingsPage).toContain('<V2DevicesPanel showHeading={false} asFold />');
expect(ruleBody(v2, '.v2-settings__avatar')).toContain('width: 44px');
expect(ruleBody(v2, '.v2-settings__avatar')).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(v2, '.v2-settings__avatar img')).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(ss, '.v2-settings__avatar')).toContain('width: 44px');
expect(ruleBody(ss, '.v2-settings__avatar')).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(ss, '.v2-settings__avatar img')).toContain('border-radius: var(--v2-radius-sm)');
expect(avatar).toContain("borderRadius: 'inherit'");
expect(v2App).toMatch(/path="settings\/devices"\s+element=\{<Navigate to="\/v2\/settings" replace \/>\}/);
expect(v2App).toMatch(/path="profile"\s+element=\{<Navigate to="\/v2\/settings" replace \/>\}/);
Expand All @@ -1244,12 +1247,21 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(app).toContain('<Route path="/settings/devices" element={<Navigate to="/v2/settings" replace />} />');
});

test('Settings errors never sit inside the value column the phone hides', () => {
const devicesPanel = read('../components/V2DevicesPanel.tsx');
// billing error is its own line under the row, not a child of .vl
expect(billingPanel).toMatch(/<\/span>\s*\{error && <span className="v2-settings__error v2-settings__error--row"/);
expect(billingPanel).not.toMatch(/<span className="vl">[^]*v2-settings__error[^]*<\/span>\s*<span className="ac">/);
expect(ruleBody(ss, '.v2-settings__error--row')).toContain('grid-column: 1 / -1');
// devices: a load failure keeps its summary visible at 390
expect(devicesPanel).toContain("'vl v2-settings__vl--error'");
expect(ss).toMatch(/\.v2-settings \.v2p-disc > summary \.v2-settings__vl--error \{ display: flex/);
});

test('Settings is one screen: grouped cards, no jump nav', () => {
expect(settingsPage).not.toContain('v2-settings__nav');
expect(ruleBody(v2, '.v2-settings__content')).toContain('width: min(100%, 760px)');
expect(ruleBody(v2, '.v2-settings__edit--closed')).toContain('display: none');
expect(ruleBody(v2, '.v2-settings__group,\n.v2-settings__fold')).toContain('overflow: hidden');
expect(ruleBody(v2, '.v2-settings__row > .v2-settings__row-value')).toContain('min-width: 0');
expect(ruleBody(ss, '.v2-settings__content')).toContain('width: min(100%, 760px)');
expect(ruleBody(ss, '.v2-settings__edit--closed')).toContain('display: none');
});

test('Settings saves a display name while allowing handle changes and exposing the email change affordance', () => {
Expand All @@ -1265,42 +1277,33 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(settingsPage).toContain('type="button">Change</button>');
expect(settingsPage).not.toContain('Email changes go through re-verification; ask us for now.');
expect(settingsPage).not.toContain('v2-settings__nav-note');
expect(ruleBody(v2, '.v2-settings__account-fields')).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))');
expect(ruleBody(v2, '.v2-settings__account-fields > label')).toContain('align-content: start');
expect(ruleBody(v2, '.v2-settings__account-fields > .v2-settings__account-email')).toContain('grid-column: 1 / -1');
expect(ruleBody(v2, '.v2-settings__account-fields input')).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(v2, '.v2-settings__email-control')).toContain('display: flex');
const accountForm = ruleBody(v2, '.v2-settings form.v2-settings__account');
expect(ruleBody(ss, '.v2-settings__account-fields')).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))');
expect(ruleBody(ss, '.v2-settings__account-fields > label')).toContain('align-content: start');
expect(ruleBody(ss, '.v2-settings__account-fields > .v2-settings__account-email')).toContain('grid-column: 1 / -1');
expect(ruleBody(ss, '.v2-settings__account-fields input')).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(ss, '.v2-settings__email-control')).toContain('display: flex');
const accountForm = ruleBody(ss, '.v2-settings form.v2-settings__account');
expect(accountForm).toContain('padding: 0');
expect(accountForm).toContain('background: transparent');
expect(accountForm).toContain('box-shadow: none');
});

test('Settings avatar controls keep the account face square and the crop step usable on phones', () => {
const avatar = ruleBody(v2, '.v2-settings__avatar');
const avatar = ruleBody(ss, '.v2-settings__avatar');
expect(avatar).toContain('width: 44px');
expect(avatar).toContain('height: 44px');
expect(avatar).toContain('border-radius: var(--v2-radius-sm)');
expect(ruleBody(v2, '.v2-settings__avatar-overlay')).toContain('background: rgba(16, 24, 40, 0.55)');
expect(ruleBody(ss, '.v2-settings__avatar-overlay')).toContain('background: rgba(16, 24, 40, 0.55)');
expect(avatarCropDialog).toContain('className="v2-root v2-modal__overlay v2-settings__avatar-overlay"');
expect(avatarCropDialog).toContain('className="v2-modal v2-settings__avatar-dialog"');
expect(ruleBody(v2, '.v2-settings__avatar-crop-stage')).toContain('aspect-ratio: 392 / 260');
expect(ruleBody(v2, '.v2-settings__avatar-crop-frame')).toContain('border: 1px solid var(--v2-surface)');
expect(ruleBody(v2, '.v2-settings__avatar-crop-dim span')).toContain('background: rgba(16, 24, 40, 0.55)');
expect(ruleBody(v2, '.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb')).toContain('width: 16px');
expect(ruleBody(v2, '.v2-root .v2-settings__avatar-dialog-actions button')).toContain('min-width: 112px');
const ariaDisabledStyle = selectorRuleBody(
v2,
'.v2-root button.v2-settings__primary[aria-disabled="true"]',
);
expect(ariaDisabledStyle).toContain('cursor: default');
expect(ariaDisabledStyle).toContain('opacity: 0.55');
expect(v2).toContain('.v2-root button.v2-settings__primary:hover:not(:disabled):not([aria-disabled="true"])');
expect(v2).toContain('.v2-root button.v2-settings__secondary:hover:not(:disabled):not([aria-disabled="true"])');

const phone = blockContaining(v2, '@media (max-width: 680px)', '.v2-settings__avatar-actions');
expect(ruleBody(ss, '.v2-settings__avatar-crop-stage')).toContain('aspect-ratio: 392 / 260');
expect(ruleBody(ss, '.v2-settings__avatar-crop-frame')).toContain('border: 1px solid var(--v2-surface)');
expect(ruleBody(ss, '.v2-settings__avatar-crop-dim span')).toContain('background: rgba(16, 24, 40, 0.55)');
expect(ruleBody(ss, '.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb')).toContain('width: 16px');
expect(ruleBody(ss, '.v2-root .v2-settings__avatar-dialog-actions button')).toContain('min-width: 112px');
const phone = blockContaining(ss, '@media (max-width: 680px)', '.v2-settings__avatar-actions');
expect(ruleBody(phone, '.v2-settings__avatar-actions', { within: phone })).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))');
expect(ruleBody(phone, '.v2-root .v2-settings__avatar-actions button.v2-settings__secondary', { within: phone })).toContain('min-height: 44px');
expect(ruleBody(phone, '.v2-settings__avatar-actions .v2p-b', { within: phone })).toContain('min-height: 44px');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
expect(ruleBody(phone, '.v2-settings__avatar-footer', { within: phone })).toContain('flex-direction: column');
expect(ruleBody(phone, '.v2-root .v2-settings__avatar-dialog-actions button', { within: phone })).toContain('min-height: 44px');

Expand All @@ -1325,13 +1328,9 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(billingPanel).toContain("t(isPro ? 'billing.manage' : 'billing.upgrade')");
expect(billingPanel).not.toContain('free in beta');
expect(billingPanel).not.toContain('included during beta');
const badge = ruleBody(v2, '.v2-settings__section .v2-billing__badge');
expect(badge).toContain('background: transparent');
expect(badge).toContain('border: 0');
expect(badge).toContain('padding: 0');
const proBadge = ruleBody(v2, '.v2-settings__section .v2-billing__badge--pro');
expect(proBadge).toContain('background: transparent');
expect(proBadge).toContain('color: var(--v2-text-primary)');
// The frame reads the plan as plain text in the row value, not a chip.
expect(billingPanel).toContain('<span className="v2-settings__plan">');
expect(billingPanel).not.toContain('v2-billing__badge');
});

test('Settings flattens connected apps and folds Developer Tips under the form', () => {
Expand All @@ -1341,8 +1340,8 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(appsManagement).toContain('className="apps-management__developer-tips"');
expect(appsManagement).toContain('{!isSettingsVariant && (');
expect(appsManagement).toContain('? <Box className={className}>{children}</Box>');
expect(ruleBody(v2, '.v2-settings .apps-management--settings .apps-management__section + .apps-management__section')).toContain('border-top: 1px solid var(--v2-border-soft)');
expect(ruleBody(v2, '.v2-settings .apps-management--settings .apps-management__integration')).toContain('border: 1px solid var(--v2-border)');
expect(ruleBody(ss, '.v2-settings .apps-management--settings .apps-management__section + .apps-management__section')).toContain('border-top: 1px solid var(--v2-border-soft)');
expect(ruleBody(ss, '.v2-settings .apps-management--settings .apps-management__integration')).toContain('border: 1px solid var(--v2-border)');
});

test('mention treatment is a tight chip on the handle, never a row wash or rail', () => {
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/v2/__tests__/v2-phone-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from 'path';
// that carry it, the same way v2-layout-invariants.test.ts does.

const css = fs.readFileSync(path.join(__dirname, '../v2.css'), 'utf8');
const settingsCss = fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.css'), 'utf8');
// The block is the tail of the sheet, from its marker comment to the end.
const phone = css.slice(css.indexOf('D223 (TASK-730 shell)'));

Expand Down Expand Up @@ -34,7 +35,7 @@ describe('D223 phone shell', () => {

test('the block comes last in the sheet, so it outranks the earlier phone rules', () => {
const at = css.indexOf('D223 (TASK-730 shell)');
expect(at).toBeGreaterThan(css.indexOf('.v2-settings__layout {'));
expect(at).toBeGreaterThan(css.indexOf('.v2-feature__body {'));
expect(css.indexOf('.v2-feature__body {')).toBeLessThan(at);
});

Expand All @@ -54,11 +55,11 @@ describe('D223 phone shell', () => {
});

test('the settings head spans the pane and only the body scrolls', () => {
expect(phone).toMatch(/\.v2-feature--settings > \.v2-feature__body\s*\{[^}]*padding:\s*0/);
expect(phone).toMatch(/\.v2-feature--settings \.v2-feature__legacy\s*\{[^}]*max-width:\s*none/);
expect(settingsCss).toMatch(/\.v2-feature--settings > \.v2-feature__body\s*\{[^}]*padding:\s*0/);
expect(settingsCss).toMatch(/\.v2-feature--settings \.v2-feature__legacy\s*\{[^}]*max-width:\s*none/);
expect(fs.readFileSync(path.join(__dirname, '../V2App.tsx'), 'utf8')).toContain("'v2-feature--settings'");
expect(fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.tsx'), 'utf8')).toContain('<header className="v2p-ph">');
expect(css).toMatch(/\.v2-settings__content\s*\{[^}]*width:\s*min\(100%, 760px\)/);
expect(phone).toMatch(/\.v2-feature--settings \.v2-settings__layout\s*\{[^}]*overflow-y:\s*auto/);
expect(settingsCss).toMatch(/\.v2-settings__content\s*\{[^}]*width:\s*min\(100%, 760px\)/);
expect(settingsCss).toMatch(/\.v2-feature--settings \.v2-settings__layout\s*\{[^}]*overflow-y:\s*auto/);
});
});
3 changes: 1 addition & 2 deletions frontend/src/v2/__tests__/v2-type-floors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ const ALLOWLIST: string[] = [
'.v2-root button.v2-chat__new-pod-copy', // 11.0px — no-mono-family
// PR3 selectors meet the floor; no remaining allowances.
// PR4
'.v2-billing__badge', // 11.0px — no-mono-family
// TASK-166 took `.v2-byo__mode-kicker` (10.0px, no family) and
// `.v2-byo__preview-label` (11.0px, no family) to mono 500 11px, so both
// entries left this burn-down list rather than being updated in place.
Expand Down Expand Up @@ -191,7 +190,7 @@ describe('v2 type floors (ruling h)', () => {
// `--v2-fs-label: 11px` is consumed by rules that declare no literal size.
// If substitution regressed, those rules would silently drop out of scope.
expect(CSS).toMatch(/--v2-fs-label:\s*11px/);
expect(CSS).toContain('font-size: var(--v2-fs-label)');
expect(CSS + fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.css'), 'utf8')).toContain('font-size: var(--v2-fs-label)');
});

test('a rule inside an @media block is keyed to its own selector', () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/v2/components/V2AvatarCropDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ const V2AvatarCropDialog: React.FC<V2AvatarCropDialogProps> = ({
))}
</div>
<div className="v2-settings__avatar-dialog-actions">
<button ref={cancelButtonRef} type="button" className="v2-settings__secondary" onClick={onCancel} disabled={saving}>
<button ref={cancelButtonRef} type="button" className="v2p-b v2p-b-s v2p-b-sm" onClick={onCancel} disabled={saving}>
{t('settings.avatar.cancel')}
</button>
<button type="button" className="v2-settings__primary" onClick={() => void save()} disabled={!image} aria-disabled={saving}>
<button type="button" className="v2p-b v2p-b-p" onClick={() => void save()} disabled={!image} aria-disabled={saving}>
{saving ? t('settings.avatar.savingPhoto') : t('settings.avatar.savePhoto')}
</button>
</div>
Expand Down
Loading
Loading