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
45 changes: 45 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2759,6 +2759,51 @@ it.live.each(
).pipe(Effect.provide(layer)),
);

it.effect("published root viewport reads stay ordered during a resize burst", () =>
Effect.scoped(
Effect.gen(function* () {
desktopRendersNext = true;
const { browser, tabId } = yield* ready;
const viewer = yield* browser.attachViewer(viewerInput(tabId, true));
const cdp = desktopConnections[0]!.context.sessions[0]!;
yield* Effect.promise(() => cdp.emitAsync("Page.frameResized"));
yield* Queue.takeAll(viewer.output);
const reads = yield* Queue.unbounded<PromiseWithResolvers<Record<string, unknown>>>();
const send = cdp.send.getMockImplementation()!;
cdp.send.mockImplementation(async (method, input) => {
if (
method === "Page.getLayoutMetrics" ||
(method === "Runtime.evaluate" && (input as { contextId?: number }).contextId === 9)
) {
const read = Promise.withResolvers<Record<string, unknown>>();
Queue.offerUnsafe(reads, read);
return read.promise;
}
return send(method, input);
});
const answer = (width: number, height: number) => ({
cssVisualViewport: { clientWidth: width, clientHeight: height },
result: { value: { width, height } },
});
const first = cdp.emitAsync("Page.frameResized");
const firstRead = yield* Queue.take(reads);
const burst = Promise.all([
cdp.emitAsync("Page.frameResized"),
cdp.emitAsync("Page.frameResized"),
]);
firstRead.resolve(answer(1000, 750));
(yield* Queue.take(reads)).resolve(answer(1250, 938));
expect(yield* Queue.size(reads)).toBe(0);
yield* Effect.promise(() => Promise.all([first, burst]));
expect(viewportSizes(yield* Queue.takeAll(viewer.output))).toEqual([
"1000x750",
"1250x938",
"1250x938",
]);
}),
).pipe(Effect.provide(layer)),
);

it.live("published root zoom keeps measured viewport, pointer and snapshot scale coherent", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,10 +997,7 @@ const make = Effect.gen(function* () {
};

const broadcastViewport = async (tab: ServerTab) => {
const size =
tab.backingPage === "desktop-root"
? await ServerBrowserPage.viewportSize(tab.page, tab.cdp)
: await layoutSize(tab);
const size = await layoutSize(tab);
if (!size || tab.closing) return;
for (const viewer of tab.viewers) viewer.push({ _tag: "viewport", ...size });
};
Expand Down
32 changes: 32 additions & 0 deletions apps/web/src/browser/ServerBrowserSurface.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ async function renderSurface(controller: PreviewStreamControl["controller"]) {

const keys = () => mocks.sent.filter((input) => input.type === "key");

it("maps streamed clicks at 80 percent panel zoom into the page CSS viewport", async () => {
setup();
const { screen, input, canvas } = await renderSurface("you");
await using _screen = screen;
const surface = input.closest<HTMLElement>("[data-server-browser-surface]")!;
surface.style.zoom = "0.8";
canvas.width = 1600;
canvas.height = 1200;
canvas.style.width = "400px";
canvas.style.height = "300px";
canvas.setPointerCapture = () => undefined;
const handlers = mocks.createClient.mock.calls.at(-1)![1];
await act(() => {
mocks.painted?.();
handlers.onViewport({ width: 1000, height: 750 });
});
const rect = canvas.getBoundingClientRect();
canvas.dispatchEvent(
new PointerEvent("pointerdown", {
button: 0,
buttons: 1,
pointerType: "mouse",
bubbles: true,
clientX: rect.left + rect.width * 0.4,
clientY: rect.top + rect.height * 0.4,
}),
);
expect(mocks.sent.filter((message) => message.type === "mouse")).toContainEqual(
expect.objectContaining({ action: "down", x: 400, y: 300 }),
);
});

describe("ServerBrowserSurface clipboard commands", () => {
it("sends a native Copy or Cut menu command to the page once", async () => {
setup();
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/browser/openFileInPreview.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setPreviewBootstrapTokenOnUrl } from "@t3tools/shared/remote";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import {
EnvironmentId,
Expand Down Expand Up @@ -215,6 +216,29 @@ describe("managed app links", () => {
},
);

it("opens a managed app directly on its local desktop with renewed navigation access", async () => {
mocks.desktopHost.mockReturnValue("local");
const url = "http://127.0.0.1:5173/app?q=1#section";
const navigationUrl = setPreviewBootstrapTokenOnUrl(new URL(url), "fresh").href;
mocks.prepareHostedPreview.mockResolvedValue({
url,
localUrl: url,
navigationUrl,
managed: true,
restored: true,
});
const openPreview = vi.fn(async (_input: { input: PreviewOpenInput }) =>
AsyncResult.success(snapshot),
);
await openUrlInPreview({ threadRef, url, openPreview });
expect(openPreview.mock.calls[0]?.[0].input).toMatchObject({
url: navigationUrl,
runtime: "server",
desktopHostId: "local",
});
expect(openPreview.mock.calls[0]?.[0].input).not.toHaveProperty("browserHost");
});

it("preserves an explicitly selected desktop profile for a managed app", async () => {
mocks.desktopHost.mockReturnValue("remote-desktop");
const url = "http://workstation.example:5173/sign-in";
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/browser/preparedPreviewOpenTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export function preparedPreviewOpenTarget(
prepared.managed &&
prepared.localUrl !== undefined &&
(profileId === undefined || isBuiltInBrowserProfileId(profileId)) &&
readEnvironmentSupportsServerBrowser(environmentId))
readEnvironmentSupportsServerBrowser(environmentId) &&
desktopBrowserHostFor(environmentId) !== "local")
) {
return {
url: hostedPreviewNavigationUrl(prepared, prepared.localUrl ?? authoredUrl),
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/browser/previewRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
rendersServerTabNatively,
} from "./previewRuntime";

import { preparedPreviewOpenTarget } from "./preparedPreviewOpenTarget";

const primary = EnvironmentId.make("primary");
const remote = EnvironmentId.make("remote");

Expand Down Expand Up @@ -211,3 +213,27 @@ describe("runtime and native ownership boundaries", () => {
).toBe(false);
});
});

describe("managed preview locality", () => {
it("uses the desktop only for a confirmed local host and honors explicit streaming", () => {
const url = "http://127.0.0.1:8123/app";
const prepared = { url, localUrl: url, managed: true, restored: true };
expect(preparedPreviewOpenTarget(primary, prepared, url, "default")).toEqual({
url,
runtime: "server",
desktopHostId: "local",
});
const streamed = { url, runtime: "server", browserHost: "environment" };
expect(preparedPreviewOpenTarget(primary, prepared, url, "default", "server")).toEqual(
streamed,
);
expect(preparedPreviewOpenTarget(remote, prepared, url, "default")).toEqual(streamed);
for (const localDesktopBrowser of [false, undefined]) {
state.localDesktopBrowser = localDesktopBrowser;
expect(preparedPreviewOpenTarget(primary, prepared, url, "default")).toEqual(streamed);
}
state.localDesktopBrowser = true;
state.electron = false;
expect(preparedPreviewOpenTarget(primary, prepared, url, "default")).toEqual(streamed);
});
});
53 changes: 45 additions & 8 deletions apps/web/src/handoffs/useOpenHandoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,13 @@ describe("opening a saved handoff", () => {
expect(readThreadHandoffs(remote)).toHaveLength(1);
});
it("leaves a failed desktop app tab intact and opens its handoff in the owning environment", async () => {
const remote = { ...ref, environmentId: EnvironmentId.make("remote-environment") };
const ops = operations();
const url = "http://workstation.example:8123/app?q=1#section";
const localUrl = "http://127.0.0.1:8123/app?q=1#section";
native.bridge = { navigate: vi.fn(async () => undefined) };
hosting.prepare.mockResolvedValue({ url, localUrl, managed: true, restored: true });
const entry = recordHandoff(ref, { kind: "url", url });
const entry = recordHandoff(remote, { kind: "url", url });
const failed: PreviewSessionSnapshot = {
...snapshot(url, "desktop-tab"),
profileId: "default",
Expand All @@ -364,8 +365,8 @@ describe("opening a saved handoff", () => {
desktopHostId: "local",
navStatus: { _tag: "LoadFailed", url, title: "", code: -102, description: "Unreachable" },
};
applyPreviewServerSnapshot(ref, failed);
rememberHandoffBrowser(ref, failed.tabId, entry.target, url);
applyPreviewServerSnapshot(remote, failed);
rememberHandoffBrowser(remote, failed.tabId, entry.target, url);
ops.openPreview.mockImplementation(async ({ input }) =>
AsyncResult.success({
...snapshot(input.url ?? "", "environment-tab"),
Expand All @@ -374,20 +375,56 @@ describe("opening a saved handoff", () => {
}),
);

await openHandoff(ref, entry, ops);
await openHandoff(remote, entry, ops);
expect(ops.openPreview.mock.calls[0]?.[0].input).toMatchObject({
url: localUrl,
runtime: "server",
browserHost: "environment",
});
expect(native.bridge.navigate).not.toHaveBeenCalled();
expect(panel().activeSurfaceId).toBe("browser:environment-tab");
expect(readThreadPreviewState(ref).sessions[failed.tabId]).toEqual(failed);
await openHandoff(ref, entry, ops);
expect(
selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, remote)
.activeSurfaceId,
).toBe("browser:environment-tab");
expect(readThreadPreviewState(remote).sessions[failed.tabId]).toEqual(failed);
await openHandoff(remote, entry, ops);
expect(ops.openPreview).toHaveBeenCalledTimes(1);
expect(panel().activeSurfaceId).toBe("browser:environment-tab");
expect(
selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, remote)
.activeSurfaceId,
).toBe("browser:environment-tab");
});

it.each(["server", "desktop"] as const)(
"restores a local managed handoff in its retained %s tab",
async (backingPage) => {
const ops = operations();
const url = "http://127.0.0.1:8123/app";
native.bridge = { navigate: vi.fn(async () => undefined) };
hosting.prepare.mockResolvedValue({
url,
localUrl: url,
managed: true,
restored: true,
restarted: true,
});
const entry = recordHandoff(ref, { kind: "url", url });
applyPreviewServerSnapshot(ref, {
...snapshot(url),
profileId: "default",
runtime: "server",
backingPage,
...(backingPage === "desktop" ? { desktopHostId: "local" } : {}),
});
await openHandoff(ref, entry, ops);
expect(ops.openPreview).not.toHaveBeenCalled();
expect(panel().activeSurfaceId).toBe("browser:tab");
if (backingPage === "desktop")
expect(native.bridge.navigate).toHaveBeenCalledWith(expect.any(String), url);
else expect(refresh.request).toHaveBeenCalledWith(ref, "tab", url);
},
);

it("keeps a managed handoff in its existing named desktop profile", async () => {
const ops = operations();
const url = "http://workstation.example:8123/account";
Expand Down
14 changes: 10 additions & 4 deletions docs/lastcode/preview-handoffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,16 @@ An existing server cannot be adopted merely by remembering its URL: relaunch it
through `preview_host` to establish native ownership. One managed listener owns
a local origin; use a distinct port for another preview. Native browser recovery
uses the owning environment and thread, never an unrelated thread or public
website. In the integrated Browser, managed app links use an environment-hosted
page and the saved listener's local address, so a loopback-only app does not
depend on desktop access to its port. An explicitly selected desktop profile
keeps its desktop identity and requires a reachable browser destination. Native
website. In the integrated Browser, how a managed app link opens in the default
or incognito profile depends on the environment. On a verified local host (the
primary desktop with inherited IPC), it opens as the usual direct desktop page.
For a remote, unknown, or non-desktop environment, it opens as a streamed
environment-hosted page at the saved listener's local address, so a
loopback-only app does not depend on desktop access to its port. An explicit
server runtime request also uses the environment page. Reopening a handoff keeps
a healthy streamed tab as it is rather than migrating it, which preserves page
state. An explicitly selected desktop profile keeps its desktop identity and
requires a reachable browser destination. Native
mobile opens managed app links in the environment Browser viewer too. External
browsers still need a reachable listener. Managed images, audio, video, and PDFs
use signed environment-hosted resource URLs, so their clients do not need access
Expand Down