dappress 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/profile.js DELETED
@@ -1,125 +0,0 @@
1
- // Onboard the wallet once, in a browser of our own, and keep the resulting
2
- // profile. Each Cypress run then starts from a copy of it, with the wallet
3
- // imported but nothing else: no connected site, no added network.
4
-
5
- const crypto = require('node:crypto');
6
- const fs = require('node:fs');
7
- const os = require('node:os');
8
- const path = require('node:path');
9
- const puppeteer = require('puppeteer-core');
10
- const metamask = require('./metamask');
11
- const { findExtensionId, getHomePage } = require('./metamask-pages');
12
-
13
- // One build at a time: Cypress calls before:browser:launch again when the
14
- // browser is slow to connect, and two builds on one profile would collide
15
- const builds = new Map();
16
-
17
- /** The cached profile for these options, built on first use. */
18
- function prepareProfile({ browserPath, extensionDir, options }) {
19
- const key = crypto.createHash('sha256').update([options.metamaskVersion, options.seedPhrase, options.password, options.backupAndSync].join('|')).digest('hex').slice(0, 16);
20
- if (!builds.has(key)) builds.set(key, buildProfile(path.join(options.cacheDir, 'profiles', key), { browserPath, extensionDir, options }));
21
- return builds.get(key);
22
- }
23
-
24
- async function buildProfile(dir, { browserPath, extensionDir, options }) {
25
- if (fs.existsSync(path.join(dir, 'ready'))) return dir;
26
-
27
- console.log('[dappress] Importing the wallet once, into a cached profile…');
28
- await fs.promises.rm(dir, { recursive: true, force: true });
29
- const launch = () =>
30
- puppeteer.launch({
31
- executablePath: browserPath,
32
- headless: false,
33
- userDataDir: dir,
34
- args: [
35
- `--load-extension=${extensionDir}`,
36
- `--disable-extensions-except=${extensionDir}`,
37
- '--no-first-run',
38
- // No sandbox on Linux: Ubuntu 23.10+ (GitHub's ubuntu-latest) blocks the
39
- // user namespaces it needs, and this browser only imports a test wallet
40
- ...(process.platform === 'linux' ? ['--no-sandbox'] : []),
41
- ],
42
- });
43
-
44
- await withMetaMask(launch, (home) => metamask.onboard(home, options));
45
- // Reopen the profile: the wallet must now be there, locked, with no onboarding left
46
- await withMetaMask(launch, async (home) => {
47
- const state = await metamask.walletState(home);
48
- if (state !== 'locked') throw new Error(`[dappress] The cached profile did not keep the wallet: MetaMask is ${state} at ${home.url()}`);
49
- await metamask.unlock(home, options);
50
- });
51
- fs.writeFileSync(path.join(dir, 'ready'), '');
52
- return dir;
53
- }
54
-
55
- // MetaMask persists its state a moment after the last screen; leave it that moment before closing
56
- async function withMetaMask(launch, action) {
57
- const browser = await launch();
58
- try {
59
- const home = await getHomePage(browser, await findExtensionId(browser));
60
- await keepInFront(browser, home);
61
- await action(home);
62
- await new Promise((resolve) => setTimeout(resolve, 3000));
63
- } finally {
64
- await browser.close();
65
- }
66
- }
67
-
68
- // Chrome stops rendering a tab that isn't the active one, and Puppeteer waits
69
- // on rendering to find an element or to click it: a hidden tab never answers.
70
- // So the home page is made the active tab, and again whenever MetaMask opens
71
- // a tab of its own, as it does for its onboarding right after the install.
72
- async function keepInFront(browser, home) {
73
- const toFront = () => home.bringToFront().catch(() => {});
74
- await toFront();
75
- browser.on('targetcreated', (target) => {
76
- if (target.type() !== 'page') return;
77
- toFront();
78
- setTimeout(toFront, 500);
79
- });
80
- }
81
-
82
- // Where Cypress is about to create the browser profile:
83
- // <app data>/Cypress/cy/<env>/browsers/<name>-<channel>/run-<pid of the Cypress process>,
84
- // or .../interactive in open mode. The pid is our parent's: Cypress runs
85
- // setupNodeEvents in a child process. Returns null when it can't be found.
86
- function cypressProfileDir(browser, isTextTerminal) {
87
- const appData = [
88
- path.join(os.homedir(), 'Library', 'Application Support'),
89
- process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
90
- process.env.APPDATA,
91
- ].filter(Boolean);
92
- const cypressData = appData.map((dir) => path.join(dir, 'Cypress', 'cy')).find((dir) => fs.existsSync(dir));
93
- if (!cypressData) return null;
94
- const env = ['production', 'development'].find((name) => fs.existsSync(path.join(cypressData, name)));
95
- if (!env) return null;
96
- return path.join(cypressData, env, 'browsers', `${browser.name}-${browser.channel}`, isTextTerminal ? `run-${process.ppid}` : 'interactive');
97
- }
98
-
99
- /**
100
- * Copy MetaMask's data from the cached profile into the profile Cypress is
101
- * about to launch. Returns false when that profile can't be located; the
102
- * wallet is then imported during the run instead.
103
- */
104
- async function installProfile(profileDir, browser, isTextTerminal) {
105
- const userDataDir = cypressProfileDir(browser, isTextTerminal);
106
- if (!userDataDir) {
107
- console.warn('[dappress] Could not locate the Cypress browser profile; importing the wallet in this run instead');
108
- return false;
109
- }
110
- for (const entry of metamaskData(profileDir)) {
111
- await fs.promises.cp(path.join(profileDir, entry), path.join(userDataDir, entry), { recursive: true });
112
- }
113
- return true;
114
- }
115
-
116
- // Where Chrome keeps an extension's storage: chrome.storage.local and IndexedDB
117
- function metamaskData(profileDir) {
118
- const base = path.join(profileDir, 'Default');
119
- const entries = ['Local Extension Settings', 'IndexedDB'];
120
- return entries
121
- .filter((entry) => fs.existsSync(path.join(base, entry)))
122
- .flatMap((entry) => fs.readdirSync(path.join(base, entry)).map((name) => path.join('Default', entry, name)));
123
- }
124
-
125
- module.exports = { prepareProfile, installProfile };
package/src/support.js DELETED
@@ -1,90 +0,0 @@
1
- // Cypress commands. Import this file from your support file:
2
- //
3
- // import 'dappress/support';
4
- //
5
- // Each command asks the Node side (src/actions.js) to drive MetaMask through
6
- // a Cypress task, then yields back to the test.
7
-
8
- const options = Cypress.expose('dappress') || {};
9
-
10
- // `shown` is what the command log displays for the argument, if anything
11
- function metamask(action, argument = null, shown = '') {
12
- Cypress.log({ name: 'metamask', message: `${action} ${shown}`.trim() });
13
- return cy.task(`dappress:${action}`, argument, { log: false });
14
- }
15
-
16
- function provider(method, params) {
17
- return cy.window({ log: false }).then((win) => win.ethereum.request({ method, params }));
18
- }
19
-
20
- Cypress.Commands.add('setupMetaMask', () => metamask('setupWallet'));
21
- Cypress.Commands.add('rejectConnection', () => metamask('rejectConnection'));
22
- Cypress.Commands.add('approveNewNetwork', () => metamask('approveNewNetwork'));
23
- Cypress.Commands.add('rejectNewNetwork', () => metamask('rejectNewNetwork'));
24
- Cypress.Commands.add('approveSwitchNetwork', () => metamask('approveSwitchNetwork'));
25
- Cypress.Commands.add('rejectSwitchNetwork', () => metamask('rejectSwitchNetwork'));
26
- Cypress.Commands.add('confirmSignature', () => metamask('confirmSignature'));
27
- Cypress.Commands.add('rejectSignature', () => metamask('rejectSignature'));
28
- Cypress.Commands.add('confirmTransaction', () => metamask('confirmTransaction'));
29
- Cypress.Commands.add('rejectTransaction', () => metamask('rejectTransaction'));
30
- Cypress.Commands.add('approveAddToken', () => metamask('approveAddToken'));
31
- Cypress.Commands.add('rejectAddToken', () => metamask('rejectAddToken'));
32
-
33
- // The wallet's own accounts. A new or imported account becomes the selected
34
- // one; the dapp sees it once it connects with it.
35
- Cypress.Commands.add('addAccount', () => metamask('addAccount'));
36
- Cypress.Commands.add('switchAccount', (name) => metamask('switchAccount', name, name));
37
- // The private key is kept out of the command log
38
- Cypress.Commands.add('importAccount', (privateKey) => metamask('importAccount', privateKey));
39
-
40
- // Connect, then move the dapp onto the network from the wallet setup, if there is one
41
- Cypress.Commands.add('connectToDapp', () => {
42
- metamask('connectToDapp');
43
- if (options.network) cy.useNetwork(options.network);
44
- });
45
-
46
- // Move the dapp onto `network` (a wallet_addEthereumChain parameter), adding
47
- // it to MetaMask if needed. A switch to a network the dapp is already allowed
48
- // on settles at once; anything else makes MetaMask ask, and we approve.
49
- Cypress.Commands.add('useNetwork', (network = options.network) => {
50
- if (!network) throw new Error('[dappress] cy.useNetwork() needs a network, or one in cypress/wallet.setup.js');
51
- Cypress.log({ name: 'metamask', message: `useNetwork ${network.chainName || network.chainId}` });
52
- cy.window({ log: false }).then((win) => {
53
- win.dappressNetwork = { settled: false };
54
- win.dappressNetwork.request = win.ethereum
55
- .request({ method: 'wallet_addEthereumChain', params: [network] })
56
- .then((result) => ({ result }), (error) => ({ error }))
57
- .finally(() => (win.dappressNetwork.settled = true));
58
- });
59
- cy.wait(1500, { log: false });
60
- cy.window({ log: false }).then((win) => {
61
- if (!win.dappressNetwork.settled) metamask('approveNetworkChange');
62
- });
63
- cy.window({ log: false }).then({ timeout: 60000 }, (win) => win.dappressNetwork.request).then(({ error }) => {
64
- if (error) throw new Error(`[dappress] MetaMask refused the network: ${error.message}`);
65
- });
66
- waitForChainId(network.chainId);
67
- });
68
-
69
- // The address the dapp sees, asked to the injected provider rather than to the
70
- // MetaMask UI, so it keeps working across MetaMask versions.
71
- Cypress.Commands.add('getAccountAddress', () => {
72
- Cypress.log({ name: 'metamask', message: 'getAccountAddress' });
73
- return provider('eth_accounts').then(([address]) => address);
74
- });
75
-
76
- // MetaMask announces the new chain to the dapp shortly after the switch
77
- function waitForChainId(chainId, attempts = 20) {
78
- return provider('eth_chainId').then((current) => {
79
- if (current === chainId) return current;
80
- if (attempts === 0) throw new Error(`[dappress] The dapp is on chain ${current}, not ${chainId}`);
81
- return cy.wait(500, { log: false }).then(() => waitForChainId(chainId, attempts - 1));
82
- });
83
- }
84
-
85
- // Onboard or unlock the wallet once per spec, before the dapp needs it.
86
- if (options.autoSetup !== false) {
87
- before(() => {
88
- cy.setupMetaMask();
89
- });
90
- }
package/types/index.d.ts DELETED
@@ -1,55 +0,0 @@
1
- /// <reference types="cypress" />
2
-
3
- /** A network, as passed to MetaMask's wallet_addEthereumChain. */
4
- export interface Network {
5
- chainId: string;
6
- chainName: string;
7
- rpcUrls: string[];
8
- nativeCurrency: { name: string; symbol: string; decimals: number };
9
- blockExplorerUrls?: string[];
10
- iconUrls?: string[];
11
- }
12
-
13
- /** What cypress/wallet.setup.ts exports. */
14
- export interface WalletSetup {
15
- /**
16
- * The wallet to import. Without one, Dappress makes a new wallet for each run.
17
- * Prefer DAPPRESS_SEED_PHRASE in cypress.env.json or the environment: this file is usually committed.
18
- */
19
- seedPhrase?: string;
20
- /** The network the dapp is moved onto by cy.connectToDapp(). */
21
- network?: Network;
22
- }
23
-
24
- export interface DappressOptions extends WalletSetup {
25
- /** Password of the throwaway browser profile's wallet. Default: Tester@1234. */
26
- password?: string;
27
- /** MetaMask release to load, downloaded from GitHub on first run. */
28
- metamaskVersion?: string;
29
- /**
30
- * Keep MetaMask's backup and sync on. It saves the accounts and contacts of a
31
- * seed phrase and restores them on other installs. Default: false.
32
- */
33
- backupAndSync?: boolean;
34
- /** Run cy.setupMetaMask() before the first test of each spec. Default: true. */
35
- autoSetup?: boolean;
36
- /** Import the wallet once and reuse the profile across runs. Keeps the vault under cacheDir. Headed runs only. Default: false. */
37
- cache?: boolean;
38
- /** Time allowed for MetaMask to display a request, in ms. Default: 20000. */
39
- timeout?: number;
40
- /** Where MetaMask builds are cached. Default: ~/.cache/dappress. */
41
- cacheDir?: string;
42
- }
43
-
44
- /**
45
- * Wire Dappress into a Cypress project, from setupNodeEvents:
46
- *
47
- * setupNodeEvents(on, config) {
48
- * return configureDappress(on, config);
49
- * }
50
- */
51
- export function configureDappress(
52
- on: Cypress.PluginEvents,
53
- config: Cypress.PluginConfigOptions,
54
- options?: DappressOptions,
55
- ): Cypress.PluginConfigOptions;
@@ -1,42 +0,0 @@
1
- /// <reference types="cypress" />
2
-
3
- import type { Network } from './index';
4
-
5
- export type WalletState = 'onboarding' | 'locked' | 'unlocked';
6
-
7
- declare global {
8
- namespace Cypress {
9
- interface Chainable {
10
- /** Imports or unlocks the wallet. Runs by itself before each spec unless autoSetup is false. */
11
- setupMetaMask(): Chainable<WalletState>;
12
- /** Accepts the connection request, then moves the dapp onto the wallet setup's network if there is one. */
13
- connectToDapp(): Chainable<void>;
14
- rejectConnection(): Chainable<void>;
15
- /** Answers a signature request (personal_sign, eth_signTypedData_*). */
16
- confirmSignature(): Chainable<void>;
17
- rejectSignature(): Chainable<void>;
18
- /** Answers a transaction, including ERC-20 approvals. */
19
- confirmTransaction(): Chainable<void>;
20
- rejectTransaction(): Chainable<void>;
21
- /** Answers wallet_addEthereumChain. */
22
- approveNewNetwork(): Chainable<void>;
23
- rejectNewNetwork(): Chainable<void>;
24
- /** Answers the permission asked before switching to a network the dapp isn't allowed on yet. */
25
- approveSwitchNetwork(): Chainable<void>;
26
- rejectSwitchNetwork(): Chainable<void>;
27
- /** Answers wallet_watchAsset ("Add suggested tokens"). */
28
- approveAddToken(): Chainable<void>;
29
- rejectAddToken(): Chainable<void>;
30
- /** Adds an account to the wallet and selects it. Yields its name, "Account N". */
31
- addAccount(): Chainable<string>;
32
- /** Selects an account of the wallet by its name. */
33
- switchAccount(name: string): Chainable<void>;
34
- /** Imports an account from its private key and selects it. Yields its name. */
35
- importAccount(privateKey: string): Chainable<string>;
36
- /** Moves the dapp onto a network, adding it to MetaMask if needed. Defaults to the wallet setup's network. Yields the chain id. */
37
- useNetwork(network?: Network): Chainable<string>;
38
- /** The address the dapp is connected with. */
39
- getAccountAddress(): Chainable<string>;
40
- }
41
- }
42
- }