| 1 | import { act, create, type ReactTestRenderer } from "react-test-renderer"; |
| 2 | import { beforeEach, afterEach, expect, it, vi } from "vite-plus/test"; |
| 3 | |
| 4 | const state = vi.hoisted(() => ({ |
| 5 | id: 0, |
| 6 | saved: false, |
| 7 | session: { |
| 8 | _tag: "Success", |
| 9 | waiting: false, |
| 10 | value: { authenticated: true, permissions: ["orchestration:operate"] }, |
| 11 | }, |
| 12 | add: vi.fn((_notice: unknown) => "notice"), |
| 13 | close: vi.fn(), |
| 14 | save: vi.fn(), |
| 15 | navigate: vi.fn(), |
| 16 | })); |
| 17 | vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.session })); |
| 18 | vi.mock("@tanstack/react-router", () => ({ useNavigate: () => state.navigate })); |
| 19 | vi.mock("../state/environments", () => ({ |
| 20 | useEnvironments: () => ({ |
| 21 | environments: [{ environmentId: `env-${state.id}`, label: "Work laptop" }], |
| 22 | }), |
| 23 | })); |
| 24 | vi.mock("../state/session", () => ({ environmentSession: { sessionStateAtom: () => null } })); |
| 25 | vi.mock("../hooks/useLocalStorage", () => ({ |
| 26 | getLocalStorageItem: () => state.saved, |
| 27 | setLocalStorageItem: (...args: unknown[]) => state.save(...args), |
| 28 | })); |
| 29 | vi.mock("./ui/toast", () => ({ toastManager: { add: state.add, close: state.close } })); |
| 30 | import { PermissionUpdateNotice } from "./PermissionUpdateNotice"; |
| 31 | |
| 32 | let renderer: ReactTestRenderer; |
| 33 | beforeEach(() => { |
| 34 | state.id++; |
| 35 | state.saved = false; |
| 36 | state.session = { |
| 37 | _tag: "Success", |
| 38 | waiting: false, |
| 39 | value: { authenticated: true, permissions: ["orchestration:operate"] }, |
| 40 | }; |
| 41 | vi.clearAllMocks(); |
| 42 | }); |
| 43 | afterEach(async () => { |
| 44 | if (renderer) await act(async () => renderer.unmount()); |
| 45 | }); |
| 46 | async function render() { |
| 47 | await act(async () => { |
| 48 | renderer = create(<PermissionUpdateNotice />); |
| 49 | }); |
| 50 | } |
| 51 | |
| 52 | it("keeps the notice visible and persists dismissal, with a route to pairing settings", async () => { |
| 53 | await render(); |
| 54 | const notice = state.add.mock.calls[0]![0] as unknown as { |
| 55 | timeout: number; |
| 56 | onClose: () => void; |
| 57 | actionProps: { onClick: () => void }; |
| 58 | }; |
| 59 | expect(notice.timeout).toBe(0); |
| 60 | expect(state.save).not.toHaveBeenCalled(); |
| 61 | notice.actionProps.onClick(); |
| 62 | expect(state.navigate).toHaveBeenCalledWith({ to: "/settings/connections" }); |
| 63 | expect(state.close).toHaveBeenCalledWith("notice"); |
| 64 | expect(state.save).toHaveBeenCalledWith( |
| 65 | `t3code:permission-update:v1:env-${state.id}`, |
| 66 | true, |
| 67 | expect.anything(), |
| 68 | ); |
| 69 | }); |
| 70 | it("does not repeat on session refresh or remount", async () => { |
| 71 | await render(); |
| 72 | await act(async () => renderer.unmount()); |
| 73 | state.session = { ...state.session }; |
| 74 | await render(); |
| 75 | expect(state.add).toHaveBeenCalledTimes(1); |
| 76 | }); |
| 77 | it("skips a notice dismissed on an earlier launch", async () => { |
| 78 | state.saved = true; |
| 79 | await render(); |
| 80 | expect(state.add).not.toHaveBeenCalled(); |
| 81 | }); |
| 82 | it("waits for a successful, settled session check", async () => { |
| 83 | state.session._tag = "Failure"; |
| 84 | await render(); |
| 85 | expect(state.add).not.toHaveBeenCalled(); |
| 86 | state.session = { ...state.session, _tag: "Success", waiting: true }; |
| 87 | await act(async () => renderer.update(<PermissionUpdateNotice />)); |
| 88 | expect(state.add).not.toHaveBeenCalled(); |
| 89 | state.session = { ...state.session, waiting: false }; |
| 90 | await act(async () => renderer.update(<PermissionUpdateNotice />)); |
| 91 | expect(state.add).toHaveBeenCalledTimes(1); |
| 92 | }); |
| 93 | it("does not warn for a granular grant", async () => { |
| 94 | state.session.value.permissions.push("filesystem:read"); |
| 95 | await render(); |
| 96 | expect(state.add).not.toHaveBeenCalled(); |
| 97 | }); |
| 98 | |
| 99 | it("persists the secondary dismissal without relying on the toast close callback", async () => { |
| 100 | await render(); |
| 101 | const notice = state.add.mock.calls[0]![0] as { |
| 102 | data: { secondaryActionProps: { onClick: () => void } }; |
| 103 | }; |
| 104 | notice.data.secondaryActionProps.onClick(); |
| 105 | expect(state.save).toHaveBeenCalledWith( |
| 106 | `t3code:permission-update:v1:env-${state.id}`, |
| 107 | true, |
| 108 | expect.anything(), |
| 109 | ); |
| 110 | expect(state.close).toHaveBeenCalledWith("notice"); |
| 111 | }); |