20308a2d16
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
132 lines
5.3 KiB
TypeScript
132 lines
5.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { useDialogFormState } from "./task-create-dialog-state";
|
|
import { useDefaultSelectionsEffect } from "./task-create-dialog-effects";
|
|
import type { StoreSelections } from "./task-create-dialog-types";
|
|
|
|
// `useBranchesByURL` triggers a real network ensure() when given a URL — stub
|
|
// it so the dialog state hook can mount in JSDOM without hitting fetch. The
|
|
// stubbed shape mirrors the production hook (branches/loading/ensure).
|
|
vi.mock("@/hooks/domains/github/use-branches-by-url", () => ({
|
|
useBranchesByURL: () => ({
|
|
branches: () => [],
|
|
loading: () => false,
|
|
ensure: () => undefined,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/hooks/domains/github/use-pr-info-by-url", async (importOriginal) => {
|
|
const original =
|
|
await importOriginal<typeof import("@/hooks/domains/github/use-pr-info-by-url")>();
|
|
return {
|
|
...original,
|
|
usePRInfoByURL: () => ({
|
|
info: () => undefined,
|
|
loading: () => false,
|
|
ensure: () => undefined,
|
|
clear: () => undefined,
|
|
}),
|
|
};
|
|
});
|
|
|
|
const SEEDED_AUTOPICKED_PROFILE = "profile-autopicked";
|
|
|
|
describe("useDialogFormState — seededExecutorProfileId", () => {
|
|
// The submit flow decides whether the user "changed" the runner by
|
|
// comparing the final selection to whatever the dialog itself seeded, so
|
|
// only a write that actually originated from autopick/stored-profile
|
|
// seeding may become that baseline — never a value the user picked.
|
|
it("is null until a value is seeded", () => {
|
|
const { result } = renderHook(() => useDialogFormState(true, "ws-1", null));
|
|
expect(result.current.seededExecutorProfileId).toBeNull();
|
|
});
|
|
|
|
it("captures a seed write and keeps it despite later user changes", () => {
|
|
const { result } = renderHook(() => useDialogFormState(true, "ws-1", null));
|
|
|
|
act(() => {
|
|
result.current.setExecutorProfileIdFromSeed(SEEDED_AUTOPICKED_PROFILE);
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBe(SEEDED_AUTOPICKED_PROFILE);
|
|
|
|
act(() => {
|
|
result.current.setExecutorProfileId("profile-user-chosen");
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBe(SEEDED_AUTOPICKED_PROFILE);
|
|
|
|
// Changing back to the seeded value doesn't create a second "seed".
|
|
act(() => {
|
|
result.current.setExecutorProfileId(SEEDED_AUTOPICKED_PROFILE);
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBe(SEEDED_AUTOPICKED_PROFILE);
|
|
});
|
|
|
|
// Regression: a user fast enough to pick a runner before the seed effect
|
|
// fires (e.g. an edit-mode stored profile that arrives a tick after open)
|
|
// must not have that pick mistaken for the seed. Racing the user's
|
|
// setExecutorProfileId ahead of setExecutorProfileIdFromSeed must not let
|
|
// the user's own choice masquerade as "what the dialog put there".
|
|
it("does not treat a user pick that arrives before the seed write as the seed", () => {
|
|
const { result } = renderHook(() => useDialogFormState(true, "ws-1", null));
|
|
|
|
act(() => {
|
|
result.current.setExecutorProfileId("profile-user-picked-first");
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBeNull();
|
|
|
|
act(() => {
|
|
result.current.setExecutorProfileIdFromSeed(SEEDED_AUTOPICKED_PROFILE);
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBe(SEEDED_AUTOPICKED_PROFILE);
|
|
});
|
|
|
|
it("resets to null on the next open cycle", () => {
|
|
const { result, rerender } = renderHook(
|
|
({ open }: { open: boolean }) => useDialogFormState(open, "ws-1", null),
|
|
{ initialProps: { open: true } },
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setExecutorProfileIdFromSeed("profile-first-cycle");
|
|
});
|
|
expect(result.current.seededExecutorProfileId).toBe("profile-first-cycle");
|
|
|
|
// Close then reopen: a rising edge bumps openCycle and the reset effects
|
|
// clear executorProfileId back to "".
|
|
rerender({ open: false });
|
|
rerender({ open: true });
|
|
|
|
expect(result.current.seededExecutorProfileId).toBeNull();
|
|
});
|
|
|
|
// Regression: on the same open-transition, the reset effect that bumps
|
|
// openCycle (useFormResetEffects) and the stored-profile seed effect
|
|
// (useDefaultSelectionsEffect) both fire and their setState calls land in
|
|
// the same subsequent render. A reset keyed on comparing openCycle in the
|
|
// render body cannot tell "openCycle changed because the dialog just
|
|
// opened" apart from "openCycle changed and a seed write from that very
|
|
// open already landed" — it must not wipe a seed that arrived on the same
|
|
// transition that triggered it.
|
|
it("keeps a stored-profile seed written on the same open transition that bumps openCycle", async () => {
|
|
const editingTaskExecutorProfileId = "profile-from-stored-task";
|
|
const sel: StoreSelections = {
|
|
agentProfiles: [],
|
|
compatibleAgentProfiles: [],
|
|
authLoaded: true,
|
|
executors: [],
|
|
workspaceDefaults: null,
|
|
};
|
|
|
|
const { result } = renderHook(() => {
|
|
const fs = useDialogFormState(true, "ws-1", null);
|
|
useDefaultSelectionsEffect(fs, true, sel, [], editingTaskExecutorProfileId);
|
|
return fs;
|
|
});
|
|
|
|
await waitFor(() =>
|
|
expect(result.current.executorProfileId).toBe(editingTaskExecutorProfileId),
|
|
);
|
|
expect(result.current.seededExecutorProfileId).toBe(editingTaskExecutorProfileId);
|
|
});
|
|
});
|