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.
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.
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 |
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.