Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = async () => {
export default async (): Promise<void> => {
const SCROLL_DISTANCE = 100;
const SCROLL_DOWN_MAX = 100;
const SCROLL_TOP_MAX = 100;
Expand All @@ -7,11 +7,11 @@ module.exports = async () => {
return;
}

const scrollToBottom = async () => {
const scrollToBottom = async (): Promise<void> => {
window.visualTestScrollingBottom = true;
let counter = 0;
let lastScrollY = 0;
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
const timer = setInterval(() => {
if (window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight || counter > SCROLL_DOWN_MAX) {
// you're at the bottom of the page
Expand All @@ -33,8 +33,8 @@ module.exports = async () => {
});
};

const scrollToTop = async () => {
await new Promise((resolve) => {
const scrollToTop = async (): Promise<void> => {
await new Promise<void>((resolve) => {
const timer = setInterval(() => {
if (!window.visualTestScrollingBottom) {
clearInterval(timer);
Expand All @@ -43,7 +43,7 @@ module.exports = async () => {
}
}, 100);
});
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
let counter = 0;
const timer = setInterval(() => {
if (window.scrollY === 0 || counter > SCROLL_TOP_MAX) {
Expand Down
101 changes: 101 additions & 0 deletions .engine_scripts/engine.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Page, Frame, BrowserContext } from 'playwright';
import type { Page as PuppeteerPage } from 'puppeteer';
import type { Scenario, Viewport } from 'backstopjs';

export interface KeyPressSelector {
selector: string;
keyPress: string;
}

export interface ScenarioAction {
frame?: string | string[];
check?: string;
click?: string;
focus?: string;
goto?: string;
hide?: string;
hover?: string;
input?: string;
value?: string;
file?: string | string[];
append?: boolean;
useFileChooser?: boolean;
remove?: string;
press?: string;
key?: string;
scroll?: string;
select?: string;
label?: string;
uncheck?: string;
wait?: number | string;
url?: string;
persist?: string;
path?: string;
}

export interface EngineScenario extends Scenario {
index: string;
total: number;
label: string;
url: string;
bypassCsp?: boolean;
cookiePath?: string;
useCssOverride?: boolean;
cssOverridePath?: string;
jsOnReadyPath?: string;
noScrollTop?: boolean;
postInteractionWait?: number | string;
hoverSelector?: string | string[];
hoverSelectors?: string | string[];
clickSelector?: string | string[];
clickSelectors?: string | string[];
keyPressSelector?: KeyPressSelector | KeyPressSelector[];
keyPressSelectors?: KeyPressSelector | KeyPressSelector[];
scrollToSelector?: string;
actions?: ScenarioAction[];
getTestUrl?: (url: string) => string;
}

export interface CookieInput {
name: string;
value: string;
domain: string | string[];
path?: string;
expirationDate?: number;
[k: string]: unknown;
}

export interface ActionsContext {
currentPage: Page;
scenario: EngineScenario;
browserContext: BrowserContext;
}

export type BrowserScript = () => Promise<void>;

export type OnBeforeScript = (
page: Page,
scenario: EngineScenario,
viewport: Viewport,
isReference: boolean,
browserContext: BrowserContext
) => Promise<void>;
export type OnReadyScript = OnBeforeScript;

export type EmbedFiles = (scenario: EngineScenario, page: Page) => Promise<void>;
export type OverrideCSS = (page: Page, scenario: EngineScenario) => Promise<void>;
export type LoadCookies = (browserContext: BrowserContext, scenario: EngineScenario) => Promise<void>;
export type Actions = (context: ActionsContext) => Promise<void>;
export type ClickAndHoverHelper = (page: Page | Frame, scenario: EngineScenario) => Promise<void>;

export type PuppetOnBeforeScript = (
page: PuppeteerPage,
scenario: EngineScenario,
viewport: Viewport,
isReference: boolean,
browserContext: unknown
) => Promise<void>;
export type PuppetOnReadyScript = (page: PuppeteerPage, scenario: EngineScenario, vp: Viewport) => Promise<void>;
export type PuppetOverrideCSS = (page: PuppeteerPage, scenario: EngineScenario) => Promise<void>;
export type PuppetClickAndHoverHelper = (page: PuppeteerPage, scenario: EngineScenario) => Promise<void>;
export type PuppetLoadCookies = (page: PuppeteerPage, scenario: EngineScenario) => Promise<void>;
8 changes: 8 additions & 0 deletions .engine_scripts/engine.globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {};

declare global {
interface Window {
visualTestScrollingBottom?: boolean;
_styleData?: string;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const fs = require('fs');
const path = require('path');
const YAML = require('js-yaml');
import * as fs from 'fs';
import * as path from 'path';
import type { Page, Frame, ElementHandle } from 'playwright';
import type { ActionsContext, ScenarioAction, Actions } from '../engine.js';

const chalkImport = import('chalk').then((m) => m.default);

module.exports = async (context) => {
export default (async (context: ActionsContext): Promise<void> => {
const { currentPage, scenario, browserContext } = context;

if (!scenario.actions) {
Expand All @@ -14,8 +16,8 @@ module.exports = async (context) => {
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);

for (let i = 0; i < scenario.actions.length; i++) {
let page = currentPage;
let action = scenario.actions[i];
let page: Page | Frame = currentPage;
let action: ScenarioAction = scenario.actions[i];

if (!action) {
continue;
Expand All @@ -25,8 +27,15 @@ module.exports = async (context) => {
const frames = typeof action.frame === 'string' ? [action.frame] : action.frame;
for (let j = 0; j < frames.length; j++) {
await page.waitForSelector(frames[j]);
const handle = await page.locator(frames[j]).elementHandle();
page = await handle.contentFrame();
const handle = (await page.locator(frames[j]).elementHandle()) as ElementHandle<SVGElement | HTMLElement>;
if (handle === null) {
throw new Error(`iframe element not found for selector: ${frames[j]}`);
}
const frame = await handle.contentFrame();
if (frame === null) {
throw new Error(`contentFrame missing for iframe element matching selector: ${frames[j]}`);
}
page = frame;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down Expand Up @@ -79,7 +88,7 @@ module.exports = async (context) => {
let el = await page.locator(action.input);

if (!action.append) {
await el.evaluate((node) => (node.value = ''));
await el.evaluate((node) => ((node as HTMLInputElement).value = ''));
}

await el.type(action.value);
Expand All @@ -89,7 +98,7 @@ module.exports = async (context) => {
let el = await page.locator(action.input);

const files = typeof action.file === 'string' ? [action.file] : action.file;
let normalizedPaths = [];
let normalizedPaths: string[] = [];

files.forEach((file) => {
if (path.isAbsolute(file)) {
Expand All @@ -105,8 +114,8 @@ module.exports = async (context) => {
});

if (action.useFileChooser) {
const fileChooserPromise = page.waitForEvent('filechooser');
el.click();
const fileChooserPromise = currentPage.waitForEvent('filechooser');
await el.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(normalizedPaths);
} else {
Expand All @@ -118,21 +127,21 @@ module.exports = async (context) => {
if (!!action.remove) {
console.log(logPrefix + 'Remove:', action.remove);
await page.waitForSelector(action.remove);
let el = await page.locator(action.hide);
let el = await page.locator(action.remove);
await el.evaluate((node) => node.style.setProperty('display', 'none', 'important'));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if (!!action.press) {
console.log(logPrefix + 'Press:', action.press);
await page.waitForSelector(action.press);
await page.locator(action.press).press(action.key);
await page.locator(action.press).press(action.key!);
}

if (!!action.scroll) {
console.log(logPrefix + 'Scroll:', action.scroll);
await page.waitForSelector(action.scroll);
await page.evaluate((scrollToSelector) => {
document.querySelector(scrollToSelector).scrollIntoView();
await page.evaluate((scrollToSelector: string) => {
document.querySelector(scrollToSelector)!.scrollIntoView();
}, action.scroll);
}

Expand Down Expand Up @@ -169,13 +178,14 @@ module.exports = async (context) => {
if (!!scenario.getTestUrl) {
url = scenario.getTestUrl(url);
}
await page.waitForURL(url);
await (page as Page).waitForURL(url);
}

if (parseInt(action.wait) > 0) {
await page.waitForTimeout(action.wait);
const waitTimeMs = parseInt(String(action.wait));
if (!Number.isNaN(waitTimeMs) && waitTimeMs >= 0) {
await page.waitForTimeout(waitTimeMs);
} else {
await page.waitForSelector(action.wait);
await page.waitForSelector(action.wait as string);
}
}

Expand All @@ -184,4 +194,4 @@ module.exports = async (context) => {
await browserContext.storageState({ path: action.path });
}
}
};
}) as Actions;
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
module.exports = async (page, scenario) => {
import type { Page, Frame } from 'playwright';
import type { EngineScenario, KeyPressSelector, ClickAndHoverHelper } from '../engine.js';

export default (async (page: Page | Frame, scenario: EngineScenario): Promise<void> => {
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
const scrollToSelector = scenario.scrollToSelector;
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]

if (keyPressSelector) {
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
for (const keyPressSelectorItem of ([] as KeyPressSelector[]).concat(keyPressSelector as KeyPressSelector[])) {
await page.waitForSelector(keyPressSelectorItem.selector);
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
}
}

if (hoverSelector) {
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
for (const hoverSelectorIndex of ([] as string[]).concat(hoverSelector as string[])) {
await page.waitForSelector(hoverSelectorIndex);
await page.hover(hoverSelectorIndex);
}
}

if (clickSelector) {
for (const clickSelectorIndex of [].concat(clickSelector)) {
for (const clickSelectorIndex of ([] as string[]).concat(clickSelector as string[])) {
await page.waitForSelector(clickSelectorIndex);
await page.click(clickSelectorIndex);
}
}

if (postInteractionWait) {
if (parseInt(postInteractionWait) > 0) {
await page.waitForTimeout(postInteractionWait);
const interactionWait = parseInt(String(postInteractionWait));
if (!Number.isNaN(interactionWait) && interactionWait >= 0) {
await page.waitForTimeout(interactionWait);
} else {
await page.waitForSelector(postInteractionWait);
await page.waitForSelector(postInteractionWait as string);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

if (scrollToSelector) {
await page.waitForSelector(scrollToSelector);
await page.evaluate(scrollToSelector => {
document.querySelector(scrollToSelector).scrollIntoView();
await page.evaluate((scrollToSel: string) => {
document.querySelector(scrollToSel)!.scrollIntoView();
}, scrollToSelector);
}
};
}) as ClickAndHoverHelper;
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const fs = require('fs');
import * as fs from 'fs';
import type { Page } from 'playwright';
import type { EngineScenario, EmbedFiles, OverrideCSS } from '../engine.js';

const chalkImport = import('chalk').then((m) => m.default);

const embedCss = async (scenario, page) => {
const embedCss = async (scenario: EngineScenario, page: Page): Promise<void> => {
if (scenario.useCssOverride) {
await require('./overrideCSS')(page, scenario);
await (require('./overrideCSS') as OverrideCSS)(page, scenario);
}
};

const embedJs = async (scenario, page) => {
const embedJs = async (scenario: EngineScenario, page: Page): Promise<void> => {
const jsOnReadyPath = scenario.jsOnReadyPath;
const chalk = await chalkImport;
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
Expand All @@ -22,7 +25,7 @@ const embedJs = async (scenario, page) => {
}
};

module.exports = async (scenario, page) => {
export default (async (scenario: EngineScenario, page: Page): Promise<void> => {
await embedJs(scenario, page);
await embedCss(scenario, page);
};
}) as EmbedFiles;
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@
*
*/

const fs = require('fs');
const path = require('path');
import * as fs from 'fs';
import * as path from 'path';
import type { Page } from 'playwright';
import type { EngineScenario } from '../engine.js';

const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
const IMAGE_STUB_URL = path.resolve(__dirname, '../../imageStub.jpg');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
const HEADERS_STUB = {};

module.exports = async function (page, scenario) {
page.route(IMAGE_URL_RE, route => {
route.fulfill({
export default async function (page: Page, scenario: EngineScenario): Promise<void> {
await page.route(IMAGE_URL_RE, async (route) => {
await route.fulfill({
body: IMAGE_DATA_BUFFER,
headers: HEADERS_STUB,
status: 200
status: 200,
});
});
};
}
Loading
Loading