import { computed, reactive, effect, ref, WritableComputedRef, isReadonly, DebuggerEvent, toRaw, TrackOpTypes, ITERATE_KEY, TriggerOpTypes } from '../src' describe('reactivity/computed', () => { it('should return updated value', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) expect(cValue.value).toBe(undefined) value.foo = 1 expect(cValue.value).toBe(1) }) it('should compute lazily', () => { const value = reactive<{ foo?: number }>({}) const getter = jest.fn(() => value.foo) const cValue = computed(getter) // lazy expect(getter).not.toHaveBeenCalled() expect(cValue.value).toBe(undefined) expect(getter).toHaveBeenCalledTimes(1) // should not compute again cValue.value expect(getter).toHaveBeenCalledTimes(1) // should not compute until needed value.foo = 1 expect(getter).toHaveBeenCalledTimes(1) // now it should compute expect(cValue.value).toBe(1) expect(getter).toHaveBeenCalledTimes(2) // should not compute again cValue.value expect(getter).toHaveBeenCalledTimes(2) }) it('should trigger effect', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) let dummy effect(() => { dummy = cValue.value }) expect(dummy).toBe(undefined) value.foo = 1 expect(dummy).toBe(1) }) it('should work when chained', () => { const value = reactive({ foo: 0 }) const c1 = computed(() => value.foo) const c2 = computed(() => c1.value + 1) expect(c2.value).toBe(1) expect(c1.value).toBe(0) value.foo++ expect(c2.value).toBe(2) expect(c1.value).toBe(1) }) it('should trigger effect when chained', () => { const value = reactive({ foo: 0 }) const getter1 = jest.fn(() => value.foo) const getter2 = jest.fn(() => { return c1.value + 1 }) const c1 = computed(getter1) const c2 = computed(getter2) let dummy effect(() => { dummy = c2.value }) 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) }) it('should trigger effect when chained (mixed invocations)', () => { const value = reactive({ foo: 0 }) const getter1 = jest.fn(() => value.foo) const getter2 = jest.fn(() => { return c1.value + 1 }) const c1 = computed(getter1) const c2 = computed(getter2) let dummy effect(() => { dummy = c1.value + c2.value }) 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', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) let dummy effect(() => { dummy = cValue.value }) expect(dummy).toBe(undefined) value.foo = 1 expect(dummy).toBe(1) cValue.effect.stop() value.foo = 2 expect(dummy).toBe(1) }) it('should support setter', () => { 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) }) it('should warn if trying to set a readonly computed', () => { const n = ref(1) const plusOne = computed(() => n.value + 1) ;(plusOne as WritableComputedRef).value++ // Type cast to prevent TS from preventing the error expect( 'Write operation failed: computed value is readonly' ).toHaveBeenWarnedLast() }) 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({ get() { return a }, set(v) { a = v } }) expect(isReadonly(z)).toBe(false) expect(isReadonly(z.value.a)).toBe(false) }) it('should expose value when stopped', () => { const x = computed(() => 1) x.effect.stop() expect(x.value).toBe(1) }) it('debug: onTrack', () => { let events: DebuggerEvent[] = [] const onTrack = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) const c = computed(() => (obj.foo, 'bar' in obj, Object.keys(obj)), { onTrack }) expect(c.value).toEqual(['foo', 'bar']) expect(onTrack).toHaveBeenCalledTimes(3) expect(events).toEqual([ { effect: c.effect, target: toRaw(obj), type: TrackOpTypes.GET, key: 'foo' }, { effect: c.effect, target: toRaw(obj), type: TrackOpTypes.HAS, key: 'bar' }, { effect: c.effect, target: toRaw(obj), type: TrackOpTypes.ITERATE, key: ITERATE_KEY } ]) }) it('debug: onTrigger', () => { let events: DebuggerEvent[] = [] const onTrigger = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1 }) const c = computed(() => obj.foo, { onTrigger }) // computed won't trigger compute until accessed c.value obj.foo++ expect(c.value).toBe(2) expect(onTrigger).toHaveBeenCalledTimes(1) expect(events[0]).toEqual({ effect: c.effect, target: toRaw(obj), type: TriggerOpTypes.SET, key: 'foo', oldValue: 1, newValue: 2 }) // @ts-ignore delete obj.foo expect(c.value).toBeUndefined() expect(onTrigger).toHaveBeenCalledTimes(2) expect(events[1]).toEqual({ effect: c.effect, target: toRaw(obj), type: TriggerOpTypes.DELETE, key: 'foo', oldValue: 2 }) }) })