diff --git a/README.md b/README.md
index 5985110..29a6db9 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,7 @@ If you are using typescript, also add the following to `cypress/tsconfig.json`
## API
-The idea of the commands – they should be as similar as possible to cypress default commands (like `cy.type`), but starts with `real` – `cy.realType`.
+The idea of the commands – they should be as similar as possible to cypress default commands (like `cy.type`), but starts with `real` – `cy.realType`.
Here is an overview of the available **real** event commands:
- [cy.realClick](#cyrealclick)
@@ -90,7 +90,7 @@ cy.get("button").realClick();
cy.get("button").realClick(options);
```
-Example:
+Example:
```js
cy.get("button").realClick({ position: "topLeft" }) // click on the top left corner of button
@@ -101,11 +101,11 @@ Options:
- `Optional` **button**: \"none\" \| \"left\" \| \"right\" \| \"middle\" \| \"back\" \| \"forward\"
- `Optional` **pointer**: \"mouse\" \| \"pen\"
-- `Optional` x coordinate to click **x**: number
+- `Optional` x coordinate to click **x**: number
- `Optional` y coordinate to click **y**: number
- `Optional` **position**: "topLeft" | "top" | "topRight" | "left" | "center" | "right" | "bottomLeft" | "bottom" | "bottomRight"
-> Make sure that `x` and `y` has a bigger priority than `position`.
+> Make sure that `x` and `y` has a bigger priority than `position`.
## cy.realHover
@@ -124,7 +124,7 @@ Options:
## cy.realPress
Fires native press event. It can fire one key event or the "shortcut" like Shift+Control+M.
-Make sure that event is global, it means that it is required to **firstly** focus any control before firing this event.
+Make sure that event is global, it means that it is required to **firstly** focus any control before firing this event.
```jsx
cy.realPress("Tab"); // switch the focus for a11y testing
@@ -154,7 +154,7 @@ cy.get("button").realTouch();
cy.get("button").realTouch(options);
```
-##### Usage:
+##### Usage:
```js
cy.get("button").realTouch({ position: "topLeft" }) // touches the top left corner of button
@@ -166,6 +166,9 @@ Options:
- `Optional` **x**: undefined \| number **`default`** 30
- `Optional` **y**: undefined \| false \| true **`default`** true
- `Optional` **position**: "topLeft" | "top" | "topRight" | "left" | "center" | "right" | "bottomLeft" | "bottom" | "bottomRight"
+- `Optional` **radius**: undefined \| number **`default`** 1
+- `Optional` **radiusX**: undefined \| number **`default`** 1
+- `Optional` **radiusY**: undefined \| number **`default`** 1
### cy.realType
@@ -204,7 +207,7 @@ Options:
Runs a native swipe events. It means that **touch events** will be fired. Actually a sequence of `touchStart` -> `touchMove` -> `touchEnd`. It can perfectly swipe drawers and other tools [like this one](https://csb-dhe0i-qj8xxmx8y.vercel.app/).
-> Make sure to enable mobile viewport :)
+> Make sure to enable mobile viewport :)
```js
@@ -229,7 +232,7 @@ cy.realType(direction, options);
Options:
- `Optional` **length**: undefined \| number **`default`** 10
-- `Optional` x coordinate to touch **x**: number
+- `Optional` x coordinate to touch **x**: number
- `Optional` y coordinate to touch **y**: number
- `Optional` **touchPosition**: "topLeft" | "top" | "topRight" | "left" | "center" | "right" | "bottomLeft" | "bottom" | "bottomRight"
diff --git a/cypress/fixtures/frame-one.html b/cypress/fixtures/frame-one.html
new file mode 100644
index 0000000..e8ddb2f
--- /dev/null
+++ b/cypress/fixtures/frame-one.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cypress/fixtures/frame-two.html b/cypress/fixtures/frame-two.html
new file mode 100644
index 0000000..ec56d21
--- /dev/null
+++ b/cypress/fixtures/frame-two.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cypress/fixtures/iframe-page.html b/cypress/fixtures/iframe-page.html
new file mode 100644
index 0000000..e0e93fc
--- /dev/null
+++ b/cypress/fixtures/iframe-page.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/cypress/integration/click.spec.ts b/cypress/integration/click.spec.ts
index 7f14b67..a5851a5 100644
--- a/cypress/integration/click.spec.ts
+++ b/cypress/integration/click.spec.ts
@@ -33,10 +33,146 @@ describe("cy.realClick", () => {
.realClick({ x: 100, y: 185 })
.realClick({ x: 125, y: 190 })
.realClick({ x: 150, y: 185 })
- .realClick({ x: 170, y: 165 } )
+ .realClick({ x: 170, y: 165 });
});
it("opens system native event on right click", () => {
cy.get(".action-btn").realClick({ button: "right" });
});
+
+ describe("scroll behavior", () => {
+ function getScreenEdges() {
+ const cypressAppWindow = window.parent.document.querySelector("iframe")
+ .contentWindow;
+ const windowTopEdge = cypressAppWindow.document.documentElement.scrollTop;
+ const windowBottomEdge = windowTopEdge + cypressAppWindow.innerHeight;
+ const windowCenter = windowTopEdge + cypressAppWindow.innerHeight / 2;
+
+ return {
+ top: windowTopEdge,
+ bottom: windowBottomEdge,
+ center: windowCenter,
+ };
+ }
+
+ function getElementEdges($el: JQuery) {
+ const $elTop = $el.offset().top;
+
+ return {
+ top: $elTop,
+ bottom: $elTop + $el.outerHeight(),
+ };
+ }
+
+ beforeEach(() => {
+ cy.window().scrollTo("top");
+ });
+
+ it("defaults to scrolling the element to the top of the viewport", () => {
+ cy.get("#action-canvas")
+ .realClick()
+ .then(($canvas: JQuery) => {
+ const { top: $elTop } = getElementEdges($canvas);
+ const { top: screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+ });
+
+ it("scrolls the element to center of viewport", () => {
+ cy.get("#action-canvas")
+ .realClick({ scrollBehavior: "center" })
+ .then(($canvas: JQuery) => {
+ const { top: $elTop, bottom: $elBottom } = getElementEdges($canvas);
+ const { top: screenTop, bottom: screenBottom } = getScreenEdges();
+
+ const screenCenter = screenTop + (screenBottom - screenTop) / 2;
+
+ expect($elTop).to.equal(screenCenter - $canvas.outerHeight() / 2);
+ expect($elBottom).to.equal(screenCenter + $canvas.outerHeight() / 2);
+ });
+ });
+
+ it("scrolls the element to the top of the viewport", () => {
+ cy.get("#action-canvas")
+ .realClick({ scrollBehavior: "top" })
+ .then(($canvas: JQuery) => {
+ const { top: $elTop } = getElementEdges($canvas);
+ const { top: screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+ });
+
+ it("scrolls the element to the bottom of the viewport", () => {
+ cy.get("#action-canvas")
+ .realClick({ scrollBehavior: "bottom" })
+ .then(($canvas: JQuery) => {
+ const { bottom: $elBottom } = getElementEdges($canvas);
+ const { bottom: screenBottom } = getScreenEdges();
+
+ expect($elBottom).to.equal(screenBottom);
+ });
+ });
+
+ it("scrolls the element to the nearest edge of the viewport", () => {
+ cy.window().scrollTo("bottom");
+
+ cy.get("#action-canvas")
+ .realClick({ scrollBehavior: "nearest" })
+ .then(($canvas: JQuery) => {
+ const { top: $elTop } = getElementEdges($canvas);
+ const { top: screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+
+ cy.window().scrollTo("top");
+
+ cy.get("#action-canvas")
+ .realClick({ scrollBehavior: "nearest" })
+ .then(($canvas: JQuery) => {
+ const { bottom: $elBottom } = getElementEdges($canvas);
+ const { bottom: screenBottom } = getScreenEdges();
+
+ expect($elBottom).to.equal(screenBottom);
+ });
+ });
+ });
+});
+
+describe("iframe behavior", () => {
+ beforeEach(() => {
+ cy.visit("./cypress/fixtures/iframe-page.html");
+ });
+
+ it("clicks elements inside iframes", () => {
+ cy.get("iframe")
+ .then(($firstIframe) => {
+ return cy.wrap($firstIframe.contents().find("iframe"));
+ })
+ .then(($secondIframe) => {
+ return cy.wrap($secondIframe.contents().find("body"));
+ })
+ .within(() => {
+ cy.get("#target").contains("clicked").should("not.exist");
+ cy.get("#target").realClick().contains("clicked").should("exist");
+ });
+ });
+
+ it("clicks elements inside transformed iframes", () => {
+ cy.get("iframe")
+ .then(($firstIframe) => {
+ $firstIframe.css("transform", "scale(.5)");
+ return cy.wrap($firstIframe.contents().find("iframe"));
+ })
+ .then(($secondIframe) => {
+ $secondIframe.css("transform", "scale(.75)");
+ return cy.wrap($secondIframe.contents().find("body"));
+ })
+ .within(() => {
+ cy.get("#target").contains("clicked").should("not.exist");
+ cy.get("#target").realClick().contains("clicked").should("exist");
+ });
+ });
});
diff --git a/cypress/integration/hover.spec.ts b/cypress/integration/hover.spec.ts
index 43b9a0b..2848f25 100644
--- a/cypress/integration/hover.spec.ts
+++ b/cypress/integration/hover.spec.ts
@@ -9,4 +9,131 @@ describe("cy.realHover", () => {
.realHover()
.should("have.css", "background-color", "rgb(201, 48, 44)");
});
+
+ describe('scroll behavior', () => {
+ function getScreenEdges() {
+ const cypressAppWindow = window.parent.document.querySelector("iframe").contentWindow;
+ const windowTopEdge = cypressAppWindow.document.documentElement.scrollTop;
+ const windowBottomEdge = windowTopEdge + cypressAppWindow.innerHeight;
+ const windowCenter = windowTopEdge + (cypressAppWindow.innerHeight / 2);
+
+ return {
+ screenTop: windowTopEdge,
+ screenBottom: windowBottomEdge,
+ screenCenter: windowCenter,
+ };
+ }
+
+ function getElementEdges($el: JQuery) {
+ const $elTop = $el.offset().top;
+
+ return {
+ $elTop,
+ $elBottom: $elTop + $el.outerHeight()
+ }
+ }
+
+ beforeEach(() => {
+ cy.window().scrollTo('top');
+ });
+
+ it('defaults to scrolling the element to the top of the viewport', () => {
+ cy.get('#action-canvas').realHover().then(($canvas: JQuery) => {
+ const { $elTop } = getElementEdges($canvas);
+ const { screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+ });
+
+ it('scrolls the element to center of viewport', () => {
+ cy.get('#action-canvas').realHover({ scrollBehavior: 'center' }).then(($canvas: JQuery) => {
+ const { $elTop, $elBottom } = getElementEdges($canvas);
+ const { screenTop, screenBottom } = getScreenEdges();
+
+ const screenCenter = screenTop + (screenBottom - screenTop) / 2;
+
+ expect($elTop).to.equal(screenCenter - ($canvas.outerHeight() / 2));
+ expect($elBottom).to.equal(screenCenter + ($canvas.outerHeight() / 2));
+ });
+ });
+
+ it('scrolls the element to the top of the viewport', () => {
+ cy.get('#action-canvas').realHover({ scrollBehavior: 'top' }).then(($canvas: JQuery) => {
+ const { $elTop } = getElementEdges($canvas);
+ const { screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+ });
+
+ it('scrolls the element to the bottom of the viewport', () => {
+ cy.get('#action-canvas').realHover({ scrollBehavior: 'bottom' }).then(($canvas: JQuery) => {
+ const { $elBottom } = getElementEdges($canvas);
+ const { screenBottom } = getScreenEdges();
+
+ expect($elBottom).to.equal(screenBottom);
+ });
+ });
+
+ it('scrolls the element to the nearest edge of the viewport', () => {
+ cy.window().scrollTo('bottom');
+
+ cy.get('#action-canvas').realHover({ scrollBehavior: 'nearest' }).then(($canvas: JQuery) => {
+ const { $elTop } = getElementEdges($canvas);
+ const { screenTop } = getScreenEdges();
+
+ expect($elTop).to.equal(screenTop);
+ });
+
+ cy.window().scrollTo('top');
+
+ cy.get('#action-canvas').realHover({ scrollBehavior: 'nearest' }).then(($canvas: JQuery) => {
+ const { $elBottom } = getElementEdges($canvas);
+ const { screenBottom } = getScreenEdges();
+
+ expect($elBottom).to.equal(screenBottom);
+ });
+ });
+ });
+});
+
+describe('iframe behavior', () => {
+ beforeEach(() => {
+ cy.visit('./cypress/fixtures/iframe-page.html');
+ });
+
+ it('hovers elements inside iframes', () => {
+ cy.get('iframe').then(($firstIframe) => {
+ return cy.wrap($firstIframe.contents().find('iframe'));
+ }).then(($secondIframe) => {
+ return cy.wrap($secondIframe.contents().find('body'));
+ }).within(() => {
+ cy.get('#target').then(($target) => {
+ expect($target.css('background-color')).to.equal('rgb(0, 128, 0)');
+ });
+
+ cy.get('#target').realHover().then(($target) => {
+ expect($target.css('background-color')).to.equal('rgb(255, 192, 203)');
+ });
+ });
+ });
+
+ it('hovers elements inside transformed iframes', () => {
+ cy.get('iframe').then(($firstIframe) => {
+ $firstIframe.css('transform', 'scale(.5)');
+ return cy.wrap($firstIframe.contents().find('iframe'));
+ }).then(($secondIframe) => {
+ $secondIframe.css('transform', 'scale(.75)');
+ return cy.wrap($secondIframe.contents().find('body'));
+ }).within(() => {
+ cy.get('#target').then(($target) => {
+ expect($target.css('background-color')).to.equal('rgb(0, 128, 0)');
+ });
+
+ cy.get('#target').realHover().then(($target) => {
+ expect($target.css('background-color')).to.equal('rgb(255, 192, 203)');
+ });
+ });
+ });
});
diff --git a/cypress/integration/swipe.spec.ts b/cypress/integration/swipe.spec.ts
index c1418a4..3558dd7 100644
--- a/cypress/integration/swipe.spec.ts
+++ b/cypress/integration/swipe.spec.ts
@@ -30,7 +30,7 @@ describe("cy.realSwipe", () => {
touchPosition: "top",
},
] as const).forEach(({ button, swipe, length, touchPosition }) => {
- it(`swipes ${button} drawer ${swipe}`, () => {
+ it(`swipes ${button} drawer ${swipe}`, { retries: 4 }, () => {
cy.contains("button", button).click();
cy.get(".MuiDrawer-paper").realSwipe(swipe, { length, step: 10, touchPosition });
@@ -38,7 +38,7 @@ describe("cy.realSwipe", () => {
});
});
- it("opens drawer with swipe", () => {
+ it("opens drawer with swipe", { retries: 4 }, () => {
cy.get('.jss3.jss4').realSwipe("toRight", { length: 150, step: 10, touchPosition: "center" });
cy.get('.MuiDrawer-paper').realSwipe("toLeft", { length: 150, step: 10, touchPosition: "center" });
});
diff --git a/cypress/integration/touch.spec.ts b/cypress/integration/touch.spec.ts
index f79308a..3513060 100644
--- a/cypress/integration/touch.spec.ts
+++ b/cypress/integration/touch.spec.ts
@@ -35,4 +35,53 @@ describe("cy.realTouch", () => {
.realTouch({ x: 150, y: 185 })
.realTouch({ x: 170, y: 165 } )
});
-});
\ No newline at end of file
+
+ it("touches with a default radius of 1", (done) => {
+ cy.get(".action-btn")
+ .then(($button) => {
+ $button.get(0).addEventListener("pointerdown", (event) => {
+ expect(event.width).to.equal(2);
+ expect(event.height).to.equal(2);
+ done();
+ });
+ })
+ .realTouch();
+ });
+
+ it("touches with a custom radius", (done) => {
+ cy.get(".action-btn")
+ .then(($button) => {
+ $button.get(0).addEventListener("pointerdown", (event) => {
+ expect(event.width).to.equal(20);
+ expect(event.height).to.equal(20);
+ done();
+ });
+ })
+ .realTouch({ radius: 10 });
+ });
+
+ it("touches with a custom radius for each axis", (done) => {
+ cy.get(".action-btn")
+ .then(($button) => {
+ $button.get(0).addEventListener("pointerdown", (event) => {
+ expect(event.width).to.equal(10);
+ expect(event.height).to.equal(14);
+ done();
+ });
+ })
+ .realTouch({ radiusX: 5, radiusY: 7 });
+ });
+
+ it("touches using provided 0 for one of the axis", (done) => {
+ cy.get(".action-btn")
+ .then(($button) => {
+ $button.get(0).addEventListener("pointerdown", (event) => {
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
+ expect(event.clientX).to.be.closeTo(rect.left - 5, 5);
+ expect(event.clientY).to.be.closeTo(rect.top, 5);
+ done();
+ });
+ })
+ .realTouch({ x: -5, y: 0, radius: 10 });
+ });
+});
diff --git a/package.json b/package.json
index b8ed1c6..48121fd 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.8.2",
"@typescript-eslint/parser": "^4.8.2",
- "cypress": "^5.6.0",
+ "cypress": "^6.1.0",
"eslint": "^7.14.0",
"eslint-plugin-cypress": "^2.11.2",
"eslint-plugin-no-only-tests": "^2.4.0",
diff --git a/src/commands/realClick.ts b/src/commands/realClick.ts
index 9fbbf5b..b75a1ae 100644
--- a/src/commands/realClick.ts
+++ b/src/commands/realClick.ts
@@ -1,6 +1,7 @@
import { fireCdpCommand } from "../fireCdpCommand";
import {
getCypressElementCoordinates,
+ ScrollBehaviorOptions,
Position,
} from "../getCypressElementCoordinates";
@@ -26,6 +27,11 @@ export interface RealClickOptions {
* cy.get("body").realClick({ x: 11, y: 12 }) // global click by coordinates
*/
y?: number;
+ /**
+ * Controls how the page is scrolled to bring the subject into view, if needed.
+ * @example cy.realHover({ scrollBehavior: "top" });
+ */
+ scrollBehavior?: ScrollBehaviorOptions;
}
/** @ignore this, update documentation for this function at index.d.ts */
@@ -38,7 +44,7 @@ export async function realClick(
? { x: options.x, y: options.y }
: options.position;
- const { x, y } = getCypressElementCoordinates(subject, position);
+ const { x, y } = getCypressElementCoordinates(subject, position, options.scrollBehavior);
const log = Cypress.log({
$el: subject,
diff --git a/src/commands/realHover.ts b/src/commands/realHover.ts
index 8b5a93a..64e443e 100644
--- a/src/commands/realHover.ts
+++ b/src/commands/realHover.ts
@@ -1,6 +1,7 @@
import { fireCdpCommand } from "../fireCdpCommand";
import {
Position,
+ ScrollBehaviorOptions,
getCypressElementCoordinates,
} from "../getCypressElementCoordinates";
@@ -14,6 +15,11 @@ export interface RealHoverOptions {
* @example cy.realHover({ position: "topLeft" })
*/
position?: Position;
+ /**
+ * Controls how the page is scrolled to bring the subject into view, if needed.
+ * @example cy.realHover({ scrollBehavior: "top" });
+ */
+ scrollBehavior?: ScrollBehaviorOptions;
}
/** @ignore this, update documentation for this function at index.d.ts */
@@ -21,7 +27,7 @@ export async function realHover(
subject: JQuery,
options: RealHoverOptions = {}
) {
- const { x, y } = getCypressElementCoordinates(subject, options.position);
+ const { x, y } = getCypressElementCoordinates(subject, options.position, options.scrollBehavior);
const log = Cypress.log({
$el: subject,
diff --git a/src/commands/realTouch.ts b/src/commands/realTouch.ts
index 4d33daf..387663f 100644
--- a/src/commands/realTouch.ts
+++ b/src/commands/realTouch.ts
@@ -19,40 +19,70 @@ export interface RealTouchOptions {
* cy.get("body").realTouch({ x: 11, y: 12 }) // global touch by coordinates
*/
y?: number;
+ /** radius of the touch area.
+ * @example
+ * cy.get("canvas").realTouch({ x: 100, y: 115, radius: 10 })
+ * cy.get("body").realTouch({ x: 11, y: 12, radius: 10 }) // global touch by coordinates
+ */
+ radius?: number;
+ /** specific radius of the X axis of the touch area
+ * @example
+ * cy.get("canvas").realTouch({ x: 100, y: 115, radiusX: 10, radiusY: 20 })
+ * cy.get("body").realTouch({ x: 11, y: 12, radiusX: 10, radiusY: 20 }) // global touch by coordinates
+ */
+ radiusX?: number;
+ /** specific radius of the Y axis of the touch area
+ * @example
+ * cy.get("canvas").realTouch({ x: 100, y: 115, radiusX: 10, radiusY: 20 })
+ * cy.get("body").realTouch({ x: 11, y: 12, radiusX: 10, radiusY: 20 }) // global touch by coordinates
+ */
+ radiusY?: number;
}
export async function realTouch(
subject: JQuery,
options: RealTouchOptions = {}
) {
- const position = options.x && options.y
- ? { x: options.x, y: options.y }
+ const position = typeof options.x === 'number' || typeof options.y === 'number'
+ ? { x: options.x || 0, y: options.y || 0 }
: options.position;
+ const radiusX = options.radiusX || options.radius || 1
+ const radiusY = options.radiusY || options.radius || 1
- const elementPoints = getCypressElementCoordinates(subject, position);
+ const elementPoint = getCypressElementCoordinates(subject, position);
const log = Cypress.log({
$el: subject,
name: "realTouch",
consoleProps: () => ({
"Applied To": subject.get(0),
- "Absolute Coordinates": [elementPoints],
+ "Absolute Coordinates": [elementPoint],
+ "Touched Area (Radius)": {
+ x: radiusX,
+ y: radiusY,
+ }
})
})
log.snapshot("before");
+ const touchPoint = {
+ ...elementPoint,
+ radiusX,
+ radiusY
+ }
+
await fireCdpCommand("Input.dispatchTouchEvent", {
type: "touchStart",
- touchPoints: [elementPoints],
+ touchPoints: [touchPoint],
});
await fireCdpCommand("Input.dispatchTouchEvent", {
type: "touchEnd",
- touchPoints: [elementPoints],
+ touchPoints: [touchPoint],
})
log.snapshot("after").end();
return subject;
-}
\ No newline at end of file
+}
diff --git a/src/getCypressElementCoordinates.ts b/src/getCypressElementCoordinates.ts
index 0640346..1ae5804 100644
--- a/src/getCypressElementCoordinates.ts
+++ b/src/getCypressElementCoordinates.ts
@@ -10,7 +10,9 @@ export type Position =
| "bottomRight"
| { x: number; y: number };
- function getPositionedCoordinates(
+export type ScrollBehaviorOptions = "center" | "top" | "bottom" | "nearest";
+
+function getPositionedCoordinates(
x0: number,
y0: number,
width: number,
@@ -44,6 +46,88 @@ export type Position =
return [x0 + width / 2, y0 + height / 2];
}
}
+/**
+ * Scrolls the given htmlElement into view on the page.
+ * The position the element is scrolled to can be customized with scrollBehavior.
+ */
+function scrollIntoView(
+ htmlElement: HTMLElement,
+ scrollBehavior: ScrollBehaviorOptions = "center"
+) {
+ let block: ScrollLogicalPosition;
+
+ if (scrollBehavior === "top") {
+ block = "start";
+ } else if (scrollBehavior === "bottom") {
+ block = "end";
+ } else {
+ block = scrollBehavior;
+ }
+
+ htmlElement.scrollIntoView({ block });
+}
+
+function getIframesPositionShift(element: HTMLElement) {
+ let currentWindow: Window | null = element.ownerDocument.defaultView;
+ const noPositionShift = {
+ frameScale: 1,
+ frameX: 0,
+ frameY: 0,
+ };
+
+ if (!currentWindow) {
+ return noPositionShift;
+ }
+
+ // eslint-disable-next-line prefer-const
+ const iframes = []
+
+ while (
+ currentWindow &&
+ currentWindow !== window.top
+ ) {
+ iframes.push(
+ // for cross origin domains .frameElement returns null so query using parentWindow
+ // but when running using --disable-web-security it will return the frame element
+ (currentWindow.frameElement as HTMLElement) ??
+ currentWindow.parent.document.querySelector("iframe")
+ );
+
+ currentWindow = currentWindow.parent;
+ }
+
+ return iframes.reduceRight(({ frameX, frameY, frameScale }, frame) => {
+ const { x, y, width } = frame.getBoundingClientRect();
+
+ return {
+ frameX: frameX + x * frameScale,
+ frameY: frameY + y * frameScale,
+ frameScale: frameScale * (width / frame.offsetWidth),
+ }
+ }, noPositionShift)
+}
+
+/**
+ * Returns the coordinates and size of a given Element, relative to the Cypress app