1import { beforeEach, expect, it, vi } from "vite-plus/test";
2import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
3
4const state = vi.hoisted(() => ({
5 navigate: vi.fn(),
6 panel: vi.fn(),
7 hasProject: true,
8}));
9vi.mock("react", async (original) => ({
10 ...(await original<typeof import("react")>()),
11 useCallback: (callback: unknown) => callback,
12 useMemo: (fn: () => unknown) => fn(),
13}));
14vi.mock("../components/ui/toast", () => ({
15 toastManager: { add: vi.fn() },
16 stackedThreadToast: (value: unknown) => value,
17}));
18vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));
19vi.mock("@tanstack/react-router", () => ({ useNavigate: () => state.navigate }));
20vi.mock("../browser/useOpenLink", () => ({ useOpenLink: () => vi.fn() }));
21vi.mock("../state/server", () => ({ serverEnvironment: {} }));
22vi.mock("../state/environments", () => ({ usePrimaryEnvironmentId: () => "env" }));
23vi.mock("../rightPanelStore", () => ({
24 useRightPanelStore: { getState: () => ({ openPullRequest: state.panel }) },
25}));
26vi.mock("../state/entities", () => ({
27 readThreadShell: () => ({ projectId: "project" }),
28 useProjects: () =>
29 state.hasProject
30 ? [
31 {
32 id: "project",
33 environmentId: "env",
34 repositoryIdentity: {
35 provider: "github",
36 canonicalKey: "github.com/work/app",
37 locator: {
38 source: "git-remote",
39 remoteName: "origin",
40 remoteUrl: "https://github.com/work/app.git",
41 },
42 },
43 },
44 ]
45 : [],
46 useServerConfigs: () =>
47 new Map([
48 [
49 "env",
50 {
51 mergebot: { origin: "https://mergebot.example", repositories: ["Work/App"] },
52 environment: { capabilities: { pullRequests: true, threadPullRequests: true } },
53 },
54 ],
55 ]),
56}));
57import { useOpenChangeRequestLink } from "./openPullRequestLink";
58const ref = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
59const event = () => ({
60 preventDefault: vi.fn(),
61 stopPropagation: vi.fn(),
62 metaKey: false,
63 ctrlKey: false,
64});
65beforeEach(() => {
66 state.hasProject = true;
67 state.navigate.mockReset();
68 state.panel.mockReset();
69});
70it.each(["https://github.com/work/app/pull/12", "https://mergebot.example/work/app/pull/12"])(
71 "opens %s in the owned PR panel that embeds Mergebot",
72 (url) => {
73 expect(useOpenChangeRequestLink(ref)(event(), url)).toBe(true);
74 expect(state.panel).toHaveBeenCalledWith(
75 ref,
76 expect.objectContaining({
77 projectId: ProjectId.make("project"),
78 host: "github.com",
79 repository: expect.stringMatching(/^work\/app$/i),
80 number: 12,
81 }),
82 );
83 expect(state.navigate).not.toHaveBeenCalled();
84 },
85);
86it.each(["https://mergebot.example/work/app/pull/12", "https://github.com/work/app/pull/12"])(
87 "opens %s in the PR page without inventing a project or conversation",
88 (url) => {
89 state.hasProject = false;
90 expect(useOpenChangeRequestLink()(event(), url)).toBe(true);
91 expect(state.navigate).toHaveBeenCalledWith({
92 to: "/pull-requests",
93 search: expect.objectContaining({
94 mergebot: "env",
95 repository: expect.stringMatching(/^work\/app$/i),
96 number: 12,
97 }),
98 });
99 expect(state.panel).not.toHaveBeenCalled();
100 },
101);
102it("leaves lookalike Mergebot origins as ordinary links", () => {
103 expect(
104 useOpenChangeRequestLink(ref)(event(), "https://mergebot.example.evil/work/app/pull/12"),
105 ).toBe(false);
106 expect(state.panel).not.toHaveBeenCalled();
107 expect(state.navigate).not.toHaveBeenCalled();
108});