Observer Layer: Happy DOM MutationObserver + Fake IntersectionObserver + ResizeObserver #10

Closed
opened 2026-06-17 13:37:44 +00:00 by Artur · 1 comment
Owner

Goal

Implement observer layer: MutationObserver (real via Happy DOM), IntersectionObserver (fake), ResizeObserver (fake).

What to Build

src/observers/mutation-observer.ts

Happy DOM already ships a working MutationObserver. We:

  1. Verify it works correctly
  2. Write tests proving correct behavior
  3. Make it available in createIsolatedContext()

Key behaviors to verify:

  • Records addedNodes, removedNodes, attribute changes, characterData changes
  • Callback fires asynchronously (microtask), not synchronously
  • MutationRecord has correct: type, target, addedNodes, removedNodes, attributeName, oldValue
  • disconnect() stops observing
  • takeRecords() returns pending records
  • subtree: true observes children
  • childList: true, attributes: true, characterData: true, etc.

src/observers/intersection-observer.ts

export class FakeIntersectionObserver {
  constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit);

  // All elements are ALWAYS intersecting:
  observe(target: Element): void {
    // Fire callback on next microtask after observe():
    //   callback([{ target, isIntersecting: true, intersectionRatio: 1.0,
    //               boundingClientRect: ..., intersectionRect: ...,
    //               rootBounds: ..., time: ... }], this);
  }

  unobserve(target: Element): void;
  disconnect(): void;
  takeRecords(): IntersectionObserverEntry[];

  root: Element | Document | null;  // null = viewport
  rootMargin: string;                // '0px 0px 0px 0px'
  thresholds: ReadonlyArray<number>; // [0]
}

src/observers/resize-observer.ts

export class FakeResizeObserver {
  constructor(callback: ResizeObserverCallback);

  observe(target: Element): void {
    // Fire callback once with current borderBox size:
    //   callback([{ target, borderBoxSize: [{ inlineSize: 1366, blockSize: 768 }],
    //               contentBoxSize: [{ inlineSize: 1366, blockSize: 768 }],
    //               devicePixelContentBoxSize: [{ inlineSize: 1366, blockSize: 768 }] }], this);
  }

  unobserve(target: Element): void;
  disconnect(): void;
}

Why Fake Observers Work

  • IntersectionObserver: For crawling, everything is "visible". This triggers lazy loaders.
  • ResizeObserver: One immediate fire per observed element. This is enough for responsive layouts to initialize.
  • Both observers fire on next microtask, not immediately, matching browser behavior.

Tests

Unit Tests

Test Verifies
mutation-observer.basic.test.ts Observes childList changes, callback fires
mutation-observer.attributes.test.ts Observes attribute changes
mutation-observer.disconnect.test.ts disconnect() stops observing
mutation-observer.take-records.test.ts takeRecords() returns pending
mutation-observer.subtree.test.ts subtree: true observes children
intersection-observer.basic.test.ts Initial observe fires callback
intersection-observer.is-intersecting.test.ts isIntersecting is always true
intersection-observer.unobserve.test.ts unobserve stops callbacks
intersection-observer.disconnect.test.ts disconnect() stops all
resize-observer.basic.test.ts Initial observe fires callback with sizes
resize-observer.unobserve.test.ts unobserve stops callbacks
resize-observer.disconnect.test.ts disconnect() stops all

Edge Cases

Test Verifies
mutation-observer-no-changes.test.ts Callback not called without changes
mutation-observer-old-value.test.ts attributeOldValue works
mutation-observer-character-data.test.ts characterData changes recorded
intersection-observer-root-margin.test.ts rootMargin stored but ignored
intersection-observer-thresholds.test.ts thresholds stored
resize-observer-multiple-elements.test.ts Multiple observed elements fire individually

Definition of Done

  • MutationObserver tested and confirmed working from Happy DOM
  • src/observers/intersection-observer.ts implemented
  • src/observers/resize-observer.ts implemented
  • All observers available in createIsolatedContext()
  • All tests pass
  • 100% line + branch coverage
## Goal Implement observer layer: MutationObserver (real via Happy DOM), IntersectionObserver (fake), ResizeObserver (fake). ## What to Build ### src/observers/mutation-observer.ts Happy DOM already ships a working MutationObserver. We: 1. Verify it works correctly 2. Write tests proving correct behavior 3. Make it available in createIsolatedContext() Key behaviors to verify: - Records addedNodes, removedNodes, attribute changes, characterData changes - Callback fires asynchronously (microtask), not synchronously - MutationRecord has correct: type, target, addedNodes, removedNodes, attributeName, oldValue - disconnect() stops observing - takeRecords() returns pending records - subtree: true observes children - childList: true, attributes: true, characterData: true, etc. ### src/observers/intersection-observer.ts ``` export class FakeIntersectionObserver { constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit); // All elements are ALWAYS intersecting: observe(target: Element): void { // Fire callback on next microtask after observe(): // callback([{ target, isIntersecting: true, intersectionRatio: 1.0, // boundingClientRect: ..., intersectionRect: ..., // rootBounds: ..., time: ... }], this); } unobserve(target: Element): void; disconnect(): void; takeRecords(): IntersectionObserverEntry[]; root: Element | Document | null; // null = viewport rootMargin: string; // '0px 0px 0px 0px' thresholds: ReadonlyArray<number>; // [0] } ``` ### src/observers/resize-observer.ts ``` export class FakeResizeObserver { constructor(callback: ResizeObserverCallback); observe(target: Element): void { // Fire callback once with current borderBox size: // callback([{ target, borderBoxSize: [{ inlineSize: 1366, blockSize: 768 }], // contentBoxSize: [{ inlineSize: 1366, blockSize: 768 }], // devicePixelContentBoxSize: [{ inlineSize: 1366, blockSize: 768 }] }], this); } unobserve(target: Element): void; disconnect(): void; } ``` ### Why Fake Observers Work - IntersectionObserver: For crawling, everything is "visible". This triggers lazy loaders. - ResizeObserver: One immediate fire per observed element. This is enough for responsive layouts to initialize. - Both observers fire on next microtask, not immediately, matching browser behavior. ## Tests ### Unit Tests | Test | Verifies | |------|----------| | mutation-observer.basic.test.ts | Observes childList changes, callback fires | | mutation-observer.attributes.test.ts | Observes attribute changes | | mutation-observer.disconnect.test.ts | disconnect() stops observing | | mutation-observer.take-records.test.ts | takeRecords() returns pending | | mutation-observer.subtree.test.ts | subtree: true observes children | | intersection-observer.basic.test.ts | Initial observe fires callback | | intersection-observer.is-intersecting.test.ts | isIntersecting is always true | | intersection-observer.unobserve.test.ts | unobserve stops callbacks | | intersection-observer.disconnect.test.ts | disconnect() stops all | | resize-observer.basic.test.ts | Initial observe fires callback with sizes | | resize-observer.unobserve.test.ts | unobserve stops callbacks | | resize-observer.disconnect.test.ts | disconnect() stops all | ### Edge Cases | Test | Verifies | |------|----------| | mutation-observer-no-changes.test.ts | Callback not called without changes | | mutation-observer-old-value.test.ts | attributeOldValue works | | mutation-observer-character-data.test.ts | characterData changes recorded | | intersection-observer-root-margin.test.ts | rootMargin stored but ignored | | intersection-observer-thresholds.test.ts | thresholds stored | | resize-observer-multiple-elements.test.ts | Multiple observed elements fire individually | ## Definition of Done - [ ] MutationObserver tested and confirmed working from Happy DOM - [ ] src/observers/intersection-observer.ts implemented - [ ] src/observers/resize-observer.ts implemented - [ ] All observers available in createIsolatedContext() - [ ] All tests pass - [ ] 100% line + branch coverage
Author
Owner

Observer Layer: MutationObserver + IntersectionObserver + ResizeObserver. Implementiert via Happy DOM + Fakes. Tests: observers coverage.

Observer Layer: MutationObserver + IntersectionObserver + ResizeObserver. ✅ Implementiert via Happy DOM + Fakes. Tests: observers coverage.
Artur closed this issue 2026-06-18 06:28:03 +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#10
No description provided.