chore: implement BiDi sendCharacter (#11000)
This commit is contained in:
parent
242004b950
commit
c3bd8eb878
@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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"],
|
||||||
|
@ -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(() => {
|
await page.evaluate(() => {
|
||||||
return document.querySelector('textarea')!.value;
|
(globalThis as any).inputCount = 0;
|
||||||
})
|
(globalThis as any).keyDownCount = 0;
|
||||||
).toBe('嗨');
|
window.addEventListener(
|
||||||
await page.evaluate(() => {
|
'input',
|
||||||
return window.addEventListener(
|
() => {
|
||||||
|
(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();
|
||||||
|
Loading…
Reference in New Issue
Block a user