diff --git a/packages/core/useStorageAsync/index.test.ts b/packages/core/useStorageAsync/index.test.ts new file mode 100644 index 00000000000..6f7dc7d0f33 --- /dev/null +++ b/packages/core/useStorageAsync/index.test.ts @@ -0,0 +1,495 @@ +import type { SerializerAsync } from '../useStorage' +import { debounceFilter } from '@vueuse/shared' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref as deepRef, nextTick, toRaw } from 'vue' +import { nextTwoTick, useSetup } from '../../.test' +import { useStorageAsync } from './index' + +const KEY = 'custom-key' + +vi.mock('../ssr-handlers', () => ({ + getSSRHandler: vi.fn().mockImplementation((name, fallback) => { + if (name === 'getDefaultStorageAsync') { + return () => { + return { + getItem: vi.fn().mockResolvedValue(null), + setItem: vi.fn().mockResolvedValue(undefined), + removeItem: vi.fn().mockResolvedValue(undefined), + } + } + } + return fallback + }), +})) + +describe('useStorageAsync', () => { + console.error = vi.fn() + const storageState = new Map() + const storageAsyncMock = { + getItem: vi.fn(async (key: string) => { + return storageState.get(key) ?? null + }), + setItem: vi.fn(async (key: string, value: string) => { + storageState.set(key, value) + }), + removeItem: vi.fn(async (key: string) => { + storageState.delete(key) + }), + clear: vi.fn(async () => { + storageState.clear() + }), + } + + beforeEach(() => { + storageState.clear() + vi.clearAllMocks() + }) + + it('export module', () => { + expect(useStorageAsync).toBeDefined() + }) + + it('string', async () => { + const vm = useSetup(() => { + const ref = useStorageAsync(KEY, 'a', storageAsyncMock) + return { ref } + }) + + await nextTwoTick() + expect(vm.ref).toBe('a') + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'a') + + vm.ref = 'b' + await nextTwoTick() + expect(vm.ref).toBe('b') + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'b') + }) + + it('number', async () => { + storageState.set(KEY, '0') + + const store = useStorageAsync(KEY, 1, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe(0) + + store.value = 2 + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '2') + + store.value = -1 + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '-1') + + store.value = 2.3 + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '2.3') + }) + + it('boolean', async () => { + const store = useStorageAsync(KEY, true, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe(true) + + store.value = false + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'false') + + store.value = true + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'true') + }) + + it('null string', async () => { + storageState.set(KEY, 'null') + + const store = useStorageAsync(KEY, null, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe('null') + }) + + it('null value', async () => { + const store = useStorageAsync(KEY, null, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe(null) + }) + + it('undefined value', async () => { + const store = useStorageAsync(KEY, undefined, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe(undefined) + }) + + it('remove value', async () => { + storageState.set(KEY, 'random') + + const store = useStorageAsync(KEY, null, storageAsyncMock) + await nextTwoTick() + expect(store.value).toBe('random') + + store.value = null + await nextTwoTick() + expect(store.value).toBe(null) + expect(storageAsyncMock.removeItem).toHaveBeenCalledWith(KEY) + }) + + it('object', async () => { + const store = useStorageAsync(KEY, { + name: 'a', + data: 123, + }, storageAsyncMock) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"a","data":123}') + + expect(store.value).toEqual({ + name: 'a', + data: 123, + }) + + const storeRaw = toRaw(store.value) + store.value.name = 'b' + await nextTwoTick() + + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"b","data":123}') + expect(storeRaw).toBe(toRaw(store.value)) + + store.value.data = 321 + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"b","data":321}') + + store.value = null + await nextTwoTick() + expect(storageAsyncMock.removeItem).toHaveBeenCalledWith(KEY) + }) + + it('array', async () => { + const store = useStorageAsync(KEY, [1, 2, 3], storageAsyncMock) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[1,2,3]') + expect(store.value).toEqual([1, 2, 3]) + + store.value.push(4) + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[1,2,3,4]') + }) + + it('map', async () => { + const store = useStorageAsync(KEY, new Map([ + [1, 'a'], + [2, 2], + ]), storageAsyncMock) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[[1,"a"],[2,2]]') + expect(store.value).toEqual(new Map([[1, 'a'], [2, 2]])) + + store.value.set(1, 'c') + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[[1,"c"],[2,2]]') + + store.value.set(2, 3) + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[[1,"c"],[2,3]]') + }) + + it('set', async () => { + const store = useStorageAsync(KEY, new Set([1, '2']), storageAsyncMock) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[1,"2"]') + expect(store.value).toEqual(new Set([1, '2'])) + + store.value.add('1') + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '[1,"2","1"]') + + store.value.delete(1) + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '["2","1"]') + }) + + it('date', async () => { + storageState.set(KEY, '2000-01-01T00:00:00.000Z') + + const store = useStorageAsync(KEY, new Date('2000-01-02T00:00:00.000Z'), storageAsyncMock) + await nextTwoTick() + expect(store.value).toEqual(new Date('2000-01-01T00:00:00.000Z')) + + store.value = new Date('2000-01-03T00:00:00.000Z') + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '2000-01-03T00:00:00.000Z') + }) + + it('pass ref as initialValue', async () => { + const init = deepRef({ + name: 'a', + data: 123, + }) + const state = useStorageAsync(KEY, init, storageAsyncMock) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"a","data":123}') + expect(state.value).toEqual(init.value) + + state.value.name = 'b' + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"b","data":123}') + }) + + it('custom serializer', async () => { + const customSerializer: SerializerAsync = { + read: async (value: string) => { + return JSON.parse(value) + }, + write: async (value: any) => { + return JSON.stringify(value) + }, + } + + const vm = useSetup(() => { + const ref = useStorageAsync(KEY, 0, storageAsyncMock, { + serializer: customSerializer, + }) + return { ref } + }) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '0') + expect(vm.ref).toBe(0) + + vm.ref = { name: 'a', data: 123 } + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"a","data":123}') + + vm.ref.name = 'b' + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"b","data":123}') + }) + + it('eventFilter', async () => { + const vm = useSetup(() => { + const ref = useStorageAsync( + KEY, + { name: 'a', data: 123 }, + storageAsyncMock, + { eventFilter: debounceFilter(100) }, + ) + return { ref } + }) + + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"a","data":123}') + expect(vm.ref).toEqual({ name: 'a', data: 123 }) + + vi.clearAllMocks() + + vm.ref.name = 'b' + expect(storageAsyncMock.setItem).not.toHaveBeenCalled() + + await vi.waitFor(() => { + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '{"name":"b","data":123}') + }, { interval: 10 }) + }) + + it('mergeDefaults option', async () => { + // object + storageState.set(KEY, JSON.stringify({ a: 1 })) + const objectRef = useStorageAsync(KEY, { a: 2, b: 3 }, storageAsyncMock, { mergeDefaults: true }) + await nextTwoTick() + expect(objectRef.value).toEqual({ a: 1, b: 3 }) + + // array + storageState.clear() + storageState.set(KEY, JSON.stringify([1])) + const arrayRef = useStorageAsync(KEY, [2], storageAsyncMock, { mergeDefaults: true }) + await nextTwoTick() + expect(arrayRef.value).toEqual([1]) + + // custom function + storageState.clear() + storageState.set(KEY, JSON.stringify([{ a: 1 }])) + const customRef = useStorageAsync(KEY, [{ a: 3 }], storageAsyncMock, { + mergeDefaults: (value, initial) => [...initial, ...value], + }) + await nextTwoTick() + expect(customRef.value).toEqual([{ a: 3 }, { a: 1 }]) + + // custom function with number + storageState.clear() + storageState.set(KEY, '1') + const customRef2 = useStorageAsync(KEY, 2, storageAsyncMock, { + mergeDefaults: (value, initial) => value + initial, + }) + await nextTwoTick() + expect(customRef2.value).toEqual(3) + }) + + it('use storage value if present', async () => { + // boolean + storageState.set(KEY, 'true') + const boolRef = useStorageAsync(KEY, false, storageAsyncMock) + await nextTwoTick() + expect(boolRef.value).toBe(true) + + // number + storageState.clear() + storageState.set(KEY, '0') + const numRef = useStorageAsync(KEY, 1, storageAsyncMock) + await nextTwoTick() + expect(numRef.value).toBe(0) + + // object + storageState.clear() + storageState.set(KEY, JSON.stringify({})) + const objRef = useStorageAsync(KEY, { a: 1 }, storageAsyncMock) + await nextTwoTick() + expect(objRef.value).toEqual({}) + + // string + storageState.clear() + storageState.set(KEY, 'a') + const strRef = useStorageAsync(KEY, 'b', storageAsyncMock) + await nextTwoTick() + expect(strRef.value).toBe('a') + }) + + it('support shallow ref', async () => { + const data = useStorageAsync(KEY, 0, storageAsyncMock, { shallow: true }) + await nextTwoTick() + expect(data.value).toBe(0) + + data.value = 1 + await nextTwoTick() + expect(data.value).toBe(1) + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, '1') + }) + + it('writeDefaults option', async () => { + // with writeDefaults: true (default) + const ref1 = useStorageAsync(KEY, 'default', storageAsyncMock, { writeDefaults: true }) + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'default') + expect(ref1.value).toBe('default') + + // with writeDefaults: false + storageState.clear() + vi.clearAllMocks() + const ref2 = useStorageAsync(KEY, 'default', storageAsyncMock, { writeDefaults: false }) + await nextTwoTick() + expect(storageAsyncMock.setItem).not.toHaveBeenCalled() + expect(ref2.value).toBe('default') + }) + + it('handle error in read', async () => { + const errorStorage = { + getItem: vi.fn().mockRejectedValue(new Error('read error')), + setItem: vi.fn().mockResolvedValue(undefined), + removeItem: vi.fn().mockResolvedValue(undefined), + } + + const onError = vi.fn() + const store = useStorageAsync(KEY, 'default', errorStorage, { onError }) + + await nextTwoTick() + expect(onError).toHaveBeenCalledWith(new Error('read error')) + expect(store.value).toBe('default') + }) + + it('handle error in write', async () => { + const errorStorage = { + getItem: vi.fn().mockResolvedValue(null), + setItem: vi.fn().mockRejectedValue(new Error('write error')), + removeItem: vi.fn().mockResolvedValue(undefined), + } + + const onError = vi.fn() + const store = useStorageAsync(KEY, 'default', errorStorage, { onError }) + + await nextTwoTick() + store.value = 'new value' + await nextTwoTick() + + expect(onError).toHaveBeenCalledWith(new Error('write error')) + }) + + it('handle error in serializer', async () => { + const errorSerializer: SerializerAsync = { + read: vi.fn().mockRejectedValue(new Error('serializer read error')), + write: vi.fn().mockRejectedValue(new Error('serializer write error')), + } + + const onError = vi.fn() + storageState.set(KEY, 'invalid') + + const store = useStorageAsync(KEY, 'default', storageAsyncMock, { + serializer: errorSerializer, + onError, + }) + + await nextTwoTick() + expect(onError).toHaveBeenCalledWith(new Error('serializer read error')) + expect(store.value).toBe('default') + }) + + it('syncs properly within the same document', async () => { + const state1 = useStorageAsync(KEY, 0, storageAsyncMock) + const state2 = useStorageAsync(KEY, 0, storageAsyncMock) + + await nextTwoTick() + state1.value = 1 + await nextTwoTick() + // Note: useStorageAsync doesn't automatically sync between instances like useStorage does + // This test would need the storage event mechanism to work properly + expect(state2.value).toBe(0) // Without event sync, this won't update + }) + + it('with no storage provided', async () => { + const store = useStorageAsync(KEY, 'default', undefined) + await nextTwoTick() + expect(store.value).toBe('default') + }) + + it('flush timing', async () => { + const store = useStorageAsync(KEY, 'default', storageAsyncMock, { flush: 'sync' }) + await nextTwoTick() + + store.value = 'new value' + await nextTwoTick() + expect(storageAsyncMock.setItem).toHaveBeenCalledWith(KEY, 'new value') + }) + + it('listenToStorageChanges option', async () => { + const store = useStorageAsync(KEY, 'default', storageAsyncMock, { + listenToStorageChanges: false, + }) + await nextTwoTick() + + // Change storage externally + storageState.set(KEY, 'external change') + + // Dispatch storage event + window.dispatchEvent(new StorageEvent('storage', { + key: KEY, + newValue: 'external change', + })) + + await nextTick() + // Should not update because listenToStorageChanges is false + expect(store.value).toBe('default') + }) + + it('listen to storage events', async () => { + const store = useStorageAsync(KEY, 'initial', storageAsyncMock) + await nextTwoTick() + + // Simulate storage event + window.dispatchEvent(new StorageEvent('storage', { + key: KEY, + newValue: 'updated from event', + })) + + await nextTwoTick() + expect(store.value).toBe('updated from event') + }) +}) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy