From be3cbc646ac8c1289a82772580b448503637263d Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 31 Mar 2026 12:01:13 +0300 Subject: [PATCH 1/3] 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) --- lib/helper/Playwright.js | 3 +++ lib/helper/Puppeteer.js | 3 +++ lib/helper/WebDriver.js | 3 +++ lib/helper/errors/NonFocusedType.js | 11 +++++++++++ lib/helper/extras/focusCheck.js | 20 ++++++++++++++++++++ test/helper/webapi.js | 23 +++++++++++++++++++++++ 6 files changed, 63 insertions(+) create mode 100644 lib/helper/errors/NonFocusedType.js create mode 100644 lib/helper/extras/focusCheck.js diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 01dfbda10..a4f4dc313 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry' import Locator from '../locator.js' import recorder from '../recorder.js' import store from '../store.js' +import { checkFocusBeforeType } from './extras/focusCheck.js' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import { empty } from '../assert/empty.js' @@ -2259,6 +2260,8 @@ class Playwright extends Helper { * {{> type }} */ async type(keys, delay = null) { + await checkFocusBeforeType(this) + // Always use page.keyboard.type for any string (including single character and national characters). if (!Array.isArray(keys)) { keys = keys.toString() diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 4a220a948..8f79f9b58 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -8,6 +8,7 @@ import promiseRetry from 'promise-retry' import Locator from '../locator.js' import recorder from '../recorder.js' import store from '../store.js' +import { checkFocusBeforeType } from './extras/focusCheck.js' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import { empty } from '../assert/empty.js' @@ -1575,6 +1576,8 @@ class Puppeteer extends Helper { * {{> type }} */ async type(keys, delay = null) { + await checkFocusBeforeType(this) + if (!Array.isArray(keys)) { keys = keys.toString() keys = keys.split('') diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 8cffa0567..953729fc9 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -10,6 +10,7 @@ import promiseRetry from 'promise-retry' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import store from '../store.js' +import { checkFocusBeforeType } from './extras/focusCheck.js' import output from '../output.js' const { debug } = output import { empty } from '../assert/empty.js' @@ -2283,6 +2284,8 @@ class WebDriver extends Helper { * {{> type }} */ async type(keys, delay = null) { + await checkFocusBeforeType(this) + if (!Array.isArray(keys)) { keys = keys.toString() keys = keys.split('') diff --git a/lib/helper/errors/NonFocusedType.js b/lib/helper/errors/NonFocusedType.js new file mode 100644 index 000000000..02c4efc4e --- /dev/null +++ b/lib/helper/errors/NonFocusedType.js @@ -0,0 +1,11 @@ +class NonFocusedType extends Error { + constructor() { + super( + 'No element is in focus. Use I.click() or I.focus() to activate an element before calling I.type(). ' + + 'This error is thrown because strict mode is enabled.', + ) + this.name = 'NonFocusedType' + } +} + +export default NonFocusedType diff --git a/lib/helper/extras/focusCheck.js b/lib/helper/extras/focusCheck.js new file mode 100644 index 000000000..eda6da0a0 --- /dev/null +++ b/lib/helper/extras/focusCheck.js @@ -0,0 +1,20 @@ +import store from '../../store.js' +import NonFocusedType from '../errors/NonFocusedType.js' + +export async function checkFocusBeforeType(helper) { + const isStrict = helper.options.strict + if (!isStrict && !store.debugMode) return + + const noFocus = await helper.executeScript(() => { + const ae = document.activeElement + return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable) + }) + + if (!noFocus) return + + if (isStrict) { + throw new NonFocusedType() + } + + helper.debugSection('Warning', 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.') +} diff --git a/test/helper/webapi.js b/test/helper/webapi.js index ce6540823..e063c4730 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2331,6 +2331,29 @@ export function tests() { expect(err.message).to.include('/html') expect(err.message).to.include('Use a more specific locator') }) + + it('should throw NonFocusedType error when typing without focus', async () => { + await I.amOnPage('/form/field') + I.options.strict = true + let err + try { + await I.type('test') + } catch (e) { + err = e + } + expect(err).to.exist + expect(err.constructor.name).to.equal('NonFocusedType') + expect(err.message).to.include('No element is in focus') + expect(err.message).to.include('strict mode') + }) + + it('should not throw NonFocusedType when element is focused', async () => { + await I.amOnPage('/form/field') + I.options.strict = true + await I.click('Name') + await I.type('test') + await I.seeInField('Name', 'test') + }) }) describe('#elementIndex step option', () => { From ccc1e299b5b7f54510029192a817a18ecc6e432a Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 31 Mar 2026 12:32:44 +0300 Subject: [PATCH 2/3] 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) --- lib/helper/Playwright.js | 3 ++- lib/helper/Puppeteer.js | 3 ++- lib/helper/WebDriver.js | 3 ++- lib/helper/errors/NonFocusedType.js | 7 ++----- lib/helper/extras/focusCheck.js | 32 +++++++++++++++++++++-------- test/helper/webapi.js | 28 ++++++++++++++++++++++++- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index a4f4dc313..c7bd6bc29 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -7,7 +7,7 @@ import promiseRetry from 'promise-retry' import Locator from '../locator.js' import recorder from '../recorder.js' import store from '../store.js' -import { checkFocusBeforeType } from './extras/focusCheck.js' +import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import { empty } from '../assert/empty.js' @@ -2246,6 +2246,7 @@ class Playwright extends Helper { } else { key = getNormalizedKey.call(this, key) } + await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.page.keyboard.down(modifier) } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 8f79f9b58..a065391db 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -8,7 +8,7 @@ import promiseRetry from 'promise-retry' import Locator from '../locator.js' import recorder from '../recorder.js' import store from '../store.js' -import { checkFocusBeforeType } from './extras/focusCheck.js' +import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import { empty } from '../assert/empty.js' @@ -1562,6 +1562,7 @@ class Puppeteer extends Helper { } else { key = getNormalizedKey.call(this, key) } + await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.page.keyboard.down(modifier) } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 953729fc9..6efea6b41 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -10,7 +10,7 @@ import promiseRetry from 'promise-retry' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import store from '../store.js' -import { checkFocusBeforeType } from './extras/focusCheck.js' +import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js' import output from '../output.js' const { debug } = output import { empty } from '../assert/empty.js' @@ -2252,6 +2252,7 @@ class WebDriver extends Helper { } else { key = getNormalizedKey.call(this, key) } + await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.pressKeyDown(modifier) } diff --git a/lib/helper/errors/NonFocusedType.js b/lib/helper/errors/NonFocusedType.js index 02c4efc4e..bd570266a 100644 --- a/lib/helper/errors/NonFocusedType.js +++ b/lib/helper/errors/NonFocusedType.js @@ -1,9 +1,6 @@ class NonFocusedType extends Error { - constructor() { - super( - 'No element is in focus. Use I.click() or I.focus() to activate an element before calling I.type(). ' - + 'This error is thrown because strict mode is enabled.', - ) + constructor(message) { + super(message) this.name = 'NonFocusedType' } } diff --git a/lib/helper/extras/focusCheck.js b/lib/helper/extras/focusCheck.js index eda6da0a0..29603cd5e 100644 --- a/lib/helper/extras/focusCheck.js +++ b/lib/helper/extras/focusCheck.js @@ -1,20 +1,34 @@ import store from '../../store.js' import NonFocusedType from '../errors/NonFocusedType.js' -export async function checkFocusBeforeType(helper) { - const isStrict = helper.options.strict - if (!isStrict && !store.debugMode) return +const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y']) - const noFocus = await helper.executeScript(() => { +async function isNoElementFocused(helper) { + return helper.executeScript(() => { const ae = document.activeElement return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable) }) +} + +export async function checkFocusBeforeType(helper) { + if (!helper.options.strict && !store.debugMode) return + if (!await isNoElementFocused(helper)) return + + const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.' + if (helper.options.strict) throw new NonFocusedType(message) + helper.debugSection('Warning', message) +} + +export async function checkFocusBeforePressKey(helper, modifiers, key) { + if (!helper.options.strict && !store.debugMode) return - if (!noFocus) return + const hasCtrlOrMeta = modifiers.some(m => m === 'Control' || m === 'Meta' + || m === 'ControlLeft' || m === 'ControlRight' || m === 'MetaLeft' || m === 'MetaRight') + if (!hasCtrlOrMeta || !EDITING_KEYS.has(key.toLowerCase())) return - if (isStrict) { - throw new NonFocusedType() - } + if (!await isNoElementFocused(helper)) return - helper.debugSection('Warning', 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.') + const message = `No element is in focus. Key combination with "${key}" may not work as expected. Use I.click() or I.focus() first.` + if (helper.options.strict) throw new NonFocusedType(message) + helper.debugSection('Warning', message) } diff --git a/test/helper/webapi.js b/test/helper/webapi.js index e063c4730..5a74e6d96 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2344,7 +2344,6 @@ export function tests() { expect(err).to.exist expect(err.constructor.name).to.equal('NonFocusedType') expect(err.message).to.include('No element is in focus') - expect(err.message).to.include('strict mode') }) it('should not throw NonFocusedType when element is focused', async () => { @@ -2354,6 +2353,33 @@ export function tests() { await I.type('test') await I.seeInField('Name', 'test') }) + + it('should throw NonFocusedType for Ctrl+A without focus', async () => { + await I.amOnPage('/form/field') + I.options.strict = true + let err + try { + await I.pressKey(['Control', 'A']) + } catch (e) { + err = e + } + expect(err).to.exist + expect(err.constructor.name).to.equal('NonFocusedType') + expect(err.message).to.include('No element is in focus') + }) + + it('should not throw for Escape without focus', async () => { + await I.amOnPage('/form/field') + I.options.strict = true + await I.pressKey('Escape') + }) + + it('should not throw for Ctrl+A when element is focused', async () => { + await I.amOnPage('/form/field') + I.options.strict = true + await I.click('Name') + await I.pressKey(['Control', 'A']) + }) }) describe('#elementIndex step option', () => { From 41ad9e5cce4d11da814d38a60b1c8f9a3409d850 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 31 Mar 2026 12:46:18 +0300 Subject: [PATCH 3/3] 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) --- lib/helper/Playwright.js | 2 +- lib/helper/Puppeteer.js | 2 +- lib/helper/WebDriver.js | 2 +- lib/helper/extras/focusCheck.js | 21 +++++++++++++++------ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index c7bd6bc29..4580ec831 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -2232,6 +2232,7 @@ class Playwright extends Helper { * {{> pressKeyWithKeyNormalization }} */ async pressKey(key) { + await checkFocusBeforePressKey(this, key) const modifiers = [] if (Array.isArray(key)) { for (let k of key) { @@ -2246,7 +2247,6 @@ class Playwright extends Helper { } else { key = getNormalizedKey.call(this, key) } - await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.page.keyboard.down(modifier) } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index a065391db..b99821220 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -1548,6 +1548,7 @@ class Puppeteer extends Helper { * {{> pressKeyWithKeyNormalization }} */ async pressKey(key) { + await checkFocusBeforePressKey(this, key) const modifiers = [] if (Array.isArray(key)) { for (let k of key) { @@ -1562,7 +1563,6 @@ class Puppeteer extends Helper { } else { key = getNormalizedKey.call(this, key) } - await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.page.keyboard.down(modifier) } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 6efea6b41..7560d30e3 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -2238,6 +2238,7 @@ class WebDriver extends Helper { * {{> pressKeyWithKeyNormalization }} */ async pressKey(key) { + await checkFocusBeforePressKey(this, key) const modifiers = [] if (Array.isArray(key)) { for (let k of key) { @@ -2252,7 +2253,6 @@ class WebDriver extends Helper { } else { key = getNormalizedKey.call(this, key) } - await checkFocusBeforePressKey(this, modifiers, key) for (const modifier of modifiers) { await this.pressKeyDown(modifier) } diff --git a/lib/helper/extras/focusCheck.js b/lib/helper/extras/focusCheck.js index 29603cd5e..cc9ade6eb 100644 --- a/lib/helper/extras/focusCheck.js +++ b/lib/helper/extras/focusCheck.js @@ -1,6 +1,7 @@ import store from '../../store.js' import NonFocusedType from '../errors/NonFocusedType.js' +const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y']) async function isNoElementFocused(helper) { @@ -19,16 +20,24 @@ export async function checkFocusBeforeType(helper) { helper.debugSection('Warning', message) } -export async function checkFocusBeforePressKey(helper, modifiers, key) { +export async function checkFocusBeforePressKey(helper, originalKey) { if (!helper.options.strict && !store.debugMode) return - - const hasCtrlOrMeta = modifiers.some(m => m === 'Control' || m === 'Meta' - || m === 'ControlLeft' || m === 'ControlRight' || m === 'MetaLeft' || m === 'MetaRight') - if (!hasCtrlOrMeta || !EDITING_KEYS.has(key.toLowerCase())) return + if (!Array.isArray(originalKey)) return + + let hasCtrlOrMeta = false + let actionKey = null + for (const k of originalKey) { + if (MODIFIER_PATTERN.test(k)) { + hasCtrlOrMeta = true + } else { + actionKey = k + } + } + if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return if (!await isNoElementFocused(helper)) return - const message = `No element is in focus. Key combination with "${key}" may not work as expected. Use I.click() or I.focus() first.` + const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.` if (helper.options.strict) throw new NonFocusedType(message) helper.debugSection('Warning', message) }