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

feat!: add playwright support #167

Merged
merged 6 commits into from
Nov 9, 2022
Merged
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
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);
mpetrunic marked this conversation as resolved.
Show resolved Hide resolved
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),
Copy link
Contributor

Choose a reason for hiding this comment

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

As a suggestion, we can use testing-library to have similar selectors regardless of the framework or platform we are using. I find it annoying to learn all the different ways of searching for the element as implementations are different. So, why not use the library which is widely used across developers to have a similar API for querying the UI elements?

https://testing-library.com/docs/pptr-testing-library/intro
https://github.com/testing-library/playwright-testing-library

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems good, could you open issue with your suggested approach?

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
Loading