1import { AuthSourceControlWriteScope, EnvironmentId } from "@t3tools/contracts";
2import { act } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
5
6const permissions = vi.hoisted(() => ({
7 allowed: new Set<string>(),
8 listeners: new Set<() => void>(),
9}));
10
11vi.mock("~/state/session", async () => {
12 const { useSyncExternalStore } = await import("react");
13 const canWrite = (environmentId: string, scope: string) =>
14 scope === AuthSourceControlWriteScope && permissions.allowed.has(environmentId);
15 return {
16 readEnvironmentScope: canWrite,
17 useEnvironmentScope: (environmentId: string, scope: string) =>
18 useSyncExternalStore(
19 (listener) => {
20 permissions.listeners.add(listener);
21 return () => permissions.listeners.delete(listener);
22 },
23 () => canWrite(environmentId, scope),
24 ),
25 };
26});
27vi.mock("~/lib/utils", () => ({ cn: () => "" }));
28vi.mock("../ui/button", () => ({ Button: "button" }));
29vi.mock("../ui/textarea", () => ({ Textarea: "textarea" }));
30vi.mock("../ui/toggle-group", () => ({ Toggle: "button", ToggleGroup: "div" }));
31vi.mock("./PullRequestMarkdown", () => ({ PullRequestMarkdown: "article" }));
32
33import { PullRequestMarkdownEditor } from "./PullRequestMarkdownEditor";
34
35const primary = EnvironmentId.make("primary");
36const secondary = EnvironmentId.make("secondary");
37let renderer: ReactTestRenderer | undefined;
38const onSave = vi.fn<(next: string) => void>();
39const onCancel = vi.fn<() => void>();
40
41beforeEach(() => {
42 permissions.allowed.clear();
43 permissions.listeners.clear();
44 renderer = undefined;
45 onSave.mockReset();
46 onCancel.mockReset();
47 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
48});
49
50afterEach(async () => {
51 await act(() => renderer?.unmount());
52 vi.unstubAllGlobals();
53});
54
55async function openEditor(options?: { value?: string; allowEmpty?: boolean }) {
56 await act(() => {
57 renderer = create(
58 <PullRequestMarkdownEditor
59 value={options?.value ?? "Original description"}
60 cwd="/repo"
61 environmentId={secondary}
62 label="Pull request description"
63 saving={false}
64 allowEmpty={options?.allowEmpty ?? false}
65 onSave={onSave}
66 onCancel={onCancel}
67 />,
68 );
69 });
70}
71
72function saveHandler(): () => void {
73 return renderer!.root.findByProps({ children: "Save" }).props.onClick;
74}
75
76async function grant(allowed: boolean) {
77 await act(() => {
78 if (allowed) permissions.allowed.add(secondary);
79 else permissions.allowed.delete(secondary);
80 permissions.listeners.forEach((listener) => listener());
81 });
82}
83
84it("does not save a secondary environment's description using the primary grant", async () => {
85 permissions.allowed.add(primary);
86 await openEditor();
87
88 await act(() => saveHandler()());
89
90 expect(onSave).not.toHaveBeenCalled();
91});
92
93it("rejects a retained Save callback when the grant changes before React updates", async () => {
94 permissions.allowed.add(secondary);
95 await openEditor();
96 const save = saveHandler();
97 permissions.allowed.delete(secondary);
98
99 await act(() => save());
100
101 expect(onSave).not.toHaveBeenCalled();
102});
103
104it("keeps an edited draft through revocation and saves it after access returns without remounting", async () => {
105 permissions.allowed.add(secondary);
106 await openEditor();
107 await act(() => {
108 renderer!.root.findByType("textarea").props.onChange({
109 target: { value: "Keep these unsaved edits" },
110 });
111 });
112
113 await grant(false);
114 await act(() => saveHandler()());
115 expect(onSave).not.toHaveBeenCalled();
116
117 await grant(true);
118 await act(() => saveHandler()());
119 expect(onSave).toHaveBeenCalledExactlyOnceWith("Keep these unsaved edits");
120});
121
122it("saves with source-control permission alone", async () => {
123 permissions.allowed.add(secondary);
124 await openEditor();
125
126 await act(() => saveHandler()());
127
128 expect(onSave).toHaveBeenCalledExactlyOnceWith("Original description");
129});
130
131it("allows clearing a description only after a pending grant becomes available", async () => {
132 await openEditor({ value: "", allowEmpty: true });
133 await act(() => saveHandler()());
134 expect(onSave).not.toHaveBeenCalled();
135
136 await grant(true);
137 await act(() => saveHandler()());
138 expect(onSave).toHaveBeenCalledExactlyOnceWith("");
139});
140
141it("keeps cancelling a local edit available without a write grant", async () => {
142 await openEditor();
143
144 await act(() => renderer!.root.findByProps({ children: "Cancel" }).props.onClick());
145
146 expect(onCancel).toHaveBeenCalledOnce();
147 expect(onSave).not.toHaveBeenCalled();
148});