Skip to content
Merged
20 changes: 16 additions & 4 deletions src/browser/client-bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path";
import fs from "fs";
import makeDebug from "debug";
import { ClientBridgeError } from "./error";
import { WdioBrowser } from "../../types";

const debug = makeDebug("testplane:client-bridge");

Expand All @@ -28,13 +29,27 @@ export class ClientBridge<T extends Record<string, (...args: any[]) => any>> {
const scriptFileName = needsCompatLib ? "bundle.compat.js" : "bundle.native.js";
const scriptFilePath = path.join(__dirname, "..", "client-scripts", namespace, "build", scriptFileName);

let debugBrowserId = "";
if (debug.enabled) {
debugBrowserId = `${(browser as WdioBrowser)?.capabilities?.browserName} ${

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.

этого точно хватит для id? у нас же может быть 2 браузера одинаковой версии, но с разными капабилити (desktp/touch)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added sessionId

(browser as WdioBrowser)?.capabilities?.browserVersion
}:${(browser as WdioBrowser)?.sessionId}`;
}

if (bundlesCache[scriptFilePath]) {
debug(

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.

Why would we need to know it?
This debug log does not seem to be usefull

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It actually is very useful, for example it highlights which version of the script — native or compat was chosen, which instantly makes it clear if calibration works as expected, etc.

`creating ClientBridge with cached script for namespace ${namespace} at ${scriptFilePath} for browser ${debugBrowserId}`,
);
return new ClientBridge(browser, bundlesCache[scriptFilePath], namespace);
}

const bundle = await fs.promises.readFile(scriptFilePath, { encoding: "utf8" });
bundlesCache[scriptFilePath] = bundle;

debug(
`creating ClientBridge with new script for namespace ${namespace} at ${scriptFilePath} for browser ${debugBrowserId}`,
);

return new this(browser, bundle, namespace);
}

Expand Down Expand Up @@ -89,10 +104,7 @@ export class ClientBridge<T extends Record<string, (...args: any[]) => any>> {
}

private async _inject(): Promise<void> {
debug(` > injecting script into namespace ${this._namespace}`);
if (debug.enabled) {
console.log(this._script);
}
debug(` > injecting script into namespace ${this._namespace}: ${this._script.slice(0, 256)}...`);
Comment thread
shadowusr marked this conversation as resolved.
await this._browser.execute(this._script, this._namespace);
}
}
9 changes: 8 additions & 1 deletion src/browser/client-scripts/calibrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@
}

function needsCompatLib() {
return !hasCSS3Selectors() || !window.getComputedStyle || !window.matchMedia || !String.prototype.trim;
return (
!hasCSS3Selectors() ||
!window.getComputedStyle ||
!window.matchMedia ||
!String.prototype.trim ||
!window.Node ||
!window.Node.prototype.getRootNode
);
}

// In safari `window.innerWidth` always returns default 980px and and even viewport meta tag setting does not change it.
Expand Down
3 changes: 2 additions & 1 deletion src/browser/client-scripts/screen-shooter/utils/dom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as lib from "@lib";
import { ScreenshooterNamespaceData } from "../types";

declare global {
Expand Down Expand Up @@ -59,7 +60,7 @@ export function forEachRoot(cb: (root: Element | ShadowRoot) => void): void {
export function getParentElement(node: Node): Element | null {
if (node instanceof ShadowRoot) return node.host;
if (node instanceof Element) {
const root = node.getRootNode();
const root = lib.getRootNode(node);
return node.parentElement || (root instanceof ShadowRoot ? root.host : null);
}
return node.parentNode instanceof Element ? node.parentNode : null;
Expand Down
10 changes: 10 additions & 0 deletions src/browser/client-scripts/shared/lib.compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,15 @@ export function trim(str: string): string {
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
}

export function getRootNode(node: Node): Node {
let root = node;

while (root.parentNode) {
root = root.parentNode;
}

return root;
}

export { getComputedStyle } from "./polyfills/getComputedStyle";
export { matchMedia } from "./polyfills/matchMedia";
4 changes: 4 additions & 0 deletions src/browser/client-scripts/shared/lib.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ export function matchMedia(mediaQuery: string): MediaQueryList {
export function trim(str: string): string {
return str.trim();
}

export function getRootNode(node: Node): Node {
return node.getRootNode();
}
32 changes: 20 additions & 12 deletions src/browser/commands/assert-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,8 @@ const getIgnoreDiffPixelCountRatio = value => {
};

module.exports.default = browser => {
const { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib } = browser;
const browserProperties = { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib };
const elementsScreenShooterPromise = ElementsScreenShooter.create({
camera: browser.camera,
browser: browser.publicAPI,
browserProperties,
});
const viewportScreenShooterPromise = ViewportScreenShooter.create({
camera: browser.camera,
browser: browser.publicAPI,
browserProperties,
});
let elementsScreenShooterPromise;
let viewportScreenShooterPromise;
const { publicAPI: session, config } = browser;
const {
assertViewOpts,
Expand Down Expand Up @@ -192,6 +182,15 @@ module.exports.default = browser => {
debug(`[${debugId}] assertView selectors: %O`, selectors);
debug(`[${debugId}] assertView opts: %O`, opts);

if (!elementsScreenShooterPromise) {
const { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib } = browser;
elementsScreenShooterPromise = ElementsScreenShooter.create({
camera: browser.camera,
browser: browser.publicAPI,
browserProperties: { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib },
});
}

const screenShooter = await elementsScreenShooterPromise;
await waitForStaticToLoad(opts);
const { image, meta } = await screenShooter.capture(selectors, opts);
Expand Down Expand Up @@ -238,6 +237,15 @@ module.exports.default = browser => {

debug(`assertViewByViewport state: ${state}, opts: %O`, opts);

if (!viewportScreenShooterPromise) {
const { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib } = browser;
viewportScreenShooterPromise = ViewportScreenShooter.create({
camera: browser.camera,
browser: browser.publicAPI,
browserProperties: { isWebdriverProtocol, shouldUsePixelRatio, needsCompatLib },
});
}

const vpScreenShooter = await viewportScreenShooterPromise;
await waitForStaticToLoad(opts);
const { image, meta } = await vpScreenShooter.capture(opts);
Expand Down
5 changes: 5 additions & 0 deletions src/browser/history/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { cleanupRrweb, filterEvents, installRrwebAndCollectEvents, sendFilteredE
import { getHistoryContext, runWithHistoryContext } from "./async-local-storage";

const debug = makeDebug("testplane:browser:history");
const debugTimeTravel = makeDebug("testplane:time-travel:history");

interface NodeData {
name: string;
Expand Down Expand Up @@ -79,8 +80,10 @@ export const requestDomSnapshots = ({
attempt,
currentTest,
}: RequestDomSnapshotsData): void => {
debugTimeTravel("requestDomSnapshots, called");
try {
if (!callstack) {
debugTimeTravel("requestDomSnapshots, callstack is not defined");
return;
}

Expand All @@ -90,6 +93,7 @@ export const requestDomSnapshots = ({
const test = currentTest ?? session.executionContext?.ctx?.currentTest;

if (shouldRecord && process.send && test) {
debugTimeTravel("requestDomSnapshots, shouldRecord and process.send and test are true");
const rrwebPromise = installRrwebAndCollectEvents(session, callstack)
.then(rrwebEvents => {
const rrwebEventsFiltered = filterEvents(rrwebEvents);
Expand All @@ -101,6 +105,7 @@ export const requestDomSnapshots = ({

snapshotsPromiseRef.current = snapshotsPromiseRef.current.then(() => rrwebPromise);
}
debugTimeTravel("requestDomSnapshots, done");
} catch (e) {
debug("An error occurred during capturing snapshots in browser: %O", e);
}
Expand Down
77 changes: 61 additions & 16 deletions src/browser/history/rrweb.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import fs from "fs";
import path from "path";
import { eventWithTime } from "@rrweb/types";
import makeDebug from "debug";
import type { Callstack } from "./callstack";
import { MasterEvents } from "../../events";
import type { SnapshotsData, Test, TestContext } from "../../types";
import { runWithoutHistory } from "./index";
import path from "path";

const debug = makeDebug("testplane:time-travel:rrweb");

// Built from branch https://github.com/gemini-testing/rrweb/tree/TESTPLANE-712.syntax_err
// PR: https://github.com/rrweb-io/rrweb/pull/1735
// Issue: https://github.com/rrweb-io/rrweb/issues/1734
const rrwebCode = fs.readFileSync(path.join(__dirname, "../client-scripts/rrweb-record.min.js"), "utf-8");
const sessionsWithRrwebRequested = new WeakSet<WebdriverIO.Browser>();
const sessionsWithRrwebSent = new WeakSet<WebdriverIO.Browser>();
const sessionsWithUnsupportedRrweb = new WeakSet<WebdriverIO.Browser>();

interface CollectRrwebEventsResult {
isRrwebSupported?: false;
isRrwebInstalled: boolean;
rrwebEvents: eventWithTime[];
evalError?: string;
}

/* eslint-disable @typescript-eslint/ban-ts-comment */
Expand All @@ -23,19 +29,47 @@ export async function installRrwebAndCollectEvents(
callstack: Callstack,
): Promise<eventWithTime[]> {
return runWithoutHistory<Promise<eventWithTime[]>>({ callstack }, async () => {
const shouldSendRrwebCode = !sessionsWithRrwebRequested.has(session);
if (sessionsWithUnsupportedRrweb.has(session)) {
return [];
}

const shouldSendRrwebCode = !sessionsWithRrwebSent.has(session);

if (shouldSendRrwebCode) {
sessionsWithRrwebRequested.add(session);
sessionsWithRrwebSent.add(session);
}

const result = await collectRrwebEvents(session, shouldSendRrwebCode ? rrwebCode : null);
let result = await collectRrwebEvents(session, shouldSendRrwebCode ? rrwebCode : undefined);

let debugBrowserId = "";
if (debug.enabled) {
debugBrowserId = `${session?.capabilities?.browserName} ${session?.capabilities?.browserVersion}:${session?.sessionId}`;
}

if (result.isRrwebSupported === false) {
debug("rrweb is not supported in browser %s, error: %s", debugBrowserId, result.evalError);
sessionsWithUnsupportedRrweb.add(session);
Comment thread
shadowusr marked this conversation as resolved.

return [];
}

// If rrweb is installed (success) or we already provided the code (no point in trying again)
if (result.isRrwebInstalled || shouldSendRrwebCode) {
return result.rrwebEvents;
}

return (await collectRrwebEvents(session, rrwebCode)).rrwebEvents;
debug("collectRrwebEvents, rrweb was not installed, sending code again");

result = await collectRrwebEvents(session, rrwebCode);

if (result.isRrwebSupported === false) {
debug("rrweb is not supported in browser %s, error: %s", debugBrowserId, result.evalError);
sessionsWithUnsupportedRrweb.add(session);

return [];
}

return result.rrwebEvents;
});
}

Expand All @@ -46,9 +80,9 @@ export async function cleanupRrweb(session: WebdriverIO.Browser, callstack: Call
try {
// @ts-expect-error
const rrwebEvents = window.rrwebEvents;
const rrwebData = rrwebEvents?.testplane;
const rrwebData = rrwebEvents && rrwebEvents.testplane;

if (rrwebData?.stopRecording) {
if (rrwebData && rrwebData.stopRecording) {
try {
rrwebData.stopRecording();
} catch (e) {
Expand All @@ -57,8 +91,8 @@ export async function cleanupRrweb(session: WebdriverIO.Browser, callstack: Call
}

try {
const colorSchemeMedia = rrwebData?.colorSchemeMedia;
const colorSchemeListener = rrwebData?.colorSchemeListener;
const colorSchemeMedia = rrwebData && rrwebData.colorSchemeMedia;
const colorSchemeListener = rrwebData && rrwebData.colorSchemeListener;
Comment on lines +94 to +95

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.

Whats the difference?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

optional chaining is supported from chrome 80: https://caniuse.com/mdn-javascript_operators_optional_chaining

While we are targeting much earlier versions.


if (colorSchemeMedia && colorSchemeListener) {
colorSchemeMedia.removeEventListener("change", colorSchemeListener);
Expand All @@ -67,7 +101,7 @@ export async function cleanupRrweb(session: WebdriverIO.Browser, callstack: Call
/**/
}

if (rrwebData?.isInstalledByTestplane) {
if (rrwebData && rrwebData.isInstalledByTestplane) {
// @ts-expect-error
delete window.rrweb;

Expand All @@ -86,21 +120,22 @@ export async function cleanupRrweb(session: WebdriverIO.Browser, callstack: Call
} catch (e) {
/**/
} finally {
sessionsWithRrwebRequested.delete(session);
sessionsWithRrwebSent.delete(session);
sessionsWithUnsupportedRrweb.delete(session);
}
}

function collectRrwebEvents(
session: WebdriverIO.Browser,
rrwebRecordFnCode: string | null,
rrwebRecordFnCode: string | undefined,

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.

nit: -> rrwebRecordFnCode?: string

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well it's actually important here.

The story is that in older versions, argument is expected to be string or undefined precisely (with null it throws an error). However, we don't mean that "argument can be omitted". We mean precisely that it must be passed, but can be either string or undefined.

): Promise<CollectRrwebEventsResult> {
return session.execute(
(rrwebRecordFnCode, serverTime) => {
const isRrwebInstalled = (): boolean => {
try {
// @ts-expect-error
return Boolean(window.rrweb);
} catch {
} catch (e) {

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.

?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The catch {} syntax became available from chrome 66, while we are targeting chrome 53: https://caniuse.com/wf-optional-catch-binding

return false;
}
};
Expand All @@ -112,7 +147,7 @@ function collectRrwebEvents(
result = window.rrwebEvents.slice(window.lastProcessedRrwebEvent + 1);
// @ts-expect-error
window.lastProcessedRrwebEvent = window.rrwebEvents.length - 1;
} catch {
} catch (e) {
result = [];
}

Expand Down Expand Up @@ -159,7 +194,17 @@ function collectRrwebEvents(

try {
if (!isRrwebInstalled() && rrwebRecordFnCode) {
window.eval(rrwebRecordFnCode);
try {
window.eval(rrwebRecordFnCode);
} catch (e) {
return {
isRrwebSupported: false,
isRrwebInstalled: false,
rrwebEvents: [],
evalError: String(e),
};
}

// @ts-expect-error
window.lastProcessedRrwebEvent = -1;
// @ts-expect-error
Expand Down
6 changes: 5 additions & 1 deletion src/browser/new-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ const headlessBrowserOptions: HeadlessBrowserOptions = {
};

export class NewBrowser extends Browser {
private _onExit: (err?: Error) => Promise<void> = async () => {};

constructor(config: Config, opts: BrowserOpts) {
super(config, opts);

signalHandler.on("exit", (err?: Error) => this.quit(err));
this._onExit = async (err?: Error): Promise<void> => await this.quit(err);
signalHandler.on("exit", this._onExit);
}

async init(): Promise<NewBrowser> {
Expand All @@ -76,6 +79,7 @@ export class NewBrowser extends Browser {

async quit(err?: Error): Promise<void> {
this._exitError = err;
signalHandler.off("exit", this._onExit);

try {
this.setHttpTimeout(this._config.sessionQuitTimeout);
Expand Down
Loading
Loading