mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
feat(dashboard): mobile layout, glass UI, context bottom nav + OIDC PKCE (#1079)
* feat(dashboard): mobile layout, glass tiles, plain-text countdown, place photos - Rework the mobile dashboard: cover hero, separate boarding-pass card, trimmed atlas (trips + days only), stacked widgets - New floating bottom tab bar with a centred context-aware + button (new trip / place / journey / entry depending on the page) - Move profile + notifications into a small top strip on the dashboard - Desktop: glassmorphic tiles (light + dark), neutral dark palette, plain-text countdown module, real place photos in the boarding pass * i18n(dashboard): translate new dashboard keys across all locales Fill the dashboard-rework keys (hero, atlas, fx, tz, upcoming, copy dialog, aria labels, countdown) that were left as English placeholders, plus the new startsIn/aria keys, for all 19 languages. * feat(oidc): send PKCE (S256) in the OIDC login flow The OIDC client now generates a code_verifier per login, sends the S256 code_challenge on the authorize request and the code_verifier on the token exchange. Works whether the provider has PKCE optional or required (fixes login against providers that require PKCE, e.g. Pocket ID).
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// FE-COMP-MOBILETOPBAR-001 to FE-COMP-MOBILETOPBAR-007
|
||||
|
||||
vi.mock('./InAppNotificationBell', () => ({ default: () => null }));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return { ...actual, useNavigate: () => mockNavigate };
|
||||
});
|
||||
|
||||
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { buildUser, buildSettings } from '../../../tests/helpers/factories';
|
||||
import MobileTopBar from './MobileTopBar';
|
||||
|
||||
const currentUser = buildUser({ id: 1, username: 'testuser', email: 'test@example.com' });
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
mockNavigate.mockClear();
|
||||
seedStore(useAuthStore, { user: currentUser, isAuthenticated: true });
|
||||
});
|
||||
|
||||
describe('MobileTopBar', () => {
|
||||
it('FE-COMP-MOBILETOPBAR-001: renders the profile avatar (no brand logo)', () => {
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
expect(screen.getByRole('button', { name: 'Profile' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('trek')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-002: avatar opens the profile sheet', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
await user.click(screen.getByRole('button', { name: 'Profile' }));
|
||||
expect(screen.getByText('testuser')).toBeInTheDocument();
|
||||
expect(screen.getByText('test@example.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-003: profile sheet shows Settings', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
await user.click(screen.getByRole('button', { name: 'Profile' }));
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-004: profile sheet shows Logout', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
await user.click(screen.getByRole('button', { name: 'Profile' }));
|
||||
expect(screen.getByText('Logout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-005: admin badge shown for admin users', async () => {
|
||||
seedStore(useAuthStore, { user: buildUser({ id: 2, username: 'adminuser', role: 'admin' }), isAuthenticated: true });
|
||||
const user = userEvent.setup();
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
await user.click(screen.getByRole('button', { name: 'Profile' }));
|
||||
expect(screen.getByText('Admin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-006: backdrop click closes the profile sheet', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
await user.click(screen.getByRole('button', { name: 'Profile' }));
|
||||
expect(screen.getByText('testuser')).toBeInTheDocument();
|
||||
const backdrop = document.querySelector('.fixed.inset-0') as HTMLElement;
|
||||
expect(backdrop).toBeTruthy();
|
||||
fireEvent.click(backdrop);
|
||||
expect(screen.queryByText('testuser')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MOBILETOPBAR-007: profile label translates when language is fr', async () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'fr' }) });
|
||||
render(<MobileTopBar />, { initialEntries: ['/dashboard'] });
|
||||
expect(await screen.findByRole('button', { name: 'Profil' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user