diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 345fba54..bb2a0522 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -38,11 +38,15 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
e2e:
- name: E2E Tests
+ name: E2E Tests (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [1, 2, 3]
steps:
- name: Clone repository
@@ -54,6 +58,17 @@ jobs:
- name: Setup pnpm
run: corepack enable pnpm
+ - name: Get pnpm store directory
+ id: pnpm-cache
+ run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT
+
+ - name: Cache pnpm dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.pnpm-cache.outputs.dir }}
+ key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: pnpm-${{ runner.os }}-
+
- name: Install dependencies
run: pnpm install
@@ -77,12 +92,12 @@ jobs:
run: pnpm exec playwright install-deps chromium
- name: Run Playwright tests
- run: pnpm run test:e2e
+ run: pnpm run test:e2e --shard=${{ matrix.shard }}/3
- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
- name: playwright-report
+ name: playwright-report-${{ matrix.shard }}
path: playwright-report/
retention-days: 15
diff --git a/biome.json b/biome.json
index 933e82b6..b1b81b1d 100644
--- a/biome.json
+++ b/biome.json
@@ -40,6 +40,14 @@
},
"files": {
"ignoreUnknown": false,
- "includes": ["**", "!dist", "!node_modules", "!specs/lib", "!src/snippets/unformatted"]
+ "includes": [
+ "**",
+ "!dist",
+ "!node_modules",
+ "!playwright-report",
+ "!specs/lib",
+ "!src/snippets/unformatted",
+ "!test-results"
+ ]
}
}
diff --git a/e2e/create-a-stablecoin.test.ts b/e2e/create-a-stablecoin.test.ts
new file mode 100644
index 00000000..de36c52f
--- /dev/null
+++ b/e2e/create-a-stablecoin.test.ts
@@ -0,0 +1,60 @@
+import { expect, test } from '@playwright/test'
+
+test('create a stablecoin', async ({ page }) => {
+ test.setTimeout(120000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/issuance/create-a-stablecoin')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ // Wait for sign out button (indicates successful sign up)
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ // Wait for "Add more funds" button (indicates funds were added)
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Fill in token details and deploy
+ // Use label-based selectors to ensure we're filling the right inputs in the demo form
+ const nameInput = page.getByLabel('Token name').first()
+ await expect(nameInput).toBeVisible()
+ await nameInput.fill('TestUSD')
+
+ const symbolInput = page.getByLabel('Token symbol').first()
+ await expect(symbolInput).toBeVisible()
+ await symbolInput.fill('TEST')
+
+ const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
+ await expect(deployButton).toBeVisible()
+ await deployButton.click()
+
+ // Wait for success - View receipt link
+ await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/distribute-rewards.test.ts b/e2e/distribute-rewards.test.ts
new file mode 100644
index 00000000..39ae4275
--- /dev/null
+++ b/e2e/distribute-rewards.test.ts
@@ -0,0 +1,110 @@
+import { expect, test } from '@playwright/test'
+
+test('distribute rewards', async ({ page }) => {
+ test.setTimeout(240000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/issuance/distribute-rewards')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Create a token
+ // Use label-based selectors to ensure we're filling the right inputs in the demo form
+ const nameInput = page.getByLabel('Token name').first()
+ await expect(nameInput).toBeVisible()
+ await nameInput.fill('RewardTestUSD')
+
+ const symbolInput = page.getByLabel('Token symbol').first()
+ await expect(symbolInput).toBeVisible()
+ await symbolInput.fill('REWARD')
+
+ const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
+ await expect(deployButton).toBeVisible()
+ await deployButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 4: Grant issuer role
+ const grantEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(grantEnterDetails).toBeVisible()
+ await grantEnterDetails.click()
+
+ const grantButton = page.getByRole('button', { name: 'Grant' }).first()
+ await grantButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 5: Mint tokens (after grant completes, Enter details button is the first visible one)
+ const mintEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(mintEnterDetails).toBeVisible()
+ await mintEnterDetails.click()
+
+ const mintButton = page.getByRole('button', { name: 'Mint' }).first()
+ await mintButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 6: Opt in to rewards
+ const optInButton = page.getByRole('button', { name: 'Opt In' }).first()
+ await expect(optInButton).toBeVisible()
+ await optInButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(3)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 7: Start reward
+ const startButton = page.getByRole('button', { name: 'Start Reward' }).first()
+ await expect(startButton).toBeVisible()
+ await startButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(4)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 8: Claim reward
+ const claimButton = page.getByRole('button', { name: 'Claim' }).first()
+ await expect(claimButton).toBeVisible()
+ await claimButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(5)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/executing-swaps.test.ts b/e2e/executing-swaps.test.ts
new file mode 100644
index 00000000..264d02d2
--- /dev/null
+++ b/e2e/executing-swaps.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from '@playwright/test'
+
+test('executing swaps', async ({ page }) => {
+ test.setTimeout(180000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/stablecoin-dex/executing-swaps')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Execute a swap (Buy AlphaUSD with BetaUSD)
+ const buyButton = page.getByRole('button', { name: 'Buy' }).first()
+ await expect(buyButton).toBeVisible()
+ await buyButton.click()
+
+ // Wait for swap receipt
+ await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/faucet.spec.ts b/e2e/faucet.test.ts
similarity index 73%
rename from e2e/faucet.spec.ts
rename to e2e/faucet.test.ts
index dceb26c4..b862fc8c 100644
--- a/e2e/faucet.spec.ts
+++ b/e2e/faucet.test.ts
@@ -1,10 +1,14 @@
import { expect, test } from '@playwright/test'
test('fund an address via faucet', async ({ page }) => {
+ test.setTimeout(120000)
+
await page.goto('/quickstart/faucet')
// Switch to "Fund an address" tab
- await page.getByRole('tab', { name: 'Fund an address' }).click()
+ const tab = page.getByRole('tab', { name: 'Fund an address' })
+ await expect(tab).toBeVisible({ timeout: 90000 })
+ await tab.click()
// Enter an address
const addressInput = page.getByPlaceholder('0x...')
@@ -14,5 +18,5 @@ test('fund an address via faucet', async ({ page }) => {
await page.getByRole('button', { name: 'Add funds' }).click()
// Confirm "View receipt" link is visible
- await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 30000 })
+ await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
})
diff --git a/e2e/manage-stablecoin.test.ts b/e2e/manage-stablecoin.test.ts
new file mode 100644
index 00000000..8aa13c4d
--- /dev/null
+++ b/e2e/manage-stablecoin.test.ts
@@ -0,0 +1,86 @@
+import { expect, test } from '@playwright/test'
+
+test('manage stablecoin - grant and revoke roles', async ({ page }) => {
+ test.setTimeout(180000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/issuance/manage-stablecoin')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Create a token
+ // Use label-based selectors to ensure we're filling the right inputs in the demo form
+ const nameInput = page.getByLabel('Token name').first()
+ await expect(nameInput).toBeVisible()
+ await nameInput.fill('ManageTestUSD')
+
+ const symbolInput = page.getByLabel('Token symbol').first()
+ await expect(symbolInput).toBeVisible()
+ await symbolInput.fill('MANAGE')
+
+ const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
+ await expect(deployButton).toBeVisible()
+ await deployButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 4: Grant issuer role
+ const grantEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(grantEnterDetails).toBeVisible()
+ await grantEnterDetails.click()
+
+ const grantButton = page.getByRole('button', { name: 'Grant' }).first()
+ await expect(grantButton).toBeVisible()
+ await grantButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 5: Revoke issuer role (now the first visible Enter details)
+ const revokeEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(revokeEnterDetails).toBeVisible()
+ await revokeEnterDetails.click()
+
+ const revokeButton = page.getByRole('button', { name: 'Revoke' }).first()
+ await expect(revokeButton).toBeVisible()
+ await revokeButton.click()
+
+ // Wait for revoke receipt
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/mint-stablecoins.test.ts b/e2e/mint-stablecoins.test.ts
new file mode 100644
index 00000000..05c2ac98
--- /dev/null
+++ b/e2e/mint-stablecoins.test.ts
@@ -0,0 +1,88 @@
+import { expect, test } from '@playwright/test'
+
+test('mint stablecoins', async ({ page }) => {
+ test.setTimeout(180000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/issuance/mint-stablecoins')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Create a token (fill form and deploy)
+ // Use label-based selectors to ensure we're filling the right inputs in the demo form
+ const nameInput = page.getByLabel('Token name').first()
+ await expect(nameInput).toBeVisible()
+ await nameInput.fill('MintTestUSD')
+
+ const symbolInput = page.getByLabel('Token symbol').first()
+ await expect(symbolInput).toBeVisible()
+ await symbolInput.fill('MINT')
+
+ const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
+ await expect(deployButton).toBeVisible()
+ await deployButton.click()
+
+ // Wait for token to be created (View receipt appears)
+ await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 4: Grant issuer role - click "Enter details" then "Grant"
+ const grantEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(grantEnterDetails).toBeVisible()
+ await grantEnterDetails.click()
+
+ const grantButton = page.getByRole('button', { name: 'Grant' }).first()
+ await expect(grantButton).toBeVisible()
+ await grantButton.click()
+
+ // Wait for grant receipt
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 5: Mint tokens - click "Enter details" then "Mint" (now the first visible Enter details)
+ const mintEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(mintEnterDetails).toBeVisible()
+ await mintEnterDetails.click()
+
+ const mintButton = page.getByRole('button', { name: 'Mint' }).first()
+ await expect(mintButton).toBeVisible()
+ await mintButton.click()
+
+ // Wait for mint receipt
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/passkey-accounts.test.ts b/e2e/passkey-accounts.test.ts
new file mode 100644
index 00000000..0503f280
--- /dev/null
+++ b/e2e/passkey-accounts.test.ts
@@ -0,0 +1,47 @@
+import { expect, test } from '@playwright/test'
+
+test('sign up, sign out, then sign in with passkey', async ({ page }) => {
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/use-accounts/embed-passkeys')
+
+ // Wait for the demo to load
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+
+ // Sign up with passkey
+ await signUpButton.click()
+
+ // Wait for sign out button (indicates successful sign up)
+ const signOutButton = page.getByRole('button', { name: 'Sign out' }).first()
+ await expect(signOutButton).toBeVisible({ timeout: 30000 })
+
+ // Sign out
+ await signOutButton.click()
+
+ // Wait for sign in button to reappear
+ const signInButton = page.getByRole('button', { name: 'Sign in' }).first()
+ await expect(signInButton).toBeVisible({ timeout: 10000 })
+
+ // Sign in with the same passkey
+ await signInButton.click()
+
+ // Confirm signed in again (sign out button visible)
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/providing-liquidity.test.ts b/e2e/providing-liquidity.test.ts
new file mode 100644
index 00000000..3967c2eb
--- /dev/null
+++ b/e2e/providing-liquidity.test.ts
@@ -0,0 +1,59 @@
+import { expect, test } from '@playwright/test'
+
+test('providing liquidity - place and query order', async ({ page }) => {
+ test.setTimeout(180000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/stablecoin-dex/providing-liquidity')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Place order
+ const placeOrderButton = page.getByRole('button', { name: 'Place order' }).first()
+ await expect(placeOrderButton).toBeVisible()
+ await placeOrderButton.click()
+
+ // Wait for order to be placed - should see View receipt
+ await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 4: Query order - button should become enabled after placing
+ const queryButton = page.getByRole('button', { name: 'Query' }).first()
+ await expect(queryButton).toBeEnabled({ timeout: 30000 })
+ await queryButton.click()
+
+ // Wait for order details to show (order type indicator)
+ await expect(page.getByText('Buy').first()).toBeVisible({ timeout: 30000 })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/send-a-payment.test.ts b/e2e/send-a-payment.test.ts
new file mode 100644
index 00000000..8049bfdd
--- /dev/null
+++ b/e2e/send-a-payment.test.ts
@@ -0,0 +1,60 @@
+import { expect, test } from '@playwright/test'
+
+test('send a payment', async ({ page }) => {
+ test.setTimeout(120000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/payments/send-a-payment')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ // Wait for sign out button (indicates successful sign up)
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ // Wait for "Add more funds" button (indicates funds were added)
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Send payment
+ const enterDetailsButton = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(enterDetailsButton).toBeVisible()
+ await enterDetailsButton.click()
+
+ // Fill in optional memo
+ const memoInput = page.getByLabel('Memo (optional)').first()
+ await expect(memoInput).toBeVisible()
+ await memoInput.fill('test-memo')
+
+ // Click send
+ const sendButton = page.getByRole('button', { name: 'Send' }).first()
+ await sendButton.click()
+
+ // Wait for transaction receipt link
+ await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/e2e/use-for-fees.test.ts b/e2e/use-for-fees.test.ts
new file mode 100644
index 00000000..5a6df28d
--- /dev/null
+++ b/e2e/use-for-fees.test.ts
@@ -0,0 +1,105 @@
+import { expect, test } from '@playwright/test'
+
+test('use stablecoin for fees', async ({ page }) => {
+ test.setTimeout(240000)
+
+ // Set up virtual authenticator via CDP
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+ const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ },
+ })
+
+ await page.goto('/guide/issuance/use-for-fees')
+
+ // Step 1: Sign up with passkey
+ const signUpButton = page.getByRole('button', { name: 'Sign up' }).first()
+ await expect(signUpButton).toBeVisible({ timeout: 90000 })
+ await signUpButton.click()
+
+ await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
+ timeout: 30000,
+ })
+
+ // Step 2: Add funds
+ const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
+ await expect(addFundsButton).toBeVisible()
+ await addFundsButton.click()
+
+ await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 3: Create a token
+ // Use label-based selectors to ensure we're filling the right inputs in the demo form
+ const nameInput = page.getByLabel('Token name').first()
+ await expect(nameInput).toBeVisible()
+ await nameInput.fill('FeeTestUSD')
+
+ const symbolInput = page.getByLabel('Token symbol').first()
+ await expect(symbolInput).toBeVisible()
+ await symbolInput.fill('FEE')
+
+ const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
+ await expect(deployButton).toBeVisible()
+ await deployButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 4: Grant issuer role
+ const grantEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(grantEnterDetails).toBeVisible()
+ await grantEnterDetails.click()
+
+ const grantButton = page.getByRole('button', { name: 'Grant' }).first()
+ await grantButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 5: Mint tokens (after grant completes, Enter details button is the first visible one)
+ const mintEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(mintEnterDetails).toBeVisible()
+ await mintEnterDetails.click()
+
+ const mintButton = page.getByRole('button', { name: 'Mint' }).first()
+ await mintButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 6: Add fee AMM liquidity
+ const addLiquidityButton = page.getByRole('button', { name: 'Add Liquidity' }).first()
+ await expect(addLiquidityButton).toBeVisible()
+ await addLiquidityButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(3)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Step 7: Send payment using token as fee (now the only Enter details button visible)
+ const payEnterDetails = page.getByRole('button', { name: 'Enter details' }).first()
+ await expect(payEnterDetails).toBeVisible()
+ await payEnterDetails.click()
+
+ const sendButton = page.getByRole('button', { name: 'Send' }).first()
+ await expect(sendButton).toBeVisible()
+ await sendButton.click()
+
+ await expect(page.getByRole('link', { name: 'View receipt' }).nth(4)).toBeVisible({
+ timeout: 90000,
+ })
+
+ // Clean up
+ await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
+})
diff --git a/playwright.config.ts b/playwright.config.ts
index 3b060a86..8c52ede9 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -4,8 +4,9 @@ export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
- retries: process.env.CI ? 2 : 0,
- workers: process.env.CI ? 1 : undefined,
+ retries: process.env.CI ? 1 : 1, // Retry once due to testnet flakiness
+ workers: process.env.CI ? 4 : undefined,
+ timeout: 180000, // 3 min default timeout for testnet transactions
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
@@ -18,8 +19,10 @@ export default defineConfig({
},
],
webServer: {
- command: 'pnpm run dev',
+ command: 'pnpm run dev 2>/dev/null',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
+ stdout: 'ignore',
+ stderr: 'ignore',
},
})
diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx
index 93ace37d..3b121866 100644
--- a/src/components/ConnectWallet.tsx
+++ b/src/components/ConnectWallet.tsx
@@ -17,7 +17,7 @@ export function ConnectWallet({ showAddChain = true }: { showAddChain?: boolean
const isSupported = chains.some((c) => c.id === chain?.id)
if (!injectedConnectors.length)
return (
-
No browser wallets found.
+ No browser wallets found.
)
if (!address || connector?.id === 'webAuthn')
return (
@@ -64,7 +64,7 @@ export function ConnectWallet({ showAddChain = true }: { showAddChain?: boolean
)}
{switchChain.isSuccess && (
-
+
Added Tempo to {connector?.name ?? 'Wallet'}!
)}
diff --git a/src/components/IndexSupplyQuery.tsx b/src/components/IndexSupplyQuery.tsx
index ac18c85c..ceac85cb 100644
--- a/src/components/IndexSupplyQuery.tsx
+++ b/src/components/IndexSupplyQuery.tsx
@@ -246,7 +246,7 @@ export function IndexSupplyQuery(props: IndexSupplyQueryProps = {}) {
return (
+
{props.title || 'IndexSupply SQL Query'}
}
@@ -325,7 +325,7 @@ export function IndexSupplyQuery(props: IndexSupplyQueryProps = {}) {
{error && (
-
+
{error}
)}
diff --git a/src/components/TokenSelector.tsx b/src/components/TokenSelector.tsx
index df4fff2b..eca9ac78 100644
--- a/src/components/TokenSelector.tsx
+++ b/src/components/TokenSelector.tsx
@@ -29,7 +29,7 @@ export function TokenSelector(props: TokenSelectorProps) {
name={name}
value={value}
onChange={(e) => onChange(e.target.value as Address)}
- className="-tracking-[2%] h-[34px] rounded-lg border border-gray4 px-3.25 font-normal text-[14px] text-black dark:text-white"
+ className="h-[34px] rounded-lg border border-gray4 px-3.25 font-normal text-[14px] text-black -tracking-[2%] dark:text-white"
>
{tokens.map((token) => (
diff --git a/src/components/guides/Demo.tsx b/src/components/guides/Demo.tsx
index fc26a485..a0f7c33b 100644
--- a/src/components/guides/Demo.tsx
+++ b/src/components/guides/Demo.tsx
@@ -51,7 +51,7 @@ export function ExplorerLink({ hash }: { hash: string }) {
href={url}
target="_blank"
rel="noreferrer"
- className="-tracking-[1%] flex items-center gap-1 text-[13px] text-accent hover:underline"
+ className="flex items-center gap-1 text-[13px] text-accent -tracking-[1%] hover:underline"
onClick={() => trackExternalLinkClick(url, 'View receipt')}
>
View receipt
@@ -71,7 +71,7 @@ export function ExplorerAccountLink({ address }: { address: string }) {
href={url}
target="_blank"
rel="noreferrer"
- className="-tracking-[1%] flex items-center gap-1 text-[13px] text-accent hover:underline"
+ className="flex items-center gap-1 text-[13px] text-accent -tracking-[1%] hover:underline"
onClick={() => trackExternalLinkClick(url, 'View account')}
>
View account
@@ -142,7 +142,7 @@ export function Container(
-
+
{name}
{showBadge && (
@@ -322,7 +322,7 @@ export function Step(
>
{completed ? : number}
-
@@ -334,7 +334,7 @@ export function Step(
{error && (
<>
-
+
{'shortMessage' in error ? error.shortMessage : error.message}
>
@@ -376,7 +376,7 @@ export function Login() {
diff --git a/src/components/guides/steps/amm/CheckFeeAmmPool.tsx b/src/components/guides/steps/amm/CheckFeeAmmPool.tsx
index f644cd69..050307ff 100644
--- a/src/components/guides/steps/amm/CheckFeeAmmPool.tsx
+++ b/src/components/guides/steps/amm/CheckFeeAmmPool.tsx
@@ -49,7 +49,7 @@ export function CheckFeeAmmPool(props: DemoStepProps) {
{active && pool && lpBalance && (
-
+
Your LP Balance
diff --git a/src/components/guides/steps/amm/MintFeeAmmLiquidity.tsx b/src/components/guides/steps/amm/MintFeeAmmLiquidity.tsx
index 724e3c21..e8cf3b95 100644
--- a/src/components/guides/steps/amm/MintFeeAmmLiquidity.tsx
+++ b/src/components/guides/steps/amm/MintFeeAmmLiquidity.tsx
@@ -115,7 +115,7 @@ export function MintFeeAmmLiquidity(props: DemoStepProps & { waitForBalance?: bo
disabled={!active || mintFeeLiquidity.isPending}
onClick={handleMintAll}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{mintFeeLiquidity.isPending
? 'Adding...'
diff --git a/src/components/guides/steps/exchange/ApproveSpend.tsx b/src/components/guides/steps/exchange/ApproveSpend.tsx
index a8e60173..3205d9e7 100644
--- a/src/components/guides/steps/exchange/ApproveSpend.tsx
+++ b/src/components/guides/steps/exchange/ApproveSpend.tsx
@@ -43,7 +43,7 @@ export function ApproveSpend(props: DemoStepProps) {
})
}}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{approve.isPending ? 'Approving...' : 'Approve Spend'}
diff --git a/src/components/guides/steps/exchange/BuySwap.tsx b/src/components/guides/steps/exchange/BuySwap.tsx
index f02019c3..0648c0b5 100644
--- a/src/components/guides/steps/exchange/BuySwap.tsx
+++ b/src/components/guides/steps/exchange/BuySwap.tsx
@@ -76,7 +76,7 @@ export function BuySwap({ onSuccess }: { onSuccess?: () => void }) {
})
}}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{sendCalls.isPending ? 'Buying...' : 'Buy'}
diff --git a/src/components/guides/steps/exchange/CancelOrder.tsx b/src/components/guides/steps/exchange/CancelOrder.tsx
index 37159f2c..19923ad8 100644
--- a/src/components/guides/steps/exchange/CancelOrder.tsx
+++ b/src/components/guides/steps/exchange/CancelOrder.tsx
@@ -45,7 +45,7 @@ export function CancelOrder(props: DemoStepProps) {
}
}}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{cancelOrder.isPending ? 'Canceling...' : 'Cancel Order'}
diff --git a/src/components/guides/steps/exchange/PlaceOrder.tsx b/src/components/guides/steps/exchange/PlaceOrder.tsx
index f8ff7378..c372fdda 100644
--- a/src/components/guides/steps/exchange/PlaceOrder.tsx
+++ b/src/components/guides/steps/exchange/PlaceOrder.tsx
@@ -78,7 +78,7 @@ export function PlaceOrder(props: DemoStepProps) {
})
}}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{sendCalls.isPending ? 'Placing Order...' : 'Place Order'}
diff --git a/src/components/guides/steps/exchange/QueryOrder.tsx b/src/components/guides/steps/exchange/QueryOrder.tsx
index 6e0255a0..75ae4db9 100644
--- a/src/components/guides/steps/exchange/QueryOrder.tsx
+++ b/src/components/guides/steps/exchange/QueryOrder.tsx
@@ -52,7 +52,7 @@ export function QueryOrder(props: DemoStepProps) {
disabled={!active || isQuerying}
onClick={handleQuery}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{isQuerying ? 'Querying...' : hasQueried ? 'Query Again' : 'Query Order'}
diff --git a/src/components/guides/steps/exchange/SellSwap.tsx b/src/components/guides/steps/exchange/SellSwap.tsx
index e705013f..8b52cd6a 100644
--- a/src/components/guides/steps/exchange/SellSwap.tsx
+++ b/src/components/guides/steps/exchange/SellSwap.tsx
@@ -76,7 +76,7 @@ export function SellSwap({ onSuccess }: { onSuccess?: () => void }) {
})
}}
type="button"
- className="-tracking-[2%] font-normal text-[14px]"
+ className="font-normal text-[14px] -tracking-[2%]"
>
{sendCalls.isPending ? 'Selling...' : 'Sell'}
diff --git a/src/components/guides/steps/issuance/BurnToken.tsx b/src/components/guides/steps/issuance/BurnToken.tsx
index 8f3b0ccc..0f3de823 100644
--- a/src/components/guides/steps/issuance/BurnToken.tsx
+++ b/src/components/guides/steps/issuance/BurnToken.tsx
@@ -73,7 +73,7 @@ export function BurnToken(props: DemoStepProps) {
@@ -98,13 +98,14 @@ export function BurnToken(props: DemoStepProps) {
-