feat(redesign): migrate Jobs Board presentation (P5-04a)

This commit is contained in:
cesnimda
2026-09-29 07:59:50 +02:00
parent 7127005e48
commit 42a696bc86
24 changed files with 446 additions and 109 deletions
@@ -5,6 +5,7 @@ import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import { api } from './api';
import { JobApplication } from './types';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
jest.mock('./api', () => ({
api: {
@@ -37,12 +38,22 @@ function renderBoard() {
return render(
<ToastProvider>
<I18nProvider>
<KanbanBoard />
<MemoryRouter initialEntries={['/kanban?source=board']}>
<Routes>
<Route path="/kanban" element={<KanbanBoard />} />
<Route path="/jobs/:id" element={<div role="main">Job workspace <RouteState /></div>} />
</Routes>
</MemoryRouter>
</I18nProvider>
</ToastProvider>,
);
}
function RouteState() {
const location = useLocation();
return <span>{String((location.state as { from?: string } | null)?.from)}</span>;
}
beforeEach(() => {
jest.clearAllMocks();
window.localStorage.clear();
@@ -127,7 +138,7 @@ test('dropping onto a group applies that group entry stage', async () => {
const column = screen.getByText('Active').closest('div')!.parentElement!.parentElement!;
fireEvent.dragStart(card.closest('.MuiCard-root')!);
expect(card.closest('.MuiCard-root')).toHaveAttribute('aria-pressed', 'true');
expect(card.closest('.MuiCard-root')!.querySelector('[role="button"]')).toHaveAttribute('aria-pressed', 'true');
fireEvent.dragEnter(column);
expect(column).toHaveAttribute('data-drop-state', 'active');
fireEvent.dragOver(column);
@@ -142,8 +153,9 @@ test('keyboard pickup and drop moves a card to a valid column', async () => {
renderBoard();
const card = (await screen.findByText('Keyboard Role')).closest('.MuiCard-root')!;
fireEvent.keyDown(card, { key: ' ' });
expect(card).toHaveAttribute('aria-pressed', 'true');
const control = card.querySelector('[role="button"]')!;
fireEvent.keyDown(control, { key: ' ' });
expect(control).toHaveAttribute('aria-pressed', 'true');
const column = screen.getByRole('group', { name: 'Active column' });
fireEvent.focus(column);
@@ -174,7 +186,7 @@ test('the card menu offers precise stages grouped by section', async () => {
renderBoard();
await screen.findByText('Saved Role');
fireEvent.click(screen.getByRole('button', { name: 'Change status for Saved Role' }));
fireEvent.click(screen.getByRole('button', { name: 'Quick status: Saved Role' }));
const menu = await screen.findByRole('menu');
// Drag is coarse; the menu is where Ghosted and Withdrawn are reachable at all.
@@ -183,3 +195,46 @@ test('the card menu offers precise stages grouped by section', async () => {
// The job's own current stage is not offered as a target.
expect(within(menu).queryByText(/Set status: Saved/)).not.toBeInTheDocument();
});
test('full-board population keeps jobs beyond the table page and offers a separate table link', async () => {
mockedApi.get.mockResolvedValue({ data: Array.from({ length: 27 }, (_, index) => job(index + 1, `Role ${index}`, index === 26 ? 'Take-home assignment' : 'Saved')) } as any);
renderBoard();
expect(await screen.findByText('Role 26')).toBeVisible();
expect(screen.getAllByRole('button', { name: /Press Space to pick up/ })).toHaveLength(27);
expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/board');
expect(screen.getByRole('link', { name: /Job Applications/ })).toHaveAttribute('href', '/jobs');
});
test('keyboard Escape cancels movement without a write and successful movement restores card focus', async () => {
mockedApi.get.mockResolvedValue({ data: [job(11, 'Keyboard Role', 'Saved')] } as any);
renderBoard();
const control = await screen.findByRole('button', { name: /^Keyboard Role.*Press Space to pick up/ });
fireEvent.keyDown(control, { key: ' ' });
fireEvent.keyDown(control, { key: 'Escape' });
expect(mockedApi.patch).not.toHaveBeenCalled();
fireEvent.keyDown(control, { key: ' ' });
const column = screen.getByRole('group', { name: 'Active column' });
fireEvent.focus(column);
fireEvent.keyDown(column, { key: 'Enter' });
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith('/jobapplications/11/status', { status: 'Applied' }));
await waitFor(() => expect(screen.getByRole('button', { name: /^Keyboard Role/ })).toHaveFocus());
});
test('opening a card preserves its board return route and does not mutate status', async () => {
mockedApi.get.mockResolvedValue({ data: [job(8, 'Open Role', 'Saved')] } as any);
renderBoard();
fireEvent.keyDown(await screen.findByRole('button', { name: /^Open Role.*Press Space to pick up/ }), { key: 'Enter' });
expect(await screen.findByText('Job workspace')).toBeVisible();
expect(screen.getByText('/kanban?source=board')).toBeVisible();
expect(mockedApi.patch).not.toHaveBeenCalled();
});
test('keyboard activation of the exact-status menu does not open the job', async () => {
mockedApi.get.mockResolvedValue({ data: [job(9, 'Menu Role', 'Saved')] } as any);
renderBoard();
const menuButton = await screen.findByRole('button', { name: 'Quick status: Menu Role' });
fireEvent.keyDown(menuButton, { key: 'Enter' });
fireEvent.click(menuButton);
expect(await screen.findByRole('menu')).toBeVisible();
expect(screen.queryByText('Job workspace')).not.toBeInTheDocument();
});