From c2d0c29cd1098767e718473f4cb7087952f58f6b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 28 Mar 2024 16:59:50 +0100 Subject: [PATCH] test: add more for subscriptions --- .../pinia/__tests__/subscriptions.spec.ts | 572 +++++++++--------- 1 file changed, 296 insertions(+), 276 deletions(-) diff --git a/packages/pinia/__tests__/subscriptions.spec.ts b/packages/pinia/__tests__/subscriptions.spec.ts index 3a1eb4f041..1c280a5249 100644 --- a/packages/pinia/__tests__/subscriptions.spec.ts +++ b/packages/pinia/__tests__/subscriptions.spec.ts @@ -1,337 +1,357 @@ import { beforeEach, describe, it, expect, vi } from 'vitest' import { createPinia, defineStore, MutationType, setActivePinia } from '../src' import { mount } from '@vue/test-utils' -import { nextTick } from 'vue' +import { nextTick, ref } from 'vue' describe('Subscriptions', () => { - const useStore = defineStore({ - id: 'main', + const useOptionsStore = defineStore('main', { state: () => ({ user: 'Eduardo', }), }) - beforeEach(() => { - setActivePinia(createPinia()) + const useSetupStore = defineStore('main', () => { + return { + user: ref('Eduardo'), + } }) - it('fires callback when patch is applied', () => { - const store = useStore() - const spy = vi.fn() - store.$subscribe(spy, { flush: 'sync' }) - store.$state.user = 'Cleiton' - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - storeId: 'main', - type: MutationType.direct, - }), - store.$state - ) + beforeEach(() => { + setActivePinia(createPinia()) }) - it('subscribe to changes done via patch', () => { - const store = useStore() - const spy = vi.fn() - store.$subscribe(spy, { flush: 'sync' }) - - const patch = { user: 'Cleiton' } - store.$patch(patch) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - payload: patch, - storeId: 'main', - type: MutationType.patchObject, - }), - store.$state - ) - }) - const flushOptions = ['post', 'pre', 'sync'] as const + describe.each([ + { useStore: useOptionsStore, name: 'Options Stores' }, + { useStore: useSetupStore, name: 'Setup Stores' }, + ])('with: $name', ({ useStore }) => { + it('fires callback changed through $state', () => { + const store = useStore() + const spy = vi.fn() + store.$subscribe(spy, { flush: 'sync' }) + store.$state.user = 'Cleiton' + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + storeId: 'main', + type: MutationType.direct, + }), + store.$state + ) + }) - flushOptions.forEach((flush) => { - it('calls once inside components with flush ' + flush, async () => { - const pinia = createPinia() - setActivePinia(pinia) - const spy1 = vi.fn() + it('fires callback when changed througg store', async () => { + const store = useStore() + const spy = vi.fn() + store.$subscribe(spy) + expect(spy).toHaveBeenCalledTimes(0) + store.user = 'Cleiton' + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + }) - mount( - { - setup() { - const s1 = useStore() - s1.$subscribe(spy1, { flush }) - }, - template: `

`, - }, - { global: { plugins: [pinia] } } + it('subscribe to changes done via patch', () => { + const store = useStore() + const spy = vi.fn() + store.$subscribe(spy, { flush: 'sync' }) + + const patch = { user: 'Cleiton' } + store.$patch(patch) + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + payload: patch, + storeId: 'main', + type: MutationType.patchObject, + }), + store.$state ) + }) + const flushOptions = ['post', 'pre', 'sync'] as const + + flushOptions.forEach((flush) => { + it('calls once inside components with flush ' + flush, async () => { + const pinia = createPinia() + setActivePinia(pinia) + const spy1 = vi.fn() + + mount( + { + setup() { + const s1 = useStore() + s1.$subscribe(spy1, { flush }) + }, + template: `

`, + }, + { global: { plugins: [pinia] } } + ) + + const s1 = useStore() + + expect(spy1).toHaveBeenCalledTimes(0) + + s1.user = 'Edu' + await nextTick() + await nextTick() + expect(spy1).toHaveBeenCalledTimes(1) + + s1.$patch({ user: 'a' }) + await nextTick() + await nextTick() + expect(spy1).toHaveBeenCalledTimes(2) + + s1.$patch((state) => { + state.user = 'other' + }) + await nextTick() + await nextTick() + expect(spy1).toHaveBeenCalledTimes(3) + }) + }) + + it('works with multiple different flush', async () => { + const spyPre = vi.fn() + const spyPost = vi.fn() + const spySync = vi.fn() const s1 = useStore() + s1.$subscribe(spyPre, { flush: 'pre' }) + s1.$subscribe(spyPost, { flush: 'post' }) + s1.$subscribe(spySync, { flush: 'sync' }) - expect(spy1).toHaveBeenCalledTimes(0) + expect(spyPre).toHaveBeenCalledTimes(0) + expect(spyPost).toHaveBeenCalledTimes(0) + expect(spySync).toHaveBeenCalledTimes(0) s1.user = 'Edu' + expect(spyPre).toHaveBeenCalledTimes(0) + expect(spyPost).toHaveBeenCalledTimes(0) + expect(spySync).toHaveBeenCalledTimes(1) await nextTick() - await nextTick() - expect(spy1).toHaveBeenCalledTimes(1) + expect(spyPre).toHaveBeenCalledTimes(1) + expect(spyPost).toHaveBeenCalledTimes(1) + expect(spySync).toHaveBeenCalledTimes(1) s1.$patch({ user: 'a' }) + // patch still triggers all subscriptions immediately + expect(spyPre).toHaveBeenCalledTimes(2) + expect(spyPost).toHaveBeenCalledTimes(2) + expect(spySync).toHaveBeenCalledTimes(2) await nextTick() - await nextTick() - expect(spy1).toHaveBeenCalledTimes(2) + expect(spyPre).toHaveBeenCalledTimes(2) + expect(spyPost).toHaveBeenCalledTimes(2) + expect(spySync).toHaveBeenCalledTimes(2) s1.$patch((state) => { state.user = 'other' }) + expect(spyPre).toHaveBeenCalledTimes(3) + expect(spyPost).toHaveBeenCalledTimes(3) + expect(spySync).toHaveBeenCalledTimes(3) await nextTick() + expect(spyPre).toHaveBeenCalledTimes(3) + expect(spyPost).toHaveBeenCalledTimes(3) + expect(spySync).toHaveBeenCalledTimes(3) + }) + + it('works with multiple different flush and multiple state changes', async () => { + const spyPre = vi.fn() + const spyPost = vi.fn() + const spySync = vi.fn() + + const s1 = useStore() + s1.$subscribe(spyPre, { flush: 'pre' }) + s1.$subscribe(spyPost, { flush: 'post' }) + s1.$subscribe(spySync, { flush: 'sync' }) + + s1.user = 'Edu' + expect(spyPre).toHaveBeenCalledTimes(0) + expect(spyPost).toHaveBeenCalledTimes(0) + expect(spySync).toHaveBeenCalledTimes(1) + s1.$patch({ user: 'a' }) + expect(spyPre).toHaveBeenCalledTimes(1) + expect(spyPost).toHaveBeenCalledTimes(1) + expect(spySync).toHaveBeenCalledTimes(2) await nextTick() - expect(spy1).toHaveBeenCalledTimes(3) + expect(spyPre).toHaveBeenCalledTimes(1) + expect(spyPost).toHaveBeenCalledTimes(1) + expect(spySync).toHaveBeenCalledTimes(2) }) - }) - it('works with multiple different flush', async () => { - const spyPre = vi.fn() - const spyPost = vi.fn() - const spySync = vi.fn() - - const s1 = useStore() - s1.$subscribe(spyPre, { flush: 'pre' }) - s1.$subscribe(spyPost, { flush: 'post' }) - s1.$subscribe(spySync, { flush: 'sync' }) - - expect(spyPre).toHaveBeenCalledTimes(0) - expect(spyPost).toHaveBeenCalledTimes(0) - expect(spySync).toHaveBeenCalledTimes(0) - - s1.user = 'Edu' - expect(spyPre).toHaveBeenCalledTimes(0) - expect(spyPost).toHaveBeenCalledTimes(0) - expect(spySync).toHaveBeenCalledTimes(1) - await nextTick() - expect(spyPre).toHaveBeenCalledTimes(1) - expect(spyPost).toHaveBeenCalledTimes(1) - expect(spySync).toHaveBeenCalledTimes(1) - - s1.$patch({ user: 'a' }) - // patch still triggers all subscriptions immediately - expect(spyPre).toHaveBeenCalledTimes(2) - expect(spyPost).toHaveBeenCalledTimes(2) - expect(spySync).toHaveBeenCalledTimes(2) - await nextTick() - expect(spyPre).toHaveBeenCalledTimes(2) - expect(spyPost).toHaveBeenCalledTimes(2) - expect(spySync).toHaveBeenCalledTimes(2) - - s1.$patch((state) => { - state.user = 'other' + it('unsubscribes callback when unsubscribe is called', () => { + const spy = vi.fn() + const store = useStore() + const unsubscribe = store.$subscribe(spy, { flush: 'sync' }) + unsubscribe() + store.$state.user = 'Cleiton' + expect(spy).not.toHaveBeenCalled() }) - expect(spyPre).toHaveBeenCalledTimes(3) - expect(spyPost).toHaveBeenCalledTimes(3) - expect(spySync).toHaveBeenCalledTimes(3) - await nextTick() - expect(spyPre).toHaveBeenCalledTimes(3) - expect(spyPost).toHaveBeenCalledTimes(3) - expect(spySync).toHaveBeenCalledTimes(3) - }) - it('works with multiple different flush and multiple state changes', async () => { - const spyPre = vi.fn() - const spyPost = vi.fn() - const spySync = vi.fn() - - const s1 = useStore() - s1.$subscribe(spyPre, { flush: 'pre' }) - s1.$subscribe(spyPost, { flush: 'post' }) - s1.$subscribe(spySync, { flush: 'sync' }) - - s1.user = 'Edu' - expect(spyPre).toHaveBeenCalledTimes(0) - expect(spyPost).toHaveBeenCalledTimes(0) - expect(spySync).toHaveBeenCalledTimes(1) - s1.$patch({ user: 'a' }) - expect(spyPre).toHaveBeenCalledTimes(1) - expect(spyPost).toHaveBeenCalledTimes(1) - expect(spySync).toHaveBeenCalledTimes(2) - await nextTick() - expect(spyPre).toHaveBeenCalledTimes(1) - expect(spyPost).toHaveBeenCalledTimes(1) - expect(spySync).toHaveBeenCalledTimes(2) - }) + it('listeners are not affected when unsubscribe is called multiple times', () => { + const func1 = vi.fn() + const func2 = vi.fn() + const store = useStore() + const unsubscribe1 = store.$subscribe(func1, { flush: 'sync' }) + store.$subscribe(func2, { flush: 'sync' }) + unsubscribe1() + unsubscribe1() + store.$state.user = 'Cleiton' + expect(func1).not.toHaveBeenCalled() + expect(func2).toHaveBeenCalledTimes(1) + }) - it('unsubscribes callback when unsubscribe is called', () => { - const spy = vi.fn() - const store = useStore() - const unsubscribe = store.$subscribe(spy, { flush: 'sync' }) - unsubscribe() - store.$state.user = 'Cleiton' - expect(spy).not.toHaveBeenCalled() - }) + describe('multiple', () => { + it('triggers subscribe only once', async () => { + const s1 = useStore() + const s2 = useStore() - it('listeners are not affected when unsubscribe is called multiple times', () => { - const func1 = vi.fn() - const func2 = vi.fn() - const store = useStore() - const unsubscribe1 = store.$subscribe(func1, { flush: 'sync' }) - store.$subscribe(func2, { flush: 'sync' }) - unsubscribe1() - unsubscribe1() - store.$state.user = 'Cleiton' - expect(func1).not.toHaveBeenCalled() - expect(func2).toHaveBeenCalledTimes(1) - }) + const spy1 = vi.fn() + const spy2 = vi.fn() - describe('multiple', () => { - it('triggers subscribe only once', async () => { - const s1 = useStore() - const s2 = useStore() + s1.$subscribe(spy1, { flush: 'sync' }) + s2.$subscribe(spy2, { flush: 'sync' }) - const spy1 = vi.fn() - const spy2 = vi.fn() + expect(spy1).toHaveBeenCalledTimes(0) + expect(spy2).toHaveBeenCalledTimes(0) - s1.$subscribe(spy1, { flush: 'sync' }) - s2.$subscribe(spy2, { flush: 'sync' }) + s1.user = 'Edu' - expect(spy1).toHaveBeenCalledTimes(0) - expect(spy2).toHaveBeenCalledTimes(0) + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(1) + }) - s1.user = 'Edu' + it('triggers pre subscriptions only once on $patch', async () => { + const s1 = useStore() + const spy1 = vi.fn() + + s1.$subscribe(spy1, { flush: 'pre' }) + + // First mutation: works as expected + s1.$patch({ user: 'Edu' }) + // anything else than awaiting a non promise or Promise.resolve() works + await false + // await Promise.resolve(false) + // adding an extra await works + // await false + // adding any other delay also works + // await delay(20) + // await nextTick() + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy1).not.toHaveBeenCalledWith( + expect.objectContaining({ type: MutationType.direct }), + s1.$state + ) + + s1.$patch({ user: 'Myk' }) + await nextTick() + + expect(spy1).toHaveBeenCalledTimes(2) + expect(spy1).not.toHaveBeenCalledWith( + expect.objectContaining({ type: MutationType.direct }), + s1.$state + ) + }) - expect(spy1).toHaveBeenCalledTimes(1) - expect(spy2).toHaveBeenCalledTimes(1) + it('removes on unmount', async () => { + const pinia = createPinia() + setActivePinia(pinia) + const spy1 = vi.fn() + const spy2 = vi.fn() + + const wrapper = mount( + { + setup() { + const s1 = useStore() + s1.$subscribe(spy1, { flush: 'sync' }) + }, + template: `

`, + }, + { global: { plugins: [pinia] } } + ) + + const s1 = useStore() + const s2 = useStore() + + s2.$subscribe(spy2, { flush: 'sync' }) + + expect(spy1).toHaveBeenCalledTimes(0) + expect(spy2).toHaveBeenCalledTimes(0) + + s1.user = 'Edu' + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(1) + + s1.$patch({ user: 'a' }) + expect(spy1).toHaveBeenCalledTimes(2) + expect(spy2).toHaveBeenCalledTimes(2) + + s1.$patch((state) => { + state.user = 'other' + }) + expect(spy1).toHaveBeenCalledTimes(3) + expect(spy2).toHaveBeenCalledTimes(3) + + wrapper.unmount() + await nextTick() + + s1.$patch({ user: 'b' }) + expect(spy1).toHaveBeenCalledTimes(3) + expect(spy2).toHaveBeenCalledTimes(4) + s1.$patch((state) => { + state.user = 'c' + }) + expect(spy1).toHaveBeenCalledTimes(3) + expect(spy2).toHaveBeenCalledTimes(5) + s1.user = 'd' + expect(spy1).toHaveBeenCalledTimes(3) + expect(spy2).toHaveBeenCalledTimes(6) + }) }) - it('triggers pre subscriptions only once on $patch', async () => { - const s1 = useStore() - const spy1 = vi.fn() - - s1.$subscribe(spy1, { flush: 'pre' }) - - // First mutation: works as expected - s1.$patch({ user: 'Edu' }) - // anything else than awaiting a non promise or Promise.resolve() works - await false - // await Promise.resolve(false) - // adding an extra await works - // await false - // adding any other delay also works - // await delay(20) - // await nextTick() - expect(spy1).toHaveBeenCalledTimes(1) - expect(spy1).not.toHaveBeenCalledWith( - expect.objectContaining({ type: MutationType.direct }), - s1.$state - ) - - s1.$patch({ user: 'Myk' }) + it('subscribe is post by default', async () => { + const spy = vi.fn() + const store = useStore() + store.$subscribe(spy) + store.$state.user = 'Cleiton' + expect(spy).toHaveBeenCalledTimes(0) await nextTick() - - expect(spy1).toHaveBeenCalledTimes(2) - expect(spy1).not.toHaveBeenCalledWith( - expect.objectContaining({ type: MutationType.direct }), - s1.$state + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + storeId: 'main', + type: MutationType.direct, + }), + store.$state ) }) - it('removes on unmount', async () => { - const pinia = createPinia() - setActivePinia(pinia) + it('subscribe once with patch', () => { const spy1 = vi.fn() const spy2 = vi.fn() - - const wrapper = mount( - { - setup() { - const s1 = useStore() - s1.$subscribe(spy1, { flush: 'sync' }) + const store = useStore() + function once() { + const unsubscribe = store.$subscribe( + () => { + spy1() + unsubscribe() }, - template: `

`, - }, - { global: { plugins: [pinia] } } - ) - - const s1 = useStore() - const s2 = useStore() - - s2.$subscribe(spy2, { flush: 'sync' }) - + { flush: 'sync' } + ) + } + once() + store.$subscribe(spy2, { flush: 'sync' }) expect(spy1).toHaveBeenCalledTimes(0) expect(spy2).toHaveBeenCalledTimes(0) - - s1.user = 'Edu' + store.$patch((state) => { + state.user = 'a' + }) expect(spy1).toHaveBeenCalledTimes(1) expect(spy2).toHaveBeenCalledTimes(1) - - s1.$patch({ user: 'a' }) - expect(spy1).toHaveBeenCalledTimes(2) - expect(spy2).toHaveBeenCalledTimes(2) - - s1.$patch((state) => { - state.user = 'other' - }) - expect(spy1).toHaveBeenCalledTimes(3) - expect(spy2).toHaveBeenCalledTimes(3) - - wrapper.unmount() - await nextTick() - - s1.$patch({ user: 'b' }) - expect(spy1).toHaveBeenCalledTimes(3) - expect(spy2).toHaveBeenCalledTimes(4) - s1.$patch((state) => { - state.user = 'c' + store.$patch((state) => { + state.user = 'b' }) - expect(spy1).toHaveBeenCalledTimes(3) - expect(spy2).toHaveBeenCalledTimes(5) - s1.user = 'd' - expect(spy1).toHaveBeenCalledTimes(3) - expect(spy2).toHaveBeenCalledTimes(6) - }) - }) - - it('subscribe is post by default', async () => { - const spy = vi.fn() - const store = useStore() - store.$subscribe(spy) - store.$state.user = 'Cleiton' - expect(spy).toHaveBeenCalledTimes(0) - await nextTick() - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - storeId: 'main', - type: MutationType.direct, - }), - store.$state - ) - }) - - it('subscribe once with patch', () => { - const spy1 = vi.fn() - const spy2 = vi.fn() - const store = useStore() - function once() { - const unsubscribe = store.$subscribe( - () => { - spy1() - unsubscribe() - }, - { flush: 'sync' } - ) - } - once() - store.$subscribe(spy2, { flush: 'sync' }) - expect(spy1).toHaveBeenCalledTimes(0) - expect(spy2).toHaveBeenCalledTimes(0) - store.$patch((state) => { - state.user = 'a' - }) - expect(spy1).toHaveBeenCalledTimes(1) - expect(spy2).toHaveBeenCalledTimes(1) - store.$patch((state) => { - state.user = 'b' + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(2) }) - expect(spy1).toHaveBeenCalledTimes(1) - expect(spy2).toHaveBeenCalledTimes(2) }) })