/** * @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 sinon from 'sinon'; 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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(); server.addHtmlRoute( '/link', html`Navigate page`, ); server.addRoute('/navigated', async (_req, res) => { await resolveNavigation.promise; res.write(html`
I was navigated
`); res.end(); }); await withMcpContext(async (response, context) => { const page = context.getSelectedMcpPage().pptrPage; 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(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`Navigate page`, ); server.addHtmlRoute('/after-click', html`
arrived
`); await withMcpContext(async (response, context) => { const page = context.getSelectedMcpPage().pptrPage; 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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` `, ); await withMcpContext(async (response, context) => { const page = context.getSelectedMcpPage().pptrPage; 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
custom two
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent(html``); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent(html`
switch
`); 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.getSelectedMcpPage().pptrPage; await page.setContent(html` `); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
drag me
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html`
`, ); 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.getSelectedMcpPage().pptrPage; await page.setContent( html` `, ); 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.getSelectedMcpPage().pptrPage; await page.setContent(html`
Not a file input
`); 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.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); 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', ]); }); }); it('releases held modifiers when the main key press fails', async () => { await withMcpContext(async (response, context) => { const page = context.getSelectedMcpPage().pptrPage; await page.setContent( html``, ); context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create( context.getSelectedMcpPage(), ); // Simulate the main key press failing mid-sequence (e.g. a CDP // hiccup) after the modifiers have already been pressed down. sinon .stub(page.keyboard, 'press') .throws(new Error('injected press failure')); try { await assert.rejects( pressKey.handler( { params: { key: 'Control+Shift+C', }, page: context.getSelectedMcpPage(), }, response, context, ), ); } finally { sinon.restore(); } // The modifiers were pressed down; both must be released even though // the main key press threw, otherwise the browser is left with the // modifiers logically stuck down. assert.deepStrictEqual(await page.evaluate('logs'), [ 'dControl', 'dShift', 'uShift', 'uControl', ]); }); }); }); });