Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,114 @@ beforeEach(() => {
desktopConnections.length = 0;
});

it.live.each(["desktop" as const, undefined])(
"explicit desktop targets ($0 runtime) fail without creating a server tab",
(runtime) =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const manager = yield* Manager.PreviewManager;
const desktop = yield* manager.open({
threadId: scope.thread.threadId,
url: "https://example.com/original",
...(runtime === undefined ? {} : { runtime }),
});
const before = yield* manager.list({ threadId: scope.thread.threadId });
const error = yield* broker
.invoke<PreviewAutomationStatus>({
scope,
tabId: PreviewTabId.make(desktop.tabId),
operation: "open",
input: { url: "https://example.com/replacement", open: true },
})
.pipe(Effect.flip);
expect(error).toMatchObject({ _tag: "PreviewAutomationExecutionError" });
expect(error.message).toContain("desktop runtime");
expect(error.message).toContain("cannot control");
expect(yield* manager.list({ threadId: scope.thread.threadId })).toEqual(before);
expect(contexts).toHaveLength(1);
// A rejected explicit target must not replace this session's working tab.
const reused = yield* broker.invoke<PreviewAutomationStatus>({
scope,
operation: "open",
input: { open: false },
});
expect(reused.tabId).toBe(tabId);
}),
).pipe(Effect.provide(layer)),
);

it.live("an unknown explicit target fails without creating a server tab", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker } = yield* ready;
const manager = yield* Manager.PreviewManager;
const before = yield* manager.list({ threadId: scope.thread.threadId });
const error = yield* broker
.invoke<PreviewAutomationStatus>({
scope,
tabId: PreviewTabId.make("missing-tab"),
operation: "open",
input: { url: "https://example.com/replacement", open: true },
})
.pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "PreviewAutomationTabNotFoundError",
tabId: "missing-tab",
});
expect(yield* manager.list({ threadId: scope.thread.threadId })).toEqual(before);
expect(contexts).toHaveLength(1);
}),
).pipe(Effect.provide(layer)),
);

it.live("an explicit server target reuses its ID and navigates without creating a tab", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const manager = yield* Manager.PreviewManager;
const before = yield* manager.list({ threadId: scope.thread.threadId });
const opened = yield* broker.invoke<PreviewAutomationStatus>({
scope,
tabId,
operation: "open",
input: { url: "https://example.com/reused", open: false },
});
expect(opened).toMatchObject({ tabId, url: "https://example.com/reused" });
const { sessions } = yield* manager.list({ threadId: scope.thread.threadId });
expect(sessions.map((session) => session.tabId)).toEqual(
before.sessions.map((session) => session.tabId),
);
expect(contexts).toHaveLength(1);
expect(contexts[0]!.page.goto).toHaveBeenCalledWith(
"https://example.com/reused",
expect.anything(),
);
}),
).pipe(Effect.provide(layer)),
);

it.live("an unresolved implicit target still falls back to a new server tab", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const manager = yield* Manager.PreviewManager;
yield* manager.close({ threadId: scope.thread.threadId, tabId });
const opened = yield* broker.invoke<PreviewAutomationStatus>({
scope,
operation: "open",
input: { url: "https://example.com/fallback", open: false },
});
expect(opened.tabId).not.toBe(tabId);
expect(opened).toMatchObject({ available: true, url: "https://example.com/fallback" });
const { sessions } = yield* manager.list({ threadId: scope.thread.threadId });
expect(sessions).toEqual([
expect.objectContaining({ tabId: opened.tabId, runtime: "server" }),
]);
}),
).pipe(Effect.provide(layer)),
);

it.live("readiness none responds immediately but takeover input waits for navigation commit", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,20 @@ const make = Effect.gen(function* () {
),
)
: undefined;
if (reuse && request.tabIdExplicit && !found) {
const { sessions } = await Effect.runPromise(
manager.list({ threadId: ThreadId.make(request.threadId) }),
);
const desktop = sessions.some(
(session) => session.tabId === request.tabId && session.runtime !== "server",
);
throw new ServerBrowserPage.ServerBrowserOperationError(
desktop ? "PreviewAutomationExecutionError" : "PreviewAutomationTabNotFoundError",
desktop
? "This tab belongs to the desktop runtime. This environment's server browser cannot control desktop tabs. Omit tabId to open a server tab instead."
: "The requested server preview tab does not exist.",
);
}
// Only an explicit tabId reuses a tab this session did not open; a tab
// it last read (such as the user's) is not taken over implicitly.
const existing =
Expand Down
Loading