Public Access
feat(welcome): add welcome page for new users after first login
Shows six feature cards and two next steps (Steam, import). The dashboard redirects to it while Auth settings.welcomeSeenAt is null; "Let's go" and "Skip" set the stamp. Settings links to it to reopen later.
This commit is contained in:
1 parent
b811255c16
commit
b129f6a6e4
11 files changed
+348
-2
No files matched your search
@@ -0,0 +1,122 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {act, cleanup, fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import {useEffect} from 'react';
|
||||
import {toast} from 'sonner';
|
||||
import WelcomePage from './WelcomePage.jsx';
|
||||
import WelcomeGate from '../components/WelcomeGate.jsx';
|
||||
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import {renderWithProviders} from '../test/renderWithProviders.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {error: vi.fn()}}));
|
||||
|
||||
const me = (overrides = {}) => ({id: 1, email: 'a@b.de', role: 'USER', steamId: null,
|
||||
settings: {welcomeSeenAt: null}, ...overrides});
|
||||
const putCalls = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT');
|
||||
|
||||
const renderPage = async ({user = me(), summary = json(200, {total: 0}), seen = json(204)} = {}) => {
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, user),
|
||||
'GET /api/library/summary': summary,
|
||||
'PUT /api/users/settings/welcome-seen': seen,
|
||||
});
|
||||
const result = renderWithProviders(<WelcomePage/>, {
|
||||
path: '/welcome', entries: ['/welcome'], routes: {'/dashboard': <p>dashboard</p>},
|
||||
});
|
||||
await act(async () => undefined);
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('WelcomePage', () => {
|
||||
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows six feature cards, five of them with a link to their page', async () => {
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getAllByRole('heading', {level: 2})).toHaveLength(6);
|
||||
expect(screen.getAllByRole('link', {name: 'WELCOME.OPEN'}).map((link) => link.getAttribute('href')))
|
||||
.toEqual(['/library', '/library', '/stats', '/settings', '/import']);
|
||||
});
|
||||
|
||||
it('checks the steps that are done', async () => {
|
||||
await renderPage({user: me({steamId: '7656'}), summary: json(200, {total: 1})});
|
||||
|
||||
expect(await screen.findAllByText('WELCOME.DONE')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('checks nothing for a new user without games', async () => {
|
||||
await renderPage();
|
||||
|
||||
expect(screen.queryByText('WELCOME.DONE')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders without checks when the summary fails', async () => {
|
||||
await renderPage({summary: json(500, {})});
|
||||
|
||||
expect(screen.getByText('WELCOME.TITLE')).toBeTruthy();
|
||||
expect(screen.queryByText('WELCOME.DONE')).toBeNull();
|
||||
});
|
||||
|
||||
it.each(['WELCOME.GO', 'WELCOME.SKIP'])('"%s" stamps once and opens the dashboard', async (label) => {
|
||||
const {router} = await renderPage();
|
||||
|
||||
fireEvent.click(screen.getByText(label));
|
||||
|
||||
await waitFor(() => expect(router.state.location.pathname).toBe('/dashboard'));
|
||||
expect(putCalls().map(([url]) => url)).toEqual([expect.stringContaining('/api/users/settings/welcome-seen')]);
|
||||
});
|
||||
|
||||
it('still opens the dashboard, silently, when the stamp fails', async () => {
|
||||
const {router} = await renderPage({seen: json(500, {})});
|
||||
|
||||
fireEvent.click(screen.getByText('WELCOME.GO'));
|
||||
|
||||
await waitFor(() => expect(router.state.location.pathname).toBe('/dashboard'));
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('staysOnTheDashboardWhenItReloadsTheUserBeforeTheStampLands', async () => {
|
||||
let stamped = false;
|
||||
function DashboardStub() {
|
||||
const {reload} = useCurrentUserContext();
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
return <p>dashboard</p>;
|
||||
}
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': () => json(200, me({settings: {welcomeSeenAt: stamped ? '2026-10-01T10:00:00Z' : null}})),
|
||||
'GET /api/library/summary': json(200, {total: 0}),
|
||||
'PUT /api/users/settings/welcome-seen': () => new Promise((resolve) => setTimeout(() => {
|
||||
stamped = true;
|
||||
resolve(json(204));
|
||||
}, 20)),
|
||||
});
|
||||
const {router} = renderWithProviders(<WelcomePage/>, {
|
||||
path: '/welcome', entries: ['/welcome'],
|
||||
routes: {'/dashboard': <WelcomeGate><DashboardStub/></WelcomeGate>},
|
||||
});
|
||||
await screen.findByText('WELCOME.TITLE');
|
||||
|
||||
fireEvent.click(screen.getByText('WELCOME.GO'));
|
||||
|
||||
await waitFor(() => expect(stamped).toBe(true));
|
||||
await act(async () => new Promise((resolve) => setTimeout(resolve, 30)));
|
||||
expect(router.state.location.pathname).toBe('/dashboard');
|
||||
});
|
||||
|
||||
it('sendsTheStampWithATimeout', async () => {
|
||||
await renderPage({seen: () => new Promise(() => undefined)});
|
||||
|
||||
fireEvent.click(screen.getByText('WELCOME.GO'));
|
||||
|
||||
await waitFor(() => expect(putCalls()).toHaveLength(1));
|
||||
expect(putCalls()[0][1].signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user