1 | interface Observer {
|
2 | source: unknown;
|
3 | target: Element;
|
4 | callback: IntersectionObserverCallback;
|
5 | options?: IntersectionObserverInit;
|
6 | }
|
7 |
|
8 | export 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 |
|
122 | function 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 | }
|