chore: implement BiDi sendCharacter (#11000)

This commit is contained in:
jrandolf 2023-09-22 08:22:25 -07:00 committed by GitHub
parent 242004b950
commit c3bd8eb878
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 172 additions and 54 deletions

View File

@ -10,7 +10,10 @@ Dispatches a `keydown` event.
```typescript ```typescript
class Keyboard { class Keyboard {
down(key: KeyInput, options?: Readonly<KeyDownOptions>): Promise<void>; abstract down(
key: KeyInput,
options?: Readonly<KeyDownOptions>
): Promise<void>;
} }
``` ```

View File

@ -9,7 +9,7 @@ Keyboard provides an api for managing a virtual keyboard. The high level api is
#### Signature: #### Signature:
```typescript ```typescript
export declare class Keyboard export declare abstract class Keyboard
``` ```
## Remarks ## Remarks

View File

@ -10,7 +10,10 @@ Shortcut for [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()]
```typescript ```typescript
class Keyboard { class Keyboard {
press(key: KeyInput, options?: Readonly<KeyPressOptions>): Promise<void>; abstract press(
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void>;
} }
``` ```

View File

@ -10,7 +10,7 @@ Dispatches a `keypress` and `input` event. This does not send a `keydown` or `ke
```typescript ```typescript
class Keyboard { class Keyboard {
sendCharacter(char: string): Promise<void>; abstract sendCharacter(char: string): Promise<void>;
} }
``` ```

View File

@ -10,7 +10,10 @@ Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in t
```typescript ```typescript
class Keyboard { class Keyboard {
type(text: string, options?: Readonly<KeyboardTypeOptions>): Promise<void>; abstract type(
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
} }
``` ```

View File

@ -10,7 +10,7 @@ Dispatches a `keyup` event.
```typescript ```typescript
class Keyboard { class Keyboard {
up(key: KeyInput): Promise<void>; abstract up(key: KeyInput): Promise<void>;
} }
``` ```

View File

@ -87,7 +87,7 @@ export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions;
* *
* @public * @public
*/ */
export class Keyboard { export abstract class Keyboard {
/** /**
* @internal * @internal
*/ */
@ -120,10 +120,10 @@ export class Keyboard {
* is the commands of keyboard shortcuts, * 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. * 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<KeyDownOptions>): Promise<void>; abstract down(
async down(): Promise<void> { key: KeyInput,
throw new Error('Not implemented'); options?: Readonly<KeyDownOptions>
} ): Promise<void>;
/** /**
* Dispatches a `keyup` event. * Dispatches a `keyup` event.
@ -132,10 +132,7 @@ export class Keyboard {
* See {@link KeyInput | KeyInput} * See {@link KeyInput | KeyInput}
* for a list of all key names. * for a list of all key names.
*/ */
async up(key: KeyInput): Promise<void>; abstract up(key: KeyInput): Promise<void>;
async up(): Promise<void> {
throw new Error('Not implemented');
}
/** /**
* Dispatches a `keypress` and `input` event. * Dispatches a `keypress` and `input` event.
@ -153,10 +150,7 @@ export class Keyboard {
* *
* @param char - Character to send into the page. * @param char - Character to send into the page.
*/ */
async sendCharacter(char: string): Promise<void>; abstract sendCharacter(char: string): Promise<void>;
async sendCharacter(): Promise<void> {
throw new Error('Not implemented');
}
/** /**
* Sends a `keydown`, `keypress`/`input`, * 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. * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
* Defaults to 0. * Defaults to 0.
*/ */
async type( abstract type(
text: string, text: string,
options?: Readonly<KeyboardTypeOptions> options?: Readonly<KeyboardTypeOptions>
): Promise<void>; ): Promise<void>;
async type(): Promise<void> {
throw new Error('Not implemented');
}
/** /**
* Shortcut for {@link Keyboard.down} * Shortcut for {@link Keyboard.down}
@ -211,13 +202,10 @@ export class Keyboard {
* is the commands of keyboard shortcuts, * 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. * 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, key: KeyInput,
options?: Readonly<KeyPressOptions> options?: Readonly<KeyPressOptions>
): Promise<void>; ): Promise<void>;
async press(): Promise<void> {
throw new Error('Not implemented');
}
} }
/** /**

View File

@ -20,11 +20,11 @@ import {type Point} from '../api/ElementHandle.js';
import { import {
Keyboard, Keyboard,
Mouse, Mouse,
MouseButton,
Touchscreen, Touchscreen,
type KeyDownOptions, type KeyDownOptions,
type KeyPressOptions, type KeyPressOptions,
type KeyboardTypeOptions, type KeyboardTypeOptions,
MouseButton,
type MouseClickOptions, type MouseClickOptions,
type MouseMoveOptions, type MouseMoveOptions,
type MouseOptions, type MouseOptions,
@ -33,6 +33,7 @@ import {
import {type KeyInput} from '../common/USKeyboardLayout.js'; import {type KeyInput} from '../common/USKeyboardLayout.js';
import {type BrowsingContext} from './BrowsingContext.js'; import {type BrowsingContext} from './BrowsingContext.js';
import {type BidiPage} from './Page.js';
const enum InputId { const enum InputId {
Mouse = '__puppeteer_mouse', Mouse = '__puppeteer_mouse',
@ -285,19 +286,19 @@ const getBidiKeyValue = (key: KeyInput) => {
* @internal * @internal
*/ */
export class BidiKeyboard extends Keyboard { export class BidiKeyboard extends Keyboard {
#context: BrowsingContext; #page: BidiPage;
constructor(context: BrowsingContext) { constructor(page: BidiPage) {
super(); super();
this.#context = context; this.#page = page;
} }
override async down( override async down(
key: KeyInput, key: KeyInput,
_options?: Readonly<KeyDownOptions> _options?: Readonly<KeyDownOptions>
): Promise<void> { ): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.connection.send('input.performActions', {
context: this.#context.id, context: this.#page.mainFrame()._id,
actions: [ actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
@ -314,8 +315,8 @@ export class BidiKeyboard extends Keyboard {
} }
override async up(key: KeyInput): Promise<void> { override async up(key: KeyInput): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.connection.send('input.performActions', {
context: this.#context.id, context: this.#page.mainFrame()._id,
actions: [ actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
@ -352,8 +353,8 @@ export class BidiKeyboard extends Keyboard {
type: ActionType.KeyUp, type: ActionType.KeyUp,
value: getBidiKeyValue(key), value: getBidiKeyValue(key),
}); });
await this.#context.connection.send('input.performActions', { await this.#page.connection.send('input.performActions', {
context: this.#context.id, context: this.#page.mainFrame()._id,
actions: [ actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
@ -404,8 +405,8 @@ export class BidiKeyboard extends Keyboard {
); );
} }
} }
await this.#context.connection.send('input.performActions', { await this.#page.connection.send('input.performActions', {
context: this.#context.id, context: this.#page.mainFrame()._id,
actions: [ actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
@ -415,6 +416,17 @@ export class BidiKeyboard extends Keyboard {
], ],
}); });
} }
override async sendCharacter(char: string): Promise<void> {
// 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);
}
} }
/** /**

View File

@ -71,6 +71,7 @@ import {
} from './BrowsingContext.js'; } from './BrowsingContext.js';
import {type BidiConnection} from './Connection.js'; import {type BidiConnection} from './Connection.js';
import {BidiDialog} from './Dialog.js'; import {BidiDialog} from './Dialog.js';
import {BidiElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {BidiFrame, lifeCycleToReadinessState} from './Frame.js'; import {BidiFrame, lifeCycleToReadinessState} from './Frame.js';
import {type BidiHTTPRequest} from './HTTPRequest.js'; import {type BidiHTTPRequest} from './HTTPRequest.js';
@ -201,7 +202,14 @@ export class BidiPage extends Page {
this.#emulationManager = new EmulationManager(browsingContext); this.#emulationManager = new EmulationManager(browsingContext);
this.#mouse = new BidiMouse(this.mainFrame().context()); this.#mouse = new BidiMouse(this.mainFrame().context());
this.#touchscreen = new BidiTouchscreen(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( override async setUserAgent(
@ -282,6 +290,27 @@ export class BidiPage extends Page {
return mainFrame; return mainFrame;
} }
/**
* @internal
*/
async focusedFrame(): Promise<BidiFrame> {
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[] { override frames(): BidiFrame[] {
return Array.from(this.#frameTree.frames()); return Array.from(this.#frameTree.frames());
} }

View File

@ -648,10 +648,10 @@
"expectations": ["FAIL", "PASS"] "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["TIMEOUT"]
}, },
{ {
"testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes", "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes",
@ -2069,6 +2069,12 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "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", "testIdPattern": "[keyboard.spec] Keyboard should specify location",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -146,27 +146,101 @@ describe('Keyboard', function () {
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea'); await page.focus('textarea');
await page.keyboard.sendCharacter('嗨');
expect(
await page.evaluate(() => {
return document.querySelector('textarea')!.value;
})
).toBe('嗨');
await page.evaluate(() => { await page.evaluate(() => {
return window.addEventListener( (globalThis as any).inputCount = 0;
(globalThis as any).keyDownCount = 0;
window.addEventListener(
'input',
() => {
(globalThis as any).inputCount += 1;
},
true
);
window.addEventListener(
'keydown', 'keydown',
e => { () => {
return e.preventDefault(); (globalThis as any).keyDownCount += 1;
}, },
true true
); );
}); });
await page.keyboard.sendCharacter('嗨');
expect(
await page.$eval('textarea', textarea => {
return {
value: textarea.value,
inputs: (globalThis as any).inputCount,
keyDowns: (globalThis as any).keyDownCount,
};
})
).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
await page.keyboard.sendCharacter('a'); await page.keyboard.sendCharacter('a');
expect( expect(
await page.evaluate(() => { await page.$eval('textarea', textarea => {
return document.querySelector('textarea')!.value; return {
value: textarea.value,
inputs: (globalThis as any).inputCount,
keyDowns: (globalThis as any).keyDownCount,
};
}) })
).toBe('嗨a'); ).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(`
<iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
`);
const frame = await page.waitForFrame(frame => {
return frame.name() === 'test';
});
await frame.focus('textarea');
await frame.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 frame.$eval('textarea', textarea => {
return {
value: textarea.value,
inputs: (globalThis as any).inputCount,
keyDowns: (globalThis as any).keyDownCount,
};
})
).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
await page.keyboard.sendCharacter('a');
expect(
await frame.$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 report shiftKey', async () => { it('should report shiftKey', async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();