Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/src/api/class-keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,35 +308,52 @@ In most cases, you should use [`method: Locator.fill`] instead. You only need to

Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Keyboard.press`]).

To press a special key, like `Control` or `ArrowDown`, use [`method: Keyboard.press`].

**Usage**

```js
await page.keyboard.type('Hello'); // Types instantly
await page.keyboard.type('World', { delay: 100 }); // Types slower, like a user

// Mix text and special keys
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });
```

```java
// Types instantly
page.keyboard().type("Hello");
// Types slower, like a user
page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100));

// Mix text and special keys
page.keyboard().type("Hello{Enter}World", new Keyboard.TypeOptions().setNamedKeys(true));
```

```python async
await page.keyboard.type("Hello") # types instantly
await page.keyboard.type("World", delay=100) # types slower, like a user

# Mix text and special keys
await page.keyboard.type("Hello{Enter}World", named_keys=True)
```

```python sync
page.keyboard.type("Hello") # types instantly
page.keyboard.type("World", delay=100) # types slower, like a user

# Mix text and special keys
page.keyboard.type("Hello{Enter}World", named_keys=True)
```

```csharp
await page.Keyboard.TypeAsync("Hello"); // types instantly
await page.Keyboard.TypeAsync("World", new() { Delay = 100 }); // types slower, like a user

// Mix text and special keys
await page.Keyboard.TypeAsync("Hello{Enter}World", new() { NamedKeys = true });
```

:::note
Expand All @@ -359,6 +376,13 @@ A text to type into a focused element.

Time to wait between key presses in milliseconds. Defaults to 0.

### option: Keyboard.type.namedKeys
* since: v1.61
- `namedKeys` <[boolean]>

When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Keyboard.press`]).
Use `{{` to type a literal brace character. Defaults to `false`.

## async method: Keyboard.up
* since: v1.8

Expand Down
28 changes: 27 additions & 1 deletion docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2103,33 +2103,52 @@ In most cases, you should use [`method: Locator.fill`] instead. You only need to

Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]).

To press a special key, like `Control` or `ArrowDown`, use [`method: Locator.press`].

**Usage**

```js
await locator.pressSequentially('Hello'); // Types instantly
await locator.pressSequentially('World', { delay: 100 }); // Types slower, like a user

// Mix characters and named keys
await locator.pressSequentially('Hello{Enter}World', { namedKeys: true });
// Use modifier combos
await locator.pressSequentially('{Control+A}{Delete}Hello', { namedKeys: true });
```

```java
locator.pressSequentially("Hello"); // Types instantly
locator.pressSequentially("World", new Locator.pressSequentiallyOptions().setDelay(100)); // Types slower, like a user

// Mix characters and named keys
locator.pressSequentially("Hello{Enter}World", new Locator.pressSequentiallyOptions().setNamedKeys(true));
```

```python async
await locator.press_sequentially("hello") # types instantly
await locator.press_sequentially("world", delay=100) # types slower, like a user

# Mix characters and named keys
await locator.press_sequentially("Hello{Enter}World", named_keys=True)
```

```python sync
locator.press_sequentially("hello") # types instantly
locator.press_sequentially("world", delay=100) # types slower, like a user

# Mix characters and named keys
locator.press_sequentially("Hello{Enter}World", named_keys=True)
```

```csharp
await locator.PressSequentiallyAsync("Hello"); // Types instantly
await locator.PressSequentiallyAsync("World", new() { Delay = 100 }); // Types slower, like a user

// Mix characters and named keys
await locator.PressSequentiallyAsync("Hello{Enter}World", new() { NamedKeys = true });
```

An example of typing into a text field and then submitting the form:
Expand Down Expand Up @@ -2168,14 +2187,21 @@ await locator.PressAsync("Enter");
* since: v1.38
- `text` <[string]>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element. When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]).

### option: Locator.pressSequentially.delay
* since: v1.38
- `delay` <[float]>

Time to wait between key presses in milliseconds. Defaults to 0.

### option: Locator.pressSequentially.namedKeys
* since: v1.61
- `namedKeys` <[boolean]>

When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]).
Use `{{` to type a literal brace character. Defaults to `false`.

### option: Locator.pressSequentially.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.38

Expand Down
37 changes: 36 additions & 1 deletion packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14701,6 +14701,11 @@ export interface Locator {
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the
* text.
*
* When [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is
* `true`, anything inside `{}` is treated as a key name (same format as
* [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). Use `{{` and `}}` to
* type literal brace characters.
*
* To press a special key, like `Control` or `ArrowDown`, use
* [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press).
*
Expand All @@ -14709,6 +14714,11 @@ export interface Locator {
* ```js
* await locator.pressSequentially('Hello'); // Types instantly
* await locator.pressSequentially('World', { delay: 100 }); // Types slower, like a user
*
* // Mix characters and named keys
* await locator.pressSequentially('Hello{Enter}World', { namedKeys: true });
* // Use modifier combos
* await locator.pressSequentially('{Control+A}{Delete}Hello', { namedKeys: true });
* ```
*
* An example of typing into a text field and then submitting the form:
Expand All @@ -14719,7 +14729,11 @@ export interface Locator {
* await locator.press('Enter');
* ```
*
* @param text String of characters to sequentially press into a focused element.
* @param text String of characters to sequentially press into a focused element. When
* [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is
* `true`, anything inside `{}` is treated as a key name (same format as
* [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). Use `{{` and `}}` to
* type literal brace characters.
* @param options
*/
pressSequentially(text: string, options?: {
Expand All @@ -14728,6 +14742,14 @@ export interface Locator {
*/
delay?: number;

/**
* When `true`, anything inside `{}` in the text is treated as a key name (same format as
* [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)), allowing you to mix
* regular characters with special keys like `{Enter}`, `{ArrowDown}`, or modifier combos like `{Control+A}`. Use `{{`
* and `}}` to type literal brace characters. Defaults to `false`.
*/
namedKeys?: boolean;

/**
* This option has no effect.
* @deprecated This option has no effect.
Expand Down Expand Up @@ -20174,6 +20196,11 @@ export interface Keyboard {
*
* Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.
*
* When [`namedKeys`](https://playwright.dev/docs/api/class-keyboard#keyboard-type-option-named-keys) is `true`,
* anything inside `{}` is treated as a key name (same format as
* [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)). Use `{{` and `}}`
* to type literal brace characters.
*
* To press a special key, like `Control` or `ArrowDown`, use
* [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press).
*
Expand All @@ -20196,6 +20223,14 @@ export interface Keyboard {
* Time to wait between key presses in milliseconds. Defaults to 0.
*/
delay?: number;

/**
* When `true`, anything inside `{}` in the text is treated as a key name (same format as
* [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)), allowing you to
* mix regular characters with special keys like `{Enter}`, `{ArrowDown}`, or modifier combos like `{Control+A}`. Use
* `{{` and `}}` to type literal brace characters. Defaults to `false`.
*/
namedKeys?: boolean;
}): Promise<void>;

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,7 @@ scheme.FrameTypeParams = tObject({
strict: tOptional(tBoolean),
text: tString,
delay: tOptional(tFloat),
namedKeys: tOptional(tBoolean),
timeout: tFloat,
});
scheme.FrameTypeResult = tOptional(tObject({}));
Expand Down Expand Up @@ -2454,6 +2455,7 @@ scheme.PageKeyboardInsertTextResult = tOptional(tObject({}));
scheme.PageKeyboardTypeParams = tObject({
text: tString,
delay: tOptional(tFloat),
namedKeys: tOptional(tBoolean),
});
scheme.PageKeyboardTypeResult = tOptional(tObject({}));
scheme.PageKeyboardPressParams = tObject({
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,13 +772,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return await progress.race(this.evaluateInUtility(([injected, node]) => injected.blurNode(node), {}));
}

async type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<void> {
async type(progress: Progress, text: string, options: { delay?: number, namedKeys?: boolean } & types.StrictOptions): Promise<void> {
await this._markAsTargetElement(progress);
const result = await this._type(progress, text, options);
return assertDone(throwRetargetableDOMError(result));
}

async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
async _type(progress: Progress, text: string, options: { delay?: number, namedKeys?: boolean } & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
progress.log(`elementHandle.type("${text}")`);
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1430,7 +1430,7 @@ export class Frame extends SdkObject<FrameEventMap> {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._drop(progress, inputFileItems, data, options)));
}

async type(progress: Progress, selector: string, text: string, options: { delay?: number, noAutoWaiting?: boolean } & types.StrictOptions) {
async type(progress: Progress, selector: string, text: string, options: { delay?: number, namedKeys?: boolean, noAutoWaiting?: boolean } & types.StrictOptions) {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._type(progress, text, options)));
}

Expand Down
48 changes: 41 additions & 7 deletions packages/playwright-core/src/server/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,26 @@ export class Keyboard {
await this._raw.sendText(progress, text);
}

async apiType(progress: Progress, text: string, options?: { delay?: number }) {
async apiType(progress: Progress, text: string, options?: { delay?: number, namedKeys?: boolean }) {
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
await this.type(progress, text, options);
}

async type(progress: Progress, text: string, options?: { delay?: number }) {
async type(progress: Progress, text: string, options?: { delay?: number, namedKeys?: boolean }) {
const delay = (options && options.delay) || undefined;
for (const char of text) {
if (usKeyboardLayout.has(char)) {
await this.press(progress, char, { delay });
} else {
for (const token of parseNamedKeys(text, !!options?.namedKeys)) {
if (token.type === 'key') {
if (delay)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an extra delay here when we pass it to this.press() in the next line? Looks different from how it was before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed the extra progress.wait(delay) before this.press(). Now the named key branch matches the original behavior for layout characters, where press() handles delay internally.

await progress.wait(delay);
await this.insertText(progress, char);
await this.press(progress, token.value, { delay });
} else {
if (usKeyboardLayout.has(token.value)) {
await this.press(progress, token.value, { delay });
} else {
if (delay)
await progress.wait(delay);
await this.insertText(progress, token.value);
}
}
}
}
Expand Down Expand Up @@ -354,6 +360,34 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
return result;
}

function parseNamedKeys(text: string, namedKeys: boolean): Array<{ type: 'key' | 'char', value: string }> {
if (!namedKeys)
return [...text].map(value => ({ type: 'char' as const, value }));
const result: Array<{ type: 'key' | 'char', value: string }> = [];
let i = 0;
while (i < text.length) {
if (text[i] === '{') {
if (i + 1 < text.length && text[i + 1] === '{') {
result.push({ type: 'char', value: '{' });
i += 2;
} else {
const end = text.indexOf('}', i + 1);
if (end === -1) {
result.push({ type: 'char', value: '{' });
i += 1;
} else {
result.push({ type: 'key', value: text.substring(i + 1, end) });
i = end + 1;
}
}
} else {
result.push({ type: 'char', value: text[i] });
i += 1;
}
}
return result;
}

export interface RawTouchscreen {
tap(progress: Progress, x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
}
Expand Down
Loading
Loading