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