Skip to content

Commit 90f19f4

Browse files
authored
test: Create anvil manager class (#15046)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This is part 2/3 of Anvil Utils. The purpose of this PR is to add an AnvilManager class, which is essentially a wrapper around @viem/anvil that manages an anvil instance that will be used in our tests. It includes: - Lifecycle methods (start, quit) for controlling the server - A `getProvider()` method that returns wallet, public, and test clients - Utility methods like getAccounts and setAccountBalance ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMQA-524 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent 8260aed commit 90f19f4

10 files changed

+535
-53
lines changed

e2e/api-specs/run-api-spec-tests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/* eslint-disable import/no-commonjs */
22
require('@babel/register');
3+
require('ts-node/register');
34
require('./json-rpc-coverage.js');

e2e/fixtures/fixture-helper.js

Lines changed: 149 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
/* eslint-disable no-console, import/no-nodejs-modules */
22
import FixtureServer, { DEFAULT_FIXTURE_SERVER_PORT } from './fixture-server';
33
import FixtureBuilder from './fixture-builder';
4+
import { AnvilManager, defaultOptions } from '../seeder/anvil-manager';
45
import Ganache from '../../app/util/test/ganache';
6+
57
import GanacheSeeder from '../../app/util/test/ganache-seeder';
68
import axios from 'axios';
79
import path from 'path';
810
import createStaticServer from '../create-static-server';
9-
import { DEFAULT_MOCKSERVER_PORT, getFixturesServerPort, getLocalTestDappPort, getMockServerPort } from './utils';
11+
import {
12+
DEFAULT_MOCKSERVER_PORT,
13+
getFixturesServerPort,
14+
getLocalTestDappPort,
15+
getMockServerPort,
16+
} from './utils';
1017
import Utilities from '../utils/Utilities';
11-
import { device } from 'detox';
1218
import TestHelpers from '../helpers';
1319
import { startMockServer, stopMockServer } from '../api-mocking/mock-server';
1420

@@ -31,6 +37,84 @@ const isFixtureServerStarted = async () => {
3137
}
3238
};
3339

40+
// SRP corresponding to the vault set in the default fixtures - it's an empty test account, not secret
41+
export const defaultGanacheOptions = {
42+
hardfork: 'london',
43+
mnemonic:
44+
'drive manage close raven tape average sausage pledge riot furnace august tip',
45+
};
46+
47+
/**
48+
*
49+
* Normalizes the localNodeOptions into a consistent format to handle different data structures.
50+
* Case 1: A string: localNodeOptions = 'anvil'
51+
* Case 2: Array of strings: localNodeOptions = ['anvil', 'bitcoin']
52+
* Case 3: Array of objects: localNodeOptions =
53+
* [
54+
* { type: 'anvil', options: {anvilOpts}},
55+
* { type: 'bitcoin',options: {bitcoinOpts}},
56+
* ]
57+
* Case 4: Options object without type: localNodeOptions = {options}
58+
*
59+
* @param {string | object | Array} localNodeOptions - The input local node options.
60+
* @returns {Array} The normalized local node options.
61+
*/
62+
function normalizeLocalNodeOptions(localNodeOptions) {
63+
if (typeof localNodeOptions === 'string') {
64+
// Case 1: Passing a string
65+
return [
66+
{
67+
type: localNodeOptions,
68+
options:
69+
localNodeOptions === 'ganache'
70+
? defaultGanacheOptions
71+
: localNodeOptions === 'anvil'
72+
? defaultOptions
73+
: {},
74+
},
75+
];
76+
} else if (Array.isArray(localNodeOptions)) {
77+
return localNodeOptions.map((node) => {
78+
if (typeof node === 'string') {
79+
// Case 2: Array of strings
80+
return {
81+
type: node,
82+
options:
83+
node === 'ganache'
84+
? defaultGanacheOptions
85+
: node === 'anvil'
86+
? defaultOptions
87+
: {},
88+
};
89+
}
90+
if (typeof node === 'object' && node !== null) {
91+
// Case 3: Array of objects
92+
const type = node.type || 'ganache';
93+
return {
94+
type,
95+
options:
96+
type === 'ganache'
97+
? { ...defaultGanacheOptions, ...(node.options || {}) }
98+
: type === 'anvil'
99+
? { ...defaultOptions, ...(node.options || {}) }
100+
: node.options || {},
101+
};
102+
}
103+
throw new Error(`Invalid localNodeOptions entry: ${node}`);
104+
});
105+
}
106+
if (typeof localNodeOptions === 'object' && localNodeOptions !== null) {
107+
// Case 4: Passing an options object without type
108+
return [
109+
{
110+
type: 'ganache',
111+
options: { ...defaultGanacheOptions, ...localNodeOptions },
112+
},
113+
];
114+
}
115+
throw new Error(`Invalid localNodeOptions type: ${typeof localNodeOptions}`);
116+
}
117+
34118
/**
35119
* Loads a fixture into the fixture server.
36120
*
@@ -99,40 +183,74 @@ export async function withFixtures(options, testSuite) {
99183
smartContract,
100184
disableGanache,
101185
dapp,
186+
localNodeOptions = 'ganache',
102187
dappOptions,
103188
dappPath = undefined,
104189
dappPaths,
105190
testSpecificMock,
106-
launchArgs
191+
launchArgs,
107192
} = options;
108193

109194
const fixtureServer = new FixtureServer();
110195
let mockServer;
111196
let mockServerPort = DEFAULT_MOCKSERVER_PORT;
197+
const localNodeOptsNormalized = normalizeLocalNodeOptions(localNodeOptions);
112198

113199
if (testSpecificMock) {
114200
mockServerPort = getMockServerPort();
115201
mockServer = await startMockServer(testSpecificMock, mockServerPort);
116202
}
117203

118-
let ganacheServer;
119-
if (!disableGanache) {
120-
ganacheServer = new Ganache();
204+
let localNode;
205+
const localNodes = [];
206+
207+
try {
208+
// Start servers based on the localNodes array
209+
if (!disableGanache) {
210+
for (let i = 0; i < localNodeOptsNormalized.length; i++) {
211+
const nodeType = localNodeOptsNormalized[i].type;
212+
const nodeOptions = localNodeOptsNormalized[i].options || {};
213+
214+
switch (nodeType) {
215+
case 'anvil':
216+
localNode = new AnvilManager();
217+
await localNode.start(nodeOptions);
218+
localNodes.push(localNode);
219+
await localNode.setAccountBalance('1200');
220+
221+
break;
222+
223+
case 'ganache':
224+
localNode = new Ganache();
225+
await localNode.start(nodeOptions);
226+
localNodes.push(localNode);
227+
break;
228+
229+
case 'none':
230+
break;
231+
232+
default:
233+
throw new Error(
234+
`Unsupported localNode: '${nodeType}'. Cannot start the server.`,
235+
);
236+
}
237+
}
238+
}
239+
} catch (error) {
240+
console.error(error);
241+
throw error;
121242
}
243+
122244
const dappBasePort = getLocalTestDappPort();
123245
let numberOfDapps = dapp ? 1 : 0;
124246
const dappServer = [];
125247

126248
try {
127249
let contractRegistry;
128-
if (ganacheOptions && !disableGanache) {
129-
await ganacheServer.start(ganacheOptions);
130-
131-
if (smartContract) {
132-
const ganacheSeeder = new GanacheSeeder(ganacheServer.getProvider());
133-
await ganacheSeeder.deploySmartContract(smartContract);
134-
contractRegistry = ganacheSeeder.getContractRegistry();
135-
}
250+
if (!disableGanache && smartContract) {
251+
const ganacheSeeder = new GanacheSeeder(localNodes[0].getProvider());
252+
await ganacheSeeder.deploySmartContract(smartContract);
253+
contractRegistry = ganacheSeeder.getContractRegistry();
136254
}
137255

138256
if (dapp) {
@@ -171,26 +289,30 @@ export async function withFixtures(options, testSuite) {
171289
);
172290
// Due to the fact that the app was already launched on `init.js`, it is necessary to
173291
// launch into a fresh installation of the app to apply the new fixture loaded perviously.
174-
if (restartDevice) {
175-
await TestHelpers.launchApp({
176-
delete: true,
177-
launchArgs: {
178-
fixtureServerPort: `${getFixturesServerPort()}`,
179-
detoxURLBlacklistRegex: Utilities.BlacklistURLs,
180-
mockServerPort: `${mockServerPort}`,
181-
...(launchArgs || {}),
182-
},
183-
});
292+
if (restartDevice) {
293+
await TestHelpers.launchApp({
294+
delete: true,
295+
launchArgs: {
296+
fixtureServerPort: `${getFixturesServerPort()}`,
297+
detoxURLBlacklistRegex: Utilities.BlacklistURLs,
298+
mockServerPort: `${mockServerPort}`,
299+
...(launchArgs || {}),
300+
},
301+
});
184302
}
185303

186-
await testSuite({ contractRegistry, mockServer });
304+
await testSuite({ contractRegistry, mockServer, localNodes }); // Pass localNodes instead of anvilServer
187305
} catch (error) {
188306
console.error(error);
189307
throw error;
190308
} finally {
191-
if (ganacheOptions && !disableGanache) {
192-
await ganacheServer.quit();
309+
// Clean up all local nodes
310+
for (const server of localNodes) {
311+
if (server) {
312+
await server.quit();
313+
}
193314
}
315+
194316
if (dapp) {
195317
for (let i = 0; i < numberOfDapps; i++) {
196318
if (dappServer[i] && dappServer[i].listening) {
@@ -213,10 +335,3 @@ export async function withFixtures(options, testSuite) {
213335
await stopFixtureServer(fixtureServer);
214336
}
215337
}
216-
217-
// SRP corresponding to the vault set in the default fixtures - it's an empty test account, not secret
218-
export const defaultGanacheOptions = {
219-
hardfork: 'london',
220-
mnemonic:
221-
'drive manage close raven tape average sausage pledge riot furnace august tip',
222-
};

e2e/pages/Network/NetworkListModal.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class NetworkListModal {
7070

7171
async changeNetworkTo(networkName, custom) {
7272
const elem = this.getCustomNetwork(networkName, custom);
73+
await TestHelpers.delay(3000);
7374
await Gestures.waitAndTap(elem);
7475
await TestHelpers.delay(3000);
7576
}
@@ -103,6 +104,7 @@ class NetworkListModal {
103104
}
104105

105106
async tapAddNetworkButton() {
107+
await TestHelpers.delay(3000);
106108
await Gestures.waitAndTap(this.addPopularNetworkButton);
107109
}
108110
async deleteNetwork() {

e2e/seeder/anvil-clients.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
http,
66
} from 'viem';
77
import { anvil as baseAnvil } from 'viem/chains';
8-
98
/**
109
* Creates a set of clients for interacting with an Anvil test node
1110
* @param {number} chainId - The chain ID for the network

0 commit comments

Comments
 (0)