feat(jobs): embed route-backed workspace
This commit is contained in:
@@ -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§ion=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§ion=match");
|
||||
});
|
||||
|
||||
test("opens a direct workspace URL and closes it without inventing browser history", async () => {
|
||||
renderTable("/jobs?workspace=42§ion=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());
|
||||
});
|
||||
Reference in New Issue
Block a user