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
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).
| ID | Category | Scenario | Pre‑conditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|---|
| OT‑01 | Happy path | User views tracking for a shipped order | User logged in, order status = “Shipped”, carrier API returns tracking number & URL | 1. 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 tab | P0 |
| OT‑02 | Happy path | User views tracking for an order still processing | Order status = “Processing”, no carrier data yet | Same as OT‑01 | Page shows “Your order is being prepared. We’ll update you when it ships.” with a spinner or placeholder, no carrier fields | P0 |
| OT‑03 | Happy path | User views tracking for a delivered order | Order status = “Delivered”, carrier API returns final scan | Same as OT‑01 | Page shows delivered status, delivery timestamp, signature (if available), and a button to Leave a Review | P0 |
| OT‑04 | Error path | Carrier API returns 500 error | Mock carrier endpoint returns HTTP 500 | Same as OT‑01 | UI 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 exposed | P1 |
| OT‑05 | Error path | Tracking number is malformed (contains spaces) | Backend stores tracking number with leading/trailing spaces | Same as OT‑01 | UI trims the value before displaying; if still invalid, shows error as in OT‑04 | P1 |
| OT‑06 | Error path | User not authenticated tries to access tracking URL directly | No session cookie, direct link to /orders/123/track | Paste URL in browser | Redirect to login page, after login redirects back to the same tracking page | P1 |
| OT‑07 | Edge case | Order split across multiple shipments | Order has two shipments, each with its own tracking number | Same as OT‑01 | Page lists each shipment in separate cards, each with its own carrier, number, map, and ETA | P1 |
| OT‑08 | Edge case | Carrier changes tracking URL format mid‑shipment | First half of shipment uses old URL pattern, second half uses new pattern (simulated via feature flag) | Same as OT‑01, refresh after carrier update | Page correctly renders both URLs; clicking each opens the appropriate carrier site | P2 |
| OT‑09 | Edge case | Very long order number ( > 50 chars ) | Order ID generated by a legacy system is 55 characters | Same as OT‑01 | Layout does not break; text truncates with ellipsis or wraps gracefully | P2 |
| OT‑10 | Accessibility | Screen reader user navigates tracking page | NVDA or VoiceOver active, page loaded | Use screen reader to read heading hierarchy, landmarks, and live regions | Heading level 1 present for order number, region labelled “Tracking information”, live region announces status updates when they change | P1 |
| OT‑11 | Accessibility | Keyboard‑only user accesses tracking | No mouse, tab order only | Tab through all interactive elements | Focus moves logically: order list → track button → carrier name → tracking number → map (if focusable) → retry button → view carrier link; no focus traps | P1 |
| OT‑12 | Accessibility | Color contrast for status indicators | Status badge uses green for delivered, orange for processing, red for error | Inspect CSS | Contrast ratio ≥ 4.5:1 for AA text, ≥ 3:1 for large text per WCAG 2.1 | P1 |
| OT‑13 | Security/Privacy | Tracking page leaks order ID in URL fragment | URL shows #order-id=98765 after navigation | Observe network and URL | No sensitive order identifiers appear in URL, query string, or fragment; only non‑guidable tokens (e.g., UUID) if needed | P1 |
| OT‑14 | Security/Privacy | Carrier site opens in same tab, exposing referrer | Click View on carrier site opens in same tab | Click link | Link uses target="_blank" rel="noopener noreferrer" to prevent referrer leakage and tab‑nabbing | P1 |
| OT‑15 | Security/Privacy | Clickjacking via iframe embedding | Attacker tries to embed tracking page in an invisible iframe | Attempt to embed | Page sends X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' | P1 |
| OT‑16 | Performance | Tracking page loads under 2 s on 3G simulated throttle | Network throttled to 3G, CPU throttled 4x | Load page | First Contentful Paint < 1.2 s, Time to Interactive < 2.5 s | P2 |
| OT‑17 | Localization | User persona‑persona = “Track” as quickly scrolls and clicks track button before order list fully renders | UI does not throw JavaScript errors; loading spinner appears if data not ready, no missing elements | P2 | ||
| OT‑18 | User‑generated content | Review section shows profanity filter | User submits a review with blocked words | Submit review | Profanity replaced with asterisks or review blocked with error message | P2 |
*Notes:*
- For error‑path tests you can mock the carrier API using tools like MSW (Mock Service Worker) or wiremock.
- Edge cases often require feature flags or backend toggles to simulate rare states without affecting production data.
- Prioritization reflects impact on conversion and support load; adjust to your product’s risk tolerance.
---
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
- 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. - 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.
- Prepare assistive technology – Install NVDA (Windows) or VoiceOver (macOS) and a browser extension like axe or Lighthouse for quick accessibility scans.
- 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.
- Login – Use a test credential that belongs to the seeded user. Verify you land on the dashboard.
- Navigate to My Orders – Click the navigation item; confirm the URL updates to
/ordersand the list loads. - 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).
- Open tracking – Click the Track Order button or link. Observe any loading indicators.
- Validate page title and heading – Ensure
contains the order number or a friendly label like “Tracking for order #12345”. - Check carrier information – Verify carrier name, tracking number (if any), and that the number is presented as plain text (not an input).
- Map component – If a map is present, pan and zoom to confirm it loads tiles; ensure no console errors related to map API keys.
- Estimated delivery – Confirm the date is formatted per locale (e.g.,
MM/DD/YYYYfor en‑US). - 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. - 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.
- Accessibility quick check – Run axe from the devtools pane; note any violations of WCAG 2.1 AA. Fix or log them.
- Keyboard navigation – Tab through the page; ensure focus order is logical and visible focus rings are present.
- 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).
- 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.
- Clean up – Clear cookies/localStorage if you tested multiple personas in the same session to avoid cross‑contamination.
Observables and logging
- Network tab – Filter to
*/track*or*/carrier*to see request/response payloads, status codes, and timing. - Console – Look for uncaught exceptions, especially when mocking latency or errors.
- Application → Storage – Verify that no sensitive data (e.g., full name, email) is stored in plain‑text localStorage or sessionStorage.
- Performance tab – Record a trace; check for long tasks (>50 ms) that could block interaction.
Common pitfalls
- Assuming static IDs – Front‑end frameworks often generate dynamic IDs; rely on data attributes (
data-testid="track-button") or text content for selectors. - Overlooking lazy‑loaded maps – Maps may load only when scrolled into view; force a scroll or wait for the map container to gain a class like
map-loaded. - Missing i18n fallbacks – If a locale lacks a translation for “Estimated delivery”, the UI may show the raw key; verify all strings are present.
- Cache stale data – Service workers may cache an old tracking response; disable cache in devtools or add a query‑string bust (
?nocache=${Date.now()}) when testing. - Third‑party consent banners – If a cookie banner appears, it can obscure the track button; handle acceptance as part of the test flow or disable the banner in the test environment.
---
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
| Feature | Playwright | Cypress | Selenium 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 & plugins | Growing fast | Mature | Very mature |
| Setup complexity | Low | Low | Medium (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
- Locator strategy – We use
data-testidattributes wherever possible; they survive refactors and are independent of visual text. - Auto‑waiting – Playwright waits for elements to be attached, stable, and visible before acting; we rarely need explicit
await page.waitForTimeout(). - Popup handling –
page.waitForEvent('popup')captures the new tab opened by the carrier link. - Accessibility – The
jest-axeintegration runs axe core on the page and asserts zero violations; you can adjust the rule set if certain violations are accepted (e.g., contrast for decorative icons).
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
- Referrer policy – Verify that carrier links include
rel="noopener noreferrer":
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();
});
- Clickjacking headers – You can assert response headers via Playwright’s
routeAPI:
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');
});
- No sensitive data in URL – Ensure that after navigation, the URL does not contain the user’s email or order ID in plain form (if you use opaque tokens):
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:
- Interaction tempo – how fast the persona clicks, scrolls, or types.
- Decision rules – what the persona does when faced with a loading spinner, an error message, or a modal.
- Exploration bias – curious personas tend to click every visible link; impatient personas abandon after a timeout; elderly personas prefer larger touch targets and avoid small icons.
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:
- A *curious* persona might open the tracking page, then click the carrier’s logo, navigate to the carrier’s site, follow a link to the carrier’s blog, and return via the browser’s back button—exercising the back‑stack handling that a script rarely tests.
- An *impatient* persona may repeatedly hit the Retry button while a spinner is visible, exposing race conditions where the retry triggers a second request before the first completes, potentially causing duplicate processing or UI state corruption.
- An *accessibility* persona using simulated screen‑reader navigation will rely on ARIA landmarks and live regions; if the tracking page fails to update a live region when the status changes, SUSA will flag a missing
aria-liveupdate. - An *adversarial* persona may attempt to inject scripts into input fields (e.g., by pasting a JavaScript URL into the tracking number box if the UI mistakenly renders it as a link) to test for XSS.
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:
| Persona | Observation | Root cause | Impact |
|---|---|---|---|
| Impatient | Clicked 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