├── .gitignore
├── LICENSE
├── README.md
├── jest.config.js
├── lib
├── TestComponent.d.ts
├── TestComponent.js
├── _types.d.ts
├── _types.js
├── asyncUtils.d.ts
├── asyncUtils.js
├── cleanup.d.ts
├── cleanup.js
├── flush-microtasks.d.ts
├── flush-microtasks.js
├── index.d.ts
├── index.js
├── renderHook.d.ts
├── renderHook.js
├── resultContainer.d.ts
└── resultContainer.js
├── package.json
├── src
├── TestComponent.tsx
├── _types.ts
├── asyncUtils.ts
├── cleanup.ts
├── flush-microtasks.ts
├── index.ts
├── renderHook.tsx
└── resultContainer.ts
├── test
├── asyncHook.test.ts
├── autoCleanup.disabled.test.ts
├── autoCleanup.noAfterEach.test.ts
├── autoCleanup.test.ts
├── cleanup.test.ts
├── customHook.test.ts
├── errorHook.test.ts
├── suspenseHook.test.ts
├── useContext.test.tsx
├── useEffect.test.ts
├── useMemo.test.ts
├── useReducer.test.ts
├── useRef.test.ts
└── useState.test.ts
├── tsconfig.json
├── types.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 | out
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and not Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | # Stores VSCode versions used for testing VSCode extensions
109 | .vscode-test
110 |
111 | # yarn v2
112 |
113 | .yarn/cache
114 | .yarn/unplugged
115 | .yarn/build-state.yml
116 | .pnp.*
117 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright 2020 trivago, N.V.
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED: Moved to [@testing-library/preact-hooks](https://github.com/testing-library/react-hooks-testing-library)
2 |
3 | # preact-hooks-testing-library
4 |
5 | preact port of the the [@testing-library/react-hooks](https://github.com/testing-library/react-hooks-testing-library) library.
6 |
7 | ## Why not `@testing-library/react-hooks`?
8 |
9 | Currently, due to the use of `react-test-renderer`, the react hooks testing library most likely will never be compatible with preact.
10 |
11 | ## Why not another library?
12 |
13 | At the time of writing, a library did not exist to test preact hooks.
14 |
15 | ## When to use this library
16 |
17 | 1. You're writing a library with one or more custom hooks that are not directly tied to a component
18 | 2. You have a complex hook that is difficult to test through component interactions
19 |
20 | ## When not to use this library
21 |
22 | 1. Your hook is defined alongside a component and is only used there
23 | 2. Your hook is easy to test by just testing the components using it
24 |
25 | ## Installation
26 |
27 | Install with your favorite package manager
28 |
29 | ```
30 | yarn add -D @trivago/preact-hooks-testing-library
31 | OR
32 | npm install --save-dev @trivago/preact-hooks-testing-library
33 | ```
34 |
35 | ## Example #1: Basic
36 | ---
37 |
38 | ### `useCounter.ts`
39 |
40 | ```typescript
41 | import { useState, useCallback } from 'react'; // aliased to preact
42 |
43 | const useCounter = () => {
44 | const [count, setCount] = useState(0);
45 |
46 | const increment = useCallback(() => setCount(c => c + 1));
47 |
48 | return {
49 | count,
50 | increment
51 | }
52 | }
53 |
54 | export default useCounter;
55 | ```
56 |
57 | ### `useCounter.test.ts`
58 |
59 | ```typescript
60 | import { renderHook, act } from 'preact-hooks-testing-library';
61 | import useCounter from './useCounter';
62 |
63 | test('should increment counter', () => {
64 | const { result } = renderHook(() => useCounter());
65 |
66 | act(() => {
67 | result.current.increment();
68 | });
69 |
70 | expect(result.current.count).toBe(1);
71 | });
72 |
73 | ```
74 |
75 | ## Example #2: Wrapped Components
76 |
77 | Sometimes, hooks may need access to values or functionality outside of itself that are provided by a context provider or some other HOC.
78 |
79 | ```typescript jsx
80 | import { createContext } from 'preact'
81 | import { useState, useCallback, useContext } from 'preact/hooks'
82 |
83 | const CounterStepContext = createContext(1)
84 | export const CounterStepProvider = ({ step, children }) => (
85 | {children}
86 | )
87 | export function useCounter(initialValue = 0) {
88 | const [count, setCount] = useState(initialValue)
89 | const step = useContext(CounterStepContext)
90 | const increment = useCallback(() => setCount((x) => x + step), [step])
91 | const reset = useCallback(() => setCount(initialValue), [initialValue])
92 | return { count, increment, reset }
93 | }
94 |
95 | ```
96 |
97 | In our test, we simply use CoounterStepProvider as the wrapper when rendering the hook:
98 |
99 | ```typescript
100 | import { renderHook, act } from 'preact-hooks-testing-library'
101 | import { CounterStepProvider, useCounter } from './counter'
102 |
103 | test('should use custom step when incrementing', () => {
104 | const wrapper = ({ children }) => {children}
105 | const { result } = renderHook(() => useCounter(), { wrapper })
106 | act(() => {
107 | result.current.increment()
108 | })
109 | expect(result.current.count).toBe(2)
110 | })
111 | ```
112 |
113 | ### TODO
114 |
115 | - [ ] remove `@ts-nocheck` flag from tests
116 | - [ ] fix disabled auto clean up tests
117 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | };
4 |
--------------------------------------------------------------------------------
/lib/TestComponent.d.ts:
--------------------------------------------------------------------------------
1 | import { Callback } from "./_types";
2 | export interface TestComponentProps
{
3 | callback: Callback
;
4 | hookProps?: P;
5 | children: (value: R) => void;
6 | onError: (error: Error) => void;
7 | }
8 | declare const TestComponent:
({ callback, hookProps, children, onError, }: TestComponentProps
) => null;
9 | export declare const Fallback: () => null;
10 | export default TestComponent;
11 |
--------------------------------------------------------------------------------
/lib/TestComponent.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.Fallback = void 0;
4 | var TestComponent = function (_a) {
5 | var callback = _a.callback, hookProps = _a.hookProps, children = _a.children, onError = _a.onError;
6 | try {
7 | var val = callback(hookProps);
8 | children(val);
9 | }
10 | catch (err) {
11 | if (err.then) {
12 | throw err;
13 | }
14 | else {
15 | onError(err);
16 | }
17 | }
18 | return null;
19 | };
20 | exports.Fallback = function () { return null; };
21 | exports.default = TestComponent;
22 |
--------------------------------------------------------------------------------
/lib/_types.d.ts:
--------------------------------------------------------------------------------
1 | export declare type Callback
= (props?: P) => R;
2 | export declare type ResolverType = () => void;
3 |
--------------------------------------------------------------------------------
/lib/_types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/lib/asyncUtils.d.ts:
--------------------------------------------------------------------------------
1 | import { ResolverType } from "./_types";
2 | export interface TimeoutOptions {
3 | timeout?: number;
4 | suppressErrors?: boolean;
5 | }
6 | declare function asyncUtils(addResolver: (resolver: ResolverType) => void): {
7 | waitForValueToChange: (selector: () => any, options?: TimeoutOptions) => Promise;
8 | waitForNextUpdate: (options?: TimeoutOptions) => Promise;
9 | wait: (callback: () => any, options?: TimeoutOptions) => Promise;
10 | };
11 | export default asyncUtils;
12 |
--------------------------------------------------------------------------------
/lib/asyncUtils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __assign = (this && this.__assign) || function () {
16 | __assign = Object.assign || function(t) {
17 | for (var s, i = 1, n = arguments.length; i < n; i++) {
18 | s = arguments[i];
19 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
20 | t[p] = s[p];
21 | }
22 | return t;
23 | };
24 | return __assign.apply(this, arguments);
25 | };
26 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
27 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
28 | return new (P || (P = Promise))(function (resolve, reject) {
29 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
30 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
31 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
32 | step((generator = generator.apply(thisArg, _arguments || [])).next());
33 | });
34 | };
35 | var __generator = (this && this.__generator) || function (thisArg, body) {
36 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
37 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
38 | function verb(n) { return function (v) { return step([n, v]); }; }
39 | function step(op) {
40 | if (f) throw new TypeError("Generator is already executing.");
41 | while (_) try {
42 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
43 | if (y = 0, t) op = [op[0] & 2, t.value];
44 | switch (op[0]) {
45 | case 0: case 1: t = op; break;
46 | case 4: _.label++; return { value: op[1], done: false };
47 | case 5: _.label++; y = op[1]; op = [0]; continue;
48 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
49 | default:
50 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
51 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
52 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
53 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
54 | if (t[2]) _.ops.pop();
55 | _.trys.pop(); continue;
56 | }
57 | op = body.call(thisArg, _);
58 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
59 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
60 | }
61 | };
62 | Object.defineProperty(exports, "__esModule", { value: true });
63 | var TimeoutError = /** @class */ (function (_super) {
64 | __extends(TimeoutError, _super);
65 | function TimeoutError(utilName, _a) {
66 | var timeout = _a.timeout;
67 | var _this = _super.call(this, "Timed out in " + utilName + " after " + timeout + "ms.") || this;
68 | _this.timeout = true;
69 | return _this;
70 | }
71 | return TimeoutError;
72 | }(Error));
73 | function asyncUtils(addResolver) {
74 | var nextUpdatePromise;
75 | function waitForNextUpdate(options) {
76 | if (options === void 0) { options = { timeout: 0 }; }
77 | return __awaiter(this, void 0, void 0, function () {
78 | return __generator(this, function (_a) {
79 | switch (_a.label) {
80 | case 0:
81 | if (!nextUpdatePromise) {
82 | nextUpdatePromise = new Promise(function (resolve, reject) {
83 | var timeoutId = options.timeout > 0
84 | ? setTimeout(function () {
85 | reject(new TimeoutError("waitForNextUpdate", options));
86 | }, options.timeout)
87 | : null;
88 | addResolver(function () {
89 | if (timeoutId) {
90 | clearTimeout(timeoutId);
91 | }
92 | nextUpdatePromise = undefined;
93 | resolve();
94 | });
95 | });
96 | }
97 | return [4 /*yield*/, nextUpdatePromise];
98 | case 1:
99 | _a.sent();
100 | return [2 /*return*/];
101 | }
102 | });
103 | });
104 | }
105 | function wait(callback, options) {
106 | if (options === void 0) { options = { timeout: 0, suppressErrors: true }; }
107 | return __awaiter(this, void 0, void 0, function () {
108 | var checkResult, waitForResult;
109 | var _this = this;
110 | return __generator(this, function (_a) {
111 | switch (_a.label) {
112 | case 0:
113 | checkResult = function () {
114 | try {
115 | var callbackResult = callback();
116 | return callbackResult || callbackResult === undefined;
117 | }
118 | catch (err) {
119 | if (!options.suppressErrors) {
120 | throw err;
121 | }
122 | }
123 | };
124 | waitForResult = function () { return __awaiter(_this, void 0, void 0, function () {
125 | var initialTimeout, startTime, err_1;
126 | return __generator(this, function (_a) {
127 | switch (_a.label) {
128 | case 0:
129 | initialTimeout = options.timeout;
130 | _a.label = 1;
131 | case 1:
132 | if (!true) return [3 /*break*/, 6];
133 | startTime = Date.now();
134 | _a.label = 2;
135 | case 2:
136 | _a.trys.push([2, 4, , 5]);
137 | return [4 /*yield*/, waitForNextUpdate({ timeout: options.timeout })];
138 | case 3:
139 | _a.sent();
140 | if (checkResult()) {
141 | return [2 /*return*/];
142 | }
143 | return [3 /*break*/, 5];
144 | case 4:
145 | err_1 = _a.sent();
146 | if (err_1.timeout) {
147 | throw new TimeoutError("wait", { timeout: initialTimeout });
148 | }
149 | throw err_1;
150 | case 5:
151 | options.timeout -= Date.now() - startTime;
152 | return [3 /*break*/, 1];
153 | case 6: return [2 /*return*/];
154 | }
155 | });
156 | }); };
157 | if (!!checkResult()) return [3 /*break*/, 2];
158 | return [4 /*yield*/, waitForResult()];
159 | case 1:
160 | _a.sent();
161 | _a.label = 2;
162 | case 2: return [2 /*return*/];
163 | }
164 | });
165 | });
166 | }
167 | function waitForValueToChange(selector, options) {
168 | if (options === void 0) { options = { timeout: 0 }; }
169 | return __awaiter(this, void 0, void 0, function () {
170 | var initialValue, err_2;
171 | return __generator(this, function (_a) {
172 | switch (_a.label) {
173 | case 0:
174 | initialValue = selector();
175 | _a.label = 1;
176 | case 1:
177 | _a.trys.push([1, 3, , 4]);
178 | return [4 /*yield*/, wait(function () { return selector() !== initialValue; }, __assign({ suppressErrors: false }, options))];
179 | case 2:
180 | _a.sent();
181 | return [3 /*break*/, 4];
182 | case 3:
183 | err_2 = _a.sent();
184 | if (err_2.timeout) {
185 | throw new TimeoutError("waitForValueToChange", options);
186 | }
187 | throw err_2;
188 | case 4: return [2 /*return*/];
189 | }
190 | });
191 | });
192 | }
193 | return {
194 | waitForValueToChange: waitForValueToChange,
195 | waitForNextUpdate: waitForNextUpdate,
196 | wait: wait,
197 | };
198 | }
199 | exports.default = asyncUtils;
200 | // function asyncUtils(addResolver: (r: ResolverType) => void) {
201 | // let nextUpdatePromise: Promise | null = null;
202 | // const waitForNextUpdate = async (
203 | // options: TimeoutOptions = { timeout: 0 }
204 | // ) => {
205 | // if (!nextUpdatePromise) {
206 | // nextUpdatePromise = new Promise((resolve, reject) => {
207 | // let timeoutId: NodeJS.Timeout;
208 | // if (options.timeout && options.timeout > 0) {
209 | // timeoutId = setTimeout(
210 | // () => reject(new TimeoutError("waitForNextUpdate", options)),
211 | // options.timeout
212 | // );
213 | // }
214 | // addResolver(() => {
215 | // clearTimeout(timeoutId);
216 | // nextUpdatePromise = null;
217 | // resolve();
218 | // });
219 | // });
220 | // await act(() => {
221 | // if (nextUpdatePromise) {
222 | // return nextUpdatePromise;
223 | // }
224 | // return;
225 | // });
226 | // }
227 | // await nextUpdatePromise;
228 | // };
229 | // const wait = async (
230 | // callback: () => any,
231 | // { timeout, suppressErrors }: WaitOptions = {
232 | // timeout: 0,
233 | // suppressErrors: true,
234 | // }
235 | // ) => {
236 | // const checkResult = () => {
237 | // try {
238 | // const callbackResult = callback();
239 | // return callbackResult || callbackResult === undefined;
240 | // } catch (e) {
241 | // if (!suppressErrors) {
242 | // throw e;
243 | // }
244 | // }
245 | // };
246 | // const waitForResult = async () => {
247 | // const initialTimeout = timeout;
248 | // while (true) {
249 | // const startTime = Date.now();
250 | // try {
251 | // await waitForNextUpdate({ timeout });
252 | // if (checkResult()) {
253 | // return;
254 | // }
255 | // } catch (e) {
256 | // if (e.timeout) {
257 | // throw new TimeoutError("wait", { timeout: initialTimeout });
258 | // }
259 | // throw e;
260 | // }
261 | // timeout -= Date.now() - startTime;
262 | // }
263 | // };
264 | // if (!checkResult()) {
265 | // await waitForResult();
266 | // }
267 | // };
268 | // const waitForValueToChange = async (
269 | // selector: () => any,
270 | // options: TimeoutOptions = {
271 | // timeout: 0,
272 | // }
273 | // ) => {
274 | // const initialValue = selector();
275 | // try {
276 | // await wait(() => selector() !== initialValue, {
277 | // suppressErrors: false,
278 | // ...options,
279 | // });
280 | // } catch (e) {
281 | // if (e.timeout) {
282 | // throw new TimeoutError("waitForValueToChange", options);
283 | // }
284 | // throw e;
285 | // }
286 | // };
287 | // return {
288 | // wait,
289 | // waitForNextUpdate,
290 | // waitForValueToChange,
291 | // };
292 | // }
293 | // export default asyncUtils;
294 |
--------------------------------------------------------------------------------
/lib/cleanup.d.ts:
--------------------------------------------------------------------------------
1 | declare type CleanupCallback = () => void;
2 | export declare function cleanup(): Promise;
3 | export declare function addCleanup(callback: CleanupCallback): void;
4 | export declare function removeCleanup(callback: CleanupCallback): void;
5 | export {};
6 |
--------------------------------------------------------------------------------
/lib/cleanup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __generator = (this && this.__generator) || function (thisArg, body) {
12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 | function verb(n) { return function (v) { return step([n, v]); }; }
15 | function step(op) {
16 | if (f) throw new TypeError("Generator is already executing.");
17 | while (_) try {
18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 | if (y = 0, t) op = [op[0] & 2, t.value];
20 | switch (op[0]) {
21 | case 0: case 1: t = op; break;
22 | case 4: _.label++; return { value: op[1], done: false };
23 | case 5: _.label++; y = op[1]; op = [0]; continue;
24 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 | default:
26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 | if (t[2]) _.ops.pop();
31 | _.trys.pop(); continue;
32 | }
33 | op = body.call(thisArg, _);
34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 | }
37 | };
38 | var __importDefault = (this && this.__importDefault) || function (mod) {
39 | return (mod && mod.__esModule) ? mod : { "default": mod };
40 | };
41 | Object.defineProperty(exports, "__esModule", { value: true });
42 | exports.removeCleanup = exports.addCleanup = exports.cleanup = void 0;
43 | var flush_microtasks_1 = __importDefault(require("./flush-microtasks"));
44 | var cleanupCallbacks = new Set();
45 | function cleanup() {
46 | return __awaiter(this, void 0, void 0, function () {
47 | return __generator(this, function (_a) {
48 | switch (_a.label) {
49 | case 0: return [4 /*yield*/, flush_microtasks_1.default()];
50 | case 1:
51 | _a.sent();
52 | cleanupCallbacks.forEach(function (cb) { return cb(); });
53 | cleanupCallbacks.clear();
54 | return [2 /*return*/];
55 | }
56 | });
57 | });
58 | }
59 | exports.cleanup = cleanup;
60 | function addCleanup(callback) {
61 | cleanupCallbacks.add(callback);
62 | }
63 | exports.addCleanup = addCleanup;
64 | function removeCleanup(callback) {
65 | cleanupCallbacks.delete(callback);
66 | }
67 | exports.removeCleanup = removeCleanup;
68 |
--------------------------------------------------------------------------------
/lib/flush-microtasks.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * copied from React's enqueueTask.js
3 | * copied again from React Testing Library's flush-microtasks.js
4 | */
5 | export default function flushMicroTasks(): Promise;
6 |
--------------------------------------------------------------------------------
/lib/flush-microtasks.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // @ts-nocheck
3 | // the part of this file that we need tested is definitely being run
4 | // and the part that is not cannot easily have useful tests written
5 | // anyway. So we're just going to ignore coverage for this file
6 | /**
7 | * copied from React's enqueueTask.js
8 | * copied again from React Testing Library's flush-microtasks.js
9 | */
10 | Object.defineProperty(exports, "__esModule", { value: true });
11 | var didWarnAboutMessageChannel = false;
12 | var enqueueTask;
13 | try {
14 | // read require off the module object to get around the bundlers.
15 | // we don't want them to detect a require and bundle a Node polyfill.
16 | var requireString = ("require" + Math.random()).slice(0, 7);
17 | var nodeRequire = module && module[requireString];
18 | // assuming we're in node, let's try to get node's
19 | // version of setImmediate, bypassing fake timers if any.
20 | enqueueTask = nodeRequire("timers").setImmediate;
21 | }
22 | catch (_err) {
23 | // we're in a browser
24 | // we can't use regular timers because they may still be faked
25 | // so we try MessageChannel+postMessage instead
26 | enqueueTask = function (callback) {
27 | var supportsMessageChannel = typeof MessageChannel === "function";
28 | if (supportsMessageChannel) {
29 | var channel = new MessageChannel();
30 | channel.port1.onmessage = callback;
31 | channel.port2.postMessage(undefined);
32 | }
33 | else if (didWarnAboutMessageChannel === false) {
34 | didWarnAboutMessageChannel = true;
35 | // eslint-disable-next-line no-console
36 | console.error("This browser does not have a MessageChannel implementation, " +
37 | "so enqueuing tasks via await act(async () => ...) will fail. " +
38 | "Please file an issue at https://github.com/facebook/react/issues " +
39 | "if you encounter this warning.");
40 | }
41 | };
42 | }
43 | function flushMicroTasks() {
44 | return new Promise(function (resolve) { return enqueueTask(resolve); });
45 | }
46 | exports.default = flushMicroTasks;
47 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from "./renderHook";
2 | import { act } from "@testing-library/preact";
3 | import { cleanup } from "./cleanup";
4 | export { renderHook, act, cleanup };
5 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __generator = (this && this.__generator) || function (thisArg, body) {
12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 | function verb(n) { return function (v) { return step([n, v]); }; }
15 | function step(op) {
16 | if (f) throw new TypeError("Generator is already executing.");
17 | while (_) try {
18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 | if (y = 0, t) op = [op[0] & 2, t.value];
20 | switch (op[0]) {
21 | case 0: case 1: t = op; break;
22 | case 4: _.label++; return { value: op[1], done: false };
23 | case 5: _.label++; y = op[1]; op = [0]; continue;
24 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 | default:
26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 | if (t[2]) _.ops.pop();
31 | _.trys.pop(); continue;
32 | }
33 | op = body.call(thisArg, _);
34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 | }
37 | };
38 | Object.defineProperty(exports, "__esModule", { value: true });
39 | exports.cleanup = exports.act = exports.renderHook = void 0;
40 | /* globals afterEach */
41 | var renderHook_1 = require("./renderHook");
42 | Object.defineProperty(exports, "renderHook", { enumerable: true, get: function () { return renderHook_1.renderHook; } });
43 | var preact_1 = require("@testing-library/preact");
44 | Object.defineProperty(exports, "act", { enumerable: true, get: function () { return preact_1.act; } });
45 | var cleanup_1 = require("./cleanup");
46 | Object.defineProperty(exports, "cleanup", { enumerable: true, get: function () { return cleanup_1.cleanup; } });
47 | // @ts-ignore
48 | if (typeof afterEach === "function" && !process.env.PHTL_SKIP_AUTO_CLEANUP) {
49 | // @ts-ignore
50 | afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
51 | return __generator(this, function (_a) {
52 | switch (_a.label) {
53 | case 0: return [4 /*yield*/, cleanup_1.cleanup()];
54 | case 1:
55 | _a.sent();
56 | return [2 /*return*/];
57 | }
58 | });
59 | }); });
60 | }
61 |
--------------------------------------------------------------------------------
/lib/renderHook.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from "preact";
2 | import { Callback } from "./_types";
3 | export interface RenderHookOptions {
4 | initialProps?: P;
5 | wrapper?: ComponentType;
6 | }
7 | export declare function renderHook
(callback: Callback
, { initialProps, wrapper }?: RenderHookOptions
): {
8 | waitForValueToChange: (selector: () => any, options?: import("./asyncUtils").TimeoutOptions) => Promise;
9 | waitForNextUpdate: (options?: import("./asyncUtils").TimeoutOptions) => Promise;
10 | wait: (callback: () => any, options?: import("./asyncUtils").TimeoutOptions) => Promise;
11 | result: {
12 | readonly current: R;
13 | readonly error: Error;
14 | };
15 | rerender: (newProps?: P | undefined) => void;
16 | unmount: () => void;
17 | };
18 |
--------------------------------------------------------------------------------
/lib/renderHook.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __assign = (this && this.__assign) || function () {
3 | __assign = Object.assign || function(t) {
4 | for (var s, i = 1, n = arguments.length; i < n; i++) {
5 | s = arguments[i];
6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7 | t[p] = s[p];
8 | }
9 | return t;
10 | };
11 | return __assign.apply(this, arguments);
12 | };
13 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14 | if (k2 === undefined) k2 = k;
15 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
16 | }) : (function(o, m, k, k2) {
17 | if (k2 === undefined) k2 = k;
18 | o[k2] = m[k];
19 | }));
20 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21 | Object.defineProperty(o, "default", { enumerable: true, value: v });
22 | }) : function(o, v) {
23 | o["default"] = v;
24 | });
25 | var __importStar = (this && this.__importStar) || function (mod) {
26 | if (mod && mod.__esModule) return mod;
27 | var result = {};
28 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
29 | __setModuleDefault(result, mod);
30 | return result;
31 | };
32 | var __importDefault = (this && this.__importDefault) || function (mod) {
33 | return (mod && mod.__esModule) ? mod : { "default": mod };
34 | };
35 | Object.defineProperty(exports, "__esModule", { value: true });
36 | exports.renderHook = void 0;
37 | var preact_1 = require("preact");
38 | var compat_1 = require("preact/compat");
39 | var preact_2 = require("@testing-library/preact");
40 | var resultContainer_1 = __importDefault(require("./resultContainer"));
41 | var TestComponent_1 = __importStar(require("./TestComponent"));
42 | var cleanup_1 = require("./cleanup");
43 | var asyncUtils_1 = __importDefault(require("./asyncUtils"));
44 | var defaultWrapper = function (Component) { return preact_1.h(Component, null); };
45 | function renderHook(callback, _a) {
46 | var _b = _a === void 0 ? {} : _a, initialProps = _b.initialProps, wrapper = _b.wrapper;
47 | var _c = resultContainer_1.default(), result = _c.result, setValue = _c.setValue, setError = _c.setError, addResolver = _c.addResolver;
48 | var hookProps = {
49 | current: initialProps,
50 | };
51 | var wrapUiIfNeeded = function (innerElement) {
52 | return wrapper ? preact_1.h(wrapper, null, innerElement) : innerElement;
53 | };
54 | var TestHook = function () {
55 | return wrapUiIfNeeded(preact_1.h(compat_1.Suspense, { fallback: preact_1.h(TestComponent_1.Fallback, null) },
56 | preact_1.h(TestComponent_1.default, { callback: callback, hookProps: hookProps.current, onError: setError }, setValue)));
57 | };
58 | var _d = preact_2.render(preact_1.h(TestHook, null)), unmount = _d.unmount, rerender = _d.rerender;
59 | function rerenderHook(newProps) {
60 | if (newProps === void 0) { newProps = hookProps.current; }
61 | hookProps.current = newProps;
62 | preact_2.act(function () {
63 | rerender(preact_1.h(TestHook, null));
64 | });
65 | }
66 | function unmountHook() {
67 | preact_2.act(function () {
68 | cleanup_1.removeCleanup(unmountHook);
69 | unmount();
70 | });
71 | }
72 | cleanup_1.addCleanup(unmountHook);
73 | return __assign({ result: result, rerender: rerenderHook, unmount: unmountHook }, asyncUtils_1.default(addResolver));
74 | }
75 | exports.renderHook = renderHook;
76 |
--------------------------------------------------------------------------------
/lib/resultContainer.d.ts:
--------------------------------------------------------------------------------
1 | import { ResolverType } from "./_types";
2 | declare function resultContainer(initialValue?: R): {
3 | result: {
4 | readonly current: R;
5 | readonly error: Error;
6 | };
7 | setValue: (val: R) => void;
8 | setError: (err: Error) => void;
9 | addResolver: (resolver: ResolverType) => void;
10 | };
11 | export default resultContainer;
12 |
--------------------------------------------------------------------------------
/lib/resultContainer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | function resultContainer(initialValue) {
4 | var value = initialValue;
5 | var error;
6 | var resolvers = [];
7 | var result = {
8 | get current() {
9 | if (error) {
10 | throw error;
11 | }
12 | return value;
13 | },
14 | get error() {
15 | return error;
16 | },
17 | };
18 | function updateResult(val, err) {
19 | value = val ? val : value;
20 | error = err ? err : error;
21 | resolvers.splice(0, resolvers.length).forEach(function (resolve) { return resolve(); });
22 | }
23 | return {
24 | result: result,
25 | setValue: function (val) { return updateResult(val); },
26 | setError: function (err) { return updateResult(undefined, err); },
27 | addResolver: function (resolver) {
28 | resolvers.push(resolver);
29 | },
30 | };
31 | }
32 | exports.default = resultContainer;
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@trivago/preact-hooks-testing-library",
3 | "description": "preact port of @testing-library/react-hooks",
4 | "version": "1.0.5",
5 | "main": "lib/index.js",
6 | "license": "MIT",
7 | "author": "Carson McKinstry ",
8 | "homepage": "https://github.com/trivago/preact-hooks-testing-library#readme",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/trivago/preact-hooks-testing-library"
12 | },
13 | "scripts": {
14 | "prepare": "npm run build",
15 | "prebuild": "npm run cleanup; npm t",
16 | "cleanup": "rimraf ./lib",
17 | "build": "tsc",
18 | "test": "jest"
19 | },
20 | "devDependencies": {
21 | "@types/jest": "^25.2.2",
22 | "jest": "^25",
23 | "rimraf": "^3.0.2",
24 | "ts-jest": "^25.5.1",
25 | "typescript": "^3.9.2"
26 | },
27 | "peerDependencies": {
28 | "@testing-library/preact": "^1.0.2",
29 | "preact": "^10.4.1"
30 | },
31 | "dependencies": {}
32 | }
33 |
--------------------------------------------------------------------------------
/src/TestComponent.tsx:
--------------------------------------------------------------------------------
1 | import { Callback } from "./_types";
2 |
3 | export interface TestComponentProps {
4 | callback: Callback
;
5 | hookProps?: P;
6 | children: (value: R) => void;
7 | onError: (error: Error) => void;
8 | }
9 |
10 | const TestComponent =
({
11 | callback,
12 | hookProps,
13 | children,
14 | onError,
15 | }: TestComponentProps
) => {
16 | try {
17 | const val = callback(hookProps);
18 | children(val);
19 | } catch (err) {
20 | if (err.then) {
21 | throw err;
22 | } else {
23 | onError(err);
24 | }
25 | }
26 |
27 | return null;
28 | };
29 |
30 | export const Fallback = () => null;
31 |
32 | export default TestComponent;
33 |
--------------------------------------------------------------------------------
/src/_types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from "preact";
2 |
3 | export type Callback
= (props?: P) => R;
4 |
5 | export type ResolverType = () => void;
6 |
--------------------------------------------------------------------------------
/src/asyncUtils.ts:
--------------------------------------------------------------------------------
1 | import { act } from "@testing-library/preact";
2 | import { ResolverType } from "./_types";
3 |
4 | export interface TimeoutOptions {
5 | timeout?: number;
6 | suppressErrors?: boolean;
7 | }
8 |
9 | class TimeoutError extends Error {
10 | constructor(utilName: string, { timeout }: TimeoutOptions) {
11 | super(`Timed out in ${utilName} after ${timeout}ms.`);
12 | }
13 |
14 | timeout = true;
15 | }
16 |
17 | function asyncUtils(addResolver: (resolver: ResolverType) => void) {
18 | let nextUpdatePromise: Promise | void;
19 |
20 | async function waitForNextUpdate(options: TimeoutOptions = { timeout: 0 }) {
21 | if (!nextUpdatePromise) {
22 | nextUpdatePromise = new Promise((resolve, reject) => {
23 | const timeoutId =
24 | options.timeout! > 0
25 | ? setTimeout(() => {
26 | reject(new TimeoutError("waitForNextUpdate", options));
27 | }, options.timeout)
28 | : null;
29 |
30 | addResolver(() => {
31 | if (timeoutId) {
32 | clearTimeout(timeoutId);
33 | }
34 | nextUpdatePromise = undefined;
35 | resolve();
36 | });
37 | });
38 | }
39 | await nextUpdatePromise;
40 | }
41 |
42 | async function wait(
43 | callback: () => any,
44 | options: TimeoutOptions = { timeout: 0, suppressErrors: true }
45 | ) {
46 | const checkResult = () => {
47 | try {
48 | const callbackResult = callback();
49 | return callbackResult || callbackResult === undefined;
50 | } catch (err) {
51 | if (!options.suppressErrors) {
52 | throw err;
53 | }
54 | }
55 | };
56 |
57 | const waitForResult = async () => {
58 | const initialTimeout = options.timeout;
59 |
60 | while (true) {
61 | const startTime = Date.now();
62 | try {
63 | await waitForNextUpdate({ timeout: options.timeout });
64 | if (checkResult()) {
65 | return;
66 | }
67 | } catch (err) {
68 | if (err.timeout) {
69 | throw new TimeoutError("wait", { timeout: initialTimeout });
70 | }
71 | throw err;
72 | }
73 | options.timeout! -= Date.now() - startTime;
74 | }
75 | };
76 |
77 | if (!checkResult()) {
78 | await waitForResult();
79 | }
80 | }
81 |
82 | async function waitForValueToChange(
83 | selector: () => any,
84 | options: TimeoutOptions = { timeout: 0 }
85 | ) {
86 | const initialValue = selector();
87 | try {
88 | await wait(() => selector() !== initialValue, {
89 | suppressErrors: false,
90 | ...options,
91 | });
92 | } catch (err) {
93 | if (err.timeout) {
94 | throw new TimeoutError("waitForValueToChange", options);
95 | }
96 | throw err;
97 | }
98 | }
99 |
100 | return {
101 | waitForValueToChange,
102 | waitForNextUpdate,
103 | wait,
104 | };
105 | }
106 |
107 | export default asyncUtils;
108 |
109 | // function asyncUtils(addResolver: (r: ResolverType) => void) {
110 | // let nextUpdatePromise: Promise | null = null;
111 |
112 | // const waitForNextUpdate = async (
113 | // options: TimeoutOptions = { timeout: 0 }
114 | // ) => {
115 | // if (!nextUpdatePromise) {
116 | // nextUpdatePromise = new Promise((resolve, reject) => {
117 | // let timeoutId: NodeJS.Timeout;
118 | // if (options.timeout && options.timeout > 0) {
119 | // timeoutId = setTimeout(
120 | // () => reject(new TimeoutError("waitForNextUpdate", options)),
121 | // options.timeout
122 | // );
123 | // }
124 | // addResolver(() => {
125 | // clearTimeout(timeoutId);
126 | // nextUpdatePromise = null;
127 | // resolve();
128 | // });
129 | // });
130 | // await act(() => {
131 | // if (nextUpdatePromise) {
132 | // return nextUpdatePromise;
133 | // }
134 | // return;
135 | // });
136 | // }
137 | // await nextUpdatePromise;
138 | // };
139 |
140 | // const wait = async (
141 | // callback: () => any,
142 | // { timeout, suppressErrors }: WaitOptions = {
143 | // timeout: 0,
144 | // suppressErrors: true,
145 | // }
146 | // ) => {
147 | // const checkResult = () => {
148 | // try {
149 | // const callbackResult = callback();
150 | // return callbackResult || callbackResult === undefined;
151 | // } catch (e) {
152 | // if (!suppressErrors) {
153 | // throw e;
154 | // }
155 | // }
156 | // };
157 |
158 | // const waitForResult = async () => {
159 | // const initialTimeout = timeout;
160 | // while (true) {
161 | // const startTime = Date.now();
162 | // try {
163 | // await waitForNextUpdate({ timeout });
164 | // if (checkResult()) {
165 | // return;
166 | // }
167 | // } catch (e) {
168 | // if (e.timeout) {
169 | // throw new TimeoutError("wait", { timeout: initialTimeout });
170 | // }
171 | // throw e;
172 | // }
173 | // timeout -= Date.now() - startTime;
174 | // }
175 | // };
176 |
177 | // if (!checkResult()) {
178 | // await waitForResult();
179 | // }
180 | // };
181 |
182 | // const waitForValueToChange = async (
183 | // selector: () => any,
184 | // options: TimeoutOptions = {
185 | // timeout: 0,
186 | // }
187 | // ) => {
188 | // const initialValue = selector();
189 | // try {
190 | // await wait(() => selector() !== initialValue, {
191 | // suppressErrors: false,
192 | // ...options,
193 | // });
194 | // } catch (e) {
195 | // if (e.timeout) {
196 | // throw new TimeoutError("waitForValueToChange", options);
197 | // }
198 | // throw e;
199 | // }
200 | // };
201 |
202 | // return {
203 | // wait,
204 | // waitForNextUpdate,
205 | // waitForValueToChange,
206 | // };
207 | // }
208 |
209 | // export default asyncUtils;
210 |
--------------------------------------------------------------------------------
/src/cleanup.ts:
--------------------------------------------------------------------------------
1 | import flushMicroTasks from "./flush-microtasks";
2 |
3 | type CleanupCallback = () => void;
4 |
5 | let cleanupCallbacks: Set = new Set();
6 |
7 | export async function cleanup() {
8 | await flushMicroTasks();
9 | cleanupCallbacks.forEach((cb) => cb());
10 | cleanupCallbacks.clear();
11 | }
12 |
13 | export function addCleanup(callback: CleanupCallback) {
14 | cleanupCallbacks.add(callback);
15 | }
16 |
17 | export function removeCleanup(callback: CleanupCallback) {
18 | cleanupCallbacks.delete(callback);
19 | }
20 |
--------------------------------------------------------------------------------
/src/flush-microtasks.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // the part of this file that we need tested is definitely being run
3 | // and the part that is not cannot easily have useful tests written
4 | // anyway. So we're just going to ignore coverage for this file
5 | /**
6 | * copied from React's enqueueTask.js
7 | * copied again from React Testing Library's flush-microtasks.js
8 | */
9 |
10 | let didWarnAboutMessageChannel = false;
11 | let enqueueTask;
12 | try {
13 | // read require off the module object to get around the bundlers.
14 | // we don't want them to detect a require and bundle a Node polyfill.
15 | const requireString = `require${Math.random()}`.slice(0, 7);
16 | const nodeRequire = module && module[requireString];
17 | // assuming we're in node, let's try to get node's
18 | // version of setImmediate, bypassing fake timers if any.
19 | enqueueTask = nodeRequire("timers").setImmediate;
20 | } catch (_err) {
21 | // we're in a browser
22 | // we can't use regular timers because they may still be faked
23 | // so we try MessageChannel+postMessage instead
24 | enqueueTask = (callback) => {
25 | const supportsMessageChannel = typeof MessageChannel === "function";
26 | if (supportsMessageChannel) {
27 | const channel = new MessageChannel();
28 | channel.port1.onmessage = callback;
29 | channel.port2.postMessage(undefined);
30 | } else if (didWarnAboutMessageChannel === false) {
31 | didWarnAboutMessageChannel = true;
32 |
33 | // eslint-disable-next-line no-console
34 | console.error(
35 | "This browser does not have a MessageChannel implementation, " +
36 | "so enqueuing tasks via await act(async () => ...) will fail. " +
37 | "Please file an issue at https://github.com/facebook/react/issues " +
38 | "if you encounter this warning."
39 | );
40 | }
41 | };
42 | }
43 |
44 | export default function flushMicroTasks() {
45 | return new Promise((resolve) => enqueueTask(resolve));
46 | }
47 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /* globals afterEach */
2 | import { renderHook } from "./renderHook";
3 | import { act } from "@testing-library/preact";
4 | import { cleanup } from "./cleanup";
5 |
6 | // @ts-ignore
7 | if (typeof afterEach === "function" && !process.env.PHTL_SKIP_AUTO_CLEANUP) {
8 | // @ts-ignore
9 | afterEach(async () => {
10 | await cleanup();
11 | });
12 | }
13 |
14 | export { renderHook, act, cleanup };
15 |
--------------------------------------------------------------------------------
/src/renderHook.tsx:
--------------------------------------------------------------------------------
1 | import { h, ComponentType } from "preact";
2 | import { Suspense } from "preact/compat";
3 | import { render, act } from "@testing-library/preact";
4 |
5 | import { Callback } from "./_types";
6 | import resultContainer from "./resultContainer";
7 | import TestComponent, { Fallback } from "./TestComponent";
8 | import { removeCleanup, addCleanup } from "./cleanup";
9 | import asyncUtils from "./asyncUtils";
10 |
11 | const defaultWrapper = (Component: any) => ;
12 | export interface RenderHookOptions {
13 | initialProps?: P;
14 | wrapper?: ComponentType;
15 | }
16 |
17 | export function renderHook
(
18 | callback: Callback
,
19 | { initialProps, wrapper }: RenderHookOptions
= {}
20 | ) {
21 | const { result, setValue, setError, addResolver } = resultContainer();
22 |
23 | const hookProps = {
24 | current: initialProps,
25 | };
26 |
27 | const wrapUiIfNeeded = (innerElement: any) =>
28 | wrapper ? h(wrapper, null, innerElement) : innerElement;
29 |
30 | const TestHook = () =>
31 | wrapUiIfNeeded(
32 | }>
33 |
38 | {setValue}
39 |
40 |
41 | );
42 |
43 | const { unmount, rerender } = render();
44 |
45 | function rerenderHook(newProps = hookProps.current) {
46 | hookProps.current = newProps;
47 | act(() => {
48 | rerender();
49 | });
50 | }
51 |
52 | function unmountHook() {
53 | act(() => {
54 | removeCleanup(unmountHook);
55 | unmount();
56 | });
57 | }
58 |
59 | addCleanup(unmountHook);
60 |
61 | return {
62 | result,
63 | rerender: rerenderHook,
64 | unmount: unmountHook,
65 | ...asyncUtils(addResolver),
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/src/resultContainer.ts:
--------------------------------------------------------------------------------
1 | import { ResolverType } from "./_types";
2 |
3 | function resultContainer(initialValue?: R) {
4 | let value: R = initialValue as R;
5 | let error: Error;
6 | const resolvers: ResolverType[] = [];
7 |
8 | const result = {
9 | get current() {
10 | if (error) {
11 | throw error;
12 | }
13 | return value;
14 | },
15 | get error() {
16 | return error;
17 | },
18 | };
19 |
20 | function updateResult(val?: R, err?: Error) {
21 | value = val ? val : value;
22 | error = err ? err : error;
23 | resolvers.splice(0, resolvers.length).forEach((resolve) => resolve());
24 | }
25 |
26 | return {
27 | result,
28 | setValue: (val: R) => updateResult(val),
29 | setError: (err: Error) => updateResult(undefined, err),
30 | addResolver: (resolver: ResolverType) => {
31 | resolvers.push(resolver);
32 | },
33 | };
34 | }
35 |
36 | export default resultContainer;
37 |
--------------------------------------------------------------------------------
/test/asyncHook.test.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from "preact/hooks";
2 | import { renderHook } from "../src";
3 |
4 | /**
5 | * Skipping for now as async utils are still a bit odd
6 | */
7 | describe("async hook tests", () => {
8 | const useSequence = (...values: any[]) => {
9 | const [first, ...otherValues] = values;
10 | const [value, setValue] = useState(first);
11 | const index = useRef(0);
12 |
13 | useEffect(() => {
14 | const interval = setInterval(() => {
15 | setValue(otherValues[index.current++]);
16 | if (index.current === otherValues.length) {
17 | clearInterval(interval);
18 | }
19 | }, 50);
20 | return () => {
21 | clearInterval(interval);
22 | };
23 | }, [...values]);
24 |
25 | return value;
26 | };
27 |
28 | test("should wait for next update", async () => {
29 | const { result, waitForNextUpdate } = renderHook(() =>
30 | useSequence("first", "second")
31 | );
32 |
33 | expect(result.current).toBe("first");
34 |
35 | await waitForNextUpdate();
36 |
37 | expect(result.current).toBe("second");
38 | });
39 |
40 | test("should wait for multiple updates", async () => {
41 | const { result, waitForNextUpdate } = renderHook(() =>
42 | useSequence("first", "second", "third")
43 | );
44 |
45 | expect(result.current).toBe("first");
46 |
47 | await waitForNextUpdate();
48 |
49 | expect(result.current).toBe("second");
50 |
51 | await waitForNextUpdate();
52 |
53 | expect(result.current).toBe("third");
54 | });
55 |
56 | test("should resolve all when updating", async () => {
57 | const { result, waitForNextUpdate } = renderHook(() =>
58 | useSequence("first", "second")
59 | );
60 |
61 | expect(result.current).toBe("first");
62 |
63 | await Promise.all([
64 | waitForNextUpdate(),
65 | waitForNextUpdate(),
66 | waitForNextUpdate(),
67 | ]);
68 |
69 | expect(result.current).toBe("second");
70 | });
71 |
72 | test("should reject if timeout exceeded when waiting for next update", async () => {
73 | const { result, waitForNextUpdate } = renderHook(() =>
74 | useSequence("first", "second")
75 | );
76 |
77 | expect(result.current).toBe("first");
78 |
79 | await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
80 | Error("Timed out in waitForNextUpdate after 10ms.")
81 | );
82 | });
83 |
84 | test("should wait for expectation to pass", async () => {
85 | const { result, wait } = renderHook(() =>
86 | useSequence("first", "second", "third")
87 | );
88 |
89 | expect(result.current).toBe("first");
90 |
91 | let complete = false;
92 | await wait(() => {
93 | expect(result.current).toBe("third");
94 | complete = true;
95 | });
96 | expect(complete).toBe(true);
97 | });
98 |
99 | test("should not hang if expectation is already passing", async () => {
100 | const { result, wait } = renderHook(() => useSequence("first", "second"));
101 |
102 | expect(result.current).toBe("first");
103 |
104 | let complete = false;
105 | await wait(() => {
106 | expect(result.current).toBe("first");
107 | complete = true;
108 | });
109 | expect(complete).toBe(true);
110 | });
111 |
112 | test("should reject if callback throws error", async () => {
113 | const { result, wait } = renderHook(() =>
114 | useSequence("first", "second", "third")
115 | );
116 |
117 | expect(result.current).toBe("first");
118 |
119 | await expect(
120 | wait(
121 | () => {
122 | if (result.current === "second") {
123 | throw new Error("Something Unexpected");
124 | }
125 | return result.current === "third";
126 | },
127 | {
128 | suppressErrors: false,
129 | }
130 | )
131 | ).rejects.toThrow(Error("Something Unexpected"));
132 | });
133 |
134 | test("should reject if callback immediately throws error", async () => {
135 | const { result, wait } = renderHook(() =>
136 | useSequence("first", "second", "third")
137 | );
138 |
139 | expect(result.current).toBe("first");
140 |
141 | await expect(
142 | wait(
143 | () => {
144 | throw new Error("Something Unexpected");
145 | },
146 | {
147 | suppressErrors: false,
148 | }
149 | )
150 | ).rejects.toThrow(Error("Something Unexpected"));
151 | });
152 |
153 | test("should wait for truthy value", async () => {
154 | const { result, wait } = renderHook(() =>
155 | useSequence("first", "second", "third")
156 | );
157 |
158 | expect(result.current).toBe("first");
159 |
160 | await wait(() => result.current === "third");
161 |
162 | expect(result.current).toBe("third");
163 | });
164 |
165 | // FIXME
166 | test.skip("should reject if timeout exceeded when waiting for expectation to pass", async () => {
167 | const { result, wait } = renderHook(() =>
168 | useSequence("first", "second", "third")
169 | );
170 |
171 | expect(result.current).toBe("first");
172 |
173 | await expect(
174 | wait(
175 | () => {
176 | expect(result.current).toBe("third");
177 | },
178 | { timeout: 75 }
179 | )
180 | ).rejects.toThrow(Error("Timed out in wait after 75ms."));
181 | });
182 |
183 | test("should wait for value to change", async () => {
184 | const { result, waitForValueToChange } = renderHook(() =>
185 | useSequence("first", "second", "third")
186 | );
187 |
188 | expect(result.current).toBe("first");
189 |
190 | await waitForValueToChange(() => result.current === "third");
191 |
192 | expect(result.current).toBe("third");
193 | });
194 |
195 | test("should reject if timeout exceeded when waiting for value to change", async () => {
196 | const { result, waitForValueToChange } = renderHook(() =>
197 | useSequence("first", "second", "third")
198 | );
199 |
200 | expect(result.current).toBe("first");
201 |
202 | await expect(
203 | waitForValueToChange(() => result.current === "third", {
204 | timeout: 75,
205 | })
206 | ).rejects.toThrow(Error("Timed out in waitForValueToChange after 75ms."));
207 | });
208 |
209 | test("should reject if selector throws error", async () => {
210 | const { result, waitForValueToChange } = renderHook(() =>
211 | useSequence("first", "second")
212 | );
213 |
214 | expect(result.current).toBe("first");
215 |
216 | await expect(
217 | waitForValueToChange(() => {
218 | if (result.current === "second") {
219 | throw new Error("Something Unexpected");
220 | }
221 | return result.current;
222 | })
223 | ).rejects.toThrow(Error("Something Unexpected"));
224 | });
225 |
226 | test("should not reject if selector throws error and suppress errors option is enabled", async () => {
227 | const { result, waitForValueToChange } = renderHook(() =>
228 | useSequence("first", "second", "third")
229 | );
230 |
231 | expect(result.current).toBe("first");
232 |
233 | await waitForValueToChange(
234 | () => {
235 | if (result.current === "second") {
236 | throw new Error("Something Unexpected");
237 | }
238 | return result.current === "third";
239 | },
240 | { suppressErrors: true }
241 | );
242 |
243 | expect(result.current).toBe("third");
244 | });
245 | });
246 |
--------------------------------------------------------------------------------
/test/autoCleanup.disabled.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "preact/hooks";
2 |
3 | // This verifies that if PHTL_SKIP_AUTO_CLEANUP is set
4 | // then we DON'T auto-wire up the afterEach for folks
5 | describe.skip("skip auto cleanup (disabled) tests", () => {
6 | let cleanupCalled = false;
7 | let renderHook: Function;
8 |
9 | beforeAll(async () => {
10 | process.env.PHTL_SKIP_AUTO_CLEANUP = "true";
11 | renderHook = (await import("../src")).renderHook;
12 | });
13 |
14 | test("first", () => {
15 | const hookWithCleanup = () => {
16 | useEffect(() => {
17 | return () => {
18 | cleanupCalled = true;
19 | };
20 | });
21 | };
22 | renderHook(() => hookWithCleanup());
23 | });
24 |
25 | test("second", () => {
26 | expect(cleanupCalled).toBe(false);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/autoCleanup.noAfterEach.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "preact/hooks";
2 |
3 | // This verifies that if PHTL_SKIP_AUTO_CLEANUP is set
4 | // then we DON'T auto-wire up the afterEach for folks
5 | describe("skip auto cleanup (no afterEach) tests", () => {
6 | let cleanupCalled = false;
7 | let renderHook: Function;
8 |
9 | beforeAll(async () => {
10 | // @ts-ignore
11 | afterEach = false;
12 | renderHook = (await import("../src")).renderHook;
13 | });
14 |
15 | test("first", () => {
16 | const hookWithCleanup = () => {
17 | useEffect(() => {
18 | return () => {
19 | cleanupCalled = true;
20 | };
21 | });
22 | };
23 | renderHook(() => hookWithCleanup());
24 | });
25 |
26 | test("second", () => {
27 | expect(cleanupCalled).toBe(false);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/autoCleanup.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "preact/hooks";
2 | import { renderHook } from "../src";
3 |
4 | // This verifies that by importing RHTL in an
5 | // environment which supports afterEach (like Jest)
6 | // we'll get automatic cleanup between tests.
7 | describe("auto cleanup tests", () => {
8 | let cleanupCalled = false;
9 |
10 | test("first", () => {
11 | const hookWithCleanup = () => {
12 | useEffect(() => {
13 | return () => {
14 | cleanupCalled = true;
15 | };
16 | });
17 | };
18 | renderHook(() => hookWithCleanup());
19 | });
20 |
21 | test("second", () => {
22 | expect(cleanupCalled).toBe(true);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/cleanup.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "preact/hooks";
2 | import { renderHook, cleanup } from "../src";
3 |
4 | describe("cleanup tests", () => {
5 | test("should flush effects on cleanup", async () => {
6 | let cleanupCalled = false;
7 |
8 | const hookWithCleanup = () => {
9 | useEffect(() => {
10 | return () => {
11 | cleanupCalled = true;
12 | };
13 | });
14 | };
15 |
16 | renderHook(() => hookWithCleanup());
17 |
18 | await cleanup();
19 |
20 | expect(cleanupCalled).toBe(true);
21 | });
22 |
23 | test("should cleanup all rendered hooks", async () => {
24 | let cleanupCalled: boolean[] = [];
25 | const hookWithCleanup = (id: number) => {
26 | useEffect(() => {
27 | return () => {
28 | cleanupCalled[id] = true;
29 | };
30 | });
31 | };
32 |
33 | renderHook(() => hookWithCleanup(1));
34 | renderHook(() => hookWithCleanup(2));
35 |
36 | await cleanup();
37 |
38 | expect(cleanupCalled[1]).toBe(true);
39 | expect(cleanupCalled[2]).toBe(true);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/customHook.test.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "preact/hooks";
2 | import { renderHook, act } from "../src";
3 |
4 | describe("custom hook tests", () => {
5 | function useCounter() {
6 | const [count, setCount] = useState(0);
7 |
8 | const increment = useCallback(() => setCount(count + 1), [count]);
9 | const decrement = useCallback(() => setCount(count - 1), [count]);
10 |
11 | return { count, increment, decrement };
12 | }
13 |
14 | test("should increment counter", () => {
15 | const { result } = renderHook(() => useCounter());
16 |
17 | act(() => result.current.increment());
18 |
19 | expect(result.current.count).toBe(1);
20 | });
21 |
22 | test("should decrement counter", () => {
23 | const { result } = renderHook(() => useCounter());
24 |
25 | act(() => result.current.decrement());
26 |
27 | expect(result.current.count).toBe(-1);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/errorHook.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { useState, useEffect } from "preact/hooks";
3 | import { renderHook } from "../src";
4 |
5 | describe("error hook tests", () => {
6 | function useError(throwError) {
7 | if (throwError) {
8 | throw new Error("expected");
9 | }
10 | return true;
11 | }
12 |
13 | function useAsyncError(throwError) {
14 | const [value, setValue] = useState();
15 | useEffect(() => {
16 | const timeout = setTimeout(() => setValue(throwError), 100);
17 | return () => clearTimeout(timeout);
18 | }, [throwError]);
19 | return useError(value);
20 | }
21 |
22 | function useEffectError(throwError) {
23 | useEffect(() => {
24 | useError(throwError);
25 | }, []);
26 | return true;
27 | }
28 |
29 | describe("synchronous", () => {
30 | test("should raise error", () => {
31 | const { result } = renderHook(() => useError(true));
32 |
33 | expect(() => {
34 | expect(result.current).not.toBe(undefined);
35 | }).toThrow(Error("expected"));
36 | });
37 |
38 | test("should capture error", () => {
39 | const { result } = renderHook(() => useError(true));
40 |
41 | expect(result.error).toEqual(Error("expected"));
42 | });
43 |
44 | test("should not capture error", () => {
45 | const { result } = renderHook(() => useError(false));
46 |
47 | expect(result.current).not.toBe(undefined);
48 | expect(result.error).toBe(undefined);
49 | });
50 |
51 | test.skip("should reset error", () => {
52 | const { result, rerender } = renderHook(
53 | (throwError) => useError(throwError),
54 | {
55 | initialProps: true,
56 | }
57 | );
58 |
59 | expect(result.error).not.toBe(undefined);
60 |
61 | rerender(false);
62 |
63 | expect(result.current).not.toBe(undefined);
64 | expect(result.error).toBe(undefined);
65 | });
66 | });
67 |
68 | describe("asynchronous", () => {
69 | test("should raise async error", async () => {
70 | const { result, waitForNextUpdate } = renderHook(() =>
71 | useAsyncError(true)
72 | );
73 | await waitForNextUpdate();
74 |
75 | expect(() => {
76 | expect(result.current).not.toBe(undefined);
77 | }).toThrow(Error("expected"));
78 | });
79 |
80 | test("should capture async error", async () => {
81 | const { result, waitForNextUpdate } = renderHook(() =>
82 | useAsyncError(true)
83 | );
84 |
85 | await waitForNextUpdate();
86 |
87 | expect(result.error).toEqual(Error("expected"));
88 | });
89 |
90 | test("should not capture async error", async () => {
91 | const { result, waitForNextUpdate } = renderHook(() =>
92 | useAsyncError(false)
93 | );
94 |
95 | await waitForNextUpdate();
96 |
97 | expect(result.current).not.toBe(undefined);
98 | expect(result.error).toBe(undefined);
99 | });
100 |
101 | test.skip("should reset async error", async () => {
102 | const { result, waitForNextUpdate, rerender } = renderHook(
103 | (throwError) => useAsyncError(throwError),
104 | {
105 | initialProps: true,
106 | }
107 | );
108 |
109 | await waitForNextUpdate();
110 |
111 | expect(result.error).not.toBe(undefined);
112 |
113 | rerender(false);
114 |
115 | await waitForNextUpdate();
116 |
117 | expect(result.current).not.toBe(undefined);
118 | expect(result.error).toBe(undefined);
119 | });
120 | });
121 |
122 | /*
123 | These tests capture error cases that are not currently being caught successfully.
124 | Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308
125 | for more details.
126 | */
127 | describe.skip("effect", () => {
128 | test("should raise effect error", () => {
129 | const { result } = renderHook(() => useEffectError(true));
130 |
131 | expect(() => {
132 | expect(result.current).not.toBe(undefined);
133 | }).toThrow(Error("expected"));
134 | });
135 |
136 | test("should capture effect error", () => {
137 | const { result } = renderHook(() => useEffectError(true));
138 | expect(result.error).toEqual(Error("expected"));
139 | });
140 |
141 | test("should not capture effect error", () => {
142 | const { result } = renderHook(() => useEffectError(false));
143 |
144 | expect(result.current).not.toBe(undefined);
145 | expect(result.error).toBe(undefined);
146 | });
147 |
148 | test("should reset effect error", () => {
149 | const { result, waitForNextUpdate, rerender } = renderHook(
150 | (throwError) => useEffectError(throwError),
151 | {
152 | initialProps: true,
153 | }
154 | );
155 |
156 | expect(result.error).not.toBe(undefined);
157 |
158 | rerender(false);
159 |
160 | expect(result.current).not.toBe(undefined);
161 | expect(result.error).toBe(undefined);
162 | });
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/test/suspenseHook.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from "../src";
2 |
3 | describe("suspense hook tests", () => {
4 | const cache: { value?: any } = {};
5 | const fetchName = (isSuccessful: boolean) => {
6 | if (!cache.value) {
7 | cache.value = new Promise((resolve, reject) => {
8 | setTimeout(() => {
9 | if (isSuccessful) {
10 | resolve("Bob");
11 | } else {
12 | reject(new Error("Failed to fetch name"));
13 | }
14 | }, 50);
15 | })
16 | .then((value) => (cache.value = value))
17 | .catch((e) => (cache.value = e));
18 | }
19 | return cache.value;
20 | };
21 |
22 | const useFetchName = (isSuccessful = true) => {
23 | const name = fetchName(isSuccessful);
24 | if (typeof name.then === "function" || name instanceof Error) {
25 | throw name;
26 | }
27 | return name;
28 | };
29 |
30 | beforeEach(() => {
31 | delete cache.value;
32 | });
33 |
34 | test("should allow rendering to be suspended", async () => {
35 | const { result, waitForNextUpdate } = renderHook(() => useFetchName(true));
36 |
37 | await waitForNextUpdate();
38 |
39 | expect(result.current).toBe("Bob");
40 | });
41 |
42 | test("should set error if suspense promise rejects", async () => {
43 | const { result, waitForNextUpdate } = renderHook(() => useFetchName(false));
44 |
45 | await waitForNextUpdate();
46 |
47 | expect(result.error).toEqual(new Error("Failed to fetch name"));
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/useContext.test.tsx:
--------------------------------------------------------------------------------
1 | import { h, createContext } from "preact";
2 | import { useContext } from "preact/hooks";
3 | import { renderHook } from "../src";
4 |
5 | describe("useContext tests", () => {
6 | test("should get default value from context", () => {
7 | const TestContext = createContext("foo");
8 |
9 | const { result } = renderHook(() => useContext(TestContext));
10 |
11 | const value = result.current;
12 |
13 | expect(value).toBe("foo");
14 | });
15 |
16 | test("should get value from context provider", () => {
17 | const TestContext = createContext("foo");
18 |
19 | const wrapper = ({ children }: any) => (
20 | {children}
21 | );
22 |
23 | const { result } = renderHook(() => useContext(TestContext), { wrapper });
24 |
25 | expect(result.current).toBe("bar");
26 | });
27 |
28 | test("should update value in context", () => {
29 | const TestContext = createContext("foo");
30 |
31 | const value = { current: "bar" };
32 |
33 | const wrapper = ({ children }: any) => (
34 |
35 | {children}
36 |
37 | );
38 |
39 | const { result, rerender } = renderHook(() => useContext(TestContext), {
40 | wrapper,
41 | });
42 |
43 | value.current = "baz";
44 |
45 | rerender();
46 |
47 | expect(result.current).toBe("baz");
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/useEffect.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect } from "preact/hooks";
2 | import { renderHook } from "../src";
3 |
4 | describe("useEffect tests", () => {
5 | test("should handle useEffect hook", () => {
6 | const sideEffect: Record = { [1]: false, [2]: false };
7 |
8 | const { rerender, unmount } = renderHook(
9 | (props) => {
10 | const { id } = props || { id: 1 };
11 | useEffect(() => {
12 | sideEffect[id] = true;
13 | return () => {
14 | sideEffect[id] = false;
15 | };
16 | }, [id]);
17 | },
18 | { initialProps: { id: 1 } }
19 | );
20 |
21 | expect(sideEffect[1]).toBe(true);
22 | expect(sideEffect[2]).toBe(false);
23 |
24 | rerender({ id: 2 });
25 |
26 | expect(sideEffect[1]).toBe(false);
27 | expect(sideEffect[2]).toBe(true);
28 |
29 | unmount();
30 |
31 | expect(sideEffect[1]).toBe(false);
32 | expect(sideEffect[2]).toBe(false);
33 | });
34 |
35 | test("should handle useLayoutEffect hook", () => {
36 | const sideEffect: Record = { [1]: false, [2]: false };
37 |
38 | const { rerender, unmount } = renderHook(
39 | (props) => {
40 | const { id } = props || { id: 1 };
41 | useLayoutEffect(() => {
42 | sideEffect[id] = true;
43 | return () => {
44 | sideEffect[id] = false;
45 | };
46 | }, [id]);
47 | },
48 | { initialProps: { id: 1 } }
49 | );
50 |
51 | expect(sideEffect[1]).toBe(true);
52 | expect(sideEffect[2]).toBe(false);
53 |
54 | rerender({ id: 2 });
55 |
56 | expect(sideEffect[1]).toBe(false);
57 | expect(sideEffect[2]).toBe(true);
58 |
59 | unmount();
60 |
61 | expect(sideEffect[1]).toBe(false);
62 | expect(sideEffect[2]).toBe(false);
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/test/useMemo.test.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useCallback } from "preact/hooks";
2 | import { renderHook } from "../src";
3 |
4 | describe("useCallback tests", () => {
5 | test("should handle useMemo hook", () => {
6 | const { result, rerender } = renderHook(
7 | (props) => {
8 | const { value } = props || { value: 0 };
9 | return useMemo(() => ({ value }), [value]);
10 | },
11 | {
12 | initialProps: {
13 | value: 1,
14 | },
15 | }
16 | );
17 |
18 | const value1 = result.current;
19 |
20 | expect(value1).toEqual({ value: 1 });
21 |
22 | rerender();
23 |
24 | const value2 = result.current;
25 |
26 | expect(value2).toEqual({ value: 1 });
27 |
28 | expect(value2).toBe(value1);
29 |
30 | rerender({ value: 2 });
31 |
32 | const value3 = result.current;
33 |
34 | expect(value3).toEqual({ value: 2 });
35 |
36 | expect(value3).not.toBe(value1);
37 | });
38 |
39 | test("should handle useCallback hook", () => {
40 | const { result, rerender } = renderHook(
41 | (props) => {
42 | const { value } = props || { value: 0 };
43 | const callback = () => ({ value });
44 | return useCallback(callback, [value]);
45 | },
46 | { initialProps: { value: 1 } }
47 | );
48 |
49 | const callback1 = result.current;
50 |
51 | const calbackValue1 = callback1();
52 |
53 | expect(calbackValue1).toEqual({ value: 1 });
54 |
55 | const callback2 = result.current;
56 |
57 | const calbackValue2 = callback2();
58 |
59 | expect(calbackValue2).toEqual({ value: 1 });
60 |
61 | expect(callback2).toBe(callback1);
62 |
63 | rerender({ value: 2 });
64 |
65 | const callback3 = result.current;
66 |
67 | const calbackValue3 = callback3();
68 |
69 | expect(calbackValue3).toEqual({ value: 2 });
70 |
71 | expect(callback3).not.toBe(callback1);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/test/useReducer.test.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from "preact/hooks";
2 | import { renderHook, act } from "../src";
3 |
4 | describe("useReducer tests", () => {
5 | test("should handle useReducer hook", () => {
6 | const reducer = (
7 | state: number,
8 | action: {
9 | type: "inc";
10 | }
11 | ) => (action.type === "inc" ? state + 1 : state);
12 | const { result } = renderHook(() => useReducer(reducer, 0));
13 |
14 | const [initialState, dispatch] = result.current;
15 |
16 | expect(initialState).toBe(0);
17 |
18 | act(() => dispatch({ type: "inc" }));
19 |
20 | const [state] = result.current;
21 |
22 | expect(state).toBe(1);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/useRef.test.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useImperativeHandle } from "preact/hooks";
2 | import { renderHook } from "../src";
3 |
4 | describe("useHook tests", () => {
5 | test("should handle useRef hook", () => {
6 | const { result } = renderHook(() => useRef());
7 |
8 | const refContainer = result.current;
9 |
10 | expect(Object.keys(refContainer)).toEqual(["current"]);
11 | expect(refContainer.current).toBeUndefined();
12 | });
13 |
14 | test("should handle useImperativeHandle hook", () => {
15 | const { result } = renderHook(() => {
16 | const ref = useRef<{
17 | fakeImperativeMethod: () => boolean;
18 | }>();
19 | useImperativeHandle(ref, () => ({
20 | fakeImperativeMethod: () => true,
21 | }));
22 | return ref;
23 | });
24 |
25 | const refContainer = result.current;
26 |
27 | expect(refContainer.current?.fakeImperativeMethod()).toBe(true);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/useState.test.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "preact/hooks";
2 | import { renderHook, act } from "../src";
3 |
4 | describe("useState tests", () => {
5 | test("should use setState value", () => {
6 | const { result } = renderHook(() => useState("foo"));
7 |
8 | const [value] = result.current;
9 |
10 | expect(value).toBe("foo");
11 | });
12 |
13 | test("should update setState value using setter", () => {
14 | const { result } = renderHook(() => useState("foo"));
15 |
16 | const [_, setValue] = result.current;
17 |
18 | act(() => setValue("bar"));
19 |
20 | const [value] = result.current;
21 |
22 | expect(value).toBe("bar");
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "jsx": "react",
11 | "jsxFactory": "h",
12 | "declaration": true,
13 | "outDir": "./lib",
14 | "strict": true,
15 | "esModuleInterop": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true
18 | },
19 | "include": [
20 | "src/*.ts",
21 | "src/*.tsx",
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from "preact";
2 |
3 | export type Wrapper = (Component: ComponentType) => ComponentType;
4 |
5 | export type Callback = (props?: P) => R;
6 |
--------------------------------------------------------------------------------