| ... | ... | @@ -0,0 +1,130 @@ |
| 1 | import type { LocalApi, PreviewSessionSnapshot, ScopedThreadRef } from "@t3tools/contracts"; |
| 2 | import * as Cause from "effect/Cause"; |
| 3 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 4 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 5 | |
| 6 | import { |
| 7 | openTerminalLinkInPreview, |
| 8 | TerminalLinkContextMenuShowError, |
| 9 | TerminalLinkPreviewOpenError, |
| 10 | } from "./openTerminalLinkInPreview"; |
| 11 | |
| 12 | vi.mock("~/previewStateStore", () => ({ |
| 13 | applyPreviewServerSnapshot: vi.fn(), |
| 14 | isPreviewSupportedInRuntime: () => true, |
| 15 | })); |
| 16 | |
| 17 | vi.mock("~/rightPanelStore", () => ({ |
| 18 | useRightPanelStore: { |
| 19 | getState: () => ({ openBrowser: vi.fn() }), |
| 20 | }, |
| 21 | })); |
| 22 | |
| 23 | const threadRef = { |
| 24 | environmentId: "local" as ScopedThreadRef["environmentId"], |
| 25 | threadId: "thread-1" as ScopedThreadRef["threadId"], |
| 26 | }; |
| 27 | |
| 28 | const snapshot: PreviewSessionSnapshot = { |
| 29 | threadId: threadRef.threadId, |
| 30 | tabId: "tab-1", |
| 31 | navStatus: { _tag: "Idle" }, |
| 32 | canGoBack: false, |
| 33 | canGoForward: false, |
| 34 | updatedAt: "2026-06-20T00:00:00.000Z", |
| 35 | }; |
| 36 | |
| 37 | afterEach(() => { |
| 38 | vi.restoreAllMocks(); |
| 39 | }); |
| 40 | |
| 41 | describe("openTerminalLinkInPreview", () => { |
| 42 | it("preserves context-menu failures with terminal link context before falling back", async () => { |
| 43 | const cause = new Error("menu unavailable"); |
| 44 | const fallbackToBrowser = vi.fn(); |
| 45 | const openPreview = vi.fn(async () => AsyncResult.success(snapshot)); |
| 46 | const reportError = vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 47 | |
| 48 | await openTerminalLinkInPreview({ |
| 49 | url: "http://localhost:3000/path?token=secret", |
| 50 | position: { x: 12, y: 34 }, |
| 51 | threadRef, |
| 52 | openPreview, |
| 53 | localApi: { |
| 54 | contextMenu: { |
| 55 | show: vi.fn(async () => { |
| 56 | throw cause; |
| 57 | }), |
| 58 | }, |
| 59 | } as unknown as LocalApi, |
| 60 | fallbackToBrowser, |
| 61 | }); |
| 62 | |
| 63 | expect(fallbackToBrowser).toHaveBeenCalledOnce(); |
| 64 | expect(openPreview).not.toHaveBeenCalled(); |
| 65 | expect(reportError).toHaveBeenCalledOnce(); |
| 66 | const error = reportError.mock.calls[0]?.[0]; |
| 67 | expect(error).toBeInstanceOf(TerminalLinkContextMenuShowError); |
| 68 | expect(error).toMatchObject({ |
| 69 | environmentId: "local", |
| 70 | threadId: "thread-1", |
| 71 | targetOrigin: "http://localhost:3000", |
| 72 | cause, |
| 73 | }); |
| 74 | expect(error.message).not.toContain("menu unavailable"); |
| 75 | expect(error.targetOrigin).not.toContain("secret"); |
| 76 | }); |
| 77 | |
| 78 | it("preserves the complete preview failure cause before falling back", async () => { |
| 79 | const rpcError = new Error("preview unavailable"); |
| 80 | const cause = Cause.combine(Cause.fail(rpcError), Cause.die("preview defect")); |
| 81 | const fallbackToBrowser = vi.fn(); |
| 82 | const reportError = vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 83 | |
| 84 | await openTerminalLinkInPreview({ |
| 85 | url: "http://127.0.0.1:5173/", |
| 86 | position: { x: 12, y: 34 }, |
| 87 | threadRef, |
| 88 | openPreview: async () => AsyncResult.failure(cause), |
| 89 | localApi: { |
| 90 | contextMenu: { |
| 91 | show: vi.fn(async () => "open-in-preview"), |
| 92 | }, |
| 93 | } as unknown as LocalApi, |
| 94 | fallbackToBrowser, |
| 95 | }); |
| 96 | |
| 97 | expect(fallbackToBrowser).toHaveBeenCalledOnce(); |
| 98 | expect(reportError).toHaveBeenCalledOnce(); |
| 99 | const error = reportError.mock.calls[0]?.[0]; |
| 100 | expect(error).toBeInstanceOf(TerminalLinkPreviewOpenError); |
| 101 | expect(error).toMatchObject({ |
| 102 | environmentId: "local", |
| 103 | threadId: "thread-1", |
| 104 | targetOrigin: "http://127.0.0.1:5173", |
| 105 | cause, |
| 106 | }); |
| 107 | expect(error.message).not.toContain("preview unavailable"); |
| 108 | }); |
| 109 | |
| 110 | it("does not report or fall back when opening the preview is interrupted", async () => { |
| 111 | const fallbackToBrowser = vi.fn(); |
| 112 | const reportError = vi.spyOn(console, "error").mockImplementation(() => undefined); |
| 113 | |
| 114 | await openTerminalLinkInPreview({ |
| 115 | url: "http://localhost:5173/", |
| 116 | position: { x: 12, y: 34 }, |
| 117 | threadRef, |
| 118 | openPreview: async () => AsyncResult.failure(Cause.interrupt()), |
| 119 | localApi: { |
| 120 | contextMenu: { |
| 121 | show: vi.fn(async () => "open-in-preview"), |
| 122 | }, |
| 123 | } as unknown as LocalApi, |
| 124 | fallbackToBrowser, |
| 125 | }); |
| 126 | |
| 127 | expect(reportError).not.toHaveBeenCalled(); |
| 128 | expect(fallbackToBrowser).not.toHaveBeenCalled(); |
| 129 | }); |
| 130 | }); |