diff --git a/src/client/core/utils/position.js b/src/client/core/utils/position.js index 963fc75aa5..e48616b32a 100644 --- a/src/client/core/utils/position.js +++ b/src/client/core/utils/position.js @@ -4,7 +4,7 @@ import * as domUtils from './dom'; import * as shared from './shared/position'; import AxisValues from '../../../shared/utils/values/axis-values'; -export { isElementVisible } from './shared/visibility'; +export { isElementVisible, isIframeVisible } from './shared/visibility'; export const getElementRectangle = hammerhead.utils.position.getElementRectangle; export const getOffsetPosition = hammerhead.utils.position.getOffsetPosition; diff --git a/src/client/core/utils/shared/visibility.ts b/src/client/core/utils/shared/visibility.ts index b4ecd456cc..3eeb6ca6a6 100644 --- a/src/client/core/utils/shared/visibility.ts +++ b/src/client/core/utils/shared/visibility.ts @@ -1,6 +1,10 @@ import adapter from './adapter/index'; import { isNotVisibleNode, hasDimensions } from './style'; +export function isIframeVisible (el: Node): boolean { + return !hiddenUsingStyles(el as HTMLElement); +} + function hiddenUsingStyles (el: HTMLElement): boolean { return adapter.style.get(el, 'visibility') === 'hidden' || adapter.style.get(el, 'display') === 'none'; diff --git a/src/client/driver/command-executors/client-functions/adapter/initializer.ts b/src/client/driver/command-executors/client-functions/adapter/initializer.ts index 5955a1e192..46352ba757 100644 --- a/src/client/driver/command-executors/client-functions/adapter/initializer.ts +++ b/src/client/driver/command-executors/client-functions/adapter/initializer.ts @@ -25,6 +25,8 @@ const initializer: ClientFunctionAdapter = { getTagName: domUtils.getTagName, isOptionElementVisible: selectElementUI.isOptionElementVisible, isElementVisible: positionUtils.isElementVisible, + isIframeVisible: positionUtils.isIframeVisible, + isIframeElement: domUtils.isIframeElement, getActiveElement: domUtils.getActiveElement, }; diff --git a/src/client/driver/command-executors/client-functions/selector-executor/utils.ts b/src/client/driver/command-executors/client-functions/selector-executor/utils.ts index 8ad73c63e7..f6a0e91331 100644 --- a/src/client/driver/command-executors/client-functions/selector-executor/utils.ts +++ b/src/client/driver/command-executors/client-functions/selector-executor/utils.ts @@ -2,6 +2,9 @@ import adapter from '..//adapter/index'; export function visible (el: Node): boolean { + if (adapter.isIframeElement(el)) + return adapter.isIframeVisible(el); + if (!adapter.isDomElement(el) && !adapter.isTextNode(el)) return false; diff --git a/src/client/driver/command-executors/client-functions/types.d.ts b/src/client/driver/command-executors/client-functions/types.d.ts index 1cbfe1f426..bd267ff42e 100644 --- a/src/client/driver/command-executors/client-functions/types.d.ts +++ b/src/client/driver/command-executors/client-functions/types.d.ts @@ -70,5 +70,7 @@ export interface ClientFunctionAdapter { getTagName (el: Element): string; isOptionElementVisible (el: Node): boolean; isElementVisible (el: Node): boolean; + isIframeElement (el: Node): boolean; + isIframeVisible (el: Node): boolean; getActiveElement (): Node; } diff --git a/src/client/driver/driver-link/iframe/child.js b/src/client/driver/driver-link/iframe/child.js index 28054db2da..7cb69027dc 100644 --- a/src/client/driver/driver-link/iframe/child.js +++ b/src/client/driver/driver-link/iframe/child.js @@ -44,7 +44,7 @@ export default class ChildIframeDriverLink { if (!domUtils.isElementInDocument(this.driverIframe)) return Promise.reject(new CurrentIframeNotFoundError()); - return waitFor(() => positionUtils.isElementVisible(this.driverIframe) ? this.driverIframe : null, + return waitFor(() => positionUtils.isIframeVisible(this.driverIframe) ? this.driverIframe : null, CHECK_IFRAME_VISIBLE_INTERVAL, this.iframeAvailabilityTimeout) .catch(() => { throw new CurrentIframeIsInvisibleError(); diff --git a/src/client/driver/driver.js b/src/client/driver/driver.js index 81bfd7bde9..c4ff529311 100644 --- a/src/client/driver/driver.js +++ b/src/client/driver/driver.js @@ -102,7 +102,11 @@ import ChildWindowDriverLink from './driver-link/window/child'; import ParentWindowDriverLink from './driver-link/window/parent'; import sendConfirmationMessage from './driver-link/send-confirmation-message'; import DriverRole from './role'; -import { CHECK_CHILD_WINDOW_CLOSED_INTERVAL, WAIT_FOR_WINDOW_DRIVER_RESPONSE_TIMEOUT } from './driver-link/timeouts'; +import { + CHECK_CHILD_WINDOW_CLOSED_INTERVAL, + WAIT_FOR_IFRAME_DRIVER_RESPONSE_TIMEOUT, + WAIT_FOR_WINDOW_DRIVER_RESPONSE_TIMEOUT, +} from './driver-link/timeouts'; import sendMessageToDriver from './driver-link/send-message-to-driver'; import getExecutorResultDriverStatus from './command-executors/get-executor-result-driver-status'; import SelectorExecutor from './command-executors/client-functions/selector-executor'; @@ -961,8 +965,14 @@ export default class Driver extends serviceUtils.EventEmitter { if (!domUtils.isIframeElement(iframe)) throw new ActionElementNotIframeError(); + // NOTE: RG-4558 Previously we waited for iframe become visible when execute selector + // We need to add a timeout to be sure that iframe driver is initialized + const childLinkResponseTimeout = hasSpecificTimeout + ? commandSelectorTimeout + : Math.max(commandSelectorTimeout, WAIT_FOR_IFRAME_DRIVER_RESPONSE_TIMEOUT); + return this._ensureChildIframeDriverLink(nativeMethods.contentWindowGetter.call(iframe), - iframeErrorCtors.NotLoadedError, commandSelectorTimeout); + iframeErrorCtors.NotLoadedError, childLinkResponseTimeout); }) .then(childDriverLink => { childDriverLink.availabilityTimeout = commandSelectorTimeout; diff --git a/src/client/proxyless/client-fn-adapter-initializer.ts b/src/client/proxyless/client-fn-adapter-initializer.ts index f9939c46f7..92dd22f153 100644 --- a/src/client/proxyless/client-fn-adapter-initializer.ts +++ b/src/client/proxyless/client-fn-adapter-initializer.ts @@ -2,7 +2,7 @@ import { ClientFunctionAdapter } from '../driver/command-executors/client-functi import nativeMethods from './native-methods'; import * as domUtils from './utils/dom'; import * as styleUtils from './utils/style'; -import { isElementVisible } from '../core/utils/shared/visibility'; +import { isElementVisible, isIframeVisible } from '../core/utils/shared/visibility'; const initializer: ClientFunctionAdapter = { @@ -19,6 +19,8 @@ const initializer: ClientFunctionAdapter = { isOptionElement: domUtils.isOptionElement, getTagName: domUtils.getTagName, getActiveElement: domUtils.getActiveElement, + isIframeVisible: isIframeVisible, + isIframeElement: domUtils.isIframeElement, isOptionElementVisible: styleUtils.isOptionElementVisible, isElementVisible: isElementVisible, diff --git a/src/client/proxyless/utils/dom.ts b/src/client/proxyless/utils/dom.ts index 3ed9a48bdf..a26cd2e173 100644 --- a/src/client/proxyless/utils/dom.ts +++ b/src/client/proxyless/utils/dom.ts @@ -214,6 +214,10 @@ export function isElementInIframe (el: Element | Document, currentDocument?: Doc return window.document !== doc; } +export function isIframeElement (el: Element): boolean { + return el instanceof HTMLIFrameElement; +} + export function isWindow (el: Node | Window | Document): el is Window { return 'pageYOffset' in el; } diff --git a/test/functional/fixtures/api/es-next/iframe-switching/pages/index.html b/test/functional/fixtures/api/es-next/iframe-switching/pages/index.html index 49a32e6b89..828ff131b5 100644 --- a/test/functional/fixtures/api/es-next/iframe-switching/pages/index.html +++ b/test/functional/fixtures/api/es-next/iframe-switching/pages/index.html @@ -13,7 +13,7 @@

Main page

Same-domain iframes

- + + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4558/pages/index.html b/test/functional/fixtures/regression/gh-4558/pages/index.html new file mode 100644 index 0000000000..366ed2c1dc --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/index.html @@ -0,0 +1,21 @@ + + + + + Title + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html b/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html new file mode 100644 index 0000000000..4bcc0fd225 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html @@ -0,0 +1,10 @@ + + + + + Title + + +OK + + diff --git a/test/functional/fixtures/regression/gh-4558/test-data/data.js b/test/functional/fixtures/regression/gh-4558/test-data/data.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/functional/fixtures/regression/gh-4558/test.js b/test/functional/fixtures/regression/gh-4558/test.js new file mode 100644 index 0000000000..500070844e --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/test.js @@ -0,0 +1,46 @@ +const expect = require('chai').expect; + +describe('[Regression](GH-4558)', () => { + it('Should fail on click an element in invisible iframe', () => { + return runTests('./testcafe-fixtures/index.js', 'Button click', { skip: ['ie'], shouldFail: true }) + .catch(err => { + expect(err[0]).contains('The element that matches the specified selector is not visible.'); + }); + }); + + it('Should press key in iframe document', () => { + return runTests('./testcafe-fixtures/index.js', 'Press key', { skip: ['ie'] }); + }); + + it('Set files to upload and clear upload', () => { + return runTests('./testcafe-fixtures/index.js', 'Set files to upload and clear upload', { skip: ['ie'] }); + }); + + it('Dispatch a Click event', () => { + return runTests('./testcafe-fixtures/index.js', 'Dispatch a Click event', { skip: ['ie'] }); + }); + + it('Eval', () => { + return runTests('./testcafe-fixtures/index.js', 'Eval', { skip: ['ie'] }); + }); + + it('Set native dialog handler and get common dialog history', () => { + return runTests('./testcafe-fixtures/index.js', 'Set native dialog handler and get common dialog history', { skip: ['ie'] }); + }); + + it('Get browser console messages', () => { + return runTests('./testcafe-fixtures/index.js', 'Get browser console messages', { skip: ['ie'] }); + }); + + it('Switch to inner iframe', () => { + return runTests('./testcafe-fixtures/index.js', 'Switch to inner iframe', { skip: ['ie'] }); + }); + + it('Hidden by visibility style', () => { + return runTests('./testcafe-fixtures/index.js', 'Hidden by visibility style', { skip: ['ie'], shouldFail: true }) + .catch(err => { + expect(err[0]).contains('The element that matches the specified selector is not visible.'); + }); + }); +}); + diff --git a/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js new file mode 100644 index 0000000000..96fcadd79f --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js @@ -0,0 +1,115 @@ +import { Selector, ClientFunction } from 'testcafe'; + +fixture(`RG-4558 - Invisible iframe`) + .page(`http://localhost:3000/fixtures/regression/gh-4558/pages/index.html`); + +async function expectResultText (t, text = 'OK') { + await t.expect(Selector('#result').innerText).eql(text); +} + +async function setNativeDialogHandler (t) { + await t + .setNativeDialogHandler((type, text) => { + switch (type) { + case 'confirm': + return text === 'confirm'; + case 'prompt': + return 'PROMPT'; + default: + return true; + } + }); +} + +const iframeSelector = Selector('#invisibleIframe'); + +const focusDocument = ClientFunction(() => { + document.getElementById('focusInput').focus(); +}); + +test('Button click', async t => { + await t.switchToIframe(iframeSelector); + await t.click(Selector('#button', { timeout: 200 })); + + throw new Error('Test rejection expected'); +}); + +test('Press key', async t => { + await t.switchToIframe(iframeSelector); + await focusDocument(); + await t.pressKey('a'); + await expectResultText(t); +}); + +test('Set files to upload and clear upload', async t => { + await t.switchToIframe(iframeSelector); + await t.setFilesToUpload('#upload', '../test-data/data.js'); + await expectResultText(t, 'ADD'); + await t.clearUpload('#upload'); + await expectResultText(t, 'CLEAR'); +}); + + +test('Dispatch a Click event', async t => { + await t.switchToIframe(iframeSelector); + + const eventArgs = { + cancelable: false, + bubbles: false, + }; + + const options = Object.assign( + { eventConstructor: 'MouseEvent' }, + eventArgs, + ); + + await t + .dispatchEvent('#button', 'click', options); + + await expectResultText(t); +}); + +test('Eval', async t => { + await t.switchToIframe(iframeSelector); + await t.eval(() => window.setSpanText()); + await expectResultText(t); +}); + +test('Set native dialog handler and get common dialog history', async t => { + await t.switchToIframe(iframeSelector); + await setNativeDialogHandler(t); + await t.eval(() => window.showDialog('confirm')); + await expectResultText(t, 'CONFIRM'); + await t.eval(() => window.showDialog('prompt')); + await expectResultText(t, 'PROMPT'); + await t.switchToMainWindow(); + await t.eval(() => window.showNativeDialog('alert')); + + const history = await t.getNativeDialogHistory(); + + await t.expect(history[0].url).contains('index.html'); + await t.expect(history[1].url).contains('iframePage.html'); + await t.expect(history[2].url).contains('iframePage.html'); +}); + +test('Get browser console messages', async t => { + await t.switchToIframe(iframeSelector); + await t.eval(() => window.logToConsole('console-test')); + const browserConsoleMessages = await t.getBrowserConsoleMessages(); + + await t.expect(browserConsoleMessages.log).contains('console-test'); +}); + +test('Switch to inner iframe', async t => { + await t.switchToIframe(iframeSelector); + await t.switchToIframe(Selector('#iframe2')); + await expectResultText(t); +}); + + +test('Hidden by visibility style', async t => { + await t.switchToIframe('#hiddenIframe'); + + throw new Error('Test rejection expected'); +}); +