feat(ui): redesign job workspace sections

This commit is contained in:
cesnimda
2026-10-07 01:58:31 +02:00
parent acd1d94684
commit ac6d78289d
123 changed files with 2842 additions and 205 deletions
@@ -27,7 +27,8 @@ jest.mock("./components/ApplicationAssets", () => ({
</div>
),
ApplicationCvSection: () => <div>CV section</div>,
ApplicationPackageDraftsSection: () => <div>Application drafts section</div>,
ApplicationPackageDraftsSection: ({ onDirtyChange }: { onDirtyChange?: (dirty: boolean) => void }) => <div>Application drafts section<button onClick={() => onDirtyChange?.(true)}>Make answers dirty</button></div>,
SubmittedPackagesSection: () => <div>Frozen packages section</div>,
}));
jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () => <div>Interview section</div> }));
jest.mock("./components/ApplicationWorkflowAssist", () => ({
@@ -174,10 +175,10 @@ test("opens the dedicated workspace from the whole row and preserves list state
expect(await screen.findByText("Backend Developer")).toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42");
fireEvent.click(screen.getByRole("tab", { name: "Analysis" }));
fireEvent.click(screen.getByRole("tab", { name: "Match" }));
expect(await screen.findByText("Analysis section")).toBeInTheDocument();
expect(await screen.findByText("Match section")).toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis");
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match");
fireEvent.click(screen.getByRole("button", { name: /back to applications/i }));
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend"));
@@ -190,7 +191,7 @@ test("opens a direct workspace URL and returns to applications", async () => {
expect(await screen.findByText("Analysis section")).toBeInTheDocument();
expect(await screen.findByText("Match section")).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Analysis" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Match" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByLabelText(/application progress: waiting/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /back to applications/i }));
@@ -219,16 +220,16 @@ 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("tab", { name: "Analysis" }));
fireEvent.click(screen.getByRole("tab", { 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("tab", { name: "Analysis" }));
fireEvent.click(screen.getByRole("tab", { name: "Match" }));
fireEvent.click(await screen.findByRole("button", { name: "Discard and leave" }));
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis"));
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match"));
});
test("renders shared workspace navigation and progress in Norwegian Bokmål", async () => {
@@ -248,8 +249,8 @@ test("renders shared workspace navigation and progress in Norwegian Bokmål", as
renderTable("/jobs/42");
expect(await screen.findByRole("tab", { name: "Oversikt" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Analyse" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Søknadsbrev" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Samsvar" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Søknad" })).toBeInTheDocument();
expect(await screen.findByLabelText(/søknadsprogresjon/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /tilbake til søknader/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Forbered en CV for denne stillingen" })).toBeInTheDocument();
@@ -323,17 +324,17 @@ test("uses recorded dates in the summary and keeps absent dates unknown", async
test("section changes retain follow-up query context and expose the selected labelled panel", async () => {
renderTable("/jobs/42?section=communication&followMode=waiting-update");
await screen.findByRole("heading", { name: "Backend Developer" });
fireEvent.click(screen.getByRole("tab", { name: "Analysis" }));
expect(await screen.findByRole("tabpanel", { name: "Analysis" })).toHaveTextContent("Match section");
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis&followMode=waiting-update");
const tab = screen.getByRole("tab", { name: "Analysis" });
expect(document.getElementById(tab.getAttribute("aria-controls")!)).toBe(screen.getByRole("tabpanel", { name: "Analysis" }));
fireEvent.click(screen.getByRole("tab", { name: "Match" }));
expect(await screen.findByRole("tabpanel", { name: "Match" })).toHaveTextContent("Match section");
expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match&followMode=waiting-update");
const tab = screen.getByRole("tab", { name: "Match" });
expect(document.getElementById(tab.getAttribute("aria-controls")!)).toBe(screen.getByRole("tabpanel", { name: "Match" }));
});
test("accordion controls identify exactly one labelled content region", async () => {
renderTable("/jobs/42");
await screen.findByRole("heading", { name: "Backend Developer" });
const trigger = screen.getByRole("button", { name: "Documents" });
const trigger = screen.getByRole("button", { name: "Job details" });
fireEvent.click(trigger);
const target = trigger.getAttribute("aria-controls")!;
expect(document.querySelectorAll(`[id='${target}']`)).toHaveLength(1);
@@ -341,12 +342,11 @@ test("accordion controls identify exactly one labelled content region", async ()
expect(document.getElementById(target)).toHaveAttribute("aria-labelledby", trigger.id);
});
test("legacy document links reveal their existing content and focus the disclosure", async () => {
test("legacy document links reveal current attachments and frozen history in the named section", async () => {
renderTable("/jobs/42?section=documents");
const trigger = await screen.findByRole("button", { name: "Documents" });
await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true"));
expect(trigger).toHaveFocus();
expect(screen.getByRole("region", { name: "Documents" })).toHaveTextContent("Documents section");
expect(await screen.findByRole("tab", { name: "Documents" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Documents" })).toHaveTextContent("Documents section");
expect(screen.getByRole("tabpanel", { name: "Documents" })).toHaveTextContent("Frozen packages section");
});
test("the missing-profile next task opens the existing canonical profile entry", async () => {
@@ -357,3 +357,29 @@ test("the missing-profile next task opens the existing canonical profile entry",
expect(mockedApi.post).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});
test("application subsection navigation retains metadata and protects unsaved answers", async () => {
renderTable("/jobs/42?section=application&sub=answers&followMode=waiting-update");
fireEvent.click(await screen.findByRole("button", { name: "Make answers dirty" }));
fireEvent.click(screen.getByRole("tab", { name: "Submission" }));
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
expect(screen.getByRole("tabpanel", { name: "Answers & message draft" })).toHaveTextContent("Application drafts section");
fireEvent.click(screen.getByRole("tab", { name: "Submission" }));
fireEvent.click(await screen.findByRole("button", { name: "Discard and leave" }));
expect(await screen.findByRole("tabpanel", { name: "Submission" })).toHaveTextContent("Frozen packages section");
expect(screen.getByTestId("location")).toHaveTextContent("section=application&sub=submission&followMode=waiting-update");
expect(mockedApi.post).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});
test("legacy correspondence and notes reveal distinct domain content", async () => {
mockedApi.get.mockResolvedValue({ data: { ...overview, notes: "Human notes, not application answers" } } as any);
renderTable("/jobs/42?section=communication&followMode=waiting-update");
expect(await screen.findByRole("tabpanel", { name: "Communication" })).toHaveTextContent("Communication section");
fireEvent.click(screen.getByRole("tab", { name: "Notes & tasks" }));
expect(await screen.findByRole("region", { name: "Notes" })).toHaveTextContent("Human notes, not application answers");
expect(screen.getByRole("tabpanel", { name: "Notes & tasks" })).toHaveTextContent("Checklist section");
expect(screen.queryByText("Application drafts section")).not.toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("followMode=waiting-update");
});