Skip to content
File

Blob: src/workerd/api/node/tests/timers-global-override-test.js

javascript389 lines
1// Copyright (c) 2017-2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// Tests for the enable_nodejs_global_timers compat flag.
6// When this flag is enabled:
7// - globalThis.setTimeout and globalThis.setInterval return Node.js-compatible
8// Timeout objects instead of numeric IDs
9// - globalThis.setImmediate and globalThis.clearImmediate are available
10 
11import { strictEqual, ok } from 'node:assert';
12 
13// Test that globalThis.setTimeout returns a Timeout object with expected methods
14export const testGlobalSetTimeoutReturnsObject = {
15 async test() {
16 const timeout = globalThis.setTimeout(() => {}, 1000);
17 
18 // Verify it's an object, not a number
19 strictEqual(typeof timeout, 'object', 'setTimeout should return an object');
20 
21 // Verify it has the expected methods
22 strictEqual(
23 typeof timeout.refresh,
24 'function',
25 'Timeout should have refresh method'
26 );
27 strictEqual(
28 typeof timeout.ref,
29 'function',
30 'Timeout should have ref method'
31 );
32 strictEqual(
33 typeof timeout.unref,
34 'function',
35 'Timeout should have unref method'
36 );
37 strictEqual(
38 typeof timeout.hasRef,
39 'function',
40 'Timeout should have hasRef method'
41 );
42 strictEqual(
43 typeof timeout.close,
44 'function',
45 'Timeout should have close method'
46 );
47 
48 // Verify Symbol.toPrimitive allows using as numeric ID for backward compat
49 strictEqual(
50 typeof +timeout,
51 'number',
52 'Timeout should be convertible to number'
53 );
54 ok(+timeout > 0, 'Timeout numeric ID should be positive');
55 
56 // Verify Symbol.dispose is present
57 strictEqual(
58 typeof timeout[Symbol.dispose],
59 'function',
60 'Timeout should have Symbol.dispose method'
61 );
62 
63 // Clean up
64 globalThis.clearTimeout(timeout);
65 },
66};
67 
68// Test that globalThis.setInterval returns a Timeout object with expected methods
69export const testGlobalSetIntervalReturnsObject = {
70 async test() {
71 const interval = globalThis.setInterval(() => {}, 1000);
72 
73 // Verify it's an object, not a number
74 strictEqual(
75 typeof interval,
76 'object',
77 'setInterval should return an object'
78 );
79 
80 // Verify it has the expected methods
81 strictEqual(
82 typeof interval.refresh,
83 'function',
84 'Interval should have refresh method'
85 );
86 strictEqual(
87 typeof interval.ref,
88 'function',
89 'Interval should have ref method'
90 );
91 strictEqual(
92 typeof interval.unref,
93 'function',
94 'Interval should have unref method'
95 );
96 strictEqual(
97 typeof interval.hasRef,
98 'function',
99 'Interval should have hasRef method'
100 );
101 
102 // Verify Symbol.toPrimitive allows using as numeric ID for backward compat
103 strictEqual(
104 typeof +interval,
105 'number',
106 'Interval should be convertible to number'
107 );
108 ok(+interval > 0, 'Interval numeric ID should be positive');
109 
110 // Clean up
111 globalThis.clearInterval(interval);
112 },
113};
114 
115// Test that clearTimeout accepts both Timeout objects and numeric IDs
116export const testClearTimeoutAcceptsBothTypes = {
117 async test() {
118 // Test clearing with Timeout object
119 const t1 = globalThis.setTimeout(() => {
120 throw new Error('timeout1 should have been cleared');
121 }, 100);
122 globalThis.clearTimeout(t1);
123 
124 // Test clearing with numeric ID (backward compatibility via Symbol.toPrimitive)
125 const t2 = globalThis.setTimeout(() => {
126 throw new Error('timeout2 should have been cleared');
127 }, 100);
128 globalThis.clearTimeout(+t2);
129 
130 // Wait to ensure cleared timeouts don't fire
131 await new Promise((r) => globalThis.setTimeout(r, 200));
132 },
133};
134 
135// Test that clearInterval accepts both Timeout objects and numeric IDs
136export const testClearIntervalAcceptsBothTypes = {
137 async test() {
138 // Test clearing with Timeout object
139 const i1 = globalThis.setInterval(() => {
140 throw new Error('interval1 should have been cleared');
141 }, 50);
142 globalThis.clearInterval(i1);
143 
144 // Test clearing with numeric ID (backward compatibility via Symbol.toPrimitive)
145 const i2 = globalThis.setInterval(() => {
146 throw new Error('interval2 should have been cleared');
147 }, 50);
148 globalThis.clearInterval(+i2);
149 
150 // Wait to ensure cleared intervals don't fire
151 await new Promise((r) => globalThis.setTimeout(r, 200));
152 },
153};
154 
155// Test that the refresh() method works correctly
156// NOTE: Using larger timing margins (200ms/100ms instead of 50ms/30ms) because
157// Windows has ~15.6ms timer resolution and significant jitter under load.
158export const testTimeoutRefresh = {
159 async test() {
160 let callCount = 0;
161 
162 // Create a timeout that would fire in 200ms
163 const timeout = globalThis.setTimeout(() => {
164 callCount++;
165 }, 200);
166 
167 // Wait 100ms, then refresh (reset the timer)
168 await new Promise((r) => globalThis.setTimeout(r, 100));
169 timeout.refresh();
170 
171 // Wait another 100ms (total 200ms from start, but only 100ms from refresh)
172 await new Promise((r) => globalThis.setTimeout(r, 100));
173 
174 // The callback shouldn't have fired yet since we refreshed
175 strictEqual(
176 callCount,
177 0,
178 'Callback should not have fired yet after refresh'
179 );
180 
181 // Wait another 150ms for the refreshed timer to fire (with margin)
182 await new Promise((r) => globalThis.setTimeout(r, 150));
183 
184 // Now it should have fired
185 strictEqual(callCount, 1, 'Callback should have fired once');
186 
187 // Clean up (already fired, but good practice)
188 globalThis.clearTimeout(timeout);
189 },
190};
191 
192// Test ref() and unref() methods (no-ops but should work without errors)
193export const testRefUnref = {
194 async test() {
195 const timeout = globalThis.setTimeout(() => {}, 1000);
196 
197 // Initially, hasRef should return true (default behavior)
198 strictEqual(timeout.hasRef(), true, 'hasRef should return true by default');
199 
200 // unref() should return the timeout object (chainable)
201 const unrefResult = timeout.unref();
202 strictEqual(unrefResult, timeout, 'unref should return the timeout object');
203 strictEqual(
204 timeout.hasRef(),
205 false,
206 'hasRef should return false after unref'
207 );
208 
209 // ref() should return the timeout object (chainable)
210 const refResult = timeout.ref();
211 strictEqual(refResult, timeout, 'ref should return the timeout object');
212 strictEqual(timeout.hasRef(), true, 'hasRef should return true after ref');
213 
214 // Clean up
215 globalThis.clearTimeout(timeout);
216 },
217};
218 
219// Test that close() method clears the timeout
220export const testClose = {
221 async test() {
222 let called = false;
223 const timeout = globalThis.setTimeout(() => {
224 called = true;
225 }, 50);
226 
227 // close() should return the timeout object (chainable)
228 const closeResult = timeout.close();
229 strictEqual(closeResult, timeout, 'close should return the timeout object');
230 
231 // Wait to ensure the callback doesn't fire
232 await new Promise((r) => globalThis.setTimeout(r, 100));
233 strictEqual(
234 called,
235 false,
236 'Callback should not have been called after close'
237 );
238 },
239};
240 
241// Test that setTimeout works with callback arguments
242export const testSetTimeoutWithArgs = {
243 async test() {
244 const { promise, resolve } = Promise.withResolvers();
245 
246 globalThis.setTimeout(
247 (a, b, c) => {
248 strictEqual(a, 1, 'First arg should be 1');
249 strictEqual(b, 'hello', 'Second arg should be "hello"');
250 strictEqual(c, true, 'Third arg should be true');
251 resolve();
252 },
253 10,
254 1,
255 'hello',
256 true
257 );
258 
259 await promise;
260 },
261};
262 
263// Test that setInterval works with callback arguments
264export const testSetIntervalWithArgs = {
265 async test() {
266 const { promise, resolve } = Promise.withResolvers();
267 
268 const interval = globalThis.setInterval(
269 (a, b) => {
270 strictEqual(a, 'foo', 'First arg should be "foo"');
271 strictEqual(b, 42, 'Second arg should be 42');
272 globalThis.clearInterval(interval);
273 resolve();
274 },
275 10,
276 'foo',
277 42
278 );
279 
280 await promise;
281 },
282};
283 
284// Test Symbol.dispose functionality
285export const testSymbolDispose = {
286 async test() {
287 let called = false;
288 const timeout = globalThis.setTimeout(() => {
289 called = true;
290 }, 50);
291 
292 // Use Symbol.dispose to clean up
293 timeout[Symbol.dispose]();
294 
295 // Wait to ensure the callback doesn't fire
296 await new Promise((r) => globalThis.setTimeout(r, 100));
297 strictEqual(
298 called,
299 false,
300 'Callback should not have been called after dispose'
301 );
302 },
303};
304 
305// Test that globalThis.setImmediate is available and returns an Immediate object
306export const testGlobalSetImmediateAvailable = {
307 async test() {
308 strictEqual(
309 typeof globalThis.setImmediate,
310 'function',
311 'setImmediate should be available on globalThis'
312 );
313 strictEqual(
314 typeof globalThis.clearImmediate,
315 'function',
316 'clearImmediate should be available on globalThis'
317 );
318 
319 const { promise, resolve } = Promise.withResolvers();
320 
321 const immediate = globalThis.setImmediate(() => {
322 resolve();
323 });
324 
325 // Verify it's an object with expected methods
326 strictEqual(
327 typeof immediate,
328 'object',
329 'setImmediate should return an object'
330 );
331 strictEqual(
332 typeof immediate.ref,
333 'function',
334 'Immediate should have ref method'
335 );
336 strictEqual(
337 typeof immediate.unref,
338 'function',
339 'Immediate should have unref method'
340 );
341 strictEqual(
342 typeof immediate.hasRef,
343 'function',
344 'Immediate should have hasRef method'
345 );
346 
347 await promise;
348 },
349};
350 
351// Test that clearImmediate works correctly
352export const testGlobalClearImmediate = {
353 async test() {
354 let called = false;
355 const immediate = globalThis.setImmediate(() => {
356 called = true;
357 });
358 
359 globalThis.clearImmediate(immediate);
360 
361 // Wait a bit to ensure the callback doesn't fire
362 await new Promise((r) => globalThis.setTimeout(r, 50));
363 strictEqual(
364 called,
365 false,
366 'Callback should not have been called after clearImmediate'
367 );
368 },
369};
370 
371// Test that setImmediate works with callback arguments
372export const testSetImmediateWithArgs = {
373 async test() {
374 const { promise, resolve } = Promise.withResolvers();
375 
376 globalThis.setImmediate(
377 (a, b) => {
378 strictEqual(a, 'test', 'First arg should be "test"');
379 strictEqual(b, 123, 'Second arg should be 123');
380 resolve();
381 },
382 'test',
383 123
384 );
385 
386 await promise;
387 },
388};