Skip to content

Commit ccc1e29

Browse files
DavertMikclaude
andcommitted
feat: add focus check for pressKey() editing combos (Ctrl+A/C/X/V/Z/Y)
NonFocusedType now accepts message from caller. checkFocusBeforePressKey() warns/throws only for editing key combos (Ctrl/Meta + A/C/X/V/Z/Y), not for navigation keys like Escape or Tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent be3cbc6 commit ccc1e29

6 files changed

Lines changed: 58 additions & 18 deletions

File tree

lib/helper/Playwright.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import promiseRetry from 'promise-retry'
77
import Locator from '../locator.js'
88
import recorder from '../recorder.js'
99
import store from '../store.js'
10-
import { checkFocusBeforeType } from './extras/focusCheck.js'
10+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
1111
import { includes as stringIncludes } from '../assert/include.js'
1212
import { urlEquals, equals } from '../assert/equal.js'
1313
import { empty } from '../assert/empty.js'
@@ -2246,6 +2246,7 @@ class Playwright extends Helper {
22462246
} else {
22472247
key = getNormalizedKey.call(this, key)
22482248
}
2249+
await checkFocusBeforePressKey(this, modifiers, key)
22492250
for (const modifier of modifiers) {
22502251
await this.page.keyboard.down(modifier)
22512252
}

lib/helper/Puppeteer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import promiseRetry from 'promise-retry'
88
import Locator from '../locator.js'
99
import recorder from '../recorder.js'
1010
import store from '../store.js'
11-
import { checkFocusBeforeType } from './extras/focusCheck.js'
11+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
1212
import { includes as stringIncludes } from '../assert/include.js'
1313
import { urlEquals, equals } from '../assert/equal.js'
1414
import { empty } from '../assert/empty.js'
@@ -1562,6 +1562,7 @@ class Puppeteer extends Helper {
15621562
} else {
15631563
key = getNormalizedKey.call(this, key)
15641564
}
1565+
await checkFocusBeforePressKey(this, modifiers, key)
15651566
for (const modifier of modifiers) {
15661567
await this.page.keyboard.down(modifier)
15671568
}

lib/helper/WebDriver.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import promiseRetry from 'promise-retry'
1010
import { includes as stringIncludes } from '../assert/include.js'
1111
import { urlEquals, equals } from '../assert/equal.js'
1212
import store from '../store.js'
13-
import { checkFocusBeforeType } from './extras/focusCheck.js'
13+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
1414
import output from '../output.js'
1515
const { debug } = output
1616
import { empty } from '../assert/empty.js'
@@ -2252,6 +2252,7 @@ class WebDriver extends Helper {
22522252
} else {
22532253
key = getNormalizedKey.call(this, key)
22542254
}
2255+
await checkFocusBeforePressKey(this, modifiers, key)
22552256
for (const modifier of modifiers) {
22562257
await this.pressKeyDown(modifier)
22572258
}

lib/helper/errors/NonFocusedType.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
class NonFocusedType extends Error {
2-
constructor() {
3-
super(
4-
'No element is in focus. Use I.click() or I.focus() to activate an element before calling I.type(). '
5-
+ 'This error is thrown because strict mode is enabled.',
6-
)
2+
constructor(message) {
3+
super(message)
74
this.name = 'NonFocusedType'
85
}
96
}

lib/helper/extras/focusCheck.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
import store from '../../store.js'
22
import NonFocusedType from '../errors/NonFocusedType.js'
33

4-
export async function checkFocusBeforeType(helper) {
5-
const isStrict = helper.options.strict
6-
if (!isStrict && !store.debugMode) return
4+
const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
75

8-
const noFocus = await helper.executeScript(() => {
6+
async function isNoElementFocused(helper) {
7+
return helper.executeScript(() => {
98
const ae = document.activeElement
109
return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
1110
})
11+
}
12+
13+
export async function checkFocusBeforeType(helper) {
14+
if (!helper.options.strict && !store.debugMode) return
15+
if (!await isNoElementFocused(helper)) return
16+
17+
const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
18+
if (helper.options.strict) throw new NonFocusedType(message)
19+
helper.debugSection('Warning', message)
20+
}
21+
22+
export async function checkFocusBeforePressKey(helper, modifiers, key) {
23+
if (!helper.options.strict && !store.debugMode) return
1224

13-
if (!noFocus) return
25+
const hasCtrlOrMeta = modifiers.some(m => m === 'Control' || m === 'Meta'
26+
|| m === 'ControlLeft' || m === 'ControlRight' || m === 'MetaLeft' || m === 'MetaRight')
27+
if (!hasCtrlOrMeta || !EDITING_KEYS.has(key.toLowerCase())) return
1428

15-
if (isStrict) {
16-
throw new NonFocusedType()
17-
}
29+
if (!await isNoElementFocused(helper)) return
1830

19-
helper.debugSection('Warning', 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.')
31+
const message = `No element is in focus. Key combination with "${key}" may not work as expected. Use I.click() or I.focus() first.`
32+
if (helper.options.strict) throw new NonFocusedType(message)
33+
helper.debugSection('Warning', message)
2034
}

test/helper/webapi.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2344,7 +2344,6 @@ export function tests() {
23442344
expect(err).to.exist
23452345
expect(err.constructor.name).to.equal('NonFocusedType')
23462346
expect(err.message).to.include('No element is in focus')
2347-
expect(err.message).to.include('strict mode')
23482347
})
23492348

23502349
it('should not throw NonFocusedType when element is focused', async () => {
@@ -2354,6 +2353,33 @@ export function tests() {
23542353
await I.type('test')
23552354
await I.seeInField('Name', 'test')
23562355
})
2356+
2357+
it('should throw NonFocusedType for Ctrl+A without focus', async () => {
2358+
await I.amOnPage('/form/field')
2359+
I.options.strict = true
2360+
let err
2361+
try {
2362+
await I.pressKey(['Control', 'A'])
2363+
} catch (e) {
2364+
err = e
2365+
}
2366+
expect(err).to.exist
2367+
expect(err.constructor.name).to.equal('NonFocusedType')
2368+
expect(err.message).to.include('No element is in focus')
2369+
})
2370+
2371+
it('should not throw for Escape without focus', async () => {
2372+
await I.amOnPage('/form/field')
2373+
I.options.strict = true
2374+
await I.pressKey('Escape')
2375+
})
2376+
2377+
it('should not throw for Ctrl+A when element is focused', async () => {
2378+
await I.amOnPage('/form/field')
2379+
I.options.strict = true
2380+
await I.click('Name')
2381+
await I.pressKey(['Control', 'A'])
2382+
})
23572383
})
23582384

23592385
describe('#elementIndex step option', () => {

0 commit comments

Comments
 (0)