Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 90 additions & 14 deletions src/browser/client-scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ exports.cleanupFrameAnimations = function cleanupFrameAnimations() {
}
};

exports.disablePointerEvents = function disablePointerEvents() {
try {
return disablePointerEventsUnsafe();
} catch (e) {
return {
errorCode: "JS",
message: e.stack || e.message
};
}
};

exports.cleanupPointerEvents = function cleanupPointerEvents() {};

function prepareScreenshotUnsafe(areas, opts) {
var logger = util.createDebugLogger(opts);

Expand Down Expand Up @@ -164,10 +177,13 @@ function prepareScreenshotUnsafe(areas, opts) {
return top;
}, 9999999);

if (
captureElementFromTop &&
(topmostCaptureElementTop < safeArea.top || topmostCaptureElementTop >= safeArea.top + safeArea.height)
) {
var scrollOffsetTopForFit = scrollElem === window || !scrollElem.parentElement ? 0 : util.getScrollTop(scrollElem);
var rectTopInViewportForFit = rect.top - scrollOffsetTopForFit - window.scrollY;
var rectBottomInViewportForFit = rectTopInViewportForFit + rect.height;
var fitsInSafeArea =
rectTopInViewportForFit >= safeArea.top && rectBottomInViewportForFit <= safeArea.top + safeArea.height;

if (captureElementFromTop && !fitsInSafeArea) {
logger("captureElementFromTop=true and capture element is outside of viewport, going to perform scroll");
if (!util.isRootElement(scrollElem) && captureElementFromTop) {
var scrollElemBoundingRect = getBoundingClientContentRect(scrollElem);
Expand Down Expand Up @@ -346,6 +362,25 @@ function prepareScreenshotUnsafe(areas, opts) {
disableFrameAnimationsUnsafe();
}

var disableHover = opts.disableHover;
var pointerEventsDisabled = false;
if (disableHover === "always") {
logger("adding stylesheet with pointer-events: none on all elements");
disablePointerEventsUnsafe();
pointerEventsDisabled = true;
} else if (disableHover === "when-scrolling-needed" && opts.compositeImage) {
var scrollOffsetTop = scrollElem === window || !scrollElem.parentElement ? 0 : util.getScrollTop(scrollElem);
var rectTopInViewport = rect.top - scrollOffsetTop - window.scrollY;
var needsScrolling =
rectTopInViewport < safeArea.top || rectTopInViewport + rect.height > safeArea.top + safeArea.height;

if (needsScrolling) {
logger("adding stylesheet with pointer-events: none on all elements (composite capture needs scrolling)");
disablePointerEventsUnsafe();
pointerEventsDisabled = true;
}
}

logger("prepareScreenshotUnsafe, final capture rect:", rect);
logger("prepareScreenshotUnsafe, pixelRatio:", pixelRatio);

Expand Down Expand Up @@ -379,10 +414,21 @@ function prepareScreenshotUnsafe(areas, opts) {
? 0
: Math.floor(util.getScrollLeft(scrollElem) * pixelRatio)
},
pointerEventsDisabled: pointerEventsDisabled,
debugLog: logger()
};
}

function createDefaultTrustedTypesPolicy() {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy("default", {
createHTML: function (string) {
return string;
}
});
}
}

function disableFrameAnimationsUnsafe() {
var everyElementSelector = "*:not(#testplane-q.testplane-w.testplane-e.testplane-r.testplane-t.testplane-y)";
var everythingSelector = ["", "::before", "::after"]
Expand Down Expand Up @@ -412,16 +458,6 @@ function disableFrameAnimationsUnsafe() {
styleElements.push(styleElement);
}

function createDefaultTrustedTypesPolicy() {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy("default", {
createHTML: function (string) {
return string;
}
});
}
}

util.forEachRoot(function (root) {
try {
appendDisableAnimationStyleElement(root);
Expand All @@ -446,6 +482,46 @@ function disableFrameAnimationsUnsafe() {
};
}

function disablePointerEventsUnsafe() {
var everyElementSelector = "*:not(#testplane-q.testplane-w.testplane-e.testplane-r.testplane-t.testplane-y)";
var everythingSelector = ["", "::before", "::after"]
.map(function (pseudo) {
return everyElementSelector + pseudo;
})
.join(", ");

var styleElements = [];

function appendDisablePointerEventsStyleElement(root) {
var styleElement = document.createElement("style");
styleElement.innerHTML = everythingSelector + ["{", " pointer-events: none !important;", "}"].join("\n");

root.appendChild(styleElement);
styleElements.push(styleElement);
}

util.forEachRoot(function (root) {
try {
appendDisablePointerEventsStyleElement(root);
} catch (err) {
if (err && err.message && err.message.includes("This document requires 'TrustedHTML' assignment")) {
createDefaultTrustedTypesPolicy();

appendDisablePointerEventsStyleElement(root);
} else {
throw err;
}
}
});

exports.cleanupPointerEvents = function () {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its possible to encounter rare issue here
If we dont receive response to disable pointer events request, we will make another one.
In this case, exports.cleanupPointerEvents will overwrite another not empty function, and then "cleanup" wont clean this style, so it would be better to store somewhere array of style elements remove and remove elements from that array inside "exports.cleanupPointerEvents"

however, i see, i implemented "disableFrameAnimationsUnsafe" the same way, and looks like nobody encountered any issues, so we can keep it, i guess

for (var i = 0; i < styleElements.length; i++) {
styleElements[i].parentNode.removeChild(styleElements[i]);
}
exports.cleanupPointerEvents = function () {};
};
}

exports.resetZoom = function () {
var meta = lib.queryFirst('meta[name="viewport"]');
if (!meta) {
Expand Down
4 changes: 1 addition & 3 deletions src/browser/commands/assert-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ module.exports.default = browser => {
const { tempOpts } = RuntimeConfig.getInstance();
temp.attach(tempOpts);

const { image: currImgInst, meta: currImgMeta } = await screenShooter
.capture(selectors, opts)
.finally(() => browser.cleanupScreenshot(opts));
const { image: currImgInst, meta: currImgMeta } = await screenShooter.capture(selectors, opts);
const currSize = await currImgInst.getSize();
const currImg = { path: temp.path(Object.assign(tempOpts, { suffix: ".png" })), size: currSize };

Expand Down
149 changes: 14 additions & 135 deletions src/browser/existing-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,15 @@ import { MIN_CHROME_VERSION_SUPPORT_ISOLATION } from "../constants/browser";
import { isSupportIsolation } from "../utils/browser";
import { isRunInNodeJsEnv } from "../utils/config";
import { Config } from "../config";
import { Image, Rect } from "../image";
import { Image } from "../image";
import type { CalibrationResult, Calibrator } from "./calibrator";
import { NEW_ISSUE_LINK } from "../constants/help";
import { runWithoutHistory } from "./history";
import type { SessionOptions } from "./types";
import { Page } from "puppeteer-core";
import { PrepareScreenshotResult } from "./screen-shooter/types";
import { CDP } from "./cdp";
import type { ElementReference } from "@testplane/wdio-protocols";

import makeDebug from "debug";

const OPTIONAL_SESSION_OPTS = ["transformRequest", "transformResponse"];

interface PrepareScreenshotOpts {
ignoreSelectors?: string[];
allowViewportOverflow?: boolean;
captureElementFromTop?: boolean;
selectorToScroll?: string;
disableAnimation?: boolean;
debug?: boolean;
}

interface ClientBridgeErrorData {
errorCode: string;
message: string;
}

interface ScrollByParams {
x: number;
y: number;
Expand All @@ -62,10 +43,6 @@ function ensure<T>(value: T | undefined | null, hint?: string): asserts value is
}
}

const isClientBridgeErrorData = (data: unknown): data is ClientBridgeErrorData => {
return Boolean(data && (data as ClientBridgeErrorData).errorCode && (data as ClientBridgeErrorData).message);
};

export const getActivePuppeteerPage = async (session: WebdriverIO.Browser): Promise<Page | undefined> => {
const puppeteer = await session.getPuppeteer();

Expand Down Expand Up @@ -172,53 +149,6 @@ export class ExistingBrowser extends Browser {
this._meta = this._initMeta();
}

async prepareScreenshot(
selectors: string[] | Rect[],
opts: PrepareScreenshotOpts = {},
): Promise<PrepareScreenshotResult> {
// Running this fragment with history causes rrweb snapshots to break on pages with iframes
return runWithoutHistory({ callstack: this._callstackHistory! }, async () => {
opts = _.extend(opts, {
usePixelRatio: this._calibration ? this._calibration.usePixelRatio : true,
});

ensure(this._clientBridge, CLIENT_BRIDGE_HINT);
const result = await this._clientBridge.call<PrepareScreenshotResult | ClientBridgeErrorData>(
"prepareScreenshot",
[selectors, opts],
);
makeDebug("testplane:screenshots:browser:prepareScreenshot")((result as PrepareScreenshotResult).debugLog);

if (isClientBridgeErrorData(result)) {
throw new Error(
`Failed to perform the visual check, because we couldn't compute screenshot area to capture.\n\n` +
`What happened:\n` +
`- You called assertView command with the following selectors: ${JSON.stringify(selectors)}\n` +
`- You passed the following options: ${JSON.stringify(opts)}\n` +
`- We tried to determine positions of these elements, but failed with the '${result.errorCode}' error: ${result.message}\n\n` +
`What you can do:\n` +
`- Check that passed selectors are valid and exist on the page\n` +
`- If you believe this is a bug on our side, re-run this test with DEBUG=testplane:screenshots* and file an issue with this log at ${NEW_ISSUE_LINK}\n`,
);
}

// https://github.com/webdriverio/webdriverio/issues/11396
if (this._config.automationProtocol === WEBDRIVER_PROTOCOL && opts.disableAnimation) {
await this._disableIframeAnimations();
}

return result;
});
}

async cleanupScreenshot(opts: { disableAnimation?: boolean } = {}): Promise<void> {
if (opts.disableAnimation) {
return runWithoutHistory({ callstack: this._callstackHistory! }, async () => {
await this._cleanupPageAnimations();
});
}
}

open(url: string): Promise<WebdriverIO.Request | string | void> {
ensure(this._session, BROWSER_SESSION_HINT);

Expand All @@ -237,6 +167,19 @@ export class ExistingBrowser extends Browser {
return this._session.execute(script);
}

callMethodOnBrowserSide<T>(name: string, args: unknown[] = []): Promise<T> {
ensure(this._clientBridge, CLIENT_BRIDGE_HINT);
return this._clientBridge.call(name, args);
}

get shouldUsePixelRatio(): boolean {
return this._calibration ? this._calibration.usePixelRatio : true;
}

get isWebdriverProtocol(): boolean {
return this._config.automationProtocol === WEBDRIVER_PROTOCOL;
}

async captureViewportImage(page?: PageMeta, screenshotDelay?: number): Promise<Image> {
if (screenshotDelay) {
await new Promise(resolve => setTimeout(resolve, screenshotDelay));
Expand Down Expand Up @@ -536,70 +479,6 @@ export class ExistingBrowser extends Browser {
);
}

protected async _runInEachDisplayedIframe(cb: (...args: unknown[]) => unknown): Promise<void> {
ensure(this._session, BROWSER_SESSION_HINT);
const session = this._session;
const iframes = await session.findElements("css selector", "iframe[src]");
const displayedIframes: ElementReference[] = [];

await Promise.all(
iframes.map(async iframe => {
const isIframeDisplayed = await session.$(iframe).isDisplayed();

if (isIframeDisplayed) {
displayedIframes.push(iframe);
}
}),
);

try {
for (const iframe of displayedIframes) {
await session.switchToFrame(iframe);
await cb();
// switchToParentFrame does not work in ios - https://github.com/appium/appium/issues/14882
await session.switchToFrame(null);
}
} catch (e) {
await session.switchToFrame(null);
throw e;
}
}

protected async _disableFrameAnimations(): Promise<void> {
ensure(this._clientBridge, CLIENT_BRIDGE_HINT);
const result = await this._clientBridge.call<void>("disableFrameAnimations");

if (isClientBridgeErrorData(result)) {
throw new Error(
`Disable animations failed with error type '${result.errorCode}' and error message: ${result.message}`,
);
}

return result;
}

protected async _disableIframeAnimations(): Promise<void> {
await this._runInEachDisplayedIframe(() => this._disableFrameAnimations());
}

protected async _cleanupFrameAnimations(): Promise<void> {
ensure(this._clientBridge, CLIENT_BRIDGE_HINT);

return this._clientBridge.call("cleanupFrameAnimations");
}

protected async _cleanupIframeAnimations(): Promise<void> {
await this._runInEachDisplayedIframe(() => this._cleanupFrameAnimations());
}

protected async _cleanupPageAnimations(): Promise<void> {
await this._cleanupFrameAnimations();

if (this._config.automationProtocol === WEBDRIVER_PROTOCOL) {
await this._cleanupIframeAnimations();
}
}

_stubCommands(): void {
if (!this._session) {
return;
Expand Down
31 changes: 31 additions & 0 deletions src/browser/screen-shooter/iframe-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ElementReference } from "@testplane/wdio-protocols";

export async function runInEachDisplayedIframe(
session: WebdriverIO.Browser,
cb: () => Promise<unknown> | unknown,
): Promise<void> {
const iframes = await session.findElements("css selector", "iframe[src]");
const displayedIframes: ElementReference[] = [];

await Promise.all(
iframes.map(async iframe => {
const isIframeDisplayed = await session.$(iframe).isDisplayed();

if (isIframeDisplayed) {
displayedIframes.push(iframe);
}
}),
);

try {
for (const iframe of displayedIframes) {
await session.switchToFrame(iframe);
await cb();
// switchToParentFrame does not work in ios - https://github.com/appium/appium/issues/14882
await session.switchToFrame(null);
}
} catch (e) {
await session.switchToFrame(null);
throw e;
}
}
Loading