17a82832ab
Replace the username/password stub with Balinyaar's real credential (phone-OTP) and add the role router every authenticated screen sits behind. - services/auth rewritten for OTP over the b2 contract: types/keys/constants, clientApi+mockApi behind a config'd seam, hooks (useRequestOtp/useVerifyOtp/ useMe/useRefresh/useLogout/useSelectRole/useSessionRoleSync). Stub removed. - A1/A2 customer login + B1/B2 nurse switch as one OTP flow at /login (PhoneStep/OtpStep: auto-verify, resend countdown, wrong/expired/lockout states). - Role router: pure resolveRoleDestination + RoleRouter -> family / nurse / admin / select-role, with a splash while /me loads (no wrong-shell flash). - SelectRole first-use screen at /select-role. - Widened AuthState (roles via SessionUser), hydrated from /me by useSessionRoleSync. - Fetch-layer silent token refresh (single-flight + one retry) + shared persistAuthTokens/clearAuthTokens; useRefresh as the on-demand path. - auth i18n namespace in both locales; tests for routing branches, countdown, OtpStep state machine, RoleRouter branches; fixed jest @/ -> src alias. - Docs: client/CLAUDE.md, frontend STATUS/report, for-backend REQ-002..004. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
import { render, waitFor } from '@testing-library/react';
|
|
import { ThemeProvider } from '../../theme';
|
|
import type { Me } from '@/services/auth/types';
|
|
|
|
const mockReplace = jest.fn();
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
...jest.requireActual('next/navigation'),
|
|
useRouter: () => ({ replace: mockReplace }),
|
|
}));
|
|
jest.mock('next-intl', () => ({ useLocale: () => 'fa', useTranslations: () => (key: string) => key }));
|
|
|
|
let meResult: { data?: Partial<Me>; isError: boolean };
|
|
jest.mock('@/services/auth', () => ({ useMe: () => meResult }));
|
|
|
|
import RoleRouter from './RoleRouter';
|
|
|
|
function renderRouter(intendedRole?: 'customer' | 'nurse') {
|
|
render(
|
|
<ThemeProvider>
|
|
<RoleRouter intendedRole={intendedRole} />
|
|
</ThemeProvider>,
|
|
);
|
|
}
|
|
|
|
describe('<RoleRouter/>', () => {
|
|
beforeEach(() => mockReplace.mockReset());
|
|
|
|
it('does not navigate while /me is loading', () => {
|
|
meResult = { data: undefined, isError: false };
|
|
renderRouter();
|
|
expect(mockReplace).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('routes a customer to the family app', async () => {
|
|
meResult = { data: { roles: ['customer'] }, isError: false };
|
|
renderRouter();
|
|
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith('/fa/'));
|
|
});
|
|
|
|
it('routes a nurse to the nurse app', async () => {
|
|
meResult = { data: { roles: ['nurse'] }, isError: false };
|
|
renderRouter();
|
|
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith('/fa/nurse'));
|
|
});
|
|
|
|
it('routes a role-less user to select-role, carrying nurse intent', async () => {
|
|
meResult = { data: { roles: [] }, isError: false };
|
|
renderRouter('nurse');
|
|
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith('/fa/select-role?role=nurse'));
|
|
});
|
|
|
|
it('falls back to login when /me errors', async () => {
|
|
meResult = { data: undefined, isError: true };
|
|
renderRouter();
|
|
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith('/fa/login'));
|
|
});
|
|
});
|