feat(jobs): complete workspace draft parity
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { createMemoryRouter, RouterProvider, Route, Routes, useLocation } from "react-router-dom";
|
||||
|
||||
import { api } from "./api";
|
||||
import JobTable from "./components/JobTable";
|
||||
@@ -21,13 +21,40 @@ jest.mock("./components/ApplicationIntelligence", () => ({
|
||||
ApplicationTimeline: () => <div>Timeline section</div>,
|
||||
}));
|
||||
jest.mock("./components/ApplicationAssets", () => ({
|
||||
ApplicationCoverLetterSection: () => <div>Cover letter section</div>,
|
||||
ApplicationCoverLetterSection: ({ onDirtyChange }: { onDirtyChange?: (dirty: boolean) => void }) => (
|
||||
<div>
|
||||
Cover letter section
|
||||
<button onClick={() => onDirtyChange?.(true)}>Make cover letter dirty</button>
|
||||
</div>
|
||||
),
|
||||
ApplicationCvSection: () => <div>CV section</div>,
|
||||
ApplicationPackageDraftsSection: () => <div>Application drafts section</div>,
|
||||
}));
|
||||
jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () => <div>Interview section</div> }));
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
// React Router's data router builds a Request for in-memory navigations. JSDOM does not provide
|
||||
// one, and these tests do not run loaders or inspect request bodies, so a small contract stub is
|
||||
// enough to exercise blocker/history behavior.
|
||||
class RouterTestRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
signal?: AbortSignal;
|
||||
headers: Headers;
|
||||
body: unknown;
|
||||
|
||||
constructor(url: string, init: RequestInit = {}) {
|
||||
this.url = url;
|
||||
this.method = init.method ?? "GET";
|
||||
this.signal = init.signal ?? undefined;
|
||||
this.headers = new Headers(init.headers);
|
||||
this.body = init.body;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(globalThis, { Request: RouterTestRequest });
|
||||
|
||||
const job = {
|
||||
id: 42,
|
||||
jobTitle: "Backend Developer",
|
||||
@@ -64,6 +91,8 @@ const overview = {
|
||||
descriptionLanguage: "en",
|
||||
tags: [".NET", "SQL"],
|
||||
notes: "Ask about the platform team.",
|
||||
applicationAnswerDraft: "Saved answer",
|
||||
recruiterMessageDraft: "Saved recruiter message",
|
||||
source: "nav",
|
||||
countryCode: "NO",
|
||||
hasJobDescription: true,
|
||||
@@ -88,18 +117,27 @@ function LocationControls() {
|
||||
}
|
||||
|
||||
function renderTable(path = "/jobs") {
|
||||
const router = createMemoryRouter([
|
||||
{
|
||||
path: "*",
|
||||
element: (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
], { initialEntries: [path] });
|
||||
|
||||
return render(
|
||||
<ToastProvider>
|
||||
<I18nProvider>
|
||||
<ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<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>
|
||||
<RouterProvider router={router} />
|
||||
</PromptProvider>
|
||||
</ConfirmProvider>
|
||||
</I18nProvider>
|
||||
@@ -136,6 +174,7 @@ test("opens the dedicated workspace from the whole row and preserves list state
|
||||
fireEvent.click(screen.getByRole("button", { name: /back to applications/i }));
|
||||
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend"));
|
||||
expect(await screen.findByRole("textbox", { name: /search/i })).toHaveValue("backend");
|
||||
await waitFor(() => expect(screen.getByRole("row", { name: /open backend developer/i })).toHaveFocus());
|
||||
});
|
||||
|
||||
test("opens a direct workspace URL and returns to applications", async () => {
|
||||
@@ -165,6 +204,22 @@ test("handles a deleted or inaccessible job without rendering a broken workspace
|
||||
expect(screen.getByRole("button", { name: /back to applications/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("warns before section navigation would discard application edits", async () => {
|
||||
renderTable("/jobs/42?section=cover-letter");
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Make cover letter dirty" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Match" }));
|
||||
|
||||
expect(await screen.findByRole("dialog", { name: /Unsaved application changes/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Keep editing" }));
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=cover-letter");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog", { name: /Unsaved application changes/i })).not.toBeInTheDocument());
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Match" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Discard and leave" }));
|
||||
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match"));
|
||||
});
|
||||
|
||||
test("hydrates list filters, sort and page from a shareable URL", async () => {
|
||||
renderTable("/jobs?q=backend&status=Interview&companyId=1&location=Oslo&needsFollowUp=1&readiness=interview&includeDeleted=1&sortBy=company&sortDir=asc&page=2");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user