| 1 | import { |
| 2 | DEFAULT_CLIENT_SETTINGS, |
| 3 | type PreviewSessionSnapshot, |
| 4 | type ScopedThreadRef, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import * as Cause from "effect/Cause"; |
| 7 | import { AsyncResult } from "effect/reactivity"; |
| 8 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 9 | |
| 10 | import { type ClosedViewEntry, useClosedViewStore } from "./closedViewStore"; |
| 11 | import { __setClientSettingsForTests } from "./hooks/useSettings"; |
| 12 | import { readThreadPreviewState, resetPreviewStateForTests } from "./previewStateStore"; |
| 13 | import { |
| 14 | planNextReopen, |
| 15 | pullRequestsSearchForRestore, |
| 16 | type ReopenOwnerState, |
| 17 | reopenClosedView, |
| 18 | } from "./reopenClosedView"; |
| 19 | import { |
| 20 | selectThreadRightPanelState, |
| 21 | type ThreadRightPanelState, |
| 22 | useRightPanelStore, |
| 23 | } from "./rightPanelStore"; |
| 24 | |
| 25 | const threadRef = { |
| 26 | environmentId: "local" as ScopedThreadRef["environmentId"], |
| 27 | threadId: "thread-1" as ScopedThreadRef["threadId"], |
| 28 | }; |
| 29 | const snapshot: PreviewSessionSnapshot = { |
| 30 | threadId: threadRef.threadId, |
| 31 | tabId: "old-tab", |
| 32 | navStatus: { _tag: "Success", url: "https://example.com", title: "Example" }, |
| 33 | canGoBack: false, |
| 34 | canGoForward: false, |
| 35 | viewport: { _tag: "freeform", width: 1200, height: 800 }, |
| 36 | profileId: "work", |
| 37 | updatedAt: "2026-09-22T12:00:00.000Z", |
| 38 | }; |
| 39 | |
| 40 | beforeEach(() => { |
| 41 | __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS); |
| 42 | resetPreviewStateForTests(); |
| 43 | useClosedViewStore.setState({ entries: [] }); |
| 44 | useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} }); |
| 45 | }); |
| 46 | |
| 47 | describe("reopenClosedView", () => { |
| 48 | it("restores a file tab at its line without creating a browser session", async () => { |
| 49 | const openPreview = vi.fn(); |
| 50 | const reopened = await reopenClosedView( |
| 51 | { |
| 52 | kind: "panel-tab", |
| 53 | threadRef, |
| 54 | surface: { |
| 55 | kind: "file", |
| 56 | id: "file:src/app.ts", |
| 57 | relativePath: "src/app.ts", |
| 58 | revealLine: 18, |
| 59 | revealRequestId: 1, |
| 60 | }, |
| 61 | }, |
| 62 | { openPreview, workspaceAvailable: true }, |
| 63 | ); |
| 64 | expect(reopened).toBe(true); |
| 65 | const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef); |
| 66 | expect(state.isOpen).toBe(true); |
| 67 | expect(state.surfaces).toMatchObject([{ id: "file:src/app.ts", revealLine: 18 }]); |
| 68 | expect(openPreview).not.toHaveBeenCalled(); |
| 69 | }); |
| 70 | |
| 71 | it("does not reopen workspace tabs without an available project", async () => { |
| 72 | const options = { |
| 73 | openPreview: vi.fn(), |
| 74 | workspaceAvailable: false, |
| 75 | }; |
| 76 | for (const surface of [ |
| 77 | { kind: "files", id: "files" }, |
| 78 | { |
| 79 | kind: "file", |
| 80 | id: "file:src/app.ts", |
| 81 | relativePath: "src/app.ts", |
| 82 | revealLine: null, |
| 83 | revealRequestId: 0, |
| 84 | }, |
| 85 | ] as const) { |
| 86 | expect(await reopenClosedView({ kind: "panel-tab", threadRef, surface }, options)).toBe( |
| 87 | false, |
| 88 | ); |
| 89 | } |
| 90 | expect( |
| 91 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces, |
| 92 | ).toEqual([]); |
| 93 | }); |
| 94 | |
| 95 | it("recreates a browser tab with saved URL, viewport and profile, then selects its new ID", async () => { |
| 96 | const openPreview = vi.fn(async () => AsyncResult.success({ ...snapshot, tabId: "new-tab" })); |
| 97 | const result = await reopenClosedView( |
| 98 | { kind: "browser", threadRef, snapshot }, |
| 99 | { openPreview, workspaceAvailable: false }, |
| 100 | ); |
| 101 | expect(result).toBe(true); |
| 102 | expect(openPreview).toHaveBeenCalledWith({ |
| 103 | environmentId: threadRef.environmentId, |
| 104 | input: { |
| 105 | threadId: threadRef.threadId, |
| 106 | url: "https://example.com", |
| 107 | viewport: snapshot.viewport, |
| 108 | profileId: "work", |
| 109 | }, |
| 110 | }); |
| 111 | expect(readThreadPreviewState(threadRef).snapshot?.tabId).toBe("new-tab"); |
| 112 | expect( |
| 113 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef) |
| 114 | .activeSurfaceId, |
| 115 | ).toBe("browser:new-tab"); |
| 116 | }); |
| 117 | |
| 118 | it("leaves the panel unchanged when a browser resource fails to reopen", async () => { |
| 119 | const result = await reopenClosedView( |
| 120 | { kind: "browser", threadRef, snapshot }, |
| 121 | { |
| 122 | openPreview: async () => AsyncResult.failure(Cause.fail(new Error("offline"))), |
| 123 | workspaceAvailable: false, |
| 124 | }, |
| 125 | ); |
| 126 | expect(result).toBe(false); |
| 127 | expect( |
| 128 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces, |
| 129 | ).toEqual([]); |
| 130 | }); |
| 131 | }); |
| 132 | |
| 133 | describe("planNextReopen", () => { |
| 134 | const entries: ClosedViewEntry[] = [ |
| 135 | { id: "a", kind: "panel-tab", threadRef, surface: { kind: "diff", id: "diff" } }, |
| 136 | { id: "b", kind: "panel-tab", threadRef, surface: { kind: "files", id: "files" } }, |
| 137 | ]; |
| 138 | const known: ReopenOwnerState = { |
| 139 | environmentKnown: true, |
| 140 | catalogReady: true, |
| 141 | ownerExists: true, |
| 142 | shellLive: true, |
| 143 | panel: { isOpen: false, activeSurfaceId: null, surfaces: [] }, |
| 144 | }; |
| 145 | const openDiff: ThreadRightPanelState = { |
| 146 | isOpen: true, |
| 147 | activeSurfaceId: "diff", |
| 148 | surfaces: [{ kind: "diff", id: "diff" }], |
| 149 | }; |
| 150 | |
| 151 | it.each([ |
| 152 | ["restores the newest entry", [known, known], [], "a"], |
| 153 | [ |
| 154 | "drops a removed environment once the catalog is ready", |
| 155 | [{ ...known, environmentKnown: false }, known], |
| 156 | ["a"], |
| 157 | "b", |
| 158 | ], |
| 159 | [ |
| 160 | "waits for the catalog before dropping", |
| 161 | [{ ...known, environmentKnown: false, catalogReady: false }, known], |
| 162 | [], |
| 163 | null, |
| 164 | ], |
| 165 | [ |
| 166 | "skips but keeps a thread deleted from a live shell", |
| 167 | [{ ...known, ownerExists: false }, known], |
| 168 | [], |
| 169 | "b", |
| 170 | ], |
| 171 | [ |
| 172 | "waits when a cached shell lacks the thread", |
| 173 | [{ ...known, ownerExists: false, shellLive: false }, known], |
| 174 | [], |
| 175 | null, |
| 176 | ], |
| 177 | ["drops a tab that is already open", [{ ...known, panel: openDiff }, known], ["a"], "b"], |
| 178 | [ |
| 179 | "restores a tab whose panel is hidden", |
| 180 | [{ ...known, panel: { ...openDiff, isOpen: false } }], |
| 181 | [], |
| 182 | "a", |
| 183 | ], |
| 184 | ] as const)("%s", (_name, owners, dropped, restored) => { |
| 185 | const plan = planNextReopen( |
| 186 | entries.slice(0, owners.length), |
| 187 | (e) => owners[entries.indexOf(e)]!, |
| 188 | ); |
| 189 | expect(plan.drop.map((e) => e.id)).toEqual(dropped); |
| 190 | expect(plan.restore?.id ?? null).toBe(restored); |
| 191 | }); |
| 192 | }); |
| 193 | |
| 194 | describe("pullRequestsSearchForRestore", () => { |
| 195 | it("replaces the selection and keeps list filters", () => { |
| 196 | const search = pullRequestsSearchForRestore( |
| 197 | { |
| 198 | involvement: "reviewing", |
| 199 | state: "open", |
| 200 | q: "mine", |
| 201 | host: "filter.example.com", |
| 202 | selectedHost: "old", |
| 203 | number: 1, |
| 204 | }, |
| 205 | { |
| 206 | kind: "pull-request", |
| 207 | id: "pull-request:example", |
| 208 | repository: "owner/repo", |
| 209 | number: 42, |
| 210 | projectId: "project-1", |
| 211 | environmentId: "remote", |
| 212 | host: "github.example.com", |
| 213 | }, |
| 214 | ); |
| 215 | expect(search).toEqual({ |
| 216 | involvement: "reviewing", |
| 217 | state: "open", |
| 218 | q: "mine", |
| 219 | host: "filter.example.com", |
| 220 | repository: "owner/repo", |
| 221 | number: 42, |
| 222 | selectedProjectId: "project-1", |
| 223 | selectedHost: "github.example.com", |
| 224 | selectedEnvironmentId: "remote", |
| 225 | }); |
| 226 | }); |
| 227 | |
| 228 | it("clears a stale selection and fills default filters when nothing is selected", () => { |
| 229 | expect(pullRequestsSearchForRestore({ repository: "old/repo", number: 7 }, null)).toEqual({ |
| 230 | involvement: "all", |
| 231 | state: "open", |
| 232 | }); |
| 233 | }); |
| 234 | }); |