1import type { EnvironmentId, MergebotRepoQueue } from "@t3tools/contracts";
2import { act, createElement } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
5
6const state = vi.hoisted(() => ({ queue: null as MergebotRepoQueue | null, read: vi.fn() }));
7vi.mock("../../state/environments", () => ({
8 useEnvironment: () => ({
9 serverConfig: { mergebot: { origin: "https://mergebot.example", repositories: ["work/app"] } },
10 }),
11}));
12vi.mock("../../state/mergebot", () => ({
13 useMergebotQueue: (...args: unknown[]) => {
14 state.read(...args);
15 return state.queue;
16 },
17}));
18vi.mock("../ui/badge", () => ({ Badge: (props: object) => createElement("span", props) }));
19vi.mock("../ui/tooltip", () => ({
20 Tooltip: "div",
21 TooltipPopup: "aside",
22 TooltipTrigger: ({
23 render,
24 children,
25 }: {
26 render: React.ReactElement;
27 children: React.ReactNode;
28 }) => createElement(render.type, render.props, children),
29}));
30
31import { MergebotQueueBadge } from "./MergebotQueueBadge";
32const environmentId = "local" as EnvironmentId;
33const repository = "work/app";
34let renderer: ReactTestRenderer | undefined;
35function ready(position: number | null = 2): MergebotRepoQueue {
36 return {
37 repo: repository,
38 url: `https://mergebot.example/${repository}`,
39 status: "ready",
40 processing: true,
41 pullRequests: [
42 {
43 number: 12,
44 headSha: "current",
45 lifecycle: "open",
46 queueStatus: "queued",
47 position,
48 mergeEtaAt: null,
49 url: `https://mergebot.example/${repository}/pull/12`,
50 },
51 ],
52 };
53}
54async function render(repo = repository, headSha = "current") {
55 await act(() => {
56 const badge = (
57 <MergebotQueueBadge
58 environmentId={environmentId}
59 repository={repo}
60 number={12}
61 headSha={headSha}
62 />
63 );
64 if (renderer) renderer.update(badge);
65 else renderer = create(badge);
66 });
67}
68function label() {
69 return renderer!.root.findByType("span").children.join("");
70}
71beforeEach(() => {
72 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
73 state.queue = ready();
74 state.read.mockReset();
75});
76afterEach(async () => {
77 await act(() => renderer?.unmount());
78 renderer = undefined;
79 vi.unstubAllGlobals();
80});
81
82it("does not mount queue polling for a repository Mergebot does not manage", async () => {
83 await render("pingdotgg/t3code");
84 expect(state.read).not.toHaveBeenCalled();
85 expect(renderer!.toJSON()).toBeNull();
86 await render();
87 expect(state.read).toHaveBeenCalledExactlyOnceWith(environmentId, repository);
88 expect(label()).toBe("Mergebot: Queued #2");
89});
90it("replaces a known queue position with Unknown after a head mismatch or unavailable observation", async () => {
91 await render();
92 expect(label()).toBe("Mergebot: Queued #2");
93 await render(repository, "new-push");
94 expect(label()).toBe("Mergebot: Unknown");
95 state.queue = null;
96 await render();
97 expect(label()).toBe("Mergebot: Unknown");
98 state.queue = ready();
99 await render();
100 expect(label()).toBe("Mergebot: Queued #2");
101});
102it("identifies a queue entry without an active position as held", async () => {
103 state.queue = ready(null);
104 await render();
105 expect(label()).toBe("Mergebot: Queued (held)");
106});