Skip to content

Commit 9332f7e

Browse files
committed
patch
1 parent 43aa400 commit 9332f7e

11 files changed

+235
-15
lines changed

package-lock.json

Lines changed: 6 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
{
2-
"name": "@playwright/mcp",
2+
"name": "@aethr/playwright-mcp",
33
"version": "0.0.19",
44
"description": "Playwright Tools for MCP",
55
"type": "module",
66
"repository": {
77
"type": "git",
8-
"url": "git+https://github.com/microsoft/playwright-mcp.git"
8+
"url": "git+https://github.com/autifyhq/aethr-playwright-mcp.git"
99
},
10-
"homepage": "https://playwright.dev",
1110
"engines": {
1211
"node": ">=18"
1312
},
1413
"author": {
15-
"name": "Microsoft Corporation"
14+
"name": "Autify Inc."
1615
},
1716
"license": "Apache-2.0",
1817
"scripts": {
@@ -36,6 +35,7 @@
3635
},
3736
"dependencies": {
3837
"@modelcontextprotocol/sdk": "^1.10.1",
38+
"@playwright/test": "1.53.0-alpha-1746218818000",
3939
"commander": "^13.1.0",
4040
"playwright": "1.53.0-alpha-1746218818000",
4141
"yaml": "^2.7.1",
@@ -44,7 +44,6 @@
4444
"devDependencies": {
4545
"@eslint/eslintrc": "^3.2.0",
4646
"@eslint/js": "^9.19.0",
47-
"@playwright/test": "1.53.0-alpha-1746218818000",
4847
"@stylistic/eslint-plugin": "^3.0.1",
4948
"@types/node": "^22.13.10",
5049
"@typescript-eslint/eslint-plugin": "^8.26.1",

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const defaultConfig: Config = {
4545
launchOptions: {
4646
channel: 'chrome',
4747
headless: os.platform() === 'linux' && !process.env.DISPLAY,
48+
handleSIGTERM: false, // To prevent the race between signal handler and context close (including stopping trace)
4849
},
4950
contextOptions: {
5051
viewport: null,

src/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ ${code.join('\n')}
275275
async close() {
276276
if (!this._browserContext)
277277
return;
278+
if (process.env.TRACE)
279+
await this._browserContext.tracing.stop({ path: process.env.TRACE });
278280
const browserContext = this._browserContext;
279281
const browser = this._browser;
280282
this._createBrowserContextPromise = undefined;
@@ -294,6 +296,8 @@ ${code.join('\n')}
294296
for (const page of this._browserContext.pages())
295297
this._onPageCreated(page);
296298
this._browserContext.on('page', page => this._onPageCreated(page));
299+
if (process.env.TRACE)
300+
await this._browserContext.tracing.start({ screenshots: true, snapshots: true, sources: true });
297301
}
298302
return this._browserContext;
299303
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import testing from './tools/testing.js';
3131
import type { Tool } from './tools/tool.js';
3232
import type { Config } from '../config.js';
3333
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
34+
import assert from './tools/assert.js';
3435

3536
const snapshotTools: Tool<any>[] = [
3637
...common(true),
@@ -45,6 +46,7 @@ const snapshotTools: Tool<any>[] = [
4546
...snapshot,
4647
...tabs(true),
4748
...testing,
49+
...assert,
4850
];
4951

5052
const screenshotTools: Tool<any>[] = [

src/tools/assert.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Autify Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { z } from 'zod';
18+
import { expect } from '@playwright/test';
19+
import { replaceEnvVar } from './utils.js';
20+
21+
import { defineTool } from './tool.js';
22+
import { generateLocator } from '../context.js';
23+
24+
const elementSchema = z.object({
25+
element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
26+
ref: z.string().describe('Exact target element reference from the page snapshot'),
27+
});
28+
29+
const assertContainTextSchema = elementSchema.partial().extend({
30+
against: z.enum(['element', 'page']).describe('Assert against the specified element or the whole page. If page, element and ref are not needed.'),
31+
expected: z.string().describe('Expected text to be contained in the specified element or the whole page'),
32+
});
33+
34+
const assertContainText = defineTool({
35+
capability: 'core',
36+
schema: {
37+
name: 'browser_assert_contain_text',
38+
description: 'Assert that the element or the whole page contains the expected text. It returns JSON having "result" (PASS or FAIL), "against" (assert against element or page) and "error" (details if result is FAIL).',
39+
inputSchema: assertContainTextSchema,
40+
},
41+
42+
handle: async (context, params) => {
43+
const validatedParams = assertContainTextSchema.parse(params);
44+
if (validatedParams.against === 'element') {
45+
if (validatedParams.ref === undefined)
46+
throw new Error('ref is required when asserting against an element');
47+
const locator = context.currentTabOrDie().snapshotOrDie().refLocator(validatedParams.ref);
48+
const code = [
49+
`// Assert ${params.element} contains ${params.expected}`,
50+
`await expect(page.${await generateLocator(locator)}).toContainText('${validatedParams.expected}');`,
51+
];
52+
return {
53+
code,
54+
action: async () => {
55+
try {
56+
await expect(locator).toContainText(replaceEnvVar(validatedParams.expected));
57+
return {
58+
content: [{
59+
type: 'text',
60+
text: JSON.stringify({ result: 'PASS', against: 'element' }),
61+
}],
62+
};
63+
} catch (err) {
64+
const error = err instanceof Error ? err.message : String(err);
65+
return {
66+
content: [{
67+
type: 'text',
68+
text: JSON.stringify({ result: 'FAIL', error, against: 'element' }),
69+
}],
70+
};
71+
}
72+
},
73+
captureSnapshot: false,
74+
waitForNetwork: false,
75+
};
76+
} else {
77+
const locator = context.currentTabOrDie().page.locator('body');
78+
const code = [
79+
`// Assert page contains ${params.expected}`,
80+
`await expect(page.${await generateLocator(locator)}).toContainText('${validatedParams.expected}');`,
81+
];
82+
return {
83+
code,
84+
action: async () => {
85+
try {
86+
await expect(locator).toContainText(replaceEnvVar(validatedParams.expected));
87+
return {
88+
content: [{
89+
type: 'text',
90+
text: JSON.stringify({ result: 'PASS', against: 'page' }),
91+
}],
92+
};
93+
} catch (err) {
94+
const error = err instanceof Error ? err.message : String(err);
95+
return {
96+
content: [{
97+
type: 'text',
98+
text: JSON.stringify({ result: 'FAIL', error, against: 'page' }),
99+
}],
100+
};
101+
}
102+
},
103+
captureSnapshot: false,
104+
waitForNetwork: false,
105+
};
106+
}
107+
},
108+
});
109+
110+
export default [assertContainText];

src/tools/navigate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { z } from 'zod';
1818
import { defineTool, type ToolFactory } from './tool.js';
19+
import { replaceEnvVar } from './utils.js';
1920

2021
const navigate: ToolFactory = captureSnapshot => defineTool({
2122
capability: 'core',
@@ -30,7 +31,7 @@ const navigate: ToolFactory = captureSnapshot => defineTool({
3031

3132
handle: async (context, params) => {
3233
const tab = await context.ensureTab();
33-
await tab.navigate(params.url);
34+
await tab.navigate(replaceEnvVar(params.url));
3435

3536
const code = [
3637
`// Navigate to ${params.url}`,

src/tools/snapshot.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { z } from 'zod';
1919
import { defineTool } from './tool.js';
2020
import * as javascript from '../javascript.js';
2121
import { outputFile } from '../config.js';
22+
import { replaceEnvVar } from './utils.js';
2223

2324
import type * as playwright from 'playwright';
2425

@@ -154,11 +155,11 @@ const type = defineTool({
154155
if (params.slowly) {
155156
code.push(`// Press "${params.text}" sequentially into "${params.element}"`);
156157
code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
157-
steps.push(() => locator.pressSequentially(params.text));
158+
steps.push(() => locator.pressSequentially(replaceEnvVar(params.text)));
158159
} else {
159160
code.push(`// Fill "${params.text}" into "${params.element}"`);
160161
code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
161-
steps.push(() => locator.fill(params.text));
162+
steps.push(() => locator.fill(replaceEnvVar(params.text)));
162163
}
163164

164165
if (params.submit) {

src/tools/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,16 @@ export function sanitizeForFilePath(s: string) {
7777
return sanitize(s);
7878
return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
7979
}
80+
81+
export function replaceEnvVar(input: string): string {
82+
if (input.startsWith('${') && input.endsWith('}')) {
83+
const variable = input.slice(2, -1);
84+
const envValue = process.env[variable];
85+
if (envValue)
86+
return envValue;
87+
else
88+
return input;
89+
} else {
90+
return input;
91+
}
92+
}

tests/assert.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) Autify Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './fixtures.js';
18+
19+
test('browser_assert_contain_text element PASS', async ({ client }) => {
20+
await client.callTool({
21+
name: 'browser_navigate',
22+
arguments: {
23+
url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
24+
},
25+
});
26+
27+
expect(await client.callTool({
28+
name: 'browser_assert_contain_text',
29+
arguments: {
30+
element: 'Submit button',
31+
ref: 's1e3',
32+
against: 'element',
33+
expected: 'Submit',
34+
},
35+
})).toHaveTextContent('{"result":"PASS","against":"element"}');
36+
});
37+
38+
test('browser_assert_contain_text element FAIL', async ({ client }) => {
39+
await client.callTool({
40+
name: 'browser_navigate',
41+
arguments: {
42+
url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
43+
},
44+
});
45+
46+
expect(await client.callTool({
47+
name: 'browser_assert_contain_text',
48+
arguments: {
49+
element: 'Submit button',
50+
ref: 's1e3',
51+
against: 'element',
52+
expected: 'Fail',
53+
},
54+
})).toHaveTextContent(/{"result":"FAIL","error":".+","against":"element"}/);
55+
});
56+
57+
test('browser_assert_contain_text page PASS', async ({ client }) => {
58+
await client.callTool({
59+
name: 'browser_navigate',
60+
arguments: {
61+
url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
62+
},
63+
});
64+
65+
expect(await client.callTool({
66+
name: 'browser_assert_contain_text',
67+
arguments: {
68+
against: 'page',
69+
expected: 'Submit',
70+
},
71+
})).toHaveTextContent('{"result":"PASS","against":"page"}');
72+
});
73+
74+
test('browser_assert_contain_text page FAIL', async ({ client }) => {
75+
await client.callTool({
76+
name: 'browser_navigate',
77+
arguments: {
78+
url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
79+
},
80+
});
81+
82+
expect(await client.callTool({
83+
name: 'browser_assert_contain_text',
84+
arguments: {
85+
against: 'page',
86+
expected: 'Fail',
87+
},
88+
})).toHaveTextContent(/{"result":"FAIL","error":".+","against":"page"}/);
89+
});

tests/capabilities.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { test, expect } from './fixtures.js';
1919
test('test snapshot tool list', async ({ client }) => {
2020
const { tools } = await client.listTools();
2121
expect(new Set(tools.map(t => t.name))).toEqual(new Set([
22+
'browser_assert_contain_text',
2223
'browser_click',
2324
'browser_console_messages',
2425
'browser_drag',

0 commit comments

Comments
 (0)