@rozenite/storage-plugin 2.0.0 → 2.1.0
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 +18 -0
- package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
- package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.js +1 -3
- package/dist/react-native/chunks/secure-storage.require.js +2 -6
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
- package/dist/react-native/index.d.ts +14 -2
- package/dist/react-native/index.js +2 -10
- package/dist/rozenite.json +1 -1
- package/package.json +29 -29
- package/react-native.ts +11 -36
- package/rozenite.config.ts +2 -6
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
- package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
- package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
- package/src/react-native/__tests__/import.test.ts +9 -8
- package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
- package/src/react-native/__tests__/stress-verification.test.ts +5 -18
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +2 -5
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +7 -24
- package/src/react-native/adapters/secure-storage.ts +2 -6
- package/src/react-native/entry-preview-pagination.ts +11 -27
- package/src/react-native/export-snapshot.ts +3 -9
- package/src/react-native/full-entry-request.ts +3 -8
- package/src/react-native/import.ts +10 -21
- package/src/react-native/storage-discovery.ts +2 -6
- package/src/react-native/storage-view.ts +2 -6
- package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
- package/src/react-native/useStorageAgentTools.ts +8 -24
- package/src/shared/__tests__/entry-preview.test.ts +9 -27
- package/src/shared/__tests__/snapshot.test.ts +10 -32
- package/src/shared/agent-tools.ts +9 -30
- package/src/shared/entry-preview.ts +3 -8
- package/src/shared/messaging.ts +15 -11
- package/src/shared/snapshot.ts +4 -17
- package/src/shared/types.ts +2 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
- package/src/ui/__tests__/binary.test.ts +13 -33
- package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
- package/src/ui/__tests__/panel.test.tsx +41 -68
- package/src/ui/__tests__/storage-groups.test.ts +1 -4
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
- package/src/ui/add-entry-dialog.tsx +4 -15
- package/src/ui/binary-value-editor-state.ts +6 -24
- package/src/ui/binary-value-editor.tsx +5 -17
- package/src/ui/binary.ts +6 -19
- package/src/ui/edit-entry-dialog.tsx +3 -11
- package/src/ui/editor-switcher.tsx +2 -11
- package/src/ui/entry-detail-dialog.tsx +5 -14
- package/src/ui/format-value.tsx +2 -9
- package/src/ui/import-dialog.tsx +7 -19
- package/src/ui/large-value-viewer-state.ts +2 -9
- package/src/ui/large-value-viewer.tsx +3 -9
- package/src/ui/panel.tsx +65 -153
- package/src/ui/query-client.tsx +4 -15
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +1 -5
- package/src/ui/use-storage-entries.ts +6 -21
- package/src/ui/utils.ts +1 -4
- package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
- package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
package/src/shared/messaging.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
StorageDescriptor,
|
|
3
|
-
StorageEntry,
|
|
4
|
-
StorageEntryPreview,
|
|
5
|
-
StorageTarget,
|
|
6
|
-
} from './types';
|
|
1
|
+
import type { StorageDescriptor, StorageEntry, StorageEntryPreview, StorageTarget } from './types';
|
|
7
2
|
import type { ImportPreview, StorageSnapshotV1 } from './snapshot';
|
|
8
3
|
|
|
9
4
|
export type StorageSetEntryEvent = {
|
|
@@ -23,11 +18,7 @@ export type StoragePurgeEvent = {
|
|
|
23
18
|
target: StorageTarget;
|
|
24
19
|
};
|
|
25
20
|
|
|
26
|
-
export type StorageInvalidationOperation =
|
|
27
|
-
| 'set'
|
|
28
|
-
| 'delete'
|
|
29
|
-
| 'import'
|
|
30
|
-
| 'purge';
|
|
21
|
+
export type StorageInvalidationOperation = 'set' | 'delete' | 'import' | 'purge';
|
|
31
22
|
|
|
32
23
|
export type StorageInvalidatedEvent = {
|
|
33
24
|
type: 'storage-invalidated';
|
|
@@ -35,6 +26,18 @@ export type StorageInvalidatedEvent = {
|
|
|
35
26
|
/** Native subscriptions only expose a key, not the operation. */
|
|
36
27
|
key?: string;
|
|
37
28
|
operation?: StorageInvalidationOperation;
|
|
29
|
+
/** Updated entry count for the storage after the invalidation. */
|
|
30
|
+
entryCount: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Announced by the device once it is listening for panel requests. The panel is
|
|
35
|
+
* recreated on every app reload and asks for storages immediately, which can
|
|
36
|
+
* land before the app's React tree has mounted the plugin; this lets the panel
|
|
37
|
+
* ask again instead of waiting forever on a dropped request.
|
|
38
|
+
*/
|
|
39
|
+
export type StorageDeviceReadyEvent = {
|
|
40
|
+
type: 'device-ready';
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
export type StorageDiscoverStoragesRequestEvent = {
|
|
@@ -156,6 +159,7 @@ export type StorageEvent =
|
|
|
156
159
|
| StorageDeleteEntryEvent
|
|
157
160
|
| StoragePurgeEvent
|
|
158
161
|
| StorageInvalidatedEvent
|
|
162
|
+
| StorageDeviceReadyEvent
|
|
159
163
|
| StorageDiscoverStoragesRequestEvent
|
|
160
164
|
| StorageDiscoverStoragesResponseEvent
|
|
161
165
|
| StorageListEntryPreviewsRequestEvent
|
package/src/shared/snapshot.ts
CHANGED
|
@@ -48,10 +48,7 @@ const describe = (value: unknown): string => {
|
|
|
48
48
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
49
49
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
50
|
|
|
51
|
-
const expectObject = (
|
|
52
|
-
value: unknown,
|
|
53
|
-
path: string,
|
|
54
|
-
): Record<string, unknown> => {
|
|
51
|
+
const expectObject = (value: unknown, path: string): Record<string, unknown> => {
|
|
55
52
|
if (!isPlainObject(value)) {
|
|
56
53
|
throw new ParseException(path, `Expected object, got ${describe(value)}`);
|
|
57
54
|
}
|
|
@@ -73,10 +70,7 @@ const expectArray = (value: unknown, path: string): unknown[] => {
|
|
|
73
70
|
};
|
|
74
71
|
|
|
75
72
|
const expectEntryType = (value: unknown, path: string): StorageEntryType => {
|
|
76
|
-
if (
|
|
77
|
-
typeof value !== 'string' ||
|
|
78
|
-
!DEFAULT_SUPPORTED_TYPES.includes(value as StorageEntryType)
|
|
79
|
-
) {
|
|
73
|
+
if (typeof value !== 'string' || !DEFAULT_SUPPORTED_TYPES.includes(value as StorageEntryType)) {
|
|
80
74
|
throw new ParseException(
|
|
81
75
|
path,
|
|
82
76
|
`Expected one of ${DEFAULT_SUPPORTED_TYPES.join(', ')}, got ${describe(value)}`,
|
|
@@ -151,12 +145,7 @@ const parseEntry = (raw: unknown, path: string): StorageEntry => {
|
|
|
151
145
|
);
|
|
152
146
|
}
|
|
153
147
|
const bytes = value.map((byte, index) => {
|
|
154
|
-
if (
|
|
155
|
-
typeof byte !== 'number' ||
|
|
156
|
-
!Number.isInteger(byte) ||
|
|
157
|
-
byte < 0 ||
|
|
158
|
-
byte > 255
|
|
159
|
-
) {
|
|
148
|
+
if (typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
160
149
|
throw new ParseException(
|
|
161
150
|
`${valuePath}[${index}]`,
|
|
162
151
|
`Expected uint8 (integer 0-255), got ${describe(byte)}`,
|
|
@@ -183,9 +172,7 @@ export const parseSnapshot = (raw: unknown): ParseResult => {
|
|
|
183
172
|
const createdAt = expectString(root.createdAt, 'createdAt');
|
|
184
173
|
const storage = parseStorageMeta(root.storage);
|
|
185
174
|
const rawEntries = expectArray(root.entries, 'entries');
|
|
186
|
-
const entries = rawEntries.map((entry, index) =>
|
|
187
|
-
parseEntry(entry, `entries[${index}]`),
|
|
188
|
-
);
|
|
175
|
+
const entries = rawEntries.map((entry, index) => parseEntry(entry, `entries[${index}]`));
|
|
189
176
|
return {
|
|
190
177
|
ok: true,
|
|
191
178
|
snapshot: {
|
package/src/shared/types.ts
CHANGED
|
@@ -80,7 +80,5 @@ export const DEFAULT_SUPPORTED_TYPES: StorageEntryType[] = [
|
|
|
80
80
|
export const getStorageViewId = ({ adapterId, storageId }: StorageTarget) =>
|
|
81
81
|
`${adapterId}:${storageId}`;
|
|
82
82
|
|
|
83
|
-
export const supportsType = (
|
|
84
|
-
capabilities
|
|
85
|
-
type: StorageEntryType,
|
|
86
|
-
) => capabilities.supportedTypes.includes(type);
|
|
83
|
+
export const supportsType = (capabilities: StorageCapabilities, type: StorageEntryType) =>
|
|
84
|
+
capabilities.supportedTypes.includes(type);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
initialState,
|
|
4
|
-
reduce,
|
|
5
|
-
validate,
|
|
6
|
-
type EditorState,
|
|
7
|
-
} from '../binary-value-editor-state';
|
|
2
|
+
import { initialState, reduce, validate, type EditorState } from '../binary-value-editor-state';
|
|
8
3
|
|
|
9
4
|
const hexState = (bytes?: number[]): EditorState =>
|
|
10
5
|
initialState({ initialBytes: bytes, mode: 'hex' });
|
|
@@ -29,9 +24,7 @@ describe('initialState', () => {
|
|
|
29
24
|
});
|
|
30
25
|
|
|
31
26
|
it('encodes initial bytes as base64 when mode is base64', () => {
|
|
32
|
-
expect(
|
|
33
|
-
initialState({ initialBytes: [0x48, 0x69], mode: 'base64' }),
|
|
34
|
-
).toEqual({
|
|
27
|
+
expect(initialState({ initialBytes: [0x48, 0x69], mode: 'base64' })).toEqual({
|
|
35
28
|
mode: 'base64',
|
|
36
29
|
text: 'SGk=',
|
|
37
30
|
bytes: [0x48, 0x69],
|
|
@@ -101,13 +94,12 @@ describe('reduce: set-text', () => {
|
|
|
101
94
|
|
|
102
95
|
describe('reduce: normalize-paste', () => {
|
|
103
96
|
it('rewrites pasted hexdump into canonical grouped hex', () => {
|
|
104
|
-
const pasted =
|
|
105
|
-
'00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |.PNG........IHDR|';
|
|
97
|
+
const pasted = '00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |.PNG........IHDR|';
|
|
106
98
|
const next = reduce(hexState(), { type: 'normalize-paste', text: pasted });
|
|
107
99
|
expect(next.text).toBe('89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52');
|
|
108
100
|
expect(next.bytes).toEqual([
|
|
109
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
110
|
-
|
|
101
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
102
|
+
0x52,
|
|
111
103
|
]);
|
|
112
104
|
expect(next.error).toBeNull();
|
|
113
105
|
});
|
|
@@ -178,9 +170,7 @@ describe('reduce: switch-mode', () => {
|
|
|
178
170
|
|
|
179
171
|
describe('reduce: replace-bytes', () => {
|
|
180
172
|
it('formats fetched bytes after the editor has had a chance to paint', () => {
|
|
181
|
-
expect(
|
|
182
|
-
reduce(hexState(), { type: 'replace-bytes', bytes: [0x48, 0x69] }),
|
|
183
|
-
).toEqual({
|
|
173
|
+
expect(reduce(hexState(), { type: 'replace-bytes', bytes: [0x48, 0x69] })).toEqual({
|
|
184
174
|
mode: 'hex',
|
|
185
175
|
text: '48 69',
|
|
186
176
|
bytes: [0x48, 0x69],
|
|
@@ -26,8 +26,8 @@ describe('bytesToGroupedHex', () => {
|
|
|
26
26
|
it('inserts a double-space gap between bytes 8 and 9', () => {
|
|
27
27
|
expect(
|
|
28
28
|
bytesToGroupedHex([
|
|
29
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
30
|
-
|
|
29
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
30
|
+
0x52,
|
|
31
31
|
]),
|
|
32
32
|
).toBe('89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52');
|
|
33
33
|
});
|
|
@@ -44,17 +44,7 @@ describe('bytesToGroupedHex', () => {
|
|
|
44
44
|
|
|
45
45
|
describe('bytesToHexdump', () => {
|
|
46
46
|
it('renders offset, grouped hex, and ASCII column', () => {
|
|
47
|
-
const bytes = [
|
|
48
|
-
...PNG_SIGNATURE,
|
|
49
|
-
0x00,
|
|
50
|
-
0x00,
|
|
51
|
-
0x00,
|
|
52
|
-
0x0d,
|
|
53
|
-
0x49,
|
|
54
|
-
0x48,
|
|
55
|
-
0x44,
|
|
56
|
-
0x52,
|
|
57
|
-
];
|
|
47
|
+
const bytes = [...PNG_SIGNATURE, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52];
|
|
58
48
|
expect(bytesToHexdump(bytes)).toBe(
|
|
59
49
|
'00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |.PNG........IHDR|',
|
|
60
50
|
);
|
|
@@ -71,9 +61,7 @@ describe('bytesToHexdump', () => {
|
|
|
71
61
|
const bytes = [0x41, 0x42, 0x43];
|
|
72
62
|
const line = bytesToHexdump(bytes);
|
|
73
63
|
// Hex section width must be exactly 48 chars before the " |..." block.
|
|
74
|
-
expect(line).toBe(
|
|
75
|
-
'00000000 41 42 43 |ABC|',
|
|
76
|
-
);
|
|
64
|
+
expect(line).toBe('00000000 41 42 43 |ABC|');
|
|
77
65
|
});
|
|
78
66
|
|
|
79
67
|
it('renders non-printable bytes as dots in the ASCII column', () => {
|
|
@@ -159,21 +147,15 @@ describe('compactBufferPreview', () => {
|
|
|
159
147
|
});
|
|
160
148
|
|
|
161
149
|
it('omits the ellipsis when the buffer fits within maxBytes', () => {
|
|
162
|
-
expect(compactBufferPreview([0x89, 0x50, 0x4e, 0x47])).toBe(
|
|
163
|
-
'89 50 4E 47 4 B',
|
|
164
|
-
);
|
|
150
|
+
expect(compactBufferPreview([0x89, 0x50, 0x4e, 0x47])).toBe('89 50 4E 47 4 B');
|
|
165
151
|
});
|
|
166
152
|
|
|
167
153
|
it('includes an ellipsis when truncated', () => {
|
|
168
|
-
expect(compactBufferPreview(new Array(128).fill(0xab))).toBe(
|
|
169
|
-
'AB AB AB AB AB AB AB AB … 128 B',
|
|
170
|
-
);
|
|
154
|
+
expect(compactBufferPreview(new Array(128).fill(0xab))).toBe('AB AB AB AB AB AB AB AB … 128 B');
|
|
171
155
|
});
|
|
172
156
|
|
|
173
157
|
it('respects a custom maxBytes', () => {
|
|
174
|
-
expect(compactBufferPreview([0x01, 0x02, 0x03], { maxBytes: 2 })).toBe(
|
|
175
|
-
'01 02 … 3 B',
|
|
176
|
-
);
|
|
158
|
+
expect(compactBufferPreview([0x01, 0x02, 0x03], { maxBytes: 2 })).toBe('01 02 … 3 B');
|
|
177
159
|
});
|
|
178
160
|
});
|
|
179
161
|
|
|
@@ -194,14 +176,12 @@ describe('hexInputToBytes', () => {
|
|
|
194
176
|
|
|
195
177
|
it('parses multiline grouped hex', () => {
|
|
196
178
|
expect(
|
|
197
|
-
hexInputToBytes(
|
|
198
|
-
'89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52\n00 00 00 20 00 00 00 20',
|
|
199
|
-
),
|
|
179
|
+
hexInputToBytes('89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52\n00 00 00 20 00 00 00 20'),
|
|
200
180
|
).toEqual({
|
|
201
181
|
ok: true,
|
|
202
182
|
value: [
|
|
203
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
204
|
-
|
|
183
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
184
|
+
0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
|
|
205
185
|
],
|
|
206
186
|
});
|
|
207
187
|
});
|
|
@@ -214,9 +194,9 @@ describe('hexInputToBytes', () => {
|
|
|
214
194
|
expect(hexInputToBytes(pasted)).toEqual({
|
|
215
195
|
ok: true,
|
|
216
196
|
value: [
|
|
217
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
198
|
+
0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x08, 0x06, 0x00, 0x00, 0x00, 0x73,
|
|
199
|
+
0x7a, 0x7a,
|
|
220
200
|
],
|
|
221
201
|
});
|
|
222
202
|
});
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
4
|
import '@testing-library/jest-dom/vitest';
|
|
5
|
-
import {
|
|
6
|
-
TEXT_CHUNK_SIZE,
|
|
7
|
-
VIRTUALIZED_TEXT_THRESHOLD,
|
|
8
|
-
} from '../large-value-viewer-state';
|
|
5
|
+
import { TEXT_CHUNK_SIZE, VIRTUALIZED_TEXT_THRESHOLD } from '../large-value-viewer-state';
|
|
9
6
|
|
|
10
7
|
vi.mock('react-virtuoso', async () => {
|
|
11
8
|
const React = await import('react');
|
|
@@ -31,9 +28,7 @@ describe('large value viewers', () => {
|
|
|
31
28
|
const { container } = render(<TextValueViewer value={value} />);
|
|
32
29
|
|
|
33
30
|
expect(screen.getByTestId('virtuoso')).toBeInTheDocument();
|
|
34
|
-
expect(
|
|
35
|
-
container.querySelectorAll('[data-testid="virtuoso"] > div'),
|
|
36
|
-
).toHaveLength(4);
|
|
31
|
+
expect(container.querySelectorAll('[data-testid="virtuoso"] > div')).toHaveLength(4);
|
|
37
32
|
expect(container.textContent).toHaveLength(TEXT_CHUNK_SIZE * 4 + 1);
|
|
38
33
|
expect(container.textContent).not.toBe(value);
|
|
39
34
|
});
|
|
@@ -43,18 +38,14 @@ describe('large value viewers', () => {
|
|
|
43
38
|
const { container } = render(<HexdumpValueViewer bytes={bytes} />);
|
|
44
39
|
|
|
45
40
|
expect(screen.getByTestId('virtuoso')).toBeInTheDocument();
|
|
46
|
-
expect(
|
|
47
|
-
container.querySelectorAll('[data-testid="virtuoso"] > div'),
|
|
48
|
-
).toHaveLength(4);
|
|
41
|
+
expect(container.querySelectorAll('[data-testid="virtuoso"] > div')).toHaveLength(4);
|
|
49
42
|
expect(container.textContent).toContain('00000000');
|
|
50
43
|
expect(container.textContent).not.toContain('00003e70');
|
|
51
44
|
});
|
|
52
45
|
|
|
53
46
|
it('drops virtualized value markup when its detail interaction closes', () => {
|
|
54
47
|
const { container, unmount } = render(
|
|
55
|
-
<TextValueViewer
|
|
56
|
-
value={'x'.repeat(VIRTUALIZED_TEXT_THRESHOLD + TEXT_CHUNK_SIZE)}
|
|
57
|
-
/>,
|
|
48
|
+
<TextValueViewer value={'x'.repeat(VIRTUALIZED_TEXT_THRESHOLD + TEXT_CHUNK_SIZE)} />,
|
|
58
49
|
);
|
|
59
50
|
|
|
60
51
|
unmount();
|
|
@@ -24,9 +24,7 @@ const mocks = vi.hoisted(() => {
|
|
|
24
24
|
});
|
|
25
25
|
return {
|
|
26
26
|
client: { send, onMessage, request },
|
|
27
|
-
searchChange: undefined as
|
|
28
|
-
| ((event: React.ChangeEvent<HTMLInputElement>) => void)
|
|
29
|
-
| undefined,
|
|
27
|
+
searchChange: undefined as ((event: React.ChangeEvent<HTMLInputElement>) => void) | undefined,
|
|
30
28
|
sortKey: undefined as (() => void) | undefined,
|
|
31
29
|
emit: (type: keyof StorageEventMap, payload: unknown) =>
|
|
32
30
|
listeners.get(type)?.forEach((listener) => listener(payload)),
|
|
@@ -46,9 +44,7 @@ vi.mock('@rozenite/plugin-bridge', () => ({
|
|
|
46
44
|
}));
|
|
47
45
|
vi.mock('@rozenite/ui', async () => {
|
|
48
46
|
const React = await import('react');
|
|
49
|
-
const PassThrough = ({ children }: { children?: ReactNode }) =>
|
|
50
|
-
<>{children}</>
|
|
51
|
-
);
|
|
47
|
+
const PassThrough = ({ children }: { children?: ReactNode }) => <>{children}</>;
|
|
52
48
|
const PluginShell = Object.assign(PassThrough, { Body: PassThrough });
|
|
53
49
|
const Split = Object.assign(PassThrough, {
|
|
54
50
|
Pane: PassThrough,
|
|
@@ -87,25 +83,16 @@ vi.mock('@rozenite/ui', async () => {
|
|
|
87
83
|
'aria-label'?: string;
|
|
88
84
|
title?: string;
|
|
89
85
|
}) => (
|
|
90
|
-
<button
|
|
91
|
-
onClick={onClick}
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
aria-label={ariaLabel}
|
|
94
|
-
title={title}
|
|
95
|
-
>
|
|
86
|
+
<button onClick={onClick} disabled={disabled} aria-label={ariaLabel} title={title}>
|
|
96
87
|
{children}
|
|
97
88
|
</button>
|
|
98
89
|
),
|
|
99
90
|
});
|
|
100
91
|
return {
|
|
101
92
|
Badge: PassThrough,
|
|
102
|
-
Button: ({
|
|
103
|
-
children
|
|
104
|
-
|
|
105
|
-
}: {
|
|
106
|
-
children?: ReactNode;
|
|
107
|
-
onClick?: () => void;
|
|
108
|
-
}) => <button onClick={onClick}>{children}</button>,
|
|
93
|
+
Button: ({ children, onClick }: { children?: ReactNode; onClick?: () => void }) => (
|
|
94
|
+
<button onClick={onClick}>{children}</button>
|
|
95
|
+
),
|
|
109
96
|
ConfirmDialog: () => null,
|
|
110
97
|
EmptyState: () => <div />,
|
|
111
98
|
PluginShell,
|
|
@@ -131,9 +118,7 @@ vi.mock('@rozenite/ui', async () => {
|
|
|
131
118
|
data: { key: string }[];
|
|
132
119
|
onEndReached?: () => void;
|
|
133
120
|
onRowClick?: (entry: { key: string }) => void;
|
|
134
|
-
onSortingChange?: (
|
|
135
|
-
updater: (current: SortingItem[]) => SortingItem[],
|
|
136
|
-
) => void;
|
|
121
|
+
onSortingChange?: (updater: (current: SortingItem[]) => SortingItem[]) => void;
|
|
137
122
|
}) => {
|
|
138
123
|
mocks.sortKey = () => {
|
|
139
124
|
onSortingChange?.((current) => {
|
|
@@ -201,10 +186,10 @@ const renderPanel = async () => {
|
|
|
201
186
|
await act(async () => root.render(<StoragePanel />));
|
|
202
187
|
return { root, container };
|
|
203
188
|
};
|
|
189
|
+
const discoveryRequests = () =>
|
|
190
|
+
mocks.client.send.mock.calls.filter(([type]) => type === 'discover-storages');
|
|
204
191
|
const discover = async () => {
|
|
205
|
-
const request =
|
|
206
|
-
([type]) => type === 'discover-storages',
|
|
207
|
-
)?.[1];
|
|
192
|
+
const request = discoveryRequests().at(-1)?.[1];
|
|
208
193
|
await act(async () =>
|
|
209
194
|
mocks.emit('storage-descriptors', {
|
|
210
195
|
type: 'storage-descriptors',
|
|
@@ -219,11 +204,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
219
204
|
mocks.reset();
|
|
220
205
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
221
206
|
mocks.client.request.mockImplementation(
|
|
222
|
-
({
|
|
223
|
-
payload,
|
|
224
|
-
}: {
|
|
225
|
-
payload: { type: string; cursor?: string; key?: string };
|
|
226
|
-
}) => {
|
|
207
|
+
({ payload }: { payload: { type: string; cursor?: string; key?: string } }) => {
|
|
227
208
|
if (payload.type === 'list-entry-previews')
|
|
228
209
|
return Promise.resolve(previewResponse(payload.cursor));
|
|
229
210
|
if (payload.type === 'get-entry')
|
|
@@ -253,9 +234,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
253
234
|
await discover();
|
|
254
235
|
expect(container.textContent).toContain('1');
|
|
255
236
|
await act(async () => {
|
|
256
|
-
await vi.waitFor(() =>
|
|
257
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(1),
|
|
258
|
-
);
|
|
237
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(1));
|
|
259
238
|
await vi.waitFor(() =>
|
|
260
239
|
expect(
|
|
261
240
|
Array.from(container.querySelectorAll('button')).some(
|
|
@@ -271,14 +250,11 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
271
250
|
edge?.click();
|
|
272
251
|
edge?.click();
|
|
273
252
|
});
|
|
274
|
-
await vi.waitFor(() =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
([options]) => options.payload.cursor,
|
|
280
|
-
),
|
|
281
|
-
).toEqual([undefined, 'next']);
|
|
253
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
254
|
+
expect(mocks.client.request.mock.calls.map(([options]) => options.payload.cursor)).toEqual([
|
|
255
|
+
undefined,
|
|
256
|
+
'next',
|
|
257
|
+
]);
|
|
282
258
|
await act(async () => root.unmount());
|
|
283
259
|
});
|
|
284
260
|
|
|
@@ -295,9 +271,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
295
271
|
} as React.ChangeEvent<HTMLInputElement>);
|
|
296
272
|
vi.advanceTimersByTime(250);
|
|
297
273
|
});
|
|
298
|
-
await vi.waitFor(() =>
|
|
299
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(2),
|
|
300
|
-
);
|
|
274
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
301
275
|
expect(mocks.client.request.mock.calls[1][0].payload).toMatchObject({
|
|
302
276
|
cursor: undefined,
|
|
303
277
|
search: 'needle',
|
|
@@ -317,25 +291,19 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
317
291
|
};
|
|
318
292
|
|
|
319
293
|
await act(async () => sortKey());
|
|
320
|
-
await vi.waitFor(() =>
|
|
321
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(2),
|
|
322
|
-
);
|
|
294
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
323
295
|
expect(mocks.client.request.mock.calls[1][0].payload).toMatchObject({
|
|
324
296
|
keySortDirection: 'descending',
|
|
325
297
|
});
|
|
326
298
|
|
|
327
299
|
await act(async () => sortKey());
|
|
328
|
-
await vi.waitFor(() =>
|
|
329
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(3),
|
|
330
|
-
);
|
|
300
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(3));
|
|
331
301
|
expect(mocks.client.request.mock.calls[2][0].payload).toMatchObject({
|
|
332
302
|
keySortDirection: 'ascending',
|
|
333
303
|
});
|
|
334
304
|
|
|
335
305
|
await act(async () => sortKey());
|
|
336
|
-
await vi.waitFor(() =>
|
|
337
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(4),
|
|
338
|
-
);
|
|
306
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(4));
|
|
339
307
|
expect(mocks.client.request.mock.calls[3][0].payload).toMatchObject({
|
|
340
308
|
keySortDirection: 'descending',
|
|
341
309
|
});
|
|
@@ -368,17 +336,11 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
368
336
|
});
|
|
369
337
|
await act(async () =>
|
|
370
338
|
Array.from(container.querySelectorAll('button'))
|
|
371
|
-
.find(
|
|
372
|
-
(button) => button.getAttribute('aria-label') === 'Refresh storage',
|
|
373
|
-
)
|
|
339
|
+
.find((button) => button.getAttribute('aria-label') === 'Refresh storage')
|
|
374
340
|
?.click(),
|
|
375
341
|
);
|
|
376
|
-
await vi.waitFor(() =>
|
|
377
|
-
|
|
378
|
-
);
|
|
379
|
-
expect(
|
|
380
|
-
mocks.client.request.mock.calls[1][0].payload.cursor,
|
|
381
|
-
).toBeUndefined();
|
|
342
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
343
|
+
expect(mocks.client.request.mock.calls[1][0].payload.cursor).toBeUndefined();
|
|
382
344
|
await act(async () => root.unmount());
|
|
383
345
|
});
|
|
384
346
|
|
|
@@ -386,9 +348,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
386
348
|
const { root } = await renderPanel();
|
|
387
349
|
await discover();
|
|
388
350
|
await act(async () => {
|
|
389
|
-
await vi.waitFor(() =>
|
|
390
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(1),
|
|
391
|
-
);
|
|
351
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(1));
|
|
392
352
|
mocks.emit('storage-invalidated', {
|
|
393
353
|
type: 'storage-invalidated',
|
|
394
354
|
target,
|
|
@@ -401,9 +361,22 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
401
361
|
key: 'next',
|
|
402
362
|
});
|
|
403
363
|
});
|
|
404
|
-
await vi.waitFor(() =>
|
|
405
|
-
|
|
406
|
-
|
|
364
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
365
|
+
await act(async () => root.unmount());
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// The panel is recreated on every app reload and asks for storages before the
|
|
369
|
+
// app's React tree has mounted the plugin, so its first request is dropped.
|
|
370
|
+
it('runs discovery again when the device announces itself', async () => {
|
|
371
|
+
const { root, container } = await renderPanel();
|
|
372
|
+
expect(discoveryRequests()).toHaveLength(1);
|
|
373
|
+
expect(container.textContent).toContain('Waiting for storages');
|
|
374
|
+
|
|
375
|
+
await act(async () => mocks.emit('device-ready', { type: 'device-ready' }));
|
|
376
|
+
expect(discoveryRequests()).toHaveLength(2);
|
|
377
|
+
await discover();
|
|
378
|
+
|
|
379
|
+
expect(container.textContent).toContain('First');
|
|
407
380
|
await act(async () => root.unmount());
|
|
408
381
|
});
|
|
409
382
|
});
|
|
@@ -23,10 +23,7 @@ describe('buildStorageSidebarGroups', () => {
|
|
|
23
23
|
const snapshots = new Map([
|
|
24
24
|
['mmkv:default', entry('mmkv', 'default', 'MMKV', 'default', 3)],
|
|
25
25
|
['mmkv:user', entry('mmkv', 'user', 'MMKV', 'user', 1)],
|
|
26
|
-
[
|
|
27
|
-
'async-storage:default',
|
|
28
|
-
entry('async-storage', 'default', 'AsyncStorage', 'default', 5),
|
|
29
|
-
],
|
|
26
|
+
['async-storage:default', entry('async-storage', 'default', 'AsyncStorage', 'default', 5)],
|
|
30
27
|
]);
|
|
31
28
|
|
|
32
29
|
expect(buildStorageSidebarGroups(snapshots)).toEqual([
|
|
@@ -25,13 +25,9 @@ describe('convertValue', () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('UTF-8 encodes a string to a byte array', () => {
|
|
28
|
-
expect(convertValue('string', 'buffer', 'hello')).toEqual([
|
|
29
|
-
0x68, 0x65, 0x6c, 0x6c, 0x6f,
|
|
30
|
-
]);
|
|
28
|
+
expect(convertValue('string', 'buffer', 'hello')).toEqual([0x68, 0x65, 0x6c, 0x6c, 0x6f]);
|
|
31
29
|
// 'é' is two bytes in UTF-8: 0xC3 0xA9.
|
|
32
|
-
expect(convertValue('string', 'buffer', 'café')).toEqual([
|
|
33
|
-
0x63, 0x61, 0x66, 0xc3, 0xa9,
|
|
34
|
-
]);
|
|
30
|
+
expect(convertValue('string', 'buffer', 'café')).toEqual([0x63, 0x61, 0x66, 0xc3, 0xa9]);
|
|
35
31
|
expect(convertValue('string', 'buffer', '')).toEqual([]);
|
|
36
32
|
});
|
|
37
33
|
});
|
|
@@ -66,20 +62,14 @@ describe('convertValue', () => {
|
|
|
66
62
|
});
|
|
67
63
|
|
|
68
64
|
it('encodes the literal "true" / "false" bytes for buffer conversion', () => {
|
|
69
|
-
expect(convertValue('boolean', 'buffer', true)).toEqual([
|
|
70
|
-
|
|
71
|
-
]);
|
|
72
|
-
expect(convertValue('boolean', 'buffer', false)).toEqual([
|
|
73
|
-
0x66, 0x61, 0x6c, 0x73, 0x65,
|
|
74
|
-
]);
|
|
65
|
+
expect(convertValue('boolean', 'buffer', true)).toEqual([0x74, 0x72, 0x75, 0x65]);
|
|
66
|
+
expect(convertValue('boolean', 'buffer', false)).toEqual([0x66, 0x61, 0x6c, 0x73, 0x65]);
|
|
75
67
|
});
|
|
76
68
|
});
|
|
77
69
|
|
|
78
70
|
describe('from buffer', () => {
|
|
79
71
|
it('UTF-8 decodes bytes back to a string (the headline round-trip)', () => {
|
|
80
|
-
expect(
|
|
81
|
-
convertValue('buffer', 'string', [0x68, 0x65, 0x6c, 0x6c, 0x6f]),
|
|
82
|
-
).toBe('hello');
|
|
72
|
+
expect(convertValue('buffer', 'string', [0x68, 0x65, 0x6c, 0x6c, 0x6f])).toBe('hello');
|
|
83
73
|
});
|
|
84
74
|
|
|
85
75
|
it('returns an empty string when bytes are not valid UTF-8', () => {
|
|
@@ -88,19 +78,13 @@ describe('convertValue', () => {
|
|
|
88
78
|
|
|
89
79
|
it('decodes then parses to number, falls back to 0 on failure', () => {
|
|
90
80
|
expect(convertValue('buffer', 'number', [0x34, 0x32])).toBe(42); // "42"
|
|
91
|
-
expect(
|
|
92
|
-
convertValue('buffer', 'number', [0x68, 0x65, 0x6c, 0x6c, 0x6f]),
|
|
93
|
-
).toBe(0); // "hello" → NaN → 0
|
|
81
|
+
expect(convertValue('buffer', 'number', [0x68, 0x65, 0x6c, 0x6c, 0x6f])).toBe(0); // "hello" → NaN → 0
|
|
94
82
|
expect(convertValue('buffer', 'number', [0xff])).toBe(0); // invalid UTF-8 → 0
|
|
95
83
|
});
|
|
96
84
|
|
|
97
85
|
it('decodes then matches "true" for boolean conversion', () => {
|
|
98
|
-
expect(convertValue('buffer', 'boolean', [0x74, 0x72, 0x75, 0x65])).toBe(
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
expect(
|
|
102
|
-
convertValue('buffer', 'boolean', [0x66, 0x61, 0x6c, 0x73, 0x65]),
|
|
103
|
-
).toBe(false);
|
|
86
|
+
expect(convertValue('buffer', 'boolean', [0x74, 0x72, 0x75, 0x65])).toBe(true);
|
|
87
|
+
expect(convertValue('buffer', 'boolean', [0x66, 0x61, 0x6c, 0x73, 0x65])).toBe(false);
|
|
104
88
|
expect(convertValue('buffer', 'boolean', [0xff])).toBe(false);
|
|
105
89
|
});
|
|
106
90
|
});
|