feat(redesign): migrate Jobs Board presentation (P5-04a)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user