1import { act, render, type Screen } from "../../test/render";
2import { EnvironmentId } from "@t3tools/contracts";
3import { AsyncResult } from "effect/reactivity";
4import { StrictMode } from "react";
5import { describe, expect, it, vi } from "vite-plus/test";
6
7vi.mock(import("~/state/session"), async (original) => ({
8 ...(await original()),
9 readEnvironmentScope: () => true,
10 useEnvironmentScope: () => true,
11}));
12
13import {
14 EMPTY_PROJECT_SCRIPT_INPUT,
15 ProjectScriptEditorDialog,
16 type ProjectScriptActionResult,
17 type ProjectScriptEditorRequest,
18} from "./projectScriptEditor";
19
20const onSubmit = vi.fn<Parameters<typeof ProjectScriptEditorDialog>[0]["onSubmit"]>();
21const onClose = vi.fn();
22const onDelete = vi.fn();
23
24function request(name: string, error?: string): ProjectScriptEditorRequest {
25 return {
26 scriptId: name,
27 initial: { ...EMPTY_PROJECT_SCRIPT_INPUT, name, command: `run-${name}` },
28 ...(error === undefined ? {} : { error }),
29 };
30}
31
32const open = (screen: Screen, nextRequest: ProjectScriptEditorRequest) =>
33 screen.rerender(
34 <StrictMode>
35 <ProjectScriptEditorDialog
36 environmentId={EnvironmentId.make("test")}
37 request={nextRequest}
38 scripts={[]}
39 onSubmit={onSubmit}
40 onClose={onClose}
41 onDelete={onDelete}
42 />
43 </StrictMode>,
44 );
45
46const submit = () => document.querySelector("form")!.requestSubmit();
47const saveButton = () => document.querySelector<HTMLButtonElement>('button[type="submit"]')!;
48
49function deferredSave() {
50 let resolve!: (result: ProjectScriptActionResult) => void;
51 let reject!: (error: Error) => void;
52 const promise = new Promise<ProjectScriptActionResult>((resolveResult, rejectResult) => {
53 resolve = resolveResult;
54 reject = rejectResult;
55 });
56 return { promise, resolve, reject };
57}
58
59function setup() {
60 onSubmit.mockReset();
61 onClose.mockReset();
62 onDelete.mockReset();
63}
64
65describe("project action editor save lifecycle", () => {
66 it("records the submit shortcut without submitting the action", async () => {
67 setup();
68 await using screen = await render(null);
69 await open(screen, request("build"));
70 const keybinding = document.querySelector<HTMLInputElement>("#script-keybinding")!;
71 await act(async () => {
72 keybinding.dispatchEvent(
73 new KeyboardEvent("keydown", {
74 key: "Enter",
75 code: "Enter",
76 ctrlKey: true,
77 metaKey: true,
78 bubbles: true,
79 cancelable: true,
80 }),
81 );
82 });
83 expect(keybinding.value).toContain("enter");
84 expect(onSubmit).not.toHaveBeenCalled();
85 expect(onClose).not.toHaveBeenCalled();
86 });
87 it("does not close a replacement request or release its in-flight save", async () => {
88 setup();
89 await using screen = await render(null);
90 const first = deferredSave();
91 const second = deferredSave();
92 onSubmit.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
93 await open(screen, request("build"));
94 await act(async () => submit());
95
96 await open(screen, request("test"));
97 expect(saveButton().disabled).toBe(false);
98 expect(document.querySelector<HTMLInputElement>("#script-name")!.value).toBe("test");
99 await act(async () => submit());
100
101 await act(async () => first.resolve(AsyncResult.success(undefined)));
102 expect(onClose).not.toHaveBeenCalled();
103 expect(saveButton().disabled).toBe(true);
104
105 await act(async () => second.resolve(AsyncResult.success(undefined)));
106 expect(onClose).toHaveBeenCalledTimes(1);
107 expect(onSubmit.mock.calls.map(([scriptId]) => scriptId)).toEqual(["build", "test"]);
108 });
109});