Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 10 additions & 0 deletions docs/src/api/class-keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ 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`]). Use `{{` and `}}` to type literal brace characters.
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.

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

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.


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

**Usage**
Expand Down Expand Up @@ -359,6 +361,14 @@ 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 `true`, anything inside `{}` in the text is treated as a key name (same format as [`method: Keyboard.press`]),
allowing you to mix regular characters with special keys like `{Enter}`, `{ArrowDown}`, or modifier combos like `{Control+A}`.
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.

Having an example that contains both text and special keys would be nice to explain the feature.

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.

Use `{{` and `}}` to type literal brace characters. Defaults to `false`.

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

Expand Down
29 changes: 28 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`]). Use `{{` and `}}` to type literal brace characters.
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.

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

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.


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,22 @@ 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`]). Use `{{` and `}}` to type literal brace characters.
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.

Suggested change
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`]). Use `{{` and `}}` to type literal brace characters.
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`]).

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.


### 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 `true`, anything inside `{}` in the text is treated as a key name (same format as [`method: 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`.

### 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
68 changes: 59 additions & 9 deletions packages/playwright-core/src/server/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,38 @@ 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 {
if (delay)
await progress.wait(delay);
await this.insertText(progress, char);
if (options?.namedKeys) {
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.

We can combine both branches by making parseNamedKeys return all "chars" when namedKeys option is not passed. This should simplify the code quite a bit.

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.

for (const token of parseNamedKeys(text)) {
if (token.type === 'key') {
if (delay)
await progress.wait(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.

Perhaps just pass delay to this.press() on the next line?

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.

await this.press(progress, token.value, { delay: undefined });
} 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);
}
}
}
} else {
for (const char of text) {
if (usKeyboardLayout.has(char)) {
await this.press(progress, char, { delay });
} else {
if (delay)
await progress.wait(delay);
await this.insertText(progress, char);
}
}
}
}
Expand Down Expand Up @@ -354,6 +372,38 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
return result;
}

function* parseNamedKeys(text: string): Generator<{ type: 'key' | 'char', value: string }> {
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.

Can we avoid a generator here? It's not like it helps with performance or something.

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.

let i = 0;
while (i < text.length) {
if (text[i] === '{') {
if (i + 1 < text.length && text[i + 1] === '{') {
yield { type: 'char', value: '{' };
i += 2;
} else {
const end = text.indexOf('}', i + 1);
if (end === -1) {
yield { type: 'char', value: '{' };
i += 1;
} else {
yield { type: 'key', value: text.substring(i + 1, end) };
i = end + 1;
}
}
} else if (text[i] === '}') {
if (i + 1 < text.length && text[i + 1] === '}') {
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.

It seems like there is no point to escape } as }} - these both yield the same result. Let's drop that and update docs to only mention {{ for escaping {.

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.

yield { type: 'char', value: '}' };
i += 2;
} else {
yield { type: 'char', value: '}' };
i += 1;
}
} else {
yield { type: 'char', value: text[i] };
i += 1;
}
}
}

export interface RawTouchscreen {
tap(progress: Progress, x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
}
Expand Down
37 changes: 36 additions & 1 deletion packages/playwright-core/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
1 change: 1 addition & 0 deletions packages/protocol/spec/frame.yml
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ Frame:
strict: boolean?
text: string
delay: float?
namedKeys: boolean?
timeout: float
flags:
slowMo: true
Expand Down
Loading
Loading