@webitel/ui-datalist 1.1.65 → 1.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/filters/__tests__/createTableFiltersStore.spec.ts +143 -0
- package/src/modules/filters/createTableFiltersStore.ts +23 -5
- package/src/modules/filters/modules/filterConfig/components/contact-group/contact-group-filter-value-field.vue +40 -25
- package/src/modules/headers/__tests__/createTableHeadersStore.spec.ts +169 -0
- package/src/modules/headers/createTableHeadersStore.ts +26 -7
- package/src/modules/pagination/__tests__/createTablePaginationStore.spec.ts +134 -0
- package/src/modules/pagination/createTablePaginationStore.ts +32 -4
- package/src/modules/persist/PersistedStorage.types.ts +14 -0
- package/src/modules/persist/__tests__/useLocalStoragePersistedStorage.spec.ts +66 -0
- package/src/modules/persist/__tests__/usePersistedStorage.spec.ts +357 -0
- package/src/modules/persist/__tests__/useRoutePersistedStorage.spec.ts +145 -0
- package/src/modules/persist/usePersistedStorage.ts +93 -60
- package/src/modules/persist/useRoutePersistedStorage.ts +40 -18
- package/src/modules/table/createTableStore.store.ts +37 -0
- package/types/.tsbuildinfo +1 -1
- package/types/modules/filter-presets/stores/createFilterPresetsStore.d.ts +1 -0
- package/types/modules/filters/createTableFiltersStore.d.ts +2 -0
- package/types/modules/filters/modules/filterConfig/components/contact-group/contact-group-filter-value-field.vue.d.ts +4 -4
- package/types/modules/headers/createTableHeadersStore.d.ts +2 -0
- package/types/modules/pagination/createTablePaginationStore.d.ts +2 -0
- package/types/modules/permissions-page/stores/createPermissionsStore.d.ts +2 -0
- package/types/modules/persist/PersistedStorage.types.d.ts +13 -0
- package/types/modules/table/createTableStore.store.d.ts +2 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createApp } from 'vue';
|
|
3
|
+
import { createMemoryHistory, createRouter, type Router } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
import { tablePaginationStoreBody } from '../createTablePaginationStore';
|
|
6
|
+
|
|
7
|
+
const routes = [
|
|
8
|
+
{
|
|
9
|
+
path: '/cases',
|
|
10
|
+
name: 'cases',
|
|
11
|
+
component: {
|
|
12
|
+
template: '<div />',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe('tablePaginationStoreBody', () => {
|
|
18
|
+
let router: Router;
|
|
19
|
+
let runWithRouter: <T>(fn: () => T) => T;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
router = createRouter({
|
|
23
|
+
history: createMemoryHistory(),
|
|
24
|
+
routes,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const app = createApp({});
|
|
28
|
+
app.use(router);
|
|
29
|
+
|
|
30
|
+
await router.push('/cases');
|
|
31
|
+
await router.isReady();
|
|
32
|
+
|
|
33
|
+
runWithRouter = (fn) => app.runWithContext(fn);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const setUpStore = async () => {
|
|
37
|
+
const store = tablePaginationStoreBody();
|
|
38
|
+
await runWithRouter(() => store.setupPersistence());
|
|
39
|
+
return store;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it('starts on the first page with the default size', () => {
|
|
43
|
+
const store = tablePaginationStoreBody();
|
|
44
|
+
|
|
45
|
+
expect(store.page.value).toBe(1);
|
|
46
|
+
expect(store.size.value).toBe(10);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('updates page and size', () => {
|
|
50
|
+
const store = tablePaginationStoreBody();
|
|
51
|
+
|
|
52
|
+
store.updatePage(3);
|
|
53
|
+
store.updateSize(20);
|
|
54
|
+
|
|
55
|
+
expect(store.page.value).toBe(3);
|
|
56
|
+
expect(store.size.value).toBe(20);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resets to the defaults', () => {
|
|
60
|
+
const store = tablePaginationStoreBody();
|
|
61
|
+
store.updatePage(3);
|
|
62
|
+
store.updateSize(20);
|
|
63
|
+
store.next.value = true;
|
|
64
|
+
|
|
65
|
+
store.$reset();
|
|
66
|
+
|
|
67
|
+
expect(store.page.value).toBe(1);
|
|
68
|
+
expect(store.size.value).toBe(10);
|
|
69
|
+
expect(store.next.value).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('persistence', () => {
|
|
73
|
+
it('restores page and size from the route query', async () => {
|
|
74
|
+
await router.push({
|
|
75
|
+
name: 'cases',
|
|
76
|
+
query: {
|
|
77
|
+
page: '3',
|
|
78
|
+
size: '20',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const store = await setUpStore();
|
|
83
|
+
|
|
84
|
+
expect(store.page.value).toBe(3);
|
|
85
|
+
expect(store.size.value).toBe(20);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('writes a changed page into the route query', async () => {
|
|
89
|
+
const store = await setUpStore();
|
|
90
|
+
|
|
91
|
+
store.updatePage(3);
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
93
|
+
|
|
94
|
+
expect(router.currentRoute.value.query.page).toBe('3');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('syncPersistence', () => {
|
|
99
|
+
it('publishes state the user changed', async () => {
|
|
100
|
+
const store = await setUpStore();
|
|
101
|
+
store.updatePage(3);
|
|
102
|
+
await router.push('/cases');
|
|
103
|
+
|
|
104
|
+
await runWithRouter(() => store.syncPersistence());
|
|
105
|
+
|
|
106
|
+
expect(router.currentRoute.value.query.page).toBe('3');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('publishes nothing while the state is still default', async () => {
|
|
110
|
+
const store = await setUpStore();
|
|
111
|
+
|
|
112
|
+
await runWithRouter(() => store.syncPersistence());
|
|
113
|
+
|
|
114
|
+
expect(router.currentRoute.value.query).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not overwrite a value already in the query', async () => {
|
|
118
|
+
const store = await setUpStore();
|
|
119
|
+
store.updatePage(3);
|
|
120
|
+
/* let the watcher settle, so its write cannot land after the push below */
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
122
|
+
await router.push({
|
|
123
|
+
name: 'cases',
|
|
124
|
+
query: {
|
|
125
|
+
page: '5',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await runWithRouter(() => store.syncPersistence());
|
|
130
|
+
|
|
131
|
+
expect(router.currentRoute.value.query.page).toBe('5');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ref } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { createDatalistStore } from '../_shared/createDatalistStore';
|
|
4
|
+
import type { PersistedStorageController } from '../persist/PersistedStorage.types';
|
|
4
5
|
import { usePersistedStorage } from '../persist/usePersistedStorage';
|
|
5
6
|
import type { Identifiable } from '../types/createDatalistStore.types';
|
|
6
7
|
import type { useTableStoreConfig } from '../types/tableStore.types';
|
|
@@ -24,10 +25,18 @@ export const tablePaginationStoreBody = () => {
|
|
|
24
25
|
next.value = false;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
let persistedStorageControllers: PersistedStorageController[] = [];
|
|
29
|
+
|
|
27
30
|
const setupPersistence = () => {
|
|
28
|
-
const
|
|
31
|
+
const pageStorage = usePersistedStorage({
|
|
29
32
|
name: 'page',
|
|
30
33
|
value: page,
|
|
34
|
+
onStore: (save, { name }) => {
|
|
35
|
+
return save({
|
|
36
|
+
name,
|
|
37
|
+
value: `${page.value}`,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
31
40
|
onRestore: async (restore, name) => {
|
|
32
41
|
const value = await restore(name);
|
|
33
42
|
const numValue = Number(value);
|
|
@@ -35,9 +44,15 @@ export const tablePaginationStoreBody = () => {
|
|
|
35
44
|
},
|
|
36
45
|
});
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const sizeStorage = usePersistedStorage({
|
|
39
48
|
name: 'size',
|
|
40
49
|
value: size,
|
|
50
|
+
onStore: (save, { name }) => {
|
|
51
|
+
return save({
|
|
52
|
+
name,
|
|
53
|
+
value: `${size.value}`,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
41
56
|
onRestore: async (restore, name) => {
|
|
42
57
|
const value = await restore(name);
|
|
43
58
|
const numValue = Number(value);
|
|
@@ -45,12 +60,24 @@ export const tablePaginationStoreBody = () => {
|
|
|
45
60
|
},
|
|
46
61
|
});
|
|
47
62
|
|
|
63
|
+
persistedStorageControllers = [
|
|
64
|
+
pageStorage,
|
|
65
|
+
sizeStorage,
|
|
66
|
+
];
|
|
67
|
+
|
|
48
68
|
return Promise.allSettled([
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
pageStorage.restore(),
|
|
70
|
+
sizeStorage.restore(),
|
|
51
71
|
]);
|
|
52
72
|
};
|
|
53
73
|
|
|
74
|
+
/* sequentially: every route write is a router.replace() on top of the current query */
|
|
75
|
+
const syncPersistence = async () => {
|
|
76
|
+
for (const controller of persistedStorageControllers) {
|
|
77
|
+
await controller.sync();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
54
81
|
return {
|
|
55
82
|
page,
|
|
56
83
|
size,
|
|
@@ -60,6 +87,7 @@ export const tablePaginationStoreBody = () => {
|
|
|
60
87
|
updateSize,
|
|
61
88
|
|
|
62
89
|
setupPersistence,
|
|
90
|
+
syncPersistence,
|
|
63
91
|
$reset,
|
|
64
92
|
};
|
|
65
93
|
};
|
|
@@ -22,6 +22,10 @@ export interface StorageLike {
|
|
|
22
22
|
removeItem(key: string): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface PersistedStorageAdapter extends StorageLike {
|
|
26
|
+
type: PersistedStorageType;
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
export interface PersistedPropertyConfig {
|
|
26
30
|
name: string;
|
|
27
31
|
// note: createTableHeadersStore passes a computed here, which the default
|
|
@@ -55,5 +59,15 @@ export interface PersistedStorageController {
|
|
|
55
59
|
watch: () => void;
|
|
56
60
|
unwatch: () => void;
|
|
57
61
|
restore: () => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Publishes the current value to every storage that holds nothing for `name`.
|
|
64
|
+
* Storages that already hold a value are left untouched, so an explicit route
|
|
65
|
+
* query always wins over in-memory state.
|
|
66
|
+
*
|
|
67
|
+
* A no-op for state still equal to the defaults: `restore()` captures the
|
|
68
|
+
* snapshot a freshly created store serializes, and a controller that never
|
|
69
|
+
* restored can only tell empty values apart.
|
|
70
|
+
*/
|
|
71
|
+
sync: () => Promise<void>;
|
|
58
72
|
reset: () => Promise<void>;
|
|
59
73
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { useLocalStoragePersistedStorage } from '../useLocalStoragePersistedStorage';
|
|
4
|
+
|
|
5
|
+
describe('useLocalStoragePersistedStorage', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
localStorage.clear();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('namespaces keys with the storagePath', async () => {
|
|
11
|
+
const storage = useLocalStoragePersistedStorage({
|
|
12
|
+
storagePath: 'cases/headers',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await storage.setItem('fields', 'name,subject');
|
|
16
|
+
|
|
17
|
+
expect(localStorage.getItem('cases/headers/fields')).toBe('name,subject');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps stores with different paths apart', async () => {
|
|
21
|
+
const cases = useLocalStoragePersistedStorage({
|
|
22
|
+
storagePath: 'cases',
|
|
23
|
+
});
|
|
24
|
+
const contacts = useLocalStoragePersistedStorage({
|
|
25
|
+
storagePath: 'contacts',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await cases.setItem('fields', 'subject');
|
|
29
|
+
await contacts.setItem('fields', 'name');
|
|
30
|
+
|
|
31
|
+
expect(await cases.getItem('fields')).toBe('subject');
|
|
32
|
+
expect(await contacts.getItem('fields')).toBe('name');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('resolves null for a missing key', async () => {
|
|
36
|
+
const storage = useLocalStoragePersistedStorage({
|
|
37
|
+
storagePath: 'cases',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(await storage.getItem('fields')).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('stores an array as a comma-joined value', async () => {
|
|
44
|
+
const storage = useLocalStoragePersistedStorage({
|
|
45
|
+
storagePath: 'cases',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await storage.setItem('fields', [
|
|
49
|
+
'name',
|
|
50
|
+
'subject',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
expect(await storage.getItem('fields')).toBe('name,subject');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('removes a key', async () => {
|
|
57
|
+
const storage = useLocalStoragePersistedStorage({
|
|
58
|
+
storagePath: 'cases',
|
|
59
|
+
});
|
|
60
|
+
await storage.setItem('fields', 'name');
|
|
61
|
+
|
|
62
|
+
await storage.removeItem('fields');
|
|
63
|
+
|
|
64
|
+
expect(await storage.getItem('fields')).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { nextTick, type Ref, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type PersistableValue,
|
|
6
|
+
PersistedStorageType,
|
|
7
|
+
type StorageLike,
|
|
8
|
+
} from '../PersistedStorage.types';
|
|
9
|
+
|
|
10
|
+
const createStorageMock = (): StorageLike & {
|
|
11
|
+
value: string | null;
|
|
12
|
+
} => {
|
|
13
|
+
const storage = {
|
|
14
|
+
value: null as string | null,
|
|
15
|
+
getItem: vi.fn(async () => storage.value),
|
|
16
|
+
setItem: vi.fn(async (_key: string, value: string) => {
|
|
17
|
+
storage.value = value;
|
|
18
|
+
}),
|
|
19
|
+
removeItem: vi.fn(async () => {
|
|
20
|
+
storage.value = null;
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return storage;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const routeStorage = createStorageMock();
|
|
28
|
+
const localStorage = createStorageMock();
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
route storage resolves `undefined` for a missing query param,
|
|
32
|
+
local storage resolves `null` – both are mocked to keep that difference
|
|
33
|
+
*/
|
|
34
|
+
vi.mock('../useRoutePersistedStorage', () => ({
|
|
35
|
+
useRoutePersistedStorage: () => ({
|
|
36
|
+
getItem: (key: string) =>
|
|
37
|
+
routeStorage.getItem(key).then((value) => value ?? undefined),
|
|
38
|
+
setItem: routeStorage.setItem,
|
|
39
|
+
removeItem: routeStorage.removeItem,
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('../useLocalStoragePersistedStorage', () => ({
|
|
44
|
+
useLocalStoragePersistedStorage: () => localStorage,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const { usePersistedStorage } = await import('../usePersistedStorage');
|
|
48
|
+
|
|
49
|
+
describe('usePersistedStorage', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
routeStorage.value = null;
|
|
52
|
+
localStorage.value = null;
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('sync', () => {
|
|
57
|
+
it('publishes the current value to an empty storage', async () => {
|
|
58
|
+
const value = ref('filtered');
|
|
59
|
+
|
|
60
|
+
await usePersistedStorage({
|
|
61
|
+
name: 'filters',
|
|
62
|
+
value,
|
|
63
|
+
}).sync();
|
|
64
|
+
|
|
65
|
+
expect(routeStorage.setItem).toHaveBeenCalledWith('filters', 'filtered');
|
|
66
|
+
expect(routeStorage.value).toBe('filtered');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not overwrite a storage that already holds a value', async () => {
|
|
70
|
+
routeStorage.value = 'from-url';
|
|
71
|
+
const value = ref('from-memory');
|
|
72
|
+
|
|
73
|
+
await usePersistedStorage({
|
|
74
|
+
name: 'filters',
|
|
75
|
+
value,
|
|
76
|
+
}).sync();
|
|
77
|
+
|
|
78
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
79
|
+
expect(routeStorage.value).toBe('from-url');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('publishes only to the storages that hold nothing', async () => {
|
|
83
|
+
localStorage.value = 'from-local-storage';
|
|
84
|
+
const value = ref('from-memory');
|
|
85
|
+
|
|
86
|
+
await usePersistedStorage({
|
|
87
|
+
name: 'fields',
|
|
88
|
+
value,
|
|
89
|
+
storages: [
|
|
90
|
+
PersistedStorageType.Route,
|
|
91
|
+
PersistedStorageType.LocalStorage,
|
|
92
|
+
],
|
|
93
|
+
}).sync();
|
|
94
|
+
|
|
95
|
+
expect(routeStorage.value).toBe('from-memory');
|
|
96
|
+
expect(localStorage.setItem).not.toHaveBeenCalled();
|
|
97
|
+
expect(localStorage.value).toBe('from-local-storage');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('skips empty values', async () => {
|
|
101
|
+
const value = ref('');
|
|
102
|
+
|
|
103
|
+
await usePersistedStorage({
|
|
104
|
+
name: 'filters',
|
|
105
|
+
value,
|
|
106
|
+
}).sync();
|
|
107
|
+
|
|
108
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
109
|
+
expect(routeStorage.value).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('skips a value still equal to the default captured by restore', async () => {
|
|
113
|
+
const value = ref('default-page');
|
|
114
|
+
|
|
115
|
+
const storage = usePersistedStorage({
|
|
116
|
+
name: 'page',
|
|
117
|
+
value,
|
|
118
|
+
});
|
|
119
|
+
await storage.restore();
|
|
120
|
+
await storage.sync();
|
|
121
|
+
|
|
122
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
123
|
+
expect(routeStorage.value).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('publishes a value changed after restore', async () => {
|
|
127
|
+
const value = ref('default-page');
|
|
128
|
+
|
|
129
|
+
const storage = usePersistedStorage({
|
|
130
|
+
name: 'page',
|
|
131
|
+
value,
|
|
132
|
+
});
|
|
133
|
+
await storage.restore();
|
|
134
|
+
storage.unwatch();
|
|
135
|
+
value.value = '3';
|
|
136
|
+
await storage.sync();
|
|
137
|
+
|
|
138
|
+
expect(routeStorage.value).toBe('3');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('captures the default through onStore serialization', async () => {
|
|
142
|
+
const filters = ref<Record<string, string>>({});
|
|
143
|
+
|
|
144
|
+
const storage = usePersistedStorage({
|
|
145
|
+
name: 'filters',
|
|
146
|
+
value: filters as unknown as Ref<PersistableValue>,
|
|
147
|
+
onStore: (save, { name }) =>
|
|
148
|
+
save({
|
|
149
|
+
name,
|
|
150
|
+
value: JSON.stringify(filters.value),
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
await storage.restore();
|
|
154
|
+
storage.unwatch();
|
|
155
|
+
|
|
156
|
+
/* an empty snapshot serializes to "{}" – the default, nothing to publish */
|
|
157
|
+
await storage.sync();
|
|
158
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
159
|
+
|
|
160
|
+
filters.value = {
|
|
161
|
+
name: 'test',
|
|
162
|
+
};
|
|
163
|
+
await storage.sync();
|
|
164
|
+
expect(routeStorage.value).toBe('{"name":"test"}');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('skips empty values when restore never ran', async () => {
|
|
168
|
+
const value = ref('');
|
|
169
|
+
|
|
170
|
+
await usePersistedStorage({
|
|
171
|
+
name: 'filters',
|
|
172
|
+
value,
|
|
173
|
+
}).sync();
|
|
174
|
+
|
|
175
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('stores value serialized by onStore', async () => {
|
|
179
|
+
const value = ref('ignored-raw-value');
|
|
180
|
+
|
|
181
|
+
await usePersistedStorage({
|
|
182
|
+
name: 'filters',
|
|
183
|
+
value,
|
|
184
|
+
onStore: (save, { name }) =>
|
|
185
|
+
save({
|
|
186
|
+
name,
|
|
187
|
+
value: 'serialized',
|
|
188
|
+
}),
|
|
189
|
+
}).sync();
|
|
190
|
+
|
|
191
|
+
expect(routeStorage.value).toBe('serialized');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not start watching the value', async () => {
|
|
195
|
+
const value = ref('filtered');
|
|
196
|
+
|
|
197
|
+
await usePersistedStorage({
|
|
198
|
+
name: 'filters',
|
|
199
|
+
value,
|
|
200
|
+
}).sync();
|
|
201
|
+
|
|
202
|
+
vi.clearAllMocks();
|
|
203
|
+
value.value = 'changed';
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
205
|
+
|
|
206
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('restore', () => {
|
|
211
|
+
it('falls back to the next storage when the route holds nothing', async () => {
|
|
212
|
+
localStorage.value = 'from-local-storage';
|
|
213
|
+
const value = ref('');
|
|
214
|
+
|
|
215
|
+
await usePersistedStorage({
|
|
216
|
+
name: 'fields',
|
|
217
|
+
value,
|
|
218
|
+
storages: [
|
|
219
|
+
PersistedStorageType.Route,
|
|
220
|
+
PersistedStorageType.LocalStorage,
|
|
221
|
+
],
|
|
222
|
+
}).restore();
|
|
223
|
+
|
|
224
|
+
expect(value.value).toBe('from-local-storage');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('prefers the route value over the next storages', async () => {
|
|
228
|
+
routeStorage.value = 'from-url';
|
|
229
|
+
localStorage.value = 'from-local-storage';
|
|
230
|
+
const value = ref('');
|
|
231
|
+
|
|
232
|
+
await usePersistedStorage({
|
|
233
|
+
name: 'fields',
|
|
234
|
+
value,
|
|
235
|
+
storages: [
|
|
236
|
+
PersistedStorageType.Route,
|
|
237
|
+
PersistedStorageType.LocalStorage,
|
|
238
|
+
],
|
|
239
|
+
}).restore();
|
|
240
|
+
|
|
241
|
+
expect(value.value).toBe('from-url');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('leaves the value untouched when no storage holds anything', async () => {
|
|
245
|
+
const value = ref('in-memory');
|
|
246
|
+
|
|
247
|
+
await usePersistedStorage({
|
|
248
|
+
name: 'fields',
|
|
249
|
+
value,
|
|
250
|
+
}).restore();
|
|
251
|
+
|
|
252
|
+
expect(value.value).toBe('in-memory');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('hands the first non-null value to onRestore', async () => {
|
|
256
|
+
localStorage.value = 'from-local-storage';
|
|
257
|
+
const onRestore = vi.fn();
|
|
258
|
+
|
|
259
|
+
await usePersistedStorage({
|
|
260
|
+
name: 'fields',
|
|
261
|
+
value: ref(''),
|
|
262
|
+
storages: [
|
|
263
|
+
PersistedStorageType.Route,
|
|
264
|
+
PersistedStorageType.LocalStorage,
|
|
265
|
+
],
|
|
266
|
+
onRestore: async (restore, name) => {
|
|
267
|
+
onRestore(await restore(name));
|
|
268
|
+
},
|
|
269
|
+
}).restore();
|
|
270
|
+
|
|
271
|
+
expect(onRestore).toHaveBeenCalledWith('from-local-storage');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('watch', () => {
|
|
276
|
+
it('writes a changed value to every storage', async () => {
|
|
277
|
+
const value = ref('initial');
|
|
278
|
+
|
|
279
|
+
await usePersistedStorage({
|
|
280
|
+
name: 'fields',
|
|
281
|
+
value,
|
|
282
|
+
storages: [
|
|
283
|
+
PersistedStorageType.Route,
|
|
284
|
+
PersistedStorageType.LocalStorage,
|
|
285
|
+
],
|
|
286
|
+
}).restore();
|
|
287
|
+
|
|
288
|
+
value.value = 'changed';
|
|
289
|
+
await nextTick();
|
|
290
|
+
|
|
291
|
+
expect(routeStorage.value).toBe('changed');
|
|
292
|
+
expect(localStorage.value).toBe('changed');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('does not write the value restored from a storage back', async () => {
|
|
296
|
+
routeStorage.value = 'from-url';
|
|
297
|
+
|
|
298
|
+
await usePersistedStorage({
|
|
299
|
+
name: 'filters',
|
|
300
|
+
value: ref(''),
|
|
301
|
+
}).restore();
|
|
302
|
+
await nextTick();
|
|
303
|
+
|
|
304
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('does not start watching when startWatchManually is set', async () => {
|
|
308
|
+
const value = ref('initial');
|
|
309
|
+
|
|
310
|
+
await usePersistedStorage({
|
|
311
|
+
name: 'filters',
|
|
312
|
+
value,
|
|
313
|
+
startWatchManually: true,
|
|
314
|
+
}).restore();
|
|
315
|
+
|
|
316
|
+
value.value = 'changed';
|
|
317
|
+
await nextTick();
|
|
318
|
+
|
|
319
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('stops writing after unwatch', async () => {
|
|
323
|
+
const value = ref('initial');
|
|
324
|
+
|
|
325
|
+
const storage = usePersistedStorage({
|
|
326
|
+
name: 'filters',
|
|
327
|
+
value,
|
|
328
|
+
});
|
|
329
|
+
await storage.restore();
|
|
330
|
+
storage.unwatch();
|
|
331
|
+
|
|
332
|
+
value.value = 'changed';
|
|
333
|
+
await nextTick();
|
|
334
|
+
|
|
335
|
+
expect(routeStorage.setItem).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('reset', () => {
|
|
340
|
+
it('removes the value from every storage', async () => {
|
|
341
|
+
routeStorage.value = 'from-url';
|
|
342
|
+
localStorage.value = 'from-local-storage';
|
|
343
|
+
|
|
344
|
+
await usePersistedStorage({
|
|
345
|
+
name: 'fields',
|
|
346
|
+
value: ref('in-memory'),
|
|
347
|
+
storages: [
|
|
348
|
+
PersistedStorageType.Route,
|
|
349
|
+
PersistedStorageType.LocalStorage,
|
|
350
|
+
],
|
|
351
|
+
}).reset();
|
|
352
|
+
|
|
353
|
+
expect(routeStorage.value).toBeNull();
|
|
354
|
+
expect(localStorage.value).toBeNull();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|