mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
feat(settings): group settings menu into labeled sections (#1286)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useCoreState } from '../../providers/CoreStateProvider';
|
||||
@@ -15,6 +15,29 @@ import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
interface SettingsSection {
|
||||
label: string;
|
||||
items: SettingsItem[];
|
||||
}
|
||||
|
||||
interface SettingsItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
dangerous?: boolean;
|
||||
}
|
||||
|
||||
// Subtle uppercase section header label separating settings groups
|
||||
const SectionHeader = ({ label }: { label: string }) => (
|
||||
<div className="px-4 pt-5 pb-1">
|
||||
<span className="text-[10px] font-semibold tracking-widest uppercase text-stone-400">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SettingsHome = () => {
|
||||
const navigate = useNavigate();
|
||||
const { navigateToSettings } = useSettingsNavigation();
|
||||
@@ -93,157 +116,185 @@ const SettingsHome = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// const handleViewEncryptionKey = () => {
|
||||
// // TODO: Show encryption key in a secure modal
|
||||
// console.log('View encryption key');
|
||||
// };
|
||||
|
||||
const groupedMenuItems = [
|
||||
const settingsSections: SettingsSection[] = [
|
||||
{
|
||||
id: 'account',
|
||||
title: 'Account',
|
||||
description: 'Recovery phrase, team, connections, and privacy',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('account'),
|
||||
dangerous: false,
|
||||
label: 'General',
|
||||
items: [
|
||||
{
|
||||
id: 'account',
|
||||
title: 'Account',
|
||||
description: 'Recovery phrase, team, connections, and privacy',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('account'),
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
description: 'Do Not Disturb and per-account notification controls',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('notifications'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing & Usage',
|
||||
description: 'Subscription plan, credits, and payment methods',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H5a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => {
|
||||
void openUrl(BILLING_DASHBOARD_URL);
|
||||
},
|
||||
dangerous: false,
|
||||
label: 'Features & AI',
|
||||
items: [
|
||||
{
|
||||
id: 'features',
|
||||
title: 'Features',
|
||||
description: 'Screen awareness, messaging, and tools',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('features'),
|
||||
},
|
||||
{
|
||||
id: 'ai-models',
|
||||
title: 'AI & Models',
|
||||
description: 'Local AI model setup and downloads',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('ai-models'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
title: 'Features',
|
||||
description: 'Screen awareness, messaging, and tools',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('features'),
|
||||
dangerous: false,
|
||||
label: 'Billing & Rewards',
|
||||
items: [
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing & Usage',
|
||||
description: 'Subscription plan, credits, and payment methods',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H5a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => {
|
||||
void openUrl(BILLING_DASHBOARD_URL);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rewards',
|
||||
title: 'Rewards',
|
||||
description: 'Referrals, coupons, and earned credits',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigate('/rewards'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai-models',
|
||||
title: 'AI & Models',
|
||||
description: 'Local AI model setup and downloads',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('ai-models'),
|
||||
dangerous: false,
|
||||
label: 'Support',
|
||||
items: [
|
||||
{
|
||||
id: 'restart-tour',
|
||||
title: 'Restart Tour',
|
||||
description: 'Replay the product walkthrough from the beginning',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => {
|
||||
resetWalkthrough();
|
||||
navigate('/home');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
title: 'About',
|
||||
description: 'App version and software updates',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('about'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
description: 'Do Not Disturb and per-account notification controls',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('notifications'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'restart-tour',
|
||||
title: 'Restart Tour',
|
||||
description: 'Replay the product walkthrough from the beginning',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => {
|
||||
resetWalkthrough();
|
||||
navigate('/home');
|
||||
},
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
title: 'About',
|
||||
description: 'App version and software updates',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('about'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'developer-options',
|
||||
title: 'Developer Options',
|
||||
description: 'Diagnostics, debug panels, webhooks, and memory inspection',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('developer-options'),
|
||||
dangerous: false,
|
||||
label: 'Advanced',
|
||||
items: [
|
||||
{
|
||||
id: 'developer-options',
|
||||
title: 'Developer Options',
|
||||
description: 'Diagnostics, debug panels, webhooks, and memory inspection',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('developer-options'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Destructive actions menu items
|
||||
const destructiveMenuItems = [
|
||||
// Destructive actions — rendered separately under "Danger Zone" heading
|
||||
const destructiveItems: SettingsItem[] = [
|
||||
{
|
||||
id: 'logout-and-clear',
|
||||
title: 'Clear App Data',
|
||||
@@ -287,22 +338,28 @@ const SettingsHome = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* Grouped Settings */}
|
||||
{groupedMenuItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
// isLast={index === groupedMenuItems.length - 1}
|
||||
/>
|
||||
{/* Grouped sections with section headers */}
|
||||
{settingsSections.map(section => (
|
||||
<div key={section.label}>
|
||||
<SectionHeader label={section.label} />
|
||||
{section.items.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === section.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Destructive Actions */}
|
||||
{destructiveMenuItems.map((item, index) => (
|
||||
{/* Danger Zone */}
|
||||
<SectionHeader label="Danger Zone" />
|
||||
{destructiveItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
@@ -311,7 +368,7 @@ const SettingsHome = () => {
|
||||
onClick={item.onClick}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === destructiveMenuItems.length - 1}
|
||||
isLast={index === destructiveItems.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import SettingsHome from '../SettingsHome';
|
||||
|
||||
// --- hoisted mocks ---
|
||||
|
||||
const { mockNavigate, mockNavigateToSettings } = vi.hoisted(() => ({
|
||||
mockNavigate: vi.fn(),
|
||||
mockNavigateToSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('react-router-dom')>();
|
||||
return { ...actual, useNavigate: () => mockNavigate };
|
||||
});
|
||||
|
||||
vi.mock('../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({ navigateToSettings: mockNavigateToSettings }),
|
||||
}));
|
||||
|
||||
vi.mock('../../../providers/CoreStateProvider', () => ({
|
||||
useCoreState: () => ({
|
||||
clearSession: vi.fn().mockResolvedValue(undefined),
|
||||
snapshot: { auth: { userId: null }, currentUser: null },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../store', () => ({ persistor: { purge: vi.fn().mockResolvedValue(undefined) } }));
|
||||
|
||||
vi.mock('../../../utils/links', () => ({ BILLING_DASHBOARD_URL: 'https://billing.example.com' }));
|
||||
|
||||
vi.mock('../../../utils/openUrl', () => ({ openUrl: vi.fn() }));
|
||||
|
||||
vi.mock('../../../utils/tauriCommands', () => ({
|
||||
resetOpenHumanDataAndRestartCore: vi.fn().mockResolvedValue(undefined),
|
||||
restartApp: vi.fn().mockResolvedValue(undefined),
|
||||
scheduleCefProfilePurge: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../../walkthrough/AppWalkthrough', () => ({ resetWalkthrough: vi.fn() }));
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
function renderSettingsHome() {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<SettingsHome />
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
describe('SettingsHome', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('section headers', () => {
|
||||
it('renders the General section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('General')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Features & AI section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Features & AI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Billing & Rewards section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Billing & Rewards')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Support section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Support')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Advanced section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Advanced')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Danger Zone section header', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Danger Zone')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('item grouping order', () => {
|
||||
it('places Account and Notifications under General', () => {
|
||||
renderSettingsHome();
|
||||
const generalHeader = screen.getByText('General');
|
||||
const accountItem = screen.getByText('Account');
|
||||
const notificationsItem = screen.getByText('Notifications');
|
||||
|
||||
// All should appear after the General header in DOM order
|
||||
expect(generalHeader.compareDocumentPosition(accountItem)).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
expect(generalHeader.compareDocumentPosition(notificationsItem)).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
|
||||
it('places Features, AI & Models under Features & AI', () => {
|
||||
renderSettingsHome();
|
||||
const header = screen.getByText('Features & AI');
|
||||
expect(header.compareDocumentPosition(screen.getByText('Features'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
expect(header.compareDocumentPosition(screen.getByText('AI & Models'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
|
||||
it('places Billing & Usage and Rewards under Billing & Rewards', () => {
|
||||
renderSettingsHome();
|
||||
const header = screen.getByText('Billing & Rewards');
|
||||
expect(header.compareDocumentPosition(screen.getByText('Billing & Usage'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
expect(header.compareDocumentPosition(screen.getByText('Rewards'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
|
||||
it('places Restart Tour and About under Support', () => {
|
||||
renderSettingsHome();
|
||||
const header = screen.getByText('Support');
|
||||
expect(header.compareDocumentPosition(screen.getByText('Restart Tour'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
expect(header.compareDocumentPosition(screen.getByText('About'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
|
||||
it('places Developer Options under Advanced', () => {
|
||||
renderSettingsHome();
|
||||
const header = screen.getByText('Advanced');
|
||||
expect(header.compareDocumentPosition(screen.getByText('Developer Options'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
|
||||
it('places Clear App Data and Log out under Danger Zone', () => {
|
||||
renderSettingsHome();
|
||||
const header = screen.getByText('Danger Zone');
|
||||
expect(header.compareDocumentPosition(screen.getByText('Clear App Data'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
expect(header.compareDocumentPosition(screen.getByText('Log out'))).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rewards menu item', () => {
|
||||
it('renders the Rewards item', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Rewards')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to /rewards when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
// The Rewards item description is used to find the right button
|
||||
const rewardsButton = screen.getByText('Rewards').closest('button');
|
||||
expect(rewardsButton).toBeTruthy();
|
||||
await user.click(rewardsButton!);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/rewards');
|
||||
});
|
||||
});
|
||||
|
||||
describe('existing navigation items', () => {
|
||||
it('navigates to account settings when Account is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Account').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('account');
|
||||
});
|
||||
|
||||
it('navigates to notifications settings when Notifications is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Notifications').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('notifications');
|
||||
});
|
||||
|
||||
it('navigates to /home when Restart Tour is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Restart Tour').closest('button')!);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
|
||||
it('navigates to features settings when Features is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Features').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('features');
|
||||
});
|
||||
|
||||
it('navigates to ai-models settings when AI & Models is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('AI & Models').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('ai-models');
|
||||
});
|
||||
|
||||
it('opens billing URL when Billing & Usage is clicked', async () => {
|
||||
const { openUrl } = await import('../../../utils/openUrl');
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Billing & Usage').closest('button')!);
|
||||
expect(openUrl).toHaveBeenCalledWith('https://billing.example.com');
|
||||
});
|
||||
|
||||
it('navigates to about settings when About is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('About').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('about');
|
||||
});
|
||||
|
||||
it('navigates to developer-options settings when Developer Options is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Developer Options').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('developer-options');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user