UNPKG

2.87 kBJavaScriptView Raw
1import { options } from 'preact';
2
3/**
4 * Setup a rerender function that will drain the queue of pending renders
5 * @returns {() => void}
6 */
7export function setupRerender() {
8 options.__test__previousDebounce = options.debounceRendering;
9 options.debounceRendering = cb => (options.__test__drainQueue = cb);
10 return () => options.__test__drainQueue && options.__test__drainQueue();
11}
12
13const isThenable = value => value != null && typeof value.then == 'function';
14
15/** Depth of nested calls to `act`. */
16let actDepth = 0;
17
18/**
19 * Run a test function, and flush all effects and rerenders after invoking it.
20 *
21 * Returns a Promise which resolves "immediately" if the callback is
22 * synchronous or when the callback's result resolves if it is asynchronous.
23 *
24 * @param {() => void|Promise<void>} cb The function under test. This may be sync or async.
25 * @return {Promise<void>}
26 */
27export function act(cb) {
28 if (++actDepth > 1) {
29 // If calls to `act` are nested, a flush happens only when the
30 // outermost call returns. In the inner call, we just execute the
31 // callback and return since the infrastructure for flushing has already
32 // been set up.
33 //
34 // If an exception occurs, the outermost `act` will handle cleanup.
35 const result = cb();
36 if (isThenable(result)) {
37 return result.then(() => {
38 --actDepth;
39 });
40 }
41 --actDepth;
42 return Promise.resolve();
43 }
44
45 const previousRequestAnimationFrame = options.requestAnimationFrame;
46 const rerender = setupRerender();
47
48 /** @type {() => void} */
49 let flush, toFlush;
50
51 // Override requestAnimationFrame so we can flush pending hooks.
52 options.requestAnimationFrame = fc => (flush = fc);
53
54 const finish = () => {
55 try {
56 rerender();
57 while (flush) {
58 toFlush = flush;
59 flush = null;
60
61 toFlush();
62 rerender();
63 }
64 teardown();
65 } catch (e) {
66 if (!err) {
67 err = e;
68 }
69 }
70
71 options.requestAnimationFrame = previousRequestAnimationFrame;
72 --actDepth;
73 };
74
75 let err;
76 let result;
77
78 try {
79 result = cb();
80 } catch (e) {
81 err = e;
82 }
83
84 if (isThenable(result)) {
85 return result.then(finish, err => {
86 finish();
87 throw err;
88 });
89 }
90
91 // nb. If the callback is synchronous, effects must be flushed before
92 // `act` returns, so that the caller does not have to await the result,
93 // even though React recommends this.
94 finish();
95 if (err) {
96 throw err;
97 }
98 return Promise.resolve();
99}
100
101/**
102 * Teardown test environment and reset preact's internal state
103 */
104export function teardown() {
105 if (options.__test__drainQueue) {
106 // Flush any pending updates leftover by test
107 options.__test__drainQueue();
108 delete options.__test__drainQueue;
109 }
110
111 if (typeof options.__test__previousDebounce != 'undefined') {
112 options.debounceRendering = options.__test__previousDebounce;
113 delete options.__test__previousDebounce;
114 } else {
115 options.debounceRendering = undefined;
116 }
117}