Skip to content

Commit

Permalink
test: add tests for wrapped ref and computed
Browse files Browse the repository at this point in the history
  • Loading branch information
hlysine committed Jun 11, 2023
1 parent f1d7323 commit ce7f05f
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 10 deletions.
56 changes: 55 additions & 1 deletion src/__tests__/core.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isReactive,
isReadonly,
isRef,
makeReactive,
reactive,
ref,
useComputed,
Expand All @@ -16,7 +17,7 @@ import {
} from '..';
import React from 'react';
import 'jest-performance-testing';
import { renderHook, act } from '@testing-library/react';
import { renderHook, act, render } from '@testing-library/react';
import * as helper from '../helper';

let consoleWarn: jest.SpyInstance;
Expand Down Expand Up @@ -258,6 +259,59 @@ describe('useComputed', () => {
expect(result.current.value).toBe(2);
expect(getter).toBeCalledTimes(2);
});
it('triggers reactive effects with makeReactive', async () => {
const mockEffect = jest.fn();
const count = ref(0);
const Tester = makeReactive(function Tester() {
const derived = useComputed(() => count.value + 1);
useWatch(derived, (val) => mockEffect(val));
return <div>{derived.value}</div>;
});
const { unmount, findByText } = render(<Tester />);

await expect(findByText('1')).resolves.toBeInTheDocument();
expect(mockEffect).toBeCalledTimes(0);

act(() => {
count.value++;
});

await expect(findByText('2')).resolves.toBeInTheDocument();
expect(mockEffect).toBeCalledTimes(1);

unmount();
act(() => {
count.value++;
});

expect(mockEffect).toBeCalledTimes(1);
});
it('triggers reactive effects without makeReactive', async () => {
const mockEffect = jest.fn();
const count = ref(0);
const Tester = function Tester() {
const derived = useComputed(() => count.value + 1);
useWatch(derived, (val) => mockEffect(val));
return <div>{derived.value}</div>;
};
const { unmount, findByText } = render(<Tester />);

await expect(findByText('1')).resolves.toBeInTheDocument();
expect(mockEffect).toBeCalledTimes(0);

act(() => {
count.value++;
});

expect(mockEffect).toBeCalledTimes(1);

unmount();
act(() => {
count.value++;
});

expect(mockEffect).toBeCalledTimes(1);
});
});

describe('useReactive', () => {
Expand Down
88 changes: 88 additions & 0 deletions src/__tests__/helper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { WrappedRef, createWrappedRef } from '../helper';

describe('createWrappedRef', () => {
it('traps defineProperty', () => {
const obj: { a: number; b?: number } = { a: 1 };
const wrapped = createWrappedRef(obj);
Object.defineProperty(wrapped, 'b', { value: 2 });
expect(wrapped.b).toBe(2);
});
it('traps deleteProperty', () => {
const obj: { a?: number } = { a: 1 };
const wrapped = createWrappedRef(obj);
delete wrapped.a;
expect(wrapped.a).toBeUndefined();
});
it('traps get', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(wrapped.a).toBe(obj.a);
expect(wrapped.__current__).toBe(obj);
});
it('traps getOwnPropertyDescriptor', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(Object.getOwnPropertyDescriptor(wrapped, 'a')).toEqual(
Object.getOwnPropertyDescriptor(obj, 'a')
);
});
it('traps getPrototypeOf', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(Object.getPrototypeOf(wrapped)).toBe(Object.getPrototypeOf(obj));
});
it('traps has', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect('a' in wrapped).toBe(true);
expect('b' in wrapped).toBe(false);
expect('__current__' in wrapped).toBe(false);
});
it('traps isExtensible', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(Object.isExtensible(wrapped)).toBe(true);
});
it('traps ownKeys', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(Reflect.ownKeys(wrapped)).toEqual(Reflect.ownKeys(obj));
});
it('traps preventExtensions', () => {
const obj = { a: 1 };
const wrapped = createWrappedRef(obj);
expect(() => Object.preventExtensions(wrapped)).toThrow();
});
it('traps set', () => {
const obj = { a: 1 };
const wrapped: WrappedRef<{ a: number } | { b: string }> =
createWrappedRef(obj);
wrapped.a = 2;
expect(wrapped.a).toBe(2);
expect(wrapped.a).toBe(obj.a);
expect(wrapped).toEqual(obj);

const newObj = { b: 'str' };
wrapped.__current__ = newObj;
expect(wrapped.__current__).toBe(newObj);
expect(wrapped).toEqual(newObj);
});
it('traps setPrototypeOf', () => {
const obj: { a: number; b?: number } = { a: 1 };
const wrapped = createWrappedRef(obj);
Object.setPrototypeOf(wrapped, { b: 2 });
expect(wrapped.b).toBe(2);
expect(obj.b).toBe(2);
});
it('does not accept non-extensible objects', () => {
const obj = Object.preventExtensions({ a: 1 });
expect(() => createWrappedRef(obj)).toThrow();

const obj2: { b?: number; c?: number } = { b: 2 };
const obj3 = Object.preventExtensions({
c: 4,
});
const wrapped = createWrappedRef(obj2);
expect(() => (wrapped.__current__ = obj3)).toThrow();
});
});
23 changes: 14 additions & 9 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ const WRAP_KEY = '__current__';

export type WrappedRef<T> = T & { [WRAP_KEY]: T };

const OBJ_NON_EXTENSIBLE =
'A wrapped ref cannot contain objects that are non-extensible.';

export function createWrappedRef<T>(ref: T): WrappedRef<T> {
if (!Object.isExtensible(ref)) {
throw new Error(OBJ_NON_EXTENSIBLE);
}
const obj = {
[WRAP_KEY]: ref,
} as WrappedRef<T>;
return new Proxy(obj, {
apply(target, thisArg, argArray) {
return Reflect.apply(target[WRAP_KEY] as any, thisArg, argArray);
},
construct(target, argArray, newTarget) {
return Reflect.construct(target[WRAP_KEY] as any, argArray, newTarget);
},
defineProperty(target, property, attributes) {
return Reflect.defineProperty(
target[WRAP_KEY] as any,
Expand All @@ -118,16 +118,21 @@ export function createWrappedRef<T>(ref: T): WrappedRef<T> {
return Reflect.has(target[WRAP_KEY] as any, p);
},
isExtensible(target) {
return Reflect.isExtensible(target[WRAP_KEY] as any);
return Reflect.isExtensible(target);
},
ownKeys(target) {
return Reflect.ownKeys(target[WRAP_KEY] as any);
},
preventExtensions(target) {
return Reflect.preventExtensions(target[WRAP_KEY] as any);
preventExtensions() {
throw new Error(
'The extensibility of a wrapped ref cannot be modified. It is always true.'
);
},
set(target, p, newValue, receiver) {
if (p === WRAP_KEY) {
if (!Object.isExtensible(newValue)) {
throw new Error(OBJ_NON_EXTENSIBLE);
}
target[WRAP_KEY] = newValue;
return true;
} else return Reflect.set(target[WRAP_KEY] as any, p, newValue, receiver);
Expand Down

0 comments on commit ce7f05f

Please sign in to comment.