#106: MutationObserver — Node-Hook-basiertes Mutation Tracking (Non-Visual-Optimized) #106

Closed
opened 2026-06-19 15:48:59 +00:00 by Artur · 1 comment
Owner

Problembeschreibung

MutationObserver ist aktuell ein noop-Shim (observe() {}, disconnect() {}, takeRecords() []). React, Vue 3, Angular und viele andere Frameworks verlassen sich auf MutationObserver fur:

  • DOM-Dirty-Checking (React testt ob DOM-Node noch im Tree ist)
  • Vue's nextTick-basierte Mutation-Erkennung
  • Angular Zone.js patching
  • Lazy-Loading von Images (IntersectionObserver-fallback)
  • Libraries die auf childList-Mutationen warten

Ohne echten MutationObserver: Framework-Fehler, Memory-Leaks durch nie-feuernde Callbacks, kaputtes Change-Detection.

Architektur-Analyse

Aktueller Stand

// src/runtime-isolation.ts (Own DOM Mode, noop shim)
win.MutationObserver = class MutationObserverShim {
  constructor(cb: any) { this._callback = cb; }
  observe() {}
  disconnect() {}
  takeRecords() { return []; }
};

Ziel-Architektur — Non-Visual-Optimized

flowchart TB
    subgraph "DOM Mutation"
        A[Node.appendChild] --> B[Queue Microtask]
        A --> C[MutationRecord erstellen]
        D[Node.removeChild] --> B
        D --> C
        E[Element.setAttribute] --> B
        E --> C
        F[Text.data setter] --> B
        F --> C
    end
    
    subgraph "MutationObserver Engine"
        B --> G[Mikrotask-Queue]
        G --> H["Callback(this._records)"]
        H --> I["this._records = []"]
        C --> J["{type, target, addedNodes, ...}"]
    end
    
    subgraph "Non-Visual Opti"
        K["attributeFilter: nur observed"]
        L["characterDataOldValue: nur wenn angefragt"]
        M["subtree: optimierter Tree-Walk"]
    end
    
    subgraph "NO Implementation"
        N["Kein Rendering-Trigger"]
        O["Keine Layout-Invalidierung"]
        P["Kein Style-Re-Calc"]
    end
    
    A -.->|"Observer.observe(node, config)"| Q[Observer-Registry]
    Q --> G

Non-Visual Optimierungen

  1. Kein Layout-Trigger: MutationObserver-Callbacks feuern ohne Layout-Invalidierung — fur Headless irrelevant
  2. Synchronous takeRecords(): records werden aus dem internen Puffer gelesen — keine visuellen Berechnungen
  3. attributeFilter-Optimierung: Nur die im attributeFilter genannten Attribute werden getrackt — kein Full-Scan
  4. characterDataOldValue-Optimierung: characterDataOldValue: true speichert nur den alten Wert wenn angefordert
  5. subtree-Optimierung: Rekursive Registrierung per WeakMap statt Prototype-Chain-Patching
  6. Kein StyleInvalidation: Mutationen triggern kein Style-Recalculation — fur Headless nutzlos

Root Causes

  1. Kein Mikrotask-Queueing: MutationObserver-Callbacks mussen als Mikrotask (via queueMicrotask) nach der aktuellen DOM-Operation feuern — aktuell nie
  2. Kein Record-Tracking: appendChild, removeChild, setAttribute, textContent-Setter erzeugen keine MutationRecords
  3. Kein observe/disconnect-Management: observe() mussen eine Registry von Target→Observer-Verbindungen pflegen

Losungsansatze

Option A (empfohlen): Node-Hook-basiertes Mutation Tracking

Kernidee: MutationRecords werden in appendChild, removeChild, setAttribute, textContent, data-Setter erzeugt. Diese Records werden in einer globalen Queue gesammelt, die bei der nachsten Mikrotask ausgeliefert wird.

Teil 1: MutationRecord-Klasse

// src/dom/mutation-record.ts (NEU)
class MutationRecord {
  type: "attributes" | "characterData" | "childList";
  target: Node;
  addedNodes: Node[];
  removedNodes: Node[];
  previousSibling: Node | null;
  nextSibling: Node | null;
  attributeName: string | null;
  attributeNamespace: string | null;
  oldValue: string | null;
}

Teil 2: MutationObserver-Klasse

// src/dom/mutation-observer.ts (NEU)
class MutationObserver {
  constructor(callback: MutationCallback);
  
  observe(target: Node, options: MutationObserverInit): void {
    // Registriere in globaler MutationRegistry pro Node
    // Filter: childList, attributes, characterData, subtree, etc.
    // attributeFilter: nur beobachtete Attribute
  }
  
  disconnect(): void {
    // Entferne ALLE observes dieses Observers
    // Keine ausstehenden Callbacks feuern
  }
  
  takeRecords(): MutationRecord[] {
    // Leere den record-Puffer fur dieses Observer
    // Gebe gefilterte Records zuruck
  }
}

Teil 3: Hooks in Node/Element

// src/dom/node.ts — Anderungen
class Node extends OwnEventTarget {
  appendChild(child: Node): Node {
    // ... existing code ...
    MutationRegistry.notify({
      type: "childList",
      target: this,
      addedNodes: [child],
      removedNodes: [],
    });
    return child;
  }
  
  removeChild(child: Node): Node {
    MutationRegistry.notify({
      type: "childList",
      target: this,
      addedNodes: [],
      removedNodes: [child],
    });
    return child;
  }
}

Vorteile:

  • Framework-kompatibel: Reacts try { mutationObserver.observe(el, { childList: true }) } funktioniert
  • Korrekte Mikrotask-Timings: appendChild → Mikrotask → Callback feuert
  • Subtree-Observation via Tree-Walk bei notify (optional per Registrierungs-Index)
  • attributeFilter spart Arbeit (nur beobachtete Props tracken)

Nachteile:

  • Hooks in bestehenden Node-Methoden (~10 Zeilen pro Hook)
  • Observer-Registry muß bei disconnect() und GC gesaubert werden (WeakRef?)

Option B: Proxy-basiertes Mutation Tracking

Wrapper um Node.prototype mit Proxy, der alle mutationstrackenden Methoden abfangt.

Problem: Proxy auf Prototype ist extrem langsam (jeder Methoden-Call durchlault Proxy-Trap). Fur Headless nicht akzeptabel.

Option C: Polling-basiertes Tracking

setInterval-Ansatz: Vergleiche DOM-Baum alle 50ms, diff zum letzten Snapshot.

Problem: CPU-intensiv (O(n^2) Vergleich), Timing falsch (nicht Mikrotask-basiert), Frameworks erwarten korrekte Mikrotask-Semantik.

Entscheidung: Option A

  1. Node-Hooks sind die sauberste Losung: appendChild/removeChild/setAttribute sind O(1). Der Hook fugt ~10 LOC pro Methode hinzu.
  2. Mikrotask-Timing ist kritisch: Reacts useEffect wartet auf Mikrotask-Queue. Proxy oder Polling liefern falsches Timing.
  3. WeakMap-Registry: target → Set<Observer> ermoglicht O(1) Lookup bei notify und automatische GC.
  4. attributeFilter spart signifikant Arbeit: Bei ungefilterten Attributen (~100 pro Seite) vs attributeFilter: ["class"] (1 pro Seite) — Faktor 100x.

Akzeptanzkriterien

  • new MutationObserver(cb) erstellt Observer
  • observer.observe(el, { childList: true }) registriert childList-Tracking
  • appendChild erzeugt childList-Record mit addedNodes
  • removeChild erzeugt childList-Record mit removedNodes
  • setAttribute() erzeugt attributes-Record
  • removeAttribute() erzeugt attributes-Record
  • textContent = "..." erzeugt characterData-Record
  • Text.data = "..." erzeugt characterData-Record
  • Callback feuert asynchron (Mikrotask, nicht synchron)
  • takeRecords() gibt ausstehende Records zuruck
  • disconnect() stoppt alle Beobachtungen
  • subtree: true beobachtet Kindes-Kinder
  • attributeFilter: ["class"] filtert auf class-Anderungen
  • attributeOldValue: true speichert alten Wert
  • characterDataOldValue: true speichert alten Text
  • Keine Regression bei bestehenden Tests
  • takeRecords() leert den internen Puffer
  • Mehrere Observer auf gleichem Node funktionieren

Betroffene Dateien

Datei Anderung Status
src/dom/mutation-observer.ts MutationObserver + MutationRecord + MutationRegistry NEU
src/dom/node.ts Hooks in appendChild, removeChild, setAttribute, textContent Andern
src/dom/event-target.ts dispatchEvent-Hook fur Event-Tracking (optional) Andern
src/dom/index.ts Export MutationObserver Andern
src/runtime-isolation.ts MutationObserver durch echte Klasse ersetzen Andern
tests/unit/dom-mutation.test.ts 25+ Tests NEU

Dependencies

  • Issue #104 — Happy DOM Replacement (gelost) — Node/Element hooks erfordern eigene Klassen
  • Kein externes Package notwendig (pure DOM Spec)

Querverweise

Technische Risiken

  1. Mikrotask-Timing: MutationObserver muss in der MIKROTASK-Queue feuern, nicht in der MACROTASK-Queue. Losung: queueMicrotask() nach batch-Mutationen.
  2. Subtree-Observation-Performance: subtree: true bedeutet Rekursion bei jedem appendChild/removeChild. Losung: WeakMap-Registry pro Node, kein Rekursions-Scan.
  3. Memory-Leaks: Observer-Referenzen verhindern GC des target-Nodes. Losung: disconnect() lost Referenzen, und WeakRef fur internes Tracking.
  4. Bulk-Mutation-Collecting: 100x appendChild hintereinander erzeugen 100 Records. Losung: Batch-Collecting per Mikrotask: alle Mutationen in einer Mikrotask sammeln, dann ein callback([records]).

Performance-Impact

  • appendChild-Hook: ~0.001ms (WeakMap-Lookup + Record-Erstellung)
  • setAttribute-Hook: ~0.001ms (attributeFilter-Check + Record)
  • Callback-Auslieferung: ~0.01ms (Array-Kopie)
  • subtree:true Registrierung: ~0.002ms pro Node
  • Erwartet: <0.01ms zusatzlich pro DOM-Operation
  • Optimierungspotential: Bulk-Records in einem Callback bündeln statt N Callbacks. attributeFilter als Set fur O(1) Check.

Testplan

Unit-Tests (20+)

  1. MutationObserver-Konstruktor akzeptiert Callback
  2. observe registriert auf Node
  3. childList-Record bei appendChild
  4. childList-Record bei removeChild
  5. attributes-Record bei setAttribute
  6. attributes-Record bei removeAttribute
  7. characterData-Record bei textContent-Setter
  8. characterData-Record bei Text.data-Setter
  9. Callback feuert in Mikrotask (nicht synchron)
  10. takeRecords() gibt Records vor Callback zuruck
  11. disconnect() stoppt Tracking
  12. disconnect() verhindert ausstehenden Callback
  13. subtree:true: Mutation in Kind-Node tracked
  14. subtree:true: Mutation in Enkel-Node tracked
  15. attributeFilter: ["class"] filtert auf class
  16. attributeFilter verwirft nicht-listete Attribute
  17. attributeOldValue:true liefert oldValue
  18. characterDataOldValue:true liefert old text
  19. Mehrere Observer auf gleichem Node
  20. Gleicher Observer auf mehreren Nodes
  21. observe auf DocumentFragment
  22. observe auf Text-Node (characterData)
  23. Callback-Reihenfolge entspricht Insertion-Order
  24. Disconnect inside callback (kein deadlock)

Integration-Tests (3+)

  1. Erstelle createIsolatedContext({useOwnDom:true}) mit echtem MutationObserver
  2. appendChild inside context lost Callback aus
  3. React-artiges Dirty-Checking (observer.observe → appendChild → callback mit addedNodes)
## Problembeschreibung MutationObserver ist aktuell ein noop-Shim (`observe() {}`, `disconnect() {}`, `takeRecords() []`). React, Vue 3, Angular und viele andere Frameworks verlassen sich auf MutationObserver fur: - DOM-Dirty-Checking (React testt ob DOM-Node noch im Tree ist) - Vue's nextTick-basierte Mutation-Erkennung - Angular Zone.js patching - Lazy-Loading von Images (IntersectionObserver-fallback) - Libraries die auf childList-Mutationen warten Ohne echten MutationObserver: Framework-Fehler, Memory-Leaks durch nie-feuernde Callbacks, kaputtes Change-Detection. ## Architektur-Analyse ### Aktueller Stand ```ts // src/runtime-isolation.ts (Own DOM Mode, noop shim) win.MutationObserver = class MutationObserverShim { constructor(cb: any) { this._callback = cb; } observe() {} disconnect() {} takeRecords() { return []; } }; ``` ### Ziel-Architektur — Non-Visual-Optimized ```mermaid flowchart TB subgraph "DOM Mutation" A[Node.appendChild] --> B[Queue Microtask] A --> C[MutationRecord erstellen] D[Node.removeChild] --> B D --> C E[Element.setAttribute] --> B E --> C F[Text.data setter] --> B F --> C end subgraph "MutationObserver Engine" B --> G[Mikrotask-Queue] G --> H["Callback(this._records)"] H --> I["this._records = []"] C --> J["{type, target, addedNodes, ...}"] end subgraph "Non-Visual Opti" K["attributeFilter: nur observed"] L["characterDataOldValue: nur wenn angefragt"] M["subtree: optimierter Tree-Walk"] end subgraph "NO Implementation" N["Kein Rendering-Trigger"] O["Keine Layout-Invalidierung"] P["Kein Style-Re-Calc"] end A -.->|"Observer.observe(node, config)"| Q[Observer-Registry] Q --> G ``` ### Non-Visual Optimierungen 1. **Kein Layout-Trigger**: MutationObserver-Callbacks feuern ohne Layout-Invalidierung — fur Headless irrelevant 2. **Synchronous takeRecords()**: records werden aus dem internen Puffer gelesen — keine visuellen Berechnungen 3. **attributeFilter-Optimierung**: Nur die im `attributeFilter` genannten Attribute werden getrackt — kein Full-Scan 4. **characterDataOldValue-Optimierung**: `characterDataOldValue: true` speichert nur den alten Wert wenn angefordert 5. **subtree-Optimierung**: Rekursive Registrierung per WeakMap statt Prototype-Chain-Patching 6. **Kein StyleInvalidation**: Mutationen triggern kein Style-Recalculation — fur Headless nutzlos ### Root Causes 1. **Kein Mikrotask-Queueing**: MutationObserver-Callbacks mussen als Mikrotask (via queueMicrotask) nach der aktuellen DOM-Operation feuern — aktuell nie 2. **Kein Record-Tracking**: `appendChild`, `removeChild`, `setAttribute`, `textContent`-Setter erzeugen keine MutationRecords 3. **Kein observe/disconnect-Management**: `observe()` mussen eine Registry von Target→Observer-Verbindungen pflegen ## Losungsansatze ### Option A (empfohlen): Node-Hook-basiertes Mutation Tracking **Kernidee:** MutationRecords werden in `appendChild`, `removeChild`, `setAttribute`, `textContent`, `data`-Setter erzeugt. Diese Records werden in einer globalen Queue gesammelt, die bei der nachsten Mikrotask ausgeliefert wird. **Teil 1: MutationRecord-Klasse** ```ts // src/dom/mutation-record.ts (NEU) class MutationRecord { type: "attributes" | "characterData" | "childList"; target: Node; addedNodes: Node[]; removedNodes: Node[]; previousSibling: Node | null; nextSibling: Node | null; attributeName: string | null; attributeNamespace: string | null; oldValue: string | null; } ``` **Teil 2: MutationObserver-Klasse** ```ts // src/dom/mutation-observer.ts (NEU) class MutationObserver { constructor(callback: MutationCallback); observe(target: Node, options: MutationObserverInit): void { // Registriere in globaler MutationRegistry pro Node // Filter: childList, attributes, characterData, subtree, etc. // attributeFilter: nur beobachtete Attribute } disconnect(): void { // Entferne ALLE observes dieses Observers // Keine ausstehenden Callbacks feuern } takeRecords(): MutationRecord[] { // Leere den record-Puffer fur dieses Observer // Gebe gefilterte Records zuruck } } ``` **Teil 3: Hooks in Node/Element** ```ts // src/dom/node.ts — Anderungen class Node extends OwnEventTarget { appendChild(child: Node): Node { // ... existing code ... MutationRegistry.notify({ type: "childList", target: this, addedNodes: [child], removedNodes: [], }); return child; } removeChild(child: Node): Node { MutationRegistry.notify({ type: "childList", target: this, addedNodes: [], removedNodes: [child], }); return child; } } ``` **Vorteile:** - Framework-kompatibel: Reacts `try { mutationObserver.observe(el, { childList: true }) }` funktioniert - Korrekte Mikrotask-Timings: `appendChild` → Mikrotask → Callback feuert - Subtree-Observation via Tree-Walk bei notify (optional per Registrierungs-Index) - `attributeFilter` spart Arbeit (nur beobachtete Props tracken) **Nachteile:** - Hooks in bestehenden Node-Methoden (~10 Zeilen pro Hook) - Observer-Registry muß bei `disconnect()` und GC gesaubert werden (WeakRef?) ### Option B: Proxy-basiertes Mutation Tracking Wrapper um Node.prototype mit Proxy, der alle mutationstrackenden Methoden abfangt. **Problem:** Proxy auf Prototype ist extrem langsam (jeder Methoden-Call durchlault Proxy-Trap). Fur Headless nicht akzeptabel. ### Option C: Polling-basiertes Tracking setInterval-Ansatz: Vergleiche DOM-Baum alle 50ms, diff zum letzten Snapshot. **Problem:** CPU-intensiv (O(n^2) Vergleich), Timing falsch (nicht Mikrotask-basiert), Frameworks erwarten korrekte Mikrotask-Semantik. ## Entscheidung: Option A 1. **Node-Hooks sind die sauberste Losung**: appendChild/removeChild/setAttribute sind O(1). Der Hook fugt ~10 LOC pro Methode hinzu. 2. **Mikrotask-Timing ist kritisch**: Reacts `useEffect` wartet auf Mikrotask-Queue. Proxy oder Polling liefern falsches Timing. 3. **WeakMap-Registry**: `target → Set<Observer>` ermoglicht O(1) Lookup bei notify und automatische GC. 4. **`attributeFilter` spart signifikant Arbeit**: Bei ungefilterten Attributen (~100 pro Seite) vs `attributeFilter: ["class"]` (1 pro Seite) — Faktor 100x. ## Akzeptanzkriterien - [ ] `new MutationObserver(cb)` erstellt Observer - [ ] `observer.observe(el, { childList: true })` registriert childList-Tracking - [ ] `appendChild` erzeugt childList-Record mit `addedNodes` - [ ] `removeChild` erzeugt childList-Record mit `removedNodes` - [ ] `setAttribute()` erzeugt attributes-Record - [ ] `removeAttribute()` erzeugt attributes-Record - [ ] `textContent = "..."` erzeugt characterData-Record - [ ] `Text.data = "..."` erzeugt characterData-Record - [ ] Callback feuert asynchron (Mikrotask, nicht synchron) - [ ] `takeRecords()` gibt ausstehende Records zuruck - [ ] `disconnect()` stoppt alle Beobachtungen - [ ] `subtree: true` beobachtet Kindes-Kinder - [ ] `attributeFilter: ["class"]` filtert auf class-Anderungen - [ ] `attributeOldValue: true` speichert alten Wert - [ ] `characterDataOldValue: true` speichert alten Text - [ ] Keine Regression bei bestehenden Tests - [ ] `takeRecords()` leert den internen Puffer - [ ] Mehrere Observer auf gleichem Node funktionieren ## Betroffene Dateien | Datei | Anderung | Status | |-------|----------|--------| | `src/dom/mutation-observer.ts` | MutationObserver + MutationRecord + MutationRegistry | **NEU** | | `src/dom/node.ts` | Hooks in appendChild, removeChild, setAttribute, textContent | Andern | | `src/dom/event-target.ts` | dispatchEvent-Hook fur Event-Tracking (optional) | Andern | | `src/dom/index.ts` | Export MutationObserver | Andern | | `src/runtime-isolation.ts` | MutationObserver durch echte Klasse ersetzen | Andern | | `tests/unit/dom-mutation.test.ts` | 25+ Tests | **NEU** | ## Dependencies - Issue #104 — Happy DOM Replacement (gelost) — Node/Element hooks erfordern eigene Klassen - Kein externes Package notwendig (pure DOM Spec) ## Querverweise - [MutationObserver Spec (WICG)](https://dom.spec.whatwg.org/#mutation-observers) - [MDN: MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) - `references/non-visual-dom-opti.md` — Non-Visual-Optimierungs-Protocol ## Technische Risiken 1. **Mikrotask-Timing**: MutationObserver muss in der MIKROTASK-Queue feuern, nicht in der MACROTASK-Queue. Losung: `queueMicrotask()` nach batch-Mutationen. 2. **Subtree-Observation-Performance**: `subtree: true` bedeutet Rekursion bei jedem appendChild/removeChild. Losung: WeakMap-Registry pro Node, kein Rekursions-Scan. 3. **Memory-Leaks**: Observer-Referenzen verhindern GC des target-Nodes. Losung: `disconnect()` lost Referenzen, und WeakRef fur internes Tracking. 4. **Bulk-Mutation-Collecting**: 100x appendChild hintereinander erzeugen 100 Records. Losung: Batch-Collecting per Mikrotask: alle Mutationen in einer Mikrotask sammeln, dann ein callback([records]). ## Performance-Impact - **appendChild-Hook**: ~0.001ms (WeakMap-Lookup + Record-Erstellung) - **setAttribute-Hook**: ~0.001ms (attributeFilter-Check + Record) - **Callback-Auslieferung**: ~0.01ms (Array-Kopie) - **subtree:true Registrierung**: ~0.002ms pro Node - **Erwartet**: <0.01ms zusatzlich pro DOM-Operation - **Optimierungspotential**: Bulk-Records in einem Callback bündeln statt N Callbacks. `attributeFilter` als Set fur O(1) Check. ## Testplan ### Unit-Tests (20+) 1. MutationObserver-Konstruktor akzeptiert Callback 2. observe registriert auf Node 3. childList-Record bei appendChild 4. childList-Record bei removeChild 5. attributes-Record bei setAttribute 6. attributes-Record bei removeAttribute 7. characterData-Record bei textContent-Setter 8. characterData-Record bei Text.data-Setter 9. Callback feuert in Mikrotask (nicht synchron) 10. takeRecords() gibt Records vor Callback zuruck 11. disconnect() stoppt Tracking 12. disconnect() verhindert ausstehenden Callback 13. subtree:true: Mutation in Kind-Node tracked 14. subtree:true: Mutation in Enkel-Node tracked 15. attributeFilter: ["class"] filtert auf class 16. attributeFilter verwirft nicht-listete Attribute 17. attributeOldValue:true liefert oldValue 18. characterDataOldValue:true liefert old text 19. Mehrere Observer auf gleichem Node 20. Gleicher Observer auf mehreren Nodes 21. observe auf DocumentFragment 22. observe auf Text-Node (characterData) 23. Callback-Reihenfolge entspricht Insertion-Order 24. Disconnect inside callback (kein deadlock) ### Integration-Tests (3+) 1. Erstelle createIsolatedContext({useOwnDom:true}) mit echtem MutationObserver 2. appendChild inside context lost Callback aus 3. React-artiges Dirty-Checking (observer.observe → appendChild → callback mit addedNodes)
Author
Owner

Resolved in commit 440573b

#106: MutationObserver vollstandig implementiert — MutationRecord, MutationObserver, MutationRegistry mit Mikrotask-Queueing, Node-Hooks fur childList/attributes/characterData
#109: Custom Elements Lifecycle — connectedCallback/disconnectedCallback/attributeChangedCallback, Parser-Integration via _customElementsRegistry, attachShadow in OwnDOM
#105/#107/#108/#110: Bereits implementiert (99 Tests grun)

Tests: 247 pass, 0 fail

✅ **Resolved** in commit 440573b **#106:** MutationObserver vollstandig implementiert — MutationRecord, MutationObserver, MutationRegistry mit Mikrotask-Queueing, Node-Hooks fur childList/attributes/characterData **#109:** Custom Elements Lifecycle — connectedCallback/disconnectedCallback/attributeChangedCallback, Parser-Integration via _customElementsRegistry, attachShadow in OwnDOM **#105/#107/#108/#110:** Bereits implementiert (99 Tests grun) Tests: 247 pass, 0 fail
Artur closed this issue 2026-06-19 15:59:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#106
No description provided.