diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx
index 1e304df..59a10a9 100644
--- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx
+++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx
@@ -1,9 +1,10 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { fireEvent, render, screen } from "@testing-library/react";
-import { MemoryRouter } from "react-router-dom";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { OrchestratorPage } from "./OrchestratorPage";
import type { Job } from "../../shared/types";
+import type { FilterTab } from "./orchestrator/constants";
const jobFixture: Job = {
id: "job-1",
@@ -63,6 +64,8 @@ const jobFixture: Job = {
updatedAt: "2025-01-02T00:00:00Z",
};
+const job2: Job = { ...jobFixture, id: "job-2", status: "discovered" };
+
const createMatchMedia = (matches: boolean) =>
vi.fn().mockImplementation((query: string) => ({
matches,
@@ -76,9 +79,9 @@ const createMatchMedia = (matches: boolean) =>
vi.mock("./orchestrator/useOrchestratorData", () => ({
useOrchestratorData: () => ({
- jobs: [jobFixture],
+ jobs: [jobFixture, job2],
stats: {
- discovered: 0,
+ discovered: 1,
processing: 0,
ready: 1,
applied: 0,
@@ -109,7 +112,21 @@ vi.mock("./orchestrator/OrchestratorSummary", () => ({
}));
vi.mock("./orchestrator/OrchestratorFilters", () => ({
- OrchestratorFilters: () =>
,
+ OrchestratorFilters: ({
+ onTabChange,
+ onSearchQueryChange,
+ onSortChange,
+ }: {
+ onTabChange: (t: FilterTab) => void;
+ onSearchQueryChange: (q: string) => void;
+ onSortChange: (s: any) => void;
+ }) => (
+
+
+
+
+
+ ),
}));
vi.mock("./orchestrator/JobDetailPanel", () => ({
@@ -120,7 +137,12 @@ vi.mock("./orchestrator/JobListPanel", () => ({
JobListPanel: ({ onSelectJob, selectedJobId }: { onSelectJob: (id: string) => void; selectedJobId: string | null }) => (
{selectedJobId ?? "none"}
-
+
+
),
}));
@@ -129,23 +151,109 @@ vi.mock("../components", () => ({
ManualImportSheet: () => ,
}));
+const LocationWatcher = () => {
+ const location = useLocation();
+ return {location.pathname + location.search}
;
+};
+
describe("OrchestratorPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
+ it("syncs tab selection to the URL", () => {
+ window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia;
+
+ render(
+
+
+
+ } />
+ } />
+
+
+ );
+
+ fireEvent.click(screen.getByText("To Discovered"));
+ expect(screen.getByTestId("location").textContent).toContain("/discovered");
+ });
+
+ it("syncs job selection to the URL", async () => {
+ window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia;
+
+ render(
+
+
+
+ } />
+ } />
+
+
+ );
+
+ // Initial load will auto-select the first matching job (job-1 for all tab)
+ const locationText = () => screen.getByTestId("location").textContent;
+ expect(locationText()).toContain("/all/job-1");
+
+ // Clicking job-2 should update URL
+ const job2Button = screen.getByTestId("select-job-2");
+ fireEvent.click(job2Button);
+
+ // Wait for URL to update
+ await waitFor(() => {
+ expect(locationText()).toContain("/all/job-2");
+ });
+ });
+
+ it("syncs search query to URL as a parameter", () => {
+ window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia;
+
+ render(
+
+
+
+ } />
+ } />
+
+
+ );
+
+ fireEvent.click(screen.getByText("Set Search"));
+ expect(screen.getByTestId("location").textContent).toContain("q=test+search");
+ });
+
+ it("syncs sorting to URL and removes it when default", () => {
+ window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia;
+
+ render(
+
+
+
+ } />
+ } />
+
+
+ );
+
+ fireEvent.click(screen.getByText("Set Sort"));
+ expect(screen.getByTestId("location").textContent).toContain("sort=title-asc");
+ });
+
it("opens the detail drawer on mobile when a job is selected", () => {
window.matchMedia = createMatchMedia(false) as unknown as typeof window.matchMedia;
render(
-
-
+
+
+ } />
+ } />
+
);
expect(screen.queryByTestId("detail-panel")).not.toBeInTheDocument();
- fireEvent.click(screen.getByRole("button", { name: /select job/i }));
+ fireEvent.click(screen.getByTestId("select-job-1"));
expect(screen.getByTestId("detail-panel")).toBeInTheDocument();
});
@@ -154,8 +262,11 @@ describe("OrchestratorPage", () => {
window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia;
render(
-
-
+
+
+ } />
+ } />
+
);
diff --git a/orchestrator/src/client/pages/orchestrator/JobListPanel.tsx b/orchestrator/src/client/pages/orchestrator/JobListPanel.tsx
index f7f0541..b41a54d 100644
--- a/orchestrator/src/client/pages/orchestrator/JobListPanel.tsx
+++ b/orchestrator/src/client/pages/orchestrator/JobListPanel.tsx
@@ -50,6 +50,7 @@ export const JobListPanel: React.FC = ({
key={job.id}
type="button"
onClick={() => onSelectJob(job.id)}
+ data-testid={`select-${job.id}`}
className={cn(
"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors",
isSelected