Files
lda-wf/web/apps/console/src/workspace/routes/LifecycleRoute.test.tsx
T

180 lines
6.0 KiB
TypeScript

import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { LifecycleExplorerController } from "../../lifecycle/useLifecycleExplorer.js";
import type { LifecycleState } from "../../lifecycle/state.js";
import { LifecycleRoute } from "./LifecycleRoute.js";
const { mockCreateLifecycleClients, mockUseLifecycleExplorer } = vi.hoisted(() => ({
mockCreateLifecycleClients: vi.fn(),
mockUseLifecycleExplorer: vi.fn(),
}));
vi.mock("../context.js", () => ({
useConsoleWorkspace: () => ({
readExecutor: { run: vi.fn() },
}),
}));
vi.mock("../domain/lifecycle-clients.js", () => ({
createLifecycleClients: mockCreateLifecycleClients,
}));
vi.mock("../../lifecycle/useLifecycleExplorer.js", () => ({
useLifecycleExplorer: mockUseLifecycleExplorer,
}));
const emptyState = (): LifecycleState => ({
artifactList: {
phase: "loaded",
value: { items: [], total: 0, nextCursor: null },
},
deploymentList: { phase: "loaded", value: { items: [] } },
runList: {
phase: "loaded",
value: { items: [], total: 0, nextCursor: null },
},
selectedArtifactId: null,
artifactDetail: null,
selectedDeploymentId: null,
deploymentDetail: null,
deploymentValidation: null,
selectedRunId: null,
runDetail: null,
trace: null,
errors: [],
});
const controller = (state: LifecycleState = emptyState()): LifecycleExplorerController => ({
state,
selectArtifact: vi.fn(),
selectDeployment: vi.fn(),
selectRun: vi.fn(),
refresh: vi.fn(),
loadMoreArtifacts: vi.fn(),
loadMoreRuns: vi.fn(),
loadTrace: vi.fn(),
});
const LocationProbe = () => <output data-testid="location">{useLocation().pathname}</output>;
const renderRoute = (entry: string) =>
render(
<MemoryRouter initialEntries={[entry]}>
<Routes>
<Route path="/console/artifacts" element={<LifecycleRoute kind="artifact" />} />
<Route path="/console/artifacts/:artifactId/:version" element={<LifecycleRoute kind="artifact" />} />
<Route path="/console/deployments" element={<LifecycleRoute kind="deployment" />} />
<Route path="/console/deployments/:deploymentId" element={<LifecycleRoute kind="deployment" />} />
<Route path="/console/runs" element={<LifecycleRoute kind="run" />} />
<Route path="/console/runs/:runId" element={<LifecycleRoute kind="run" />} />
</Routes>
<LocationProbe />
</MemoryRouter>,
);
beforeEach(() => {
mockCreateLifecycleClients.mockReset().mockReturnValue({
artifacts: {},
deployments: {},
runs: {},
});
mockUseLifecycleExplorer.mockReset().mockReturnValue(controller());
});
afterEach(() => cleanup());
describe("LifecycleRoute", () => {
it.each([
["/console/artifacts/report/2", "artifact", "selectArtifact", "report@2", "Artifacts"],
["/console/deployments/report.default", "deployment", "selectDeployment", "report.default", "Deployments"],
["/console/runs/run_123", "run", "selectRun", "run_123", "Runs"],
] as const)("synchronizes %s with the %s selection", async (entry, kind, selector, identity, heading) => {
const currentController = controller();
mockUseLifecycleExplorer.mockReturnValue(currentController);
renderRoute(entry);
expect(await screen.findByRole("heading", { name: heading, level: 1 })).toBeInTheDocument();
await waitFor(() => {
expect(currentController[selector]).toHaveBeenCalledWith(identity);
});
expect(mockCreateLifecycleClients).toHaveBeenCalledTimes(1);
expect(mockUseLifecycleExplorer).toHaveBeenCalledWith(
expect.objectContaining({ artifacts: {}, deployments: {}, runs: {} }),
);
expect(kind).toBeDefined();
});
it("renders a collection without selecting a record", async () => {
const currentController = controller();
mockUseLifecycleExplorer.mockReturnValue(currentController);
renderRoute("/console/artifacts");
expect(await screen.findByRole("heading", { name: "Artifacts", level: 1 })).toBeInTheDocument();
expect(currentController.selectArtifact).not.toHaveBeenCalled();
});
it("suppresses stale detail synchronously until it matches the URL identity", () => {
const currentController = controller({
...emptyState(),
selectedArtifactId: "old@1",
artifactDetail: {
artifactId: "old",
version: 1,
title: "Old artifact",
kind: "workflow",
description: null,
outcomes: [],
plan: { nodes: [], edges: [] },
requiredCapabilities: [],
workflowDependencies: {},
createdFromCatalogVersion: null,
},
});
mockUseLifecycleExplorer.mockReturnValue(currentController);
renderRoute("/console/artifacts/report/2");
expect(screen.queryByText("Old artifact")).toBeNull();
expect(currentController.selectArtifact).toHaveBeenCalledWith("report@2");
});
it("navigates to the canonical detail URL before selection follows it", async () => {
const currentController = controller({
...emptyState(),
artifactList: {
phase: "loaded",
value: {
items: [
{
key: "report@2",
artifactId: "report",
version: 2,
kind: "workflow",
displayName: "Report",
description: null,
outcomes: ["ok"],
requiredSources: [],
diagnosticCount: 0,
},
],
total: 1,
nextCursor: null,
},
},
});
mockUseLifecycleExplorer.mockReturnValue(currentController);
renderRoute("/console/artifacts");
await userEvent.click(screen.getByRole("option", { name: /Report version 2/i }));
await waitFor(() => {
expect(screen.getByTestId("location")).toHaveTextContent("/console/artifacts/report/2");
expect(currentController.selectArtifact).toHaveBeenCalledWith("report@2");
});
});
});