vue3-yuanma/packages/reactivity/__tests__/ref.spec.ts
2022-01-18 09:22:41 +08:00

404 lines
8.7 KiB
TypeScript

import {
ref,
effect,
reactive,
isRef,
toRef,
toRefs,
Ref,
isReactive
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
import { isShallow } from '../src/reactive'
describe('reactivity/ref', () => {
it('should hold a value', () => {
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
it('should be reactive', () => {
const a = ref(1)
let dummy
let calls = 0
effect(() => {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
})
it('should make nested properties reactive', () => {
const a = ref({
count: 1
})
let dummy
effect(() => {
dummy = a.value.count
})
expect(dummy).toBe(1)
a.value.count = 2
expect(dummy).toBe(2)
})
it('should work without initial value', () => {
const a = ref()
let dummy
effect(() => {
dummy = a.value
})
expect(dummy).toBe(undefined)
a.value = 2
expect(dummy).toBe(2)
})
it('should work like a normal property when nested in a reactive object', () => {
const a = ref(1)
const obj = reactive({
a,
b: {
c: a
}
})
let dummy1: number
let dummy2: number
effect(() => {
dummy1 = obj.a
dummy2 = obj.b.c
})
const assertDummiesEqualTo = (val: number) =>
[dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
assertDummiesEqualTo(1)
a.value++
assertDummiesEqualTo(2)
obj.a++
assertDummiesEqualTo(3)
obj.b.c++
assertDummiesEqualTo(4)
})
it('should unwrap nested ref in types', () => {
const a = ref(0)
const b = ref(a)
expect(typeof (b.value + 1)).toBe('number')
})
it('should unwrap nested values in types', () => {
const a = {
b: ref(0)
}
const c = ref(a)
expect(typeof (c.value.b + 1)).toBe('number')
})
it('should NOT unwrap ref types nested inside arrays', () => {
const arr = ref([1, ref(3)]).value
expect(isRef(arr[0])).toBe(false)
expect(isRef(arr[1])).toBe(true)
expect((arr[1] as Ref).value).toBe(3)
})
it('should unwrap ref types as props of arrays', () => {
const arr = [ref(0)]
const symbolKey = Symbol('')
arr['' as any] = ref(1)
arr[symbolKey as any] = ref(2)
const arrRef = ref(arr).value
expect(isRef(arrRef[0])).toBe(true)
expect(isRef(arrRef['' as any])).toBe(false)
expect(isRef(arrRef[symbolKey as any])).toBe(false)
expect(arrRef['' as any]).toBe(1)
expect(arrRef[symbolKey as any]).toBe(2)
})
it('should keep tuple types', () => {
const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
0,
'1',
{ a: 1 },
() => 0,
ref(0)
]
const tupleRef = ref(tuple)
tupleRef.value[0]++
expect(tupleRef.value[0]).toBe(1)
tupleRef.value[1] += '1'
expect(tupleRef.value[1]).toBe('11')
tupleRef.value[2].a++
expect(tupleRef.value[2].a).toBe(2)
expect(tupleRef.value[3]()).toBe(0)
tupleRef.value[4].value++
expect(tupleRef.value[4].value).toBe(1)
})
it('should keep symbols', () => {
const customSymbol = Symbol()
const obj = {
[Symbol.asyncIterator]: ref(1),
[Symbol.hasInstance]: { a: ref('a') },
[Symbol.isConcatSpreadable]: { b: ref(true) },
[Symbol.iterator]: [ref(1)],
[Symbol.match]: new Set<Ref<number>>(),
[Symbol.matchAll]: new Map<number, Ref<string>>(),
[Symbol.replace]: { arr: [ref('a')] },
[Symbol.search]: { set: new Set<Ref<number>>() },
[Symbol.species]: { map: new Map<number, Ref<string>>() },
[Symbol.split]: new WeakSet<Ref<boolean>>(),
[Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
[Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
[Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
[customSymbol]: { arr: [ref(1)] }
}
const objRef = ref(obj)
const keys: (keyof typeof obj)[] = [
Symbol.asyncIterator,
Symbol.hasInstance,
Symbol.isConcatSpreadable,
Symbol.iterator,
Symbol.match,
Symbol.matchAll,
Symbol.replace,
Symbol.search,
Symbol.species,
Symbol.split,
Symbol.toPrimitive,
Symbol.toStringTag,
Symbol.unscopables,
customSymbol
]
keys.forEach(key => {
expect(objRef.value[key]).toStrictEqual(obj[key])
})
})
test('unref', () => {
expect(unref(1)).toBe(1)
expect(unref(ref(1))).toBe(1)
})
test('shallowRef', () => {
const sref = shallowRef({ a: 1 })
expect(isReactive(sref.value)).toBe(false)
let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)
sref.value = { a: 2 }
expect(isReactive(sref.value)).toBe(false)
expect(dummy).toBe(2)
})
test('shallowRef force trigger', () => {
const sref = shallowRef({ a: 1 })
let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)
sref.value.a = 2
expect(dummy).toBe(1) // should not trigger yet
// force trigger
triggerRef(sref)
expect(dummy).toBe(2)
})
test('shallowRef isShallow', () => {
expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
})
test('isRef', () => {
expect(isRef(ref(1))).toBe(true)
expect(isRef(computed(() => 1))).toBe(true)
expect(isRef(0)).toBe(false)
expect(isRef(1)).toBe(false)
// an object that looks like a ref isn't necessarily a ref
expect(isRef({ value: 0 })).toBe(false)
})
test('toRef', () => {
const a = reactive({
x: 1
})
const x = toRef(a, 'x')
expect(isRef(x)).toBe(true)
expect(x.value).toBe(1)
// source -> proxy
a.x = 2
expect(x.value).toBe(2)
// proxy -> source
x.value = 3
expect(a.x).toBe(3)
// reactivity
let dummyX
effect(() => {
dummyX = x.value
})
expect(dummyX).toBe(x.value)
// mutating source should trigger effect using the proxy refs
a.x = 4
expect(dummyX).toBe(4)
// should keep ref
const r = { x: ref(1) }
expect(toRef(r, 'x')).toBe(r.x)
})
test('toRef default value', () => {
const a: { x: number | undefined } = { x: undefined }
const x = toRef(a, 'x', 1)
expect(x.value).toBe(1)
a.x = 2
expect(x.value).toBe(2)
a.x = undefined
expect(x.value).toBe(1)
})
test('toRefs', () => {
const a = reactive({
x: 1,
y: 2
})
const { x, y } = toRefs(a)
expect(isRef(x)).toBe(true)
expect(isRef(y)).toBe(true)
expect(x.value).toBe(1)
expect(y.value).toBe(2)
// source -> proxy
a.x = 2
a.y = 3
expect(x.value).toBe(2)
expect(y.value).toBe(3)
// proxy -> source
x.value = 3
y.value = 4
expect(a.x).toBe(3)
expect(a.y).toBe(4)
// reactivity
let dummyX, dummyY
effect(() => {
dummyX = x.value
dummyY = y.value
})
expect(dummyX).toBe(x.value)
expect(dummyY).toBe(y.value)
// mutating source should trigger effect using the proxy refs
a.x = 4
a.y = 5
expect(dummyX).toBe(4)
expect(dummyY).toBe(5)
})
test('toRefs should warn on plain object', () => {
toRefs({})
expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
})
test('toRefs should warn on plain array', () => {
toRefs([])
expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
})
test('toRefs reactive array', () => {
const arr = reactive(['a', 'b', 'c'])
const refs = toRefs(arr)
expect(Array.isArray(refs)).toBe(true)
refs[0].value = '1'
expect(arr[0]).toBe('1')
arr[1] = '2'
expect(refs[1].value).toBe('2')
})
test('customRef', () => {
let value = 1
let _trigger: () => void
const custom = customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue: number) {
value = newValue
_trigger = trigger
}
}))
expect(isRef(custom)).toBe(true)
let dummy
effect(() => {
dummy = custom.value
})
expect(dummy).toBe(1)
custom.value = 2
// should not trigger yet
expect(dummy).toBe(1)
_trigger!()
expect(dummy).toBe(2)
})
test('should not trigger when setting value to same proxy', () => {
const obj = reactive({ count: 0 })
const a = ref(obj)
const spy1 = jest.fn(() => a.value)
effect(spy1)
a.value = obj
expect(spy1).toBeCalledTimes(1)
const b = shallowRef(obj)
const spy2 = jest.fn(() => b.value)
effect(spy2)
b.value = obj
expect(spy2).toBeCalledTimes(1)
})
})