diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index 8888aaed1..681a96099 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -1,6 +1,9 @@ import { reactive, isReactive, toRaw, markNonReactive } from '../src/reactive' +import { mockWarn } from '@vue/runtime-test' describe('reactivity/reactive', () => { + mockWarn() + test('Object', () => { const original = { foo: 1 } const observed = reactive(original) @@ -124,17 +127,11 @@ describe('reactivity/reactive', () => { }) test('non-observable values', () => { - const warn = jest.spyOn(console, 'warn') - let lastMsg: string - warn.mockImplementation(msg => { - lastMsg = msg - }) - - const getMsg = (value: any) => - `value cannot be made reactive: ${String(value)}` const assertValue = (value: any) => { reactive(value) - expect(lastMsg).toMatch(getMsg(value)) + expect( + `value cannot be made reactive: ${String(value)}` + ).toHaveBeenWarnedLast() } // number @@ -151,8 +148,6 @@ describe('reactivity/reactive', () => { const s = Symbol() assertValue(s) - warn.mockRestore() - // built-ins should work and return same value const p = Promise.resolve() expect(reactive(p)).toBe(p) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 49209857a..0c4ee1530 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -11,18 +11,10 @@ import { effect, ref } from '../src' +import { mockWarn } from '@vue/runtime-test' describe('reactivity/readonly', () => { - let warn: any - - beforeEach(() => { - warn = jest.spyOn(console, 'warn') - warn.mockImplementation(() => {}) - }) - - afterEach(() => { - warn.mockRestore() - }) + mockWarn() describe('Object', () => { it('should make nested values readonly', () => { @@ -49,16 +41,24 @@ describe('reactivity/readonly', () => { const observed: any = readonly({ foo: 1, bar: { baz: 2 } }) observed.foo = 2 expect(observed.foo).toBe(1) - expect(warn).toHaveBeenCalledTimes(1) + expect( + `Set operation on key "foo" failed: target is readonly.` + ).toHaveBeenWarnedLast() observed.bar.baz = 3 expect(observed.bar.baz).toBe(2) - expect(warn).toHaveBeenCalledTimes(2) + expect( + `Set operation on key "baz" failed: target is readonly.` + ).toHaveBeenWarnedLast() delete observed.foo expect(observed.foo).toBe(1) - expect(warn).toHaveBeenCalledTimes(3) + expect( + `Delete operation on key "foo" failed: target is readonly.` + ).toHaveBeenWarnedLast() delete observed.bar.baz expect(observed.bar.baz).toBe(2) - expect(warn).toHaveBeenCalledTimes(4) + expect( + `Delete operation on key "baz" failed: target is readonly.` + ).toHaveBeenWarnedLast() }) it('should allow mutation when unlocked', () => { @@ -73,7 +73,7 @@ describe('reactivity/readonly', () => { expect(observed.foo).toBeUndefined() expect(observed.bar.qux).toBe(3) expect('baz' in observed.bar).toBe(false) - expect(warn).not.toHaveBeenCalled() + expect(`target is readonly`).not.toHaveBeenWarned() }) it('should not trigger effects when locked', () => { @@ -128,22 +128,28 @@ describe('reactivity/readonly', () => { const observed: any = readonly([{ foo: 1 }]) observed[0] = 1 expect(observed[0]).not.toBe(1) - expect(warn).toHaveBeenCalledTimes(1) + expect( + `Set operation on key "0" failed: target is readonly.` + ).toHaveBeenWarned() observed[0].foo = 2 expect(observed[0].foo).toBe(1) - expect(warn).toHaveBeenCalledTimes(2) + expect( + `Set operation on key "foo" failed: target is readonly.` + ).toHaveBeenWarned() // should block length mutation observed.length = 0 expect(observed.length).toBe(1) expect(observed[0].foo).toBe(1) - expect(warn).toHaveBeenCalledTimes(3) + expect( + `Set operation on key "length" failed: target is readonly.` + ).toHaveBeenWarned() // mutation methods invoke set/length internally and thus are blocked as well observed.push(2) expect(observed.length).toBe(1) // push triggers two warnings on [1] and .length - expect(warn).toHaveBeenCalledTimes(5) + expect(`target is readonly.`).toHaveBeenWarnedTimes(5) }) it('should allow mutation when unlocked', () => { @@ -159,7 +165,7 @@ describe('reactivity/readonly', () => { expect(observed[2]).toBe(3) expect(observed[0].foo).toBe(2) expect(observed[0].bar.baz).toBe(3) - expect(warn).not.toHaveBeenCalled() + expect(`target is readonly`).not.toHaveBeenWarned() }) it('should not trigger effects when locked', () => { @@ -232,7 +238,9 @@ describe('reactivity/readonly', () => { map.set(key, 1) expect(dummy).toBeUndefined() expect(map.has(key)).toBe(false) - expect(warn).toHaveBeenCalledTimes(1) + expect( + `Set operation on key "${key}" failed: target is readonly.` + ).toHaveBeenWarned() }) test('should allow mutation & trigger effect when unlocked', () => { @@ -249,7 +257,7 @@ describe('reactivity/readonly', () => { lock() expect(dummy).toBe(isWeak ? 1 : 2) expect(map.get(key)).toBe(1) - expect(warn).not.toHaveBeenCalled() + expect(`target is readonly`).not.toHaveBeenWarned() }) if (Collection === Map) { @@ -301,7 +309,9 @@ describe('reactivity/readonly', () => { set.add(key) expect(dummy).toBe(false) expect(set.has(key)).toBe(false) - expect(warn).toHaveBeenCalledTimes(1) + expect( + `Add operation on key "${key}" failed: target is readonly.` + ).toHaveBeenWarned() }) test('should allow mutation & trigger effect when unlocked', () => { @@ -317,7 +327,7 @@ describe('reactivity/readonly', () => { lock() expect(dummy).toBe(true) expect(set.has(key)).toBe(true) - expect(warn).not.toHaveBeenCalled() + expect(`target is readonly`).not.toHaveBeenWarned() }) if (Collection === Set) { @@ -396,6 +406,8 @@ describe('reactivity/readonly', () => { const n: any = readonly(ref(1)) n.value = 2 expect(n.value).toBe(1) - expect(warn).toHaveBeenCalledTimes(1) + expect( + `Set operation on key "value" failed: target is readonly.` + ).toHaveBeenWarned() }) }) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 11b194b7e..34dc28e77 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -2,7 +2,7 @@ import { toRaw, reactive, readonly } from './reactive' import { track, trigger } from './effect' import { OperationTypes } from './operations' import { LOCKED } from './lock' -import { isObject } from '@vue/shared' +import { isObject, capitalize } from '@vue/shared' const toReactive = (value: any) => (isObject(value) ? reactive(value) : value) const toReadonly = (value: any) => (isObject(value) ? readonly(value) : value) @@ -166,9 +166,9 @@ function createReadonlyMethod( return function(...args: any[]) { if (LOCKED) { if (__DEV__) { - const key = args[0] ? `on key "${args[0]}"` : `` + const key = args[0] ? `on key "${args[0]}" ` : `` console.warn( - `${type} operation ${key}failed: target is readonly.`, + `${capitalize(type)} operation ${key}failed: target is readonly.`, toRaw(this) ) } diff --git a/packages/runtime-test/src/index.ts b/packages/runtime-test/src/index.ts index db8d0cad6..4c211fd9f 100644 --- a/packages/runtime-test/src/index.ts +++ b/packages/runtime-test/src/index.ts @@ -10,4 +10,5 @@ export const render = createRenderer({ export { serialize } from './serialize' export { triggerEvent } from './triggerEvent' export * from './nodeOps' +export * from './jestUtils' export * from '@vue/runtime-core' diff --git a/packages/runtime-test/src/jestUtils.ts b/packages/runtime-test/src/jestUtils.ts new file mode 100644 index 000000000..e8997103d --- /dev/null +++ b/packages/runtime-test/src/jestUtils.ts @@ -0,0 +1,83 @@ +declare global { + namespace jest { + interface Matchers { + toHaveBeenWarned(): R + toHaveBeenWarnedLast(): R + toHaveBeenWarnedTimes(n: number): R + } + } +} + +export function mockWarn() { + expect.extend({ + toHaveBeenWarned(received: string) { + const passed = warn.mock.calls.some( + args => args[0].indexOf(received) > -1 + ) + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned.` + } + } else { + const msgs = warn.mock.calls.map(args => args[0]).join('\n - ') + return { + pass: false, + message: () => + `expected "${received}" to have been warned.\n\nActual messages:\n\n - ${msgs}` + } + } + }, + + toHaveBeenWarnedLast(received: string) { + const passed = + warn.mock.calls[warn.mock.calls.length - 1][0].indexOf(received) > -1 + if (passed) { + return { + pass: true, + message: () => `expected "${received}" not to have been warned last.` + } + } else { + const msgs = warn.mock.calls.map(args => args[0]).join('\n - ') + return { + pass: false, + message: () => + `expected "${received}" to have been warned last.\n\nActual messages:\n\n - ${msgs}` + } + } + }, + + toHaveBeenWarnedTimes(received: string, n: number) { + let found = 0 + warn.mock.calls.forEach(args => { + if (args[0].indexOf(received) > -1) { + found++ + } + }) + if (found > 0) { + return { + pass: true, + message: () => + `expected "${received}" not to have been warned ${n} times.` + } + } else { + return { + pass: false, + message: () => + `expected "${received}" to have been warned ${n} times but got ${found}.` + } + } + } + }) + + let warn: jest.SpyInstance + + beforeEach(() => { + warn = jest.spyOn(console, 'warn') + warn.mockImplementation(() => {}) + }) + + afterEach(() => { + warn.mockRestore() + }) +}