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
72 changes: 67 additions & 5 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2649,8 +2649,14 @@ it.live("reports why a page failed to load while the tab shows the error page",
page.emit("request", request);
page.emit("requestfailed", request);
page.url = () => "chrome-error://chromewebdata/";
expect((yield* status()).loadError).toEqual(certificateError);
expect((yield* snapshot()).loadError).toEqual(certificateError);
expect(yield* status()).toMatchObject({
url: certificateError.url,
loadError: certificateError,
});
expect(yield* snapshot()).toMatchObject({
url: certificateError.url,
loadError: certificateError,
});

const subframeRequest = {
...request,
Expand All @@ -2659,7 +2665,10 @@ it.live("reports why a page failed to load while the tab shows the error page",
};
page.emit("request", subframeRequest);
page.emit("requestfailed", subframeRequest);
expect((yield* status()).loadError).toEqual(certificateError);
expect(yield* status()).toMatchObject({
url: certificateError.url,
loadError: certificateError,
});

// Clearing must happen before the next navigation replaces the error page.
page.emit("request", { ...request, url: () => "https://device.local/login" });
Expand All @@ -2670,6 +2679,53 @@ it.live("reports why a page failed to load while the tab shows the error page",
page.emit("request", dataRequest);
page.emit("requestfailed", dataRequest);
expect((yield* status()).loadError?.url).toBe("data:text/html,…");
initialNavigation = async () => {
page.emit("request", request);
page.emit("requestfailed", request);
};
const error = yield* broker
.invoke<PreviewAutomationStatus>({
scope,
tabId,
operation: "navigate",
input: { url: certificateError.url },
})
.pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "PreviewAutomationExecutionError",
reason: `Navigation to ${certificateError.url} failed: ${certificateError.description}`,
});
}),
).pipe(Effect.provide(layer)),
);

it.effect("preview_open rejects a desktop navigation that failed before automation attached", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const manager = yield* Manager.PreviewManager;
const channel = yield* DesktopChannel.DesktopBrowserChannel;
desktopRendersNext = true;
desktopPageUrl = "chrome-error://chromewebdata/";
const failure = {
...certificateError,
url: "https://user:secret@device.local/?token=private#fragment",
};
using _failure = vi.spyOn(channel, "loadFailure").mockReturnValue(failure);
const error = yield* broker
.invoke<PreviewAutomationStatus>({
scope,
operation: "open",
input: { url: failure.url, reuseExistingTab: false, open: false },
})
.pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "PreviewAutomationExecutionError",
reason: `Navigation to ${certificateError.url} failed: ${certificateError.description}`,
});
expect(JSON.stringify(error)).not.toMatch(/secret|private|fragment/);
const { sessions } = yield* manager.list({ threadId: scope.thread.threadId });
expect(sessions.map((session) => session.tabId)).toEqual([tabId]);
}),
).pipe(Effect.provide(layer)),
);
Expand Down Expand Up @@ -2701,8 +2757,14 @@ it.live("reports a desktop tab's load failure from before the server connected",
input: {},
tabId: PreviewTabId.make(opened.tabId),
});
expect((yield* status()).loadError).toEqual(certificateError);
expect((yield* snapshot()).loadError).toEqual(certificateError);
expect(yield* status()).toMatchObject({
url: certificateError.url,
loadError: certificateError,
});
expect(yield* snapshot()).toMatchObject({
url: certificateError.url,
loadError: certificateError,
});
desktopLoadFailures.set(opened.tabId, { ...certificateError, url: dataUrl });
expect((yield* status()).loadError?.url).toBe("data:text/html,…");
// A navigation the server sees supersedes it, even before the error page goes.
Expand Down
59 changes: 43 additions & 16 deletions apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,27 @@ const make = Effect.gen(function* () {
return reported && { ...reported, url: ServerBrowserPage.withoutDataPayload(reported.url) };
};

const assertNavigationSucceeded = (tab: ServerTab) => {
const failure = loadErrorOf(tab);
if (failure) {
const diagnosticUrl = URL.parse(failure.url);
if (diagnosticUrl) {
diagnosticUrl.username = "";
diagnosticUrl.password = "";
diagnosticUrl.search = "";
diagnosticUrl.hash = "";
}
const url = diagnosticUrl
? ServerBrowserPage.withoutDataPayload(diagnosticUrl.href)
: "the requested page";
throw new ServerBrowserPage.ServerBrowserOperationError({
tag: "PreviewAutomationExecutionError",
message: `Navigation to ${url} failed: ${failure.description}`,
detail: { ...failure, url },
});
}
};

const statusWithTitle = async (
tab: ServerTab | undefined,
agentSessionId?: string,
Expand All @@ -1646,7 +1667,7 @@ const make = Effect.gen(function* () {
available: true,
visible: isVisible(tab),
tabId: tab.tabId,
url: url === "about:blank" ? null : url,
url: loadError?.url ?? (url === "about:blank" ? null : url),
title: null,
loading: tab.loading,
...(loadError ? { loadError } : {}),
Expand All @@ -1668,7 +1689,8 @@ const make = Effect.gen(function* () {
url:
candidate.page.url() === "about:blank"
? null
: ServerBrowserPage.withoutDataPayload(candidate.page.url()),
: (loadErrorOf(candidate)?.url ??
ServerBrowserPage.withoutDataPayload(candidate.page.url())),
...(candidate.openerTabId === undefined ? {} : { openerTabId: candidate.openerTabId }),
owner: tabOwner(candidate),
ownedByCaller: candidate.control.agentId === agentSessionId,
Expand Down Expand Up @@ -1732,6 +1754,7 @@ const make = Effect.gen(function* () {
}
throw ServerBrowserPage.toOperationError(cause, timeout);
});
assertNavigationSucceeded(tab);
};

const withCaptureLock = <A>(tab: ServerTab, operation: () => Promise<A>): Promise<A> => {
Expand Down Expand Up @@ -2092,15 +2115,24 @@ const make = Effect.gen(function* () {
if (url) await navigate(tab, url, "load", navigationTimeout);
} else {
// A failed open returns no tabId; a leftover hidden tab would block later reuse.
await tab.initialNavigation?.catch(async (cause: unknown) => {
// Playwright rejects downloads too; their tab must survive to save the file.
if (cause instanceof Error && cause.message.includes("Download is starting")) {
await Promise.resolve(tab.initialNavigation)
.then(async () => {
if (url) {
await tab.page
.waitForLoadState("load", { timeout: navigationTimeout })
.catch(constVoid);
}
assertNavigationSucceeded(tab);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
})
.catch(async (cause: unknown) => {
// Playwright rejects downloads too; their tab must survive to save the file.
if (cause instanceof Error && cause.message.includes("Download is starting")) {
throw cause;
}
await runPromise(manager.close({ threadId: tab.threadId, tabId: tab.tabId }));
dropTab(tab, false);
throw cause;
}
await runPromise(manager.close({ threadId: tab.threadId, tabId: tab.tabId }));
dropTab(tab, false);
throw cause;
});
});
}
const reveal = open.open ?? open.show;
if (reveal !== false) {
Expand All @@ -2112,11 +2144,6 @@ const make = Effect.gen(function* () {
}),
);
}
if (!existing && url) {
await tab.page
.waitForLoadState("load", { timeout: navigationTimeout })
.catch(constVoid);
}
return statusWithTitle(tab, request.agentSessionId);
},
deadlineMs,
Expand Down Expand Up @@ -2270,7 +2297,7 @@ const make = Effect.gen(function* () {
}),
);
const loadError = loadErrorOf(tab);
return loadError ? { ...snapshot, loadError } : snapshot;
return loadError ? { ...snapshot, url: loadError.url, loadError } : snapshot;
}
case "click": {
const clickInput = input as PreviewAutomationClickInput;
Expand Down
Loading