How to Test Address Autocomplete on Web (Complete Guide)

Address autocomplete is a UI pattern that lets users start typing a location and receive a drop‑down of suggestions sourced from a geocoding service. When it works well, friction drops, conversion ris

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

Why Address Autocomplete Matters

Address autocomplete is a UI pattern that lets users start typing a location and receive a drop‑down of suggestions sourced from a geocoding service. When it works well, friction drops, conversion rises, and support tickets shrink. When it fails, users abandon checkout, enter wrong shipping data, or trigger downstream validation errors that surface as failed orders, fraud alerts, or compliance issues.

In production you will see three classes of failure that are rarely caught by unit tests:

  1. Data‑source mismatches – the API returns a format the front‑end does not expect (extra fields, missing place_id, localized strings).
  2. Interaction glitches – keyboard navigation, screen‑reader announcements, or touch‑event handling break under specific browser/OS combos.
  3. State‑leakage bugs – the component retains stale suggestions after a user clears the field, switches locales, or opens a modal that overlays the input.

Because autocomplete touches networking, DOM manipulation, ARIA handling, and persisting state, a single change in any of those layers can ripple into a visible defect. A focused test strategy therefore needs to cover the happy path, the ways the service can misbehave, the ways users can interact oddly, and the ways the component must stay accessible and secure.

Core Concepts and Common Implementations

Most web autocomplete widgets share a handful of responsibilities:

Popular libraries (Google Maps Places API, Algolia Places, Mapbox Search, or custom back‑ends) differ mainly in the shape of their JSON response and the authentication method (API key vs. token). Knowing which library your project uses helps you predict failure modes: Google may return status: ZERO_RESULTS; Algolia may wrap results in hits; a custom endpoint might expose pagination cursors.

Test Matrix

Below is a comprehensive matrix that you can copy into a test‑plan spreadsheet. Each row is a distinct scenario; columns indicate the test type, expected outcome, and notes on automation feasibility.

#CategoryScenarioExpected ResultManual?Automated?Notes
1Happy pathUser types “1600 Amphitheatre Parkway, Mountain View, CA” and selects first suggestionInput shows full formatted address; hidden field receives place_id; no JS errors✅ (e2e)Verify network request contains correct query
2Happy path – localeUI language set to es; user types “Calle de Alcalá, Madrid”Suggestions appear in Spanish; address components ordered per locale✅ (e2e + i18n)Check lang attribute on and ARIA labels
3Happy path – keyboardUser types “New York”, presses ↓ twice, then EnterSecond suggestion highlighted; selection commits on Enter✅ (e2e)Ensure focus stays inside listbox
4Happy path – touchOn mobile, user taps the input, types “San Fran”, taps suggestionKeyboard may stay open; selected address appears; no double‑tap zoom⚠️ (device lab)Use real device or BrowserStack; check touch-action
5Error path – API timeoutGeocoding service returns 504 after 8 sUI shows “Unable to fetch suggestions” inline; retry button appears after 2 s✅ (mock server)Verify abortController usage
6Error path – malformed JSONService returns HTML error page instead of JSONUI gracefully falls back to empty list; logs warning to console✅ (unit + mock)Test with msw or nock
7Error path – zero resultsQuery “xxxxxxxxxx” yields empty predictions arrayListbox stays hidden; input shows helper text “No matches found”✅ (e2e)Ensure ARIA aria-expanded="false"
8Edge case – rapid keystrokesUser pastes a 200‑character string; component debounces at 300 msOnly one request sent after debounce expires; UI does not flood✅ (unit)Use sinon fake timers
9Edge case – IME compositionUser types Japanese with IME; compositionend event firesNo request sent during composition; request sent after commit✅ (unit)Listen for compositionstart/compositionend
10Edge case – clipboard pasteUser pastes address from clipboard via Ctrl+VSame behavior as typing; suggestions appear after debounce✅ (e2e)Verify paste event handling
11Edge case – component re‑mountUser opens a modal that contains the autocomplete, closes it, reopensFresh state: no stale suggestions, pending requests cleared✅ (e2e)Check cleanup in useEffect return
12Accessibility – screen readerNVDA/Jaws reads each suggestion as option, announces selected valueAll listbox items have role="option"; live region updates when selection changes✅ (axe + e2e)Verify aria-activedescendant
13Accessibility – color contrastSuggestion highlight uses #ffeb3b on white backgroundContrast ratio ≥ 4.5:1 (AA) for normal text✅ (axe)Use automated contrast checks
14Accessibility – keyboard trapPressing Tab moves focus out of listbox after last itemNo trap; focus goes to next tabbable element✅ (e2e)Test with tab key sequences
15Security – XSS via suggestionService returns in descriptionHTML is escaped; no script execution✅ (unit)Render suggestions as text or use DOMPurify
16Security – API key leakageKey embedded in JS bundle; network tab shows it in query stringKey is proxied through backend or uses token with restricted referrer✅ (network sniff)Verify via CSP or Subresource Integrity
17Privacy – data minimizationComponent sends only the current input value, not full formNo extra fields (e.g., user ID) appear in request payload✅ (unit)Inspect request body with devtools
18Production‑only – network flakySimulate 30 % packet loss with tc or ToxiProxyUI shows retry mechanism; no infinite spinner⚠️ (requires infra)✅ (chaos testing)Use feature flag to enable throttling
19Production‑only – locale mismatchBrowser Accept-Language is fr-FR but service defaults to en-USService honors Accept-Language or falls back gracefully⚠️ (needs i18n test)✅ (mock i18n)Verify hl param for Google Places
20Production‑only – third‑party CDN failureAutocomplete CSS/JS served from CDN returns 404Fallback to bundled copy; UI remains functional⚠️ (requires CDN mock)✅ (service worker)Test with offline simulation

How to Read the Matrix

Manual Testing Approach

A disciplined manual session follows a repeatable script that mirrors the matrix but adds exploratory steps. Below is a step‑by‑step checklist you can paste into a test‑case management tool.

  1. Preparation
  1. Baseline Happy Path
  1. Keyboard‑Only Navigation
  1. Touch Interaction
  1. Error Injection
  1. Accessibility Audit
  1. State and Cleanup
  1. Security & Privacy Spot‑check
  1. Exploratory Bursts

Following this script will catch the majority of matrix items. The exploratory bursts (step 9) are where you often discover production‑only bugs that automated scripts miss because they rely on predictable timing or static mocks.

Automated Testing Approaches

Unit Tests

Isolate the pure logic: debounce, response normalization, suggestion filtering, and ARIA state updates. Use Jest (or Vitest) with jsdom for DOM‑based assertions.


// debounce.test.js
import { debounce } from './autocomplete-utils';

test('debounce delays execution', () => {
  const fn = jest.fn();
  const debounced = debounce(fn, 150);
  debounced();
  debounced();
  jest.advanceTimersByTime(100);
  expect(fn).not.toHaveBeenCalled();
  jest.advanceTimersByTime(60);
  expect(fn).toHaveBeenCalledTimes(1);
});

// normalizeResponse.test.js
import { normalizeGoogleResponse } from './responseParsers';

test('extracts label and place_id', () => {
  const raw = {
    predictions: [
      { description: '1600 Amphitheatre Parkway, Mountain View, CA 94043, USA',
        place_id: 'ChIJ2eUgeAK6j4ARbn5u_wAGqWA',
        structured_formatting: { main_text: '1600 Amphitheatre Parkway' } }
    ]
  };
  const result = normalizeGoogleResponse(raw);
  expect(result).toEqual([
    { label: '1600 Amphitheatre Parkway', value: 'ChIJ2eUgeAK6j4ARbn5u_wAGqWA', raw: raw.predictions[0] }
  ]);
});

Unit tests give you fast feedback on algorithmic changes and guard against regressions in the debounce timing or response shape.

Integration Tests (Component Level)

Render the autocomplete component in isolation with React Testing Library or Vue Test Utils. Mock the fetch call using MSW (Mock Service Worker) to simulate success, error, and empty responses.


// autocomplete.integration.test.js
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import Autocomplete from './Autocomplete';

const server = setupServer(
  rest.get('https://example.com/geocode', (req, res, ctx) => {
    const query = req.url.searchParams.get('input');
    if (!query) return res(ctx.status(400));
    return res(
      ctx.json({
        predictions: [
          { description: `${query}, USA`, place_id: `id_${query}` }
        ]
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows suggestions after debounce', async () => {
  render(<Autocomplete />);
  const input = screen.getByRole('textbox', { name: /address/i });
  await userEvent.type(input, '1600 Amphitheatre');
  // advance timers if debounce uses setTimeout
  await waitFor(() => {
    expect(screen.getByRole('option')).toBeInTheDocument();
  });
  const firstOption = screen.getByRole('option');
  expect(firstOption).toHaveTextContent(/1600 Amphitheatre/);
});

Integration tests verify that the component wires up networking, state, and rendering correctly while keeping the test suite fast (no real browser).

End‑to‑End Tests

Use Playwright (recommended for its auto‑waiting and tracing) or Cypress to drive a real browser against a staging deployment. The following Playwright script covers the happy path, keyboard navigation, and error simulation.


// tests/address-autocomplete.spec.js
const { test, expect } = require('@playwright/test');

test.describe('Address autocomplete', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://staging.example.com/checkout');
  });

  test('user can select an address via keyboard', async ({ page }) => {
    const input = page.locator('input[aria-label="Street address"]');
    await input.fill('1600 Amphitheatre Pkwy');
    await page.waitForTimeout(300); // matches debounce
    const listbox = page.locator('role=listbox');
    await expect(listbox).toBeVisible();
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('Enter');
    await expect(input).toHaveValue(/1600 Amphitheatre Parkway/);
  });

  test('shows error when geocoding service fails', async ({ page }) => {
    // Intercept and fail the request
    await page.route('**/geocode', route => route.fulfill({ status: 504 }));
    const input = page.locator('input[aria-label="Street address"]');
    await input.fill('Invalid Street');
    await page.waitForTimeout(300);
    const errorMsg = page.locator('text=Unable to fetch suggestions');
    await expect(errorMsg).toBeVisible();
    const retryBtn = page.locator('button:has-text("Retry")');
    await expect(retryBtn).toBeEnabled();
    await retryBtn.click();
    await page.waitForResponse('**/geocode');
    await expect(page.locator('role=listbox')).toBeVisible();
  });

  test('maintains ARIA attributes', async ({ page }) => {
    const input = page.locator('input[aria-label="Street address"]');
    await input.fill('New York');
    await page.waitForTimeout(300);
    const listbox = page.locator('role=listbox');
    await expect(listbox).toHaveAttribute('aria-expanded', 'true');
    await page.keyboard.press('Escape');
    await expect(listbox).toHaveAttribute('aria-expanded', 'false');
  });
});

Why Playwright?

Performance & Load Tests

While not functional, measuring request volume under load helps catch debounce misconfigurations that cause a thundering herd. Use k6 or Artillery to simulate many virtual users typing rapidly.


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

export let options = {
  stages: [
    { duration: '2m', target: 50 }, // ramp-up
    { duration: '5m', target: 50 }, // steady
    { duration: '2m', target: 0 },  // ramp-down
  ],
};

export default function () {
  const payload = JSON.stringify({ input: '1600 Amphitheatre' });
  const params = { headers: { 'Content-Type': 'application/json' } };
  const res = http.post('https://api.example.com/geocode', payload, params);
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(0.2); // simulate think time between keystrokes
}

Run the script against a staging endpoint; watch the server’s autoscaling metrics and the frontend’s network panel for duplicated requests.

Tooling and Code Examples

Below is a quick‑reference table that maps testing goals to concrete tools, their strengths, and a one‑liner command or snippet.

GoalToolWhy FitExample Command / Snippet
Unit logic (debounce, normalization)Jest + jsdomFast, zero‑browser overheadnpm test -- --testPathPattern=autocomplete
Component rendering + mock APIReact Testing Library + MSWDeclarative queries, realistic network mocksnpx jest --watch
End‑to‑end (real browser)PlaywrightAuto‑wait, tracing, multi‑browsernpx playwright test --project=chromium
Accessibility auditaxe-core (CLI or integrated)WCAG 2.1 rules, CI‑friendlynpx axe-playwright --tags wcag2aa
Visual regressionStorybook + ChromaticCatch CSS regressions in suggestion stylingnpx chromatic --project-token=
Network throttling & offline simulationDevTools → Network → Throttle / Service WorkerReal‑world flaky conditionsN/A (manual)
Chaos injection (latency, errors)ToxiProxy or tc + netemSimulate packet loss, latency spikestoxiproxy-cli create autocomplete -l localhost:8080 -u upstream:8080
Performance / loadk6Scriptable, CLI‑friendly, integrates with CIk6 run load-test.js
Accessibility screen‑reader testingNVDA (Windows) / VoiceOver (macOS)Actual assistive tech feedbackN/A (manual)
Security scanning (XSS, CSP)OWASP ZAP passive scanDetects reflected XSS in suggestionszap-baseline.py -t https://staging.example.com

Sample Playwright Helper for Mocking the Geocoding Endpoint


// playwright-helper.js
exports.setupGeocodeMock = async (page, mode = 'ok') => {
  await page.route('**/geocode', async route => {
    if (mode === 'error') {
      return route.fulfill({ status: 504, body: 'Gateway Timeout' });
    }
    if (mode === 'empty') {
      return route.fulfill({ status: 200, body: JSON.stringify({ predictions: [] }) });
    }
    const url = new URL(route.request().url());
    const query = url.searchParams.get('input') || '';
    const mock = {
      predictions: [{ description: `${query}, USA`, place_id: `mock_${query}` }]
    };
    return route.fulfill({ status: 200, body: JSON.stringify(mock) });
  });
};

Use it in a test:


const { setupGeocodeMock } = require('./playwright-helper');

test('handles empty response gracefully', async ({ page }) => {
  await setupGeocodeMock(page, 'empty');
  await page.goto('https://staging.example.com/');
  const input = page.locator('input[aria-label="Street address"]');
  await input.fill('Nowhere');
  await page.waitForTimeout(300);
  await expect(page.locator('text=No matches found')).toBeVisible();
});

Example of an Axe Integration in CI

Add this to your playwright.config.ts:


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

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], },
    },
  ],
  // Global setup that runs axe after each test
  async teardown() {
    const axe = new AxePlaywright();
    const results = await axe.run(page);
    if (results.violations.length > 0) {
      console.error('Accessibility violations:', results.violations);
      process.exit(1);
    }
  },
});

This will fail the build if any WCAG rule is violated during a test run.

Autonomous, Persona‑Driven Exploration

Even the most thorough matrix can miss edge cases that only appear when real users with varied habits interact with the UI. Autonomous testing platforms like SUSA address this by:

  1. Loading the APK or URL (in our case, a web URL) and constructing a state graph of reachable screens.
  2. Applying persona profiles – each persona has a distinct distribution of input speed, error‑prone behavior, assistive‑technology usage, and intention (e.g., “impatient” users may mash keys rapidly, “elderly” users may rely heavily on screen‑reader navigation, “adversarial” users may attempt to inject scripts).
  3. Executing exploratory actions – taps, scrolls, keyboard sequences, voice commands, and paste events are generated according to the persona’s model.
  4. Detecting observable failures – JavaScript exceptions, ANR‑like long tasks, ARIA violations, network errors, and unexpected DOM mutations are flagged.
  5. Feeding discoveries back – the platform remembers which states led to dead ends or crashes and prioritizes them in subsequent runs, continuously expanding coverage.

How Persona‑Driven Finds Missed Autocomplete Bugs

PersonaTypical BehaviorBug It May Surface
ImpatientTypes fast, presses Enter before suggestions appearRace condition where the input value is cleared before the popup renders, leaving the field blank.
NoviceRelies on placeholder text, rarely uses arrow keysMissing aria-label on the input causing screen readers to announce “edit text” instead of “Street address”.
AccessibilityUses VoiceOver, navigates with touch gesturesTouch‑start on the suggestion list does not move focus, leaving the user stuck.
AdversarialPastes strings with HTML entities, attempts XSSImproper sanitization leading to script execution in the suggestion container.
ElderlyUses zoom, prefers larger tap targetsSuggestion list items too small (≥ 44 dp) causing mis‑taps.
Power userUses keyboard shortcuts, expects Ctrl+Enter to submit formLack of custom shortcut handling causing unexpected form submission.
CuriousTypes partial address, then deletes characters rapidlyDebounce not resetting on backspace, resulting in stale suggestions after deletion.

SUSA’s built‑in heuristics for each persona translate into concrete action sequences. For example, the “impatient” persona might generate a stream of keydown events with 30 ms intervals, followed by an immediate Enter. If the component’s debounce is set to 250 ms, the test will expose a scenario where the request is cancelled mid‑flight and the UI shows a stale list.

Integrating SUSA Into Your Pipeline

You can run SUSA as a containerized step in CI:


docker run --rm \
  -e SUSA_URL=https://staging.example.com \
  -e SUSA_PERSONAS=impatient,accessible,adversarial \
  -e SUSA_OUTPUT=junit \
  susatest/agent:latest

The agent will upload a JUnit XML report that your CI can parse, marking the build as unstable if any persona discovers a crash or WCAG violation. Because SUSA explores without pre‑written scripts, it often finds the “unknown unknowns” that slip past matrix‑based test cases.

Production‑Only Edge Cases and Monitoring

Some defects only manifest under real‑world traffic patterns or environmental factors that are hard to reproduce locally.

1. CDN or Third‑Party Script Latency

If the autocomplete CSS is served from a CDN that occasionally experiences high latency, the page may render the input before the dropdown stylesheet loads, causing mis‑aligned panels. Detect this with Real User Monitoring (RUM) by measuring the time between DOMContentLoaded and the first getBoundingClientRect of the suggestion list.

2. Locale‑Specific Shape Data

Certain countries return address components in a different order (e.g., Japan: prefecture → city → street). If your UI assumes a fixed order (street, city, state, zip), the formatted address may appear garbled. Use server‑side logs to capture the raw structured_formatting object from Google Places and compare it against the displayed string.

3. Ad‑Blocker Interference

Some ad‑blocking lists mistakenly flag geocoding endpoints as tracking. When blocked, the request fails silently, and the UI may show a spinner forever. Monitor the percentage of requests with status (blocked) via your CSP reporting endpoint or a custom window.onerror handler that logs net::ERR_BLOCKED_BY_CLIENT.

4. Battery‑Saving Mode (Mobile)

On Android, battery saver can increase the timeout of background timers, effectively breaking a setTimeout‑based debounce. Test by enabling battery saver in device settings and measuring the actual delay between keystrokes and request emission.

5. Network Switching (Wi‑Fi → Cellular)

When a user moves between networks, the browser may reuse a stale socket, leading to a NET::ERR_CERT_DATE_INVALID if the certificate chain changed mid‑session. Capture these events with the navigator.connection API and log any change in type or effectiveType.

#### Monitoring Snippet (Browser‑Side)


// collect.js
(function () {
  const endpoint = '/_/autocomplete/metrics';
  function send(payload) {
    navigator.sendBeacon(endpoint, JSON.stringify(payload));
  }

  // debounce timing
  let lastKey = 0;
  document.querySelector('input[autocomplete]').addEventListener('input', e => {
    const now = performance.now();
    if (lastKey) {
      send({ type: 'debounce-delay', value: now - lastKey });
    }
    lastKey = now;
  });

  // request outcome
  const originalFetch = window.fetch;
  window.fetch = async (...args) => {
    const res = await originalFetch.apply(this, args);
    send({ type: 'fetch-result', url: args[0], status: res.status, ok: res.ok });
    return res;
  };

  // visibility changes (possible tabbing away while suggestions open)
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      send({ type: 'page-hidden', timestamp: performance.now() });
    }
  });
})();

Deploy this snippet via your tag manager or embed it in the bundle. Aggregate the events in your analytics backend and set alerts for outliers (e.g., debounce delay > 2× configured value, fetch error rate > 1 %).

Checklist for Release

Before merging a change that touches the address autocomplete, run through this concise list. Treat each item as a gate; if any fails, block the merge.

✅ ItemHow to Verify
Unit tests pass (npm test)No regressions in debounce, normalization, filtering.
Integration tests pass (npm run test:integration)Component renders, handles mock success/error/empty.
E2E smoke passes (npx playwright test --project=chromium)Happy path, keyboard navigation, error fallback work in Chrome.
Accessibility audit clean (npx axe-playwright)Zero WCAG 2.1 AA violations on the autocomplete region.
Persona‑driven smoke (SUSA) passesNo crashes, ANRs, or security flags from impatient, accessible, adversarial personas.
Network resilience (manual or ToxiProxy)UI shows retry spinner on 504, recovers after service restored.
No API key leakage (Network tab inspection)Key absent from query strings or headers; request proxied if required.
Privacy compliance (request payload inspection)Only the current input value is sent; no extra PII.
Visual regression baseline (Chromatic)No unexpected styling changes in suggestion list across breakpoints.
Performance budget (Lighthouse)Time to interactive < 2 s; total blocking time < 50 ms for the autocomplete module.
Documentation updated (JSX props, Storybook)New props or behavior changes are documented; examples reflect latest API.

If any item is red, create a ticket, fix the root cause, and re‑run the checklist.

Closing Takeaways

Address autocomplete sits at the intersection of networking, DOM state, and assistive‑technology contracts. A solid testing strategy therefore needs layers:

  1. Fast unit tests that lock down the pure functions (debounce, parsing, filtering).
  2. Component‑level integration tests with realistic API mocks to confirm that networking, state, and rendering work together.
  3. End‑to‑end scenarios exercised in real browsers (Playwright or Cypress) to verify keyboard, touch, and screen‑reader interactions.
  4. Accessibility automation (axe) run on every commit to catch regressions in ARIA roles, contrast, and focus management.
  5. Exploratory, persona‑driven runs (via a platform like SUSA) that surface the “unknown unknowns” – rapid‑fire keystrokes

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