How to Test Gift Cards on Web (Complete Guide)

Gift cards are a high‑value touchpoint for any e‑commerce site. They sit at the intersection of commerce, finance, and user experience, making them a prime target for bugs that can leak money, frustra

February 22, 2026 · 18 min read · How-To Guides

Why Gift Card Testing Matters

Gift cards are a high‑value touchpoint for any e‑commerce site. They sit at the intersection of commerce, finance, and user experience, making them a prime target for bugs that can leak money, frustrate customers, or expose compliance gaps. A single mis‑handled code can allow an attacker to generate unlimited credit, while a confusing UI can cause legitimate users to abandon a purchase and never return. Because gift‑card flows often involve multiple steps—selection, amount entry, personalization, payment, delivery, and redemption—defects can hide in any of those stages and only surface under specific combinations of data, device, or user behavior.

Testing gift cards therefore requires a disciplined approach that goes beyond “does the button work?”. You need to verify that the system correctly enforces business rules, protects against abuse, remains accessible to all users, and integrates cleanly with payment gateways and order‑management systems. The following sections give you a complete playbook: a concrete test matrix, manual and automated techniques, tooling tips, and a look at how autonomous, persona‑driven exploration can surface issues that scripted tests never think to try.

---

Gift Card Flow Overview

Before writing test cases, map the typical web‑based gift‑card journey. Although each implementation varies, most share these logical blocks:

BlockTypical ActionsKey Data Points
SelectionUser browses gift‑card catalog, picks a design or brandcardId, designId, priceTier
Amount EntryUser chooses a preset amount or enters a custom valueamount, currency, minAmount, maxAmount
PersonalizationOptional message, recipient name, delivery datesenderName, recipientName, message, deliveryDate
PaymentUser adds card to cart, proceeds to checkout, pays with credit card, PayPal, etc.paymentMethod, transactionId, tax, shippingFee
ConfirmationSystem shows order summary, sends email/SMS with codeorderId, giftCardCode, expiryDate
DeliveryCode delivered via email, SMS, or downloadable PDFdeliveryChannel, timestamp
Redemption (often tested separately)Recipient enters code at checkout, system validates and applies balancecode, appliedAmount, remainingBalance

Each block presents its own validation surface. For example, the amount block must reject non‑numeric input, enforce min/max limits, and handle currency rounding correctly. The personalization block may need to sanitize HTML to prevent XSS. The payment block must correctly invoke the gateway and handle asynchronous callbacks. The delivery block must ensure the code is transmitted only once and is not exposed in logs or client‑side storage. Mapping these blocks helps you build a matrix that covers every combination of valid and invalid data.

---

Test Matrix

Below is a comprehensive matrix that you can copy into a test‑management tool. Each row represents a distinct scenario; columns indicate the expected outcome and the type of test (manual, automated, or both).

IDFlow BlockDescriptionInput / ConditionExpected ResultTest Type
GC‑01SelectionHappy path – pick first available designValid cardIdCard added to cart, price shown correctlyBoth
GC‑02SelectionInvalid design IDNon‑existent designIdError message: “Design not found”Manual
GC‑03Amount EntryPreset amount selectionClick $25 presetAmount field shows 25.00, total updatesBoth
GC‑04Amount EntryCustom amount within limitsEnter 15.00 (min=10, max=500)Accepted, total = 15.00Both
GC‑05Amount EntryCustom amount below minimumEnter 5.00Inline validation: “Amount must be at least $10.00”Manual
GC‑06Amount EntryCustom amount above maximumEnter 1000.00Inline validation: “Amount cannot exceed $500.00”Manual
GC‑07Amount EntryNon‑numeric inputEnter “abc”Field rejects, shows error “Please enter a valid number”Both
GC‑08Amount EntryDecimal precisionEnter 10.005System rounds to 10.01 (or rejects based on policy)Automated
GC‑09PersonalizationEmpty sender nameLeave sender blankWarning: “Sender name is required” (if required)Manual
GC‑10PersonalizationHTML injection in messageMessage sanitized, script not executedAutomated (security)
GC‑11PersonalizationLong message > 200 chars250‑character stringTruncated to 200 or error if limit enforcedBoth
GC‑12PaymentSuccessful credit‑card chargeValid test card (e.g., 4242…)Order created, payment status = succeededBoth
GC‑13PaymentDeclined cardTest card 4000 0000 0000 0002Payment error shown, order not createdManual
GC‑14PaymentMissing CVVCVV field emptyInline error: “CVV is required”Manual
GC‑15PaymentDuplicate submission (double click)Click Pay twice quicklyOnly one transaction processed, second click shows “Processing…” or disabled buttonAutomated
GC‑16PaymentPayment gateway timeoutSimulate gateway delay >30sUser sees timeout message, option to retryManual
GC‑17ConfirmationEmail contains correct codeAfter successful paymentEmail body includes alphanumeric code matching DBAutomated
GC‑18ConfirmationCode exposed in URLAfter redemption, URL shows ?code=ABC123No code in URL; only token or session identifierAutomated (privacy)
GC‑19DeliverySMS delivery fails (invalid number)Enter malformed phone numberError: “Invalid phone number”, no SMS sentManual
GC‑20DeliveryEmail bounce handlingSend to non‑existent domainSystem logs bounce, shows “Delivery failed” in order historyManual
GC‑21RedemptionValid code appliedEnter correct code at checkoutBalance reduced by gift‑card amount, order total updatedBoth
GC‑22RedemptionInvalid code formatEnter “123” (too short)Error: “Invalid gift‑card code”Manual
GC‑23RedemptionExpired codeUse code with past expiry dateError: “This gift card has expired”Manual
GC‑24RedemptionAlready used codeRe‑use same codeError: “Gift card already redeemed”Manual
GC‑25RedemptionCode case sensitivityEnter lower‑case version of upper‑case codeSystem treats as invalid (if case‑sensitive) or accepts (if normalized)Both
GC‑26AccessibilityKeyboard navigationTab through gift‑card formAll interactive elements reachable, visible focus indicatorManual
GC‑27AccessibilityScreen reader labelsUse NVDA/JAWSEach field announces purpose (e.g., “Gift card amount, edit text”)Manual
GC‑28AccessibilityColor contrastVerify text vs background ratiosMinimum 4.5:1 for normal text, 3:1 for large textAutomated (axe)
GC‑29SecurityRate limiting on code generationAttempt 100 requests/min to generate codesAfter threshold, HTTP 429 Too Many RequestsAutomated
GC‑30SecuritySQL injection via amount fieldEnter 10'; DROP TABLE giftcards;--Input sanitized, no DB error, validation failsAutomated
GC‑31PrivacyLogging of full codeCheck server logs after purchaseOnly last 4 digits or hash stored, full code never loggedManual (log review)
GC‑32InternationalizationCurrency switchChange site locale to EUR, amounts in eurosAll amounts display with € symbol, correct conversion if applicableBoth
GC‑33Edge case – concurrent redemptionTwo users try same code at same timeSimultaneous requestsOnly first succeeds, second gets “already used”Automated (stress)
GC‑34Edge case – zero‑amount cardCreate card with amount 0.00Allowed by business?If not allowed, validation error; if allowed, code behaves like a promotional tokenManual
GC‑35Edge case – negative amountAttempt to enter –5.00System rejectsValidation error: “Amount must be greater than zero”Manual
GC‑36Edge case – maximum length fieldsEnter 500‑char sender nameIf limit 100, truncated or errorConsistent handling per specManual
GC‑37Edge case – special characters in recipient nameEnter “O’Connor‑Jean”Apostrophe and hyphen acceptedName stored correctly, no SQL errorManual
GC‑38Edge case – timezone on delivery dateSelect delivery date in future, user in different TZCode delivered at correct UTC timeDelivery timestamp matches user’s selected local dateManual
GC‑39Edge case – disabled JavaScriptDisable JS, load gift‑card pageFallback to server‑rendered formForm still functional, albeit with full‑page reloadsManual
GC‑40Edge case – slow network (3G)Throttle network to 3G speedsAll steps complete, no timeoutsUI shows loading spinners, no broken statesManual (DevTools)

How to use the matrix

---

Manual Testing Approach

Even with strong automation, manual testing remains essential for exploratory work, usability checks, and validation of edge cases that are hard to script. Follow this step‑by‑step routine for each release candidate.

1. Environment Preparation

2. Happy‑Path Walkthrough

  1. Navigate to the gift‑card catalog.
  2. Select a design, verify the thumbnail and price update instantly.
  3. Choose a preset amount (e.g., $50). Confirm the amount field reflects the choice and the subtotal updates.
  4. Click “Add to cart”, then proceed to checkout.
  5. Fill in payment details using a test card that always succeeds.
  6. Submit the order. Observe the confirmation page: order number, gift‑card code, and delivery method displayed.
  7. Check your test email inbox (or SMS simulator) for the delivery message containing the exact code shown on the confirmation page.
  8. Log out, then log in as a recipient (or use a separate test account) and attempt to redeem the code at checkout. Verify the balance deducted correctly and the remaining gift‑card amount shown.

If any step deviates, log the defect with screenshots, network request/response, and console errors.

3. Error‑Path Validation

For each error case in the matrix (GC‑02, GC‑05, GC‑06, GC‑08, etc.):

4. Accessibility Checks

5. Security & Privacy Spot Checks

6. Exploratory & Production‑Like Scenarios

Document any deviation with a concise reproduction steps, expected vs. actual, and severity rating.

---

Automated Approaches and Tooling

Automation provides regression safety and enables rapid feedback. For web‑based gift cards, focus on three layers: unit/service tests, API contract tests, and end‑to‑end UI tests.

1. Unit / Service Tests

Test the core gift‑card service logic in isolation. Typical language: Java/Node/Python depending on your stack.

Example (Node/Jest)


// giftCardService.test.js
const { generateCode, validateCode, applyCode } = require('../src/giftCardService');

describe('gift card service', () => {
  test('generates a 16‑character alphanumeric code', () => {
    const code = generateCode();
    expect(code.length).toBe(16);
    expect(/^[A-Z0-9]+$/.test(code)).toBe(true);
  });

  test('rejects codes with invalid checksum', () => {
    const bad = 'AAAAAAAAAAAAAAAA'; // purposely fails checksum
    expect(validateCode(bad)).toBe(false);
  });

  test('applies code and returns remaining balance', () => {
    const { remaining } = applyCode('VALIDCODE1234', 100.00, 25.00);
    expect(remaining).toBe(75.00);
  });
});

Run these tests on every commit; they guard against regressions in the business rules that drive the UI.

2. API Contract Tests

If your front‑end talks to a dedicated gift‑card micro‑service, validate the contract with tools like Pact or Dredd.

Example (Pact – JavaScript)


const { Pact } = require('@pact-foundation/pact');
const path = require('path');

const provider = new Pact({
  consumer: 'giftcard-web',
  provider: 'giftcard-service',
  port: 1234,
  log: path.resolve(process.cwd(), 'logs', 'pact.log'),
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('Gift Card Generation API', () => {
  describe('POST /v1/gift-cards', () => {
    before(() => provider.setup());
    after(() => provider.finalize());

    it('returns a code when amount is valid', () => {
      return provider
        .uponReceiving('a valid generation request')
        .withRequest({
          method: 'POST',
          path: '/v1/gift-cards',
          headers: { 'Content-Type': 'application/json' },
          body: { amount: 25, currency: 'USD' },
        })
        .willRespondWith({
          status: 201,
          headers: { 'Content-Type': 'application/json' },
          body: { code: like('ABCD1234EFGH5678') },
        })
        .then(() => {
          return fetch('http://localhost:1234/v1/gift-cards', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ amount: 25, currency: 'USD' }),
          })
            .then(res => res.json())
            .then(body => {
              expect(body.code).toMatch(/^[A-Z0-9]{16}$/);
            });
        });
    });
  });
});

Running this as part of CI ensures that UI changes that rely on the API will not break due to contract drift.

3. End‑to‑End UI Tests

Playwright is a strong choice for modern web apps because it offers auto‑waiting, network interception, and multi‑browser support. Below is a comprehensive script that covers happy path, error handling, and a security check.

Playwright (TypeScript) – giftCard.spec.ts


import { test, expect } from '@playwright/test';

test.describe('Gift Card Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/gift-cards');
  });

  test('happy path purchase and redemption', async ({ page }) => {
    // ---- Selection ----
    await page.selectOption('#design-select', 'design-01');
    await expect(page.locator('#price-display')).toHaveText('$25.00');

    // ---- Amount Entry ----
    await page.fill('#amount-input', '50.00');
    await expect(page.locator('#amount-input')).toHaveValue('50.00');
    await expect(page.locator('#subtotal')).toHaveText('$50.00');

    // ---- Personalization ----
    await page.fill('#sender-name', 'Alex');
    await page.fill('#recipient-name', 'Sam');
    await page.fill('#message', 'Happy Birthday!');
    await page.fill('#delivery-date', '2025-12-25');

    // ---- Payment (test card) ----
    await page.click('#checkout-btn');
    await page.waitForSelector('#card-number', { state: 'visible' });
    await page.fill('#card-number', '4242424242424242');
    await page.fill('#card-expiry', '12/34');
    await page.fill('#card-cvc', '123');
    await page.click('#pay-btn');

    // ---- Confirmation ----
    await expect(page.locator('#order-confirmation')).toBeVisible();
    const codeLocator = page.locator('#gift-card-code');
    await expect(codeLocator).toBeVisible();
    const giftCode = await codeLocator.innerText();
    expect(giftCode.length).toBe(16);
    expect(/^[A-Z0-9]+$/.test(giftCode)).toBe(true);

    // ---- Email verification (using a test mailbox like Mailosaur) ----
    const mail = await page.context().request.post('https://mailosaur.com/api/messages', {
      // omitted for brevity – fetch latest email and assert body contains giftCode
    });
    expect(mail.body).toContain(giftCode);

    // ---- Logout and login as recipient ----
    await page.click('#logout-link');
    await page.goto('/login');
    await page.fill('#email', 'sam@test.com');
    await page.fill('#password', 'TestPass123!');
    await page.click('#login-btn');

    // ---- Redemption ----
    await page.goto('/cart');
    await page.fill('#gift-card-input', giftCode);
    await page.click('#apply-giftcard');
    await expect(page.locator('#discount')).toHaveText('-$50.00');
    await expect(page.locator('#total')).toHaveText('$0.00');
  });

  test('amount validation rejects below minimum', async ({ page }) => {
    await page.selectOption('#design-select', 'design-01');
    await page.fill('#amount-input', '5.00'); // below $10 min
    await expect(page.locator('#amount-error')).toHaveText(/Amount must be at least \$10\.00/);
    await expect(page.locator('#checkout-btn')).toBeDisabled();
  });

  test('message field sanitizes XSS', async ({ page }) => {
    await page.selectOption('#design-select', 'design-01');
    await page.fill('#amount-input', '20.00');
    await page.fill('#message', '<script>alert(1)</script>');
    await page.click('#checkout-btn');
    // After submit, check that the script tag is escaped in the confirmation modal
    await page.waitForSelector('#gift-card-code');
    const displayedMessage = await page.locator('#confirmation-message').innerText();
    expect(displayedMessage).not.toContain('<script>');
    expect(displayedMessage).toContain('<script>');
  });

  test('rate limiting on generation endpoint', async ({ page }) => {
    // Use API request directly to bypass UI throttling
    const apiResponse = await page.request.post('/api/v1/gift-cards/generate', {
      data: JSON.stringify({ amount: 10, currency: 'USD' }),
      headers: { 'Content-Type': 'application/json' },
    });
    // First few succeed
    for (let i = 0; i < 5; i++) {
      const resp = await page.request.post('/api/v1/gift-cards/generate', {
        data: JSON.stringify({ amount: 10, currency: 'USD' }),
        headers: { 'Content-Type': 'application/json' },
      });
      expect(resp.ok()).toBeTruthy();
    }
    // After threshold, expect 429
    const resp = await page.request.post('/api/v1/gift-cards/generate', {
      data: JSON.stringify({ amount: 10, currency: 'USD' }),
      headers: { 'Content-Type': 'application/json' },
    });
    expect(resp.status()).toBe(429);
  });
});

Why this script works

4. Visual Regression

Gift‑card UI often includes custom designs and thematic backgrounds. Use a tool like Percy or Chromatic to capture screenshots of the gift‑card picker and confirmation modal after each release.

Example (Percy CLI)


# After building the storybook or running the app
PERCY_TOKEN=your_token npx percy exec -- playwright test

Percy will compare the new screenshots against the baseline and flag any unintended visual changes (e.g., a button shifted, a missing icon).

5. Performance & Load Testing

While functional correctness is primary, gift‑card generation can be a hotspot during promotions. Use k6 or Gatling to simulate bursts of purchase requests.

k6 script snippet


import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 }, // ramp up to 50 VUs
    { duration: '5m', target: 50 }, // stay at 50
    { duration: '2m', target: 0 },  // ramp down
  ],
};

export default function () {
  const payload = JSON.stringify({
    amount: 25,
    currency: 'USD',
    senderName: 'Tester',
    recipientName: 'Friend',
    message: 'Thanks!',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post('https://staging.example.com/api/v1/gift-cards', payload, params);
  check(res, {
    'status is 201': (r) => r.status === 201,
    'code present': (r) => r.json().code !== '',
  });
  sleep(1);
}

Run the script and inspect the response time percentiles; ensure the 95th‑percentile stays under your SLA (e.g., 2 seconds).

---

Tooling & Setup

CategoryToolReason for ChoiceQuick Setup
Test FrameworkPlaywright (JS/TS)Auto‑wait, multi‑browser, API request supportnpm i -D @playwright/test
Unit TestingJest / MochaFast, mature assertion librarynpm i -D jest
API ContractPactConsumer‑driven contracts, language‑agnosticnpm i -D @pact-foundation/pact
Accessibilityaxe‑core (via playwright-axe)Integrated WCAG checksnpm i -D @axe-core/playwright
Visual RegressionPercy / ChromaticBaseline comparison, CI‑friendlynpx percy exec -- playwright test
Load Testingk6Scriptable in JS, cloud or localbrew install k6 (mac)
Mail CaptureMailosaur / EtherealReliable test inbox for email/SMS verificationSign up, obtain API key
Secrets Managementdotenv / VaultKeep test API keys out of reponpm i dotenv
CI IntegrationGitHub Actions / GitLab CIRun matrix on push/PRAdd workflow file (see below)

Sample GitHub Actions Workflow


name: Gift Card CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - run: npm ci
      - name: Run Unit Tests
        run: npm test
      - name: Run Playwright Tests
        env:
          PLAYWRIGHT_BROWSERS_PATH: 0
        run: npx playwright test --project=${{ matrix.browser }}
      - name: Upload Playwright Report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
      - name: Run Accessibility Scan
        run: npx playwright test --grep @accessibility
      - name: Run Visual Regression (Percy)
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
        run: npx percy exec -- playwright test

This workflow runs unit tests, Playwright UI tests across three browsers, accessibility‑tagged tests, and Percy visual checks on every push. Adjust the matrix and secrets to fit your environment.

---

Autonomous, Persona‑Driven Exploration

Even the most thorough test matrix can miss scenarios that arise from real‑world user behavior—especially when users act outside the “happy‑path” assumptions. Autonomous QA platforms like SUSA address this gap by exploring the application with simulated user personalities, each driven by a distinct behavior profile.

How It Works

  1. Model Extraction – Upon receiving a URL or an APK (for hybrid web views), SUSA builds a dynamic state‑transition model of the front‑end: pages, UI elements, and possible actions (click, type, select, scroll).
  2. Persona Injection – For each session, SUSA selects a persona (e.g., *impatient*, *elderly*, *adversarial*) and applies its policy:
  1. Exploration Loop – The agent walks the model, making choices guided by the persona’s policy, while logging every network request, DOM mutation, and console error.
  2. Learning – Screens visited and dead ends (e.g., a button that leads to a 500 error) are stored; subsequent runs prioritize unexplored paths, increasing coverage over time.

Gift‑Card‑Specific Findings

When run against a staging gift‑card flow, SUSA has repeatedly uncovered issues that scripted tests never considered:

PersonaDiscovered IssueWhy Scripts Missed It
ImpatientDouble‑click on the “Apply Gift Card” button caused the discount to be applied twice, leading to a negative order total.Automated scripts usually insert a wait or a single click; they never simulate rapid repeated clicks without explicit throttling.
ElderlyThe gift‑card amount field’s increment/decrement arrows were too small for users with motor impairments, causing repeated mis‑clicks.UI tests interact with the input directly via fill(), bypassing the arrow buttons entirely.
AdversarialEntering '; SELECT * FROM users;-- in the recipient name field triggered a DB error that was logged and exposed a stack trace in the response body.Security tests often target known endpoints (like search) but neglect fields assumed to be “just text”.
Curious (novice)After a failed payment, the “Try Again” button redirected to a blank page because the query parameter ?error=true was not handled by the route.Test cases usually follow the success path; error‑state redirects are rarely enumerated unless explicitly added.
Accessibility (screen‑reader)Custom tooltip announcing the remaining balance used aria-label that changed dynamically but was not updated when the balance changed, causing stale announcements.Automated axe checks flag missing labels but not label‑staleness caused by JS state updates.

Integrating SUSA into Your Pipeline

You can run SUSA as a lightweight container or CLI step after your UI test suite:


# Install the agent (once)
pip install susatest-agent

# Execute a 5‑minute exploratory session against staging
susatest-agent run \
  --url https://staging.example.com/gift-cards \
  --personas impatient,elderly,adversarial,curious,accessibility \
  --duration 5m \
  --output susa-report.json

The output contains a JSON log of every action, any observed errors, and a coverage map (percentage of states visited). You can fail the build if the error count exceeds a threshold or if coverage drops below a baseline (indicating regressions that block exploration).

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free