@excom/kit-shims 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/kit-shims.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/close-watcher-polyfill.ts +131 -0
- package/config/rig.json +6 -0
- package/idle-cb.ts +30 -0
- package/index.ts +2 -0
- package/package.json +35 -0
- package/rush-logs/kit-shims.apply-exports.cache.log +1 -0
- package/rush-logs/kit-shims.apply-exports.log +1 -0
- package/support/tests/close-watcher.test.ts +226 -0
- package/support/tests/kit-shims.test.ts +62 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const closeWatcherStack: CloseWatcher[] = [];
|
|
2
|
+
class CloseWatcher extends EventTarget {
|
|
3
|
+
#isActive = true;
|
|
4
|
+
#firingCancelEvent = false;
|
|
5
|
+
#oncancelHandler = null;
|
|
6
|
+
#oncloseHandler = null;
|
|
7
|
+
|
|
8
|
+
constructor({ signal }: { signal?: AbortSignal } = {}) {
|
|
9
|
+
super();
|
|
10
|
+
// No user-activation check. Real CloseWatcher would sometimes group
|
|
11
|
+
// with previous watchers; this polyfill never does.
|
|
12
|
+
|
|
13
|
+
if (signal) {
|
|
14
|
+
if (signal.aborted) {
|
|
15
|
+
this.#isActive = false;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
signal.addEventListener("abort", () => this.#deactivate());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
closeWatcherStack.push(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
destroy() {
|
|
25
|
+
this.#deactivate();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
close() {
|
|
29
|
+
if (!this.#isActive || !document.defaultView) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.dispatchEvent(new Event("close"));
|
|
34
|
+
|
|
35
|
+
this.#deactivate();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
requestClose() {
|
|
39
|
+
if (!this.#isActive) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (this.#firingCancelEvent) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// No user-activation check. Real CloseWatcher would sometimes skip
|
|
47
|
+
// `cancel`; this polyfill always fires it.
|
|
48
|
+
|
|
49
|
+
this.#firingCancelEvent = true;
|
|
50
|
+
const shouldContinue = this.dispatchEvent(
|
|
51
|
+
new Event("cancel", { cancelable: true })
|
|
52
|
+
);
|
|
53
|
+
this.#firingCancelEvent = false;
|
|
54
|
+
if (!shouldContinue) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (this.#isActive && document.defaultView) {
|
|
59
|
+
this.dispatchEvent(new Event("close"));
|
|
60
|
+
}
|
|
61
|
+
this.#deactivate();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#deactivate() {
|
|
65
|
+
this.#isActive = false;
|
|
66
|
+
|
|
67
|
+
// May not be top of stack if `destroy()` ran.
|
|
68
|
+
const index = closeWatcherStack.indexOf(this);
|
|
69
|
+
if (index !== -1) {
|
|
70
|
+
closeWatcherStack.splice(index, 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get oncancel() {
|
|
75
|
+
return this.#oncancelHandler;
|
|
76
|
+
}
|
|
77
|
+
set oncancel(handler) {
|
|
78
|
+
if (handler !== this.#oncancelHandler || handler === null) {
|
|
79
|
+
this.removeEventListener("cancel", this.#oncancelHandler);
|
|
80
|
+
this.#oncancelHandler = null;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.#oncancelHandler = handler;
|
|
85
|
+
this.addEventListener("cancel", this.#oncancelHandler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get onclose() {
|
|
89
|
+
return this.#oncancelHandler;
|
|
90
|
+
}
|
|
91
|
+
set onclose(handler) {
|
|
92
|
+
if (handler !== this.#oncloseHandler || handler === null) {
|
|
93
|
+
this.removeEventListener("close", this.#oncloseHandler);
|
|
94
|
+
this.#oncloseHandler = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.#oncloseHandler = handler;
|
|
99
|
+
this.addEventListener("close", this.#oncloseHandler);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Escape keydowns only. No Android back button (or other close signals).
|
|
104
|
+
document.addEventListener("keydown", (e) => {
|
|
105
|
+
if (!e.isTrusted) {
|
|
106
|
+
// Not a user-triggered keydown.
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (e.key !== "Escape") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const closeWatcher = closeWatcherStack.at(-1);
|
|
115
|
+
if (!closeWatcher) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Let other listeners run first. Queue a task; `queueMicrotask()`
|
|
120
|
+
empties between each listener for browser-triggered events. */
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
if (e.defaultPrevented) {
|
|
123
|
+
// Another listener canceled; don't deliver to CloseWatcher.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
closeWatcher.requestClose();
|
|
128
|
+
}, 0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export { CloseWatcher };
|
package/config/rig.json
ADDED
package/idle-cb.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const requestIdleCb = (
|
|
2
|
+
cb: (arg?: { didTimeout: boolean; timeRemaining: () => number }) => void,
|
|
3
|
+
fallbackDelay: number = 1
|
|
4
|
+
) => {
|
|
5
|
+
if (window.requestIdleCallback) {
|
|
6
|
+
/* Use the caller delay as a timeout so idle work can't race ahead of
|
|
7
|
+
"wait until after paint" (Safari fallback already uses
|
|
8
|
+
`fallbackDelay` as setTimeout ms). */
|
|
9
|
+
return window.requestIdleCallback(cb, { timeout: fallbackDelay });
|
|
10
|
+
} else {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
return setTimeout(() => {
|
|
13
|
+
cb({
|
|
14
|
+
didTimeout: false,
|
|
15
|
+
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
|
|
16
|
+
});
|
|
17
|
+
}, fallbackDelay);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* unused
|
|
23
|
+
* export const cancelIdleCb = (id: number) => {
|
|
24
|
+
* if (window.cancelIdleCallback) {
|
|
25
|
+
* window.cancelIdleCallback(id);
|
|
26
|
+
* } else {
|
|
27
|
+
* clearTimeout(id);
|
|
28
|
+
* }
|
|
29
|
+
* };
|
|
30
|
+
*/
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/kit-shims",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "kit-shims library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {},
|
|
11
|
+
"peerDependencies": {},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@excom/heft-rig": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"url": "excom-dev/nucleus",
|
|
17
|
+
"directory": "packages/kit-shims"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/kit-shims/support/docs/README.md",
|
|
20
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"kit-shims"
|
|
23
|
+
],
|
|
24
|
+
"excom": {
|
|
25
|
+
"documented": false,
|
|
26
|
+
"packageType": "library"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
30
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
31
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
32
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
33
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { CloseWatcher, requestIdleCb } from "../../index";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
} from "@excom/heft-rig/node_modules/vitest";
|
|
10
|
+
|
|
11
|
+
/** Dispatch a keydown that the polyfill treats as user-triggered. */
|
|
12
|
+
const pressKey = (key: string, { prevent = false } = {}) => {
|
|
13
|
+
const evt = new KeyboardEvent("keydown", { key, cancelable: true });
|
|
14
|
+
// happy-dom does not model `isTrusted`; the polyfill only honours trusted keydowns.
|
|
15
|
+
Object.defineProperty(evt, "isTrusted", { value: true });
|
|
16
|
+
if (prevent) {
|
|
17
|
+
document.addEventListener("keydown", (e) => e.preventDefault(), {
|
|
18
|
+
once: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
document.dispatchEvent(evt);
|
|
22
|
+
return evt;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("index", () => {
|
|
26
|
+
it("re-exports the shims", () => {
|
|
27
|
+
expect(typeof CloseWatcher).toBe("function");
|
|
28
|
+
expect(typeof requestIdleCb).toBe("function");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("CloseWatcher", () => {
|
|
33
|
+
const live: CloseWatcher[] = [];
|
|
34
|
+
const make = (opts?: { signal?: AbortSignal }) => {
|
|
35
|
+
const w = new CloseWatcher(opts);
|
|
36
|
+
live.push(w);
|
|
37
|
+
return w;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
// Empty the module-level stack so tests stay independent.
|
|
46
|
+
while (live.length) live.pop()!.destroy();
|
|
47
|
+
vi.runOnlyPendingTimers();
|
|
48
|
+
vi.useRealTimers();
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("is an EventTarget", () => {
|
|
53
|
+
const w = make();
|
|
54
|
+
expect(w).toBeInstanceOf(EventTarget);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("close() fires close once and then deactivates", () => {
|
|
58
|
+
const w = make();
|
|
59
|
+
const onClose = vi.fn();
|
|
60
|
+
w.addEventListener("close", onClose);
|
|
61
|
+
w.close();
|
|
62
|
+
w.close();
|
|
63
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("requestClose() fires cancel then close", () => {
|
|
67
|
+
const w = make();
|
|
68
|
+
const order: string[] = [];
|
|
69
|
+
w.addEventListener("cancel", () => order.push("cancel"));
|
|
70
|
+
w.addEventListener("close", () => order.push("close"));
|
|
71
|
+
w.requestClose();
|
|
72
|
+
expect(order).toEqual(["cancel", "close"]);
|
|
73
|
+
// Deactivated: subsequent calls are no-ops.
|
|
74
|
+
w.requestClose();
|
|
75
|
+
w.close();
|
|
76
|
+
expect(order).toEqual(["cancel", "close"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("cancel event is cancelable and preventDefault() stops close", () => {
|
|
80
|
+
const w = make();
|
|
81
|
+
const onClose = vi.fn();
|
|
82
|
+
let cancelable = false;
|
|
83
|
+
w.addEventListener("cancel", (e) => {
|
|
84
|
+
cancelable = e.cancelable;
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
});
|
|
87
|
+
w.addEventListener("close", onClose);
|
|
88
|
+
w.requestClose();
|
|
89
|
+
expect(cancelable).toBe(true);
|
|
90
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
91
|
+
// Still active: a later requestClose without preventDefault closes.
|
|
92
|
+
w.addEventListener("cancel", (e) => e.stopImmediatePropagation(), {
|
|
93
|
+
capture: true,
|
|
94
|
+
});
|
|
95
|
+
w.close();
|
|
96
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("re-entrant requestClose() from inside cancel is ignored", () => {
|
|
100
|
+
const w = make();
|
|
101
|
+
const onCancel = vi.fn(() => w.requestClose());
|
|
102
|
+
const onClose = vi.fn();
|
|
103
|
+
w.addEventListener("cancel", onCancel);
|
|
104
|
+
w.addEventListener("close", onClose);
|
|
105
|
+
w.requestClose();
|
|
106
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("destroy() deactivates without firing events", () => {
|
|
111
|
+
const w = make();
|
|
112
|
+
const onCancel = vi.fn();
|
|
113
|
+
const onClose = vi.fn();
|
|
114
|
+
w.addEventListener("cancel", onCancel);
|
|
115
|
+
w.addEventListener("close", onClose);
|
|
116
|
+
w.destroy();
|
|
117
|
+
w.requestClose();
|
|
118
|
+
w.close();
|
|
119
|
+
expect(onCancel).not.toHaveBeenCalled();
|
|
120
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("an already-aborted signal creates an inactive watcher", () => {
|
|
124
|
+
const ac = new AbortController();
|
|
125
|
+
ac.abort();
|
|
126
|
+
const w = make({ signal: ac.signal });
|
|
127
|
+
const onClose = vi.fn();
|
|
128
|
+
w.addEventListener("close", onClose);
|
|
129
|
+
w.requestClose();
|
|
130
|
+
w.close();
|
|
131
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
132
|
+
// Not on the stack: Escape reaches nobody.
|
|
133
|
+
pressKey("Escape");
|
|
134
|
+
vi.runAllTimers();
|
|
135
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("aborting the signal later destroys the watcher", () => {
|
|
139
|
+
const ac = new AbortController();
|
|
140
|
+
const w = make({ signal: ac.signal });
|
|
141
|
+
const onClose = vi.fn();
|
|
142
|
+
w.addEventListener("close", onClose);
|
|
143
|
+
ac.abort();
|
|
144
|
+
w.close();
|
|
145
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("Escape keydown requests close on the top-most watcher only", () => {
|
|
149
|
+
const first = make();
|
|
150
|
+
const second = make();
|
|
151
|
+
const firstClose = vi.fn();
|
|
152
|
+
const secondClose = vi.fn();
|
|
153
|
+
first.addEventListener("close", firstClose);
|
|
154
|
+
second.addEventListener("close", secondClose);
|
|
155
|
+
|
|
156
|
+
pressKey("Escape");
|
|
157
|
+
// Delivered on a queued task so other keydown listeners run first.
|
|
158
|
+
expect(secondClose).not.toHaveBeenCalled();
|
|
159
|
+
vi.runAllTimers();
|
|
160
|
+
expect(secondClose).toHaveBeenCalledTimes(1);
|
|
161
|
+
expect(firstClose).not.toHaveBeenCalled();
|
|
162
|
+
|
|
163
|
+
// The stack unwinds: next Escape hits the first watcher.
|
|
164
|
+
pressKey("Escape");
|
|
165
|
+
vi.runAllTimers();
|
|
166
|
+
expect(firstClose).toHaveBeenCalledTimes(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("destroying a watcher mid-stack removes it from the stack", () => {
|
|
170
|
+
const bottom = make();
|
|
171
|
+
const middle = make();
|
|
172
|
+
const top = make();
|
|
173
|
+
const closes = { bottom: vi.fn(), middle: vi.fn(), top: vi.fn() };
|
|
174
|
+
bottom.addEventListener("close", closes.bottom);
|
|
175
|
+
middle.addEventListener("close", closes.middle);
|
|
176
|
+
top.addEventListener("close", closes.top);
|
|
177
|
+
|
|
178
|
+
middle.destroy();
|
|
179
|
+
pressKey("Escape");
|
|
180
|
+
vi.runAllTimers();
|
|
181
|
+
pressKey("Escape");
|
|
182
|
+
vi.runAllTimers();
|
|
183
|
+
expect(closes.top).toHaveBeenCalledTimes(1);
|
|
184
|
+
expect(closes.bottom).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(closes.middle).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("ignores Escape when another listener prevented default", () => {
|
|
189
|
+
const w = make();
|
|
190
|
+
const onClose = vi.fn();
|
|
191
|
+
w.addEventListener("close", onClose);
|
|
192
|
+
pressKey("Escape", { prevent: true });
|
|
193
|
+
vi.runAllTimers();
|
|
194
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("ignores non-Escape keys and untrusted keydowns", () => {
|
|
198
|
+
const w = make();
|
|
199
|
+
const onClose = vi.fn();
|
|
200
|
+
w.addEventListener("close", onClose);
|
|
201
|
+
pressKey("Enter");
|
|
202
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
203
|
+
vi.runAllTimers();
|
|
204
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("Escape with an empty stack is a no-op", () => {
|
|
208
|
+
expect(() => {
|
|
209
|
+
pressKey("Escape");
|
|
210
|
+
vi.runAllTimers();
|
|
211
|
+
}).not.toThrow();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("oncancel / onclose accessors", () => {
|
|
215
|
+
const w = make();
|
|
216
|
+
const handler = () => {};
|
|
217
|
+
expect(w.oncancel).toBeNull();
|
|
218
|
+
expect(w.onclose).toBeNull();
|
|
219
|
+
w.oncancel = handler as any;
|
|
220
|
+
w.onclose = handler as any;
|
|
221
|
+
w.oncancel = null;
|
|
222
|
+
w.onclose = null;
|
|
223
|
+
expect(w.oncancel).toBeNull();
|
|
224
|
+
expect(w.onclose).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { requestIdleCb } from "../../index";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
vi,
|
|
8
|
+
} from "@excom/heft-rig/node_modules/vitest";
|
|
9
|
+
|
|
10
|
+
describe("requestIdleCb", () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("falls back to setTimeout when requestIdleCallback is missing", () => {
|
|
17
|
+
vi.useFakeTimers();
|
|
18
|
+
const cb = vi.fn();
|
|
19
|
+
const id = requestIdleCb(cb, 10);
|
|
20
|
+
expect(id).toBeTruthy();
|
|
21
|
+
vi.advanceTimersByTime(10);
|
|
22
|
+
expect(cb).toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("provides didTimeout and timeRemaining in fallback", () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
let received: any;
|
|
28
|
+
requestIdleCb((arg) => {
|
|
29
|
+
received = arg;
|
|
30
|
+
}, 5);
|
|
31
|
+
vi.advanceTimersByTime(5);
|
|
32
|
+
expect(received).toBeDefined();
|
|
33
|
+
expect(received.didTimeout).toBe(false);
|
|
34
|
+
expect(typeof received.timeRemaining).toBe("function");
|
|
35
|
+
expect(received.timeRemaining()).toBeGreaterThanOrEqual(0);
|
|
36
|
+
expect(received.timeRemaining()).toBeLessThanOrEqual(50);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses default fallback delay of 1ms", () => {
|
|
40
|
+
vi.useFakeTimers();
|
|
41
|
+
const cb = vi.fn();
|
|
42
|
+
requestIdleCb(cb);
|
|
43
|
+
vi.advanceTimersByTime(0);
|
|
44
|
+
expect(cb).not.toHaveBeenCalled();
|
|
45
|
+
vi.advanceTimersByTime(1);
|
|
46
|
+
expect(cb).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("uses native requestIdleCallback when available", () => {
|
|
50
|
+
const nativeCb = vi.fn((cb) => {
|
|
51
|
+
cb();
|
|
52
|
+
return 99;
|
|
53
|
+
});
|
|
54
|
+
vi.stubGlobal("requestIdleCallback", nativeCb);
|
|
55
|
+
const handler = vi.fn();
|
|
56
|
+
requestIdleCb(handler, 17);
|
|
57
|
+
expect(nativeCb).toHaveBeenCalledWith(handler, { timeout: 17 });
|
|
58
|
+
expect(handler).toHaveBeenCalled();
|
|
59
|
+
vi.unstubAllGlobals();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|