diff --git a/src/client-functions/selectors/selector-builder.js b/src/client-functions/selectors/selector-builder.js index ba6137ade9..3cab1c6ec4 100644 --- a/src/client-functions/selectors/selector-builder.js +++ b/src/client-functions/selectors/selector-builder.js @@ -156,6 +156,7 @@ export default class SelectorBuilder extends ClientFunctionBuilder { apiFnChain: this.options.apiFnChain, visibilityCheck: !!this.options.visibilityCheck, timeout: this.options.timeout, + strictError: this.options.strictError, }); } diff --git a/src/client/driver/driver.js b/src/client/driver/driver.js index c4ff529311..700be3f10e 100644 --- a/src/client/driver/driver.js +++ b/src/client/driver/driver.js @@ -50,7 +50,6 @@ import { CurrentIframeIsNotLoadedError, CurrentIframeNotFoundError, CurrentIframeIsInvisibleError, - CannotObtainInfoForElementSpecifiedBySelectorError, UncaughtErrorInCustomClientScriptCode, UncaughtErrorInCustomClientScriptLoadedFromModule, ChildWindowIsNotLoadedError, @@ -112,7 +111,11 @@ import getExecutorResultDriverStatus from './command-executors/get-executor-resu import SelectorExecutor from './command-executors/client-functions/selector-executor'; import SelectorElementActionTransform from './command-executors/client-functions/replicator/transforms/selector-element-action-transform'; import BarriersComplex from '../../shared/barriers/complex-barrier'; -import createErrorCtorCallback from '../../shared/errors/selector-error-ctor-callback'; +import createErrorCtorCallback, { + getCannotObtainInfoErrorCtor, + getInvisibleErrorCtor, + getNotFoundErrorCtor, +} from '../../shared/errors/selector-error-ctor-callback'; import './command-executors/actions-initializer'; const settings = hammerhead.settings; @@ -1207,14 +1210,15 @@ export default class Driver extends serviceUtils.EventEmitter { _onExecuteSelectorCommand (command) { const startTime = this.contextStorage.getItem(SELECTOR_EXECUTION_START_TIME) || new DateCtor(); - const elementNotFoundOrNotVisible = fn => new CannotObtainInfoForElementSpecifiedBySelectorError(null, fn); - const createError = command.needError ? elementNotFoundOrNotVisible : null; + const elementNotFoundOrNotVisible = createErrorCtorCallback(getCannotObtainInfoErrorCtor()); + const elementNotFound = command.strictError ? createErrorCtorCallback(getNotFoundErrorCtor()) : elementNotFoundOrNotVisible; + const elementIsInvisible = command.strictError ? createErrorCtorCallback(getInvisibleErrorCtor()) : elementNotFoundOrNotVisible; getExecuteSelectorResultDriverStatus(command, this.selectorTimeout, startTime, - createError, - createError, + command.needError ? elementNotFound : null, + command.needError ? elementIsInvisible : null, this.statusBar) .then(driverStatus => { this.contextStorage.setItem(SELECTOR_EXECUTION_START_TIME, null); diff --git a/src/shared/errors/selector-error-ctor-callback.ts b/src/shared/errors/selector-error-ctor-callback.ts index 91a4dacc71..15dd4454a8 100644 --- a/src/shared/errors/selector-error-ctor-callback.ts +++ b/src/shared/errors/selector-error-ctor-callback.ts @@ -2,6 +2,23 @@ import { AutomationErrorCtor } from '../types'; import { FnInfo, SelectorErrorCb } from '../../client/driver/command-executors/client-functions/types'; import * as Errors from './index'; +export function getInvisibleErrorCtor (elementName?: string): AutomationErrorCtor | string { + return !elementName ? 'ActionElementIsInvisibleError' : { + name: 'ActionAdditionalElementIsInvisibleError', + firstArg: elementName, + }; +} + +export function getNotFoundErrorCtor (elementName?: string): AutomationErrorCtor | string { + return !elementName ? 'ActionElementNotFoundError' : { + name: 'ActionAdditionalElementNotFoundError', + firstArg: elementName, + }; +} + +export function getCannotObtainInfoErrorCtor (): AutomationErrorCtor | string { + return 'CannotObtainInfoForElementSpecifiedBySelectorError'; +} export default function createErrorCtorCallback (errCtor: AutomationErrorCtor | string): SelectorErrorCb { // @ts-ignore diff --git a/src/shared/utils/elements-retriever.ts b/src/shared/utils/elements-retriever.ts index 578cb8a5b7..42c8e0e5e2 100644 --- a/src/shared/utils/elements-retriever.ts +++ b/src/shared/utils/elements-retriever.ts @@ -6,6 +6,7 @@ import { ActionSelectorMatchesWrongNodeTypeError, ActionAdditionalSelectorMatchesWrongNodeTypeError, } from '../../shared/errors'; +import { getInvisibleErrorCtor, getNotFoundErrorCtor } from '../errors/selector-error-ctor-callback'; export default class ElementsRetriever { @@ -27,14 +28,8 @@ export default class ElementsRetriever { this._ensureElementsPromise = this._ensureElementsPromise .then(() => { return this._executeSelectorFn(selector, { - invisible: !elementName ? 'ActionElementIsInvisibleError' : { - name: 'ActionAdditionalElementIsInvisibleError', - firstArg: elementName, - }, - notFound: !elementName ? 'ActionElementNotFoundError' : { - name: 'ActionAdditionalElementNotFoundError', - firstArg: elementName, - }, + invisible: getInvisibleErrorCtor(elementName), + notFound: getNotFoundErrorCtor(elementName), }, this._ensureElementsStartTime); }) .then(el => { diff --git a/src/test-run/commands/actions.js b/src/test-run/commands/actions.js index c24cbe8a8e..d783a4aba2 100644 --- a/src/test-run/commands/actions.js +++ b/src/test-run/commands/actions.js @@ -14,7 +14,11 @@ import { CookieOptions, } from './options'; -import { initSelector, initUploadSelector } from './validations/initializers'; +import { + initSelector, + initTypeSelector, + initUploadSelector, +} from './validations/initializers'; import { executeJsExpression } from '../execute-js-expression'; import { isJSExpression } from './utils'; @@ -217,7 +221,7 @@ export class TypeTextCommand extends ActionCommandBase { _getAssignableProperties () { return [ - { name: 'selector', init: initSelector, required: true }, + { name: 'selector', init: initTypeSelector, required: true }, { name: 'text', type: nonEmptyStringArgument, required: true }, { name: 'options', type: actionOptions, init: initTypeOptions, required: true }, ]; diff --git a/src/test-run/commands/observation.d.ts b/src/test-run/commands/observation.d.ts index 6d72df1c12..ba1e687b07 100644 --- a/src/test-run/commands/observation.d.ts +++ b/src/test-run/commands/observation.d.ts @@ -21,6 +21,7 @@ export class ExecuteSelectorCommand extends ExecuteClientFunctionCommandBase { public apiFnChain: string[]; public needError: boolean; public index: number; + public strictError: boolean; } export class WaitCommand extends ActionCommandBase { diff --git a/src/test-run/commands/observation.js b/src/test-run/commands/observation.js index 67f9d20226..22eb2279c0 100644 --- a/src/test-run/commands/observation.js +++ b/src/test-run/commands/observation.js @@ -56,6 +56,7 @@ export class ExecuteSelectorCommand extends ExecuteClientFunctionCommandBase { { name: 'apiFnChain' }, { name: 'needError' }, { name: 'index', defaultValue: 0 }, + { name: 'strictError' }, ]); } } diff --git a/src/test-run/commands/validations/initializers.js b/src/test-run/commands/validations/initializers.js index 024cc99502..5dd7befc94 100644 --- a/src/test-run/commands/validations/initializers.js +++ b/src/test-run/commands/validations/initializers.js @@ -11,6 +11,13 @@ export function initUploadSelector (name, val, initOptions) { return initSelector(name, val, initOptions); } +export function initTypeSelector (name, val, initOptions) { + initOptions.needError = true; + initOptions.strictError = true; + + return initSelector(name, val, initOptions); +} + export function initSelector (name, val, { testRun, ...options }) { if (val instanceof ExecuteSelectorCommand) return val; diff --git a/src/test-run/index.ts b/src/test-run/index.ts index b721e5ccca..b3e78f0263 100644 --- a/src/test-run/index.ts +++ b/src/test-run/index.ts @@ -1044,12 +1044,12 @@ export default class TestRun extends AsyncEventEmitter { command.generateScreenshotMark(); } - public async _adjustCommandOptionsAndEnvironment (command: CommandBase): Promise { + public async _adjustCommandOptionsAndEnvironment (command: CommandBase, callsite: CallsiteRecord): Promise { if ((command as any).options?.confidential !== void 0) return; if (command.type === COMMAND_TYPE.typeText) { - const result = await this._internalExecuteCommand((command as any).selector); + const result = await this._internalExecuteCommand((command as any).selector, callsite); if (!result) return; @@ -1095,14 +1095,21 @@ export default class TestRun extends AsyncEventEmitter { let error = null; let result = null; - await this._adjustCommandOptionsAndEnvironment(command); + const start = new Date().getTime(); + + try { + await this._adjustCommandOptionsAndEnvironment(command, callsite); + } + catch (err) { + error = err; + } await this.emitActionEvent('action-start', actionArgs); - const start = new Date().getTime(); try { - result = await this._internalExecuteCommand(command, callsite); + if (!error) + result = await this._internalExecuteCommand(command, callsite); } catch (err) { if (this.phase === TestRunPhase.pendingFinalization && err instanceof ExternalAssertionLibraryError) diff --git a/test/functional/fixtures/api/es-next/type/test.js b/test/functional/fixtures/api/es-next/type/test.js index b19a4215a4..f265824eaf 100644 --- a/test/functional/fixtures/api/es-next/type/test.js +++ b/test/functional/fixtures/api/es-next/type/test.js @@ -1,3 +1,4 @@ +const config = require('../../../../config'); const expect = require('chai').expect; // NOTE: we run tests in chrome only, because we mainly test server API functionality. @@ -43,4 +44,21 @@ describe('[API] t.typeText()', function () { expect(errs[0]).to.contains('> 19 | await t.typeText(NaN, \'a\');'); }); }); + + if (!config.proxyless) { + it('Should not execute selector twice for non-existing element due to "confidential" option (GH-6623)', function () { + return runTests('./testcafe-fixtures/type-test.js', 'Not found selector', { + shouldFail: true, + only: 'chrome', + selectorTimeout: 3000, + }) + .catch(function (errs) { + expect(testReport.durationMs).lessThan(6000); + expect(errs[0]).to.contains( + 'The specified selector does not match any element in the DOM tree.' + ); + expect(errs[0]).to.contains('> 31 | await t.typeText(\'#not-found\', \'a\');'); + }); + }); + } }); diff --git a/test/functional/fixtures/api/es-next/type/testcafe-fixtures/type-test.js b/test/functional/fixtures/api/es-next/type/testcafe-fixtures/type-test.js index acf6505760..978af38ca8 100644 --- a/test/functional/fixtures/api/es-next/type/testcafe-fixtures/type-test.js +++ b/test/functional/fixtures/api/es-next/type/testcafe-fixtures/type-test.js @@ -26,3 +26,7 @@ test('Incorrect action text', async t => { test('Incorrect action options', async t => { await t.typeText('#input', 'a', { replace: null, paste: null }); }); + +test('Not found selector', async t => { + await t.typeText('#not-found', 'a'); +}); diff --git a/test/server/test-run-commands-test.js b/test/server/test-run-commands-test.js index 81b5d4a3c8..f09041d0c6 100644 --- a/test/server/test-run-commands-test.js +++ b/test/server/test-run-commands-test.js @@ -69,8 +69,11 @@ function assertErrorMessage (fn, expectedErrMessage) { expect(actualErr.message).eql(expectedErrMessage); } -function makeSelector (str, skipVisibilityCheck) { - const builder = new SelectorBuilder(str, { visibilityCheck: !skipVisibilityCheck }, { instantiation: 'Selector' }); +function makeSelector (str, skipVisibilityCheck, needError, strictError) { + const builder = new SelectorBuilder(str, { + visibilityCheck: !skipVisibilityCheck, + needError, strictError, + }, { instantiation: 'Selector' }); const command = builder.getCommand([]); command.actionId = 'child-command-selector'; @@ -565,7 +568,7 @@ describe('Test run commands', () => { expect(JSON.parse(JSON.stringify(command))).eql({ type: TYPE.typeText, actionId: TYPE.typeText, - selector: makeSelector('#yo'), + selector: makeSelector('#yo', false, true, true), text: 'testText', options: { @@ -598,7 +601,7 @@ describe('Test run commands', () => { expect(JSON.parse(JSON.stringify(command))).eql({ type: TYPE.typeText, actionId: TYPE.typeText, - selector: makeSelector('#yo'), + selector: makeSelector('#yo', false, true, true), text: 'testText', options: {