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>
149 lines
5.4 KiB
Markdown
149 lines
5.4 KiB
Markdown
# 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
|
|
|
|
```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
|
|
|
|
```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 |
|