1import { describe, expect, it, vi } from "vite-plus/test";
2
3import { observeIntersection } from "./observeIntersection";
4
5class FakeIntersectionObserver {
6 static instances: FakeIntersectionObserver[] = [];
7 readonly observed = new Set<Element>();
8 disconnected = false;
9 constructor(
10 readonly callback: (entries: IntersectionObserverEntry[]) => void,
11 readonly options: IntersectionObserverInit,
12 ) {
13 FakeIntersectionObserver.instances.push(this);
14 }
15 observe(element: Element) {
16 this.observed.add(element);
17 }
18 unobserve(element: Element) {
19 this.observed.delete(element);
20 }
21 disconnect() {
22 this.observed.clear();
23 this.disconnected = true;
24 }
25 emit(target: Element, isIntersecting: boolean) {
26 this.callback([{ target, isIntersecting } as IntersectionObserverEntry]);
27 }
28}
29
30function setup() {
31 FakeIntersectionObserver.instances = [];
32 vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver);
33 return {
34 [Symbol.dispose]: () => vi.unstubAllGlobals(),
35 };
36}
37
38const element = (name: string) => ({ name }) as unknown as Element;
39
40describe("observeIntersection", () => {
41 it("shares one observer across elements with the same root and options", () => {
42 using _globals = setup();
43 const first = element("first");
44 const second = element("second");
45 const onFirst = vi.fn();
46 const onSecond = vi.fn();
47
48 const stopFirst = observeIntersection(first, onFirst, { rootMargin: "160px 0px" });
49 const stopSecond = observeIntersection(second, onSecond, { rootMargin: "160px 0px" });
50 observeIntersection(first, () => {}, { rootMargin: "600px" })();
51
52 expect(FakeIntersectionObserver.instances).toHaveLength(2);
53 const [shared] = FakeIntersectionObserver.instances;
54 shared!.emit(second, true);
55 expect(onFirst).not.toHaveBeenCalled();
56 expect(onSecond).toHaveBeenCalledWith(expect.objectContaining({ isIntersecting: true }));
57
58 stopFirst();
59 expect(shared!.disconnected).toBe(false);
60 stopSecond();
61 expect(shared!.disconnected).toBe(true);
62 });
63
64 it("gives a late subscriber the current state without repeating it to earlier ones", () => {
65 using _globals = setup();
66 const target = element("row");
67 const early = vi.fn();
68 const late = vi.fn();
69 observeIntersection(target, early);
70 const [observer] = FakeIntersectionObserver.instances;
71 observer!.emit(target, true);
72
73 observeIntersection(target, late);
74 // The browser answers a re-observe with the unchanged state.
75 observer!.emit(target, true);
76
77 expect(early).toHaveBeenCalledTimes(1);
78 expect(late).toHaveBeenCalledTimes(1);
79
80 observer!.emit(target, false);
81 expect(early).toHaveBeenCalledTimes(2);
82 expect(late).toHaveBeenCalledTimes(2);
83 });
84});