Keep an explicit link to prepared Boat previews

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 12:22:44 -05:00
1 parent 1f358b7079
commit 76a499cc54
2 files changed
+39 -5

No files matched your search

@@ -41,6 +41,35 @@ beforeEach(() => {
afterEach(async () => { if (!unmounted) await unmount(); host.remove(); delete (Element.prototype as { checkVisibility?: unknown }).checkVisibility; vi.restoreAllMocks(); vi.useRealTimers(); });
describe("Computer connection lifetime", () => {
it("offers an explicit preview link when the browser blocks the new window", async () => {
vi.spyOn(window, "open").mockReturnValue(null);
vi.mocked(computersApi.preview).mockResolvedValue({ url: "https://preview.example/app" });
await render();
await act(async () => host.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })));
expect(computersApi.preview).toHaveBeenCalledWith("task-a", "environment-a", 5173);
const link = host.querySelector("a")!;
expect(link.href).toBe("https://preview.example/app");
expect(link.textContent).toBe("Open port 5173 in a new tab");
expect(link.rel).toBe("noopener noreferrer");
await render({ environmentId: "environment-b" });
expect(host.querySelector("a")).toBeNull();
});
it("discards a preview response after the task changes", async () => {
const close = vi.fn();
const replace = vi.fn();
vi.spyOn(window, "open").mockReturnValue({ close, location: { replace } } as unknown as Window);
const pending = deferred<{ url: string }>();
vi.mocked(computersApi.preview).mockReturnValue(pending.promise);
await render();
await act(async () => host.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })));
await render({ issueId: "task-b" });
await act(async () => pending.resolve({ url: "https://preview.example/app" }));
expect(close).toHaveBeenCalledOnce();
expect(replace).not.toHaveBeenCalled();
expect(host.querySelector("a")).toBeNull();
});
it("connects only after the explicit action and keeps credentials in the iframe with no referrer", async () => {
await render(); await tick(60_000);
expect(computersApi.connect).not.toHaveBeenCalled();
@@ -23,19 +23,22 @@ function ComputerConnection({ issueId, environmentId, active = true }: ComputerP
const connectPending = useRef(false);
const [port, setPort] = useState("5173");
const [previewPending, setPreviewPending] = useState(false);
const [preview, setPreview] = useState<{ url: string; port: string } | null>(null);
async function openPreview() {
const previewWindow = window.open("about:blank", "_blank");
if (!previewWindow) { setError("Allow a new tab to open the preview."); return; }
previewWindow.opener = null;
if (previewWindow) previewWindow.opener = null;
setPreviewPending(true);
setPreview(null);
setError(null);
try {
const result = await computersApi.preview(issueId, environmentId, Number(port));
if (mounted.current) previewWindow.location.replace(result.url);
else previewWindow.close();
if (mounted.current) {
setPreview({ url: result.url, port });
previewWindow?.location.replace(result.url);
} else previewWindow?.close();
} catch (cause) {
previewWindow.close();
previewWindow?.close();
if (mounted.current) setError(cause instanceof Error ? cause.message : "Could not open the preview.");
} finally { if (mounted.current) setPreviewPending(false); }
}
@@ -155,6 +158,8 @@ function ComputerConnection({ issueId, environmentId, active = true }: ComputerP
className="min-w-0 flex-1" onChange={event => setPort(event.target.value)} />
<Button type="submit" variant="outline" disabled={previewPending || !port}>Open preview</Button>
</form>
{preview && preview.port === port && <a href={preview.url} target="_blank" rel="noopener noreferrer"
className="text-sm text-primary underline">Open port {preview.port} in a new tab</a>}
</div>
</div>;
}