mirror of
https://github.com/streamich/react-use.git
synced 2026-01-25 14:17:16 +00:00
v15 release
BREAKING CHANGE: implementation of useMeasure and useLocalStorage changed
This commit is contained in:
commit
0f82ba650e
99
.github/FUNDING.yml
vendored
99
.github/FUNDING.yml
vendored
@ -1,3 +1,100 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: streamich
|
||||
github: [
|
||||
"streamich",
|
||||
"wardoost",
|
||||
"xobotyi",
|
||||
"Belco90",
|
||||
"ankithkonda",
|
||||
"ayush987goyal",
|
||||
"NullVoxPopuli",
|
||||
"lintuming",
|
||||
"Granipouss",
|
||||
"ythecombinator",
|
||||
"james2406",
|
||||
"jakapatb",
|
||||
"MrHuangJser",
|
||||
"zaguiini",
|
||||
"ppeeou",
|
||||
"liuyuchenzh",
|
||||
"brickspert",
|
||||
"artywhite",
|
||||
"PetterIve",
|
||||
"realdennis",
|
||||
"lvl99",
|
||||
"gelove",
|
||||
"KusStar",
|
||||
"xiaoxiangmoe",
|
||||
"nmccready",
|
||||
"mattleonowicz",
|
||||
"kevinnorris",
|
||||
"dubzzz",
|
||||
"dependabot[bot]",
|
||||
"ShizukuIchi",
|
||||
"ManojBahuguna",
|
||||
"Jivings",
|
||||
"Dosant",
|
||||
"zsh2401",
|
||||
"xiaoboost",
|
||||
"revskill10",
|
||||
"mtinner",
|
||||
"monkeywithacupcake",
|
||||
"mitchheddles",
|
||||
"maxzitron",
|
||||
"macinjoke",
|
||||
"jeetiss",
|
||||
"ilyalesik",
|
||||
"hijiangtao",
|
||||
"f",
|
||||
"elliottsj",
|
||||
"droganov",
|
||||
"denysdovhan",
|
||||
"dabuside",
|
||||
"benneq",
|
||||
"azukaar",
|
||||
"ariesjia",
|
||||
"andrico1234",
|
||||
"adesurirey",
|
||||
"OBe95",
|
||||
"FredyC",
|
||||
"Cretezy",
|
||||
"zyy7259",
|
||||
"zslabs",
|
||||
"vinitsood",
|
||||
"uxitten",
|
||||
"thevtm",
|
||||
"tanem",
|
||||
"suyingtao",
|
||||
"srph",
|
||||
"rkostrzewski",
|
||||
"qianL93",
|
||||
"o-alexandrov",
|
||||
"nucleartux",
|
||||
"natew",
|
||||
"maxmalov",
|
||||
"liaoyinglong",
|
||||
"koenvanzuijlen",
|
||||
"josmardias",
|
||||
"jeemyeong",
|
||||
"jazzqi",
|
||||
"jakyle",
|
||||
"jakeboone02",
|
||||
"inker",
|
||||
"glarivie",
|
||||
"garrettmaring",
|
||||
"dovidweisz",
|
||||
"daniel-hauser",
|
||||
"d-asensio",
|
||||
"charlax",
|
||||
"TylerR909",
|
||||
"Rogdham",
|
||||
"OctoD",
|
||||
"MajorBreakfast",
|
||||
"Jfelix61",
|
||||
"Flydiverny",
|
||||
"FlickerLogicalStack",
|
||||
"DmacMcgreg",
|
||||
"Dattaya",
|
||||
"Andrey-Bazhanov",
|
||||
"AlvaroBernalG"
|
||||
]
|
||||
|
||||
@ -32,5 +32,5 @@ const Demo = ({url}) => {
|
||||
## Reference
|
||||
|
||||
```ts
|
||||
useAsyncFn(fn, deps?: any[]);
|
||||
useAsyncFn<Result, Args>(fn, deps?: any[], initialState?: AsyncState<Result>);
|
||||
```
|
||||
|
||||
@ -8,13 +8,14 @@ React side-effect hook that manages a single `localStorage` key.
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [value, setValue] = useLocalStorage('my-key', 'foo');
|
||||
const [value, setValue, remove] = useLocalStorage('my-key', 'foo');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Value: {value}</div>
|
||||
<button onClick={() => setValue('bar')}>bar</button>
|
||||
<button onClick={() => setValue('baz')}>baz</button>
|
||||
<button onClick={() => remove()}>Remove</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -25,6 +25,21 @@ const Demo = () => {
|
||||
};
|
||||
```
|
||||
|
||||
This hook uses [`ResizeObserver` API][resize-observer], if you want to support
|
||||
legacy browsers, consider installing [`resize-observer-polyfill`][resize-observer-polyfill]
|
||||
before running your app.
|
||||
|
||||
```js
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = (await import('resize-observer-polyfill')).default;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Related hooks
|
||||
|
||||
- [useSize](./useSize.md)
|
||||
|
||||
|
||||
[resize-observer]: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
|
||||
[resize-observer-polyfill]: https://www.npmjs.com/package/resize-observer-polyfill
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-use",
|
||||
"version": "14.3.0",
|
||||
"version": "15.0.0-alpha.1",
|
||||
"description": "Collection of React Hooks",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -169,7 +169,7 @@
|
||||
"<rootDir>/tests/**/*.test.(ts|tsx)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"./tests/setupTests.ts"
|
||||
"<rootDir>/tests/setupTests.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { DependencyList, useEffect } from 'react';
|
||||
import useAsyncFn from './useAsyncFn';
|
||||
import { FnReturningPromise } from './util';
|
||||
|
||||
export { AsyncState, AsyncFn } from './useAsyncFn';
|
||||
export { AsyncState, AsyncFnReturn } from './useAsyncFn';
|
||||
|
||||
export default function useAsync<Result = any, Args extends any[] = any[]>(
|
||||
fn: (...args: Args | []) => Promise<Result>,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
const [state, callback] = useAsyncFn<Result, Args>(fn, deps, {
|
||||
export default function useAsync<T extends FnReturningPromise>(fn: T, deps: DependencyList = []) {
|
||||
const [state, callback] = useAsyncFn(fn, deps, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { DependencyList, useCallback, useState, useRef } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
import { FnReturningPromise, PromiseType } from './util';
|
||||
|
||||
export type AsyncState<T> =
|
||||
| {
|
||||
@ -8,6 +9,11 @@ export type AsyncState<T> =
|
||||
error?: undefined;
|
||||
value?: undefined;
|
||||
}
|
||||
| {
|
||||
loading: true;
|
||||
error?: Error | undefined;
|
||||
value?: T;
|
||||
}
|
||||
| {
|
||||
loading: false;
|
||||
error: Error;
|
||||
@ -19,24 +25,22 @@ export type AsyncState<T> =
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type AsyncFn<Result = any, Args extends any[] = any[]> = [
|
||||
AsyncState<Result>,
|
||||
(...args: Args | []) => Promise<Result>
|
||||
];
|
||||
type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>;
|
||||
|
||||
export default function useAsyncFn<Result = any, Args extends any[] = any[]>(
|
||||
fn: (...args: Args | []) => Promise<Result>,
|
||||
export type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [StateFromFnReturningPromise<T>, T];
|
||||
|
||||
export default function useAsyncFn<T extends FnReturningPromise>(
|
||||
fn: T,
|
||||
deps: DependencyList = [],
|
||||
initialState: AsyncState<Result> = { loading: false }
|
||||
): AsyncFn<Result, Args> {
|
||||
initialState: StateFromFnReturningPromise<T> = { loading: false }
|
||||
): AsyncFnReturn<T> {
|
||||
const lastCallId = useRef(0);
|
||||
const [state, set] = useState<AsyncState<Result>>(initialState);
|
||||
|
||||
const isMounted = useMountedState();
|
||||
const [state, set] = useState<StateFromFnReturningPromise<T>>(initialState);
|
||||
|
||||
const callback = useCallback((...args: Args | []) => {
|
||||
const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
|
||||
const callId = ++lastCallId.current;
|
||||
set({ loading: true });
|
||||
set(prevState => ({ ...prevState, loading: true }));
|
||||
|
||||
return fn(...args).then(
|
||||
value => {
|
||||
@ -49,8 +53,8 @@ export default function useAsyncFn<Result = any, Args extends any[] = any[]>(
|
||||
|
||||
return error;
|
||||
}
|
||||
);
|
||||
) as ReturnType<T>;
|
||||
}, deps);
|
||||
|
||||
return [state, callback];
|
||||
return [state, (callback as unknown) as T];
|
||||
}
|
||||
|
||||
@ -19,31 +19,46 @@ const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] =
|
||||
});
|
||||
|
||||
const copyToClipboard = useCallback(value => {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
let noUserInteraction;
|
||||
let normalizedValue;
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (typeof value !== 'string') {
|
||||
console.error(`Cannot copy typeof ${typeof value} to clipboard, must be a string`);
|
||||
}
|
||||
}
|
||||
|
||||
const noUserInteraction = writeText(value);
|
||||
|
||||
if (!isMounted()) {
|
||||
// only strings and numbers casted to strings can be copied to clipboard
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
const error = new Error(`Cannot copy typeof ${typeof value} to clipboard, must be a string`);
|
||||
if (process.env.NODE_ENV === 'development') console.error(error);
|
||||
setState({
|
||||
value,
|
||||
error,
|
||||
noUserInteraction: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// empty strings are also considered invalid
|
||||
else if (value === '') {
|
||||
const error = new Error(`Cannot copy empty string to clipboard.`);
|
||||
if (process.env.NODE_ENV === 'development') console.error(error);
|
||||
setState({
|
||||
value,
|
||||
error,
|
||||
noUserInteraction: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
normalizedValue = value.toString();
|
||||
noUserInteraction = writeText(normalizedValue);
|
||||
setState({
|
||||
value,
|
||||
value: normalizedValue,
|
||||
error: undefined,
|
||||
noUserInteraction,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
value: undefined,
|
||||
value: normalizedValue,
|
||||
error,
|
||||
noUserInteraction: true,
|
||||
noUserInteraction,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -2,9 +2,13 @@ import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
|
||||
|
||||
const isPrimitive = (val: any) => val !== Object(val);
|
||||
|
||||
type DepsEqualFnType = (prevDeps: DependencyList, nextDeps: DependencyList) => boolean;
|
||||
type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps) => boolean;
|
||||
|
||||
const useCustomCompareEffect = (effect: EffectCallback, deps: DependencyList, depsEqual: DepsEqualFnType) => {
|
||||
const useCustomCompareEffect = <TDeps extends DependencyList>(
|
||||
effect: EffectCallback,
|
||||
deps: TDeps,
|
||||
depsEqual: DepsEqualFnType<TDeps>
|
||||
) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!(deps instanceof Array) || !deps.length) {
|
||||
console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
|
||||
@ -21,7 +25,7 @@ const useCustomCompareEffect = (effect: EffectCallback, deps: DependencyList, de
|
||||
}
|
||||
}
|
||||
|
||||
const ref = useRef<DependencyList | undefined>(undefined);
|
||||
const ref = useRef<TDeps | undefined>(undefined);
|
||||
|
||||
if (!ref.current || !depsEqual(deps, ref.current)) {
|
||||
ref.current = deps;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState, useCallback, Dispatch, SetStateAction } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
type parserOptions<T> =
|
||||
@ -12,21 +12,26 @@ type parserOptions<T> =
|
||||
deserializer: (value: string) => T;
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue?: T,
|
||||
options?: parserOptions<T>
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] => {
|
||||
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => {
|
||||
if (!isClient) {
|
||||
return [initialValue as T, () => {}];
|
||||
return [initialValue as T, noop, noop];
|
||||
}
|
||||
if (!key) {
|
||||
throw new Error('useLocalStorage key may not be falsy');
|
||||
}
|
||||
|
||||
// Use provided serializer/deserializer or the default ones
|
||||
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
|
||||
const deserializer = options ? (options.raw ? String : options.deserializer) : JSON.parse;
|
||||
const deserializer = options ? (options.raw ? value => value : options.deserializer) : JSON.parse;
|
||||
|
||||
const [state, setState] = useState<T>(() => {
|
||||
const [state, setState] = useState<T | undefined>(() => {
|
||||
try {
|
||||
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
|
||||
|
||||
const localStorageValue = localStorage.getItem(key);
|
||||
if (localStorageValue !== null) {
|
||||
return deserializer(localStorageValue);
|
||||
@ -42,16 +47,42 @@ const useLocalStorage = <T>(
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const set: Dispatch<SetStateAction<T | undefined>> = useCallback(
|
||||
valOrFunc => {
|
||||
try {
|
||||
const newState = typeof valOrFunc === 'function' ? (valOrFunc as Function)(state) : valOrFunc;
|
||||
if (typeof newState === 'undefined') return;
|
||||
let value: string;
|
||||
|
||||
if (options)
|
||||
if (options.raw)
|
||||
if (typeof newState === 'string') value = newState;
|
||||
else value = JSON.stringify(newState);
|
||||
else if (options.serializer) value = options.serializer(newState);
|
||||
else value = JSON.stringify(newState);
|
||||
else value = JSON.stringify(newState);
|
||||
|
||||
localStorage.setItem(key, value);
|
||||
setState(deserializer(value));
|
||||
} catch {
|
||||
// If user is in private mode or has storage restriction
|
||||
// localStorage can throw. Also JSON.stringify can throw.
|
||||
}
|
||||
},
|
||||
[key, setState]
|
||||
);
|
||||
|
||||
const remove = useCallback(() => {
|
||||
try {
|
||||
localStorage.setItem(key, serializer(state));
|
||||
localStorage.removeItem(key);
|
||||
setState(undefined);
|
||||
} catch {
|
||||
// If user is in private mode or has storage restriction
|
||||
// localStorage can throw. Also JSON.stringify can throw.
|
||||
// localStorage can throw.
|
||||
}
|
||||
}, [state]);
|
||||
}, [key, setState]);
|
||||
|
||||
return [state, setState];
|
||||
return [state, set, remove];
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
|
||||
@ -1,40 +1,51 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { useState, useMemo } from 'react';
|
||||
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
|
||||
import { isClient } from './util';
|
||||
|
||||
export type ContentRect = Pick<DOMRectReadOnly, 'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'>;
|
||||
export type UseMeasureRect = Pick<
|
||||
DOMRectReadOnly,
|
||||
'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
|
||||
>;
|
||||
export type UseMeasureRef = (element: HTMLElement) => void;
|
||||
export type UseMeasureResult = [UseMeasureRef, UseMeasureRect];
|
||||
|
||||
const useMeasure = <T>(): [(instance: T) => void, ContentRect] => {
|
||||
const [rect, set] = useState<ContentRect>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
});
|
||||
const defaultState: UseMeasureRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
|
||||
const [observer] = useState(
|
||||
const useMeasure = (): UseMeasureResult => {
|
||||
const [element, ref] = useState<HTMLElement | null>(null);
|
||||
const [rect, setRect] = useState<UseMeasureRect>(defaultState);
|
||||
|
||||
const observer = useMemo(
|
||||
() =>
|
||||
new ResizeObserver(entries => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
set(entry.contentRect);
|
||||
new (window as any).ResizeObserver(entries => {
|
||||
if (entries[0]) {
|
||||
const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
|
||||
setRect({ x, y, width, height, top, left, bottom, right });
|
||||
}
|
||||
})
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const ref = useCallback(
|
||||
node => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!element) return;
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (node) {
|
||||
observer.observe(node);
|
||||
}
|
||||
},
|
||||
[observer]
|
||||
);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
return [ref, rect];
|
||||
};
|
||||
|
||||
export default useMeasure;
|
||||
const useMeasureMock = () => [() => {}, defaultState];
|
||||
|
||||
export default (isClient && !!(window as any).ResizeObserver) ? useMeasure : useMeasureMock;
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useReducer } from 'react';
|
||||
|
||||
const incrementParameter = (num: number): number => ++num % 1_000_000;
|
||||
const updateReducer = (num: number): number => (num + 1) % 1_000_000;
|
||||
|
||||
const useUpdate = () => {
|
||||
const [, setState] = useState(0);
|
||||
// useCallback with empty deps as we only want to define updateCb once
|
||||
return useCallback(() => setState(incrementParameter), []);
|
||||
const [, update] = useReducer(updateReducer, 0);
|
||||
return update as () => void;
|
||||
};
|
||||
|
||||
export default useUpdate;
|
||||
|
||||
@ -4,4 +4,8 @@ export const on = (obj: any, ...args: any[]) => obj.addEventListener(...args);
|
||||
|
||||
export const off = (obj: any, ...args: any[]) => obj.removeEventListener(...args);
|
||||
|
||||
export type FnReturningPromise = (...args: any[]) => Promise<any>;
|
||||
|
||||
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T> ? T : never;
|
||||
|
||||
export const isDeepEqual: (a: any, b: any) => boolean = require('fast-deep-equal/react');
|
||||
|
||||
@ -5,12 +5,19 @@ import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const [value, setValue] = useLocalStorage('hello-key', 'foo');
|
||||
const [removableValue, setRemovableValue, remove] = useLocalStorage('removeable-key');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Value: {value}</div>
|
||||
<button onClick={() => setValue('bar')}>bar</button>
|
||||
<button onClick={() => setValue('baz')}>baz</button>
|
||||
<br />
|
||||
<br />
|
||||
<div>Removable Value: {removableValue}</div>
|
||||
<button onClick={() => setRemovableValue('foo')}>foo</button>
|
||||
<button onClick={() => setRemovableValue('bar')}>bar</button>
|
||||
<button onClick={() => remove()}>Remove</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1 +1,6 @@
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
@ -148,7 +148,7 @@ describe('useAsync', () => {
|
||||
hook = renderHook(
|
||||
({ fn, counter }) => {
|
||||
const callback = useCallback(() => fn(counter), [counter]);
|
||||
return useAsync<string>(callback, [callback]);
|
||||
return useAsync<any>(callback, [callback]);
|
||||
},
|
||||
{
|
||||
initialProps: {
|
||||
|
||||
@ -126,4 +126,31 @@ describe('useAsyncFn', () => {
|
||||
await hook.waitForNextUpdate();
|
||||
expect(hook.result.current[0]).toEqual({ loading: false, value: 2 });
|
||||
});
|
||||
|
||||
it('should keeping value of initialState when loading', async () => {
|
||||
const fetch = async () => 'new state';
|
||||
const initialState = { loading: false, value: 'init state' };
|
||||
|
||||
const hook = renderHook<{ fn: () => Promise<string> }, [AsyncState<string>, () => Promise<string>]>(
|
||||
({ fn }) => useAsyncFn(fn, [fn], initialState),
|
||||
{
|
||||
initialProps: { fn: fetch },
|
||||
}
|
||||
);
|
||||
|
||||
const [state, callback] = hook.result.current;
|
||||
expect(state.loading).toBe(false);
|
||||
expect(state.value).toBe('init state');
|
||||
|
||||
act(() => {
|
||||
callback();
|
||||
});
|
||||
|
||||
expect(hook.result.current[0].loading).toBe(true);
|
||||
expect(hook.result.current[0].value).toBe('init state');
|
||||
|
||||
await hook.waitForNextUpdate();
|
||||
expect(hook.result.current[0].loading).toBe(false);
|
||||
expect(hook.result.current[0].value).toBe('new state');
|
||||
});
|
||||
});
|
||||
|
||||
103
tests/useCopyToClipboard.test.ts
Normal file
103
tests/useCopyToClipboard.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import writeText from 'copy-to-clipboard';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useCopyToClipboard } from '../src';
|
||||
|
||||
const valueToRaiseMockException = 'fake input causing exception in copy to clipboard';
|
||||
|
||||
jest.mock('copy-to-clipboard', () =>
|
||||
jest.fn().mockImplementation(input => {
|
||||
if (input === valueToRaiseMockException) {
|
||||
throw new Error(input);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('useCopyToClipboard', () => {
|
||||
let hook;
|
||||
|
||||
beforeEach(() => {
|
||||
hook = renderHook(() => useCopyToClipboard());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined ', () => {
|
||||
expect(useCopyToClipboard).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass a given value to copy to clipboard and set state', () => {
|
||||
const testValue = 'test';
|
||||
let [state, copyToClipboard] = hook.result.current;
|
||||
act(() => copyToClipboard(testValue));
|
||||
[state, copyToClipboard] = hook.result.current;
|
||||
|
||||
expect(writeText).toBeCalled();
|
||||
expect(state.value).toBe(testValue);
|
||||
expect(state.noUserInteraction).toBe(true);
|
||||
expect(state.error).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should not call writeText if passed an invalid input and set state', () => {
|
||||
let testValue = {}; // invalid value
|
||||
let [state, copyToClipboard] = hook.result.current;
|
||||
act(() => copyToClipboard(testValue));
|
||||
[state, copyToClipboard] = hook.result.current;
|
||||
|
||||
expect(writeText).not.toBeCalled();
|
||||
expect(state.value).toBe(testValue);
|
||||
expect(state.noUserInteraction).toBe(true);
|
||||
expect(state.error).toBeDefined();
|
||||
|
||||
testValue = ''; // emtpy string is also invalid
|
||||
act(() => copyToClipboard(testValue));
|
||||
[state, copyToClipboard] = hook.result.current;
|
||||
|
||||
expect(writeText).not.toBeCalled();
|
||||
expect(state.value).toBe(testValue);
|
||||
expect(state.noUserInteraction).toBe(true);
|
||||
expect(state.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should catch exception thrown by copy-to-clipboard and set state', () => {
|
||||
let [state, copyToClipboard] = hook.result.current;
|
||||
act(() => copyToClipboard(valueToRaiseMockException));
|
||||
[state, copyToClipboard] = hook.result.current;
|
||||
|
||||
expect(writeText).toBeCalledWith(valueToRaiseMockException);
|
||||
expect(state.value).toBe(valueToRaiseMockException);
|
||||
expect(state.noUserInteraction).not.toBeDefined();
|
||||
expect(state.error).toStrictEqual(new Error(valueToRaiseMockException));
|
||||
});
|
||||
|
||||
it('should return initial state while unmounted', () => {
|
||||
hook.unmount();
|
||||
const [state, copyToClipboard] = hook.result.current;
|
||||
|
||||
act(() => copyToClipboard('value'));
|
||||
expect(state.value).not.toBeDefined();
|
||||
expect(state.error).not.toBeDefined();
|
||||
expect(state.noUserInteraction).toBe(true);
|
||||
});
|
||||
|
||||
it('should console error if in dev environment', () => {
|
||||
const ORIGINAL_NODE_ENV = process.env.NODE_ENV;
|
||||
const testValue = {}; // invalid value
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
let [state, copyToClipboard] = hook.result.current;
|
||||
act(() => copyToClipboard(testValue));
|
||||
process.env.NODE_ENV = ORIGINAL_NODE_ENV;
|
||||
|
||||
[state, copyToClipboard] = hook.result.current;
|
||||
|
||||
expect(writeText).not.toBeCalled();
|
||||
expect(console.error).toBeCalled();
|
||||
expect(state.value).toBe(testValue);
|
||||
expect(state.noUserInteraction).toBe(true);
|
||||
expect(state.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,95 +1,237 @@
|
||||
/* eslint-disable */
|
||||
import useLocalStorage from '../src/useLocalStorage';
|
||||
import 'jest-localstorage-mock';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useLocalStorage } from '../src';
|
||||
|
||||
const STRINGIFIED_VALUE = '{"a":"b"}';
|
||||
const JSONIFIED_VALUE = { a: 'b' };
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined if no initialValue provided and localStorage empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('some_key'));
|
||||
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set the value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key));
|
||||
|
||||
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
|
||||
});
|
||||
|
||||
it('should return initialValue if localStorage empty and set that to localStorage', () => {
|
||||
const key = 'some_key';
|
||||
const value = 'some_value';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, value));
|
||||
|
||||
expect(result.current[0]).toBe(value);
|
||||
expect(localStorage.__STORE__[key]).toBe(`"${value}"`);
|
||||
});
|
||||
|
||||
it('should return the value from localStorage if exists even if initialValue provied', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'random_value'));
|
||||
|
||||
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
|
||||
});
|
||||
|
||||
it('should properly update the localStorage on change', () => {
|
||||
const key = 'some_key';
|
||||
const updatedValue = { b: 'a' };
|
||||
const expectedValue = '{"b":"a"}';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key));
|
||||
|
||||
act(() => {
|
||||
result.current[1](updatedValue);
|
||||
describe(useLocalStorage, () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(updatedValue);
|
||||
expect(localStorage.__STORE__[key]).toBe(expectedValue);
|
||||
});
|
||||
|
||||
describe('Options with raw true', () => {
|
||||
it('should set the value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, '', { raw: true }));
|
||||
|
||||
expect(result.current[0]).toEqual(STRINGIFIED_VALUE);
|
||||
it('retrieves an existing value from localStorage', () => {
|
||||
localStorage.setItem('foo', '"bar"');
|
||||
const { result } = renderHook(() => useLocalStorage('foo'));
|
||||
const [state] = result.current;
|
||||
expect(state).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should return initialValue if localStorage empty and set that to localStorage', () => {
|
||||
const key = 'some_key';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, STRINGIFIED_VALUE, { raw: true }));
|
||||
|
||||
expect(result.current[0]).toBe(STRINGIFIED_VALUE);
|
||||
expect(localStorage.__STORE__[key]).toBe(STRINGIFIED_VALUE);
|
||||
const { result } = renderHook(() => useLocalStorage('foo', 'bar'));
|
||||
const [state] = result.current;
|
||||
expect(state).toEqual('bar');
|
||||
expect(localStorage.__STORE__.foo).toEqual('"bar"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options with raw false and provided serializer/deserializer', () => {
|
||||
const serializer = (_: string) => '321';
|
||||
const deserializer = (_: string) => '123';
|
||||
it('prefers existing value over initial state', () => {
|
||||
localStorage.setItem('foo', '"bar"');
|
||||
const { result } = renderHook(() => useLocalStorage('foo', 'baz'));
|
||||
const [state] = result.current;
|
||||
expect(state).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should return valid serialized value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
it('does not clobber existing localStorage with initialState', () => {
|
||||
localStorage.setItem('foo', '"bar"');
|
||||
const { result } = renderHook(() => useLocalStorage('foo', 'buzz'));
|
||||
result.current; // invoke current to make sure things are set
|
||||
expect(localStorage.__STORE__.foo).toEqual('"bar"');
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(key, STRINGIFIED_VALUE, { raw: false, serializer, deserializer })
|
||||
it('correctly updates localStorage', () => {
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', 'bar'));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
act(() => setFoo('baz'));
|
||||
rerender();
|
||||
|
||||
expect(localStorage.__STORE__.foo).toEqual('"baz"');
|
||||
});
|
||||
|
||||
it('should return undefined if no initialValue provided and localStorage empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('some_key'));
|
||||
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns and allow setting null', () => {
|
||||
localStorage.setItem('foo', 'null');
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo'));
|
||||
|
||||
const [foo1, setFoo] = result.current;
|
||||
act(() => setFoo(null));
|
||||
rerender();
|
||||
|
||||
const [foo2] = result.current;
|
||||
expect(foo1).toEqual(null);
|
||||
expect(foo2).toEqual(null);
|
||||
});
|
||||
|
||||
it('sets initialState if initialState is an object', () => {
|
||||
renderHook(() => useLocalStorage('foo', { bar: true }));
|
||||
expect(localStorage.__STORE__.foo).toEqual('{"bar":true}');
|
||||
});
|
||||
|
||||
it('correctly and promptly returns a new value', () => {
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', 'bar'));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
act(() => setFoo('baz'));
|
||||
rerender();
|
||||
|
||||
const [foo] = result.current;
|
||||
expect(foo).toEqual('baz');
|
||||
});
|
||||
|
||||
/*
|
||||
it('keeps multiple hooks accessing the same key in sync', () => {
|
||||
localStorage.setItem('foo', 'bar');
|
||||
const { result: r1, rerender: rerender1 } = renderHook(() => useLocalStorage('foo'));
|
||||
const { result: r2, rerender: rerender2 } = renderHook(() => useLocalStorage('foo'));
|
||||
|
||||
const [, setFoo] = r1.current;
|
||||
act(() => setFoo('potato'));
|
||||
rerender1();
|
||||
rerender2();
|
||||
|
||||
const [val1] = r1.current;
|
||||
const [val2] = r2.current;
|
||||
|
||||
expect(val1).toEqual(val2);
|
||||
expect(val1).toEqual('potato');
|
||||
expect(val2).toEqual('potato');
|
||||
});
|
||||
*/
|
||||
|
||||
it('parses out objects from localStorage', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ ok: true }));
|
||||
const { result } = renderHook(() => useLocalStorage<{ ok: boolean }>('foo'));
|
||||
const [foo] = result.current;
|
||||
expect(foo!.ok).toEqual(true);
|
||||
});
|
||||
|
||||
it('safely initializes objects to localStorage', () => {
|
||||
const { result } = renderHook(() => useLocalStorage<{ ok: boolean }>('foo', { ok: true }));
|
||||
const [foo] = result.current;
|
||||
expect(foo!.ok).toEqual(true);
|
||||
});
|
||||
|
||||
it('safely sets objects to localStorage', () => {
|
||||
const { result, rerender } = renderHook(() => useLocalStorage<{ ok: any }>('foo', { ok: true }));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
act(() => setFoo({ ok: 'bar' }));
|
||||
rerender();
|
||||
|
||||
const [foo] = result.current;
|
||||
expect(foo!.ok).toEqual('bar');
|
||||
});
|
||||
|
||||
it('safely returns objects from updates', () => {
|
||||
const { result, rerender } = renderHook(() => useLocalStorage<{ ok: any }>('foo', { ok: true }));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
act(() => setFoo({ ok: 'bar' }));
|
||||
rerender();
|
||||
|
||||
const [foo] = result.current;
|
||||
expect(foo).toBeInstanceOf(Object);
|
||||
expect(foo!.ok).toEqual('bar');
|
||||
});
|
||||
|
||||
it('sets localStorage from the function updater', () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useLocalStorage<{ foo: string; fizz?: string }>('foo', { foo: 'bar' })
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('123');
|
||||
const [, setFoo] = result.current;
|
||||
act(() => setFoo(state => ({ ...state!, fizz: 'buzz' })));
|
||||
rerender();
|
||||
|
||||
const [value] = result.current;
|
||||
expect(value!.foo).toEqual('bar');
|
||||
expect(value!.fizz).toEqual('buzz');
|
||||
});
|
||||
|
||||
it('rejects nullish or undefined keys', () => {
|
||||
const { result } = renderHook(() => useLocalStorage(null as any));
|
||||
try {
|
||||
result.current;
|
||||
fail('hook should have thrown');
|
||||
} catch (e) {
|
||||
expect(String(e)).toMatch(/key may not be/i);
|
||||
}
|
||||
});
|
||||
|
||||
/* Enforces proper eslint react-hooks/rules-of-hooks usage */
|
||||
describe('eslint react-hooks/rules-of-hooks', () => {
|
||||
it('memoizes an object between rerenders', () => {
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', { ok: true }));
|
||||
|
||||
result.current; // if localStorage isn't set then r1 and r2 will be different
|
||||
rerender();
|
||||
const [r2] = result.current;
|
||||
rerender();
|
||||
const [r3] = result.current;
|
||||
expect(r2).toBe(r3);
|
||||
});
|
||||
|
||||
it('memoizes an object immediately if localStorage is already set', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ ok: true }));
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', { ok: true }));
|
||||
|
||||
const [r1] = result.current; // if localStorage isn't set then r1 and r2 will be different
|
||||
rerender();
|
||||
const [r2] = result.current;
|
||||
expect(r1).toBe(r2);
|
||||
});
|
||||
|
||||
it('memoizes the setState function', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ ok: true }));
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', { ok: true }));
|
||||
const [, s1] = result.current;
|
||||
rerender();
|
||||
const [, s2] = result.current;
|
||||
expect(s1).toBe(s2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options: raw', () => {
|
||||
it('returns a string when localStorage is a stringified object', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ fizz: 'buzz' }));
|
||||
const { result } = renderHook(() => useLocalStorage('foo', null, { raw: true }));
|
||||
const [foo] = result.current;
|
||||
expect(typeof foo).toBe('string');
|
||||
});
|
||||
|
||||
it('returns a string after an update', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ fizz: 'buzz' }));
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', null, { raw: true }));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
|
||||
act(() => setFoo({ fizz: 'bang' } as any));
|
||||
rerender();
|
||||
|
||||
const [foo] = result.current;
|
||||
expect(typeof foo).toBe('string');
|
||||
|
||||
expect(JSON.parse(foo!)).toBeInstanceOf(Object);
|
||||
|
||||
// expect(JSON.parse(foo!).fizz).toEqual('bang');
|
||||
});
|
||||
|
||||
it('still forces setState to a string', () => {
|
||||
localStorage.setItem('foo', JSON.stringify({ fizz: 'buzz' }));
|
||||
const { result, rerender } = renderHook(() => useLocalStorage('foo', null, { raw: true }));
|
||||
|
||||
const [, setFoo] = result.current;
|
||||
|
||||
act(() => setFoo({ fizz: 'bang' } as any));
|
||||
rerender();
|
||||
|
||||
const [value] = result.current;
|
||||
|
||||
expect(JSON.parse(value!).fizz).toEqual('bang');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,78 +1,14 @@
|
||||
/* eslint-disable */
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import useMeasure, { ContentRect } from '../src/useMeasure';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import useMeasure, { UseMeasureRef } from '../src/useMeasure';
|
||||
|
||||
interface Entry {
|
||||
target: HTMLElement;
|
||||
contentRect: ContentRect;
|
||||
}
|
||||
|
||||
jest.mock('resize-observer-polyfill', () => {
|
||||
return class ResizeObserver {
|
||||
private cb: (entries: Entry[]) => void;
|
||||
private map: WeakMap<HTMLElement, any>;
|
||||
private targets: HTMLElement[];
|
||||
constructor(cb: () => void) {
|
||||
this.cb = cb;
|
||||
this.map = new WeakMap();
|
||||
this.targets = [];
|
||||
}
|
||||
public disconnect() {
|
||||
this.targets.map(target => {
|
||||
const originMethod = this.map.get(target);
|
||||
target.setAttribute = originMethod;
|
||||
this.map.delete(target);
|
||||
});
|
||||
}
|
||||
public observe(target: HTMLElement) {
|
||||
const method = 'setAttribute';
|
||||
const originMethod = target[method];
|
||||
this.map.set(target, originMethod);
|
||||
this.targets.push(target);
|
||||
target[method] = (...args) => {
|
||||
const [attrName, value] = args;
|
||||
if (attrName === 'style') {
|
||||
const rect: DOMRectReadOnly = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
} as DOMRectReadOnly;
|
||||
value.split(';').map(kv => {
|
||||
const [key, v] = kv.split(':');
|
||||
if (['top', 'bottom', 'left', 'right', 'width', 'height'].includes(key)) {
|
||||
rect[key] = parseInt(v, 10);
|
||||
}
|
||||
});
|
||||
target.getBoundingClientRect = () => rect;
|
||||
}
|
||||
originMethod.apply(target, args);
|
||||
this.fireCallback();
|
||||
};
|
||||
}
|
||||
private fireCallback() {
|
||||
if (this.cb) {
|
||||
this.cb(
|
||||
this.targets.map(target => {
|
||||
return {
|
||||
target,
|
||||
contentRect: target.getBoundingClientRect() as ContentRect,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('reacts to changes in size of any of the observed elements', () => {
|
||||
it('by default, state defaults every value to -1', () => {
|
||||
const { result } = renderHook(() => useMeasure());
|
||||
const div = document.createElement('div');
|
||||
result.current[0](div);
|
||||
|
||||
act(() => {
|
||||
const div = document.createElement('div');
|
||||
(result.current[0] as UseMeasureRef)(div);
|
||||
});
|
||||
|
||||
expect(result.current[1]).toMatchObject({
|
||||
width: 0,
|
||||
height: 0,
|
||||
@ -81,9 +17,63 @@ it('reacts to changes in size of any of the observed elements', () => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
});
|
||||
act(() => div.setAttribute('style', 'width:200px;height:200px;top:100;left:100'));
|
||||
});
|
||||
|
||||
it('synchronously sets up ResizeObserver listener', () => {
|
||||
let listener: ((rect: any) => void) | undefined = undefined;
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
constructor(ls) {
|
||||
listener = ls;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useMeasure());
|
||||
|
||||
act(() => {
|
||||
const div = document.createElement('div');
|
||||
(result.current[0] as UseMeasureRef)(div);
|
||||
});
|
||||
|
||||
expect(typeof listener).toBe('function');
|
||||
});
|
||||
|
||||
it('tracks rectangle of a DOM element', () => {
|
||||
let listener: ((rect: any) => void) | undefined = undefined;
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
constructor(ls) {
|
||||
listener = ls;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useMeasure());
|
||||
|
||||
act(() => {
|
||||
const div = document.createElement('div');
|
||||
(result.current[0] as UseMeasureRef)(div);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
listener!([{
|
||||
contentRect: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 200,
|
||||
height: 200,
|
||||
top: 100,
|
||||
bottom: 0,
|
||||
left: 100,
|
||||
right: 0,
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
expect(result.current[1]).toMatchObject({
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 200,
|
||||
height: 200,
|
||||
top: 100,
|
||||
@ -92,3 +82,96 @@ it('reacts to changes in size of any of the observed elements', () => {
|
||||
right: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks multiple updates', () => {
|
||||
let listener: ((rect: any) => void) | undefined = undefined;
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
constructor(ls) {
|
||||
listener = ls;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useMeasure());
|
||||
|
||||
act(() => {
|
||||
const div = document.createElement('div');
|
||||
(result.current[0] as UseMeasureRef)(div);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
listener!([{
|
||||
contentRect: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 1,
|
||||
right: 1,
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
expect(result.current[1]).toMatchObject({
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 1,
|
||||
right: 1,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
listener!([{
|
||||
contentRect: {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
left: 2,
|
||||
right: 2,
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
expect(result.current[1]).toMatchObject({
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
left: 2,
|
||||
right: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls .disconnect() on ResizeObserver when component unmounts', () => {
|
||||
const disconnect = jest.fn();
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
disconnect() {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
const { result, unmount } = renderHook(() => useMeasure());
|
||||
|
||||
act(() => {
|
||||
const div = document.createElement('div');
|
||||
(result.current[0] as UseMeasureRef)(div);
|
||||
});
|
||||
|
||||
expect(disconnect).toHaveBeenCalledTimes(0);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user