Quand on teste en headless (Playwright, Puppeteer) une UI qui dépend de navigator.geolocation (watchPosition) ou navigator.wakeLock, on doit substituer ces APIs par des mocks. Tentative naïve, qui marche dans 50% des cas et silently no-op dans l’autre :
// Approche 1 : assignation directe
navigator.geolocation = {
watchPosition: (cb) => { /* mock */ return 1; },
clearWatch: () => {}
};
navigator.wakeLock = {
request: async () => ({ released: false, release: () => {}, ... })
};
Symptôme
Avec ce mock en place, on lance le test :
- la géoloc semble marcher : le callback est appelé, la position s’affiche
- le wake lock semble être pris : aucune erreur, le code continue
Sauf que les compteurs sont à 0 : state.watchers.size === 0, __lastWakeLock === undefined. Le mock n’a jamais été exécuté.
En réalité, les vraies méthodes natives ont tourné (ou ont failé silencieusement). L’assignation a été ignorée.
Cause
Les propriétés navigator.geolocation et navigator.wakeLock ne sont pas symétriques côté implémentation :
| Propriété | Descriptor | Effet de navigator.X = {…} |
|---|---|---|
navigator.geolocation | data property, writable | l’assignation marche ✓ |
navigator.wakeLock | accessor property (getter only), readonly | l’assignation silencieusement ignorée en non-strict, throw en strict |
Pire : 'wakeLock' in navigator reste true après tentative d’override (la propriété native existe toujours), donc tes checks de feature detection mentent.
C’est implementation-defined par le browser. En général : APIs anciennes (geolocation, plugins, mimeTypes…) sont des data properties writable historiquement ; APIs modernes (wakeLock, locks, presentation, share, credentials…) sont quasi-systématiquement des getters readonly.
La solution qui marche partout
Object.defineProperty(navigator, 'X', { value, configurable: true }) force la redéfinition même sur les propriétés readonly. Le configurable: true permet de re-redéfinir (utile pour rejouer le mock entre tests).
// Pour geolocation : on peut soit assigner les méthodes inline,
// soit defineProperty si on veut un objet complet. Les 2 marchent.
const geo = navigator.geolocation;
geo.watchPosition = (cb) => { /* mock */ };
geo.clearWatch = () => {};
// Pour wakeLock : defineProperty obligatoire
Object.defineProperty(navigator, 'wakeLock', {
configurable: true,
value: {
request: async () => {
const sentinel = {
released: false, type: 'screen',
__listeners: [],
addEventListener: (ev, fn) => { if (ev === 'release') sentinel.__listeners.push(fn); },
removeEventListener: () => {},
release: async () => { sentinel.released = true; sentinel.__listeners.forEach(fn => fn()); }
};
window.__lastWakeLock = sentinel;
return sentinel;
}
}
});
Le détail crucial sur le sentinel wakeLock : ne pas oublier que addEventListener('release', fn) doit réellement enregistrer le listener pour qu’il soit appelé quand on simule un release par le browser. Sinon ta logique de re-acquisition au visibilitychange (cf #83) ne pourra jamais être testée — elle dépend du flag mis à jour par ce listener.
Lessons learned
- Les APIs
navigator.Xmodernes sont quasi-toutes en getters readonly —wakeLock,locks,share,clipboard,mediaDevices,credentials,presentation, etc. Pour mocker :Object.definePropertyobligatoire - Les APIs
navigator.Xanciennes sont souvent writable —geolocation,plugins,mimeTypes. Mais ne pas en faire une règle, vérifier avecObject.getOwnPropertyDescriptor(navigator, 'X') - Override des méthodes vs override de la propriété : si tu peux mocker en touchant juste les méthodes (
navigator.geolocation.watchPosition = ...), c’est moins invasif que de remplacer toute la propriété. À utiliser en priorité - Toujours mettre
configurable: truedans le defineProperty, sinon le mock est verrouillé : tu ne peux plus le re-redéfinir entre tests - Tester avec un mock qui émet vraiment les events (release, change, error) — sinon ton code de gestion d’event n’est jamais exercé et tu shippes avec un bug latent (typiquement : ré-acquisition après release auto par le browser)
- Symptôme à connaître : le test passe sans erreur mais les compteurs internes du mock restent à 0 → 90% du temps c’est une propriété readonly que l’assignation n’a pas touchée
- Diagnostic en 1 ligne :
console.log(Object.getOwnPropertyDescriptor(navigator, 'wakeLock'))— si tu vois{get: f, set: undefined, ...}c’est readonly, oublie l’assignation directe
#testing #playwright #web-apis #wake-lock #geolocation #mocking