mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 23:20:53 +01:00
chore: reconcile contribution rules and onboarding fixtures
Extract the guided setup test fixture without dropping cases. Supply current settings/discovery contracts and Opus 5.5 in the isolated Storybook boundary. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
21c4f8afd5
commit
48d4ab54d6
19 files changed
+277
-232
No files matched your search
@@ -3,7 +3,6 @@ import { Field } from "./agent-config-primitives";
|
||||
import { SelectPopover } from "./ui/select";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
/** The override is request-level. Automatic selection is resolved by the server. */
|
||||
export function AgentRunnerSelect({
|
||||
value,
|
||||
onChange,
|
||||
|
||||
@@ -178,7 +178,6 @@ describe("OnboardingWizard — which step it lands on", () => {
|
||||
dialogState.onboardingOpen = false;
|
||||
dialogState.onboardingOptions = {};
|
||||
dialogState.onboardingRouteDismissed = false;
|
||||
// The scoped discovery contract must resolve before the hire seam runs.
|
||||
mockAdaptersApi.list.mockResolvedValue(["claude_local", "codex_local"].map(type => ({
|
||||
type, loaded: true, disabled: false, defaultRunner: "legacy", supportedRunners: ["legacy"],
|
||||
})));
|
||||
|
||||
@@ -3180,7 +3180,6 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
|
||||
const { root, queryClient } = await openStep4({ adapterType: "claude_local" });
|
||||
try {
|
||||
await act(async () => {
|
||||
// A warm value remains successful while its current fetch is pending.
|
||||
if (pending === "environments") queryClient.setQueryData(queryKeys.environments.list("company-new"), [LOCAL_ENVIRONMENT]);
|
||||
else if (pending === "settings") queryClient.setQueryData(queryKeys.instance.settings, { defaultEnvironmentId: null });
|
||||
else queryClient.setQueryData(queryKeys.instance.experimentalSettings, { enableManagedSandboxOnly: false });
|
||||
|
||||
@@ -1060,9 +1060,6 @@ function OnboardingWizardInner({
|
||||
}, effectiveOnboardingOpen && step === 4 && canUseLocalLogin && credentialMode !== "api" &&
|
||||
Boolean(managedProvider) && !savedSubscription && !savedKeys.storedLogin.data && !managedBindingForStep(),
|
||||
);
|
||||
// A result from a previous selection must not hire or advance this wizard.
|
||||
// Discovery starts after environment settings resolve. A later target or
|
||||
// runner change invalidates the configuration an in-flight test was built from.
|
||||
useEffect(() => {
|
||||
autoConnectStartedRef.current = false;
|
||||
hiringAgentRef.current = null;
|
||||
|
||||
@@ -1775,8 +1775,6 @@ export function CompanyImport() {
|
||||
applyChanges(original.adapterSchemaValues ?? {}, configVals.adapterSchemaValues ?? {});
|
||||
applyChanges(baseline, edited);
|
||||
if (selectedType === "claude_local") {
|
||||
// The native builder preserves source schema fields. Project common
|
||||
// form edits separately so a removed binding cannot be restored there.
|
||||
const commonBuild = getUIAdapter("claude_local").buildAdapterConfig;
|
||||
const commonBefore = commonBuild(original);
|
||||
const commonAfter = commonBuild(configVals);
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { NewAgent } from "./NewAgent";
|
||||
import { ApiError } from "@/api/client";
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
adapterModels: vi.fn(),
|
||||
list: vi.fn(),
|
||||
hire: vi.fn(),
|
||||
testEnvironment: vi.fn(),
|
||||
getAdapterAuthSignal: vi.fn(),
|
||||
getClaudeOAuthTokenStatus: vi.fn(),
|
||||
}));
|
||||
const envApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn() }));
|
||||
const settings = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
getExperimental: vi.fn(),
|
||||
getGeneral: vi.fn(),
|
||||
}));
|
||||
const secrets = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
list: vi.fn(),
|
||||
listMyUserSecrets: vi.fn(),
|
||||
createUserSecretDefinition: vi.fn(),
|
||||
createMyUserSecret: vi.fn(),
|
||||
rotateMyUserSecret: vi.fn(),
|
||||
removeUserSecretDefinition: vi.fn(),
|
||||
}));
|
||||
const state = vi.hoisted(() => ({
|
||||
params: new URLSearchParams(),
|
||||
adapters: [] as object[],
|
||||
navigate: vi.fn(),
|
||||
openNewIssue: vi.fn(),
|
||||
}));
|
||||
const managedApi = vi.hoisted(() => ({
|
||||
list: vi.fn(async () => ({ currentUserId: "user-1", canManageConnections: true, connections: [] })),
|
||||
create: vi.fn(async () => ({ connectionId: "managed-connection", grantId: "managed-grant" })),
|
||||
setDefault: vi.fn(async () => ({})),
|
||||
}));
|
||||
vi.mock("@/api/ai-connections", () => ({ aiConnectionsApi: managedApi }));
|
||||
vi.mock("@/api/agents", () => ({ agentsApi: api }));
|
||||
vi.mock("@/api/environments", () => ({ environmentsApi: envApi }));
|
||||
vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: settings }));
|
||||
vi.mock("@/api/secrets", () => ({ secretsApi: secrets }));
|
||||
const adapterApi = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
vi.mock("@/api/adapters", () => ({ adaptersApi: adapterApi }));
|
||||
vi.mock("@/context/CompanyContext", () => ({
|
||||
useCompany: () => ({ selectedCompanyId: "company-1" }),
|
||||
}));
|
||||
vi.mock("../context/BreadcrumbContext", () => ({
|
||||
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
|
||||
}));
|
||||
vi.mock("@/context/DialogContext", () => ({
|
||||
useDialogActions: () => ({ openNewIssue: state.openNewIssue }),
|
||||
}));
|
||||
vi.mock("@/lib/router", () => ({
|
||||
useNavigate: () => state.navigate,
|
||||
useSearchParams: () => [state.params],
|
||||
}));
|
||||
vi.mock("@/components/AgentConfigForm", () => ({
|
||||
ModelDropdown: ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<input
|
||||
aria-label="Model"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
|
||||
<button onClick={() => onConnected("login-session")}>
|
||||
Complete subscription login
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/onboarding/PillGuy", () => ({ PillGuy: () => null }));
|
||||
vi.mock("motion/react", () => ({
|
||||
AnimatePresence: ({ children }: any) => children,
|
||||
MotionConfig: ({ children }: any) => children,
|
||||
motion: {
|
||||
span: ({ children }: any) => <span>{children}</span>,
|
||||
div: ({ children, initial, animate, exit, transition, ...rest }: any) => (
|
||||
<div {...rest}>{children}</div>
|
||||
),
|
||||
},
|
||||
}));
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
let cache: QueryClient;
|
||||
async function settle() {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
});
|
||||
}
|
||||
async function click(text: string) {
|
||||
const button = [...container.querySelectorAll("button")].find(
|
||||
(b) => b.textContent?.trim() === text,
|
||||
);
|
||||
expect(button, `Missing button ${text}`).toBeTruthy();
|
||||
await act(async () => button!.click());
|
||||
await settle();
|
||||
}
|
||||
async function fill(label: string, value: string) {
|
||||
const input = container.querySelector(
|
||||
`[aria-label="${label}"]`,
|
||||
) as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
await act(async () => {
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
)!.set!.call(input, value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
async function render(adapter = "pi_local", runnerProvider = "codex") {
|
||||
state.params = new URLSearchParams({
|
||||
name: "Atlas",
|
||||
adapterType: adapter,
|
||||
runnerProvider,
|
||||
});
|
||||
await renderCurrentParams();
|
||||
}
|
||||
async function renderCurrentParams() {
|
||||
await act(async () =>
|
||||
root.render(
|
||||
<QueryClientProvider client={cache}>
|
||||
<TooltipProvider><NewAgent /></TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
);
|
||||
await settle();
|
||||
}
|
||||
async function connect(provider: string) {
|
||||
await click(provider + "Subscription");
|
||||
await click("Connect");
|
||||
}
|
||||
const pass = {
|
||||
adapterType: "pi_local",
|
||||
status: "pass",
|
||||
checks: [
|
||||
{ code: "hello_probe_passed", level: "info", message: "Model replied" },
|
||||
],
|
||||
testedAt: "2026-09-07T00:00:00Z",
|
||||
};
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
adapterApi.list.mockImplementation(async () => state.adapters);
|
||||
managedApi.list.mockResolvedValue({ currentUserId: "user-1", canManageConnections: true, connections: [] });
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
cache = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
state.adapters = [
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
"paperclip_runner", "cursor_cloud", "cursor", "gemini_local", "kimi_local", "grok_local", "hermes_local", "hermes_gateway",
|
||||
].map((type) => ({ type, loaded: true, disabled: false }));
|
||||
api.adapterModels.mockResolvedValue([]);
|
||||
api.list.mockResolvedValue([{ id: "ceo", role: "ceo", status: "idle" }]);
|
||||
api.getAdapterAuthSignal.mockResolvedValue({ status: "present" });
|
||||
api.getClaudeOAuthTokenStatus.mockRejectedValue(new ApiError("Not found", 404, null));
|
||||
api.testEnvironment.mockResolvedValue(pass);
|
||||
api.hire.mockImplementation(async (_company, input) => ({
|
||||
agent: { ...input, id: "new-agent", status: "idle", urlKey: "atlas" },
|
||||
}));
|
||||
envApi.list.mockResolvedValue([
|
||||
{ id: "local-1", name: "Local", driver: "local", config: {} },
|
||||
]);
|
||||
envApi.capabilities.mockResolvedValue({ sandboxProviders: {} });
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" });
|
||||
settings.getExperimental.mockResolvedValue({ enableNativeRunner: true });
|
||||
settings.getGeneral.mockResolvedValue({ executionMode: "any" });
|
||||
secrets.list.mockResolvedValue([]);
|
||||
secrets.create.mockResolvedValue({ id: "org-secret-1" });
|
||||
secrets.remove.mockResolvedValue({ ok: true });
|
||||
secrets.listMyUserSecrets.mockResolvedValue([]);
|
||||
secrets.createUserSecretDefinition.mockResolvedValue({ id: "definition-1" });
|
||||
secrets.createMyUserSecret.mockResolvedValue({ id: "secret-1" });
|
||||
secrets.removeUserSecretDefinition.mockResolvedValue({ ok: true });
|
||||
});
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
cache.clear();
|
||||
container.remove();
|
||||
});
|
||||
|
||||
export { api, envApi, settings, secrets, state, managedApi, adapterApi, root, container, cache, settle, click, fill, render, connect, pass, renderCurrentParams };
|
||||
@@ -1,204 +1,13 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { NewAgent } from "./NewAgent";
|
||||
import { ApiError } from "@/api/client";
|
||||
import {
|
||||
api, envApi, settings, secrets, state, managedApi, adapterApi,
|
||||
root, container, cache, settle, click, fill, render, connect, pass,
|
||||
renderCurrentParams,
|
||||
} from "./NewAgent.test-support";
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
adapterModels: vi.fn(),
|
||||
list: vi.fn(),
|
||||
hire: vi.fn(),
|
||||
testEnvironment: vi.fn(),
|
||||
getAdapterAuthSignal: vi.fn(),
|
||||
getClaudeOAuthTokenStatus: vi.fn(),
|
||||
}));
|
||||
const envApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn() }));
|
||||
const settings = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
getExperimental: vi.fn(),
|
||||
getGeneral: vi.fn(),
|
||||
}));
|
||||
const secrets = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
list: vi.fn(),
|
||||
listMyUserSecrets: vi.fn(),
|
||||
createUserSecretDefinition: vi.fn(),
|
||||
createMyUserSecret: vi.fn(),
|
||||
rotateMyUserSecret: vi.fn(),
|
||||
removeUserSecretDefinition: vi.fn(),
|
||||
}));
|
||||
const state = vi.hoisted(() => ({
|
||||
params: new URLSearchParams(),
|
||||
adapters: [] as object[],
|
||||
navigate: vi.fn(),
|
||||
openNewIssue: vi.fn(),
|
||||
}));
|
||||
const managedApi = vi.hoisted(() => ({
|
||||
list: vi.fn(async () => ({ currentUserId: "user-1", canManageConnections: true, connections: [] })),
|
||||
create: vi.fn(async () => ({ connectionId: "managed-connection", grantId: "managed-grant" })),
|
||||
setDefault: vi.fn(async () => ({})),
|
||||
}));
|
||||
vi.mock("@/api/ai-connections", () => ({ aiConnectionsApi: managedApi }));
|
||||
vi.mock("@/api/agents", () => ({ agentsApi: api }));
|
||||
vi.mock("@/api/environments", () => ({ environmentsApi: envApi }));
|
||||
vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: settings }));
|
||||
vi.mock("@/api/secrets", () => ({ secretsApi: secrets }));
|
||||
const adapterApi = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
vi.mock("@/api/adapters", () => ({ adaptersApi: adapterApi }));
|
||||
vi.mock("@/context/CompanyContext", () => ({
|
||||
useCompany: () => ({ selectedCompanyId: "company-1" }),
|
||||
}));
|
||||
vi.mock("../context/BreadcrumbContext", () => ({
|
||||
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
|
||||
}));
|
||||
vi.mock("@/context/DialogContext", () => ({
|
||||
useDialogActions: () => ({ openNewIssue: state.openNewIssue }),
|
||||
}));
|
||||
vi.mock("@/lib/router", () => ({
|
||||
useNavigate: () => state.navigate,
|
||||
useSearchParams: () => [state.params],
|
||||
}));
|
||||
// Exercise API/persistence contracts with deterministic presentation primitives.
|
||||
// The actual searchable dropdown and login panel are covered by their tests and browser verification.
|
||||
vi.mock("@/components/AgentConfigForm", () => ({
|
||||
ModelDropdown: ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<input
|
||||
aria-label="Model"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
|
||||
<button onClick={() => onConnected("login-session")}>
|
||||
Complete subscription login
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/onboarding/PillGuy", () => ({ PillGuy: () => null }));
|
||||
vi.mock("motion/react", () => ({
|
||||
AnimatePresence: ({ children }: any) => children,
|
||||
MotionConfig: ({ children }: any) => children,
|
||||
motion: {
|
||||
span: ({ children }: any) => <span>{children}</span>,
|
||||
div: ({ children, initial, animate, exit, transition, ...rest }: any) => (
|
||||
<div {...rest}>{children}</div>
|
||||
),
|
||||
},
|
||||
}));
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
let cache: QueryClient;
|
||||
async function settle() {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
});
|
||||
}
|
||||
async function click(text: string) {
|
||||
const button = [...container.querySelectorAll("button")].find(
|
||||
(b) => b.textContent?.trim() === text,
|
||||
);
|
||||
expect(button, `Missing button ${text}`).toBeTruthy();
|
||||
await act(async () => button!.click());
|
||||
await settle();
|
||||
}
|
||||
async function fill(label: string, value: string) {
|
||||
const input = container.querySelector(
|
||||
`[aria-label="${label}"]`,
|
||||
) as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
await act(async () => {
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
)!.set!.call(input, value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
async function render(adapter = "pi_local", runnerProvider = "codex") {
|
||||
state.params = new URLSearchParams({
|
||||
name: "Atlas",
|
||||
adapterType: adapter,
|
||||
runnerProvider,
|
||||
});
|
||||
await act(async () =>
|
||||
root.render(
|
||||
<QueryClientProvider client={cache}>
|
||||
<TooltipProvider><NewAgent /></TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
);
|
||||
await settle();
|
||||
}
|
||||
async function connect(provider: string) {
|
||||
await click(provider + "Subscription");
|
||||
await click("Connect");
|
||||
}
|
||||
const pass = {
|
||||
adapterType: "pi_local",
|
||||
status: "pass",
|
||||
checks: [
|
||||
{ code: "hello_probe_passed", level: "info", message: "Model replied" },
|
||||
],
|
||||
testedAt: "2026-09-07T00:00:00Z",
|
||||
};
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
adapterApi.list.mockImplementation(async () => state.adapters);
|
||||
managedApi.list.mockResolvedValue({ currentUserId: "user-1", canManageConnections: true, connections: [] });
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
cache = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
state.adapters = [
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
"paperclip_runner", "cursor_cloud", "cursor", "gemini_local", "kimi_local", "grok_local", "hermes_local", "hermes_gateway",
|
||||
].map((type) => ({ type, loaded: true, disabled: false }));
|
||||
api.adapterModels.mockResolvedValue([]);
|
||||
api.list.mockResolvedValue([{ id: "ceo", role: "ceo", status: "idle" }]);
|
||||
api.getAdapterAuthSignal.mockResolvedValue({ status: "present" });
|
||||
api.getClaudeOAuthTokenStatus.mockRejectedValue(new ApiError("Not found", 404, null));
|
||||
api.testEnvironment.mockResolvedValue(pass);
|
||||
api.hire.mockImplementation(async (_company, input) => ({
|
||||
agent: { ...input, id: "new-agent", status: "idle", urlKey: "atlas" },
|
||||
}));
|
||||
envApi.list.mockResolvedValue([
|
||||
{ id: "local-1", name: "Local", driver: "local", config: {} },
|
||||
]);
|
||||
envApi.capabilities.mockResolvedValue({ sandboxProviders: {} });
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" });
|
||||
settings.getExperimental.mockResolvedValue({ enableNativeRunner: true });
|
||||
settings.getGeneral.mockResolvedValue({ executionMode: "any" });
|
||||
secrets.list.mockResolvedValue([]);
|
||||
secrets.create.mockResolvedValue({ id: "org-secret-1" });
|
||||
secrets.remove.mockResolvedValue({ ok: true });
|
||||
secrets.listMyUserSecrets.mockResolvedValue([]);
|
||||
secrets.createUserSecretDefinition.mockResolvedValue({ id: "definition-1" });
|
||||
secrets.createMyUserSecret.mockResolvedValue({ id: "secret-1" });
|
||||
secrets.removeUserSecretDefinition.mockResolvedValue({ ok: true });
|
||||
});
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
cache.clear();
|
||||
container.remove();
|
||||
});
|
||||
describe("New agent setup", () => {
|
||||
it("waits for the current instance default instead of testing cached SSH discovery", async () => {
|
||||
let finish!: (value: object) => void;
|
||||
@@ -663,14 +472,7 @@ describe("New agent setup", () => {
|
||||
adapterType: "pi_local",
|
||||
createdAgentId: "saved-agent",
|
||||
});
|
||||
await act(async () =>
|
||||
root.render(
|
||||
<QueryClientProvider client={cache}>
|
||||
<TooltipProvider><NewAgent /></TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
);
|
||||
await settle();
|
||||
await renderCurrentParams();
|
||||
expect(container.textContent).toContain("Your agent is ready");
|
||||
expect(container.textContent).not.toContain("Finish setup");
|
||||
expect(api.hire).not.toHaveBeenCalled();
|
||||
|
||||
Reference in new issue
Block a user