Complete Claude Code plugin with: - 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report) - 3 specialized agents (test-architect, test-debugger, migration-planner) - 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility) - TestRail MCP server (TypeScript) — 8 tools for bidirectional sync - BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing - Smart hooks (auto-validate tests, auto-detect Playwright projects) - 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests) - Leverages Claude Code built-ins (/batch, /debug, Explore subagent) - Zero-config for core features; TestRail/BrowserStack via env vars - Both TypeScript and JavaScript support throughout Co-authored-by: Leo <leo@openclaw.ai>
5.4 KiB
5.4 KiB
Payment Template
Tests card form entry, validation, and payment processing.
Prerequisites
- Cart with items, shipping filled
- Test card numbers:
{{testCardNumber}}(success),{{declinedCardNumber}}(decline) - App running at
{{baseUrl}}
TypeScript
import { test, expect, Page } from '@playwright/test';
async function fillCardForm(page: Page, card: {
number: string; expiry: string; cvc: string; name: string;
}): Promise<void> {
// Stripe/Braintree iframes — adapt frame locator to your provider
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill(card.number);
const expiryFrame = page.frameLocator('[data-testid="expiry-frame"]');
await expiryFrame.getByRole('textbox', { name: /expiry/i }).fill(card.expiry);
const cvcFrame = page.frameLocator('[data-testid="cvc-frame"]');
await cvcFrame.getByRole('textbox', { name: /cvc|cvv/i }).fill(card.cvc);
await page.getByRole('textbox', { name: /cardholder name/i }).fill(card.name);
}
test.describe('Payment', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/checkout/payment');
});
// Happy path: successful payment
test('completes payment with valid card', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page).toHaveURL(/\/order-confirmation|\/success/);
await expect(page.getByRole('heading', { name: /order confirmed|thank you/i })).toBeVisible();
});
// Happy path: processing state shown
test('shows processing state while payment is pending', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
const payBtn = page.getByRole('button', { name: /pay|place order/i });
await payBtn.click();
await expect(payBtn).toBeDisabled();
await expect(page.getByText(/processing|please wait/i)).toBeVisible();
});
// Error case: declined card
test('shows decline error for rejected card', async ({ page }) => {
await fillCardForm(page, {
number: '{{declinedCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/declined|card.*not accepted/i);
await expect(page).toHaveURL(/\/checkout\/payment/);
});
// Error case: invalid card number format
test('shows inline error for invalid card number', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('1234');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByText(/invalid.*card number/i)).toBeVisible();
});
// Error case: expired card
test('shows error for expired card', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '01/20',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/expired|invalid.*expiry/i);
});
// Edge case: 3DS authentication required
test('handles 3DS challenge and completes payment', async ({ page }) => {
await fillCardForm(page, {
number: '{{threeDsCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
// 3DS modal appears
const challengeFrame = page.frameLocator('[data-testid="3ds-challenge-frame"]');
await challengeFrame.getByRole('button', { name: /complete authentication/i }).click();
await expect(page).toHaveURL(/\/order-confirmation|\/success/);
});
});
JavaScript
const { test, expect } = require('@playwright/test');
test.describe('Payment', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/checkout/payment');
});
test('completes payment with valid card', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('{{testCardNumber}}');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page).toHaveURL(/\/order-confirmation/);
});
test('shows decline error for rejected card', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('{{declinedCardNumber}}');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/declined/i);
});
});
Variants
| Variant | Description |
|---|---|
| Successful payment | Valid test card → order confirmation |
| Processing state | Button disabled + spinner during processing |
| Declined card | Error alert, stays on payment page |
| Invalid card number | Inline validation from provider |
| Expired card | Expiry error |
| 3DS challenge | Modal completed, payment succeeds |