Ctrl K
ComunidadGuías…

src/test/intersection.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// An `IntersectionObserver` for jsdom, which implements none.
//
// It has to exist at all because `new IntersectionObserver` throws outright
// without it: `LoadMoreTrigger` observes its button so a list pulls the next
// page in as the diver scrolls, and every page rendering a list would fail on
// mount rather than at an assertion.
//
// It reports nothing by default, which is the honest answer - whether an element
// is near the viewport is a layout question, and "jsdom answers no layout
// question" (DECISIONS.md). A test that wants the answer says so with `reveal`,
// which is a statement about the scenario rather than about the environment: the
// reader has scrolled to the end of the list, or far enough down the gear page
// to reach the sets card. Anything genuinely about *when* the browser decides an
// element is near belongs in the browser lane.
//
// Installed globally from `vitest.setup.ts`, unlike `memoryStorage`, because the
// stub holds no per-test state a suite could leak through - `reveal` is a
// one-shot call over whatever is mounted right now, and the registry is cleared
// after every test.

interface Registration {
  element: Element;
  callback: IntersectionObserverCallback;
  observer: IntersectionObserver;
}

const registrations: Registration[] = [];

class StubIntersectionObserver implements IntersectionObserver {
  readonly root = null;
  readonly rootMargin = "";
  readonly scrollMargin = "";
  readonly thresholds: readonly number[] = [];

  constructor(private readonly callback: IntersectionObserverCallback) {}

  observe(element: Element) {
    registrations.push({ element, callback: this.callback, observer: this });
  }

  unobserve(element: Element) {
    for (let i = registrations.length - 1; i >= 0; i -= 1) {
      if (
        registrations[i].observer === this &&
        registrations[i].element === element
      ) {
        registrations.splice(i, 1);
      }
    }
  }

  disconnect() {
    for (let i = registrations.length - 1; i >= 0; i -= 1) {
      if (registrations[i].observer === this) registrations.splice(i, 1);
    }
  }

  takeRecords(): IntersectionObserverEntry[] {
    return [];
  }
}

export function installIntersectionObserverStub() {
  window.IntersectionObserver =
    StubIntersectionObserver as unknown as typeof IntersectionObserver;
}

export function clearIntersectionObservers() {
  registrations.length = 0;
}

/**
 * Report every element currently under observation as on screen - "the reader
 * has scrolled to it".
 *
 * Wrap the call in `act()`: it runs React state updates synchronously. The list
 * is copied first because a callback can mount or unmount observed nodes, which
 * would otherwise mutate the array mid-iteration.
 */
export function reveal() {
  for (const { element, callback, observer } of [...registrations]) {
    callback(
      [
        {
          isIntersecting: true,
          intersectionRatio: 1,
          target: element,
          boundingClientRect: { top: 100, bottom: 140 } as DOMRectReadOnly,
          rootBounds: { top: 0, bottom: 799 } as DOMRectReadOnly,
        } as IntersectionObserverEntry,
      ],
      observer,
    );
  }
}

/**
 * Report every element currently under observation as scrolled up past the top
 * of the viewport - off screen, and above it rather than below. Wrap it in
 * `act()`, as `reveal`.
 *
 * `within` narrows it to the elements inside one node - the reader has scrolled
 * past this card and not the next.
 */
export function scrollPast(within?: Element) {
  for (const { element, callback, observer } of [...registrations]) {
    if (within && !within.contains(element)) continue;
    callback(
      [
        {
          isIntersecting: false,
          intersectionRatio: 0,
          target: element,
          boundingClientRect: { top: -2, bottom: -1 } as DOMRectReadOnly,
          rootBounds: { top: 0, bottom: 799 } as DOMRectReadOnly,
        } as IntersectionObserverEntry,
      ],
      observer,
    );
  }
}

/**
 * Report every element currently under observation as held at the foot of the
 * viewport by `position: sticky` - its last pixel past a root shrunk by one at
 * the bottom, so not wholly in view, and below rather than above. Wrap it in
 * `act()`, as `reveal`.
 */
export function pinToBottom() {
  for (const { element, callback, observer } of [...registrations]) {
    callback(
      [
        {
          isIntersecting: true,
          intersectionRatio: 0.98,
          target: element,
          boundingClientRect: { bottom: 800 } as DOMRectReadOnly,
          rootBounds: { bottom: 799 } as DOMRectReadOnly,
        } as IntersectionObserverEntry,
      ],
      observer,
    );
  }
}

/**
 * Report every element currently under observation as held at the top of its
 * scroll box by `position: sticky` - its first pixel past a root grown by one at
 * the top, so not wholly in view, and above rather than below. Wrap it in
 * `act()`, as `reveal`.
 */
export function pinToTop() {
  for (const { element, callback, observer } of [...registrations]) {
    callback(
      [
        {
          isIntersecting: true,
          intersectionRatio: 0.98,
          target: element,
          boundingClientRect: { top: -2, bottom: 40 } as DOMRectReadOnly,
          rootBounds: { top: -1, bottom: 800 } as DOMRectReadOnly,
        } as IntersectionObserverEntry,
      ],
      observer,
    );
  }
}