How to Test Order Tracking on Web (Complete Guide)

Order tracking is a critical touch‑point in any e‑commerce or service lets users see whereabout to‑status. When this feature works, customers feel confident that their purchase is on track; when it fa

April 20, 2026 · 16 min read · How-To Guides

Introduction

Order tracking is a critical touch‑point in any e‑commerce or service lets users see whereabout to‑status. When this feature works, customers feel confident that their purchase is on track; when it fails, frustration spikes, support tickets rise, and repeat purchase likelihood drops. Because order tracking often pulls data from multiple micro‑services, caches, and third‑party carriers, it is a hotspot for race conditions, stale data, permission bugs, and UI glitches that only surface under real‑world load.

Testing this flow therefore requires more than a happy‑path click‑through. You must verify that the UI correctly reflects every possible state of an order (processing stage, that error messages are helpful and accessible, that no personal data leaks, and that the experience works for users with varied abilities and devices. The following guide walks you through a complete strategy: a test matrix that captures all relevant scenarios, a manual step‑by‑step routine, automated approaches using popular web test frameworks, how autonomous, persona‑driven exploration can surface hidden bugs, production observability tips, and a concise checklist you can attach to any release.

---

Test Matrix for Order Tracking

Below is a comprehensive matrix that you can copy into a test‑management tool or keep as a living document. Each row groups a scenario, describes the pre‑conditions, the actions to perform, the expected outcome, and a suggested priority (P0 = must‑fix before release, P1 = important, P2 = nice‑to‑have).

IDCategoryScenarioPre‑conditionsStepsExpected ResultPriority
OT‑01Happy pathUser views tracking for a shipped orderUser logged in, order status = “Shipped”, carrier API returns tracking number & URL1. Navigate to My Orders
2. Click the order row
3. Select Track Order
Tracking page loads, shows carrier name, tracking number, map with current location, estimated delivery date, and a “View on carrier site” link that opens the carrier’s URL in a new tabP0
OT‑02Happy pathUser views tracking for an order still processingOrder status = “Processing”, no carrier data yetSame as OT‑01Page shows “Your order is being prepared. We’ll update you when it ships.” with a spinner or placeholder, no carrier fieldsP0
OT‑03Happy pathUser views tracking for a delivered orderOrder status = “Delivered”, carrier API returns final scanSame as OT‑01Page shows delivered status, delivery timestamp, signature (if available), and a button to Leave a ReviewP0
OT‑04Error pathCarrier API returns 500 errorMock carrier endpoint returns HTTP 500Same as OT‑01UI displays a friendly error: “We’re unable to retrieve tracking info at the moment. Please try again later.” with a Retry button; no stack trace exposedP1
OT‑05Error pathTracking number is malformed (contains spaces)Backend stores tracking number with leading/trailing spacesSame as OT‑01UI trims the value before displaying; if still invalid, shows error as in OT‑04P1
OT‑06Error pathUser not authenticated tries to access tracking URL directlyNo session cookie, direct link to /orders/123/trackPaste URL in browserRedirect to login page, after login redirects back to the same tracking pageP1
OT‑07Edge caseOrder split across multiple shipmentsOrder has two shipments, each with its own tracking numberSame as OT‑01Page lists each shipment in separate cards, each with its own carrier, number, map, and ETAP1
OT‑08Edge caseCarrier changes tracking URL format mid‑shipmentFirst half of shipment uses old URL pattern, second half uses new pattern (simulated via feature flag)Same as OT‑01, refresh after carrier updatePage correctly renders both URLs; clicking each opens the appropriate carrier siteP2
OT‑09Edge caseVery long order number ( > 50 chars )Order ID generated by a legacy system is 55 charactersSame as OT‑01Layout does not break; text truncates with ellipsis or wraps gracefullyP2
OT‑10AccessibilityScreen reader user navigates tracking pageNVDA or VoiceOver active, page loadedUse screen reader to read heading hierarchy, landmarks, and live regionsHeading level 1 present for order number, region labelled “Tracking information”, live region announces status updates when they changeP1
OT‑11AccessibilityKeyboard‑only user accesses trackingNo mouse, tab order onlyTab through all interactive elementsFocus moves logically: order list → track button → carrier name → tracking number → map (if focusable) → retry button → view carrier link; no focus trapsP1
OT‑12AccessibilityColor contrast for status indicatorsStatus badge uses green for delivered, orange for processing, red for errorInspect CSSContrast ratio ≥ 4.5:1 for AA text, ≥ 3:1 for large text per WCAG 2.1P1
OT‑13Security/PrivacyTracking page leaks order ID in URL fragmentURL shows #order-id=98765 after navigationObserve network and URLNo sensitive order identifiers appear in URL, query string, or fragment; only non‑guidable tokens (e.g., UUID) if neededP1
OT‑14Security/PrivacyCarrier site opens in same tab, exposing referrerClick View on carrier site opens in same tabClick linkLink uses target="_blank" rel="noopener noreferrer" to prevent referrer leakage and tab‑nabbingP1
OT‑15Security/PrivacyClickjacking via iframe embeddingAttacker tries to embed tracking page in an invisible iframeAttempt to embedPage sends X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none'P1
OT‑16PerformanceTracking page loads under 2 s on 3G simulated throttleNetwork throttled to 3G, CPU throttled 4xLoad pageFirst Contentful Paint < 1.2 s, Time to Interactive < 2.5 sP2
OT‑17LocalizationUser persona‑persona = “Track” as quickly scrolls and clicks track button before order list fully rendersUI does not throw JavaScript errors; loading spinner appears if data not ready, no missing elementsP2
OT‑18User‑generated contentReview section shows profanity filterUser submits a review with blocked wordsSubmit reviewProfanity replaced with asterisks or review blocked with error messageP2

*Notes:*

---

Manual Testing Approach

Even with strong automation, a manual sanity check catches nuances that scripts may overlook, especially around visual layout, unexpected dialogs, and accessibility heuristics.

Setting up a test environment

  1. Isolate a test tenant – Use a dedicated environment (e.g., staging-ordertracking) with its own database seed. Seed data should include orders in every status (pending, processing, shipped, delivered, cancelled) and at least one order with a split shipment.
  2. Enable API mocking – If you rely on external carriers, spin up a local mock server (e.g., using json-server or Mockoon) that returns controllable responses (success, 500, delayed, malformed). Point the frontend to this mock via environment variables or a feature flag.
  3. Prepare assistive technology – Install NVDA (Windows) or VoiceOver (macOS) and a browser extension like axe or Lighthouse for quick accessibility scans.
  4. Set up device lab – Have at least one desktop (Chrome/Firefox/Safari) and one mobile viewport (iOS Safari, Android Chrome) to verify responsive breakpoints.

Step‑by‑step manual test execution

Below is a repeatable script you can follow for each test case in the matrix. Adjust the data values per case.

  1. Login – Use a test credential that belongs to the seeded user. Verify you land on the dashboard.
  2. Navigate to My Orders – Click the navigation item; confirm the URL updates to /orders and the list loads.
  3. Locate the target order – Use the order ID column or a filter (if available) to find the order matching the pre‑condition (e.g., status = Shipped).
  4. Open tracking – Click the Track Order button or link. Observe any loading indicators.
  5. Validate page title and heading – Ensure

    contains the order number or a friendly label like “Tracking for order #12345”.

  6. Check carrier information – Verify carrier name, tracking number (if any), and that the number is presented as plain text (not an input).
  7. Map component – If a map is present, pan and zoom to confirm it loads tiles; ensure no console errors related to map API keys.
  8. Estimated delivery – Confirm the date is formatted per locale (e.g., MM/DD/YYYY for en‑US).
  9. Action buttons – Test Retry (if present) and View on carrier site. For the latter, right‑click → *Open link in new tab* and verify the new tab opens to the carrier’s URL with noopener.
  10. Error simulation – If testing an error path, trigger the mock to return 500 or malformed data, then repeat steps 5‑9 and verify the error UI appears.
  11. Accessibility quick check – Run axe from the devtools pane; note any violations of WCAG 2.1 AA. Fix or log them.
  12. Keyboard navigation – Tab through the page; ensure focus order is logical and visible focus rings are present.
  13. Screen reader test – Activate NVDA/VoiceOver, navigate to the tracking region, and listen for announcements of status changes (e.g., when you click Retry and the status updates).
  14. Logout and re‑login – Confirm that after a session expires, attempting to access the tracking URL redirects to login and then back to the same page.
  15. Clean up – Clear cookies/localStorage if you tested multiple personas in the same session to avoid cross‑contamination.

Observables and logging

Common pitfalls

---

Automated Testing with Code

Automation provides repeatability, scalability, and the ability to run the matrix on every commit. Below we outline a practical setup using Playwright (Chromium/Firefox/WebKit) because of its strong auto‑waiting, tracing, and built‑in support for multiple browsers. Equivalent patterns apply to Cypress or Selenium; a comparison table follows.

Choosing a framework

FeaturePlaywrightCypressSelenium WebDriver
Multi‑browser (Chrome, Firefox, Safari)❌ (Chrome‑only, Firefox experimental)
Auto‑waiting for network/idle✅ (limited)❌ (requires explicit waits)
Built‑in tracing & video❌ (needs plugins)
Cross‑language (JS/TS, Python, Java, .NET)✅ (JS/TS primary)✅ (JS only)✅ (many languages)
Easy iframe handling❌ (cumbersome)
Community & pluginsGrowing fastMatureVery mature
Setup complexityLowLowMedium (driver binaries)

For most web‑only teams, Playwright offers the best balance of power and simplicity.

Setting up the test project


# Initialize a Node project
npm init -y
# Install Playwright and the axe-core accessibility plugin
npm i -D @playwright/test @axe-core
# Install TypeScript (optional but recommended)
npm i -D typescript @types/node
# Create a basic config
npx playwright install

Create playwright.config.ts:


import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://staging-ordertracking.example.com',
    trace: 'on-first-retry',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Add a helper for API mocking with MSW (optional but useful for error paths):


npm i -D msw

Create src/mocks/handlers.ts:


import { rest } from 'msw';

export const handlers = [
  rest.get('https://api.carrier.example/track/:id', (req, res, ctx) => {
    // Simulate success by default
    return res(
      ctx.status(200),
      ctx.json({
        carrier: 'FastShip',
        trackingNumber: req.params.id as string,
        events: [
          { time: '2025-09-20T10:00:00Z', status: 'Out for delivery' },
          { time: '2025-09-21T14:30:00Z', status: 'Delivered' },
        ],
      })
    );
  }),
];

Start the mock server in tests/setup.ts:


import { setupServer } from 'msw/node';
import { handlers } from '../src/mocks/handlers';

export const server = setupServer(...handlers);

// Establish API mocking before all tests.
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
// Reset any request handlers that we may add during the tests,
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());

Reference the setup in playwright.config.ts:


use: {
  // ...
  // Launch the server before each test
  // (Playwright does not directly import test hooks, so we use a global setup)
},
globalSetup: require.resolve('./tests/setup.ts'),

Implementing happy path test

Create tests/order-tracking.spec.ts:


import { test, expect } from '@playwright/test';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test.describe('Order Tracking – happy path', () => {
  test.beforeEach(async ({ page }) => {
    // Login via UI or API shortcut
    await page.goto('/login');
    await page.fill('#email', 'testbuyer@example.com');
    await page.fill('#password', 'Secure!23');
    await page.click('button[type="submit"]');
    await page.waitForURL('/orders');
  });

  test('shows tracking info for a shipped order', async ({ page }) => {
    // Find the order with status Shipped (assume a badge)
    const shippedRow = page.locator('tr[data-status="shipped"]').first();
    await expect(shippedRow).toBeVisible();
    await shippedRow.click('button:has-text("Track Order")');

    // Wait for tracking page to load
    await expect(page).toHaveURL(/\/orders\/.*\/track/);
    await expect(page.locator('h1')).toContainText('Tracking for order');

    // Carrier name and number
    await expect(page.locator('[data-testid="carrier-name"]')).toHaveText('FastShip');
    await expect(page.locator('[data-testid="tracking-number"]')).toMatch(/^[A-Z0-9]{10,}$/);

    // Map component – wait for the iframe to load
    const mapFrame = page.frameLocator('iframe[title="tracking-map"]');
    await expect(mapFrame.locator('.map-container')).toBeVisible();

    // Estimated delivery
    await expect(page.locator('[data-testid="estimated-delivery"]'))
      .toHaveText(/Estimated delivery: .+/);

    // View on carrier site opens in new tab with rel attributes
    const [newPage] = await Promise.all([
      page.waitForEvent('popup'),
      page.click('[data-testid="view-carrier-link"]'),
    ]);
    await expect(newPage).toHaveURL(/carrier\.example/);
    // Verify opener is null due to noopener
    await expect(newPage.evaluate(() => window.opener)).toBeNull();

    // Accessibility check
    const accessibilitySnapshot = await axe.run(page);
    expect(accessibilitySnapshot).toHaveNoViolations();
  });
});

Explanation of key patterns

Handling dynamic IDs and waiting for network requests

If the backend returns a tracking ID that changes each run‑dependent URL like /orders/abc123/track, you can still assert using a regex:


await expect(page).toHaveURL(/\/orders\/[^/]+\/track/);

For situations where the tracking data is fetched via an XHR after the initial render, wait for the request to finish:


await page.waitForResponse(resp =>
  resp.url().includes('/api/order/track') && resp.status() === 200
);

Or, if you prefer to wait for a specific DOM change that indicates data arrived:


await page.waitForSelector('[data-testid="tracking-number"]:not(:empty)');

Parameterizing error scenarios

Use test.each to feed multiple mock responses:


const errorCases = [
  { status: 500, description: 'carrier server error' },
  { status: 404, description: 'tracking not found' },
  { status: 200, body: { trackingNumber: '' }, description: 'empty tracking number' },
];

test.each(errorCases)('handles $description', async ({ page }, { status, description, body }) => {
  // Override the MSW handler for this test only
  server.use(
    rest.get('https://api.carrier.example/track/:id', (req, res, ctx) => {
      if (body) {
        return res(ctx.status(200), ctx.json(body));
      }
      return res(ctx.status(status));
    })
  );

  await test.step('login and navigate to order', async () => {
    // ... login steps as before ...
  });

  await test.step('open tracking and verify error UI', async () => {
    await page.click('button:has-text("Track Order")');
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="retry-button"]')).toBeEnabled();
    // Ensure no stack trace is shown
    await expect(page.locator('text=Stack trace')).not.toBeVisible();
  });
});

Data‑driven testing for edge cases

Edge cases like split shipments or long order IDs can be driven from a JSON fixture:


import splitShipmentFixture from '../fixtures/split-shipment.json';

test.each(splitShipmentFixture.scenarios)('split shipment: $description', async ({ page }, scenario) => {
  // Setup backend to return scenario.payload via MSW
  server.use(
    rest.get('/api/orders/:id/track', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json(scenario.payload));
    })
  );

  await page.goto(`/orders/${scenario.orderId}`);
  await page.click('button:has-text("Track Order")');
  // Assert each shipment card
  const cards = page.locator('[data-testid="shipment-card"]');
  await expect(cards).toHaveCount(scenario.expectedCardCount);
  for (let i = 0; i < scenario.expectedCardCount; i++) {
    await expect(cards.nth(i)).toContainText(scenario.expectedCarriers[i]);
  }
});

Accessibility automation

Beyond the inline axe.run call, you can run a full audit in CI:


test('full page passes WCAG 2.1 AA', async ({ page }) => {
  await page.goto('/orders/123/track');
  const results = await axe.run(page, {
    rules: {
      // Disable rules that are not applicable in your context
      color-contrast: { enabled: true },
      label: { enabled: true },
    },
  });
  expect(results.violations).toHaveLength(0);
});

If you prefer to generate a report:


npx playwright test --reporter=json > results.json
npx axe-cli results.json --output=report.html

Security/privacy checks


  test('carrier link does not leak referrer', async ({ page }) => {
    await page.goto('/orders/123/track');
    const [popup] = await Promise.all([
      page.waitForEvent('popup'),
      page.click('[data-testid="view-carrier-link"]'),
    ]);
    const openerVal = await popup.evaluate(() => window.opener);
    expect(openerVal).toBeNull();
  });

  test('tracking page sends X-Frame-Options: DENY', async ({ page }) => {
    let frameHeader = '';
    await page.route('**/orders/*/track', route => {
      const response = await route.fetch();
      frameHeader = response.headers()['x-frame-options'] || '';
      route.continue();
    });
    await page.goto('/orders/123/track');
    expect(frameHeader.toUpperCase()).toBe('DENY');
  });

  test('URL does not expose PII', async ({ page }) => {
    await page.goto('/orders/123/track');
    const url = page.url();
    expect(url).not.toMatch(/email=/i);
    expect(url).not.toMatch(/orderId=\d+/);
  });

CI integration

Add a script to your package.json:


{
  "scripts": {
    "test": "playwright test",
    "test:ci": "playwright test --workers=4"
  }
}

In GitHub Actions (example):


name: Web Tests
on: [push, pull_request]
jobs:
  playwright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm test
      - if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          location: playwright-report/

This setup gives you a fast, reliable test suite that covers the matrix, catches regressions, and provides rich diagnostics when something fails.

---

Autonomous, Persona‑Driven Exploration

Even the most thorough matrix can miss unexpected interaction patterns—especially those that arise from real users behaving in ways a script never anticipates. Autonomous exploration tools, such as SUSA (SUSATest), address this gap by simulating a variety of user personas that interact with the application without pre‑written steps.

How persona‑driven testing works

SUSA builds a behavior model for each persona (e.g., *curious*, *impatient*, *elderly*, *accessibility‑focused*, *adversarial*). Each model defines:

The platform then drives a headless browser, letting the persona navigate freely. It records every DOM mutation, network request, console error, and accessibility violation. Over multiple runs, SUSA builds a knowledge map of visited screens and dead ends, learning which paths lead to crashes or infinite loops and which are safe to prune.

What SUSA does differently

Unlike scripted tests that follow a fixed sequence, SUSA’s strength lies in its stateless exploration combined with persona‑specific heuristics. For order tracking, this means:

Because SUSA does not depend on pre‑defined selectors, it can discover bugs tied to dynamic IDs, shadow DOM, or Web Components that break traditional locators.

Example findings from autonomous exploration

In a recent run on a staging e‑commerce site, SUSA surfaced the following order‑tracking issues that were not in the original matrix:

PersonaObservationRoot causeImpact
ImpatientClicked Retry three times within 800 ms while the spinner was visible.The retry handler did not debounce; each click launched a new XHR, and the UI overwrote the tracking number with the response from the *last* request, causing a flash of incorrect data.Users could see a wrong tracking number briefly, leading to confusion and support calls.
Curious

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