Files
baya-monorepo/client/src/components/auth/RoleRouter.test.tsx
T
hamid 17a82832ab frontend phase 1: auth — phone-OTP login, role routing & session refresh
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>
2026-07-02 11:58:52 +03:30

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'));
});
});