chromedevtools--chrome-devtools-mcp
f3ef0fb1ee
Bumps the bundled group with 1 update: [puppeteer](https://github.com/puppeteer/puppeteer). Updates `puppeteer` from 25.0.4 to 25.1.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/puppeteer/puppeteer/releases">puppeteer's releases</a>.</em></p> <blockquote> <h2>puppeteer-core: v25.1.0</h2> <h2><a href="https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v25.0.4...puppeteer-core-v25.1.0">25.1.0</a> (2026-05-26)</h2> <h3>🎉 Features</h3> <ul> <li>roll to Chrome 149.0.7827.2 (<a href="https://github.com/puppeteer/puppeteer/commit/af1b9be6b6a178f7ea6e197f738ca3cf99d786f7">af1b9be</a>)</li> <li>roll to Firefox 151.0 (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15013">#15013</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/767ea549da1510f0838987f250b5d6be4e0f8960">767ea54</a>)</li> </ul> <h3>🛠️ Fixes</h3> <ul> <li>roll to Chrome 148.0.7778.178 (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15014">#15014</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/59764acccc9e1c7bca75a60472b288595d3f6cbf">59764ac</a>)</li> </ul> <h3>📄 Documentation</h3> <ul> <li>use ESM and top level await (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15030">#15030</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/34ecc62957bdd2c83820a51d5d1a6acb81e2957d">34ecc62</a>)</li> </ul> <h3>🏗️ Refactor</h3> <ul> <li>remove debug dependency (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15023">#15023</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/94d1e1c3dc84ed102d2e801a4e6343cd9fd4a9af">94d1e1c</a>)</li> </ul> <h3>Dependencies</h3> <ul> <li>The following workspace dependencies were updated <ul> <li>dependencies <ul> <li><code>@puppeteer/browsers</code> bumped from 3.0.3 to 3.0.4</li> </ul> </li> </ul> </li> </ul> <h2>puppeteer: v25.1.0</h2> <h2><a href="https://github.com/puppeteer/puppeteer/compare/puppeteer-v25.0.4...puppeteer-v25.1.0">25.1.0</a> (2026-05-26)</h2> <h3>🎉 Features</h3> <ul> <li>roll to Chrome 149.0.7827.2 (<a href="https://github.com/puppeteer/puppeteer/commit/af1b9be6b6a178f7ea6e197f738ca3cf99d786f7">af1b9be</a>)</li> </ul> <h3>🛠️ Fixes</h3> <ul> <li>improve progress bar and install (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15042">#15042</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/51db32a5bb48f40cc635ac3eaa2ebb60e6f5bfd5">51db32a</a>)</li> <li>support concurrency in progress bars (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15045">#15045</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/ab0171d8f4206bdb9dd4e93c2db8d28530bf3fb9">ab0171d</a>)</li> </ul> <h3>🏗️ Refactor</h3> <ul> <li>replace cosmiconfig with lilconfig (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15031">#15031</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/4a1c2fffe634d430228cac3f8bc7a72f4b5a4a29">4a1c2ff</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/puppeteer/puppeteer/blob/main/CHANGELOG.md">puppeteer's changelog</a>.</em></p> <blockquote> <h2><a href="https://github.com/puppeteer/puppeteer/compare/puppeteer-v25.0.4...puppeteer-v25.1.0">25.1.0</a> (2026-05-26)</h2> <h3>🎉 Features</h3> <ul> <li>roll to Chrome 149.0.7827.2 (<a href="https://github.com/puppeteer/puppeteer/commit/af1b9be6b6a178f7ea6e197f738ca3cf99d786f7">af1b9be</a>)</li> <li>roll to Firefox 151.0 (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15013">#15013</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/767ea549da1510f0838987f250b5d6be4e0f8960">767ea54</a>)</li> </ul> <h3>🛠️ Fixes</h3> <ul> <li>roll to Chrome 148.0.7778.178 (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15014">#15014</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/59764acccc9e1c7bca75a60472b288595d3f6cbf">59764ac</a>)</li> </ul> <h3>🏗️ Refactor</h3> <ul> <li>remove debug dependency (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15023">#15023</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/94d1e1c3dc84ed102d2e801a4e6343cd9fd4a9af">94d1e1c</a>)</li> </ul> <h3>Dependencies</h3> <ul> <li>The following workspace dependencies were updated <ul> <li>dependencies <ul> <li><code>@puppeteer/browsers</code> bumped from 3.0.3 to 3.0.4</li> </ul> </li> </ul> </li> </ul> <h3>📄 Documentation</h3> <ul> <li>use ESM and top level await (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15030">#15030</a>) (<a href="https://github.com/puppeteer/puppeteer/commit/34ecc62957bdd2c83820a51d5d1a6acb81e2957d">34ecc62</a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/puppeteer/puppeteer/commit/ede66693bc0a6d9a8029f66288d13e26966173ce"><code>ede6669</code></a> chore: release main (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15056">#15056</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/7bc09e7d30e98687668dfe33089b9a3a633cece7"><code>7bc09e7</code></a> chore(deps): bump the all group with 5 updates (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15052">#15052</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/8c8117047020a49d61f1dff5e302f05ab672951d"><code>8c81170</code></a> chore(deps): bump the all group in /website with 3 updates (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15051">#15051</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/09eced565903369338ff7537b20727b24a284495"><code>09eced5</code></a> chore: update lock</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/53b9fdadc131449377ed6de59162426140d29db6"><code>53b9fda</code></a> chore(deps): bump the dependencies group with 2 updates (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15049">#15049</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/d8424116590afac99369c0e996e0dfaf9029b34c"><code>d842411</code></a> chore(deps): bump node from <code>050bf2b</code> to <code>8530f76</code> in /docker in the all grou...</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/1d2a5696d3c7b592b8d65327d76cc2264f9fba9f"><code>1d2a569</code></a> docs: document read-only Docker directories (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15048">#15048</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/ab0171d8f4206bdb9dd4e93c2db8d28530bf3fb9"><code>ab0171d</code></a> fix: support concurrency in progress bars (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15045">#15045</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/51db32a5bb48f40cc635ac3eaa2ebb60e6f5bfd5"><code>51db32a</code></a> fix: improve progress bar and install (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15042">#15042</a>)</li> <li><a href="https://github.com/puppeteer/puppeteer/commit/d32384baa5ef8bcd81319ee698e9d46edba1e957"><code>d32384b</code></a> chore(deps): bump qs and express in /website (<a href="https://redirect.github.com/puppeteer/puppeteer/issues/15040">#15040</a>)</li> <li>Additional commits viewable in <a href="https://github.com/puppeteer/puppeteer/compare/puppeteer-v25.0.4...puppeteer-v25.1.0">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
1364 行
40 KiB
TypeScript
1364 行
40 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import {describe, it} from 'node:test';
|
|
|
|
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
|
|
import {McpResponse} from '../../src/McpResponse.js';
|
|
import {TextSnapshot} from '../../src/TextSnapshot.js';
|
|
import {
|
|
click,
|
|
hover,
|
|
fill,
|
|
drag,
|
|
fillForm,
|
|
uploadFile,
|
|
pressKey,
|
|
clickAt,
|
|
typeText,
|
|
} from '../../src/tools/input.js';
|
|
import {parseKey} from '../../src/utils/keyboard.js';
|
|
import {serverHooks} from '../server.js';
|
|
import {html, withMcpContext, getTextContent} from '../utils.js';
|
|
|
|
describe('input', () => {
|
|
const server = serverHooks();
|
|
|
|
describe('click', () => {
|
|
it('clicks', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button onclick="this.innerText = 'clicked';">test</button>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/clicked'));
|
|
});
|
|
});
|
|
it('double clicks', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button ondblclick="this.innerText = 'dblclicked';"
|
|
>test</button
|
|
>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
dblClick: true,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully double clicked on the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/dblclicked'));
|
|
});
|
|
});
|
|
it('waits for navigation', async () => {
|
|
const resolveNavigation = Promise.withResolvers<void>();
|
|
server.addHtmlRoute(
|
|
'/link',
|
|
html`<a href="/navigated">Navigate page</a>`,
|
|
);
|
|
server.addRoute('/navigated', async (_req, res) => {
|
|
await resolveNavigation.promise;
|
|
res.write(html`<main>I was navigated</main>`);
|
|
res.end();
|
|
});
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.goto(server.getRoute('/link'));
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
const clickPromise = click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
const [t1, t2] = await Promise.all([
|
|
clickPromise.then(() => Date.now()),
|
|
new Promise<number>(res => {
|
|
setTimeout(() => {
|
|
resolveNavigation.resolve();
|
|
res(Date.now());
|
|
}, 300);
|
|
}),
|
|
]);
|
|
|
|
assert(t1 > t2, 'Waited for navigation');
|
|
});
|
|
});
|
|
|
|
it('reports the new URL when click triggers a navigation', async () => {
|
|
server.addHtmlRoute(
|
|
'/start',
|
|
html`<a href="/after-click">Navigate page</a>`,
|
|
);
|
|
server.addHtmlRoute('/after-click', html`<main>arrived</main>`);
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.goto(server.getRoute('/start'));
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_2',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
const result = await response.handle('click', context);
|
|
const textContent = getTextContent(result.content[0]);
|
|
const expectedUrl = server.getRoute('/after-click');
|
|
assert.ok(
|
|
textContent.includes(`Page navigated to ${expectedUrl}.`),
|
|
`Expected response to mention navigation to ${expectedUrl}, got: ${textContent}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('does not report navigation when click does not navigate', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button onclick="this.innerText = 'clicked';">test</button>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
const result = await response.handle('click', context);
|
|
const textContent = getTextContent(result.content[0]);
|
|
assert.ok(
|
|
!textContent.includes('Page navigated to '),
|
|
`Did not expect a navigation line, got: ${textContent}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('waits for stable DOM', async () => {
|
|
server.addHtmlRoute(
|
|
'/unstable',
|
|
html`
|
|
<button>Click to change to see time</button>
|
|
<script>
|
|
const button = document.querySelector('button');
|
|
button.addEventListener('click', () => {
|
|
setTimeout(() => {
|
|
button.textContent = Date.now();
|
|
}, 50);
|
|
});
|
|
</script>
|
|
`,
|
|
);
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.goto(server.getRoute('/unstable'));
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
const handlerResolveTime = await click
|
|
.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
)
|
|
.then(() => Date.now());
|
|
const buttonChangeTime = await page.evaluate(() => {
|
|
const button = document.querySelector('button');
|
|
return Number(button?.textContent);
|
|
});
|
|
|
|
assert(handlerResolveTime > buttonChangeTime, 'Waited for navigation');
|
|
});
|
|
});
|
|
|
|
it('does not include snapshot by default', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button onclick="this.innerText = 'clicked';">test</button>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.strictEqual(response.snapshotParams, undefined);
|
|
});
|
|
});
|
|
|
|
it('includes snapshot if includeSnapshot is true', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button onclick="this.innerText = 'clicked';">test</button>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
includeSnapshot: true,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.notStrictEqual(response.snapshotParams, undefined);
|
|
});
|
|
});
|
|
|
|
it('selects a collapsed native select option by option uid', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<select onchange="document.body.dataset.selected = this.value">
|
|
<option value="v1">one</option>
|
|
<option value="v2">two</option>
|
|
</select>`,
|
|
);
|
|
const mcpPage = context.getSelectedMcpPage();
|
|
mcpPage.textSnapshot = await TextSnapshot.create(mcpPage);
|
|
const optionNode = [...mcpPage.textSnapshot.idToNode.values()].find(
|
|
node => node.role === 'option' && node.name === 'two',
|
|
);
|
|
assert.ok(optionNode);
|
|
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: optionNode.id,
|
|
},
|
|
page: mcpPage,
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.deepStrictEqual(
|
|
await page.evaluate(() => {
|
|
const select = document.querySelector('select');
|
|
return {
|
|
selectedValue: select?.value,
|
|
changeEventValue: document.body.dataset.selected,
|
|
};
|
|
}),
|
|
{
|
|
selectedValue: 'v2',
|
|
changeEventValue: 'v2',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it('selects a collapsed native optgroup option by option uid', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<select onchange="document.body.dataset.selected = this.value">
|
|
<optgroup label="Numbers">
|
|
<option value="v1">one</option>
|
|
<option value="v2">two</option>
|
|
</optgroup>
|
|
</select>`,
|
|
);
|
|
const mcpPage = context.getSelectedMcpPage();
|
|
mcpPage.textSnapshot = await TextSnapshot.create(mcpPage);
|
|
const optionNode = [...mcpPage.textSnapshot.idToNode.values()].find(
|
|
node => node.role === 'option' && node.name === 'two',
|
|
);
|
|
assert.ok(optionNode);
|
|
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: optionNode.id,
|
|
},
|
|
page: mcpPage,
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.deepStrictEqual(
|
|
await page.evaluate(() => {
|
|
const select = document.querySelector('select');
|
|
return {
|
|
selectedValue: select?.value,
|
|
changeEventValue: document.body.dataset.selected,
|
|
};
|
|
}),
|
|
{
|
|
selectedValue: 'v2',
|
|
changeEventValue: 'v2',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it('clicks custom ARIA option elements through the normal click path', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<div role="listbox">
|
|
<div
|
|
role="option"
|
|
tabindex="0"
|
|
onclick="document.body.dataset.clicked = this.textContent.trim()"
|
|
>
|
|
custom two
|
|
</div>
|
|
</div>`,
|
|
);
|
|
const mcpPage = context.getSelectedMcpPage();
|
|
mcpPage.textSnapshot = await TextSnapshot.create(mcpPage);
|
|
const optionNode = [...mcpPage.textSnapshot.idToNode.values()].find(
|
|
node => node.role === 'option' && node.name === 'custom two',
|
|
);
|
|
assert.ok(optionNode);
|
|
|
|
await click.handler(
|
|
{
|
|
params: {
|
|
uid: optionNode.id,
|
|
},
|
|
page: mcpPage,
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked on the element',
|
|
);
|
|
assert.strictEqual(
|
|
await page.evaluate(() => document.body.dataset.clicked),
|
|
'custom two',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hover', () => {
|
|
it('hovers', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button onmouseover="this.innerText = 'hovered';">test</button>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await hover.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully hovered over the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/hovered'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('click_at', () => {
|
|
it('clicks at coordinates', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<div
|
|
style="width: 100px; height: 100px; background: red;"
|
|
onclick="this.innerText = 'clicked'"
|
|
></div>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await clickAt.handler(
|
|
{
|
|
params: {
|
|
x: 50,
|
|
y: 50,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully clicked at the coordinates',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/clicked'));
|
|
});
|
|
});
|
|
|
|
it('double clicks at coordinates', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<div
|
|
style="width: 100px; height: 100px; background: red;"
|
|
ondblclick="this.innerText = 'dblclicked'"
|
|
></div>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await clickAt.handler(
|
|
{
|
|
params: {
|
|
x: 50,
|
|
y: 50,
|
|
dblClick: true,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully double clicked at the coordinates',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/dblclicked'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fill', () => {
|
|
it('fills out an input', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<input />`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'test',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(await page.$('text/test'));
|
|
});
|
|
});
|
|
|
|
it('fills out a select by text', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<select
|
|
><option value="v1">one</option
|
|
><option value="v2">two</option></select
|
|
>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'two',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
const selectedValue = await page.evaluate(
|
|
() => document.querySelector('select')!.value,
|
|
);
|
|
assert.strictEqual(selectedValue, 'v2');
|
|
});
|
|
});
|
|
|
|
it('fills out a textarea marked as combobox', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<textarea role="combobox"></textarea>`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: '1',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(
|
|
await page.evaluate(() => {
|
|
return document.body.querySelector('textarea')?.value === '1';
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('fills out a textarea with long text', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<textarea></textarea>`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
page.setDefaultTimeout(1000);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: '1'.repeat(3000),
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.ok(
|
|
await page.evaluate(() => {
|
|
return (
|
|
document.body.querySelector('textarea')?.value.length === 3_000
|
|
);
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('types text', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<textarea></textarea>`);
|
|
await page.click('textarea');
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await typeText.handler(
|
|
{
|
|
params: {
|
|
text: 'test',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(response.responseLines[0], 'Typed text "test"');
|
|
assert.strictEqual(
|
|
await page.evaluate(() => {
|
|
return document.body.querySelector('textarea')?.value;
|
|
}),
|
|
'test',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('types text with submit key', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<textarea></textarea>`);
|
|
await page.click('textarea');
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await typeText.handler(
|
|
{
|
|
params: {
|
|
text: 'test',
|
|
submitKey: 'Tab',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Typed text "test + Tab"',
|
|
);
|
|
assert.strictEqual(
|
|
await page.evaluate(() => {
|
|
return document.body.querySelector('textarea')?.value;
|
|
}),
|
|
'test',
|
|
);
|
|
assert.ok(
|
|
await page.evaluate(() => {
|
|
return (
|
|
document.body.querySelector('textarea') !== document.activeElement
|
|
);
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('errors on invalid submit key', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<textarea></textarea>`);
|
|
await page.click('textarea');
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
try {
|
|
await typeText.handler(
|
|
{
|
|
params: {
|
|
text: 'test',
|
|
submitKey: 'XXX',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Unknown key: "XXX"');
|
|
}
|
|
});
|
|
});
|
|
|
|
it('reproduction: fill isolation', async () => {
|
|
await withMcpContext(async (_response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<form>
|
|
<input
|
|
id="email"
|
|
value="user@test.com"
|
|
/>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
/>
|
|
</form>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
// Fill email
|
|
const response1 = new McpResponse({} as ParsedArguments);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_2', // email input
|
|
value: 'new@test.com',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response1,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response1.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
|
|
// Fill password
|
|
const response2 = new McpResponse({} as ParsedArguments);
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_3', // password input
|
|
value: 'secret',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response2,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
response2.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
|
|
// Verify values
|
|
const values = await page.evaluate(() => {
|
|
return {
|
|
email: (document.getElementById('email') as HTMLInputElement).value,
|
|
password: (document.getElementById('password') as HTMLInputElement)
|
|
.value,
|
|
};
|
|
});
|
|
|
|
assert.strictEqual(
|
|
values.email,
|
|
'new@test.com',
|
|
'Email should be updated correctly',
|
|
);
|
|
assert.strictEqual(
|
|
values.password,
|
|
'secret',
|
|
'Password should be updated correctly',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('toggles checkboxes', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<input
|
|
type="checkbox"
|
|
id="cb"
|
|
/>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
// Check it
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'true',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the element',
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
let isChecked = await page.$eval(
|
|
'#cb',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
assert.strictEqual(isChecked, true);
|
|
|
|
// Uncheck it
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'false',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
new McpResponse({} as ParsedArguments),
|
|
context,
|
|
);
|
|
|
|
isChecked = await page.$eval(
|
|
'#cb',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
assert.strictEqual(isChecked, false);
|
|
});
|
|
});
|
|
|
|
it('toggles switches', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`
|
|
<div
|
|
role="switch"
|
|
aria-checked="false"
|
|
id="sw"
|
|
style="width: 20px; height: 20px; background: blue;"
|
|
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
|
|
>
|
|
switch
|
|
</div>
|
|
`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
// Turn it on
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'true',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
let swChecked = await page.$eval(
|
|
'#sw',
|
|
el => el.getAttribute('aria-checked') === 'true',
|
|
);
|
|
assert.strictEqual(swChecked, true);
|
|
|
|
// Turn it off
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
value: 'false',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
new McpResponse({} as ParsedArguments),
|
|
context,
|
|
);
|
|
|
|
swChecked = await page.$eval(
|
|
'#sw',
|
|
el => el.getAttribute('aria-checked') === 'true',
|
|
);
|
|
assert.strictEqual(swChecked, false);
|
|
});
|
|
});
|
|
|
|
it('selects radio buttons', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`
|
|
<input
|
|
type="radio"
|
|
name="group1"
|
|
id="r1"
|
|
checked
|
|
/>
|
|
<input
|
|
type="radio"
|
|
name="group1"
|
|
id="r2"
|
|
/>
|
|
`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
// Initial state
|
|
let r1Checked = await page.$eval(
|
|
'#r1',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
let r2Checked = await page.$eval(
|
|
'#r2',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
assert.strictEqual(r1Checked, true);
|
|
assert.strictEqual(r2Checked, false);
|
|
|
|
// Fill second radio with true
|
|
await fill.handler(
|
|
{
|
|
params: {
|
|
uid: '1_2',
|
|
value: 'true',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
r1Checked = await page.$eval(
|
|
'#r1',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
r2Checked = await page.$eval(
|
|
'#r2',
|
|
el => (el as HTMLInputElement).checked,
|
|
);
|
|
assert.strictEqual(r1Checked, false);
|
|
assert.strictEqual(r2Checked, true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('drags', () => {
|
|
it('drags one element onto another', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<div
|
|
role="button"
|
|
id="drag"
|
|
draggable="true"
|
|
>drag me</div
|
|
>
|
|
<div
|
|
id="drop"
|
|
aria-label="drop"
|
|
style="width: 100px; height: 100px; border: 1px solid black;"
|
|
ondrop="this.innerText = 'dropped';"
|
|
>
|
|
</div>
|
|
<script>
|
|
drag.addEventListener('dragstart', event => {
|
|
event.dataTransfer.setData('text/plain', event.target.id);
|
|
});
|
|
drop.addEventListener('dragover', event => {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'move';
|
|
});
|
|
drop.addEventListener('drop', event => {
|
|
event.preventDefault();
|
|
const data = event.dataTransfer.getData('text/plain');
|
|
event.target.appendChild(document.getElementById(data));
|
|
});
|
|
</script>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await drag.handler(
|
|
{
|
|
params: {
|
|
from_uid: '1_1',
|
|
to_uid: '1_2',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully dragged an element',
|
|
);
|
|
assert.ok(await page.$('text/dropped'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fill form', () => {
|
|
it('successfully fills out the form', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<form>
|
|
<label
|
|
>username<input
|
|
name="username"
|
|
type="text"
|
|
/></label>
|
|
<label
|
|
>email<input
|
|
name="email"
|
|
type="text"
|
|
/></label>
|
|
<input
|
|
type="submit"
|
|
value="Submit"
|
|
/>
|
|
</form>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await fillForm.handler(
|
|
{
|
|
params: {
|
|
elements: [
|
|
{
|
|
uid: '1_3',
|
|
value: 'test',
|
|
},
|
|
{
|
|
uid: '1_5',
|
|
value: 'test2',
|
|
},
|
|
],
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
'Successfully filled out the form',
|
|
);
|
|
assert.deepStrictEqual(
|
|
await page.evaluate(() => {
|
|
return [
|
|
// @ts-expect-error missing types
|
|
document.querySelector('input[name=username]').value,
|
|
// @ts-expect-error missing types
|
|
document.querySelector('input[name=email]').value,
|
|
];
|
|
}),
|
|
['test', 'test2'],
|
|
);
|
|
});
|
|
});
|
|
|
|
it('fill_form handles checkboxes', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<input
|
|
name="username"
|
|
type="text"
|
|
/><input
|
|
name="cb"
|
|
type="checkbox"
|
|
/>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await fillForm.handler(
|
|
{
|
|
params: {
|
|
elements: [
|
|
{
|
|
uid: '1_1',
|
|
value: 'test',
|
|
},
|
|
{
|
|
uid: '1_2',
|
|
value: 'true',
|
|
},
|
|
],
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.strictEqual(
|
|
await page.evaluate(() => {
|
|
// @ts-expect-error missing types
|
|
return document.querySelector('input[name=username]').value;
|
|
}),
|
|
'test',
|
|
);
|
|
assert.strictEqual(
|
|
await page.evaluate(() => {
|
|
// @ts-expect-error missing types
|
|
return document.querySelector('input[name=cb]').checked;
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('uploadFile', () => {
|
|
it('uploads a file to a file input', async () => {
|
|
const testFilePath = path.join(process.cwd(), 'test.txt');
|
|
await fs.writeFile(testFilePath, 'test file content');
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<form>
|
|
<input
|
|
type="file"
|
|
id="file-input"
|
|
/>
|
|
</form>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await uploadFile.handler(
|
|
{
|
|
params: {
|
|
uid: '1_2',
|
|
filePath: testFilePath,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
`File uploaded from ${testFilePath}.`,
|
|
);
|
|
});
|
|
|
|
await fs.unlink(testFilePath);
|
|
});
|
|
|
|
it('uploads a file when clicking an element opens a file uploader', async () => {
|
|
const testFilePath = path.join(process.cwd(), 'test.txt');
|
|
await fs.writeFile(testFilePath, 'test file content');
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<button id="file-chooser-button">Upload file</button>
|
|
<input
|
|
type="file"
|
|
id="file-input"
|
|
style="display: none;"
|
|
/>
|
|
<script>
|
|
document
|
|
.getElementById('file-chooser-button')
|
|
.addEventListener('click', () => {
|
|
document.getElementById('file-input').click();
|
|
});
|
|
</script>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
await uploadFile.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
filePath: testFilePath,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
assert.ok(response.includeSnapshot);
|
|
assert.strictEqual(
|
|
response.responseLines[0],
|
|
`File uploaded from ${testFilePath}.`,
|
|
);
|
|
const uploadedFileName = await page.$eval('#file-input', el => {
|
|
const input = el as HTMLInputElement;
|
|
return input.files?.[0]?.name;
|
|
});
|
|
assert.strictEqual(uploadedFileName, 'test.txt');
|
|
|
|
await fs.unlink(testFilePath);
|
|
});
|
|
});
|
|
|
|
it('throws an error if the element is not a file input and does not open a file chooser', async () => {
|
|
const testFilePath = path.join(process.cwd(), 'test.txt');
|
|
await fs.writeFile(testFilePath, 'test file content');
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(html`<div>Not a file input</div>`);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
await assert.rejects(
|
|
uploadFile.handler(
|
|
{
|
|
params: {
|
|
uid: '1_1',
|
|
filePath: testFilePath,
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
),
|
|
{
|
|
message:
|
|
'Failed to upload file. The element could not accept the file directly, and clicking it did not trigger a file chooser.',
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(response.responseLines.length, 0);
|
|
assert.strictEqual(response.snapshotParams, undefined);
|
|
|
|
await fs.unlink(testFilePath);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('press_key', () => {
|
|
it('parses keys', () => {
|
|
assert.deepStrictEqual(parseKey('Shift+A'), ['A', 'Shift']);
|
|
assert.deepStrictEqual(parseKey('Shift++'), ['+', 'Shift']);
|
|
assert.deepStrictEqual(parseKey('Control+Shift++'), [
|
|
'+',
|
|
'Control',
|
|
'Shift',
|
|
]);
|
|
assert.deepStrictEqual(parseKey('Shift'), ['Shift']);
|
|
assert.deepStrictEqual(parseKey('KeyA'), ['KeyA']);
|
|
});
|
|
it('throws on empty key', () => {
|
|
assert.throws(() => {
|
|
parseKey('');
|
|
});
|
|
});
|
|
it('throws on invalid key', () => {
|
|
assert.throws(() => {
|
|
parseKey('aaaaa');
|
|
});
|
|
});
|
|
it('throws on multiple keys', () => {
|
|
assert.throws(() => {
|
|
parseKey('Shift+Shift');
|
|
});
|
|
});
|
|
|
|
it('processes press_key', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = context.getSelectedPptrPage();
|
|
await page.setContent(
|
|
html`<script>
|
|
logs = [];
|
|
document.addEventListener('keydown', e => logs.push('d' + e.key));
|
|
document.addEventListener('keyup', e => logs.push('u' + e.key));
|
|
</script>`,
|
|
);
|
|
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
|
|
context.getSelectedMcpPage(),
|
|
);
|
|
|
|
await pressKey.handler(
|
|
{
|
|
params: {
|
|
key: 'Control+Shift+C',
|
|
},
|
|
page: context.getSelectedMcpPage(),
|
|
},
|
|
response,
|
|
context,
|
|
);
|
|
|
|
assert.deepStrictEqual(await page.evaluate('logs'), [
|
|
'dControl',
|
|
'dShift',
|
|
'dC',
|
|
'uC',
|
|
'uShift',
|
|
'uControl',
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|