diff --git a/src/client/automation/move.ts b/src/client/automation/move.ts index a0375dc159e..dd77c2ee47e 100644 --- a/src/client/automation/move.ts +++ b/src/client/automation/move.ts @@ -217,7 +217,9 @@ export default class MoveAutomation { const events: any[] = []; await mouseMoveStep(mouseMoveOptions, nativeMethods.dateNow, async currPosition => { - events.push(this.proxylessInput?.createMouseMoveEvent(currPosition)); + const moveEvent = await this.proxylessInput?.createMouseMoveEvent(currPosition); + + events.push(moveEvent); return nextTick(); }); diff --git a/src/client/automation/playback/click/index.ts b/src/client/automation/playback/click/index.ts index 87e356edb98..04b1782b517 100644 --- a/src/client/automation/playback/click/index.ts +++ b/src/client/automation/playback/click/index.ts @@ -6,7 +6,6 @@ import delay from '../../../core/utils/delay'; import { utils, Promise } from '../../deps/hammerhead'; import { createMouseClickStrategy, MouseClickStrategy } from './browser-click-strategy'; import ProxylessInput from '../../../../proxyless/client/input'; -import AxisValues from '../../../core/utils/values/axis-values'; import { setCaretPosition } from '../../utils/utils'; import { DispatchEventFn } from '../../../../proxyless/client/types'; @@ -23,8 +22,8 @@ export default class ClickAutomation extends VisibleElementAutomation { private modifiers: Modifiers; public strategy: MouseClickStrategy; - protected constructor (element: HTMLElement, clickOptions: ClickOptions, win: Window, cursor: Cursor, dispatchProxylessEventFn?: DispatchEventFn, leftTopPoint?: AxisValues) { - super(element, clickOptions, win, cursor, dispatchProxylessEventFn, leftTopPoint); + protected constructor (element: HTMLElement, clickOptions: ClickOptions, win: Window, cursor: Cursor, dispatchProxylessEventFn?: DispatchEventFn) { + super(element, clickOptions, win, cursor, dispatchProxylessEventFn); this.modifiers = clickOptions.modifiers; this.strategy = createMouseClickStrategy(this.element, clickOptions.caretPos); diff --git a/src/client/automation/playback/hover.js b/src/client/automation/playback/hover.js index b5eeabd01ab..30a15550f4f 100644 --- a/src/client/automation/playback/hover.js +++ b/src/client/automation/playback/hover.js @@ -3,8 +3,8 @@ import cursor from '../cursor'; export default class HoverAutomation extends VisibleElementAutomation { - constructor (element, hoverOptions, dispatchProxylessEventFn, leftTopPoint) { - super(element, hoverOptions, window, cursor, dispatchProxylessEventFn, leftTopPoint); + constructor (element, hoverOptions, dispatchProxylessEventFn) { + super(element, hoverOptions, window, cursor, dispatchProxylessEventFn); } run (useStrictElementCheck) { diff --git a/src/client/automation/types.d.ts b/src/client/automation/types.d.ts index 5f5f852512a..a6f30db7b5f 100644 --- a/src/client/automation/types.d.ts +++ b/src/client/automation/types.d.ts @@ -1,10 +1,10 @@ import { ActionCommandBase } from '../../test-run/commands/base'; import EventEmitter from '../core/utils/event-emitter'; -import AxisValues, { AxisValuesData } from '../core/utils/values/axis-values'; +import { AxisValuesData } from '../core/utils/values/axis-values'; import { DispatchEventFn } from '../../proxyless/client/types'; export interface AutomationHandler { - create: (cmd: ActionCommandBase, elements: any[], dispatchProxylessEventFn?: DispatchEventFn, leftTopPoint?: AxisValues) => Automation; + create: (cmd: ActionCommandBase, elements: any[], dispatchProxylessEventFn?: DispatchEventFn) => Automation; ensureElsProps?: (elements: any[]) => void; ensureCmdArgs?: (cmd: ActionCommandBase) => void; additionalSelectorProps?: string[]; diff --git a/src/client/automation/visible-element-automation.ts b/src/client/automation/visible-element-automation.ts index 699a9236bdc..04f0869dcf5 100644 --- a/src/client/automation/visible-element-automation.ts +++ b/src/client/automation/visible-element-automation.ts @@ -101,7 +101,7 @@ export default class VisibleElementAutomation extends SharedEventEmitter { protected readonly options: OffsetOptions; protected readonly proxylessInput: ProxylessInput | null; - protected constructor (element: HTMLElement, offsetOptions: OffsetOptions, win: Window, cursor: Cursor, dispatchProxylessEventFn?: DispatchEventFn, topLeftPoint?: AxisValues) { + protected constructor (element: HTMLElement, offsetOptions: OffsetOptions, win: Window, cursor: Cursor, dispatchProxylessEventFn?: DispatchEventFn) { super(); this.TARGET_ELEMENT_FOUND_EVENT = 'automation|target-element-found-event'; @@ -114,7 +114,7 @@ export default class VisibleElementAutomation extends SharedEventEmitter { this.window = win; this.cursor = cursor; - this.proxylessInput = dispatchProxylessEventFn ? new ProxylessInput(dispatchProxylessEventFn, topLeftPoint) : null; + this.proxylessInput = dispatchProxylessEventFn ? new ProxylessInput(dispatchProxylessEventFn) : null; // NOTE: only for legacy API this._ensureWindowAndCursorForLegacyTests(this); diff --git a/src/client/driver/command-executors/action-executor/actions-initializer.ts b/src/client/driver/command-executors/action-executor/actions-initializer.ts index 3d72cadf49f..a5f29b38c92 100644 --- a/src/client/driver/command-executors/action-executor/actions-initializer.ts +++ b/src/client/driver/command-executors/action-executor/actions-initializer.ts @@ -39,7 +39,6 @@ import { import COMMAND_TYPE from '../../../../test-run/commands/type'; import { ActionCommandBase } from '../../../../test-run/commands/base'; import { Automation } from '../../../automation/types'; -import AxisValues from '../../../core/utils/values/axis-values'; import { DispatchEventFn } from '../../../../proxyless/client/types'; @@ -66,11 +65,11 @@ ActionExecutor.ACTIONS_HANDLERS[COMMAND_TYPE.pressKey] = { }; ActionExecutor.ACTIONS_HANDLERS[COMMAND_TYPE.click] = { - create: (command, elements, dispatchProxylessEventFn?: DispatchEventFn, leftTopPoint?: AxisValues) => { + create: (command, elements, dispatchProxylessEventFn?: DispatchEventFn) => { if (/option|optgroup/.test(domUtils.getTagName(elements[0]))) return new SelectChildClickAutomation(elements[0], command.options); - return new ClickAutomation(elements[0], command.options, window, cursor, dispatchProxylessEventFn, leftTopPoint); + return new ClickAutomation(elements[0], command.options, window, cursor, dispatchProxylessEventFn); }, }; @@ -83,8 +82,8 @@ ActionExecutor.ACTIONS_HANDLERS[COMMAND_TYPE.doubleClick] = { }; ActionExecutor.ACTIONS_HANDLERS[COMMAND_TYPE.hover] = { - create: (command, elements, dispatchProxylessEventFn?: DispatchEventFn, leftTopPoint?: AxisValues) => { - return new HoverAutomation(elements[0], command.options, dispatchProxylessEventFn, leftTopPoint); + create: (command, elements, dispatchProxylessEventFn?: DispatchEventFn) => { + return new HoverAutomation(elements[0], command.options, dispatchProxylessEventFn); }, }; diff --git a/src/client/driver/command-executors/action-executor/index.ts b/src/client/driver/command-executors/action-executor/index.ts index d20a6abaa31..3c7e967108a 100644 --- a/src/client/driver/command-executors/action-executor/index.ts +++ b/src/client/driver/command-executors/action-executor/index.ts @@ -11,7 +11,6 @@ import { Automation, AutomationHandler } from '../../../automation/types'; import { nativeMethods, Promise } from '../../deps/hammerhead'; import { getOffsetOptions } from '../../../core/utils/offsets'; import { TEST_RUN_ERRORS } from '../../../../errors/types'; -import AxisValues from '../../../core/utils/values/axis-values'; import { DispatchEventFn } from '../../../../proxyless/client/types'; const MAX_DELAY_AFTER_EXECUTION = 2000; @@ -22,7 +21,6 @@ interface ActionExecutorOptions { testSpeed: number; executeSelectorFn: ExecuteSelectorFn; dispatchProxylessEventFn: DispatchEventFn; - leftTopPoint: AxisValues; } export default class ActionExecutor extends EventEmitter { @@ -140,7 +138,7 @@ export default class ActionExecutor extends EventEmitter { if (!handler) throw new Error(`There is no handler for the "${this._command.type}" command.`); - return handler.create(this._command, this._elements, this._options.dispatchProxylessEventFn, this._options.leftTopPoint); + return handler.create(this._command, this._elements, this._options.dispatchProxylessEventFn); } private _runAction (strictElementCheck: boolean): Promise { diff --git a/src/client/driver/driver-link/iframe/child.js b/src/client/driver/driver-link/iframe/child.js index f53c031e4cd..5365fb6b5b3 100644 --- a/src/client/driver/driver-link/iframe/child.js +++ b/src/client/driver/driver-link/iframe/child.js @@ -27,8 +27,6 @@ import { } from '../timeouts'; import sendConfirmationMessage from '../send-confirmation-message'; -import { getBordersWidthFloat, getElementPaddingFloat } from '../../../core/utils/style'; - export default class ChildIframeDriverLink { constructor (driverWindow, driverId, dispatchProxylessEventUrls) { @@ -92,20 +90,6 @@ export default class ChildIframeDriverLink { }); } - _getLeftTopPoint (proxyless) { - if (!proxyless) - return null; - - const rect = this.driverIframe.getBoundingClientRect(); - const borders = getBordersWidthFloat(this.driverIframe); - const paddings = getElementPaddingFloat(this.driverIframe); - - return { - x: rect.left + borders.left + paddings.left, - y: rect.top + borders.top + paddings.top, - }; - } - sendConfirmationMessage (requestMsgId) { sendConfirmationMessage({ requestMsgId, @@ -114,20 +98,13 @@ export default class ChildIframeDriverLink { }); } - executeCommand (command, testSpeed, proxyless, leftTopPoint) { + executeCommand (command, testSpeed) { // NOTE: We should check if the iframe is visible and exists before executing the next // command, because the iframe might be hidden or removed since the previous command. return this ._ensureIframe() .then(() => { - const currentLeftTopPoint = this._getLeftTopPoint(proxyless); - - if (leftTopPoint) { - currentLeftTopPoint.x += leftTopPoint.x; - currentLeftTopPoint.y += leftTopPoint.y; - } - - const msg = new ExecuteCommandMessage(command, testSpeed, currentLeftTopPoint); + const msg = new ExecuteCommandMessage(command, testSpeed); return Promise.all([ sendMessageToDriver(msg, this.driverWindow, this.iframeAvailabilityTimeout, CurrentIframeIsNotLoadedError), diff --git a/src/client/driver/driver-link/messages.js b/src/client/driver/driver-link/messages.js index e5569e02847..eed7752ebd2 100644 --- a/src/client/driver/driver-link/messages.js +++ b/src/client/driver/driver-link/messages.js @@ -84,12 +84,11 @@ export class CommandExecutedMessage extends InterDriverMessage { } export class ExecuteCommandMessage extends InterDriverMessage { - constructor (command, testSpeed, leftTopPoint) { + constructor (command, testSpeed) { super(TYPE.executeCommand); - this.command = command; - this.testSpeed = testSpeed; - this.leftTopPoint = leftTopPoint; + this.command = command; + this.testSpeed = testSpeed; } } diff --git a/src/client/driver/driver.js b/src/client/driver/driver.js index 0560396cb00..c79818a9724 100644 --- a/src/client/driver/driver.js +++ b/src/client/driver/driver.js @@ -941,7 +941,7 @@ export default class Driver extends serviceUtils.EventEmitter { .then(() => { this.contextStorage.setItem(this.EXECUTING_IN_IFRAME_FLAG, true); - return this.activeChildIframeDriverLink.executeCommand(command, this.speed, this.options.proxyless, this.leftTopPoint); + return this.activeChildIframeDriverLink.executeCommand(command, this.speed, this.options.proxyless); }) .then(status => this._onCommandExecutedInIframe(status)) .catch(err => this._onCommandExecutedInIframe(new DriverStatus({ @@ -1194,7 +1194,6 @@ export default class Driver extends serviceUtils.EventEmitter { globalSelectorTimeout: this.options.selectorTimeout, testSpeed: this.speed, executeSelectorFn: executeSelectorCb, - leftTopPoint: this.leftTopPoint, dispatchProxylessEventFn: this.createDispatchProxylessEventFunctions(), }); diff --git a/src/client/driver/iframe-driver.js b/src/client/driver/iframe-driver.js index 9588b16f22b..b0bb8b19362 100644 --- a/src/client/driver/iframe-driver.js +++ b/src/client/driver/iframe-driver.js @@ -17,7 +17,6 @@ import { StopInternalFromFrameMessage, TYPE as MESSAGE_TYPE, } from './driver-link/messages'; -import AxisValues from '../core/utils/values/axis-values'; const messageSandbox = eventSandbox.message; @@ -29,8 +28,6 @@ export default class IframeDriver extends Driver { this.parentDriverLink = new ParentIframeDriverLink(window.parent); this._initParentDriverListening(); - - this.leftTopPoint = new AxisValues(0, 0); } // Errors handling @@ -70,8 +67,7 @@ export default class IframeDriver extends Driver { this.lastParentDriverMessageId = msg.id; this.readyPromise.then(() => { - this.speed = msg.testSpeed; - this.leftTopPoint = msg.leftTopPoint; + this.speed = msg.testSpeed; this.parentDriverLink.sendConfirmationMessage(msg.id); this._onCommand(msg.command); diff --git a/src/proxyless/client/event-descriptor.ts b/src/proxyless/client/event-descriptor.ts index 3e2d4d328a7..f675add4111 100644 --- a/src/proxyless/client/event-descriptor.ts +++ b/src/proxyless/client/event-descriptor.ts @@ -1,15 +1,70 @@ import { EventType } from '../types'; import { SimulatedKeyInfo } from './key-press/utils'; // @ts-ignore -import { utils } from '../../client/core/deps/hammerhead'; +import { utils, eventSandbox } from '../../client/core/deps/hammerhead'; import { calculateKeyModifiersValue, calculateMouseButtonValue } from './utils'; -import AxisValues from '../../client/core/utils/values/axis-values'; +import { AxisValuesData } from '../../client/core/utils/values/axis-values'; +import sendRequestToFrame from '../../client/core/utils/send-request-to-frame'; +import { findIframeByWindow } from '../../client/core/utils/dom'; +import { getBordersWidthFloat, getElementPaddingFloat } from '../../client/core/utils/style'; + +const messageSandbox = eventSandbox.message; const MOUSE_EVENT_OPTIONS = { clickCount: 1, button: 'left', }; +const CALCULATE_TOP_LEFT_POINT_REQUEST_CMD = 'proxyless|calculate-top-left-point|request'; +const CALCULATE_TOP_LEFT_POINT_RESPONSE_CMD = 'proxyless|calculate-top-left-point|response'; + +function getLeftTopPoint (driverIframe: any): AxisValuesData { + const rect = driverIframe.getBoundingClientRect(); + const borders = getBordersWidthFloat(driverIframe); + const paddings = getElementPaddingFloat(driverIframe); + + return { + x: rect.left + borders.left + paddings.left, + y: rect.top + borders.top + paddings.top, + }; +} + +// Setup cross-iframe interaction +messageSandbox.on(messageSandbox.SERVICE_MSG_RECEIVED_EVENT, async (e:any) => { + if (e.message.cmd === CALCULATE_TOP_LEFT_POINT_REQUEST_CMD) { + const iframeWin = e.source; + + const { x, y } = await calculateIFrameTopLeftPoint(); + + const iframe = findIframeByWindow(iframeWin); + const topLeftPoint = getLeftTopPoint(iframe); + + const responseMsg = { + cmd: CALCULATE_TOP_LEFT_POINT_RESPONSE_CMD, + topLeftPoint: { + x: topLeftPoint.x + x, + y: topLeftPoint.y + y, + }, + }; + + messageSandbox.sendServiceMsg(responseMsg, iframeWin); + } +}); + +async function calculateIFrameTopLeftPoint (): Promise> { + if (window !== window.parent) { + const msg: any = { + cmd: CALCULATE_TOP_LEFT_POINT_REQUEST_CMD, + }; + + const { topLeftPoint } = await sendRequestToFrame(msg, CALCULATE_TOP_LEFT_POINT_RESPONSE_CMD, window.parent); + + return topLeftPoint; + } + + return { x: 0, y: 0 }; +} + export default class CDPEventDescriptor { private static _getKeyDownEventText (options: SimulatedKeyInfo): any { if (options.isNewLine) @@ -42,10 +97,12 @@ export default class CDPEventDescriptor { }; } - public static createMouseEventOptions (type: string, options: any, leftTopPoint: AxisValues): any { + public static async createMouseEventOptions (type: string, options: any): Promise { + const { x, y } = await calculateIFrameTopLeftPoint(); + return utils.extend({ - x: options.options.clientX + leftTopPoint.x, - y: options.options.clientY + leftTopPoint.y, + x: options.options.clientX + x, + y: options.options.clientY + y, modifiers: calculateKeyModifiersValue(options.options), button: calculateMouseButtonValue(options.options), type, diff --git a/src/proxyless/client/input.ts b/src/proxyless/client/input.ts index 560abe27a86..4e95ed246c6 100644 --- a/src/proxyless/client/input.ts +++ b/src/proxyless/client/input.ts @@ -1,26 +1,23 @@ import { EventType } from '../types'; -import AxisValues, { AxisValuesData } from '../../client/core/utils/values/axis-values'; +import { AxisValuesData } from '../../client/core/utils/values/axis-values'; import { SimulatedKeyInfo } from './key-press/utils'; import { DispatchEventFn } from './types'; import CDPEventDescriptor from './event-descriptor'; export default class ProxylessInput { - private readonly _dispatchEventFn: DispatchEventFn; - private readonly _leftTopPoint: AxisValues; - constructor (dispatchEventFn: DispatchEventFn, leftTopPoint?: AxisValues) { + constructor (dispatchEventFn: DispatchEventFn) { this._dispatchEventFn = dispatchEventFn; - this._leftTopPoint = leftTopPoint || new AxisValues(0, 0); } - public mouseDown (options: any): Promise { - const eventOptions = CDPEventDescriptor.createMouseEventOptions('mousePressed', options, this._leftTopPoint); + public async mouseDown (options: any): Promise { + const eventOptions = await CDPEventDescriptor.createMouseEventOptions('mousePressed', options); return this._dispatchEventFn.single(EventType.Mouse, eventOptions); } - public mouseUp (options: any): Promise { - const eventOptions = CDPEventDescriptor.createMouseEventOptions('mouseReleased', options, this._leftTopPoint); + public async mouseUp (options: any): Promise { + const eventOptions = await CDPEventDescriptor.createMouseEventOptions('mouseReleased', options); return this._dispatchEventFn.single(EventType.Mouse, eventOptions); } @@ -40,15 +37,14 @@ export default class ProxylessInput { return this._dispatchEventFn.sequence(eventSequence); } - public createMouseMoveEvent (currPosition: AxisValuesData): any { - const options = CDPEventDescriptor.createMouseEventOptions('mouseMoved', { + public async createMouseMoveEvent (currPosition: AxisValuesData): Promise { + const options = await CDPEventDescriptor.createMouseEventOptions('mouseMoved', { options: { clientX: currPosition.x, clientY: currPosition.y, button: 'none', }, - // @ts-ignore - }, this._leftTopPoint); + }); return { type: EventType.Mouse, diff --git a/test/functional/fixtures/regression/gh-7557/pages/frame.html b/test/functional/fixtures/regression/gh-7557/pages/frame.html new file mode 100644 index 00000000000..a564ed7447e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/frame.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/pages/index.html b/test/functional/fixtures/regression/gh-7557/pages/index.html new file mode 100644 index 00000000000..00206503b56 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html b/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html new file mode 100644 index 00000000000..802c5ddd4db --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/test.js b/test/functional/fixtures/regression/gh-7557/test.js new file mode 100644 index 00000000000..02e47d6e5aa --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/test.js @@ -0,0 +1,9 @@ +const { onlyInProxyless } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7557)', function () { + onlyInProxyless('Should consider document scroll in CDP clicking', function () { + return runTests('./testcafe-fixtures/index.js', null, { only: 'chrome' }); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js new file mode 100644 index 00000000000..629dbea375e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js @@ -0,0 +1,24 @@ +fixture `Should consider document scroll in CDP clicking` + .page `http://localhost:3000/fixtures/regression/gh-7557/pages/index.html`; + +test('Should consider document scroll in CDP clicking', async t => { + await t.click('button'); + + const isClickedInParent = await t.eval(() => window.clickedInParent); + + await t.expect(isClickedInParent).eql(true); + + await t.switchToIframe('iframe'); + await t.click('button'); + + const isClickedInChild = await t.eval(() => window.clickedInChild); + + await t.expect(isClickedInChild).eql(true); + + await t.switchToIframe('iframe'); + await t.click('button'); + + const isClickedInNestedChild = await t.eval(() => window.clickedInNestedChild); + + await t.expect(isClickedInNestedChild).eql(true); +});