diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a50d14..3a6f6e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,4 +58,4 @@ jobs: run: npx playwright install --with-deps - name: Run tests - run: npm test + run: npm test -- --forbid-only diff --git a/src/context.ts b/src/context.ts index 73e58f3..cb059e2 100644 --- a/src/context.ts +++ b/src/context.ts @@ -317,6 +317,7 @@ export class Tab { readonly context: Context; readonly page: playwright.Page; private _console: playwright.ConsoleMessage[] = []; + private _requests: Map = new Map(); private _snapshot: PageSnapshot | undefined; private _onPageClose: (tab: Tab) => void; @@ -325,9 +326,11 @@ export class Tab { this.page = page; this._onPageClose = onPageClose; page.on('console', event => this._console.push(event)); + page.on('request', request => this._requests.set(request, null)); + page.on('response', response => this._requests.set(response.request(), response)); page.on('framenavigated', frame => { if (!frame.parentFrame()) - this._console.length = 0; + this._clearCollectedArtifacts(); }); page.on('close', () => this._onClose()); page.on('filechooser', chooser => { @@ -342,8 +345,13 @@ export class Tab { page.setDefaultTimeout(5000); } - private _onClose() { + private _clearCollectedArtifacts() { this._console.length = 0; + this._requests.clear(); + } + + private _onClose() { + this._clearCollectedArtifacts(); this._onPageClose(this); } @@ -367,6 +375,10 @@ export class Tab { return this._console; } + async requests(): Promise> { + return this._requests; + } + async captureSnapshot() { this._snapshot = await PageSnapshot.create(this.page); } diff --git a/src/index.ts b/src/index.ts index 2831c4e..eade841 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import files from './tools/files'; import install from './tools/install'; import keyboard from './tools/keyboard'; import navigate from './tools/navigate'; +import network from './tools/network'; import pdf from './tools/pdf'; import snapshot from './tools/snapshot'; import tabs from './tools/tabs'; @@ -43,6 +44,7 @@ const snapshotTools: Tool[] = [ ...install, ...keyboard(true), ...navigate(true), + ...network, ...pdf, ...snapshot, ...tabs(true), @@ -56,6 +58,7 @@ const screenshotTools: Tool[] = [ ...install, ...keyboard(false), ...navigate(false), + ...network, ...pdf, ...screen, ...tabs(false), diff --git a/src/tools/network.ts b/src/tools/network.ts new file mode 100644 index 0000000..3286b82 --- /dev/null +++ b/src/tools/network.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import { defineTool } from './tool'; + +import type * as playwright from 'playwright'; + +const requests = defineTool({ + capability: 'core', + + schema: { + name: 'browser_network_requests', + description: 'Returns all network requests since loading the page', + inputSchema: z.object({}), + }, + + handle: async context => { + const requests = await context.currentTabOrDie().requests(); + const log = [...requests.entries()].map(([request, response]) => renderRequest(request, response)).join('\n'); + return { + code: [`// `], + action: async () => { + return { + content: [{ type: 'text', text: log }] + }; + }, + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +function renderRequest(request: playwright.Request, response: playwright.Response | null) { + const result: string[] = []; + result.push(`[${request.method().toUpperCase()}] ${request.url()}`); + if (response) + result.push(`=> [${response.status()}] ${response.statusText()}`); + return result.join(' '); +} + +export default [ + requests, +]; diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts index fd8070b..f4ad688 100644 --- a/tests/capabilities.spec.ts +++ b/tests/capabilities.spec.ts @@ -32,6 +32,7 @@ test('test snapshot tool list', async ({ client }) => { 'browser_navigate_back', 'browser_navigate_forward', 'browser_navigate', + 'browser_network_requests', 'browser_pdf_save', 'browser_press_key', 'browser_resize', @@ -56,6 +57,7 @@ test('test vision tool list', async ({ visionClient }) => { 'browser_navigate_back', 'browser_navigate_forward', 'browser_navigate', + 'browser_network_requests', 'browser_pdf_save', 'browser_press_key', 'browser_resize', diff --git a/tests/fixtures.ts b/tests/fixtures.ts index cd7b4fd..5fd4c46 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -21,6 +21,7 @@ import { test as baseTest, expect as baseExpect } from '@playwright/test'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { spawn } from 'child_process'; +import { TestServer } from './testserver'; type TestFixtures = { client: Client; @@ -28,11 +29,14 @@ type TestFixtures = { startClient: (options?: { args?: string[] }) => Promise; wsEndpoint: string; cdpEndpoint: string; + server: TestServer; + httpsServer: TestServer; }; type WorkerFixtures = { mcpHeadless: boolean; mcpBrowser: string | undefined; + _workerServers: { server: TestServer, httpsServer: TestServer }; }; export const test = baseTest.extend({ @@ -103,7 +107,32 @@ export const test = baseTest.extend({ await use(headless); }, { scope: 'worker' }], - mcpBrowser: ['chromium', { option: true, scope: 'worker' }], + mcpBrowser: ['chrome', { option: true, scope: 'worker' }], + + _workerServers: [async ({}, use, workerInfo) => { + const port = 8907 + workerInfo.workerIndex * 4; + const server = await TestServer.create(port); + + const httpsPort = port + 1; + const httpsServer = await TestServer.createHTTPS(httpsPort); + + await use({ server, httpsServer }); + + await Promise.all([ + server.stop(), + httpsServer.stop(), + ]); + }, { scope: 'worker' }], + + server: async ({ _workerServers }, use) => { + _workerServers.server.reset(); + await use(_workerServers.server); + }, + + httpsServer: async ({ _workerServers }, use) => { + _workerServers.httpsServer.reset(); + await use(_workerServers.httpsServer); + }, }); type Response = Awaited>; diff --git a/tests/network.spec.ts b/tests/network.spec.ts new file mode 100644 index 0000000..b202903 --- /dev/null +++ b/tests/network.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_network_requests', async ({ client, server }) => { + server.route('/', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(``); + }); + + server.route('/json', (req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ name: 'John Doe' })); + }); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me button', + ref: 's1e3', + }, + }); + + expect.poll(() => client.callTool({ + name: 'browser_network_requests', + arguments: {}, + })).toHaveTextContent(`[GET] http://localhost:8907/json => [200] OK`); +}); diff --git a/tests/testserver/cert.pem b/tests/testserver/cert.pem new file mode 100644 index 0000000..3388ed5 --- /dev/null +++ b/tests/testserver/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCjCCAvKgAwIBAgIULU/gkDm8IqC7PG8u3RID0AYyP6gwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MB4XDTIzMDgxMDIyNTc1MFoX +DTMzMDgwNzIyNTc1MFowGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArbS99qjKcnHr5G0Zc2xhDaOZnjQv +Fbiqxf/nbXt/7WaqryzpVKu7AT1ainBvuPEo7If9DhVnfF//2pGl0gbU31OU4/mr +ymQmczGEyZvOBDsZhtCif54o5OoO0BjhODNT8OWec9RT87n6RkH58MHlOi8xsPxQ +9n5U1CN/h2DyQF3aRKunEFCgtwPKWSjG+J/TAI9i0aSENXPiR8wjTrjg79s8Ehuj +NN8Wk6rKLU3sepG3GIMID5vLsVa2t9xqn562sP95Ee+Xp2YX3z7oYK99QCJdzacw +alhMHob1GCEKjDyxsD2IFRi7Dysiutfyzy3pMo6NALxFrwKVhWX0L4zVFIsI6JlV +dK8dHmDk0MRSqgB9sWXvEfSTXADEe8rncFSFpFz4Z8RNLmn5YSzQJzokNn41DUCP +dZTlTkcGTqvn5NqoY4sOV8rkFbgmTcqyijV/sebPjxCbJNcNmaSWa9FJ5IjRTpzM +38wLmxn+eKGK68n2JB3P7JP6LtsBShQEpXAF3rFfyNsP1bjquvGZVSjV8w/UwPE4 +kV5eq3j3D4913Zfxvzjp6PEmhStG0EQtIXvx/TRoYpaNWypIgZdbkZQp1HUIQL15 +D2Web4nazP3so1FC3ZgbrJZ2ozoadjLMp49NcSFdh+WRyVKuo0DIqR0zaiAzzf2D +G1q7TLKimM3XBMUCAwEAAaNIMEYwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLAYD +VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG +SIb3DQEBCwUAA4ICAQAvC5M1JFc21WVSLPvE2iVbt4HmirO3EENdDqs+rTYG5VJG +iE5ZuI6h/LjS5ptTfKovXQKaMr3pwp1pLMd/9q+6ZR1Hs9Z2wF6OZan4sb0uT32Y +1KGlj86QMiiSLdrJ/1Z9JHskHYNCep1ZTsUhGk0qqiNv+G3K2y7ZpvrT/xlnYMth +KLTuSVUwM8BBEPrCRLoXuaEy0LnvMvMVepIfP8tnMIL6zqmj3hXMPe4r4OFV/C5o +XX25bC7GyuPWIRYn2OWP92J1CODZD1rGRoDtmvqrQpHdeX9RYcKH0ZLZoIf5L3Hf +pPUtVkw3QGtjvKeG3b9usxaV9Od2Z08vKKk1PRkXFe8gqaeyicK7YVIOMTSuspAf +JeJEHns6Hg61Exbo7GwdX76xlmQ/Z43E9BPHKgLyZ9WuJ0cysqN4aCyvS9yws9to +ki7iMZqJUsmE2o09n9VaEsX6uQANZtLjI9wf+IgJuueDTNrkzQkhU7pbaPMsSG40 +AgGY/y4BR0H8sbhNnhqtZH7RcXV9VCJoPBAe+YiuXRiXyZHWxwBRyBE3e7g4MKHg +hrWtaWUAs7gbavHwjqgU63iVItDSk7t4fCiEyObjK09AaNf2DjjaSGf8YGza4bNy +BjYinYJ6/eX//gp+abqfocFbBP7D9zRDgMIbVmX/Ey6TghKiLkZOdbzcpO4Wgg== +-----END CERTIFICATE----- diff --git a/tests/testserver/index.ts b/tests/testserver/index.ts new file mode 100644 index 0000000..a1d60ec --- /dev/null +++ b/tests/testserver/index.ts @@ -0,0 +1,145 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import path from 'path'; + +const fulfillSymbol = Symbol('fulfil callback'); +const rejectSymbol = Symbol('reject callback'); + +export class TestServer { + private _server: http.Server; + readonly debugServer: any; + private _routes = new Map any>(); + private _csp = new Map(); + private _extraHeaders = new Map(); + private _requestSubscribers = new Map>(); + readonly PORT: number; + readonly PREFIX: string; + readonly CROSS_PROCESS_PREFIX: string; + + static async create(port: number): Promise { + const server = new TestServer(port); + await new Promise(x => server._server.once('listening', x)); + return server; + } + + static async createHTTPS(port: number): Promise { + const server = new TestServer(port, { + key: await fs.promises.readFile(path.join(__dirname, 'key.pem')), + cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')), + passphrase: 'aaaa', + }); + await new Promise(x => server._server.once('listening', x)); + return server; + } + + constructor(port: number, sslOptions?: object) { + if (sslOptions) + this._server = https.createServer(sslOptions, this._onRequest.bind(this)); + else + this._server = http.createServer(this._onRequest.bind(this)); + this._server.listen(port); + this.debugServer = require('debug')('pw:testserver'); + + const cross_origin = '127.0.0.1'; + const same_origin = 'localhost'; + const protocol = sslOptions ? 'https' : 'http'; + this.PORT = port; + this.PREFIX = `${protocol}://${same_origin}:${port}`; + this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}`; + } + + setCSP(path: string, csp: string) { + this._csp.set(path, csp); + } + + setExtraHeaders(path: string, object: Record) { + this._extraHeaders.set(path, object); + } + + async stop() { + this.reset(); + await new Promise(x => this._server.close(x)); + } + + route(path: string, handler: (request: http.IncomingMessage, response: http.ServerResponse) => any) { + this._routes.set(path, handler); + } + + redirect(from: string, to: string) { + this.route(from, (req, res) => { + const headers = this._extraHeaders.get(req.url!) || {}; + res.writeHead(302, { ...headers, location: to }); + res.end(); + }); + } + + waitForRequest(path: string): Promise { + let promise = this._requestSubscribers.get(path); + if (promise) + return promise; + let fulfill, reject; + promise = new Promise((f, r) => { + fulfill = f; + reject = r; + }); + promise[fulfillSymbol] = fulfill; + promise[rejectSymbol] = reject; + this._requestSubscribers.set(path, promise); + return promise; + } + + reset() { + this._routes.clear(); + this._csp.clear(); + this._extraHeaders.clear(); + this._server.closeAllConnections(); + const error = new Error('Static Server has been reset'); + for (const subscriber of this._requestSubscribers.values()) + subscriber[rejectSymbol].call(null, error); + this._requestSubscribers.clear(); + } + + _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { + request.on('error', error => { + if ((error as any).code === 'ECONNRESET') + response.end(); + else + throw error; + }); + (request as any).postBody = new Promise(resolve => { + const chunks: Buffer[] = []; + request.on('data', chunk => { + chunks.push(chunk); + }); + request.on('end', () => resolve(Buffer.concat(chunks))); + }); + const path = request.url || '/'; + this.debugServer(`request ${request.method} ${path}`); + // Notify request subscriber. + if (this._requestSubscribers.has(path)) { + this._requestSubscribers.get(path)![fulfillSymbol].call(null, request); + this._requestSubscribers.delete(path); + } + const handler = this._routes.get(path); + if (handler) + handler.call(null, request, response); + } +} diff --git a/tests/testserver/key.pem b/tests/testserver/key.pem new file mode 100644 index 0000000..28edf51 --- /dev/null +++ b/tests/testserver/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +ZkZVSOEp+sYBf/tmptlKr49nO+dTjQ== +-----END PRIVATE KEY----- diff --git a/tests/testserver/san.cnf b/tests/testserver/san.cnf new file mode 100644 index 0000000..2f4864b --- /dev/null +++ b/tests/testserver/san.cnf @@ -0,0 +1,19 @@ +# openssl req -new -x509 -days 3650 -key key.pem -out cert.pem -config san.cnf -extensions v3_req + +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +CN = playwright-test + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 +IP.2 = ::1