@lobehub/chat 0.118.5 → 0.118.7
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/CHANGELOG.md +50 -0
- package/next.config.mjs +0 -1
- package/package.json +1 -1
- package/src/app/api/openai/chat/createChatCompletion.test.ts +37 -0
- package/src/app/api/openai/chat/createChatCompletion.ts +10 -2
- package/src/app/api/openai/chat/desensitizeUrl.test.ts +47 -0
- package/src/app/api/openai/chat/desensitizeUrl.ts +34 -0
- package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/Assistant.tsx +3 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/Error.tsx +3 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/Fallback.tsx +4 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/Function.tsx +4 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/User.tsx +3 -1
- package/src/features/Conversation/{ChatList/Actions → Actions}/index.ts +1 -2
- package/src/features/Conversation/{ChatList/Error → Error}/InvalidAccess.tsx +2 -1
- package/src/features/Conversation/{ChatList/Error → Error}/OpenAPIKey.tsx +1 -1
- package/src/features/Conversation/{ChatList/Error → Error}/OpenAiBizError.tsx +2 -1
- package/src/features/Conversation/{ChatList/Error → Error}/Plugin/PluginError.tsx +3 -1
- package/src/features/Conversation/{ChatList/Error → Error}/Plugin/PluginSettings.tsx +2 -1
- package/src/features/Conversation/{ChatList/Error → Error}/index.tsx +1 -1
- package/src/features/Conversation/{ChatList/Extras → Extras}/Translate.tsx +1 -1
- package/src/features/Conversation/{ChatList/Extras → Extras}/index.ts +1 -2
- package/src/features/Conversation/{ChatList/Messages → Messages}/Default.tsx +1 -1
- package/src/features/Conversation/{ChatList/Messages → Messages}/User.tsx +1 -1
- package/src/features/Conversation/{ChatList/Messages → Messages}/index.ts +5 -5
- package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/Loading.tsx +10 -11
- package/src/features/Conversation/components/ChatList/ActionsBar.tsx +27 -0
- package/src/features/Conversation/components/ChatList/HistoryDivider.tsx +23 -0
- package/src/features/Conversation/components/ChatList/Item.tsx +236 -0
- package/src/features/Conversation/components/ChatList/index.tsx +88 -0
- package/src/features/Conversation/components/ChatList/style.ts +10 -0
- package/src/features/Conversation/{ChatList → container}/index.tsx +6 -7
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +43 -0
- package/src/features/Conversation/index.tsx +3 -3
- package/src/store/middleware/createHyperStorage/index.test.ts +341 -0
- package/src/store/middleware/createHyperStorage/indexedDB.test.ts +64 -0
- package/src/store/middleware/createHyperStorage/urlStorage.test.ts +84 -0
- package/src/store/middleware/createHyperStorage/urlStorage.ts +25 -8
- /package/src/features/Conversation/{ChatList/Actions → Actions}/customAction.ts +0 -0
- /package/src/features/Conversation/{ChatList/Error → Error}/ApiKeyForm.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Error → Error}/ErrorActionContainer.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Error → Error}/style.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/Assistant.test.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/Assistant.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/ExtraContainer.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/TTS/FilePlayer.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/TTS/InitPlayer.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/TTS/Player.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/TTS/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/User.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Extras → Extras}/type.ts +0 -0
- /package/src/features/Conversation/{ChatList/Messages → Messages}/Assistant.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Messages → Messages}/Function.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Inspector/PluginResultJSON.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Inspector/Settings.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Inspector/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Inspector/style.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/BuiltinType/index.test.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/BuiltinType/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/DefaultType/IFrameRender/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/DefaultType/SystemJsRender/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/DefaultType/SystemJsRender/utils.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/DefaultType/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/MarkdownType/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/StandaloneType/Iframe.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/StandaloneType/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/index.tsx +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/useParseContent.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/iframeOnReady.test.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/iframeOnReady.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/listenToPlugin.test.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/listenToPlugin.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginSettings.test.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginSettings.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginState.test.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginState.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/postMessage.test.ts +0 -0
- /package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/postMessage.ts +0 -0
- /package/src/features/Conversation/{ChatList/Loading.tsx → components/BubblesLoading.tsx} +0 -0
- /package/src/features/Conversation/{ChatList → components}/OTPInput.tsx +0 -0
- /package/src/features/Conversation/{ScrollAnchor → components/ScrollAnchor}/index.tsx +0 -0
- /package/src/features/Conversation/{ScrollAnchor → components/ScrollAnchor}/useAtBottom.ts +0 -0
- /package/src/features/Conversation/{ChatList → components}/SkeletonList.tsx +0 -0
- /package/src/features/Conversation/{useInitConversation.ts → hooks/useInitConversation.ts} +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createHyperStorage } from './index';
|
|
4
|
+
import { createIndexedDB } from './indexedDB';
|
|
5
|
+
import { createKeyMapper } from './keyMapper';
|
|
6
|
+
import { createLocalStorage } from './localStorage';
|
|
7
|
+
import { creatUrlStorage } from './urlStorage';
|
|
8
|
+
|
|
9
|
+
// Mock the dependent modules
|
|
10
|
+
vi.mock('./indexedDB', () => {
|
|
11
|
+
return {
|
|
12
|
+
createIndexedDB: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('./localStorage', () => ({
|
|
17
|
+
createLocalStorage: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('./urlStorage', () => ({
|
|
21
|
+
creatUrlStorage: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('./keyMapper', () => ({
|
|
25
|
+
createKeyMapper: vi.fn().mockReturnValue({
|
|
26
|
+
mapStateKeyToStorageKey: vi.fn((k) => k),
|
|
27
|
+
getStateKeyFromStorageKey: vi.fn((k) => k),
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.mocked(createKeyMapper).mockClear();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('createHyperStorage', () => {
|
|
36
|
+
it('should create storage with default configuration if no options provided', async () => {
|
|
37
|
+
const storage = createHyperStorage({});
|
|
38
|
+
expect(storage).toBeDefined();
|
|
39
|
+
// Add more assertions to verify default behavior
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should skip local storage if explicitly disabled', async () => {
|
|
43
|
+
const storage = createHyperStorage({ localStorage: false });
|
|
44
|
+
await storage.setItem('key', { state: {}, version: 1 });
|
|
45
|
+
|
|
46
|
+
// The setItem should not call the local storage functions
|
|
47
|
+
const indexDBSetItemMock = vi.fn();
|
|
48
|
+
|
|
49
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
50
|
+
getItem: vi.fn(),
|
|
51
|
+
removeItem: vi.fn(),
|
|
52
|
+
setItem: indexDBSetItemMock,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const localStorageSetItemMock = vi.fn();
|
|
56
|
+
|
|
57
|
+
vi.mocked(createLocalStorage).mockImplementation(() => ({
|
|
58
|
+
getItem: vi.fn(),
|
|
59
|
+
removeItem: vi.fn(),
|
|
60
|
+
setItem: localStorageSetItemMock,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
expect(indexDBSetItemMock).not.toHaveBeenCalled();
|
|
64
|
+
expect(localStorageSetItemMock).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should use indexedDB mode if set in local storage options', async () => {
|
|
68
|
+
// The setItem should call the indexedDB functions
|
|
69
|
+
// The setItem should not call the local storage functions
|
|
70
|
+
const indexDBSetItemMock = vi.fn();
|
|
71
|
+
|
|
72
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
73
|
+
getItem: vi.fn(),
|
|
74
|
+
removeItem: vi.fn(),
|
|
75
|
+
setItem: indexDBSetItemMock,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const storage = createHyperStorage({
|
|
79
|
+
localStorage: { mode: 'indexedDB', dbName: 'testDB', selectors: [] },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await storage.setItem('key', { state: {}, version: 1 });
|
|
83
|
+
|
|
84
|
+
expect(indexDBSetItemMock).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use the provided dbName for indexedDB', async () => {
|
|
88
|
+
const dbName = 'customDB';
|
|
89
|
+
createHyperStorage({ localStorage: { mode: 'indexedDB', dbName, selectors: [] } });
|
|
90
|
+
|
|
91
|
+
expect(vi.mocked(createIndexedDB)).toHaveBeenCalledWith(dbName);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle URL storage if URL options are provided', async () => {
|
|
95
|
+
const urlMode = 'hash';
|
|
96
|
+
const setItemMock = vi.fn();
|
|
97
|
+
|
|
98
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
99
|
+
getItem: vi.fn(),
|
|
100
|
+
removeItem: vi.fn(),
|
|
101
|
+
setItem: setItemMock,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const storage = createHyperStorage({ url: { mode: urlMode, selectors: [] } });
|
|
105
|
+
|
|
106
|
+
await storage.setItem('key', { state: {}, version: 1 });
|
|
107
|
+
|
|
108
|
+
expect(creatUrlStorage).toHaveBeenCalledWith(urlMode);
|
|
109
|
+
// The setItem should call the urlStorage functions
|
|
110
|
+
expect(setItemMock).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('getItem method', () => {
|
|
114
|
+
it('should retrieve item from indexedDB when useIndexedDB is true', async () => {
|
|
115
|
+
const indexedDBGetItemMock = vi
|
|
116
|
+
.fn()
|
|
117
|
+
.mockResolvedValue({ state: { key: 'value' }, version: 1 });
|
|
118
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
119
|
+
getItem: indexedDBGetItemMock,
|
|
120
|
+
removeItem: vi.fn(),
|
|
121
|
+
setItem: vi.fn(),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
const storage = createHyperStorage({ localStorage: { mode: 'indexedDB', selectors: [] } });
|
|
125
|
+
const item = await storage.getItem('key');
|
|
126
|
+
|
|
127
|
+
expect(indexedDBGetItemMock).toHaveBeenCalledWith('key');
|
|
128
|
+
expect(item).toEqual({ state: { key: 'value' }, version: 1 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should fallback to localStorage if item not found in indexedDB', async () => {
|
|
132
|
+
const indexedDBGetItemMock = vi.fn().mockResolvedValue(undefined);
|
|
133
|
+
const localStorageGetItemMock = vi
|
|
134
|
+
.fn()
|
|
135
|
+
.mockReturnValue({ state: { key: 'value' }, version: 1 });
|
|
136
|
+
|
|
137
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
138
|
+
getItem: indexedDBGetItemMock,
|
|
139
|
+
removeItem: vi.fn(),
|
|
140
|
+
setItem: vi.fn(),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
vi.mocked(createLocalStorage).mockImplementation(() => ({
|
|
144
|
+
getItem: localStorageGetItemMock,
|
|
145
|
+
removeItem: vi.fn(),
|
|
146
|
+
setItem: vi.fn(),
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
const storage = createHyperStorage({ localStorage: { mode: 'indexedDB', selectors: [] } });
|
|
150
|
+
const item = await storage.getItem('key');
|
|
151
|
+
|
|
152
|
+
expect(indexedDBGetItemMock).toHaveBeenCalledWith('key');
|
|
153
|
+
expect(localStorageGetItemMock).toHaveBeenCalledWith('key');
|
|
154
|
+
expect(item).toEqual({ state: { key: 'value' }, version: 1 });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should not attempt to retrieve from any storage if skipLocalStorage is true', async () => {
|
|
158
|
+
const storage = createHyperStorage({ localStorage: false });
|
|
159
|
+
const item = await storage.getItem('key');
|
|
160
|
+
|
|
161
|
+
const indexedDBGetItemMock = vi.fn().mockResolvedValue(undefined);
|
|
162
|
+
const localStorageGetItemMock = vi
|
|
163
|
+
.fn()
|
|
164
|
+
.mockReturnValue({ state: { key: 'value' }, version: 1 });
|
|
165
|
+
|
|
166
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
167
|
+
getItem: indexedDBGetItemMock,
|
|
168
|
+
removeItem: vi.fn(),
|
|
169
|
+
setItem: vi.fn(),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
vi.mocked(createLocalStorage).mockImplementation(() => ({
|
|
173
|
+
getItem: localStorageGetItemMock,
|
|
174
|
+
removeItem: vi.fn(),
|
|
175
|
+
setItem: vi.fn(),
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
expect(indexedDBGetItemMock).not.toHaveBeenCalled();
|
|
179
|
+
expect(localStorageGetItemMock).not.toHaveBeenCalled();
|
|
180
|
+
expect(item).toEqual({ state: {}, version: undefined });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('getItem method with URL storage', () => {
|
|
184
|
+
it('should override state from URL storage if hasUrl is true', async () => {
|
|
185
|
+
const urlStorageGetItemMock = vi.fn().mockReturnValue({ state: { urlKey: 'urlValue' } });
|
|
186
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
187
|
+
getItem: urlStorageGetItemMock,
|
|
188
|
+
removeItem: vi.fn(),
|
|
189
|
+
setItem: vi.fn(),
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
// Mock createKeyMapper to simulate state key mapping from URL storage keys
|
|
193
|
+
vi.mocked(createKeyMapper).mockReturnValue({
|
|
194
|
+
mapStateKeyToStorageKey: vi.fn((k) => k),
|
|
195
|
+
getStateKeyFromStorageKey: vi.fn((k) => (k === 'urlKey' ? 'mappedKey' : undefined)),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const storage = createHyperStorage({
|
|
199
|
+
url: { mode: 'hash', selectors: [] },
|
|
200
|
+
localStorage: false,
|
|
201
|
+
});
|
|
202
|
+
const item = await storage.getItem('key');
|
|
203
|
+
|
|
204
|
+
expect(urlStorageGetItemMock).toHaveBeenCalled();
|
|
205
|
+
expect(item?.state).toEqual({ mappedKey: 'urlValue' });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should not include URL storage state if key mapping is undefined', async () => {
|
|
209
|
+
const urlStorageGetItemMock = vi.fn().mockReturnValue({ state: { urlKey: 'urlValue' } });
|
|
210
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
211
|
+
getItem: urlStorageGetItemMock,
|
|
212
|
+
removeItem: vi.fn(),
|
|
213
|
+
setItem: vi.fn(),
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
// Mock createKeyMapper to simulate state key mapping from URL storage keys
|
|
217
|
+
vi.mocked(createKeyMapper).mockReturnValue({
|
|
218
|
+
mapStateKeyToStorageKey: vi.fn((k) => k),
|
|
219
|
+
getStateKeyFromStorageKey: vi.fn(() => undefined), // No key will be mapped
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const storage = createHyperStorage({
|
|
223
|
+
url: { mode: 'hash', selectors: [] },
|
|
224
|
+
localStorage: false,
|
|
225
|
+
});
|
|
226
|
+
const item = await storage.getItem('key');
|
|
227
|
+
|
|
228
|
+
expect(urlStorageGetItemMock).toHaveBeenCalled();
|
|
229
|
+
expect(item?.state).toEqual({}); // No state from URL storage should be included
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not attempt to retrieve from URL storage if hasUrl is false', async () => {
|
|
233
|
+
const urlStorageGetItemMock = vi.fn();
|
|
234
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
235
|
+
getItem: urlStorageGetItemMock,
|
|
236
|
+
removeItem: vi.fn(),
|
|
237
|
+
setItem: vi.fn(),
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const storage = createHyperStorage({ localStorage: false }); // No URL options provided
|
|
241
|
+
await storage.getItem('key');
|
|
242
|
+
|
|
243
|
+
expect(urlStorageGetItemMock).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('removeItem method', () => {
|
|
249
|
+
it('should remove item from indexedDB when useIndexedDB is true', async () => {
|
|
250
|
+
const indexedDBRemoveItemMock = vi.fn().mockResolvedValue(undefined);
|
|
251
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
252
|
+
getItem: vi.fn(),
|
|
253
|
+
removeItem: indexedDBRemoveItemMock,
|
|
254
|
+
setItem: vi.fn(),
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
const storage = createHyperStorage({ localStorage: { mode: 'indexedDB', selectors: [] } });
|
|
258
|
+
await storage.removeItem('key');
|
|
259
|
+
|
|
260
|
+
expect(indexedDBRemoveItemMock).toHaveBeenCalledWith('key');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should remove item from localStorage when useIndexedDB is false', async () => {
|
|
264
|
+
const localStorageRemoveItemMock = vi.fn().mockResolvedValue(undefined);
|
|
265
|
+
vi.mocked(createLocalStorage).mockImplementation(() => ({
|
|
266
|
+
getItem: vi.fn(),
|
|
267
|
+
removeItem: localStorageRemoveItemMock,
|
|
268
|
+
setItem: vi.fn(),
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
const storage = createHyperStorage({});
|
|
272
|
+
await storage.removeItem('key');
|
|
273
|
+
|
|
274
|
+
expect(localStorageRemoveItemMock).toHaveBeenCalledWith('key');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should remove item from URL storage if URL options provided', async () => {
|
|
278
|
+
const urlStorageRemoveItemMock = vi.fn();
|
|
279
|
+
|
|
280
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
281
|
+
getItem: vi.fn(),
|
|
282
|
+
removeItem: urlStorageRemoveItemMock,
|
|
283
|
+
setItem: vi.fn(),
|
|
284
|
+
}));
|
|
285
|
+
// Mock createKeyMapper to simulate state key mapping from URL storage keys
|
|
286
|
+
vi.mocked(createKeyMapper).mockReturnValue({
|
|
287
|
+
mapStateKeyToStorageKey: vi.fn((k) => k),
|
|
288
|
+
getStateKeyFromStorageKey: vi.fn((k) => k), // No key will be mapped
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const storage = createHyperStorage({ url: { mode: 'hash', selectors: ['key'] } });
|
|
292
|
+
await storage.removeItem('key');
|
|
293
|
+
|
|
294
|
+
expect(urlStorageRemoveItemMock).toHaveBeenCalledWith('key');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('setItem method', () => {
|
|
299
|
+
it('should save item to indexedDB when useIndexedDB is true', async () => {
|
|
300
|
+
const indexedDBSetItemMock = vi.fn().mockResolvedValue(undefined);
|
|
301
|
+
vi.mocked(createIndexedDB).mockImplementation(() => ({
|
|
302
|
+
getItem: vi.fn(),
|
|
303
|
+
removeItem: vi.fn(),
|
|
304
|
+
setItem: indexedDBSetItemMock,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const storage = createHyperStorage({ localStorage: { mode: 'indexedDB', selectors: [] } });
|
|
308
|
+
await storage.setItem('key', { state: { key: 'value' }, version: 1 });
|
|
309
|
+
|
|
310
|
+
expect(indexedDBSetItemMock).toHaveBeenCalledWith('key', { key: 'value' }, 1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should save item to localStorage when useIndexedDB is false', async () => {
|
|
314
|
+
const localStorageSetItemMock = vi.fn().mockResolvedValue(undefined);
|
|
315
|
+
vi.mocked(createLocalStorage).mockImplementation(() => ({
|
|
316
|
+
getItem: vi.fn(),
|
|
317
|
+
removeItem: vi.fn(),
|
|
318
|
+
setItem: localStorageSetItemMock,
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
const storage = createHyperStorage({});
|
|
322
|
+
await storage.setItem('key', { state: { key: 'value' }, version: 1 });
|
|
323
|
+
|
|
324
|
+
expect(localStorageSetItemMock).toHaveBeenCalledWith('key', { key: 'value' }, 1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should save state to URL storage if URL options provided', async () => {
|
|
328
|
+
const urlStorageSetItemMock = vi.fn().mockResolvedValue(undefined);
|
|
329
|
+
vi.mocked(creatUrlStorage).mockImplementation(() => ({
|
|
330
|
+
getItem: vi.fn(),
|
|
331
|
+
removeItem: vi.fn(),
|
|
332
|
+
setItem: urlStorageSetItemMock,
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
const storage = createHyperStorage({ url: { mode: 'hash', selectors: [] } });
|
|
336
|
+
await storage.setItem('key', { state: { key: 'value' }, version: 1 });
|
|
337
|
+
|
|
338
|
+
expect(urlStorageSetItemMock).toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { delMany, getMany, setMany } from 'idb-keyval';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createIndexedDB } from './indexedDB';
|
|
5
|
+
|
|
6
|
+
// Mock idb-keyval methods
|
|
7
|
+
vi.mock('idb-keyval', () => ({
|
|
8
|
+
createStore: vi.fn().mockImplementation(() => 'abc'),
|
|
9
|
+
getMany: vi.fn(),
|
|
10
|
+
setMany: vi.fn(),
|
|
11
|
+
delMany: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('createIndexedDB', () => {
|
|
15
|
+
const dbName = 'testDB';
|
|
16
|
+
const storeName = 'testStore';
|
|
17
|
+
const indexedDB = createIndexedDB(dbName);
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset all mocks before each test
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('getItem should return the correct state and version', async () => {
|
|
25
|
+
const mockState = { key: 'value' };
|
|
26
|
+
const mockVersion = 1;
|
|
27
|
+
|
|
28
|
+
// Setup mock behavior for getMany
|
|
29
|
+
vi.mocked(getMany).mockResolvedValue([mockVersion, mockState]);
|
|
30
|
+
|
|
31
|
+
const result = await indexedDB.getItem(storeName);
|
|
32
|
+
|
|
33
|
+
expect(vi.mocked(getMany).mock.calls[0][0]).toEqual(['version', 'state']);
|
|
34
|
+
expect(result).toEqual({ state: mockState, version: mockVersion });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('getItem should return undefined if state does not exist', async () => {
|
|
38
|
+
// Setup mock behavior for getMany
|
|
39
|
+
vi.mocked(getMany).mockResolvedValue([undefined, undefined]);
|
|
40
|
+
|
|
41
|
+
const result = await indexedDB.getItem(storeName);
|
|
42
|
+
|
|
43
|
+
expect(vi.mocked(getMany).mock.calls[0][0]).toEqual(['version', 'state']);
|
|
44
|
+
expect(result).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('removeItem should call delMany with the correct keys', async () => {
|
|
48
|
+
await indexedDB.removeItem(storeName);
|
|
49
|
+
|
|
50
|
+
expect(vi.mocked(delMany).mock.calls[0][0]).toEqual(['version', 'state']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('setItem should call setMany with the correct keys and values', async () => {
|
|
54
|
+
const mockState = { key: 'value' };
|
|
55
|
+
const mockVersion = 1;
|
|
56
|
+
|
|
57
|
+
await indexedDB.setItem(storeName, mockState, mockVersion);
|
|
58
|
+
|
|
59
|
+
expect(vi.mocked(setMany).mock.calls[0][0]).toEqual([
|
|
60
|
+
['version', mockVersion],
|
|
61
|
+
['state', mockState],
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { creatUrlStorage } from './urlStorage';
|
|
4
|
+
|
|
5
|
+
// Mock global location and history objects
|
|
6
|
+
const mockLocation = {
|
|
7
|
+
hash: '',
|
|
8
|
+
search: '',
|
|
9
|
+
};
|
|
10
|
+
const mockHistory = {
|
|
11
|
+
replaceState: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
global.location = mockLocation as Location;
|
|
15
|
+
global.history = mockHistory as any as History;
|
|
16
|
+
|
|
17
|
+
describe('creatUrlStorage', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Reset all mocks and location properties before each test
|
|
20
|
+
vi.resetAllMocks();
|
|
21
|
+
mockLocation.hash = '';
|
|
22
|
+
mockLocation.search = '';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('getItem should return the correct state from URL hash', () => {
|
|
26
|
+
mockLocation.hash = '#key1=value1&key2=value2';
|
|
27
|
+
const storage = creatUrlStorage('hash');
|
|
28
|
+
const result = storage.getItem();
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ state: { key1: 'value1', key2: 'value2' } });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('getItem should return the correct state from URL search', () => {
|
|
34
|
+
mockLocation.search = '?key1=value1&key2=value2';
|
|
35
|
+
const storage = creatUrlStorage('search');
|
|
36
|
+
const result = storage.getItem();
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual({ state: { key1: 'value1', key2: 'value2' } });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('getItem should return undefined if no state in URL', () => {
|
|
42
|
+
const storage = creatUrlStorage('hash');
|
|
43
|
+
const result = storage.getItem();
|
|
44
|
+
|
|
45
|
+
expect(result).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('removeItem should remove a specific key from URL hash', () => {
|
|
49
|
+
mockLocation.hash = '#key1=value1&key2=value2';
|
|
50
|
+
const storage = creatUrlStorage('hash');
|
|
51
|
+
storage.removeItem('key1');
|
|
52
|
+
|
|
53
|
+
expect(location.hash).toEqual('key2=value2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('removeItem should remove a specific key from URL search', () => {
|
|
57
|
+
mockLocation.search = '?key1=value1&key2=value2';
|
|
58
|
+
const storage = creatUrlStorage('search');
|
|
59
|
+
storage.removeItem('key1');
|
|
60
|
+
|
|
61
|
+
expect(mockHistory.replaceState).toHaveBeenCalledWith({}, '', '?key2=value2');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('setItem should set state in URL hash', () => {
|
|
65
|
+
const storage = creatUrlStorage('hash');
|
|
66
|
+
storage.setItem('state', { key1: 'value1', key2: 'value2', key3: 1, key4: false });
|
|
67
|
+
|
|
68
|
+
expect(location.hash).toEqual('key1=value1&key2=value2&key3=1&key4=0');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('setItem should set state in URL search', () => {
|
|
72
|
+
const storage = creatUrlStorage('search');
|
|
73
|
+
storage.setItem('state', { key1: 'value1', key2: 'value2' });
|
|
74
|
+
|
|
75
|
+
expect(mockHistory.replaceState).toHaveBeenCalledWith({}, '', '?key1=value1&key2=value2');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('setItem should handle non-string values by JSON stringifying them', () => {
|
|
79
|
+
const storage = creatUrlStorage('hash');
|
|
80
|
+
storage.setItem('state', { key1: { nested: 'value' }, key2: 123, key4: {} });
|
|
81
|
+
|
|
82
|
+
expect(location.hash).toEqual(`key1=%7B%22nested%22%3A%22value%22%7D&key2=123`);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -46,15 +46,32 @@ export const creatUrlStorage = <State extends object>(mode: 'hash' | 'search' =
|
|
|
46
46
|
const searchParameters = new URLSearchParams(getUrlSearch());
|
|
47
47
|
|
|
48
48
|
for (const [urlKey, v] of Object.entries(state)) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
switch (typeof v) {
|
|
50
|
+
case 'boolean': {
|
|
51
|
+
searchParameters.set(urlKey, (v ? 1 : 0).toString());
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'bigint':
|
|
56
|
+
case 'number': {
|
|
57
|
+
searchParameters.set(urlKey, v.toString());
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'string': {
|
|
62
|
+
searchParameters.set(urlKey, v);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'object': {
|
|
67
|
+
if (isEmpty(v)) {
|
|
68
|
+
searchParameters.delete(urlKey);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
searchParameters.set(urlKey, JSON.stringify(v));
|
|
72
|
+
searchParameters.set(urlKey, JSON.stringify(v));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
77
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Inspector/PluginResultJSON.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/BuiltinType/index.test.tsx
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/BuiltinType/index.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/DefaultType/index.tsx
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/MarkdownType/index.tsx
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/StandaloneType/Iframe.tsx
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/StandaloneType/index.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/iframeOnReady.test.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/iframeOnReady.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/listenToPlugin.test.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/listenToPlugin.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginSettings.test.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginSettings.ts
RENAMED
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/pluginState.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
/package/src/features/Conversation/{ChatList/Plugins → Plugins}/Render/utils/postMessage.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|