mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
fix(desktop): browser_logs reports the HTTP status of each request (#8956)
* fix(desktop): browser_logs reports the HTTP status of each request * test(desktop): browser_logs reports the status the browser recorded * test(desktop): e2e checks browser_logs reports a 404 and a 200 * docs(browser): browser_logs also reports the HTTP status
This commit is contained in:
@@ -246,6 +246,67 @@ async function closeServer(server) {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
// browser_logs must report what the server answered: a 404 may not look like a 200.
|
||||
async function verifyNetworkLogsReportStatus(client) {
|
||||
const server = createServer((request, response) => {
|
||||
response.writeHead(request.url === "/missing" ? 404 : 200, {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
});
|
||||
response.end("<!doctype html><title>Status page</title><h1>Status page heading</h1>");
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
assert(address && typeof address !== "string", "Status page did not bind a TCP port");
|
||||
let browserId = null;
|
||||
try {
|
||||
const created = await callBrowserTool(client, "browser_new_tab", {
|
||||
url: `http://127.0.0.1:${address.port}/`,
|
||||
});
|
||||
browserId = created.browserId;
|
||||
assert(
|
||||
typeof browserId === "string",
|
||||
"browser_new_tab returned no browserId for the status page",
|
||||
);
|
||||
await callBrowserTool(client, "browser_wait", {
|
||||
browserId,
|
||||
text: "Status page heading",
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
const fetched = await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function:
|
||||
"async () => { const response = await fetch('/missing'); await response.text(); return response.status; }",
|
||||
});
|
||||
assert(
|
||||
JSON.parse(fetched.resultJson) === 404,
|
||||
`The status page did not answer 404: ${fetched.resultJson}`,
|
||||
);
|
||||
// The browser records a request's timing entry a moment after the response completes.
|
||||
const deadline = Date.now() + 5_000;
|
||||
let seen = [];
|
||||
while (Date.now() < deadline) {
|
||||
const logs = await callBrowserTool(client, "browser_logs", { browserId });
|
||||
seen = logs.network.map((entry) => ({
|
||||
pathname: new URL(entry.url).pathname,
|
||||
status: entry.status ?? null,
|
||||
}));
|
||||
const reported = (pathname, status) =>
|
||||
seen.some((entry) => entry.pathname === pathname && entry.status === status);
|
||||
if (reported("/missing", 404) && reported("/", 200)) return;
|
||||
await delay(100);
|
||||
}
|
||||
throw new Error(
|
||||
`browser_logs did not report 404 for /missing and 200 for the page: ${JSON.stringify(seen)}`,
|
||||
);
|
||||
} finally {
|
||||
if (browserId) await callBrowserTool(client, "browser_close_tab", { browserId });
|
||||
await closeServer(server);
|
||||
}
|
||||
}
|
||||
|
||||
function mcpPayload(result, command) {
|
||||
const payload = result.structuredContent;
|
||||
assert(
|
||||
@@ -1020,6 +1081,7 @@ async function runRegression({
|
||||
JSON.parse(readyStateResult.resultJson) === "complete",
|
||||
"Local browser document did not finish loading",
|
||||
);
|
||||
await verifyNetworkLogsReportStatus(client);
|
||||
// Reproduce the report's mismatch: the guest is complete, but the pane's
|
||||
// last loading signal says it is not ready.
|
||||
await page.evaluate((id) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolve as resolvePath } from "node:path";
|
||||
import { createContext, runInContext } from "node:vm";
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type {
|
||||
@@ -434,6 +435,26 @@ function containsScript(tab: FakeTab, ...parts: string[]): boolean {
|
||||
return tab.scripts.some((script) => parts.every((part) => script.includes(part)));
|
||||
}
|
||||
|
||||
/**
|
||||
* A browser whose tab runs the network script the host injects against `performance`, the way a
|
||||
* page runs it, so the test sees what the script reads from Chromium's timing entries.
|
||||
*/
|
||||
function browserWithTimingEntries(entries: { resource: unknown[]; navigation: unknown[] }) {
|
||||
const browser = new BrowserAutomationHarness();
|
||||
const fakeAnswer = browser.tab.executeJavaScript.bind(browser.tab);
|
||||
const page = createContext({
|
||||
performance: {
|
||||
getEntriesByType: (type: string) =>
|
||||
type === "navigation" ? entries.navigation : entries.resource,
|
||||
},
|
||||
});
|
||||
browser.tab.executeJavaScript = async (script: string) =>
|
||||
script.includes("performance.getEntriesByType")
|
||||
? runInContext(script, page)
|
||||
: fakeAnswer(script);
|
||||
return browser;
|
||||
}
|
||||
|
||||
function requireSnapshotRefs(result: Awaited<ReturnType<BrowserAutomationHarness["snapshot"]>>) {
|
||||
expect(result).toEqual({
|
||||
requestId: "req-snapshot",
|
||||
@@ -1406,6 +1427,72 @@ describe("executeAutomationCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("logs reports the HTTP status the browser recorded, and none it did not", async () => {
|
||||
const timing = (name: string, initiatorType: string, responseStatus?: number) => ({
|
||||
name,
|
||||
initiatorType,
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
transferSize: 3,
|
||||
...(responseStatus === undefined ? {} : { responseStatus }),
|
||||
});
|
||||
const browser = browserWithTimingEntries({
|
||||
resource: [
|
||||
timing("https://a.test/api/orders", "fetch", 404),
|
||||
// Chromium reports 0 for a cross-origin response that sends no Timing-Allow-Origin.
|
||||
timing("https://cdn.test/lib.js", "script", 0),
|
||||
// A browser without the field.
|
||||
timing("https://a.test/old.css", "css"),
|
||||
],
|
||||
navigation: [timing("https://a.test/form", "navigation", 200)],
|
||||
});
|
||||
|
||||
const result = await browser.execute({ command: "logs", args: { browserId: BROWSER_A } });
|
||||
|
||||
expect(result).toEqual({
|
||||
requestId: "req-logs",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "logs",
|
||||
browserId: BROWSER_A,
|
||||
console: [],
|
||||
network: [
|
||||
{
|
||||
url: "https://a.test/api/orders",
|
||||
type: "fetch",
|
||||
status: 404,
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
transferSize: 3,
|
||||
},
|
||||
{
|
||||
url: "https://cdn.test/lib.js",
|
||||
type: "script",
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
transferSize: 3,
|
||||
},
|
||||
{
|
||||
url: "https://a.test/old.css",
|
||||
type: "css",
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
transferSize: 3,
|
||||
},
|
||||
{
|
||||
url: "https://a.test/form",
|
||||
method: "GET",
|
||||
type: "navigation",
|
||||
status: 200,
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
transferSize: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("evaluate returns primitive JSON from the page context", async () => {
|
||||
const browser = new BrowserAutomationHarness();
|
||||
browser.tab.evaluateScriptResult = { ok: true, resultJson: "42" };
|
||||
|
||||
@@ -1628,6 +1628,8 @@ const NETWORK_PERFORMANCE_SCRIPT = String.raw`(() => {
|
||||
url: entry.name,
|
||||
method: entry.initiatorType === 'navigation' ? 'GET' : undefined,
|
||||
type: entry.initiatorType,
|
||||
// 0 when the browser does not say (a cross-origin response without Timing-Allow-Origin).
|
||||
status: typeof entry.responseStatus === 'number' && entry.responseStatus > 0 ? entry.responseStatus : undefined,
|
||||
startTime: entry.startTime,
|
||||
duration: entry.duration,
|
||||
transferSize: typeof entry.transferSize === 'number' ? entry.transferSize : undefined,
|
||||
|
||||
Reference in New Issue
Block a user