Reproducing Share Bugs on Simulators and Emulators

This guide is part of Testing and Debugging Web Share Flows, which itself is part of Web Share API and Security Contexts.

Simulators and emulators sit awkwardly between a headless browser and a phone. They run the real browser engine, so layout, feature detection and JavaScript behaviour are trustworthy. But sharing ends in the operating system, and a simulated OS has a thin, partly fictional set of share targets. Knowing which half of your bug you are looking at saves hours of chasing a defect that exists only in the image.

Feature Detection Gate

Always start by asking the environment what it is, because a simulator’s answer often differs from the phone it imitates.

// environment-probe.js
export function environmentProbe() {
  return {
    origin: location.origin,
    secureContext: window.isSecureContext === true,
    hasShare: typeof navigator.share === 'function',
    hasCanShare: typeof navigator.canShare === 'function',
    canShareFiles: typeof navigator.canShare === 'function'
      ? navigator.canShare({ files: [new File(['x'], 'probe.txt', { type: 'text/plain' })] })
      : null,
    ua: navigator.userAgent
  };
}

Run this before anything else. hasShare: false on an Android Emulator usually means the image has no share targets installed rather than that Android lacks support — a conclusion that would send you rewriting working code.

What Each Environment Can Prove Four environments in increasing fidelity. A headless browser proves fallback routing only. An Android emulator adds engine-accurate rendering and detection. An iOS simulator adds a real system share sheet with a limited app set. A physical device is the only place target behaviour, file handling and activation timing are real. Headless no share API fallback routing ✓ payload asserts ✓ sheet ✗ targets ✗ activation ✗ Android emu real Chrome engine detection ✓ layout ✓ sheet depends on image targets sparse activation ~ iOS Simulator real WebKit detection ✓ system sheet ✓ Messages / Mail only file targets limited activation ~ Physical device everything real third-party targets ✓ file handling ✓ activation timing ✓ throttling ✓ the only proof

Step 1 — Load the Page on a Secure Origin

Both platforms make it easy to load your dev server on an origin that silently disables sharing.

On Android, the host machine is reachable at 10.0.2.2, but that is a plain IP and therefore not a secure context, so navigator.share disappears. Forward the port instead so the page loads from localhost, which is trusted:

adb reverse tcp:3000 tcp:3000
# then open http://localhost:3000 inside the emulator's Chrome

On the iOS Simulator, the simulator shares the host’s network stack, so http://localhost:3000 works directly and is already a secure context. The trap is different: a self-signed HTTPS certificate is not trusted until you install and enable it in the simulated device’s settings, and an untrusted certificate fails the secure-context test just as HTTP does. Both routes are covered in configuring HTTPS for local development.

Step 2 — Confirm What the Image Supports

Run environmentProbe() and compare it with what the real platform should report.

// expectations.js
export const EXPECTED = {
  'ios-simulator':   { hasShare: true,  hasCanShare: true,  canShareFiles: true },
  'android-emulator':{ hasShare: null,  hasCanShare: null,  canShareFiles: null }, // image-dependent
  'physical-android':{ hasShare: true,  hasCanShare: true,  canShareFiles: true },
  'physical-ios':    { hasShare: true,  hasCanShare: true,  canShareFiles: true }
};

export function isImageArtefact(probe, expected) {
  if (expected.hasShare === null) return true;          // cannot conclude from this image
  return probe.hasShare !== expected.hasShare;          // mismatch = environment, not code
}

Encoding the expectation stops the most expensive mistake in simulator debugging: concluding from hasShare: false that your detection logic is broken, when the image simply has nothing to share to.

Step 3 — Know Which Bugs Escalate to Hardware

Simulators reproduce anything that lives in the page. They cannot reproduce anything that lives past the sheet.

Reproducible in a simulator or emulator: feature-detection results, routing between native and fallback, payload construction, layout of your own share UI, AbortError handling when you dismiss the sheet, and service-worker behaviour behind offline share queues.

Not reproducible: how a specific third-party target handles your files, DataError from a target that rejects a MIME type, iOS timer throttling while the sheet is open, and true user-activation timing under a slow main thread. Those need a real handset and the remote session described in debugging navigator.share with remote DevTools.

Simulator or Hardware: A Triage Path A bug report enters a decision. If the symptom appears before the sheet opens it is reproducible in a simulator. If it appears at or after the sheet, or involves a specific target or file type, it escalates to a physical device. share bug report reproduce where? symptom before the sheet opens? yes Simulator is enough detection · routing · payload · UI no Escalate to hardware targets · DataError · throttling

Making a Simulator Run Reproducible

The value of a simulator is that it can be reset to a known state, which a phone in someone’s pocket cannot. Take advantage of it: record the exact image and the exact origin alongside the result, so a “cannot reproduce” reply has somewhere to start.

// run-manifest.js
export function runManifest(probe) {
  return {
    origin: probe.origin,                 // http://localhost:3000 — not 10.0.2.2
    secureContext: probe.secureContext,
    hasShare: probe.hasShare,
    engine: /CriOS|Chrome/.test(probe.ua) ? 'chromium' : 'webkit',
    platform: /iPhone|iPad/.test(probe.ua) ? 'ios' : 'android'
  };
}

Two habits pay for themselves. Reset the simulated device between sessions — a stale service worker from an earlier build is responsible for a surprising share of “it only fails in the emulator” reports, and wiping the image removes the variable entirely. And cold-boot the emulator when testing installed-PWA behaviour, because a snapshot restored from an older run can hold a manifest the current build no longer serves, which makes share target registration appear broken when it is merely stale.

Note that both platforms let you install the site as a PWA, and the standalone window is a different context from the browser tab: separate storage in some configurations, a different user-agent string, and a different inspection target. If a share bug appears only after installation, reproduce it in the installed window rather than the tab.

Failure Modes and Recovery

Symptom Cause Minimal fix
hasShare: false in the Android Emulator Image has no share targets installed Verify on hardware before changing code
Share works on host, not in the emulator Page loaded from 10.0.2.2 — not a secure context adb reverse the port and load localhost
Secure context false on the iOS Simulator over HTTPS Self-signed certificate not trusted in the image Install and enable the certificate in device settings
Sheet opens with only Messages and Mail Simulator image ships a minimal app set Expected; test target behaviour on hardware
File share succeeds in the simulator, fails on device Real targets enforce stricter type and size rules Validate with canShare and test on hardware
Cannot reproduce a timing bug Simulated main thread is faster than a real phone Throttle the CPU in DevTools, then confirm on device
Reaching the Dev Server on a Trusted Origin The iOS Simulator shares the host network stack, so localhost resolves directly and is already a secure context. The Android Emulator sees the host at 10.0.2.2, which is not trusted, so the port must be forwarded with adb reverse to keep the origin localhost. dev server localhost:3000 iOS Simulator shares the host stack — localhost works Android Emulator host is 10.0.2.2 — not a secure context secure context navigator.share available adb reverse tcp:3000 tcp:3000 — makes the emulator see localhost, which is trusted

Browser and Platform Caveat

The iOS Simulator runs genuine WebKit, so anything about detection, layout or JavaScript semantics transfers directly to a real iPhone; only the set of share targets is fictional. The Android Emulator runs genuine Chrome, but its share behaviour is entirely a property of the system image — a bare AOSP image may expose no share implementation at all, while a Google Play image behaves close to a phone. Neither environment reproduces the aggressive timer throttling iOS applies while a modal sheet is open, which is the root of most “my spinner froze” reports. Treat simulators as a fast pre-check that catches the majority of bugs, and keep one physical device of each platform for the final pass before release.

FAQ

Does the iOS Simulator open a real share sheet?

It opens the system share sheet, but with only the apps the simulator image contains — typically Messages, Mail, Notes and Reminders. That is enough to verify the sheet appears and the promise settles, and not enough to test how a third-party target handles your payload.

How does the Android Emulator reach a dev server on my machine?

The host is reachable at 10.0.2.2 from inside the emulator, but that address is not a secure context. Use adb reverse tcp:3000 tcp:3000 instead so the page loads from http://localhost:3000 inside the emulator, which is trusted and keeps navigator.share available.

Can I trust an emulator result that says sharing is unsupported?

Not without checking the image. Emulator images without Google Play services and without any installed share targets frequently expose no share implementation, so the absence tells you about the image rather than about real Android. Verify support claims on hardware.