mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-16 21:48:45 +00:00
feat(ui): add Classic, Smart, and Compact desktop navigation styles (#1642)
* feat(ui): add Classic, Smart, and Compact desktop navigation styles Introduce a shared app-nav registry and reachable model so TopBar, the command palette, and mobile menus share one destination source. Smart bar is the default; Appearance gains a Navigation subsection with quick links. * fix(e2e): stop clearing top-nav prefs on every reload The desktop-navigation suite used addInitScript to wipe mode storage, which re-ran on reload and undid the classic/compact values under test. * fix(ui): harden nav PR docs and Compact quick-link coverage Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload. * fix(ui): polish Compact quick links and menu mastheads Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove. * test(e2e): exact-match Compact Networking pin locator Avoid Playwright strict-mode clash with Actions for Networking. * fix(ui): simplify Compact quick link removal and fix + button trailing Remove the (...) dropdown per quick link in Compact mode. Right-click context menu remains as the sole on-bar removal affordance. Fix the + add-button to trail quick links rather than pinning to the far right by removing flex-1 from the quick-link rail.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Home, Radar } from 'lucide-react';
|
||||
import { TopBar, type TopBarNavItem } from '../TopBar';
|
||||
|
||||
@@ -87,3 +88,215 @@ describe('TopBar showLabels', () => {
|
||||
expect(screen.getByRole('navigation', { name: 'Primary' }).previousElementSibling).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TopBar smart and compact modes', () => {
|
||||
const overflowGroups = [
|
||||
{
|
||||
group: 'operations' as const,
|
||||
label: 'Operations',
|
||||
items: [{ value: 'global-observability' as const, label: 'Logs', icon: Home }],
|
||||
},
|
||||
];
|
||||
const launcherGroups = [
|
||||
{
|
||||
group: 'overview' as const,
|
||||
label: 'Overview',
|
||||
items: [{ value: 'dashboard' as const, label: 'Home', icon: Home }],
|
||||
},
|
||||
{
|
||||
group: 'settings' as const,
|
||||
label: 'Settings',
|
||||
items: [{ value: 'settings' as const, label: 'Settings', icon: Radar }],
|
||||
},
|
||||
];
|
||||
const emptyModel = {
|
||||
allPageItems: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
primaryItems: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
overflowGroups: [] as typeof overflowGroups,
|
||||
launcherGroups: [] as typeof launcherGroups,
|
||||
quickLinkCandidates: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
};
|
||||
|
||||
it('marks More with aria-current when the active page is in overflow', () => {
|
||||
renderTopBar({
|
||||
navMode: 'smart',
|
||||
activeView: 'global-observability',
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
overflowGroups,
|
||||
},
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'More navigation' })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'More navigation' })).toHaveTextContent('More');
|
||||
});
|
||||
|
||||
it('opens the More menu with masthead chrome and keeps overflow labels', async () => {
|
||||
const onNavigate = vi.fn();
|
||||
renderTopBar({
|
||||
navMode: 'smart',
|
||||
onNavigate,
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
overflowGroups,
|
||||
},
|
||||
});
|
||||
const more = screen.getByRole('button', { name: 'More navigation' });
|
||||
more.focus();
|
||||
fireEvent.keyDown(more, { key: 'Enter' });
|
||||
expect(await screen.findByText('More', { selector: '.font-heading' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('menuitem', { name: /Logs/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Logs/i }));
|
||||
expect(onNavigate).toHaveBeenCalledWith('global-observability');
|
||||
});
|
||||
|
||||
it('renders Compact pins with always-inline labels and a trailing Add control', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onNavigate = vi.fn();
|
||||
const onAddQuickLink = vi.fn();
|
||||
const onRemoveQuickLink = vi.fn();
|
||||
const onOpenSettings = vi.fn();
|
||||
renderTopBar({
|
||||
navMode: 'compact',
|
||||
activeView: 'dashboard',
|
||||
onNavigate,
|
||||
onAddQuickLink,
|
||||
onRemoveQuickLink,
|
||||
onOpenSettings,
|
||||
persistedQuickLinkIds: ['dashboard'],
|
||||
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
launcherGroups,
|
||||
quickLinkCandidates: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const home = screen.getByRole('button', { name: 'Home' });
|
||||
expect(home.querySelector('span.inline')).toBeTruthy();
|
||||
expect(home.querySelector('span.hidden')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Add quick link' }));
|
||||
await user.click(await screen.findByRole('menuitem', { name: /Fleet/i }));
|
||||
expect(onAddQuickLink).toHaveBeenCalledWith('fleet');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Home' }));
|
||||
expect(onNavigate).toHaveBeenCalledWith('dashboard');
|
||||
|
||||
await user.pointer({ keys: '[MouseRight]', target: screen.getByRole('button', { name: 'Home' }) });
|
||||
await user.click(await screen.findByRole('menuitem', { name: /^Remove$/i }));
|
||||
expect(onRemoveQuickLink).toHaveBeenCalledWith('dashboard');
|
||||
expect(onNavigate).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
|
||||
expect(await screen.findByText('Navigate', { selector: '.font-heading' })).toBeInTheDocument();
|
||||
await user.click(await screen.findByRole('menuitem', { name: /Settings/i }));
|
||||
expect(onOpenSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables Add when persisted capacity is full even if fewer pins are visible', () => {
|
||||
renderTopBar({
|
||||
navMode: 'compact',
|
||||
persistedQuickLinkIds: ['dashboard', 'fleet', 'resources', 'security', 'networking'],
|
||||
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
launcherGroups,
|
||||
quickLinkCandidates: emptyModel.quickLinkCandidates,
|
||||
},
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'Add quick link' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('offers Compact launcher context Add for unpinned destinations', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAddQuickLink = vi.fn();
|
||||
const compactLauncher = [
|
||||
{
|
||||
group: 'overview' as const,
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
},
|
||||
];
|
||||
renderTopBar({
|
||||
navMode: 'compact',
|
||||
onAddQuickLink,
|
||||
persistedQuickLinkIds: ['dashboard'],
|
||||
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
launcherGroups: compactLauncher,
|
||||
quickLinkCandidates: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
|
||||
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Fleet/i }));
|
||||
await user.click(await screen.findByRole('menuitem', { name: /Add to quick links/i }));
|
||||
expect(onAddQuickLink).toHaveBeenCalledWith('fleet');
|
||||
});
|
||||
|
||||
it('hides Compact launcher context Add for already-pinned destinations', async () => {
|
||||
const user = userEvent.setup();
|
||||
const compactLauncher = [
|
||||
{
|
||||
group: 'overview' as const,
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{ value: 'dashboard' as const, label: 'Home', icon: Home },
|
||||
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
|
||||
],
|
||||
},
|
||||
];
|
||||
renderTopBar({
|
||||
navMode: 'compact',
|
||||
onAddQuickLink: vi.fn(),
|
||||
persistedQuickLinkIds: ['dashboard'],
|
||||
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
launcherGroups: compactLauncher,
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
|
||||
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Home/i }));
|
||||
expect(screen.queryByRole('menuitem', { name: /Add to quick links/i })).toBeNull();
|
||||
});
|
||||
|
||||
it('does not offer Add to quick links on Smart More', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTopBar({
|
||||
navMode: 'smart',
|
||||
onAddQuickLink: vi.fn(),
|
||||
navModel: {
|
||||
...emptyModel,
|
||||
primaryItems: [{ value: 'dashboard' as const, label: 'Home', icon: Home }],
|
||||
overflowGroups,
|
||||
},
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: 'More navigation' }));
|
||||
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Logs/i }));
|
||||
expect(screen.queryByRole('menuitem', { name: /Add to quick links/i })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user