Writing MetaMask Tests with Synpress
A practical guide to writing MetaMask tests with Synpress, including the issues I ran into and the workarounds that helped.
When you need to write tests for a Web3 application, Synpress can be one of the most practical options. It is built as a Cypress plugin, and in this article I want to share my experience with it, including the problems I ran into along the way. This guide is written for readers who already have basic knowledge of Web3 testing and want to go a little deeper.
Installation
Install Synpress and Cypress with the following commands:
npm install --save-dev [email protected]
npm install --save-dev [email protected]
Note: I used Synpress 3.7.1 because the newer versions were still in beta and did not work reliably in my setup. I used Cypress 12.17.3 because of this issue.
Here is an example run script for package.json:
"scripts": {
"synpress:run": "env-cmd -f .env.testing synpress run --config='fixturesFolder=tests/e2e/fixtures'"
}
Here, we define both a custom environment file and an additional config value.
Project structure:
project_dir
└── src
└── tests
└── e2e
└── .eslintrc.js
└── support.js
└── tsconfig.json
└── specs
└── example-spec.js
└── pages
└── example-page.js
- Create the
.eslintrc.jsfile under/project_dir/tests/e2e:
const path = require("path");
const synpressPath = path.join(
process.cwd(),
"/node_modules/@synthetixio/synpress"
);
module.exports = {
extends: `${synpressPath}/.eslintrc.js`,
};
- Create the
support.jsfile under/project_dir/tests/e2e:
import "@synthetixio/synpress/support/index";
- Create the
tsconfig.jsonfile under/project_dir/tests/e2e:
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../../node_modules",
"types": [
"cypress",
"@synthetixio/synpress/support",
"cypress-wait-until",
"@testing-library/cypress"
],
"outDir": "./output"
},
"include": ["**/*.*"]
}
After completing these steps, you will need an environment file:
NETWORK_NAME="HardhatNetwork"
RPC_URL=http://127.0.0.1:8545/
CHAIN_ID=31337
SYMBOL=ETH
PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
PASSWORD="your_password"
In this .env.testing example, I start a blockchain network with Hardhat. You should update NETWORK_NAME, RPC_URL, CHAIN_ID, SYMBOL, PASSWORD, and either PRIVATE_KEY or SECRET_WORDS according to your own network. The example package.json script loads this .env.testing file through env-cmd.
Usage
You can write your tests under the specs folder. Here is an example test file:
describe("Wallet", () => {
it("connect wallet", () => {
cy.visit("/wallet");
cy.get("h1").should("contain", "Wallet");
cy.get("button").contains("Connect").click();
cy.acceptMetamaskAccess();
cy.contains("p.text-body", "Connected address");
cy.contains("p.address", cy.getMetamaskAddress());
});
});
Let's walk through this test. Before the test starts, you can think of Synpress as running a setup step. It uses the environment values you defined, calls cy.setupMetamask(), opens the MetaMask wallet, and prepares the wallet before the test itself begins. If you get an error at this stage, it is usually related to configuration.
In the example, we visit the wallet route and check that the h1 contains "Wallet". Then we click the Connect button. At that point, MetaMask asks for access, and we accept it with cy.acceptMetamaskAccess(). After that, we check that p.text-body contains "Connected address". Finally, we verify that p.address contains the MetaMask address.
In these tests, you can use the Synpress APIs in addition to the Cypress API.
Despite the Synpress documentation, you may run into cases where the examples do not work because of environment-related issues. Pay close attention to those values. I especially recommend avoiding METAMASK_VERSION=latest, because the plugin is usually built against a specific MetaMask version and may break with newer releases.
I could not run Synpress reliably in --headless mode because I kept getting wallet-specific errors. I also saw problems in synpress open: during tests, the MetaMask wallet would get stuck and fail to open, then show a message asking whether I wanted to restart the wallet. I was not able to solve those issues.
For environment management, the only approach that worked reliably for me was the env-cmd setup shown above.
I also had to add cy.wait(5000) around cy.confirmMetamaskPermissionToSpend() calls. Without that wait, some flows did not have enough time to complete. This happened even on my local Hardhat network, so if you use a testnet, you should pay extra attention to timing.
When importing a second account, you may run into the autoconnect problem. I worked around it with the following hacky solution:
describe("projects", () => {
it("change account", () => {
cy.visit("/wallet");
cy.disconnectMetamaskWalletFromAllDapps();
cy.fixture("user2.json").then((user) => {
cy.importMetamaskAccount(user.privateKey);
cy.switchMetamaskAccount(3);
});
cy.resetMetamaskAccount();
cy.get("button").contains("Connect").click();
cy.acceptMetamaskAccess({ allAccounts: true });
cy.contains("p.text-body1", "Connected address");
});
});Conclusion
It is possible to write MetaMask tests with Synpress, but in my experience there are still several rough edges. I spent more time than I initially planned solving these issues. My goal with this article is to help you recognize the most likely problems early and avoid losing the same amount of time.