1import type {
2 PreviewCloseInput,
3 PreviewSessionSnapshot,
4 ScopedThreadRef,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import { AsyncResult } from "effect/reactivity";
8import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
9
10import {
11 applyPreviewServerSnapshot,
12 readThreadPreviewState,
13 resetPreviewStateForTests,
14} from "~/previewStateStore";
15
16import { useClosedViewStore } from "~/closedViewStore";
17
18import { closePreviewSession } from "./closePreviewSession";
19
20const threadRef = {
21 environmentId: "local" as ScopedThreadRef["environmentId"],
22 threadId: "thread-1" as ScopedThreadRef["threadId"],
23};
24
25const snapshot: PreviewSessionSnapshot = {
26 threadId: threadRef.threadId,
27 tabId: "tab-1",
28 navStatus: {
29 _tag: "Success",
30 url: "http://localhost:3000/",
31 title: "Local app",
32 },
33 canGoBack: false,
34 canGoForward: false,
35 updatedAt: "2026-06-18T19:00:00.000Z",
36};
37
38beforeEach(() => {
39 resetPreviewStateForTests();
40 useClosedViewStore.setState({ entries: [] });
41});
42
43describe("closePreviewSession", () => {
44 it("suppresses stale server snapshots while the close is in flight", async () => {
45 applyPreviewServerSnapshot(threadRef, snapshot);
46 let finishClose: (() => void) | undefined;
47 const closePreview = vi.fn(
48 (_input: PreviewCloseInput) =>
49 new Promise<ReturnType<typeof AsyncResult.success<void>>>((resolve) => {
50 finishClose = () => resolve(AsyncResult.success(undefined));
51 }),
52 );
53
54 const closing = closePreviewSession({
55 closePreview: ({ input }) => closePreview(input),
56 snapshot,
57 tabId: snapshot.tabId,
58 threadRef,
59 });
60
61 expect(useClosedViewStore.getState().entries).toEqual([]);
62 expect(readThreadPreviewState(threadRef).sessions).toEqual({});
63 applyPreviewServerSnapshot(threadRef, snapshot);
64 expect(readThreadPreviewState(threadRef).sessions).toEqual({});
65
66 finishClose?.();
67 await closing;
68 expect(useClosedViewStore.getState().entries).toMatchObject([
69 { kind: "browser", threadRef, snapshot },
70 ]);
71 expect(closePreview).toHaveBeenCalledWith({ threadId: "thread-1", tabId: "tab-1" });
72 });
73
74 it("restores the last snapshot and keeps full history when the server close fails", async () => {
75 applyPreviewServerSnapshot(threadRef, snapshot);
76 for (let index = 0; index < 20; index++) {
77 useClosedViewStore.getState().remember({
78 kind: "browser",
79 threadRef,
80 snapshot: { ...snapshot, tabId: `closed-${index}` },
81 });
82 }
83 const history = useClosedViewStore.getState().entries;
84
85 const result = await closePreviewSession({
86 closePreview: async () => AsyncResult.failure(Cause.fail(new Error("close failed"))),
87 snapshot,
88 tabId: snapshot.tabId,
89 threadRef,
90 });
91
92 expect(result._tag).toBe("Failure");
93 expect(useClosedViewStore.getState().entries).toEqual(history);
94 expect(readThreadPreviewState(threadRef).snapshot).toEqual(snapshot);
95 expect(readThreadPreviewState(threadRef).sessions).toEqual({ [snapshot.tabId]: snapshot });
96 });
97});