How to Test QR Code Scanning on Web (Complete Guide)

Web applications increasingly rely on QR codes for actions such as logging in, sharing links, adding payment information, or launching augmented‑reality experiences. Unlike native mobile apps, a web‑b

March 07, 2026 · 18 min read · How-To Guides

Why QR Code Scanning Matters on the Web

Web applications increasingly rely on QR codes for actions such as logging in, sharing links, adding payment information, or launching augmented‑reality experiences. Unlike native mobile apps, a web‑based scanner runs inside a browser, which means it inherits the quirks of the rendering engine, the permissions model, and the variability of device cameras. When a QR‑code flow breaks, users cannot complete a core task, leading to abandoned carts, failed authentications, or missed marketing conversions. Because the scanner is often a thin wrapper around the HTML5 getUserMedia API or a third‑party JavaScript library, defects can hide in permission handling, video stream constraints, canvas drawing, or the decoding logic itself. Testing this functionality therefore requires attention to both the visual layer (camera UI, overlay, feedback) and the data layer (payload validation, error propagation, security checks).

Common Failure Modes in Production

Several patterns repeatedly appear in production logs for web QR scanners:

Failure CategoryTypical SymptomRoot Cause
Permission denialScanner never starts, shows “Camera not available”Missing allow attribute in iframe, user denied once and browser remembers, or HTTPS requirement not met
Stream constraints mismatchBlack or frozen video, low FPSRequested width/height unsupported by device, or aspect‑ratio forced causing cropping
Decoding library bugValid QR returns null, or invalid data acceptedOut‑of‑date library version, incorrect error‑correction level handling, or failure to handle UTF‑8 payloads
Overlay occlusionScan area hidden behind fixed header/footerCSS z-index issues, or dynamic layout shifts after keyboard appears
Performance stallUI freezes for >2 s after detecting codeHeavy image processing on main thread, or large canvas re‑draws on each frame
Security bypassMalicious QR injects script via URL schemeLack of sanitization before assigning window.location or using innerHTML
Accessibility gapScreen‑reader users cannot perceive scan statusNo ARIA live region, missing label for start/stop button, or reliance on color‑only cues

Understanding these categories helps shape a test matrix that covers not only the happy path but also the conditions that surface only under specific device/browser combos or user behaviors.

Test Matrix: Happy Path, Error Paths, Edge Cases, Accessibility, Security

Below is a comprehensive matrix you can copy into a test‑management tool. Each row lists a test ID, description, pre‑conditions, steps, expected result, and notes on automation feasibility.

IDCategoryDescriptionPreconditionsStepsExpected ResultAutomation Notes
H1Happy PathScan a correctly formatted QR code containing a plain‑text URLBrowser supports getUserMedia, site served over HTTPS, camera permission granted1. Open scanner page 2. Point device camera at QR 3. Wait for detectionScanner decodes URL, navigates to target page or shows toast with URLFully automatable with mocked video stream
H2Happy PathScan QR encoding a vCard (MECARD)Same as H1Same as H1, QR encodes MECARD:N:Doe,John;TEL:+15551234567;Parser extracts name and phone, fills contact form or shows previewRequires library that supports vCard; verify output fields
H3Happy PathScan QR with UTF‑8 Japanese charactersSame as H1, page charset UTF‑8Same as H1, QR encodes https://example.com/日本語Decoded URL preserves characters, navigation succeedsCheck that library does not corrupt multi‑byte sequences
E1Error PathDenied camera permissionBrowser previously blocked camera for origin1. Load scanner page 2. Observe permission prompt 3. Click “Block”Scanner shows permission‑denied message, no video streamCan simulate via navigator.mediaDevices.getUserMedia rejection in test
E2Error PathUnsupported video constraintsDevice camera cannot satisfy requested 1920x10801. Set constraints {width:1920,height:1080} 2. Attempt to start streamStream fails, fallback to lower resolution or error shownTest by forcing unsupported constraints and verifying fallback logic
E3Error PathCorrupted QR (low contrast)Print QR on glossy paper, low lighting1. Place QR under dim light 2. ScanScanner either retries and fails gracefully or shows “Unable to read”Use image processing to lower contrast in mocked stream
E4Error PathQR larger than viewport (requires scrolling)QR code printed at A3 size, displayed on mobile screen1. Load page with scanner in a fixed‑size container 2. Show QR that overflows container 3. Attempt scanScanner allows user to pan/zoom or shows instruction to fit QR in viewVerify UI provides zoom/pan controls or responsive container
E5Error PathMultiple QRs in frame, ambiguousTwo valid QRs side by side1. Show both QRs 2. Point cameraScanner either picks one based on proximity to center or asks user to selectTest selection logic and UI feedback
A1AccessibilityScreen reader announces scan stateScreen reader enabled (NVDA, VoiceOver, TalkBack)1. Focus start‑scan button 2. Activate 3. Observe live regionLive region updates with “Scanning…”, “QR detected”, or errorUse axe‑core or manual inspection; ensure ARIA live region present
A2AccessibilityColor‑blind friendly overlayUser with deuteranopia1. Scan QR with green success overlayOverlay uses shape or text in addition to color to indicate successVerify contrast ratio ≥ 4.5:1 and non‑color cues
A3AccessibilityKeyboard operable controlsNo mouse/touch1. Tab to start/stop button 2. Press Enter/SpaceButton activates, scanner togglesEnsure all interactive elements are focusable and have visible focus outline
S1SecuritySanitization of URL payloadQR encodes javascript:alert(1)1. Scan QR 2. Observe behaviorNo script execution; URL either blocked or shown as plain textConfirm library strips or validates schemes before navigation
S2SecurityPrevention of clickjacking via iframeScanner embeddable in third‑party site1. Host scanner in iframe with allow="camera" 2. Attempt to trick user into clicking invisible buttonClickjacking protection (e.g., X-Frame-Options: DENY or CSP frame‑ancestors) prevents unauthorized interactionTest with framing attempts and verify header/CSP
S3PrivacyNo retention of video frames after scanPrivacy‑conscious user1. Scan QR 2. Stop scanner 3. Inspect page memoryNo video blob or image data remains in DOM or JS heapUse heap snapshot tools to confirm cleanup
X1Edge CaseRapid successive scans (burst mode)User scans multiple QRs quickly1. Show sequence of 5 QRs 2. Scan each within 500 msEach QR decoded correctly, no frame loss or crashStress test with timed sequence; check for queue overflow
X2Edge CaseDevice orientation change mid‑scanUser rotates phone from portrait to landscape1. Start scan in portrait 2. Rotate device 3. Continue scanningScanner adapts, video stream continues, overlay re‑orientsListen to orientationchange event and verify constraints updated
X3Edge CaseLow‑bandwidth video (emulated throttling)Network throttling to 50 kbps (affects some getUserMedia implementations on Android)1. Apply network throttle 2. Attempt scanScanner still works; may show reduced FPS but decodesUse Chrome DevTools throttling or WebPageTest emulation
X4Edge CaseBrowser in incognito/private modeIncognito disables some persistent permissions1. Open incognito window 2. Load scanner 3. Grant permissionPermission prompt appears each session; scanner works after grantVerify that permission is not persisted across incognito sessions
X5Edge CaseQR with FNC1 mode (GS1) for product codesGS1 QR encodes (01)01234567890128(10)ABC1231. Scan GS1 QR 2. Observe outputLibrary returns raw byte string or parsed GS1 structureConfirm support for GS1 extensions or note limitation

Happy Path Tests

These verify that the core scanning pipeline works when everything is ideal: proper lighting, a well‑formed QR, granted permissions, and a compatible browser. Automating happy‑path tests is straightforward because you can replace the real video stream with a pre‑recorded canvas that draws the QR image on each frame. The key is to ensure the mock respects the same constraints (width, height, frame rate) that the production code requests.

Error Path Tests

Error paths expose how the scanner handles missing or malformed input. Permission denial, unsupported constraints, low‑contrast symbols, and overlapping QRs are typical failure points. For each, you assert that the UI shows an informative message, that the scanner does not crash, and that recovery (e.g., retrying after fixing the issue) works.

Accessibility Tests

Accessibility goes beyond WCAG contrast checks. You need to confirm that screen‑reader users receive timely updates, that color‑blind users can discern success/failure via shape or text, and that all controls are reachable via keyboard. Automated tools like axe‑core can catch many static issues, but live‑region announcements and focus order often require manual validation or custom assertions in test scripts.

Security & Privacy Tests

A QR code can carry JavaScript URLs, phishing links, or attempts to trigger unwanted downloads. The scanner must sanitize or whitelist allowed schemes (commonly http, https, mailto, tel). Additionally, embedding the scanner in an iframe should respect click‑jacking defenses, and the implementation must not retain video frames after the user stops scanning. These tests often require inspecting network requests, checking CSP headers, and performing heap snapshots.

Edge‑Case Tests

Edge cases combine multiple stressors: rapid scans, orientation shifts, bandwidth limits, private‑browsing modes, and specialized QR formats (GS1, FNC1). They reveal race conditions, resource leaks, and assumptions about device capabilities that only manifest under load or unusual configurations.

Manual Testing Approach: Step‑by‑Step

Even with automation, a manual exploratory pass catches nuances that scripts miss. Follow this procedure on a representative set of devices (desktop Chrome, mobile Safari, Android Chrome) and browsers.

  1. Prepare the environment
  1. Happy‑path baseline
  1. Permission flows
  1. Constraint testing
  1. Overlay and UI checks
  1. Accessibility walkthrough
  1. Security checks
  1. Privacy verification
  1. Stress and burst testing
  1. Document observations

This manual routine should take roughly 30‑45 minutes per device/browser combination and yields a rich set of notes that inform both bug triage and test‑automation priorities.

Automated Testing Approaches for Web

Automation replaces the flaky human element with repeatable checks, especially valuable for regression suites and CI pipelines. The core idea is to substitute the real camera feed with a controllable video source that emits frames containing a QR image. Below are patterns for the three most common test frameworks.

1. Playwright (JavaScript/TypeScript)

Playwright can intercept navigator.mediaDevices.getUserMedia and replace it with a mock that yields a MediaStream generated from a element.


// qr-scanner.test.ts
import { test, expect } from '@playwright/test';

test.describe('QR scanner happy path', () => {
  test('decodes a URL QR and navigates', async ({ page }) => {
    // 1. Prepare a data URL containing a QR PNG (generated offline or via lib)
    const qrDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...';

    // 2. Mock getUserMedia to return a stream that paints the QR on each frame
    await page.addInitScript(({ qrDataUrl }) => {
      const fakeStream = new MediaStream();
      const canvas = document.createElement('canvas');
      canvas.width = 640;
      canvas.height = 480;
      const ctx = canvas.getContext('2d');
      const img = new Image();
      img.src = qrDataUrl;
      img.onload = () => {
        // Draw the QR once; we will reuse the same frame
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      };
      // Create a video track that yields the canvas as an image bitmap
      const track = canvas.captureStream().getVideoTracks()[0];
      fakeStream.addTrack(track);
      // Override the global method
      const original = navigator.mediaDevices.getUserMedia;
      navigator.mediaDevices.getUserMedia = () => Promise.resolve(fakeStream);
    }, { qrDataUrl });

    // 3. Navigate to the scanner page
    await page.goto('https://example.com/qr-scanner');

    // 4. Wait for navigation or toast indicating success
    await expect(page).to haveURL(/.*\/destination\/page/);
    // Alternatively, check for a toast element
    // await expect(page.locator('.toast')).toContainText('https://example.com');
  });
});

Why this works:

2. Cypress

Cypress does not natively support mocking getUserMedia, but you can use a combination of cy.route‑style stubbing and a custom script that overwrites the method before the page loads.


// cypress/integration/qr_scanner.spec.js
describe('QR scanner', () => {
  const qrBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...';

  beforeEach(() => {
    cy.visit('/qr-scanner');
    // Inject the mock before any scanner code runs
    cy.window().then((win) => {
      const originalGetUM = win.navigator.mediaDevices.getUserMedia;
      win.navigator.mediaDevices.getUserMedia = () => {
        const canvas = document.createElement('canvas');
        canvas.width = 640;
        canvas.height = 480;
        const ctx = canvas.getContext('2d');
        const img = new Image();
        img.src = qrBase64;
        img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const stream = canvas.captureStream();
        return Promise.resolve(stream);
      };
    });
  });

  it('navigates after scanning a URL QR', () => {
    // Assume the scanner redirects to /scanned?url=...
    cy.url().should('include', '/scanned');
    cy.location('search').should('contain', 'url=https%3A%2F%2Fexample.com');
  });

  it('shows error when permission denied', () => {
    cy.window().then((win) => {
      win.navigator.mediaDevices.getUserMedia = () => Promise.reject(new Error('NotAllowedError'));
    });
    cy.reload();
    cy.contains('Camera access denied').should('be.visible');
  });
});

Tips:

3. Selenium WebDriver (Java)

Selenium can execute asynchronous JavaScript to replace the getUserMedia call, then interact with the page as usual.


public class QRScannerTest {
    private WebDriver driver;
    private static final String QR_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...";

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver();
        driver.get("https://example.com/qr-scanner");
        // Inject mock
        JavascriptExecutor js = (JavascriptExecutor) driver;
        js.executeScript(
            "const original = navigator.mediaDevices.getUserMedia;" +
            "navigator.mediaDevices.getUserMedia = function() {" +
            "  const canvas = document.createElement('canvas');" +
            "  canvas.width = 640; canvas.height = 480;" +
            "  const ctx = canvas.getContext('2d');" +
            "  const img = new Image();" +
            "  img.src = arguments[0];" +
            "  img.onload = () => ctx.drawImage(img,0,0,canvas.width,canvas.height);" +
            "  return Promise.resolve(canvas.captureStream());" +
            "};", QR_BASE64);
    }

    @Test
    void shouldDecodeUrlAndNavigate() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        // Wait for navigation or a specific element indicating success
        wait.until(ExpectedConditions.urlContains("/destination"));
        Assertions.assertTrue(driver.getCurrentUrl().contains("https://example.com"));
    }

    @AfterEach
    void tearDown() {
        if (driver != null) driver.quit();
    }
}

Notes on robustness:

Generating QR Images on the Fly

Rather than storing a static base64 string for each test payload, you can generate QR codes programmatically within the test script using a library like qrcode (Node) or ZXing (Java). Example with Playwright:


const QRCode = require('qrcode');
// inside test
const payload = 'https://example.com/product/42';
const qrDataUrl = await QRCode.toDataURL(payload);
await page.addInitScript(({ qrDataUrl }) => { /* mock as before */ }, { qrDataUrl });

This approach lets you parametrize a single test case over a matrix of payloads (URLs, vCards, GS1 strings) without maintaining a library of image files.

Tooling and Libraries

Beyond the core test frameworks, several helper tools simplify QR‑specific validation:

ToolLanguagePurposeExample Usage
jsqrJavaScriptPure‑JS QR decoder (no camera) – useful for unit‑testing the decoding logic directlyconst decoded = jsqr(new Uint8ClampedArray(data), width, height);
zxing-browserJavaScriptPort of ZXing to the browser, provides BrowserQRCodeReader that works with a video elementconst reader = new BrowserQRCodeReader(); reader.decodeFromVideoDevice(undefined, videoEl).then(result=>…);
qrcode-readerNodeDecodes QR from a Buffer or image file – handy for CI sanity checks on generated test imagesqr.decode(buffer, (err, value) => { … })
axe-coreJavaScriptAutomated accessibility audits; can be integrated into Playwright/Cypressawait page.evaluate(() => axe.run());
pa11yNodeCLI for running axe on URLs; good for nightly scans of the scanner pagepa11y https://example.com/qr-scanner --standard WCAG2AA
devtools-protocolMulti‑languageDirectly control Chrome’s emulation features (device metrics, geolocation, network throttling)Use Playwright’s context.setGeolocation or page.emulateMedia

Incorporating these libraries into your test suite reduces the amount of brittle DOM scraping and lets you focus on behavior rather than implementation details.

Autonomous, Persona‑Driven Exploration with SUSA

While scripted tests cover known scenarios, real users interact with the scanner in ways that are hard to anticipate: an impatient power user may repeatedly tap the start button, an elderly user may hold the device at an awkward angle, a curious user may try to scan a QR displayed on a glossy billboard under direct sunlight.

SUSA’s autonomous agent can be pointed at the QR‑scanner URL (or fed an APK that hosts a web view) and will explore the interface using a variety of built‑in personas:

During each run, SUSA builds a internal map of screens and dead ends. If it discovers a state where the scanner shows a blank video feed, or where an error toast disappears too quickly to be read, it flags that as a potential bug. Because the agent does not rely on predetermined selectors, it can surface issues such as:

The agent also generates regression scripts (Appium for Android wrappers, Playwright for pure web) that capture the exact interaction sequence leading to the failure. Those scripts can be added to your CI suite, ensuring that the same class of bug does not reappear.

How to start:

  1. pip install susatest-agent
  2. Run susatest scan --url https://yourapp.com/qr-scanner --personas curious,impatient,elderly,accessibility,adversarial
  3. Review the generated report; each finding includes a short video clip, console logs, and an auto‑produced test script.

Integrating this step into a nightly pipeline complements your unit and integration tests by exercising the scanner under realistic, varied user behavior that would be difficult to script exhaustively.

Checklist for QR Code Scanning Testing

Use this list before marking a story as done. Tick each item after you have verified it on at least one representative device/browser pair.

If any item remains unchecked, prioritize a fix before release.

Closing Takeaways

Testing QR code scanning on the web is more than verifying that a camera can read a pattern. It is a convergence of media permissions, real‑time video processing, UI overlay management, data validation, accessibility, and security considerations. A solid strategy combines:

  1. Clear, exhaustive test matrices that enumerate happy paths, error conditions, accessibility checks, security probes, and edge‑case stressors.
  2. Manual exploratory sessions on real devices to catch context‑specific glitches such as lighting variations, orientation quirks, and permission‑flow nuances.
  3. Automated mocks that replace getUserMedia with controllable canvas‑based streams, enabling fast, deterministic CI pipelines for each matrix entry.
  4. Tool‑specific helpers (jsqr, zxing-browser, axe-core) to isolate decoding logic, assert accessibility, and verify security headers.
  5. Autonomous, persona‑driven exploration (via tools like SUSA) to surface unexpected interaction patterns that scripted tests miss, and to generate regression scripts that lock in fixes.

By treating the scanner as a full‑featured component rather than a simple “camera‑plus‑decoder” widget, you reduce the risk of post‑release failures that frustrate users and damage trust. Apply the checklist, iterate on the matrix, and let both manual insight and automated rigor guide your implementation toward a robust, inclusive, and secure QR‑scanning experience on the web.

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