Skip to content
Source
Sign inSign up

ThreadAutomationsPanel.permissions.browser.test.tsx

87 lines3.2 KB

1import { EnvironmentId, ThreadId, ScheduledTaskId, type ScheduledTask } from "@t3tools/contracts";
2import { expect, it, vi } from "vite-plus/test";
3​
4const state = vi.hoisted(() => ({ allowed: true, toggle: vi.fn(), run: vi.fn() }));
5vi.mock(import("../../state/session"), async (importOriginal) => ({
6 ...(await importOriginal()),
7 useEnvironmentScope: () => state.allowed,
8 readEnvironmentScope: () => state.allowed,
9}));
10vi.mock(import("@effect/atom-react"), async (importOriginal) => ({
11 ...(await importOriginal()),
12 useAtomValue: (() => state.allowed) as never,
13}));
14vi.mock(import("@tanstack/react-router"), async (importOriginal) => ({
15 ...(await importOriginal()),
16 useNavigate: () => vi.fn(),
17}));
18vi.mock(import("../../state/use-atom-command"), async (importOriginal) => {
19 const { serverEnvironment } = await import("../../state/server");
20 return {
21 ...(await importOriginal()),
22 useAtomCommand: ((command: unknown) =>
23 command === serverEnvironment.setScheduledTaskEnabled ? state.toggle : state.run) as never,
24 };
25});
26vi.mock(import("../../state/query"), async (importOriginal) => ({
27 ...(await importOriginal()),
28 useEnvironmentQuery: (() => ({ data: { tasks: [task] }, error: null })) as never,
29}));
30​
31import { act, render as renderUi } from "../../../test/render";
32import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel";
33const environmentId = EnvironmentId.make("remote");
34const threadId = ThreadId.make("thread");
35const task = {
36 id: ScheduledTaskId.make("task"),
37 threadId,
38 title: "Daily review",
39 enabled: true,
40 schedule: { type: "interval", everyMs: 60000 },
41 lastRunStatus: "never",
42} as ScheduledTask;
43​
44async function setup() {
45 state.allowed = true;
46 state.toggle.mockReset().mockResolvedValue({ _tag: "Success" });
47 state.run.mockReset().mockResolvedValue({ _tag: "Success" });
48​
49 await using stack = new AsyncDisposableStack();
50 const screen = stack.use(await renderUi(null));
51 const container = screen.container;
52 async function renderPanel() {
53 await act(
54 async () =>
55 await screen.rerender(
56 <ThreadAutomationsPanel environmentId={environmentId} threadId={threadId} />,
57 ),
58 );
59 }
60​
61 const byLabel = (label: string) =>
62 container.querySelector<HTMLElement>(`[aria-label="${label}"]`)!;
63​
64 const environment = stack.move();
65 return { renderPanel, byLabel, [Symbol.asyncDispose]: () => environment.disposeAsync() };
67it.each([true, false])("runs scheduled work only with task permission (%s)", async (allowed) => {
68 await using fixture = await setup();
69 const { renderPanel, byLabel } = fixture;
70 state.allowed = allowed;
71 await renderPanel();
72 await act(async () => byLabel("Run Daily review now").click());
73 expect(state.run).toHaveBeenCalledTimes(allowed ? 1 : 0);
74});
75​
76it("rejects a retained toggle after task permission is revoked", async () => {
77 await using fixture = await setup();
78 const { renderPanel, byLabel } = fixture;
79 await renderPanel();
80 state.allowed = false;
81 await act(async () => byLabel("Pause Daily review").click());
82 expect(state.toggle).not.toHaveBeenCalled();
83​
84 state.allowed = true;
85 await act(async () => byLabel("Pause Daily review").click());
86 expect(state.toggle).toHaveBeenCalledOnce();
87});