feat(jobs): add dedicated workspace page
Make /jobs/:id the canonical application workspace while preserving list state and compatibility links. Replace popup and expandable-row navigation with accessible whole-row routing and richer job details.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
|
||||
import { api } from "./api";
|
||||
import JobTable from "./components/JobTable";
|
||||
@@ -9,6 +9,7 @@ import { ConfirmProvider } from "./confirm";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import { PromptProvider } from "./prompt";
|
||||
import { ToastProvider } from "./toast";
|
||||
import ApplicationWorkspacePage from "./views/ApplicationWorkspacePage";
|
||||
|
||||
jest.mock("./components/Attachments", () => () => <div>Documents section</div>);
|
||||
jest.mock("./components/Correspondence", () => () => <div>Communication section</div>);
|
||||
@@ -57,6 +58,14 @@ const overview = {
|
||||
followUpAt: null,
|
||||
nextAction: null,
|
||||
jobUrl: null,
|
||||
savedAt: "2026-08-01T00:00:00Z",
|
||||
description: "Build APIs",
|
||||
translatedDescription: null,
|
||||
descriptionLanguage: "en",
|
||||
tags: [".NET", "SQL"],
|
||||
notes: "Ask about the platform team.",
|
||||
source: "nav",
|
||||
countryCode: "NO",
|
||||
hasJobDescription: true,
|
||||
cv: { variantId: null, variantName: null, themeId: null, hasTailoredCvText: false, updatedAtUtc: null },
|
||||
hasCoverLetter: false,
|
||||
@@ -71,11 +80,9 @@ const overview = {
|
||||
|
||||
function LocationControls() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<output data-testid="location">{location.pathname}{location.search}</output>
|
||||
<button onClick={() => navigate(1)}>Browser forward</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -86,10 +93,11 @@ function renderTable(path = "/jobs") {
|
||||
<I18nProvider>
|
||||
<ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<MemoryRouter initialEntries={[path]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<LocationControls />
|
||||
<Routes>
|
||||
<Route path="/jobs" element={<JobTable refreshToken={0} pageSize={15} onPageSizeChange={() => {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} />} />
|
||||
<Route path="/jobs/:id" element={<ApplicationWorkspacePage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</PromptProvider>
|
||||
@@ -104,46 +112,57 @@ beforeEach(() => {
|
||||
if (url === "/companies") return Promise.resolve({ data: [{ id: 1, name: "Acme" }] } as any);
|
||||
if (url === "/jobapplications") return Promise.resolve({ data: { items: [job], total: 1, page: 1, pageSize: 15 } } as any);
|
||||
if (url === "/jobapplications/42/workspace") return Promise.resolve({ data: overview } as any);
|
||||
if (url === "/jobapplications/999/workspace") return Promise.reject({ response: { status: 404, data: { detail: "Application not found." } } });
|
||||
return Promise.resolve({ data: [] } as any);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
test("opens the workspace in a route-backed overlay and preserves list state through Back/Forward", async () => {
|
||||
test("opens the dedicated workspace from the whole row and preserves list state on return", async () => {
|
||||
renderTable();
|
||||
|
||||
const search = await screen.findByRole("textbox", { name: /search/i });
|
||||
fireEvent.change(search, { target: { value: "backend" } });
|
||||
fireEvent.click(await screen.findByRole("button", { name: /open: backend developer/i }));
|
||||
fireEvent.click(await screen.findByRole("row", { name: /open backend developer/i }));
|
||||
|
||||
await screen.findByRole("dialog", { name: /application workspace/i });
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend&workspace=42");
|
||||
expect(screen.getByRole("link", { name: /open full-page workspace/i })).toHaveAttribute("href", "/applications/42?section=overview");
|
||||
expect(await screen.findByText("Backend Developer")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Match" }));
|
||||
expect(await screen.findByText("Match section")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend&workspace=42§ion=match");
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /back to applications/i }));
|
||||
await waitFor(() => expect(screen.queryByRole("dialog", { name: /application workspace/i })).not.toBeInTheDocument());
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend");
|
||||
expect(screen.getByRole("textbox", { name: /search/i })).toHaveValue("backend");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /browser forward/i }));
|
||||
await screen.findByRole("dialog", { name: /application workspace/i });
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend&workspace=42§ion=match");
|
||||
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend"));
|
||||
expect(await screen.findByRole("textbox", { name: /search/i })).toHaveValue("backend");
|
||||
});
|
||||
|
||||
test("opens a direct workspace URL and closes it without inventing browser history", async () => {
|
||||
renderTable("/jobs?workspace=42§ion=match");
|
||||
test("opens a direct workspace URL and returns to applications", async () => {
|
||||
renderTable("/jobs/42?section=match");
|
||||
|
||||
await screen.findByRole("dialog", { name: /application workspace/i });
|
||||
expect(await screen.findByText("Match section")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /back to applications/i }));
|
||||
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs"));
|
||||
await waitFor(() => expect(screen.queryByRole("dialog", { name: /application workspace/i })).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test("row controls do not trigger navigation", async () => {
|
||||
renderTable();
|
||||
|
||||
const row = await screen.findByRole("row", { name: /open backend developer/i });
|
||||
const checkbox = within(row).getByRole("checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(/^\/jobs$/);
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
test("handles a deleted or inaccessible job without rendering a broken workspace", async () => {
|
||||
renderTable("/jobs/999");
|
||||
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Could not open this application");
|
||||
expect(screen.getByRole("button", { name: /back to applications/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hydrates list filters, sort and page from a shareable URL", async () => {
|
||||
|
||||
Reference in New Issue
Block a user