diff --git a/docs/api/puppeteer.keyboard.down.md b/docs/api/puppeteer.keyboard.down.md index cd972ca243879..c0b971e2b4367 100644 --- a/docs/api/puppeteer.keyboard.down.md +++ b/docs/api/puppeteer.keyboard.down.md @@ -10,7 +10,10 @@ Dispatches a `keydown` event. ```typescript class Keyboard { - down(key: KeyInput, options?: Readonly): Promise; + abstract down( + key: KeyInput, + options?: Readonly + ): Promise; } ``` diff --git a/docs/api/puppeteer.keyboard.md b/docs/api/puppeteer.keyboard.md index 44bebaf7f7a95..6f519c75daf4e 100644 --- a/docs/api/puppeteer.keyboard.md +++ b/docs/api/puppeteer.keyboard.md @@ -9,7 +9,7 @@ Keyboard provides an api for managing a virtual keyboard. The high level api is #### Signature: ```typescript -export declare class Keyboard +export declare abstract class Keyboard ``` ## Remarks diff --git a/docs/api/puppeteer.keyboard.press.md b/docs/api/puppeteer.keyboard.press.md index 8edb6342da980..2b0cd7e726cd5 100644 --- a/docs/api/puppeteer.keyboard.press.md +++ b/docs/api/puppeteer.keyboard.press.md @@ -10,7 +10,10 @@ Shortcut for [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()] ```typescript class Keyboard { - press(key: KeyInput, options?: Readonly): Promise; + abstract press( + key: KeyInput, + options?: Readonly + ): Promise; } ``` diff --git a/docs/api/puppeteer.keyboard.sendcharacter.md b/docs/api/puppeteer.keyboard.sendcharacter.md index eefeefce36e11..15f6de72e1d99 100644 --- a/docs/api/puppeteer.keyboard.sendcharacter.md +++ b/docs/api/puppeteer.keyboard.sendcharacter.md @@ -10,7 +10,7 @@ Dispatches a `keypress` and `input` event. This does not send a `keydown` or `ke ```typescript class Keyboard { - sendCharacter(char: string): Promise; + abstract sendCharacter(char: string): Promise; } ``` diff --git a/docs/api/puppeteer.keyboard.type.md b/docs/api/puppeteer.keyboard.type.md index b9a09d149ef47..026d5a4e94623 100644 --- a/docs/api/puppeteer.keyboard.type.md +++ b/docs/api/puppeteer.keyboard.type.md @@ -10,7 +10,10 @@ Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in t ```typescript class Keyboard { - type(text: string, options?: Readonly): Promise; + abstract type( + text: string, + options?: Readonly + ): Promise; } ``` diff --git a/docs/api/puppeteer.keyboard.up.md b/docs/api/puppeteer.keyboard.up.md index 053a4f41687d4..57518ee7a4887 100644 --- a/docs/api/puppeteer.keyboard.up.md +++ b/docs/api/puppeteer.keyboard.up.md @@ -10,7 +10,7 @@ Dispatches a `keyup` event. ```typescript class Keyboard { - up(key: KeyInput): Promise; + abstract up(key: KeyInput): Promise; } ``` diff --git a/packages/puppeteer-core/src/api/Input.ts b/packages/puppeteer-core/src/api/Input.ts index 35d2f5f5f8cec..8be06ba474b30 100644 --- a/packages/puppeteer-core/src/api/Input.ts +++ b/packages/puppeteer-core/src/api/Input.ts @@ -87,7 +87,7 @@ export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions; * * @public */ -export class Keyboard { +export abstract class Keyboard { /** * @internal */ @@ -120,10 +120,10 @@ export class Keyboard { * is the commands of keyboard shortcuts, * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names. */ - async down(key: KeyInput, options?: Readonly): Promise; - async down(): Promise { - throw new Error('Not implemented'); - } + abstract down( + key: KeyInput, + options?: Readonly + ): Promise; /** * Dispatches a `keyup` event. @@ -132,10 +132,7 @@ export class Keyboard { * See {@link KeyInput | KeyInput} * for a list of all key names. */ - async up(key: KeyInput): Promise; - async up(): Promise { - throw new Error('Not implemented'); - } + abstract up(key: KeyInput): Promise; /** * Dispatches a `keypress` and `input` event. @@ -153,10 +150,7 @@ export class Keyboard { * * @param char - Character to send into the page. */ - async sendCharacter(char: string): Promise; - async sendCharacter(): Promise { - throw new Error('Not implemented'); - } + abstract sendCharacter(char: string): Promise; /** * Sends a `keydown`, `keypress`/`input`, @@ -181,13 +175,10 @@ export class Keyboard { * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. * Defaults to 0. */ - async type( + abstract type( text: string, options?: Readonly ): Promise; - async type(): Promise { - throw new Error('Not implemented'); - } /** * Shortcut for {@link Keyboard.down} @@ -211,13 +202,10 @@ export class Keyboard { * is the commands of keyboard shortcuts, * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names. */ - async press( + abstract press( key: KeyInput, options?: Readonly ): Promise; - async press(): Promise { - throw new Error('Not implemented'); - } } /** diff --git a/packages/puppeteer-core/src/bidi/Input.ts b/packages/puppeteer-core/src/bidi/Input.ts index 7307384fbc0ec..d6a3d28d484eb 100644 --- a/packages/puppeteer-core/src/bidi/Input.ts +++ b/packages/puppeteer-core/src/bidi/Input.ts @@ -20,11 +20,11 @@ import {type Point} from '../api/ElementHandle.js'; import { Keyboard, Mouse, + MouseButton, Touchscreen, type KeyDownOptions, type KeyPressOptions, type KeyboardTypeOptions, - MouseButton, type MouseClickOptions, type MouseMoveOptions, type MouseOptions, @@ -33,6 +33,7 @@ import { import {type KeyInput} from '../common/USKeyboardLayout.js'; import {type BrowsingContext} from './BrowsingContext.js'; +import {type BidiPage} from './Page.js'; const enum InputId { Mouse = '__puppeteer_mouse', @@ -285,19 +286,19 @@ const getBidiKeyValue = (key: KeyInput) => { * @internal */ export class BidiKeyboard extends Keyboard { - #context: BrowsingContext; + #page: BidiPage; - constructor(context: BrowsingContext) { + constructor(page: BidiPage) { super(); - this.#context = context; + this.#page = page; } override async down( key: KeyInput, _options?: Readonly ): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, + await this.#page.connection.send('input.performActions', { + context: this.#page.mainFrame()._id, actions: [ { type: SourceActionsType.Key, @@ -314,8 +315,8 @@ export class BidiKeyboard extends Keyboard { } override async up(key: KeyInput): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, + await this.#page.connection.send('input.performActions', { + context: this.#page.mainFrame()._id, actions: [ { type: SourceActionsType.Key, @@ -352,8 +353,8 @@ export class BidiKeyboard extends Keyboard { type: ActionType.KeyUp, value: getBidiKeyValue(key), }); - await this.#context.connection.send('input.performActions', { - context: this.#context.id, + await this.#page.connection.send('input.performActions', { + context: this.#page.mainFrame()._id, actions: [ { type: SourceActionsType.Key, @@ -404,8 +405,8 @@ export class BidiKeyboard extends Keyboard { ); } } - await this.#context.connection.send('input.performActions', { - context: this.#context.id, + await this.#page.connection.send('input.performActions', { + context: this.#page.mainFrame()._id, actions: [ { type: SourceActionsType.Key, @@ -415,6 +416,17 @@ export class BidiKeyboard extends Keyboard { ], }); } + + override async sendCharacter(char: string): Promise { + // Measures the number of code points rather than UTF-16 code units. + if ([...char].length > 1) { + throw new Error('Cannot send more than 1 character.'); + } + const frame = await this.#page.focusedFrame(); + await frame.isolatedRealm().evaluate(async char => { + document.execCommand('insertText', false, char); + }, char); + } } /** diff --git a/packages/puppeteer-core/src/bidi/Page.ts b/packages/puppeteer-core/src/bidi/Page.ts index 0ca50ee432179..b0f7ad36dec7f 100644 --- a/packages/puppeteer-core/src/bidi/Page.ts +++ b/packages/puppeteer-core/src/bidi/Page.ts @@ -71,6 +71,7 @@ import { } from './BrowsingContext.js'; import {type BidiConnection} from './Connection.js'; import {BidiDialog} from './Dialog.js'; +import {BidiElementHandle} from './ElementHandle.js'; import {EmulationManager} from './EmulationManager.js'; import {BidiFrame, lifeCycleToReadinessState} from './Frame.js'; import {type BidiHTTPRequest} from './HTTPRequest.js'; @@ -201,7 +202,14 @@ export class BidiPage extends Page { this.#emulationManager = new EmulationManager(browsingContext); this.#mouse = new BidiMouse(this.mainFrame().context()); this.#touchscreen = new BidiTouchscreen(this.mainFrame().context()); - this.#keyboard = new BidiKeyboard(this.mainFrame().context()); + this.#keyboard = new BidiKeyboard(this); + } + + /** + * @internal + */ + get connection(): BidiConnection { + return this.#connection; } override async setUserAgent( @@ -282,6 +290,27 @@ export class BidiPage extends Page { return mainFrame; } + /** + * @internal + */ + async focusedFrame(): Promise { + using frame = await this.mainFrame() + .isolatedRealm() + .evaluateHandle(() => { + let frame: HTMLIFrameElement | undefined; + let win: Window | null = window; + while (win?.document.activeElement instanceof HTMLIFrameElement) { + frame = win.document.activeElement; + win = frame.contentWindow; + } + return frame; + }); + if (!(frame instanceof BidiElementHandle)) { + return this.mainFrame(); + } + return await frame.contentFrame(); + } + override frames(): BidiFrame[] { return Array.from(this.#frameTree.frames()); } diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 309836f1c6192..e96be62e8832c 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -648,10 +648,10 @@ "expectations": ["FAIL", "PASS"] }, { - "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter", + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], - "expectations": ["FAIL"] + "expectations": ["TIMEOUT"] }, { "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes", @@ -2069,6 +2069,12 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, { "testIdPattern": "[keyboard.spec] Keyboard should specify location", "platforms": ["darwin", "linux", "win32"], diff --git a/test/src/keyboard.spec.ts b/test/src/keyboard.spec.ts index e91f500faa884..71989baf30174 100644 --- a/test/src/keyboard.spec.ts +++ b/test/src/keyboard.spec.ts @@ -146,27 +146,101 @@ describe('Keyboard', function () { await page.goto(server.PREFIX + '/input/textarea.html'); await page.focus('textarea'); + + await page.evaluate(() => { + (globalThis as any).inputCount = 0; + (globalThis as any).keyDownCount = 0; + window.addEventListener( + 'input', + () => { + (globalThis as any).inputCount += 1; + }, + true + ); + window.addEventListener( + 'keydown', + () => { + (globalThis as any).keyDownCount += 1; + }, + true + ); + }); + await page.keyboard.sendCharacter('嗨'); expect( - await page.evaluate(() => { - return document.querySelector('textarea')!.value; + await page.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; }) - ).toBe('嗨'); - await page.evaluate(() => { - return window.addEventListener( + ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0}); + + await page.keyboard.sendCharacter('a'); + expect( + await page.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; + }) + ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0}); + }); + it('should send a character with sendCharacter in iframe', async () => { + this.timeout(2000); + + const {page} = await getTestState(); + + await page.setContent(` +