feat(jobs): embed route-backed workspace
CI and Deploy / test (pull_request) Successful in 4m34s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-10 11:28:01 +02:00
parent a54b70960c
commit b67a531af4
9 changed files with 303 additions and 19 deletions
@@ -0,0 +1,147 @@
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 { api } from "./api";
import JobTable from "./components/JobTable";
import { ConfirmProvider } from "./confirm";
import { I18nProvider } from "./i18n/I18nProvider";
import { PromptProvider } from "./prompt";
import { ToastProvider } from "./toast";
jest.mock("./components/Attachments", () => () => <div>Documents section</div>);
jest.mock("./components/Correspondence", () => () => <div>Communication section</div>);
jest.mock("./components/AiWorkspacePanel", () => () => <div>AI panel</div>);
jest.mock("./components/ApplicationChecklist", () => () => <div>Checklist section</div>);
jest.mock("./components/ApplicationIntelligence", () => ({
ApplicationAnalysis: () => <div>Analysis section</div>,
ApplicationMatch: () => <div>Match section</div>,
ApplicationTimeline: () => <div>Timeline section</div>,
}));
jest.mock("./components/ApplicationAssets", () => ({
ApplicationCoverLetterSection: () => <div>Cover letter section</div>,
ApplicationCvSection: () => <div>CV section</div>,
}));
jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () => <div>Interview section</div> }));
const mockedApi = api as jest.Mocked<typeof api>;
const job = {
id: 42,
jobTitle: "Backend Developer",
company: { id: 1, name: "Acme" },
companyId: 1,
status: "Waiting",
dateApplied: "2026-08-01T00:00:00Z",
savedAt: "2026-08-01T00:00:00Z",
location: "Oslo",
description: "Build APIs",
daysSince: 9,
isDeleted: false,
needsFollowUp: false,
workflowSignal: null,
};
const overview = {
id: 42,
jobTitle: "Backend Developer",
company: "Acme",
location: "Oslo",
salary: null,
status: "Waiting",
stageGroup: "Applied",
stageOrder: 1,
dateApplied: "2026-08-01T00:00:00Z",
deadline: null,
followUpAt: null,
nextAction: null,
jobUrl: null,
hasJobDescription: true,
cv: { variantId: null, variantName: null, themeId: null, hasTailoredCvText: false, updatedAtUtc: null },
hasCoverLetter: false,
documentCount: 0,
hasPortfolio: false,
aiInteractionCount: 0,
lastAiAtUtc: null,
recentActivity: [],
nextStep: null,
checklistProgress: { total: 0, completed: 0, dismissed: 0, percent: 0 },
};
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>
</>
);
}
function renderTable(path = "/jobs") {
return render(
<ToastProvider>
<I18nProvider>
<ConfirmProvider>
<PromptProvider>
<MemoryRouter initialEntries={[path]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<LocationControls />
<Routes>
<Route path="/jobs" element={<JobTable refreshToken={0} pageSize={15} onPageSizeChange={() => {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} />} />
</Routes>
</MemoryRouter>
</PromptProvider>
</ConfirmProvider>
</I18nProvider>
</ToastProvider>,
);
}
beforeEach(() => {
mockedApi.get.mockImplementation((url: string) => {
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);
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 () => {
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 }));
await screen.findByRole("dialog", { name: /application workspace/i });
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?workspace=42");
expect(screen.getByRole("link", { name: /open full-page workspace/i })).toHaveAttribute("href", "/applications/42?section=overview");
fireEvent.click(screen.getByRole("button", { name: "Match" }));
expect(await screen.findByText("Match section")).toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("/jobs?workspace=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");
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?workspace=42&section=match");
});
test("opens a direct workspace URL and closes it without inventing browser history", async () => {
renderTable("/jobs?workspace=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());
});