2019-10-10 00:20:53 +08:00
|
|
|
import {
|
|
|
|
computed,
|
|
|
|
reactive,
|
|
|
|
effect,
|
|
|
|
ref,
|
2020-07-21 05:35:31 +08:00
|
|
|
WritableComputedRef,
|
2021-07-07 22:10:56 +08:00
|
|
|
isReadonly,
|
|
|
|
setComputedScheduler
|
2019-10-10 00:20:53 +08:00
|
|
|
} from '../src'
|
2018-09-21 06:03:59 +08:00
|
|
|
|
2019-08-20 21:58:10 +08:00
|
|
|
describe('reactivity/computed', () => {
|
2018-09-21 06:03:59 +08:00
|
|
|
it('should return updated value', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive<{ foo?: number }>({})
|
2018-09-21 06:03:59 +08:00
|
|
|
const cValue = computed(() => value.foo)
|
2019-05-29 18:47:29 +08:00
|
|
|
expect(cValue.value).toBe(undefined)
|
2018-09-21 06:03:59 +08:00
|
|
|
value.foo = 1
|
2019-05-29 18:47:29 +08:00
|
|
|
expect(cValue.value).toBe(1)
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
it('should compute lazily', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive<{ foo?: number }>({})
|
2018-09-21 06:03:59 +08:00
|
|
|
const getter = jest.fn(() => value.foo)
|
|
|
|
const cValue = computed(getter)
|
|
|
|
|
|
|
|
// lazy
|
|
|
|
expect(getter).not.toHaveBeenCalled()
|
|
|
|
|
2019-05-29 18:47:29 +08:00
|
|
|
expect(cValue.value).toBe(undefined)
|
2018-09-21 06:03:59 +08:00
|
|
|
expect(getter).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
// should not compute again
|
2019-05-29 18:47:29 +08:00
|
|
|
cValue.value
|
2018-09-21 06:03:59 +08:00
|
|
|
expect(getter).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
// should not compute until needed
|
|
|
|
value.foo = 1
|
|
|
|
expect(getter).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
// now it should compute
|
2019-05-29 18:47:29 +08:00
|
|
|
expect(cValue.value).toBe(1)
|
2018-09-21 06:03:59 +08:00
|
|
|
expect(getter).toHaveBeenCalledTimes(2)
|
|
|
|
|
|
|
|
// should not compute again
|
2019-05-29 18:47:29 +08:00
|
|
|
cValue.value
|
2018-09-21 06:03:59 +08:00
|
|
|
expect(getter).toHaveBeenCalledTimes(2)
|
|
|
|
})
|
|
|
|
|
2018-11-14 00:03:35 +08:00
|
|
|
it('should trigger effect', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive<{ foo?: number }>({})
|
2018-09-21 06:03:59 +08:00
|
|
|
const cValue = computed(() => value.foo)
|
|
|
|
let dummy
|
2018-11-14 00:03:35 +08:00
|
|
|
effect(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
dummy = cValue.value
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
expect(dummy).toBe(undefined)
|
|
|
|
value.foo = 1
|
|
|
|
expect(dummy).toBe(1)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should work when chained', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive({ foo: 0 })
|
2018-09-21 06:03:59 +08:00
|
|
|
const c1 = computed(() => value.foo)
|
2019-05-29 18:47:29 +08:00
|
|
|
const c2 = computed(() => c1.value + 1)
|
|
|
|
expect(c2.value).toBe(1)
|
|
|
|
expect(c1.value).toBe(0)
|
2018-09-21 06:03:59 +08:00
|
|
|
value.foo++
|
2019-05-29 18:47:29 +08:00
|
|
|
expect(c2.value).toBe(2)
|
|
|
|
expect(c1.value).toBe(1)
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
|
2018-11-14 00:03:35 +08:00
|
|
|
it('should trigger effect when chained', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive({ foo: 0 })
|
2018-09-21 06:03:59 +08:00
|
|
|
const getter1 = jest.fn(() => value.foo)
|
|
|
|
const getter2 = jest.fn(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
return c1.value + 1
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
const c1 = computed(getter1)
|
|
|
|
const c2 = computed(getter2)
|
|
|
|
|
|
|
|
let dummy
|
2018-11-14 00:03:35 +08:00
|
|
|
effect(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
dummy = c2.value
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
expect(dummy).toBe(1)
|
|
|
|
expect(getter1).toHaveBeenCalledTimes(1)
|
|
|
|
expect(getter2).toHaveBeenCalledTimes(1)
|
|
|
|
value.foo++
|
|
|
|
expect(dummy).toBe(2)
|
|
|
|
// should not result in duplicate calls
|
|
|
|
expect(getter1).toHaveBeenCalledTimes(2)
|
|
|
|
expect(getter2).toHaveBeenCalledTimes(2)
|
|
|
|
})
|
|
|
|
|
2018-11-14 00:03:35 +08:00
|
|
|
it('should trigger effect when chained (mixed invocations)', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive({ foo: 0 })
|
2018-09-21 06:03:59 +08:00
|
|
|
const getter1 = jest.fn(() => value.foo)
|
|
|
|
const getter2 = jest.fn(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
return c1.value + 1
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
const c1 = computed(getter1)
|
|
|
|
const c2 = computed(getter2)
|
|
|
|
|
|
|
|
let dummy
|
2018-11-14 00:03:35 +08:00
|
|
|
effect(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
dummy = c1.value + c2.value
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
expect(dummy).toBe(1)
|
|
|
|
|
|
|
|
expect(getter1).toHaveBeenCalledTimes(1)
|
|
|
|
expect(getter2).toHaveBeenCalledTimes(1)
|
|
|
|
value.foo++
|
|
|
|
expect(dummy).toBe(3)
|
|
|
|
// should not result in duplicate calls
|
|
|
|
expect(getter1).toHaveBeenCalledTimes(2)
|
|
|
|
expect(getter2).toHaveBeenCalledTimes(2)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should no longer update when stopped', () => {
|
2019-10-05 22:39:40 +08:00
|
|
|
const value = reactive<{ foo?: number }>({})
|
2018-09-21 06:03:59 +08:00
|
|
|
const cValue = computed(() => value.foo)
|
|
|
|
let dummy
|
2018-11-14 00:03:35 +08:00
|
|
|
effect(() => {
|
2019-05-29 18:47:29 +08:00
|
|
|
dummy = cValue.value
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|
|
|
|
expect(dummy).toBe(undefined)
|
|
|
|
value.foo = 1
|
|
|
|
expect(dummy).toBe(1)
|
2021-06-25 05:44:32 +08:00
|
|
|
cValue.effect.stop()
|
2018-09-21 06:03:59 +08:00
|
|
|
value.foo = 2
|
|
|
|
expect(dummy).toBe(1)
|
|
|
|
})
|
2019-06-06 15:19:04 +08:00
|
|
|
|
|
|
|
it('should support setter', () => {
|
2019-08-22 00:01:05 +08:00
|
|
|
const n = ref(1)
|
|
|
|
const plusOne = computed({
|
|
|
|
get: () => n.value + 1,
|
|
|
|
set: val => {
|
|
|
|
n.value = val - 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(plusOne.value).toBe(2)
|
|
|
|
n.value++
|
|
|
|
expect(plusOne.value).toBe(3)
|
|
|
|
|
|
|
|
plusOne.value = 0
|
|
|
|
expect(n.value).toBe(-1)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should trigger effect w/ setter', () => {
|
|
|
|
const n = ref(1)
|
|
|
|
const plusOne = computed({
|
|
|
|
get: () => n.value + 1,
|
|
|
|
set: val => {
|
|
|
|
n.value = val - 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
let dummy
|
|
|
|
effect(() => {
|
|
|
|
dummy = n.value
|
|
|
|
})
|
|
|
|
expect(dummy).toBe(1)
|
|
|
|
|
|
|
|
plusOne.value = 0
|
|
|
|
expect(dummy).toBe(-1)
|
2019-06-06 15:19:04 +08:00
|
|
|
})
|
2019-10-10 00:20:53 +08:00
|
|
|
|
|
|
|
it('should warn if trying to set a readonly computed', () => {
|
|
|
|
const n = ref(1)
|
|
|
|
const plusOne = computed(() => n.value + 1)
|
|
|
|
;(plusOne as WritableComputedRef<number>).value++ // Type cast to prevent TS from preventing the error
|
|
|
|
|
|
|
|
expect(
|
|
|
|
'Write operation failed: computed value is readonly'
|
|
|
|
).toHaveBeenWarnedLast()
|
|
|
|
})
|
2020-07-21 05:35:31 +08:00
|
|
|
|
|
|
|
it('should be readonly', () => {
|
|
|
|
let a = { a: 1 }
|
|
|
|
const x = computed(() => a)
|
|
|
|
expect(isReadonly(x)).toBe(true)
|
|
|
|
expect(isReadonly(x.value)).toBe(false)
|
|
|
|
expect(isReadonly(x.value.a)).toBe(false)
|
|
|
|
const z = computed<typeof a>({
|
|
|
|
get() {
|
|
|
|
return a
|
|
|
|
},
|
|
|
|
set(v) {
|
|
|
|
a = v
|
|
|
|
}
|
|
|
|
})
|
|
|
|
expect(isReadonly(z)).toBe(false)
|
|
|
|
expect(isReadonly(z.value.a)).toBe(false)
|
|
|
|
})
|
2021-05-28 08:53:21 +08:00
|
|
|
|
|
|
|
it('should expose value when stopped', () => {
|
|
|
|
const x = computed(() => 1)
|
2021-06-25 05:44:32 +08:00
|
|
|
x.effect.stop()
|
2021-05-28 08:53:21 +08:00
|
|
|
expect(x.value).toBe(1)
|
|
|
|
})
|
2021-07-07 22:10:56 +08:00
|
|
|
|
|
|
|
describe('with scheduler', () => {
|
2021-07-08 12:31:26 +08:00
|
|
|
// a simple scheduler similar to the main Vue scheduler
|
|
|
|
const tick = Promise.resolve()
|
|
|
|
const queue: any[] = []
|
|
|
|
let queued = false
|
|
|
|
|
|
|
|
const schedule = (fn: any) => {
|
|
|
|
queue.push(fn)
|
|
|
|
if (!queued) {
|
|
|
|
queued = true
|
|
|
|
tick.then(flush)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const flush = () => {
|
|
|
|
for (let i = 0; i < queue.length; i++) {
|
|
|
|
queue[i]()
|
|
|
|
}
|
|
|
|
queue.length = 0
|
|
|
|
queued = false
|
|
|
|
}
|
|
|
|
|
2021-07-07 22:10:56 +08:00
|
|
|
beforeEach(() => {
|
2021-07-08 12:31:26 +08:00
|
|
|
setComputedScheduler(schedule)
|
2021-07-07 22:10:56 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
setComputedScheduler(undefined)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('should only trigger once on multiple mutations', async () => {
|
|
|
|
const src = ref(0)
|
|
|
|
const c = computed(() => src.value)
|
|
|
|
const spy = jest.fn()
|
|
|
|
effect(() => {
|
|
|
|
spy(c.value)
|
|
|
|
})
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
|
|
src.value = 1
|
|
|
|
src.value = 2
|
|
|
|
src.value = 3
|
|
|
|
// not called yet
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1)
|
2021-07-08 12:31:26 +08:00
|
|
|
await tick
|
2021-07-07 22:10:56 +08:00
|
|
|
// should only trigger once
|
|
|
|
expect(spy).toHaveBeenCalledTimes(2)
|
|
|
|
expect(spy).toHaveBeenCalledWith(c.value)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('should not trigger if value did not change', async () => {
|
|
|
|
const src = ref(0)
|
|
|
|
const c = computed(() => src.value % 2)
|
|
|
|
const spy = jest.fn()
|
|
|
|
effect(() => {
|
|
|
|
spy(c.value)
|
|
|
|
})
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
|
|
src.value = 1
|
|
|
|
src.value = 2
|
|
|
|
|
2021-07-08 12:31:26 +08:00
|
|
|
await tick
|
2021-07-07 22:10:56 +08:00
|
|
|
// should not trigger
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
src.value = 3
|
|
|
|
src.value = 4
|
|
|
|
src.value = 5
|
2021-07-08 12:31:26 +08:00
|
|
|
await tick
|
2021-07-07 22:10:56 +08:00
|
|
|
// should trigger because latest value changes
|
|
|
|
expect(spy).toHaveBeenCalledTimes(2)
|
|
|
|
})
|
2021-07-08 12:31:26 +08:00
|
|
|
|
2021-07-09 01:41:38 +08:00
|
|
|
test('chained computed trigger', async () => {
|
|
|
|
const effectSpy = jest.fn()
|
|
|
|
const c1Spy = jest.fn()
|
|
|
|
const c2Spy = jest.fn()
|
|
|
|
|
|
|
|
const src = ref(0)
|
|
|
|
const c1 = computed(() => {
|
|
|
|
c1Spy()
|
|
|
|
return src.value % 2
|
|
|
|
})
|
|
|
|
const c2 = computed(() => {
|
|
|
|
c2Spy()
|
|
|
|
return c1.value + 1
|
|
|
|
})
|
|
|
|
|
|
|
|
effect(() => {
|
|
|
|
effectSpy(c2.value)
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(c1Spy).toHaveBeenCalledTimes(1)
|
|
|
|
expect(c2Spy).toHaveBeenCalledTimes(1)
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
src.value = 1
|
|
|
|
await tick
|
|
|
|
expect(c1Spy).toHaveBeenCalledTimes(2)
|
|
|
|
expect(c2Spy).toHaveBeenCalledTimes(2)
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(2)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('chained computed avoid re-compute', async () => {
|
|
|
|
const effectSpy = jest.fn()
|
|
|
|
const c1Spy = jest.fn()
|
|
|
|
const c2Spy = jest.fn()
|
|
|
|
|
|
|
|
const src = ref(0)
|
|
|
|
const c1 = computed(() => {
|
|
|
|
c1Spy()
|
|
|
|
return src.value % 2
|
|
|
|
})
|
|
|
|
const c2 = computed(() => {
|
|
|
|
c2Spy()
|
|
|
|
return c1.value + 1
|
|
|
|
})
|
|
|
|
|
|
|
|
effect(() => {
|
|
|
|
effectSpy(c2.value)
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
|
|
|
src.value = 2
|
|
|
|
src.value = 4
|
|
|
|
src.value = 6
|
|
|
|
await tick
|
|
|
|
// c1 should re-compute once.
|
|
|
|
expect(c1Spy).toHaveBeenCalledTimes(2)
|
|
|
|
// c2 should not have to re-compute because c1 did not change.
|
|
|
|
expect(c2Spy).toHaveBeenCalledTimes(1)
|
|
|
|
// effect should not trigger because c2 did not change.
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
|
|
|
})
|
|
|
|
|
2021-07-08 12:31:26 +08:00
|
|
|
test('chained computed value invalidation', async () => {
|
|
|
|
const effectSpy = jest.fn()
|
|
|
|
const c1Spy = jest.fn()
|
|
|
|
const c2Spy = jest.fn()
|
|
|
|
|
|
|
|
const src = ref(0)
|
|
|
|
const c1 = computed(() => {
|
|
|
|
c1Spy()
|
|
|
|
return src.value % 2
|
|
|
|
})
|
|
|
|
const c2 = computed(() => {
|
|
|
|
c2Spy()
|
|
|
|
return c1.value + 1
|
|
|
|
})
|
|
|
|
|
|
|
|
effect(() => {
|
|
|
|
effectSpy(c2.value)
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
|
|
|
expect(effectSpy).toHaveBeenCalledWith(1)
|
|
|
|
expect(c2.value).toBe(1)
|
|
|
|
|
|
|
|
expect(c1Spy).toHaveBeenCalledTimes(1)
|
|
|
|
expect(c2Spy).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
src.value = 1
|
|
|
|
// value should be available sync
|
|
|
|
expect(c2.value).toBe(2)
|
|
|
|
expect(c2Spy).toHaveBeenCalledTimes(2)
|
2021-07-09 01:41:38 +08:00
|
|
|
})
|
2021-07-08 12:31:26 +08:00
|
|
|
|
2021-07-09 01:41:38 +08:00
|
|
|
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
|
|
|
|
const effectSpy = jest.fn()
|
|
|
|
const c1Spy = jest.fn()
|
|
|
|
const c2Spy = jest.fn()
|
2021-07-08 12:31:26 +08:00
|
|
|
|
2021-07-09 01:41:38 +08:00
|
|
|
const src = ref(0)
|
|
|
|
const c1 = computed(() => {
|
|
|
|
c1Spy()
|
|
|
|
return src.value % 2
|
|
|
|
})
|
|
|
|
const c2 = computed(() => {
|
|
|
|
c2Spy()
|
|
|
|
return c1.value + 1
|
|
|
|
})
|
|
|
|
|
|
|
|
effect(() => {
|
|
|
|
effectSpy(c2.value)
|
|
|
|
})
|
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
src.value = 1
|
|
|
|
// sync access c2
|
|
|
|
c2.value
|
2021-07-08 12:31:26 +08:00
|
|
|
await tick
|
2021-07-09 01:41:38 +08:00
|
|
|
expect(effectSpy).toHaveBeenCalledTimes(2)
|
2021-07-08 12:31:26 +08:00
|
|
|
})
|
2021-07-07 22:10:56 +08:00
|
|
|
})
|
2018-09-21 06:03:59 +08:00
|
|
|
})
|