Web3 Testing Guide

How to Test NFT Mint Transaction with Playwright: Confirmation Modals, Gas UI, and Chain State

A scenario-by-scenario guide to testing NFT mint flows end to end with Playwright. Covers wallet connection, transaction confirmation modals, gas estimation display, pending/success/fail UI states, ethers.js mocking, and running against a local Hardhat node.

$24.7B

NFT trading volume exceeded $24.7 billion in 2022 according to DappRadar's annual report, and minting remains the most critical user-facing transaction in any NFT marketplace.

DappRadar 2022 Annual Report

0Async state transitions per mint
0Scenarios covered
0%Fewer lines with Assrt
0sHardhat block time

NFT Mint Transaction End-to-End Flow

BrowserdApp FrontendWallet (MetaMask)Smart ContractBlockchain NodeClick Mint buttoneth_sendTransaction (mint call)Show confirmation modalUser confirms transactionBroadcast signed txExecute mint()Emit Transfer eventtx receipt (success/fail)Update UI: minted NFT

1. Why Testing NFT Mint Transactions Is Harder Than It Looks

An NFT mint looks simple on the surface: the user clicks a button, a wallet pops up, they confirm, and a token appears in their collection. But beneath that interaction sits a chain of async operations spanning at least three systems (your dApp frontend, the wallet provider, and the blockchain node) with no synchronous handshake between them. Playwright was built for browser automation, not blockchain state machines, and bridging that gap requires deliberate architectural decisions in your test harness.

The first obstacle is the wallet confirmation modal. MetaMask and other injected wallets render their UI in a separate browser extension popup or in an iframe injected into the page. Playwright cannot interact with extension popups natively because they live in a different browser context. You must either mock the wallet provider entirely (replacing window.ethereum with a programmatic signer) or use a headless wallet library that exposes the signing interface without a popup.

The second obstacle is gas estimation. Before the transaction confirmation modal appears, your dApp typically calls estimateGas on the contract method, converts the result to a human-readable cost in ETH, and displays it alongside the mint price. That gas estimate depends on the current block state, which changes every 12 seconds on mainnet. In a test environment with a local Hardhat node, gas estimation is deterministic, but only if you control the block state carefully.

The third obstacle is state transitions. A mint transaction moves through at least five UI states: idle, awaiting wallet confirmation, pending (transaction broadcast but not mined), success (receipt received with status 1), and failure (receipt with status 0, or a revert). Your frontend must handle all five, and your test must assert on each transition without race conditions. Polling for transaction receipts introduces timing issues. Hardhat can auto-mine blocks instantly, but that makes it impossible to observe the pending state unless you configure manual mining.

The fourth obstacle is contract reverts. An NFT contract might revert for dozens of reasons: the sale is not active, the caller has exceeded the per-wallet limit, the total supply is minted out, the proof for an allowlist mint is invalid, or the sent ETH does not match the required price. Each revert reason produces a different error message, and your dApp should display a specific, helpful message for each one rather than a generic “transaction failed.”

NFT Mint UI State Machine

🌐

Idle

Mint button enabled

💳

Awaiting Wallet

Confirmation modal open

⚙️

Pending

Tx broadcast, waiting for block

Success

Receipt status 1, NFT minted

Failed

Revert or receipt status 0

Common Contract Revert Reasons

Sale Inactive

saleIsActive == false

Max Per Wallet

Exceeds mint limit

Supply Exhausted

totalSupply == maxSupply

Wrong Price

msg.value != price * qty

🔒

Invalid Proof

Allowlist verification fails

A thorough NFT mint test suite covers all of these surfaces. The sections below walk through each scenario you need, with runnable Playwright TypeScript that uses a local Hardhat node and a mocked ethers.js provider so every test is fast, deterministic, and repeatable.

2. Setting Up a Reliable Test Environment

The foundation of reliable NFT mint testing is a local blockchain node that gives you complete control over block mining, account balances, and contract state. Hardhat is the standard choice. It runs an in-process EVM, supports instant or manual mining, and ships with a set of funded test accounts whose private keys are known. Your Playwright tests will interact with your dApp frontend, which connects to the Hardhat node via a mocked wallet provider instead of MetaMask.

NFT Mint Test Environment Checklist

  • Install Hardhat and configure a local network on port 8545
  • Deploy your NFT contract to the Hardhat local network before each test suite
  • Create a mock wallet provider that replaces window.ethereum
  • Fund test accounts with sufficient ETH for minting and gas
  • Configure Hardhat for manual mining (to test pending states)
  • Set up contract ABI and address as environment variables
  • Create a deploy script that activates the sale and sets a known price
  • Disable any allowlist or Merkle proof requirements for public mint tests

Hardhat Configuration

hardhat.config.ts

Deploy Script for Testing

scripts/deploy-test-nft.ts
Starting Hardhat Node and Deploying Contract

Mock Wallet Provider

The key to testability is replacing MetaMask with a programmatic wallet. Instead of fighting with extension popups, inject a mock window.ethereum provider that uses ethers.js under the hood. This provider exposes the same EIP-1193 interface that your dApp expects, but signs transactions automatically (or rejects them on demand for failure testing).

test/helpers/mock-provider.ts

Environment Variables

.env.test

Playwright Configuration

playwright.config.ts

3. Scenario: Wallet Connection and Account Detection

Before any mint can happen, the user must connect their wallet. Your dApp calls eth_requestAccountsthrough the injected provider, receives the user's address, and updates the UI to show a truncated address, the user's ETH balance, and an enabled Mint button. This scenario verifies that the wallet connection flow works correctly with the mock provider and that the dApp transitions from a “Connect Wallet” state to a “Ready to Mint” state.

1

Wallet Connection and Account Detection

Straightforward

Goal

Inject the mock wallet provider, click Connect Wallet, and verify the UI displays the correct account address and ETH balance.

Preconditions

  • Hardhat node running on port 8545
  • NFT contract deployed with sale active
  • Test account funded with at least 10,000 ETH (Hardhat default)

Playwright Implementation

test/nft/wallet-connect.spec.ts

What to Assert Beyond the UI

  • The eth_requestAccounts RPC call was made exactly once
  • The displayed chain ID matches Hardhat (31337 or 0x7a69)
  • No console errors related to provider injection

4. Scenario: Happy Path Mint with Confirmation Modal

The happy path mint is the most important scenario. The user has connected their wallet, clicks the Mint button, sees a confirmation modal showing the mint price and estimated gas, confirms the transaction, waits for it to be mined, and sees a success message with a link to their newly minted NFT. Every state transition in this flow has a corresponding UI element that your test must verify.

Because we are using a mock provider instead of MetaMask, the “wallet confirmation” step is handled by your dApp's own confirmation modal, not the MetaMask popup. Most well-built NFT dApps show their own pre-confirmation screen before invoking eth_sendTransaction, giving the user a chance to review the price, quantity, and total cost. Your mock provider then signs and broadcasts the transaction automatically.

2

Happy Path Mint with Confirmation Modal

Moderate

Goal

Complete a full mint of one NFT, verify every UI state transition from idle through success, and confirm the token was actually minted on chain.

Playwright Implementation

test/nft/happy-mint.spec.ts

Happy Path Mint: Playwright vs Assrt

import { test, expect } from "@playwright/test";
import { JsonRpcProvider, Contract } from "ethers";

test("mint one NFT", async ({ page }) => {
  const provider = new JsonRpcProvider("http://127.0.0.1:8545");
  const contract = new Contract(addr, abi, provider);
  const supplyBefore = await contract.totalSupply();

  await page.goto("/mint");
  await page.getByRole("button", { name: /connect/i }).click();
  await expect(page.getByText(/0xf39F/i)).toBeVisible();
  await page.getByRole("button", { name: /^mint$/i }).click();

  const modal = page.getByRole("dialog");
  await expect(modal).toBeVisible();
  await expect(modal.getByText(/0\.08.*ETH/)).toBeVisible();
  await modal.getByRole("button", { name: /confirm/i }).click();

  await expect(page.getByText(/pending/i)).toBeVisible();
  await expect(page.getByText(/success/i)).toBeVisible({ timeout: 30000 });

  const supplyAfter = await contract.totalSupply();
  expect(Number(supplyAfter)).toBe(Number(supplyBefore) + 1);
});
11% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Gas Estimation UI and Price Display

Gas estimation is one of the trickiest parts of NFT mint UIs to test correctly. Your dApp calls estimateGas on the mint function, multiplies the result by the current gas price (obtained via eth_gasPrice), and displays the estimated transaction cost alongside the mint price. The total cost shown to the user is mint price plus gas, and it needs to update if the user changes the quantity.

On a local Hardhat node, gas prices are fixed and predictable. The default gas price is 8 gwei, and a typical ERC-721 mint consumes roughly 80,000 to 150,000 gas units depending on the contract implementation. This makes assertions on the displayed gas cost deterministic, which is exactly what you want in a test.

3

Gas Estimation UI and Price Display

Moderate

Goal

Verify that the confirmation modal displays an accurate gas estimate, the correct mint price, and that the total updates when the user changes the mint quantity.

Playwright Implementation

test/nft/gas-estimation.spec.ts

What to Assert Beyond the UI

  • The eth_estimateGas RPC call was made with the correct contract address and calldata
  • Gas estimate falls within a reasonable range (50,000 to 200,000 gas units for a standard mint)
  • The displayed total equals mint price multiplied by quantity plus gas, with no rounding errors visible to the user

6. Scenario: Pending Transaction State and Loading UI

The pending state is the most commonly skipped test in NFT mint suites. When Hardhat is configured with auto-mining (the default), transactions are mined instantly and the pending state flashes by so fast that your test never observes it. To properly test the pending UI, you need Hardhat in manual mining mode, where blocks are only mined when you explicitly call evm_mine.

With manual mining, you can send the transaction, verify that the UI shows a pending spinner or “Confirming...” message, then mine the block from the test and verify the transition to success. This gives you complete control over the timing and lets you assert on every intermediate state.

4

Pending Transaction State with Manual Mining

Complex

Goal

Send a mint transaction, observe the pending state in the UI, then manually mine a block and verify the success transition.

Playwright Implementation

test/nft/pending-state.spec.ts

Pending State: Playwright vs Assrt

test("pending state", async ({ page }) => {
  const provider = new JsonRpcProvider(rpcUrl);
  await provider.send("evm_setAutomine", [false]);
  await provider.send("evm_setIntervalMining", [0]);

  await page.goto("/mint");
  // connect wallet...
  await page.getByRole("button", { name: /mint/i }).click();
  // confirm modal...
  await expect(page.getByText(/pending/i)).toBeVisible();
  await expect(page.locator(".animate-spin")).toBeVisible();

  await provider.send("evm_mine", []);

  await expect(page.getByText(/success/i)).toBeVisible();
  await provider.send("evm_setAutomine", [true]);
});
19% fewer lines

7. Scenario: Transaction Failure and Revert Handling

Transaction failures in NFT mints are not edge cases; they are common user experiences. A revert happens when the smart contract throws an error during execution: the sale is paused, the user has exceeded their per-wallet limit, the total supply has been reached, or the sent ETH does not match the required price. Your dApp must parse the revert reason from the error, display a helpful message, and return the UI to an actionable state (not a dead end).

To trigger specific reverts in your test, manipulate the Hardhat contract state before the mint attempt. For example, call setSaleActive(false)to test the “sale not active” path, or mint tokens up to the max supply to test the “sold out” path. Hardhat reverts include the custom error message from the contract, which your dApp should parse and surface.

5

Transaction Revert: Sale Not Active

Moderate

Goal

Attempt a mint when the sale is paused and verify the dApp shows a specific, helpful error message rather than a generic failure.

Playwright Implementation

test/nft/tx-revert.spec.ts

What to Assert Beyond the UI

  • The revert reason string from the contract is parsed and displayed, not a raw hex error
  • No ETH was deducted from the user's balance (reverted transactions refund gas on Hardhat)
  • The UI returns to an actionable state where the user can retry

8. Scenario: Insufficient Funds and User Rejection

Two common failure modes happen before the transaction even reaches the blockchain. First, the user might not have enough ETH to cover the mint price plus gas. Second, the user might reject the transaction in their wallet (click “Reject” in MetaMask). Both produce different error types, and your dApp should handle each with a distinct message.

To test insufficient funds, create a mock provider backed by a Hardhat account with very little ETH. To test user rejection, modify the mock provider's eth_sendTransaction handler to throw an error with code 4001 (the EIP-1193 standard code for user-rejected requests).

6

Insufficient Funds Error

Straightforward

Goal

Connect with an account that has less ETH than the mint price and verify the dApp displays an insufficient funds message.

Playwright Implementation

test/nft/insufficient-funds.spec.ts

Error Handling: Playwright vs Assrt

test("insufficient funds error", async ({ page }) => {
  // Setup poor wallet via Hardhat...
  await page.goto("/mint");
  await page.getByRole("button", { name: /connect/i }).click();
  await page.getByRole("button", { name: /mint/i }).click();

  await expect(
    page.getByText(/insufficient.*funds/i)
  ).toBeVisible({ timeout: 10_000 });
});

test("user rejection", async ({ page }) => {
  await page.addInitScript({
    content: 'window.__REJECT_NEXT_TX = true;',
  });
  await page.goto("/mint");
  // connect, mint, confirm...
  await expect(
    page.getByText(/rejected|cancelled/i)
  ).toBeVisible({ timeout: 10_000 });
  await expect(
    page.getByRole("button", { name: /mint/i })
  ).toBeEnabled();
});
23% fewer lines

9. Common Pitfalls That Break NFT Mint Test Suites

Hardhat Auto-Mining Hides Real Bugs

The default Hardhat configuration mines every transaction instantly. This means your pending state UI is never exercised, race conditions between concurrent transactions never surface, and the user experience of waiting for confirmation is never tested. Always run at least one test suite with manual mining or interval mining (2 second blocks) to catch timing bugs. A common issue reported on the Hardhat GitHub (issue #1585) is that evm_setAutomine does not persist across node restarts, so your test setup must set it explicitly.

Nonce Collisions in Parallel Tests

If you run multiple mint tests in parallel using the same Hardhat account, transactions will collide on the nonce. Hardhat queues them, but the order is not guaranteed, and one test's assertion about “total supply increased by 1” will fail because another test's mint landed in between. The fix is to use separate Hardhat accounts for each parallel worker. Hardhat provides 20 pre-funded accounts by default; assign one per Playwright worker using the workerIndex from the test info object.

Stale Contract State Between Tests

Unlike a traditional database where you can roll back a transaction, blockchain state is append-only. If one test mints 5 tokens and the next test expects a fresh contract with 0 supply, it will fail. Use Hardhat snapshots (evm_snapshot and evm_revert) to save and restore state between tests. Take a snapshot after deployment and revert to it in each test's setup hook. This is the blockchain equivalent of database transaction rollback.

MetaMask Extension Popups in CI

Some teams attempt to load the actual MetaMask extension in a Chromium browser for testing. This approach is fragile because MetaMask renders its confirmation popup in a separate extension page that Playwright cannot reliably target in headless mode. Browser extensions also update independently, breaking selectors without warning. The Synpress library (built on top of Playwright) attempted to solve this, but its GitHub issues tracker shows persistent reliability problems with MetaMask version updates. Use a mock provider as described in Section 2 instead.

Gas Price Fluctuations in Fork Mode

If your test suite uses Hardhat's mainnet fork mode (to test against real contract deployments), gas prices will reflect the forked block's base fee. This makes gas estimation assertions non-deterministic. Pin the gas price in your Hardhat config using initialBaseFeePerGas: 1000000000 (1 gwei) to get predictable gas cost calculations in forked mode.

NFT Mint Testing Anti-Patterns

  • Using MetaMask extension in CI instead of a mock provider
  • Running all tests with auto-mining enabled (hides pending state bugs)
  • Sharing a single Hardhat account across parallel test workers
  • Not snapshotting contract state between tests
  • Asserting exact gas costs without pinning the gas price
  • Catching raw hex revert errors instead of parsing the reason string
  • Not testing the user rejection path (wallet cancel)
  • Hardcoding block numbers in assertions (breaks on re-deploy)
NFT Mint Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above requires deep knowledge of ethers.js, Hardhat RPC methods, EIP-1193 provider interfaces, and Playwright internals. The happy path mint test alone is over 40 lines of TypeScript with contract instantiation, ABI definitions, provider setup, and on-chain assertions. Multiply that by eight scenarios and you have a substantial, brittle test suite that breaks whenever your dApp renames a button, restructures the confirmation modal, or upgrades to a new smart contract version.

Assrt lets you describe each scenario in plain English. It generates the equivalent Playwright TypeScript with the correct ethers.js integration, mock provider setup, and Hardhat RPC calls. When your dApp changes its UI, Assrt detects the broken selectors, analyzes the new DOM structure, and updates the generated code. Your scenario files remain stable across contract upgrades, UI redesigns, and wallet library migrations.

scenarios/nft-mint-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections. The generated tests include Hardhat RPC calls for mining control, ethers.js contract interactions for on-chain assertions, and proper mock provider setup for wallet simulation. When your NFT contract adds a new revert reason or your dApp redesigns the confirmation modal, Assrt detects the failure, analyzes the updated DOM and ABI, and opens a pull request with corrected selectors and assertions.

Start with the happy path mint scenario. Once it passes in your CI, add the pending state test with manual mining, then the revert scenarios, then insufficient funds and user rejection. Within an afternoon you will have comprehensive NFT mint coverage that catches UI regressions, contract integration bugs, and wallet interaction failures before they reach production.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk