UNPKG

3.95 kBPlain TextView Raw
1interface Observer {
2 source: unknown;
3 target: Element;
4 callback: IntersectionObserverCallback;
5 options?: IntersectionObserverInit;
6}
7
8export default class IntersectionObserverMock {
9 observers: Observer[] = [];
10
11 private isUsingMockIntersectionObserver = false;
12 private originalIntersectionObserver = (global as any).IntersectionObserver;
13 private originalIntersectionObserverEntry = (global as any)
14 .IntersectionObserverEntry;
15
16 simulate(
17 entry:
18 | Partial<IntersectionObserverEntry>
19 | Partial<IntersectionObserverEntry>[],
20 ) {
21 this.ensureMocked();
22
23 const arrayOfEntries = Array.isArray(entry) ? entry : [entry];
24 const targets = arrayOfEntries.map(({target}) => target);
25 const noCustomTargets = targets.every(target => target == null);
26
27 for (const observer of this.observers) {
28 if (noCustomTargets || targets.includes(observer.target)) {
29 observer.callback(
30 arrayOfEntries.map(entry => normalizeEntry(entry, observer.target)),
31 observer as any,
32 );
33 }
34 }
35 }
36
37 mock() {
38 if (this.isUsingMockIntersectionObserver) {
39 throw new Error(
40 'IntersectionObserver is already mocked, but you tried to mock it again.',
41 );
42 }
43
44 this.isUsingMockIntersectionObserver = true;
45
46 const setObservers = (setter: (observers: Observer[]) => Observer[]) =>
47 (this.observers = setter(this.observers));
48
49 (global as any).IntersectionObserverEntry = class IntersectionObserverEntry {};
50 Object.defineProperty(
51 IntersectionObserverEntry.prototype,
52 'intersectionRatio',
53 {
54 get() {
55 return 0;
56 },
57 },
58 );
59
60 (global as any).IntersectionObserver = class FakeIntersectionObserver {
61 constructor(
62 private callback: IntersectionObserverCallback,
63 private options?: IntersectionObserverInit,
64 ) {}
65
66 observe(target: Element) {
67 setObservers(observers => [
68 ...observers,
69 {
70 source: this,
71 target,
72 callback: this.callback,
73 options: this.options,
74 },
75 ]);
76 }
77
78 disconnect() {
79 setObservers(observers =>
80 observers.filter(observer => observer.source !== this),
81 );
82 }
83
84 unobserve(target: Element) {
85 setObservers(observers =>
86 observers.filter(
87 observer =>
88 !(observer.target === target && observer.source === this),
89 ),
90 );
91 }
92 };
93 }
94
95 restore() {
96 if (!this.isUsingMockIntersectionObserver) {
97 throw new Error(
98 'IntersectionObserver is already real, but you tried to restore it again.',
99 );
100 }
101
102 (global as any).IntersectionObserver = this.originalIntersectionObserver;
103 (global as any).IntersectionObserverEntry = this.originalIntersectionObserverEntry;
104
105 this.isUsingMockIntersectionObserver = false;
106 this.observers.length = 0;
107 }
108
109 isMocked() {
110 return this.isUsingMockIntersectionObserver;
111 }
112
113 private ensureMocked() {
114 if (!this.isUsingMockIntersectionObserver) {
115 throw new Error(
116 'You must call intersectionObserver.mock() before interacting with the fake IntersectionObserver.',
117 );
118 }
119 }
120}
121
122function normalizeEntry(
123 entry: Partial<IntersectionObserverEntry>,
124 target: Element,
125): IntersectionObserverEntry {
126 const isIntersecting =
127 entry.isIntersecting == null
128 ? Boolean(entry.intersectionRatio)
129 : entry.isIntersecting;
130
131 const intersectionRatio = entry.intersectionRatio || (isIntersecting ? 1 : 0);
132
133 return {
134 boundingClientRect:
135 entry.boundingClientRect || target.getBoundingClientRect(),
136 intersectionRatio,
137 intersectionRect: entry.intersectionRect || target.getBoundingClientRect(),
138 isIntersecting,
139 rootBounds: entry.rootBounds || document.body.getBoundingClientRect(),
140 target,
141 time: entry.time || Date.now(),
142 };
143}