Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat!: add playwright support (#167)
Browse files Browse the repository at this point in the history
* feat!: add playwright support

* fix: lint

* fix: ci matrix

* don't run snap tests on non flask

* address PR comments
  • Loading branch information
mpetrunic committed Dec 15, 2022
1 parent f777a9a commit c4c0e5f
Show file tree
Hide file tree
Showing 57 changed files with 936 additions and 278 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
strategy:
matrix:
mm-version: [mm, flask]
automation: [playwright, puppeteer]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand All @@ -28,7 +29,7 @@ jobs:
- name: Lint
run: yarn run lint
- name: Tests
run: 'xvfb-run --auto-servernum yarn run test:${{ matrix.mm-version }} --timeout 50000'
run: 'xvfb-run --auto-servernum yarn run test:${{matrix.automation}}:${{ matrix.mm-version }} --timeout 50000'
- uses: actions/upload-artifact@v3
if: always()
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build
dist
.vscode/
/metamask
/.metamask
.idea
*.log
test/dapp/data.js
Expand Down
27 changes: 20 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
"build": "tsc -p tsconfig.build.json",
"lint": "eslint --color --ext .ts src/ test/",
"lint:fix": "yarn run lint --fix",
"test": "yarn run test:*",
"test:mm": "mocha --require ts-node/register --require test/global.ts --exclude test/flask/snaps.spec.ts",
"test:flask": "mocha --require ts-node/register --require test/global_flask.ts"
"test:mm": "mocha --require ts-node/register --require test/global.ts",
"test:flask": "mocha --require ts-node/register --require test/global_flask.ts",
"test:puppeteer:mm": "AUTOMATION=puppeteer yarn run test:mm",
"test:puppeteer:flask": "AUTOMATION=puppeteer yarn run test:flask",
"test:playwright:mm": "AUTOMATION=playwright yarn run test:mm",
"test:playwright:flask": "AUTOMATION=playwright yarn run test:flask"
},
"repository": {
"type": "git",
Expand All @@ -40,8 +43,6 @@
"license": "MIT",
"dependencies": {
"@metamask/providers": "^9.1.0",
"@types/chai-as-promised": "^7.1.5",
"chai-as-promised": "^7.1.1",
"node-stream-zip": "^1.13.0"
},
"devDependencies": {
Expand All @@ -51,13 +52,16 @@
"@metamask/snaps-cli": "^0.22.0",
"@rushstack/eslint-patch": "^1.2.0",
"@types/chai": "^4.2.22",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.1.1",
"@types/serve-handler": "^6.1.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"eslint": "^8.24.0",
"ganache": "^7.4.3",
"jest-environment-node": "^27.1.1",
"mocha": "^10.0.0",
"playwright": "^1.27.1",
"prettier": "^2.2.1",
"puppeteer": "14.0.0",
"serve-handler": "5.0.8",
Expand All @@ -67,6 +71,15 @@
"web3": "1.3.4"
},
"peerDependencies": {
"puppeteer": ">13"
"puppeteer": ">13",
"playwright": ">=1"
},
"peerDependenciesMeta": {
"soy-puppeteer": {
"optional": true
},
"playwright": {
"optional": true
}
}
}
}
17 changes: 17 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EventEmitter } from "events";
import { DappeteerPage } from "./page";

export interface DappeteerBrowser<Browser = unknown, Page = unknown>
extends EventEmitter {
isMetaMaskFlask(): boolean;

pages(): Promise<DappeteerPage<Page>[]>;

newPage(): Promise<DappeteerPage<Page>>;

getSource(): Browser;

close(): Promise<void>;

wsEndpoint(): string;
}
11 changes: 11 additions & 0 deletions src/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface DappeteerElementHandle<
Source = unknown,
Element = HTMLElement
> {
$$(selector: string): Promise<DappeteerElementHandle[]>;
evaluate(fn: (e: Element) => void | Promise<void>): Promise<void>;
type(value: string): Promise<void>;
click(): Promise<void>;
hover(): Promise<void>;
getSource(): Source;
}
35 changes: 20 additions & 15 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ElementHandle, Page } from "puppeteer";

import { DappeteerElementHandle } from "../element";
import { DappeteerPage } from "../page";
import {
getAccountMenuButton,
getElementByContent,
Expand All @@ -8,14 +8,16 @@ import {
} from "./selectors";

export const clickOnSettingsSwitch = async (
page: Page,
page: DappeteerPage,
text: string
): Promise<void> => {
const button = await getSettingsSwitch(page, text);
await button.click();
};

export const openNetworkDropdown = async (page: Page): Promise<void> => {
export const openNetworkDropdown = async (
page: DappeteerPage
): Promise<void> => {
const networkSwitcher = await page.waitForSelector(".network-display", {
visible: true,
});
Expand All @@ -35,14 +37,18 @@ export const openNetworkDropdown = async (page: Page): Promise<void> => {
}
};

export const openProfileDropdown = async (page: Page): Promise<void> => {
export const openProfileDropdown = async (
page: DappeteerPage
): Promise<void> => {
const accountSwitcher = await page.waitForSelector(".identicon", {
visible: true,
});
await accountSwitcher.click();
};

export const openAccountDropdown = async (page: Page): Promise<void> => {
export const openAccountDropdown = async (
page: DappeteerPage
): Promise<void> => {
const accMenu = await getAccountMenuButton(page);
await accMenu.click();
await page.waitForSelector(".menu__container.account-options-menu", {
Expand All @@ -51,7 +57,7 @@ export const openAccountDropdown = async (page: Page): Promise<void> => {
};

export const clickOnElement = async (
page: Page,
page: DappeteerPage,
text: string,
type?: string
): Promise<void> => {
Expand All @@ -60,14 +66,14 @@ export const clickOnElement = async (
};

export const clickOnButton = async (
page: Page,
page: DappeteerPage,
text: string
): Promise<void> => {
const button = await getElementByContent(page, text, "button");
await button.click();
};

export const clickOnLogo = async (page: Page): Promise<void> => {
export const clickOnLogo = async (page: DappeteerPage): Promise<void> => {
const header = await page.waitForSelector(".app-header__logo-container", {
visible: true,
});
Expand All @@ -85,25 +91,24 @@ export const clickOnLogo = async (page: Page): Promise<void> => {
* @returns true if found and updated, false otherwise
*/
export const typeOnInputField = async (
page: Page,
page: DappeteerPage,
label: string,
text: string,
clear = false,
excludeSpan = false,
optional = false
): Promise<boolean> => {
let input: ElementHandle<HTMLInputElement>;
let input: DappeteerElementHandle;
try {
input = await getInputByLabel(page, label, excludeSpan, 1000);
} catch (e) {
if (optional) return false;
throw e;
}

if (clear)
await page.evaluate((node: HTMLInputElement) => {
node.value = "";
}, input);
if (clear) {
await input.type("");
}
await input.type(text);
return true;
};
43 changes: 26 additions & 17 deletions src/helpers/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { ElementHandle, Page } from "puppeteer";
import { DappeteerElementHandle } from "../element";
import { DappeteerPage, Serializable } from "../page";

// TODO: change text() with '.';
export const getElementByContent = (
page: Page,
page: DappeteerPage,
text: string,
type = "*"
): Promise<ElementHandle | null> =>
): Promise<DappeteerElementHandle | null> =>
page.waitForXPath(`//${type}[contains(text(), '${text}')]`, {
timeout: 20000,
visible: true,
});

export const getInputByLabel = (
page: Page,
page: DappeteerPage,
text: string,
excludeSpan = false,
timeout = 1000
): Promise<ElementHandle> =>
): Promise<DappeteerElementHandle> =>
page.waitForXPath(
[
`//label[contains(.,'${text}')]/following-sibling::textarea`,
Expand All @@ -34,9 +35,9 @@ export const getInputByLabel = (
);

export const getSettingsSwitch = (
page: Page,
page: DappeteerPage,
text: string
): Promise<ElementHandle | null> =>
): Promise<DappeteerElementHandle | null> =>
page.waitForXPath(
[
`//span[contains(.,'${text}')]/parent::div/following-sibling::div/div/div/div`,
Expand All @@ -45,22 +46,30 @@ export const getSettingsSwitch = (
{ visible: true }
);

export const getErrorMessage = async (page: Page): Promise<string | false> => {
const options: Parameters<Page["waitForSelector"]>[1] = { timeout: 1000 };
export const getErrorMessage = async (
page: DappeteerPage
): Promise<string | false> => {
const options: Parameters<DappeteerPage["waitForSelector"]>[1] = {
timeout: 1000,
};

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const errorElement: ElementHandle<HTMLElement> | null = await Promise.race([
page.waitForSelector(`span.error`, options),
page.waitForSelector(`.typography--color-error-1`, options),
page.waitForSelector(`.typography--color-error-default`, options),
]).catch(() => null);
const errorElement: DappeteerElementHandle<Serializable> | null =
await Promise.race([
page.waitForSelector(`span.error`, options),
page.waitForSelector(`.typography--color-error-1`, options),
page.waitForSelector(`.typography--color-error-default`, options),
]).catch(() => null);
if (!errorElement) return false;
return page.evaluate((node: HTMLElement) => node.textContent, errorElement);
return page.evaluate(
(node) => (node as unknown as HTMLElement).textContent,
errorElement.getSource()
);
};

export const getAccountMenuButton = (
page: Page
): Promise<ElementHandle | null> =>
page: DappeteerPage
): Promise<DappeteerElementHandle | null> =>
page.waitForXPath(`//button[contains(@title,'Account options')]`, {
visible: true,
});
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export { getMetaMask, getMetaMaskWindow } from "./metamask";
export * from "./types";
export * from "./setup";
export { DapeteerJestConfig as DappateerJestConfig } from "./jest/global";
export { DapeteerJestConfig } from "./jest/global";

// default constants
export const RECOMMENDED_METAMASK_VERSION = "v10.20.0";
5 changes: 4 additions & 1 deletion src/jest/DappeteerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import NodeEnvironment from "jest-environment-node";
import puppeteer from "puppeteer";

import { getMetaMaskWindow } from "../index";
import { DPuppeteerBrowser } from "../puppeteer";

class DappeteerEnvironment extends NodeEnvironment {
constructor(config: Config.ProjectConfig) {
Expand All @@ -23,7 +24,9 @@ class DappeteerEnvironment extends NodeEnvironment {
browserWSEndpoint: wsEndpoint,
});
this.global.browser = browser;
this.global.metamask = await getMetaMaskWindow(browser);
this.global.metamask = await getMetaMaskWindow(
new DPuppeteerBrowser(browser, false)
);
this.global.page = await browser.newPage();
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/jest/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import path from "path";
import { existsSync } from "node:fs";
import { cwd } from "node:process";

import { RECOMMENDED_METAMASK_VERSION } from "../index";
import { LaunchOptions } from "../types";
import { DappeteerLaunchOptions, RECOMMENDED_METAMASK_VERSION } from "../index";

import { DapeteerJestConfig } from "./global";

export const DAPPETEER_DEFAULT_CONFIG: LaunchOptions = {
export const DAPPETEER_DEFAULT_CONFIG: DappeteerLaunchOptions = {
metaMaskVersion: RECOMMENDED_METAMASK_VERSION,
browser: "chrome",
};

export async function getDappeteerConfig(): Promise<DapeteerJestConfig> {
Expand Down
12 changes: 6 additions & 6 deletions src/jest/global.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Browser, Page } from "puppeteer";

import { Dappeteer, LaunchOptions, MetaMaskOptions } from "..";
import { Dappeteer, DappeteerLaunchOptions, MetaMaskOptions } from "..";
import { DappeteerBrowser } from "../browser";
import { DappeteerPage } from "../page";

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
page: Page;
browser: Browser;
page: DappeteerPage;
browser: DappeteerBrowser;
metaMask: Dappeteer;
}
}
}

export type DapeteerJestConfig = Partial<{
dappeteer: LaunchOptions;
dappeteer: DappeteerLaunchOptions;
metaMask: MetaMaskOptions;
}>;
4 changes: 1 addition & 3 deletions src/jest/setup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import puppeteer from "puppeteer";

import { launch, setupMetaMask } from "../index";

import { getDappeteerConfig } from "./config";

export default async function (): Promise<void> {
const { dappeteer, metaMask } = await getDappeteerConfig();

const browser = await launch(puppeteer, dappeteer);
const browser = await launch(dappeteer);
try {
await setupMetaMask(browser, metaMask);
global.browser = browser;
Expand Down

0 comments on commit c4c0e5f

Please sign in to comment.