import { Schema } from "effect";import { describe, expect, it } from "vite-plus/test"; import { DiscoveredLocalServer, PreviewEvent, PreviewNavStatus, PreviewSessionSnapshot, PreviewViewportSetting,} from "./preview.ts";import { PreviewAutomationHost, PreviewAutomationError, PreviewAutomationOpenInput, PreviewAutomationResizeInput, PreviewAutomationResizeResult, PreviewAutomationStatus,} from "./previewAutomation.ts"; const decodePreviewEvent = Schema.decodeUnknownSync(PreviewEvent);const decodeSnapshot = Schema.decodeUnknownSync(PreviewSessionSnapshot);const decodeNavStatus = Schema.decodeUnknownSync(PreviewNavStatus);const decodeServer = Schema.decodeUnknownSync(DiscoveredLocalServer);const decodeViewport = Schema.decodeUnknownSync(PreviewViewportSetting);const decodeResizeInput = Schema.decodeUnknownSync(PreviewAutomationResizeInput);const decodeOpenInput = Schema.decodeUnknownSync(PreviewAutomationOpenInput);const decodeResizeResult = Schema.decodeUnknownSync(PreviewAutomationResizeResult);const decodeAutomationHost = Schema.decodeUnknownSync(PreviewAutomationHost);const decodeAutomationError = Schema.decodeUnknownSync(PreviewAutomationError);const decodeAutomationStatus = Schema.decodeUnknownSync(PreviewAutomationStatus); describe("PreviewAutomationOpenInput", () => { it("accepts the inline preview visibility flag", () => { expect(decodeOpenInput({ open: false })).toEqual({ open: false }); }); it("retains the legacy show visibility alias", () => { expect(decodeOpenInput({ show: false })).toEqual({ show: false }); });}); describe("PreviewNavStatus", () => { it("decodes Idle", () => { expect(decodeNavStatus({ _tag: "Idle" })).toEqual({ _tag: "Idle" }); }); it("decodes Loading with title", () => { expect(decodeNavStatus({ _tag: "Loading", url: "http://localhost:5173/", title: "" })).toEqual({ _tag: "Loading", url: "http://localhost:5173/", title: "", }); }); it("decodes LoadFailed with code/description", () => { expect( decodeNavStatus({ _tag: "LoadFailed", url: "https://example.com/", title: "Example", code: -105, description: "ERR_NAME_NOT_RESOLVED", }), ).toEqual({ _tag: "LoadFailed", url: "https://example.com/", title: "Example", code: -105, description: "ERR_NAME_NOT_RESOLVED", }); }); it("rejects empty url", () => { expect(() => decodeNavStatus({ _tag: "Loading", url: "", title: "" })).toThrow(); });}); describe("PreviewSessionSnapshot", () => { it("round-trips a Success snapshot", () => { const snapshot = decodeSnapshot({ threadId: "thread-1", tabId: "preview-thread-1", navStatus: { _tag: "Success", url: "http://localhost:5173/", title: "Vite App", }, canGoBack: false, canGoForward: false, updatedAt: "2026-01-01T00:00:00.000Z", }); expect(snapshot.tabId).toBe("preview-thread-1"); expect(snapshot.navStatus._tag).toBe("Success"); });}); describe("PreviewViewportSetting", () => { it("decodes fill, freeform, and preset modes", () => { expect(decodeViewport({ _tag: "fill" })).toEqual({ _tag: "fill" }); expect(decodeViewport({ _tag: "freeform", width: 1024, height: 768 })).toEqual({ _tag: "freeform", width: 1024, height: 768, }); expect( decodeViewport({ _tag: "preset", presetId: "iphone-15-pro", width: 393, height: 852, }), ).toMatchObject({ _tag: "preset", presetId: "iphone-15-pro" }); }); it("rejects unsafe dimensions and oversized render areas", () => { expect(() => decodeViewport({ _tag: "freeform", width: 100, height: 800 })).toThrow(); expect(() => decodeViewport({ _tag: "freeform", width: 3840, height: 3840 })).toThrow(); });}); describe("PreviewAutomationResizeInput", () => { it("requires fields that match the selected mode", () => { expect(decodeResizeInput({ mode: "fill" })).toEqual({ mode: "fill" }); expect( decodeResizeInput({ mode: "preset", preset: "pixel-7", orientation: "landscape" }), ).toMatchObject({ mode: "preset", preset: "pixel-7" }); expect(() => decodeResizeInput({ mode: "preset", preset: "pixel-8" })).toThrow(); expect(() => decodeResizeInput({ mode: "freeform", width: 1024 })).toThrow(); expect(() => decodeResizeInput({ mode: "fill", width: 1024, height: 768 })).toThrow(); }); it("allows fill-mode measurements below the minimum selectable fixed size", () => { expect( decodeResizeResult({ tabId: "preview-t", setting: { _tag: "fill" }, viewport: { width: 180, height: 120 }, }).viewport, ).toEqual({ width: 180, height: 120 }); });}); describe("preview automation tab targeting", () => { it("accepts an explicit tab and rejects contradictory open behavior", () => { expect(decodeResizeInput({ tabId: "tab-app", mode: "fill" })).toMatchObject({ tabId: "tab-app", mode: "fill", }); expect(decodeOpenInput({ tabId: "tab-app", reuseExistingTab: true })).toMatchObject({ tabId: "tab-app", reuseExistingTab: true, }); expect(() => decodeOpenInput({ tabId: "tab-app", reuseExistingTab: false })).toThrow(); });}); describe("PreviewAutomationHost", () => { it("accepts legacy hosts and current operation advertisements", () => { expect(decodeAutomationHost({ clientId: "legacy", environmentId: "environment-1" })).toEqual({ clientId: "legacy", environmentId: "environment-1", }); expect( decodeAutomationHost({ clientId: "current", environmentId: "environment-1", supportedOperations: ["status", "resize"], }).supportedOperations, ).toEqual(["status", "resize"]); });}); describe("PreviewAutomationError", () => { it("preserves a typed non-editable target failure", () => { const error = decodeAutomationError({ _tag: "PreviewAutomationTargetNotEditableError", operation: "type", environmentId: "environment-1", threadId: "thread-1", providerSessionId: "provider-session-1", providerInstanceId: "codex", clientId: "client-1", connectionId: "connection-1", requestId: "request-1", tabId: "tab-1", timeoutMs: 1_000, remoteTag: "PreviewAutomationTargetNotEditableError", remoteMessageLength: 12, cause: {}, selectorKind: "focused-element", }); expect(error._tag).toBe("PreviewAutomationTargetNotEditableError"); if (error._tag === "PreviewAutomationTargetNotEditableError") { expect(error.selectorKind).toBe("focused-element"); expect(error.message).toBe("Preview automation type requires an editable focused element."); } });}); describe("PreviewAutomationStatus", () => { it("accepts old hosts without viewport data and exposes it from current hosts", () => { const base = { available: true, visible: false, tabId: "preview-t", url: "https://example.com", title: "Example", loading: false, }; expect(decodeAutomationStatus(base)).toEqual(base); expect( decodeAutomationStatus({ ...base, viewportSetting: { _tag: "preset", presetId: "pixel-8", width: 412, height: 915 }, viewport: { width: 412, height: 915 }, }).viewport, ).toEqual({ width: 412, height: 915 }); });}); describe("PreviewEvent", () => { it("decodes opened", () => { const event = decodePreviewEvent({ type: "opened", threadId: "t", tabId: "preview-t", createdAt: "2026-01-01T00:00:00.000Z", serverEpoch: "server-a", revision: 1, snapshot: { threadId: "t", tabId: "preview-t", navStatus: { _tag: "Idle" }, canGoBack: false, canGoForward: false, updatedAt: "2026-01-01T00:00:00.000Z", }, }); expect(event.type).toBe("opened"); }); it("decodes failed with code/description", () => { const event = decodePreviewEvent({ type: "failed", threadId: "t", tabId: "preview-t", createdAt: "2026-01-01T00:00:00.000Z", serverEpoch: "server-a", revision: 1, url: "https://example.com/", title: "", code: -105, description: "ERR_NAME_NOT_RESOLVED", }); expect(event.type).toBe("failed"); if (event.type === "failed") { expect(event.code).toBe(-105); } }); it("decodes resized with tab viewport state", () => { const event = decodePreviewEvent({ type: "resized", threadId: "t", tabId: "preview-t", createdAt: "2026-01-01T00:00:00.000Z", serverEpoch: "server-a", revision: 1, snapshot: { threadId: "t", tabId: "preview-t", navStatus: { _tag: "Idle" }, canGoBack: false, canGoForward: false, viewport: { _tag: "freeform", width: 1024, height: 768 }, updatedAt: "2026-01-01T00:00:00.000Z", }, }); expect(event.type).toBe("resized"); }); it("decodes closed without snapshot", () => { const event = decodePreviewEvent({ type: "closed", threadId: "t", tabId: "preview-t", createdAt: "2026-01-01T00:00:00.000Z", serverEpoch: "server-a", revision: 1, }); expect(event.type).toBe("closed"); });}); describe("DiscoveredLocalServer", () => { it("decodes a server with process metadata", () => { const server = decodeServer({ host: "localhost", port: 5173, url: "http://localhost:5173", processName: "node", pid: 12345, terminal: null, }); expect(server.port).toBe(5173); expect(server.processName).toBe("node"); }); it("decodes a server without process metadata", () => { const server = decodeServer({ host: "localhost", port: 3000, url: "http://localhost:3000", processName: null, pid: null, terminal: null, }); expect(server.processName).toBeNull(); }); it("rejects invalid ports", () => { expect(() => decodeServer({ host: "localhost", port: 0, url: "http://localhost:0", processName: null, pid: null, terminal: null, }), ).toThrow(); expect(() => decodeServer({ host: "localhost", port: 70000, url: "http://localhost:70000", processName: null, pid: null, terminal: null, }), ).toThrow(); });});
import { Schema } from "effect";
import { describe, expect, it } from "vite-plus/test";
import {
DiscoveredLocalServer,
PreviewEvent,
PreviewNavStatus,
PreviewSessionSnapshot,
PreviewViewportSetting,
} from "./preview.ts";
import {
PreviewAutomationHost,
PreviewAutomationError,
PreviewAutomationOpenInput,
PreviewAutomationResizeInput,
PreviewAutomationResizeResult,
PreviewAutomationStatus,
} from "./previewAutomation.ts";
const decodePreviewEvent = Schema.decodeUnknownSync(PreviewEvent);
const decodeSnapshot = Schema.decodeUnknownSync(PreviewSessionSnapshot);
const decodeNavStatus = Schema.decodeUnknownSync(PreviewNavStatus);
const decodeServer = Schema.decodeUnknownSync(DiscoveredLocalServer);
const decodeViewport = Schema.decodeUnknownSync(PreviewViewportSetting);
const decodeResizeInput = Schema.decodeUnknownSync(PreviewAutomationResizeInput);
const decodeOpenInput = Schema.decodeUnknownSync(PreviewAutomationOpenInput);
const decodeResizeResult = Schema.decodeUnknownSync(PreviewAutomationResizeResult);
const decodeAutomationHost = Schema.decodeUnknownSync(PreviewAutomationHost);
const decodeAutomationError = Schema.decodeUnknownSync(PreviewAutomationError);
const decodeAutomationStatus = Schema.decodeUnknownSync(PreviewAutomationStatus);
describe("PreviewAutomationOpenInput", () => {
it("accepts the inline preview visibility flag", () => {
expect(decodeOpenInput({ open: false })).toEqual({ open: false });
});
it("retains the legacy show visibility alias", () => {
expect(decodeOpenInput({ show: false })).toEqual({ show: false });
});
});
describe("PreviewNavStatus", () => {
it("decodes Idle", () => {
expect(decodeNavStatus({ _tag: "Idle" })).toEqual({ _tag: "Idle" });
});
it("decodes Loading with title", () => {
expect(decodeNavStatus({ _tag: "Loading", url: "http://localhost:5173/", title: "" })).toEqual({
_tag: "Loading",
url: "http://localhost:5173/",
title: "",
});
});
it("decodes LoadFailed with code/description", () => {
expect(
decodeNavStatus({
_tag: "LoadFailed",
url: "https://example.com/",
title: "Example",
code: -105,
description: "ERR_NAME_NOT_RESOLVED",
}),
).toEqual({
_tag: "LoadFailed",
url: "https://example.com/",
title: "Example",
code: -105,
description: "ERR_NAME_NOT_RESOLVED",
});
});
it("rejects empty url", () => {
expect(() => decodeNavStatus({ _tag: "Loading", url: "", title: "" })).toThrow();
});
});
describe("PreviewSessionSnapshot", () => {
it("round-trips a Success snapshot", () => {
const snapshot = decodeSnapshot({
threadId: "thread-1",
tabId: "preview-thread-1",
navStatus: {
_tag: "Success",
url: "http://localhost:5173/",
title: "Vite App",
},
canGoBack: false,
canGoForward: false,
updatedAt: "2026-01-01T00:00:00.000Z",
});
expect(snapshot.tabId).toBe("preview-thread-1");
expect(snapshot.navStatus._tag).toBe("Success");
});
});
describe("PreviewViewportSetting", () => {
it("decodes fill, freeform, and preset modes", () => {
expect(decodeViewport({ _tag: "fill" })).toEqual({ _tag: "fill" });
expect(decodeViewport({ _tag: "freeform", width: 1024, height: 768 })).toEqual({
_tag: "freeform",
width: 1024,
height: 768,
});
expect(
decodeViewport({
_tag: "preset",
presetId: "iphone-15-pro",
width: 393,
height: 852,
}),
).toMatchObject({ _tag: "preset", presetId: "iphone-15-pro" });
});
it("rejects unsafe dimensions and oversized render areas", () => {
expect(() => decodeViewport({ _tag: "freeform", width: 100, height: 800 })).toThrow();
expect(() => decodeViewport({ _tag: "freeform", width: 3840, height: 3840 })).toThrow();
});
});
describe("PreviewAutomationResizeInput", () => {
it("requires fields that match the selected mode", () => {
expect(decodeResizeInput({ mode: "fill" })).toEqual({ mode: "fill" });
expect(
decodeResizeInput({ mode: "preset", preset: "pixel-7", orientation: "landscape" }),
).toMatchObject({ mode: "preset", preset: "pixel-7" });
expect(() => decodeResizeInput({ mode: "preset", preset: "pixel-8" })).toThrow();
expect(() => decodeResizeInput({ mode: "freeform", width: 1024 })).toThrow();
expect(() => decodeResizeInput({ mode: "fill", width: 1024, height: 768 })).toThrow();
});
it("allows fill-mode measurements below the minimum selectable fixed size", () => {
expect(
decodeResizeResult({
tabId: "preview-t",
setting: { _tag: "fill" },
viewport: { width: 180, height: 120 },
}).viewport,
).toEqual({ width: 180, height: 120 });
});
});
describe("preview automation tab targeting", () => {
it("accepts an explicit tab and rejects contradictory open behavior", () => {
expect(decodeResizeInput({ tabId: "tab-app", mode: "fill" })).toMatchObject({
tabId: "tab-app",
mode: "fill",
});
expect(decodeOpenInput({ tabId: "tab-app", reuseExistingTab: true })).toMatchObject({
tabId: "tab-app",
reuseExistingTab: true,
});
expect(() => decodeOpenInput({ tabId: "tab-app", reuseExistingTab: false })).toThrow();
});
});
describe("PreviewAutomationHost", () => {
it("accepts legacy hosts and current operation advertisements", () => {
expect(decodeAutomationHost({ clientId: "legacy", environmentId: "environment-1" })).toEqual({
clientId: "legacy",
environmentId: "environment-1",
});
expect(
decodeAutomationHost({
clientId: "current",
environmentId: "environment-1",
supportedOperations: ["status", "resize"],
}).supportedOperations,
).toEqual(["status", "resize"]);
});
});
describe("PreviewAutomationError", () => {
it("preserves a typed non-editable target failure", () => {
const error = decodeAutomationError({
_tag: "PreviewAutomationTargetNotEditableError",
operation: "type",
environmentId: "environment-1",
threadId: "thread-1",
providerSessionId: "provider-session-1",
providerInstanceId: "codex",
clientId: "client-1",
connectionId: "connection-1",
requestId: "request-1",
tabId: "tab-1",
timeoutMs: 1_000,
remoteTag: "PreviewAutomationTargetNotEditableError",
remoteMessageLength: 12,
cause: {},
selectorKind: "focused-element",
});
expect(error._tag).toBe("PreviewAutomationTargetNotEditableError");
if (error._tag === "PreviewAutomationTargetNotEditableError") {
expect(error.selectorKind).toBe("focused-element");
expect(error.message).toBe("Preview automation type requires an editable focused element.");
}
});
});
describe("PreviewAutomationStatus", () => {
it("accepts old hosts without viewport data and exposes it from current hosts", () => {
const base = {
available: true,
visible: false,
tabId: "preview-t",
url: "https://example.com",
title: "Example",
loading: false,
};
expect(decodeAutomationStatus(base)).toEqual(base);
expect(
decodeAutomationStatus({
...base,
viewportSetting: { _tag: "preset", presetId: "pixel-8", width: 412, height: 915 },
viewport: { width: 412, height: 915 },
}).viewport,
).toEqual({ width: 412, height: 915 });
});
});
describe("PreviewEvent", () => {
it("decodes opened", () => {
const event = decodePreviewEvent({
type: "opened",
threadId: "t",
tabId: "preview-t",
createdAt: "2026-01-01T00:00:00.000Z",
serverEpoch: "server-a",
revision: 1,
snapshot: {
threadId: "t",
tabId: "preview-t",
navStatus: { _tag: "Idle" },
canGoBack: false,
canGoForward: false,
updatedAt: "2026-01-01T00:00:00.000Z",
},
});
expect(event.type).toBe("opened");
});
it("decodes failed with code/description", () => {
const event = decodePreviewEvent({
type: "failed",
threadId: "t",
tabId: "preview-t",
createdAt: "2026-01-01T00:00:00.000Z",
serverEpoch: "server-a",
revision: 1,
url: "https://example.com/",
title: "",
code: -105,
description: "ERR_NAME_NOT_RESOLVED",
});
expect(event.type).toBe("failed");
if (event.type === "failed") {
expect(event.code).toBe(-105);
}
});
it("decodes resized with tab viewport state", () => {
const event = decodePreviewEvent({
type: "resized",
threadId: "t",
tabId: "preview-t",
createdAt: "2026-01-01T00:00:00.000Z",
serverEpoch: "server-a",
revision: 1,
snapshot: {
threadId: "t",
tabId: "preview-t",
navStatus: { _tag: "Idle" },
canGoBack: false,
canGoForward: false,
viewport: { _tag: "freeform", width: 1024, height: 768 },
updatedAt: "2026-01-01T00:00:00.000Z",
},
});
expect(event.type).toBe("resized");
});
it("decodes closed without snapshot", () => {
const event = decodePreviewEvent({
type: "closed",
threadId: "t",
tabId: "preview-t",
createdAt: "2026-01-01T00:00:00.000Z",
serverEpoch: "server-a",
revision: 1,
});
expect(event.type).toBe("closed");
});
});
describe("DiscoveredLocalServer", () => {
it("decodes a server with process metadata", () => {
const server = decodeServer({
host: "localhost",
port: 5173,
url: "http://localhost:5173",
processName: "node",
pid: 12345,
terminal: null,
});
expect(server.port).toBe(5173);
expect(server.processName).toBe("node");
});
it("decodes a server without process metadata", () => {
const server = decodeServer({
host: "localhost",
port: 3000,
url: "http://localhost:3000",
processName: null,
pid: null,
terminal: null,
});
expect(server.processName).toBeNull();
});
it("rejects invalid ports", () => {
expect(() =>
decodeServer({
host: "localhost",
port: 0,
url: "http://localhost:0",
processName: null,
pid: null,
terminal: null,
}),
).toThrow();
expect(() =>
decodeServer({
host: "localhost",
port: 70000,
url: "http://localhost:70000",
processName: null,
pid: null,
terminal: null,
}),
).toThrow();
});
});