Examples
The code below comes from the examples/test-dapp directory in the repository. It is a full Scaffold Stellar app with two Soroban contracts deployed on testnet: a Counter and an OpenZeppelin ERC-4626 XLM Vault. The test files live in tests/.
Each example demonstrates a different kind of dApp interaction you can automate with stellar-wallet-mock — from basic wallet connection to Soroban auth entry signing.
Playwright Fixture
A Playwright fixture lets you run setup code before each test. Here we create a wallet fixture that installs the mock wallet so every test starts with a connected wallet automatically.
1import { test as base } from "@playwright/test";2import { installMockStellarWallet, type MockWallet } from "stellar-wallet-mock";3import { Keypair, Networks } from "@stellar/stellar-sdk";4
5const SECRET_KEY = "SDPDMYEWFZEL6MW37FTPNTPZFYU2QYX4MLDSA7QBS4VSNZL5JL4IKDVQ";6const PUBLIC_KEY = Keypair.fromSecret(SECRET_KEY).publicKey();7
8export const test = base.extend<{ wallet: MockWallet }>({9 wallet: async ({ page }, use) => {10 const wallet = await installMockStellarWallet({11 page,12 secretKey: SECRET_KEY,13 network: "TESTNET",14 networkPassphrase: Networks.TESTNET,15 });16 await use(wallet);17 },18});19
20export { expect } from "@playwright/test";21export { PUBLIC_KEY };All examples below import test and expect from this fixture file instead of from @playwright/test directly.
Connect Wallet
The simplest test — navigate to the dApp and confirm the wallet connected with the right address. The mock pre-seeds localStorage, so the dApp boots directly into a connected state.
1import { test, expect, PUBLIC_KEY } from "./fixtures";2
3test("wallet connects and shows address", async ({ page }) => {4 await page.goto("http://localhost:5173");5
6 // The dApp reads localStorage and displays the connected address7 await expect(page.getByText(PUBLIC_KEY.slice(0, 5))).toBeVisible({8 timeout: 15_000,9 });10});Sign a Transaction
Clicking the increment button on a Soroban counter contract triggers a transaction. The mock intercepts the SUBMIT_TRANSACTION message and signs it in Node.js — no popup, no manual approval.
1import { test, expect, PUBLIC_KEY } from "./fixtures";2
3test("counter increment signs and updates count", async ({ page }) => {4 await page.goto("http://localhost:5173");5
6 // Wait for wallet to connect7 await expect(page.getByText(PUBLIC_KEY.slice(0, 5))).toBeVisible({8 timeout: 15_000,9 });10
11 // Click increment — triggers a Soroban transaction12 const incrementBtn = page.getByTestId("increment-btn");13 await expect(incrementBtn).toBeEnabled({ timeout: 5_000 });14 await incrementBtn.click();15
16 // Wait for the on-chain state to update17 const counterValue = page.getByTestId("counter-value");18 await expect(counterValue).not.toContainText("—", { timeout: 45_000 });19
20 // Verify the count is a positive number21 const text = await counterValue.textContent();22 const match = text?.match(/(\d+)/);23 expect(match).toBeTruthy();24 expect(Number(match![1])).toBeGreaterThan(0);25});Vault Deposit (Auth Entry Signing)
Depositing XLM into an OpenZeppelin ERC-4626 vault is more complex than a simple transaction. The vault's deposit() calls require_auth() for both the token transfer and the vault interaction, which means the wallet needs to sign Soroban auth entries (via SUBMIT_AUTH_ENTRY). The mock handles this automatically.
1import { test, expect, PUBLIC_KEY } from "./fixtures";2
3test("deposit XLM into vault and receive shares", async ({ page }) => {4 await page.goto("http://localhost:5173");5
6 // Wait for wallet7 await expect(page.getByText(PUBLIC_KEY.slice(0, 5))).toBeVisible({8 timeout: 15_000,9 });10
11 // Enter deposit amount12 const depositInput = page.getByTestId("deposit-input");13 await expect(depositInput).toBeVisible({ timeout: 5_000 });14 await depositInput.fill("1");15
16 // Click deposit — triggers require_auth + auth entry signing17 const depositBtn = page.getByTestId("deposit-btn");18 await expect(depositBtn).toBeEnabled();19 await depositBtn.click();20
21 // Verify shares were minted22 const sharesMinted = page.getByTestId("shares-minted");23 await expect(sharesMinted).toBeVisible({ timeout: 45_000 });24 const text = await sharesMinted.textContent();25 const match = text?.match(/([\d.]+)/);26 expect(match).toBeTruthy();27 expect(Number(match![1])).toBeGreaterThan(0);28});Vault Withdraw
After depositing, withdraw XLM and verify the vault balance decreases. This also uses auth entry signing under the hood.
1import { test, expect, PUBLIC_KEY } from "./fixtures";2
3test("withdraw XLM from vault", async ({ page }) => {4 await page.goto("http://localhost:5173");5
6 // Wait for wallet7 await expect(page.getByText(PUBLIC_KEY.slice(0, 5))).toBeVisible({8 timeout: 15_000,9 });10
11 // Refresh to get current vault balance12 const refreshBtn = page.getByTestId("refresh-balance-btn");13 await expect(refreshBtn).toBeVisible({ timeout: 5_000 });14 await refreshBtn.click();15
16 const vaultBalance = page.getByTestId("vault-balance");17 await expect(vaultBalance).toContainText("XLM", { timeout: 15_000 });18
19 // Record balance before withdrawal20 const beforeText = await vaultBalance.textContent();21 const beforeMatch = beforeText?.match(/([\d.]+)\s*XLM/);22 const beforeBalance = beforeMatch ? Number(beforeMatch[1]) : 0;23
24 // Withdraw 0.5 XLM25 const withdrawInput = page.getByTestId("withdraw-input");26 await expect(withdrawInput).toBeVisible({ timeout: 5_000 });27 await withdrawInput.fill("0.5");28
29 const withdrawBtn = page.getByTestId("withdraw-btn");30 await expect(withdrawBtn).toBeEnabled();31 await withdrawBtn.click();32
33 // Verify vault balance decreased34 await expect(async () => {35 const afterText = await vaultBalance.textContent();36 const afterMatch = afterText?.match(/([\d.]+)\s*XLM/);37 const afterBalance = afterMatch ? Number(afterMatch[1]) : 0;38 expect(afterBalance).toBeLessThan(beforeBalance);39 }).toPass({ timeout: 45_000 });40});Read-Only Contract Calls
Not every interaction needs signing. Reading contract state (counter value, vault balance) works without wallet authorization — but you still need the mock installed so the dApp boots into a connected state.
1import { test, expect } from "./fixtures";2
3test("read counter value without signing", async ({ page }) => {4 await page.goto("http://localhost:5173");5
6 const getCountBtn = page.getByTestId("get-count-btn");7 await expect(getCountBtn).toBeVisible({ timeout: 10_000 });8 await getCountBtn.click();9
10 const counterValue = page.getByTestId("counter-value");11 await expect(counterValue).toContainText(/\d+/, { timeout: 15_000 });12});Network Configuration
The examples above use testnet. Pass different network and networkPassphrase values for other networks:
Mainnet
1import { Networks } from "@stellar/stellar-sdk";2
3await installMockStellarWallet({4 page,5 secretKey: SECRET_KEY,6 network: "PUBLIC",7 networkPassphrase: Networks.PUBLIC,8});Local Standalone
1await installMockStellarWallet({2 page,3 secretKey: SECRET_KEY,4 network: "STANDALONE",5 networkPassphrase: "Standalone Network ; February 2017",6});Playwright Configuration
The Playwright config used by the example dApp. It starts the dev server automatically and runs tests serially (required when tests share a testnet account to avoid sequence number conflicts).
1import { defineConfig } from "@playwright/test";2
3export default defineConfig({4 testDir: "./tests",5 timeout: 60_000,6 use: { headless: true },7 projects: [8 {9 name: "chromium",10 use: { browserName: "chromium" },11 },12 ],13 webServer: {14 command: "npm run dev",15 port: 5173,16 reuseExistingServer: true,17 },18});Running the Example dApp
To run the example dApp and its tests locally:
1# Build the Soroban contracts and generate TS clients2cd examples/test-dapp3npm install4stellar scaffold build --build-clients5
6# Start the dev server7npm run dev8
9# In another terminal — run the tests10cd ../..11npm run build12npx playwright test