Skip to content

Commit 4708fa6

Browse files
committed
patch
1 parent b5be37e commit 4708fa6

11 files changed

+234
-12
lines changed

package-lock.json

Lines changed: 3 additions & 4 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.22",
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.11.0",
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
@@ -48,6 +48,7 @@ const defaultConfig: Config = {
4848
launchOptions: {
4949
channel: 'chrome',
5050
headless: os.platform() === 'linux' && !process.env.DISPLAY,
51+
handleSIGTERM: false, // To prevent the race between signal handler and context close (including stopping trace)
5152
},
5253
contextOptions: {
5354
viewport: null,

src/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ ${code.join('\n')}
280280
async close() {
281281
if (!this._browserContext)
282282
return;
283+
if (process.env.TRACE)
284+
await this._browserContext.tracing.stop({ path: process.env.TRACE });
283285
const browserContext = this._browserContext;
284286
const browser = this._browser;
285287
this._createBrowserContextPromise = undefined;
@@ -314,6 +316,8 @@ ${code.join('\n')}
314316
for (const page of this._browserContext.pages())
315317
this._onPageCreated(page);
316318
this._browserContext.on('page', page => this._onPageCreated(page));
319+
if (process.env.TRACE)
320+
await this._browserContext.tracing.start({ screenshots: true, snapshots: true, sources: true });
317321
}
318322
return this._browserContext;
319323
}

src/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import snapshot from './tools/snapshot.js';
2727
import tabs from './tools/tabs.js';
2828
import screen from './tools/screen.js';
2929
import testing from './tools/testing.js';
30+
import assert from './tools/assert.js';
3031

3132
import type { Tool } from './tools/tool.js';
3233

@@ -43,6 +44,7 @@ export const snapshotTools: Tool<any>[] = [
4344
...snapshot,
4445
...tabs(true),
4546
...testing,
47+
...assert
4648
];
4749

4850
export const screenshotTools: Tool<any>[] = [

src/tools/assert.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
title: 'Assert that the element or the whole page contains the expected text',
39+
description: 'Returns JSON having "result" (PASS or FAIL), "against" (assert against element or page) and "error" (details if result is FAIL).',
40+
inputSchema: assertContainTextSchema,
41+
type: 'readOnly',
42+
},
43+
44+
handle: async (context, params) => {
45+
const validatedParams = assertContainTextSchema.parse(params);
46+
if (validatedParams.against === 'element') {
47+
if (validatedParams.ref === undefined)
48+
throw new Error('ref is required when asserting against an element');
49+
const locator = context.currentTabOrDie().snapshotOrDie().refLocator(validatedParams.ref);
50+
const code = [
51+
`// Assert ${params.element} contains ${params.expected}`,
52+
`await expect(page.${await generateLocator(locator)}).toContainText('${validatedParams.expected}');`,
53+
];
54+
return {
55+
code,
56+
action: async () => {
57+
try {
58+
await expect(locator).toContainText(replaceEnvVar(validatedParams.expected));
59+
return {
60+
content: [{
61+
type: 'text',
62+
text: JSON.stringify({ result: 'PASS', against: 'element' }),
63+
}],
64+
};
65+
} catch (err) {
66+
const error = err instanceof Error ? err.message : String(err);
67+
return {
68+
content: [{
69+
type: 'text',
70+
text: JSON.stringify({ result: 'FAIL', error, against: 'element' }),
71+
}],
72+
};
73+
}
74+
},
75+
captureSnapshot: false,
76+
waitForNetwork: false,
77+
};
78+
} else {
79+
const locator = context.currentTabOrDie().page.locator('body');
80+
const code = [
81+
`// Assert page contains ${params.expected}`,
82+
`await expect(page.${await generateLocator(locator)}).toContainText('${validatedParams.expected}');`,
83+
];
84+
return {
85+
code,
86+
action: async () => {
87+
try {
88+
await expect(locator).toContainText(replaceEnvVar(validatedParams.expected));
89+
return {
90+
content: [{
91+
type: 'text',
92+
text: JSON.stringify({ result: 'PASS', against: 'page' }),
93+
}],
94+
};
95+
} catch (err) {
96+
const error = err instanceof Error ? err.message : String(err);
97+
return {
98+
content: [{
99+
type: 'text',
100+
text: JSON.stringify({ result: 'FAIL', error, against: 'page' }),
101+
}],
102+
};
103+
}
104+
},
105+
captureSnapshot: false,
106+
waitForNetwork: false,
107+
};
108+
}
109+
},
110+
});
111+
112+
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',
@@ -32,7 +33,7 @@ const navigate: ToolFactory = captureSnapshot => defineTool({
3233

3334
handle: async (context, params) => {
3435
const tab = await context.ensureTab();
35-
await tab.navigate(params.url);
36+
await tab.navigate(replaceEnvVar(params.url));
3637

3738
const code = [
3839
`// 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

@@ -164,11 +165,11 @@ const type = defineTool({
164165
if (params.slowly) {
165166
code.push(`// Press "${params.text}" sequentially into "${params.element}"`);
166167
code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
167-
steps.push(() => locator.pressSequentially(params.text));
168+
steps.push(() => locator.pressSequentially(replaceEnvVar(params.text)));
168169
} else {
169170
code.push(`// Fill "${params.text}" into "${params.element}"`);
170171
code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
171-
steps.push(() => locator.fill(params.text));
172+
steps.push(() => locator.fill(replaceEnvVar(params.text)));
172173
}
173174

174175
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)