Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(keyboard): add array overload to pressSequentially by SebTardif · Pull Request #40748 · microsoft/playwright · GitHub
Skip to content

feat(keyboard): add array overload to pressSequentially - #40748

Merged
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array
May 18, 2026
Merged

feat(keyboard): add array overload to pressSequentially#40748
Dmitry Gozman (dgozman) merged 5 commits into
microsoft:mainfrom
SebTardif:feat/press-sequentially-array

Conversation

@SebTardif

@SebTardifSebastien Tardif (SebTardif) commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds namedKeys option to Locator.pressSequentially
awaitlocator.pressSequentially('Hello{Enter}World!',{namedKeys: true});

Fixes#40740

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-frame.md Outdated
### option: Frame.press.timeout = %%-input-timeout-js-%%
* since: v1.8

## async method: Frame.pressSequentially

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.

Let's not introduce a discouraged method 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, removed.

Comment threaddocs/src/api/class-locator.md Outdated
- `text` <[string]|[Array]<[string]>>

String of characters to sequentially press into a focused element.
String of characters to sequentially press into a focused element, or an array of key names to press sequentially. Key names follow the same format as [`method: Locator.press`].

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.

How about we support the following notation: Hello{ArrowRight}{Ctrl+A}world!{Backspace}? Basically, treating anything inside {} as a key from the press method, when passed a namedKeys: true option.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Reworked. Added namedKeys: true option to Locator.pressSequentially and Keyboard.type. {KeyName} treats brace content as a key name for press(), with {{/}} for literal braces.

awaitlocator.pressSequentially('Hello{Enter}World',{namedKeys: true});awaitlocator.pressSequentially('{Control+A}{Delete}Hello',{namedKeys: true});awaitlocator.pressSequentially('type {{braces}}',{namedKeys: true});// types: type {braces}

Add string[] overload to Locator.pressSequentially and new
Keyboard.pressSequentially method, as requested in microsoft#40740.
When given an array, each element is treated as a key name
(same format as keyboard.press()) and pressed sequentially
with optional delay between presses.
Fixesmicrosoft#40740
Add missing documentation for Frame.pressSequentially selector-based
method to fix doclint CI failure.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Rework pressSequentially based on review feedback:
- Drop Frame.pressSequentially (no new discouraged methods)
- Drop Keyboard.pressSequentially and keyboardPressSequentially protocol
- Add namedKeys option to Locator.pressSequentially and Keyboard.type
- {KeyName} syntax treats braces content as key names for press()
- {{ and }} escape to literal brace characters
Fixes: microsoft#40740
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment threaddocs/src/api/class-keyboard.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated
- `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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-locator.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threaddocs/src/api/class-keyboard.md Outdated

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello {World}');
});

it('should type with namedKeys and delay', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

Comment threadtests/page/page-keyboard.spec.ts Outdated
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello\nWorld');
});

it('should type with namedKeys and modifier combos', async ({ page }) => {

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.

Let's remove this test.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

}
}
} 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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

- Combine namedKeys/non-namedKeys branches into single code path
- Pass delay to press() for named keys
- Replace generator with regular function returning array
- Drop redundant }} escaping, keep only {{ for literal brace
- Simplify docs wording per reviewer suggestions
- Add mixed text+key example to Keyboard.type docs
- Trim tests to two meaningful cases using textarea
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Almost there!

Comment threadtests/page/page-keyboard.spec.ts Outdated
document.body.appendChild(textarea);
textarea.focus();
});
await page.keyboard.type('Hello{Enter}World', { namedKeys: true });

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.

Let's type the following: He{{ll}o{Enter}Wor{ld and see that it produces He{ll}o\nWor{ld

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. Updated the test to type He{{ll}o{Enter}Wor{ld and verify it produces He{ll}o\nWor{ld, covering escaped braces, unmatched braces, and named keys.

} 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
ContributorAuthor

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.

- Remove extraneous progress.wait(delay) before this.press() in the
named key branch, matching the original behavior for layout characters
where press() handles delay internally.
- Update namedKeys test to use edge-case input as suggested: escaped
braces ({{), unmatched }, named key ({Enter}), and unmatched {.
- Regenerate types to sync with doc wording updates.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chromium] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-chromium
❌ [chromium] › mcp/annotate.spec.ts:137 › should abort MCP annotation when last screenshot is removed @mcp-windows-latest-chromium

7097 passed, 1104 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-page] › page/page-request-continue.spec.ts:756 › propagate headers cross origin redirect after interception `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-node22`

41925 passed, 850 skipped


Merge workflow run.

@dgozmanDmitry Gozman (dgozman) left a comment

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.

Looks great, merging in. Thank you for the PR!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: keyboard.pressSequence() for batched named key presses

3 participants

@SebTardif@pavelfeldman@dgozman