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
3 changes: 3 additions & 0 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ export function renderTabMarkdown(tab: TabHeader): string[] {
lines.push(`- Page Title: ${tab.title}`);
if (tab.crashed)
lines.push(`- Page status: crashed`);
const status = tab.mainDocumentStatus;
if (status && (status.status < 200 || status.status >= 300))
lines.push(`- HTTP status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`);
if (tab.console.errors || tab.console.warnings)
lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`);
return lines;
Expand Down
13 changes: 11 additions & 2 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type TabHeader = {
url: string;
current: boolean;
crashed: boolean;
mainDocumentStatus?: { status: number, statusText: string };
console: { total: number, warnings: number, errors: number };
};

Expand All @@ -93,6 +94,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
private _lastHeader: TabHeader = { title: 'about:blank', url: 'about:blank', current: false, crashed: false, console: { total: 0, warnings: 0, errors: 0 } };
private _downloads: Download[] = [];
private _requests: playwright.Request[] = [];
private _mainDocumentStatus: { status: number, statusText: string } | undefined;
private _onPageClose: (tab: Tab) => void;
crashed = false;
private _modalStates: ModalState[] = [];
Expand Down Expand Up @@ -219,6 +221,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
private _clearCollectedArtifacts() {
this._downloads.length = 0;
this._requests.length = 0;
this._mainDocumentStatus = undefined;
this._recentEventEntries.length = 0;
this._resetLogs();
}
Expand All @@ -238,9 +241,12 @@ export class Tab extends EventEmitter<TabEventsInterface> {
}

private _handleResponse(response: playwright.Response) {
const timing = response.request().timing();
const request = response.request();
if (request.isNavigationRequest() && response.frame() === this.page.mainFrame() && !request.redirectedTo())
this._mainDocumentStatus = { status: response.status(), statusText: response.statusText() };
const timing = request.timing();
const wallTime = timing.responseStart + timing.startTime;
this._addLogEntry({ type: 'request', wallTime, request: response.request() });
this._addLogEntry({ type: 'request', wallTime, request });
}

private _handleRequestFailed(request: playwright.Request) {
Expand Down Expand Up @@ -284,6 +290,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
url: this.page.url(),
current: this.isCurrentTab(),
crashed: this.crashed,
mainDocumentStatus: this._mainDocumentStatus,
console: consoleCounts,
};

Expand Down Expand Up @@ -583,6 +590,8 @@ function tabHeaderEquals(a: TabHeader, b: TabHeader): boolean {
a.url === b.url &&
a.current === b.current &&
a.crashed === b.crashed &&
a.mainDocumentStatus?.status === b.mainDocumentStatus?.status &&
a.mainDocumentStatus?.statusText === b.mainDocumentStatus?.statusText &&
a.console.errors === b.console.errors &&
a.console.warnings === b.console.warnings &&
a.console.total === b.console.total;
Expand Down
27 changes: 27 additions & 0 deletions tests/mcp/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ test('browser_navigate', async ({ client, server }) => {
});
});

test('browser_navigate surfaces non-2xx HTTP status', async ({ client, server }) => {
server.setRoute('/locked', (req, res) => {
res.writeHead(402, { 'Content-Type': 'text/html' });
res.end('<title>Payment Required</title><body>Pay up</body>');
});

expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX + '/locked' },
})).toHaveResponse({
page: expect.stringContaining(`- HTTP status: 402 Payment Required`),
});

// A redirect to a 2xx page must not carry a status line: the intermediate
// 302 hop must not leak, and the final 2xx landing renders nothing.
server.setRoute('/redirect', (req, res) => {
res.writeHead(302, { location: server.HELLO_WORLD });
res.end();
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX + '/redirect' },
})).not.toHaveResponse({
page: expect.stringContaining('HTTP status'),
});
});

test('browser_navigate blocks file:// URLs by default', async ({ client }) => {
expect(await client.callTool({
name: 'browser_navigate',
Expand Down
Loading