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

feat: add support for installing metamask flask #153

Merged
merged 2 commits into from
Oct 10, 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
5 changes: 4 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
checks:
name: Checks
runs-on: ubuntu-latest
strategy:
matrix:
mm-version: [mm, flask]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand All @@ -25,7 +28,7 @@ jobs:
- name: Lint
run: yarn run lint
- name: Tests
run: xvfb-run --auto-servernum yarn run test --timeout 50000
run: 'xvfb-run --auto-servernum yarn run test:${{ matrix.mm-version }} --timeout 50000'
- uses: actions/upload-artifact@v3
if: always()
with:
Expand Down
3 changes: 0 additions & 3 deletions .mocharc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@ recursive: true
color: true
timeout: 20000
exit: true
require:
- 'ts-node/register'
- 'test/global.ts'
spec:
- 'test/**/*.spec.ts'
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
"build": "tsc -p tsconfig.build.json",
"lint": "eslint --color --ext .ts src/ test/",
"lint:fix": "yarn run lint --fix",
"test": "mocha",
"test:dev": "mocha --timeout 36000000"
"test": "yarn run test:*",
"test:mm": "mocha --require ts-node/register --require test/global.ts",
"test:flask": "mocha --require ts-node/register --require test/global_flask.ts"
},
"repository": {
"type": "git",
Expand Down
12 changes: 9 additions & 3 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ export const openNetworkDropdown = async (page: Page): Promise<void> => {
};

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

export const openAccountDropdown = async (page: Page): Promise<void> => {
const accMenu = await getAccountMenuButton(page);
await accMenu.click();
await page.waitForSelector(".menu__container.account-options-menu");
await page.waitForSelector(".menu__container.account-options-menu", {
visible: true,
});
};

export const clickOnElement = async (
Expand All @@ -64,7 +68,9 @@ export const clickOnButton = async (
};

export const clickOnLogo = async (page: Page): Promise<void> => {
const header = await page.waitForSelector(".app-header__logo-container");
const header = await page.waitForSelector(".app-header__logo-container", {
visible: true,
});
await header.click();
};

Expand Down
4 changes: 3 additions & 1 deletion src/helpers/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ export const getErrorMessage = async (page: Page): Promise<string | false> => {
export const getAccountMenuButton = (
page: Page
): Promise<ElementHandle | null> =>
page.waitForXPath(`//button[contains(@title,'Account options')]`);
page.waitForXPath(`//button[contains(@title,'Account options')]`, {
visible: true,
});
4 changes: 3 additions & 1 deletion src/metamask/addToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export const addToken =
async ({ tokenAddress, symbol, decimals = 0 }: AddToken): Promise<void> => {
await page.bringToFront();
await clickOnButton(page, "Assets");
await page.waitForSelector(".asset-list-item__token-button");
await page.waitForSelector(".asset-list-item__token-button", {
visible: true,
});
await clickOnElement(page, "import tokens");
await clickOnButton(page, "Custom token");

Expand Down
23 changes: 19 additions & 4 deletions src/setup/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { LaunchOptions } from "../types";
import { isNewerVersion } from "./isNewerVersion";
import downloader from "./metaMaskDownloader";

export type DappeteerBrowser = puppeteer.Browser & { flask?: boolean };

/**
* Launch Puppeteer chromium instance with MetaMask plugin installed
* */
export async function launch(
puppeteerLib: typeof puppeteer,
options: LaunchOptions
): Promise<puppeteer.Browser> {
): Promise<DappeteerBrowser> {
if (
!options ||
(!options.metaMaskVersion && !(options as CustomOptions).metaMaskPath)
Expand Down Expand Up @@ -50,19 +52,27 @@ export async function launch(
`Seems you are running older version of MetaMask that recommended by dappeteer team.
Use it at your own risk or set the recommended version "${RECOMMENDED_METAMASK_VERSION}".`
);
else console.log(`Running tests on MetaMask version ${metaMaskVersion}`);
else
console.log(
`Running tests on MetaMask version ${metaMaskVersion} (flask: ${String(
options.metaMaskFlask ?? false
)})`
);

console.log(); // new line

METAMASK_PATH = await downloader(metaMaskVersion, metaMaskLocation);
METAMASK_PATH = await downloader(metaMaskVersion, {
location: metaMaskLocation,
flask: options.metaMaskFlask,
});
} else {
console.log(`Running tests on local MetaMask build`);

METAMASK_PATH = (rest as CustomOptions).metaMaskPath;
/* eslint-enable no-console */
}

return puppeteerLib.launch({
const browser = await puppeteerLib.launch({
headless: false,
args: [
`--disable-extensions-except=${METAMASK_PATH}`,
Expand All @@ -71,4 +81,9 @@ export async function launch(
],
...rest,
});

if (options.metaMaskFlask) {
Object.assign(browser, { flask: true });
}
return browser;
}
42 changes: 30 additions & 12 deletions src/setup/metaMaskDownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export type Path =
extract: string;
};

export default async (version: string, location?: Path): Promise<string> => {
export default async (
version: string,
options?: { location?: Path; flask?: boolean }
): Promise<string> => {
const location = options.location;
const metaMaskDirectory =
typeof location === "string"
? location
Expand All @@ -26,17 +30,22 @@ export default async (version: string, location?: Path): Promise<string> => {
: location?.download || path.resolve(defaultDirectory, "download");

if (version !== "latest") {
const extractDestination = path.resolve(
metaMaskDirectory,
version.replace(/\./g, "_")
);
let filename = version.replace(/\./g, "_");
if (options?.flask) {
filename = "flask_" + filename;
}
const extractDestination = path.resolve(metaMaskDirectory, filename);
if (fs.existsSync(extractDestination)) return extractDestination;
}
const { filename, downloadUrl, tag } = await getMetaMaskReleases(version);
const extractDestination = path.resolve(
metaMaskDirectory,
tag.replace(/\./g, "_")
const { filename, downloadUrl, tag } = await getMetaMaskReleases(
version,
options?.flask ?? false
);
let destFilename = tag.replace(/\./g, "_");
if (options?.flask) {
destFilename = "flask_" + filename;
}
const extractDestination = path.resolve(metaMaskDirectory, destFilename);
if (!fs.existsSync(extractDestination)) {
const downloadedFile = await downloadMetaMaskReleases(
filename,
Expand Down Expand Up @@ -94,7 +103,10 @@ const downloadMetaMaskReleases = (
type MetaMaskReleases = { downloadUrl: string; filename: string; tag: string };
const metaMaskReleasesUrl =
"https://api.github.com/repos/metamask/metamask-extension/releases";
const getMetaMaskReleases = (version: string): Promise<MetaMaskReleases> =>
const getMetaMaskReleases = (
version: string,
flask: boolean
): Promise<MetaMaskReleases> =>
new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const request = get(
Expand All @@ -118,16 +130,22 @@ const getMetaMaskReleases = (version: string): Promise<MetaMaskReleases> =>
) {
for (const asset of result.assets) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (asset.name.includes("chrome"))
if (
(!flask && asset.name.includes("chrome")) ||
(flask &&
asset.name.includes("flask") &&
asset.name.includes("chrome"))
) {
resolve({
downloadUrl: asset.browser_download_url,
filename: asset.name,
tag: result.tag_name,
});
}
}
}
}
reject(`Version ${version} not found!`);
reject(`Version ${version} (flask: ${String(flask)}) not found!`);
});
}
);
Expand Down
4 changes: 4 additions & 0 deletions src/setup/setupActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export async function showTestNets(metaMaskPage: Page): Promise<void> {
await clickOnLogo(metaMaskPage);
}

export async function acceptTheRisks(metaMaskPage: Page): Promise<void> {
await clickOnButton(metaMaskPage, "I accept the risks");
}

export async function confirmWelcomeScreen(metaMaskPage: Page): Promise<void> {
await clickOnButton(metaMaskPage, "Get started");
}
Expand Down
20 changes: 17 additions & 3 deletions src/setup/setupMetaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Browser, BrowserContext, Page, Target } from "puppeteer";
import { getMetaMask } from "../metamask";
import { Dappeteer, MetaMaskOptions } from "../types";

import { DappeteerBrowser } from "./launch";
import {
acceptTheRisks,
closePortfolioTooltip,
closeWhatsNewModal,
confirmWelcomeScreen,
Expand All @@ -25,14 +27,26 @@ const defaultMetaMaskSteps: Step<MetaMaskOptions>[] = [
closeWhatsNewModal,
closeWhatsNewModal,
];
const flaskMetaMaskSteps: Step<MetaMaskOptions>[] = [
acceptTheRisks,
importAccount,
showTestNets,
closePortfolioTooltip,
closeWhatsNewModal,
closeWhatsNewModal,
];

export async function setupMetaMask<Options = MetaMaskOptions>(
browser: Browser | BrowserContext,
browser: Browser | BrowserContext | DappeteerBrowser,
options?: Options,
steps: Step<Options>[] = defaultMetaMaskSteps
steps?: Step<Options>[]
): Promise<Dappeteer> {
const page = await getMetamaskPage(browser);
await page.setViewport({ height: 1200, width: 800 });
steps = steps ?? defaultMetaMaskSteps;
if ((browser as DappeteerBrowser).flask) {
steps = flaskMetaMaskSteps;
}
await page.setViewport({ height: 800, width: 800 });
// goes through the installation steps required by MetaMask
for (const step of steps) {
await step(page, options);
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Path } from "./setup/metaMaskDownloader";

import { RECOMMENDED_METAMASK_VERSION } from "./index";

export type LaunchOptions = OfficialOptions | CustomOptions;
export type LaunchOptions = (OfficialOptions | CustomOptions) & {
//install flask (canary) version of metamask.
metaMaskFlask?: boolean;
};

type PuppeteerLaunchOptions = puppeteer.LaunchOptions &
puppeteer.BrowserLaunchArgumentOptions &
Expand Down
2 changes: 1 addition & 1 deletion test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Page } from "puppeteer";
import * as dappeteer from "../src";
import { openProfileDropdown } from "../src/helpers";

import { PASSWORD, TestContext } from "./global";
import { PASSWORD, TestContext } from "./constant";
import { clickElement } from "./utils/utils";

use(chaiAsPromised);
Expand Down
25 changes: 25 additions & 0 deletions test/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import http from "http";

import { Provider, Server } from "ganache";
import { Browser } from "puppeteer";

import { Dappeteer } from "../src";

import { Contract } from "./deploy";

export type InjectableContext = Readonly<{
provider: Provider;
ethereum: Server<"ethereum">;
testPageServer: http.Server;
browser: Browser;
metamask: Dappeteer;
contract: Contract;
flask: boolean;
}>;

// TestContext will be used by all the test
export type TestContext = Mocha.Context & InjectableContext;

export const LOCAL_PREFUNDED_MNEMONIC =
"pioneer casual canoe gorilla embrace width fiction bounce spy exhibit another dog";
export const PASSWORD = "password1234";
2 changes: 1 addition & 1 deletion test/contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Page } from "puppeteer";

import { Dappeteer } from "../src";

import { TestContext } from "./constant";
import { Contract } from "./deploy";
import { TestContext } from "./global";
import { clickElement, pause } from "./utils/utils";

describe("contract interactions", function () {
Expand Down
33 changes: 8 additions & 25 deletions test/global.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
import http from "http";
import path from "path";

import { Provider, Server } from "ganache";
import puppeteer, { Browser } from "puppeteer";
import puppeteer from "puppeteer";

import * as dappeteer from "../src";
import { Dappeteer } from "../src";

import {
Contract,
deployContract,
startLocalEthereum,
startTestServer,
} from "./deploy";

export type InjectableContext = Readonly<{
provider: Provider;
ethereum: Server<"ethereum">;
testPageServer: http.Server;
browser: Browser;
metamask: Dappeteer;
contract: Contract;
}>;

// TestContext will be used by all the test
export type TestContext = Mocha.Context & InjectableContext;

export const LOCAL_PREFUNDED_MNEMONIC =
"pioneer casual canoe gorilla embrace width fiction bounce spy exhibit another dog";
export const PASSWORD = "password1234";
InjectableContext,
LOCAL_PREFUNDED_MNEMONIC,
PASSWORD,
TestContext,
} from "./constant";
import { deployContract, startLocalEthereum, startTestServer } from "./deploy";

export const mochaHooks = {
async beforeAll(this: Mocha.Context): Promise<void> {
Expand Down Expand Up @@ -57,6 +39,7 @@ export const mochaHooks = {
browser,
testPageServer: server,
metamask,
flask: false,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
contract,
};
Expand Down
Loading