Add per-profile keyword sets, job source settings, sponsorship signal pills, and several ATS extractors. Fix Hiring Cafe discovery via Next.js SSR search, profile activate for comma-separated basicAuthUser aliases, and resume path backfill migrations. Update settings and hiring-cafe docs; localhost compose overlay for loopback-only deploys.
157 lines
4.2 KiB
TypeScript
157 lines
4.2 KiB
TypeScript
import * as api from "@client/api";
|
|
import { useSettings } from "@client/hooks/useSettings";
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
import type React from "react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { renderWithQueryClient } from "../test/renderWithQueryClient";
|
|
import { OnboardingGate } from "./OnboardingGate";
|
|
|
|
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
|
|
renderWithQueryClient(ui);
|
|
|
|
vi.mock("@client/api", () => ({
|
|
getDemoInfo: vi.fn(),
|
|
validateLlm: vi.fn(),
|
|
updateSettings: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@client/hooks/useSettings", () => ({
|
|
useSettings: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@client/pages/settings/components/SettingsInput", () => ({
|
|
SettingsInput: ({
|
|
label,
|
|
inputProps,
|
|
}: {
|
|
label: string;
|
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
}) => (
|
|
<label>
|
|
<span>{label}</span>
|
|
<input {...inputProps} />
|
|
</label>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/alert-dialog", () => ({
|
|
AlertDialog: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
AlertDialogContent: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
AlertDialogDescription: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
AlertDialogHeader: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
AlertDialogTitle: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/select", () => ({
|
|
Select: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
SelectContent: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
SelectItem: ({ children }: { children: React.ReactNode }) => (
|
|
<div>{children}</div>
|
|
),
|
|
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
|
<button type="button">{children}</button>
|
|
),
|
|
SelectValue: ({ children }: { children: React.ReactNode }) => (
|
|
<span>{children}</span>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/progress", () => ({
|
|
Progress: () => <div>Progress</div>,
|
|
}));
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: {
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
success: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const settingsResponse = {
|
|
settings: {
|
|
llmProvider: { value: "openrouter", default: "openrouter", override: null },
|
|
llmApiKeyHint: null,
|
|
localResumeProfilePath: null,
|
|
localResumeFileConfigured: false,
|
|
},
|
|
isLoading: false,
|
|
refreshSettings: vi.fn(),
|
|
};
|
|
|
|
describe("OnboardingGate", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(api.getDemoInfo).mockResolvedValue({
|
|
demoMode: false,
|
|
resetCadenceHours: 6,
|
|
lastResetAt: null,
|
|
nextResetAt: null,
|
|
baselineVersion: null,
|
|
baselineName: null,
|
|
});
|
|
vi.mocked(useSettings).mockReturnValue(settingsResponse as any);
|
|
});
|
|
|
|
it("renders the gate when LLM validation fails", async () => {
|
|
vi.mocked(api.validateLlm).mockResolvedValue({
|
|
valid: false,
|
|
message: "Invalid",
|
|
});
|
|
|
|
render(<OnboardingGate />);
|
|
|
|
await waitFor(() => expect(api.validateLlm).toHaveBeenCalled());
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Welcome to Job Ops")).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.queryByLabelText("Local resume path"),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.queryByText("Resume file")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("hides the gate when LLM validation succeeds", async () => {
|
|
vi.mocked(api.validateLlm).mockResolvedValue({
|
|
valid: true,
|
|
message: null,
|
|
});
|
|
|
|
render(<OnboardingGate />);
|
|
|
|
await waitFor(() => expect(api.validateLlm).toHaveBeenCalled());
|
|
expect(screen.queryByText("Welcome to Job Ops")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("hides the gate for providers without API keys", async () => {
|
|
vi.mocked(useSettings).mockReturnValue({
|
|
...settingsResponse,
|
|
settings: {
|
|
...settingsResponse.settings,
|
|
llmProvider: { value: "ollama", default: "ollama", override: null },
|
|
},
|
|
} as any);
|
|
|
|
render(<OnboardingGate />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("Welcome to Job Ops")).not.toBeInTheDocument();
|
|
});
|
|
expect(api.validateLlm).not.toHaveBeenCalled();
|
|
});
|
|
});
|