Skip to content

Commit

Permalink
refactor: switching to an invisible iframe(closes #4558) (#7020)
Browse files Browse the repository at this point in the history
* Allow switching to an invisible iframe(closes #4558)

* add display none check and fix tests

* add isIframeElement to proxyless

* increase driver connection timeout

* add note to timeout
  • Loading branch information
Artem-Babich committed May 31, 2022
1 parent 4bb4066 commit 70f753d
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/client/core/utils/position.js
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions 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';
Expand Down
Expand Up @@ -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,
};

Expand Down
Expand Up @@ -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;

Expand Down
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/client/driver/driver-link/iframe/child.js
Expand Up @@ -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();
Expand Down
14 changes: 12 additions & 2 deletions src/client/driver/driver.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/client/proxyless/client-fn-adapter-initializer.ts
Expand Up @@ -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 = {
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/client/proxyless/utils/dom.ts
Expand Up @@ -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;
}
Expand Down
Expand Up @@ -13,7 +13,7 @@ <h1>Main page</h1>
<h2>Same-domain iframes</h2>

<iframe id="iframe" src="iframe.html" width="500px" height="300px"></iframe>
<iframe id="invisible-iframe" style="width: 0; height: 0; border: 0"></iframe>
<iframe id="invisible-iframe" style="visibility: hidden;"></iframe>
<iframe name="long" id="slowly-loading-iframe" src="iframe.html?delay=1000" height="300px"></iframe>

<script>
Expand Down
49 changes: 49 additions & 0 deletions test/functional/fixtures/regression/gh-4558/pages/iframePage.html
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function setSpanText (text = 'OK') {
document.querySelector('#result').innerHTML = text;
}

function showDialog (type) {
switch (type) {
case 'confirm':
return setSpanText(confirm('confirm') ? 'CONFIRM' : 'FAIL');
case 'prompt':
return setSpanText(prompt('test'));
case 'alert':
return alert('test');
}
}

function logToConsole (text){
console.log(text);
}

document.addEventListener('keydown', function () {
setSpanText();
})

function onUploadChange () {
const files = document.getElementById('upload').files;
setSpanText(files && files.length ? 'ADD' : 'CLEAR');
}

</script>
</head>
<body style="overflow-y: scroll; height: 2000px;">

<input id="focusInput"/>

<input id="upload" type="file" onchange="onUploadChange()"/>

<span id="result"></span>

<button id="button" onclick="setSpanText()">OK</button>

<iframe id="iframe2" src="innerIframePage.html" style="width: 0px; height: 0px"></iframe>
</body>
</html>
21 changes: 21 additions & 0 deletions test/functional/fixtures/regression/gh-4558/pages/index.html
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
iframe {
border: none !important;
}
</style>
<script>
function showNativeDialog(text){
alert(text)
}
</script>
</head>
<body>
<iframe id="invisibleIframe" src="iframePage.html" style="width: 0px; height: 0px;"></iframe>
<iframe id="hiddenIframe" src="iframePage.html" style="visibility: hidden"></iframe>
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<span id="result">OK</span>
</body>
</html>
Empty file.
46 changes: 46 additions & 0 deletions 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.');
});
});
});

115 changes: 115 additions & 0 deletions 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');
});

0 comments on commit 70f753d

Please sign in to comment.