Skip to content

Commit

Permalink
Allow switching to an invisible iframe(closes DevExpress#4558)
Browse files Browse the repository at this point in the history
  • Loading branch information
Artem-Babich committed May 10, 2022
1 parent 40a2462 commit f3c89e2
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 3 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
3 changes: 3 additions & 0 deletions 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))
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
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.isElementInIframe,

isOptionElementVisible: styleUtils.isOptionElementVisible,
isElementVisible: isElementVisible,
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(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');
});

0 comments on commit f3c89e2

Please sign in to comment.