diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 01dfbda10..4580ec831 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, 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' @@ -2231,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) { @@ -2259,6 +2261,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..b99821220 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, 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' @@ -1547,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) { @@ -1575,6 +1577,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..7560d30e3 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, checkFocusBeforePressKey } from './extras/focusCheck.js' import output from '../output.js' const { debug } = output import { empty } from '../assert/empty.js' @@ -2237,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) { @@ -2283,6 +2285,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..bd570266a --- /dev/null +++ b/lib/helper/errors/NonFocusedType.js @@ -0,0 +1,8 @@ +class NonFocusedType extends Error { + constructor(message) { + super(message) + 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..cc9ade6eb --- /dev/null +++ b/lib/helper/extras/focusCheck.js @@ -0,0 +1,43 @@ +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) { + 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, originalKey) { + if (!helper.options.strict && !store.debugMode) 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 "${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) +} diff --git a/test/helper/webapi.js b/test/helper/webapi.js index ce6540823..5a74e6d96 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2331,6 +2331,55 @@ 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') + }) + + 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') + }) + + 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', () => {