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');
+});
+