Aller au contenu
Sylvain Joffraud l4w
Retour

Mocker `navigator.wakeLock` readonly : defineProperty

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 :

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éDescriptorEffet de navigator.X = {…}
navigator.geolocationdata property, writablel’assignation marche ✓
navigator.wakeLockaccessor property (getter only), readonlyl’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

#testing #playwright #web-apis #wake-lock #geolocation #mocking

Questions fréquentes

Pourquoi mon mock de navigator.wakeLock est ignoré sans erreur en headless ?
Parce que navigator.wakeLock est une accessor property en getter seul, donc readonly : l’assignation directe navigator.wakeLock = {…} est silencieusement ignorée en mode non-strict et la méthode native continue de tourner. Symptôme typique : le test passe sans erreur mais les compteurs internes du mock restent à 0 (par exemple __lastWakeLock === undefined).
Comment mocker navigator.wakeLock dans Playwright ou Puppeteer ?
Utilisez Object.defineProperty(navigator, 'wakeLock', { configurable: true, value: {…} }), qui force la redéfinition même sur une propriété readonly. Mettez toujours configurable: true pour pouvoir re-redéfinir le mock entre les tests, sinon il reste verrouillé.
Pourquoi navigator.geolocation accepte l’assignation directe mais pas wakeLock ?
Les deux ne sont pas symétriques côté implémentation : navigator.geolocation est une data property writable (héritage des APIs anciennes), donc l’assignation marche, tandis que navigator.wakeLock est un getter readonly comme la plupart des APIs modernes (locks, share, clipboard, credentials…). Le mieux est de mocker la méthode plutôt que la propriété, par exemple navigator.geolocation.watchPosition = …, ce qui est moins invasif.
Comment vérifier si une propriété de navigator est readonly avant de la mocker ?
Inspectez le descriptor en une ligne : console.log(Object.getOwnPropertyDescriptor(navigator, 'wakeLock')). Si vous voyez {get: f, set: undefined, …} c’est un getter readonly et l’assignation directe ne fera rien : passez par Object.defineProperty. Attention aussi à 'wakeLock' in navigator qui reste true même après une tentative d’override ratée, donc vos checks de feature detection peuvent mentir.

Partager cet article :

Article précédent
llms.txt : donner un plan de site aux LLM
Article suivant
Un POC sans tests n’est pas un départ, c’est une direction