UNPKG

4.02 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 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
50 (global as any).IntersectionObserverEntry = class IntersectionObserverEntry {};
51 Object.defineProperty(
52 IntersectionObserverEntry.prototype,
53 'intersectionRatio',
54 {
55 get() {
56 return 0;
57 },
58 },
59 );
60
61 (global as any).IntersectionObserver = class FakeIntersectionObserver {
62 constructor(
63 private callback: IntersectionObserverCallback,
64 private options?: IntersectionObserverInit,
65 ) {}
66
67 observe(target: Element) {
68 setObservers(observers => [
69 ...observers,
70 {
71 source: this,
72 target,
73 callback: this.callback,
74 options: this.options,
75 },
76 ]);
77 }
78
79 disconnect() {
80 setObservers(observers =>
81 observers.filter(observer => observer.source !== this),
82 );
83 }
84
85 unobserve(target: Element) {
86 setObservers(observers =>
87 observers.filter(
88 observer =>
89 !(observer.target === target && observer.source === this),
90 ),
91 );
92 }
93 };
94 }
95
96 restore() {
97 if (!this.isUsingMockIntersectionObserver) {
98 throw new Error(
99 'IntersectionObserver is already real, but you tried to restore it again.',
100 );
101 }
102
103 (global as any).IntersectionObserver = this.originalIntersectionObserver;
104 (global as any).IntersectionObserverEntry = this.originalIntersectionObserverEntry;
105
106 this.isUsingMockIntersectionObserver = false;
107 this.observers.length = 0;
108 }
109
110 isMocked() {
111 return this.isUsingMockIntersectionObserver;
112 }
113
114 private ensureMocked() {
115 if (!this.isUsingMockIntersectionObserver) {
116 throw new Error(
117 'You must call intersectionObserver.mock() before interacting with the fake IntersectionObserver.',
118 );
119 }
120 }
121}
122
123function normalizeEntry(
124 entry: Partial<IntersectionObserverEntry>,
125 target: Element,
126): IntersectionObserverEntry {
127 const isIntersecting =
128 entry.isIntersecting == null
129 ? Boolean(entry.intersectionRatio)
130 : entry.isIntersecting;
131
132 const intersectionRatio = entry.intersectionRatio || (isIntersecting ? 1 : 0);
133
134 return {
135 boundingClientRect:
136 entry.boundingClientRect || target.getBoundingClientRect(),
137 intersectionRatio,
138 intersectionRect: entry.intersectionRect || target.getBoundingClientRect(),
139 isIntersecting,
140 rootBounds: entry.rootBounds || document.body.getBoundingClientRect(),
141 target,
142 time: entry.time || Date.now(),
143 };
144}