Skip to content

Commit 2db3021

Browse files
DavertMikclaude
andcommitted
fix(CDPBrowser): deterministic label focus forwarding and per-char key event typing
Clicking a <label> via the synthetic click path now explicitly focuses its associated control (htmlFor lookup, else the first focusable descendant) after el.click(), mirroring native label-activation behavior that Obscura doesn't implement. This makes focus deterministic after I.click('Name')-style label clicks and fixes the flakiness previously worked around with skips. type() now dispatches a real keydown/keypress/input/keyup sequence per character instead of setting el.value directly, and mutates textContent for contenteditable hosts instead of a non-existent .value — closing a review finding that the old implementation bypassed both real key events and the contenteditable path. Delay pacing is unchanged (one client call per character, real setTimeout between them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3d4c76d commit 2db3021

4 files changed

Lines changed: 27 additions & 12 deletions

File tree

docs/helpers/CDPBrowser.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1604,7 +1604,11 @@ Returns **void**&#x20;
16041604

16051605
### type
16061606

1607-
Types characters into the currently focused element (as set by `click`, `focus`, etc).
1607+
Types characters into the currently focused element (as set by `click`, `focus`, etc), one at a
1608+
time. Each character dispatches a real `keydown``keypress` → (value mutated) → `input`
1609+
`keyup` sequence, and mutates a `contenteditable` host's `textContent` instead of `.value`, so
1610+
this works on rich-text/contenteditable targets as well as `input`/`textarea`. Mirrors
1611+
Puppeteer's `type(text, options)` semantics.
16081612

16091613
```js
16101614
I.click('Name');

lib/helper/CDPBrowser.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,7 +1968,11 @@ class CDPBrowser extends Helper {
19681968
}
19691969

19701970
/**
1971-
* Types characters into the currently focused element (as set by `click`, `focus`, etc).
1971+
* Types characters into the currently focused element (as set by `click`, `focus`, etc), one at a
1972+
* time. Each character dispatches a real `keydown` → `keypress` → (value mutated) → `input` →
1973+
* `keyup` sequence, and mutates a `contenteditable` host's `textContent` instead of `.value`, so
1974+
* this works on rich-text/contenteditable targets as well as `input`/`textarea`. Mirrors
1975+
* Puppeteer's `type(text, options)` semantics.
19721976
*
19731977
* ```js
19741978
* I.click('Name');
@@ -1987,9 +1991,15 @@ class CDPBrowser extends Helper {
19871991
const ok = await this._evaluate(`(function(){
19881992
var el = document.activeElement
19891993
if (!el) return false
1990-
el.value = (el.value || '') + ${JSON.stringify(key)}
1994+
var key = ${JSON.stringify(key)}
1995+
var opts = { key: key, bubbles: true, cancelable: true }
1996+
el.dispatchEvent(new KeyboardEvent('keydown', opts))
1997+
el.dispatchEvent(new KeyboardEvent('keypress', opts))
1998+
var isEditable = el.isContentEditable === true || el.getAttribute('contenteditable') === 'true'
1999+
if (isEditable) el.textContent = (el.textContent || '') + key
2000+
else el.value = (el.value || '') + key
19912001
el.dispatchEvent(new Event('input', { bubbles: true }))
1992-
el.dispatchEvent(new Event('change', { bubbles: true }))
2002+
el.dispatchEvent(new KeyboardEvent('keyup', opts))
19932003
return true
19942004
})()`)
19952005
if (!ok) throw new Error('No element is in focus. Use click or focus to set the active element before typing.')

lib/helper/clientscripts/cdpBrowserClient.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ export default function installCodeceptClient() {
159159
}
160160
const isAriaCheckable = el => el.getAttribute && el.getAttribute('aria-checked') != null
161161
const isChecked = el => (isAriaCheckable(el) ? el.getAttribute('aria-checked') === 'true' : el.checked === true)
162+
const focusLabelledControl = label => {
163+
let control = null
164+
if (label.htmlFor) control = document.getElementById(label.htmlFor)
165+
if (!control) control = label.querySelector('input, select, textarea, button, [contenteditable="true"]')
166+
if (control && !control.disabled && control.focus) control.focus()
167+
}
162168
const setChecked = (el, value) => {
163169
if (isAriaCheckable(el)) {
164170
if (isChecked(el) !== value) el.click()
@@ -231,7 +237,9 @@ export default function installCodeceptClient() {
231237
return { x: r.x, y: r.y, width: r.width, height: r.height }
232238
},
233239
click: els => {
234-
els[0].click()
240+
const el = els[0]
241+
el.click()
242+
if (el.tagName === 'LABEL') focusLabelledControl(el)
235243
return true
236244
},
237245
fill: (els, p) => {

test/helper/webapi.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,6 @@ export function tests() {
10191019
describe('#type', () => {
10201020
beforeEach(function () {
10211021
if (I.capabilities?.layout === 'none') this.skip() // clicking does not move real focus without a layout engine, so there is no activeElement to type into
1022-
if (isHelper('Obscura')) this.skip() // clicking a <label> to focus its associated field is non-deterministic on Obscura (verified: document.activeElement sometimes moves to the field, sometimes stays <body>, across otherwise-identical repeated runs), so typing has no reliable target
10231022
})
10241023

10251024
it('should type into a field', async () => {
@@ -2582,11 +2581,6 @@ export function tests() {
25822581
})
25832582

25842583
it('should not throw NonFocusedType when element is focused', async function () {
2585-
// I.click('Name') clicks the <label for="name"> to forward focus to its associated input —
2586-
// already-documented as non-deterministic on Obscura (document.activeElement sometimes
2587-
// moves to the field, sometimes stays <body>, across otherwise-identical repeated runs),
2588-
// same root cause as the #type describe's Obscura guard.
2589-
if (isHelper('Obscura')) this.skip()
25902584
await I.amOnPage('/form/field')
25912585
I.options.strict = true
25922586
await I.click('Name')
@@ -2615,7 +2609,6 @@ export function tests() {
26152609
})
26162610

26172611
it('should not throw for Ctrl+A when element is focused', async function () {
2618-
if (isHelper('Obscura')) this.skip() // same label-click focus non-determinism as "should not throw NonFocusedType when element is focused" above
26192612
await I.amOnPage('/form/field')
26202613
I.options.strict = true
26212614
await I.click('Name')

0 commit comments

Comments
 (0)