Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow switching to an invisible iframe(closes #4558) #7020

Merged
merged 6 commits into from May 31, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';
AndreyBelym marked this conversation as resolved.
Show resolved Hide resolved
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' && adapter.style.get(el, 'display') !== 'none';
}

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