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 744520a751..d501f3035a 100644 --- a/src/client/core/utils/shared/visibility.ts +++ b/src/client/core/utils/shared/visibility.ts @@ -1,6 +1,9 @@ import adapter from './adapter/index'; import { isNotVisibleNode, hasDimensions } from './style'; +export function isIframeVisible (el: Node): boolean { + return adapter.style.get(el, 'visibility') !== 'hidden'; +} export function isElementVisible (el: Node): boolean { if (adapter.dom.isTextNode(el)) 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/proxyless/client-fn-adapter-initializer.ts b/src/client/proxyless/client-fn-adapter-initializer.ts index f9939c46f7..611ae83f64 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.isElementInIframe, isOptionElementVisible: styleUtils.isOptionElementVisible, isElementVisible: isElementVisible, diff --git a/test/functional/fixtures/regression/gh-4558/pages/iframePage.html b/test/functional/fixtures/regression/gh-4558/pages/iframePage.html new file mode 100644 index 0000000000..8ab5f22c3c --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/iframePage.html @@ -0,0 +1,49 @@ + + + + + Title + + + + + + + + + + + + + + + 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..6efaad2c3b --- /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(Selector(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'); +}); +