Skip to content

Commit c5572aa

Browse files
DavertMikDavertMikclaude
authored
feat: detect unfocused element before type() and pressKey() (#5500)
* feat: add focus detection before type() to warn or throw when no element is focused type() sends keystrokes via page.keyboard which silently drops input when no element has focus. Add a shared checkFocusBeforeType() that warns in debug mode and throws NonFocusedType in strict mode. Applied to Playwright, Puppeteer, and WebDriver helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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> * fix: check raw key array before normalization in pressKey focus check WebDriver's getNormalizedKey converts key names to Unicode code points, so checking after normalization misses the modifier. Now check the original user-provided key array before any normalization happens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 33f0c4d commit c5572aa

File tree

6 files changed

+112
-0
lines changed

6 files changed

+112
-0
lines changed

lib/helper/Playwright.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +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, checkFocusBeforePressKey } from './extras/focusCheck.js'
1011
import { includes as stringIncludes } from '../assert/include.js'
1112
import { urlEquals, equals } from '../assert/equal.js'
1213
import { empty } from '../assert/empty.js'
@@ -2231,6 +2232,7 @@ class Playwright extends Helper {
22312232
* {{> pressKeyWithKeyNormalization }}
22322233
*/
22332234
async pressKey(key) {
2235+
await checkFocusBeforePressKey(this, key)
22342236
const modifiers = []
22352237
if (Array.isArray(key)) {
22362238
for (let k of key) {
@@ -2259,6 +2261,8 @@ class Playwright extends Helper {
22592261
* {{> type }}
22602262
*/
22612263
async type(keys, delay = null) {
2264+
await checkFocusBeforeType(this)
2265+
22622266
// Always use page.keyboard.type for any string (including single character and national characters).
22632267
if (!Array.isArray(keys)) {
22642268
keys = keys.toString()

lib/helper/Puppeteer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +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, checkFocusBeforePressKey } from './extras/focusCheck.js'
1112
import { includes as stringIncludes } from '../assert/include.js'
1213
import { urlEquals, equals } from '../assert/equal.js'
1314
import { empty } from '../assert/empty.js'
@@ -1547,6 +1548,7 @@ class Puppeteer extends Helper {
15471548
* {{> pressKeyWithKeyNormalization }}
15481549
*/
15491550
async pressKey(key) {
1551+
await checkFocusBeforePressKey(this, key)
15501552
const modifiers = []
15511553
if (Array.isArray(key)) {
15521554
for (let k of key) {
@@ -1575,6 +1577,8 @@ class Puppeteer extends Helper {
15751577
* {{> type }}
15761578
*/
15771579
async type(keys, delay = null) {
1580+
await checkFocusBeforeType(this)
1581+
15781582
if (!Array.isArray(keys)) {
15791583
keys = keys.toString()
15801584
keys = keys.split('')

lib/helper/WebDriver.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +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, checkFocusBeforePressKey } from './extras/focusCheck.js'
1314
import output from '../output.js'
1415
const { debug } = output
1516
import { empty } from '../assert/empty.js'
@@ -2237,6 +2238,7 @@ class WebDriver extends Helper {
22372238
* {{> pressKeyWithKeyNormalization }}
22382239
*/
22392240
async pressKey(key) {
2241+
await checkFocusBeforePressKey(this, key)
22402242
const modifiers = []
22412243
if (Array.isArray(key)) {
22422244
for (let k of key) {
@@ -2283,6 +2285,8 @@ class WebDriver extends Helper {
22832285
* {{> type }}
22842286
*/
22852287
async type(keys, delay = null) {
2288+
await checkFocusBeforeType(this)
2289+
22862290
if (!Array.isArray(keys)) {
22872291
keys = keys.toString()
22882292
keys = keys.split('')
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class NonFocusedType extends Error {
2+
constructor(message) {
3+
super(message)
4+
this.name = 'NonFocusedType'
5+
}
6+
}
7+
8+
export default NonFocusedType

lib/helper/extras/focusCheck.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import store from '../../store.js'
2+
import NonFocusedType from '../errors/NonFocusedType.js'
3+
4+
const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
5+
const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
6+
7+
async function isNoElementFocused(helper) {
8+
return helper.executeScript(() => {
9+
const ae = document.activeElement
10+
return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
11+
})
12+
}
13+
14+
export async function checkFocusBeforeType(helper) {
15+
if (!helper.options.strict && !store.debugMode) return
16+
if (!await isNoElementFocused(helper)) return
17+
18+
const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
19+
if (helper.options.strict) throw new NonFocusedType(message)
20+
helper.debugSection('Warning', message)
21+
}
22+
23+
export async function checkFocusBeforePressKey(helper, originalKey) {
24+
if (!helper.options.strict && !store.debugMode) return
25+
if (!Array.isArray(originalKey)) return
26+
27+
let hasCtrlOrMeta = false
28+
let actionKey = null
29+
for (const k of originalKey) {
30+
if (MODIFIER_PATTERN.test(k)) {
31+
hasCtrlOrMeta = true
32+
} else {
33+
actionKey = k
34+
}
35+
}
36+
if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
37+
38+
if (!await isNoElementFocused(helper)) return
39+
40+
const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
41+
if (helper.options.strict) throw new NonFocusedType(message)
42+
helper.debugSection('Warning', message)
43+
}

test/helper/webapi.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,55 @@ export function tests() {
23312331
expect(err.message).to.include('/html')
23322332
expect(err.message).to.include('Use a more specific locator')
23332333
})
2334+
2335+
it('should throw NonFocusedType error when typing without focus', async () => {
2336+
await I.amOnPage('/form/field')
2337+
I.options.strict = true
2338+
let err
2339+
try {
2340+
await I.type('test')
2341+
} catch (e) {
2342+
err = e
2343+
}
2344+
expect(err).to.exist
2345+
expect(err.constructor.name).to.equal('NonFocusedType')
2346+
expect(err.message).to.include('No element is in focus')
2347+
})
2348+
2349+
it('should not throw NonFocusedType when element is focused', async () => {
2350+
await I.amOnPage('/form/field')
2351+
I.options.strict = true
2352+
await I.click('Name')
2353+
await I.type('test')
2354+
await I.seeInField('Name', 'test')
2355+
})
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+
})
23342383
})
23352384

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

0 commit comments

Comments
 (0)