@rozenite/storage-plugin 2.0.0 → 2.2.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 +36 -0
- package/dist/devtools/assets/panel-D5z1BY8U.js +32 -0
- package/dist/devtools/assets/panel-DVlWRg6d.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 +50 -63
- 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 +66 -87
- package/src/ui/binary-value-editor-state.ts +6 -24
- package/src/ui/binary-value-editor.tsx +6 -18
- package/src/ui/binary.ts +6 -19
- package/src/ui/edit-entry-dialog.tsx +59 -78
- package/src/ui/editor-switcher.tsx +2 -11
- package/src/ui/entry-detail-dialog.tsx +9 -16
- package/src/ui/format-value.tsx +2 -9
- package/src/ui/import-dialog.tsx +12 -24
- package/src/ui/large-value-viewer-state.ts +2 -9
- package/src/ui/large-value-viewer.tsx +3 -9
- package/src/ui/panel.tsx +129 -235
- 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/tsconfig.json +4 -7
- 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,28 +83,33 @@ 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: ({
|
|
93
|
+
Button: ({ children, onClick }: { children?: ReactNode; onClick?: () => void }) => (
|
|
94
|
+
<button onClick={onClick}>{children}</button>
|
|
95
|
+
),
|
|
96
|
+
IconButton: ({
|
|
103
97
|
children,
|
|
104
98
|
onClick,
|
|
99
|
+
label,
|
|
105
100
|
}: {
|
|
106
101
|
children?: ReactNode;
|
|
107
102
|
onClick?: () => void;
|
|
108
|
-
|
|
103
|
+
label: string;
|
|
104
|
+
}) => (
|
|
105
|
+
<button onClick={onClick} aria-label={label}>
|
|
106
|
+
{children}
|
|
107
|
+
</button>
|
|
108
|
+
),
|
|
109
109
|
ConfirmDialog: () => null,
|
|
110
110
|
EmptyState: () => <div />,
|
|
111
111
|
PluginShell,
|
|
112
|
+
useConfirmDialog: () => async () => true,
|
|
112
113
|
SearchField: ({
|
|
113
114
|
value,
|
|
114
115
|
onChange,
|
|
@@ -131,9 +132,7 @@ vi.mock('@rozenite/ui', async () => {
|
|
|
131
132
|
data: { key: string }[];
|
|
132
133
|
onEndReached?: () => void;
|
|
133
134
|
onRowClick?: (entry: { key: string }) => void;
|
|
134
|
-
onSortingChange?: (
|
|
135
|
-
updater: (current: SortingItem[]) => SortingItem[],
|
|
136
|
-
) => void;
|
|
135
|
+
onSortingChange?: (updater: (current: SortingItem[]) => SortingItem[]) => void;
|
|
137
136
|
}) => {
|
|
138
137
|
mocks.sortKey = () => {
|
|
139
138
|
onSortingChange?.((current) => {
|
|
@@ -201,10 +200,10 @@ const renderPanel = async () => {
|
|
|
201
200
|
await act(async () => root.render(<StoragePanel />));
|
|
202
201
|
return { root, container };
|
|
203
202
|
};
|
|
203
|
+
const discoveryRequests = () =>
|
|
204
|
+
mocks.client.send.mock.calls.filter(([type]) => type === 'discover-storages');
|
|
204
205
|
const discover = async () => {
|
|
205
|
-
const request =
|
|
206
|
-
([type]) => type === 'discover-storages',
|
|
207
|
-
)?.[1];
|
|
206
|
+
const request = discoveryRequests().at(-1)?.[1];
|
|
208
207
|
await act(async () =>
|
|
209
208
|
mocks.emit('storage-descriptors', {
|
|
210
209
|
type: 'storage-descriptors',
|
|
@@ -219,11 +218,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
219
218
|
mocks.reset();
|
|
220
219
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
221
220
|
mocks.client.request.mockImplementation(
|
|
222
|
-
({
|
|
223
|
-
payload,
|
|
224
|
-
}: {
|
|
225
|
-
payload: { type: string; cursor?: string; key?: string };
|
|
226
|
-
}) => {
|
|
221
|
+
({ payload }: { payload: { type: string; cursor?: string; key?: string } }) => {
|
|
227
222
|
if (payload.type === 'list-entry-previews')
|
|
228
223
|
return Promise.resolve(previewResponse(payload.cursor));
|
|
229
224
|
if (payload.type === 'get-entry')
|
|
@@ -253,9 +248,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
253
248
|
await discover();
|
|
254
249
|
expect(container.textContent).toContain('1');
|
|
255
250
|
await act(async () => {
|
|
256
|
-
await vi.waitFor(() =>
|
|
257
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(1),
|
|
258
|
-
);
|
|
251
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(1));
|
|
259
252
|
await vi.waitFor(() =>
|
|
260
253
|
expect(
|
|
261
254
|
Array.from(container.querySelectorAll('button')).some(
|
|
@@ -271,14 +264,11 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
271
264
|
edge?.click();
|
|
272
265
|
edge?.click();
|
|
273
266
|
});
|
|
274
|
-
await vi.waitFor(() =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
([options]) => options.payload.cursor,
|
|
280
|
-
),
|
|
281
|
-
).toEqual([undefined, 'next']);
|
|
267
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
268
|
+
expect(mocks.client.request.mock.calls.map(([options]) => options.payload.cursor)).toEqual([
|
|
269
|
+
undefined,
|
|
270
|
+
'next',
|
|
271
|
+
]);
|
|
282
272
|
await act(async () => root.unmount());
|
|
283
273
|
});
|
|
284
274
|
|
|
@@ -295,9 +285,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
295
285
|
} as React.ChangeEvent<HTMLInputElement>);
|
|
296
286
|
vi.advanceTimersByTime(250);
|
|
297
287
|
});
|
|
298
|
-
await vi.waitFor(() =>
|
|
299
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(2),
|
|
300
|
-
);
|
|
288
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
301
289
|
expect(mocks.client.request.mock.calls[1][0].payload).toMatchObject({
|
|
302
290
|
cursor: undefined,
|
|
303
291
|
search: 'needle',
|
|
@@ -317,25 +305,19 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
317
305
|
};
|
|
318
306
|
|
|
319
307
|
await act(async () => sortKey());
|
|
320
|
-
await vi.waitFor(() =>
|
|
321
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(2),
|
|
322
|
-
);
|
|
308
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
323
309
|
expect(mocks.client.request.mock.calls[1][0].payload).toMatchObject({
|
|
324
310
|
keySortDirection: 'descending',
|
|
325
311
|
});
|
|
326
312
|
|
|
327
313
|
await act(async () => sortKey());
|
|
328
|
-
await vi.waitFor(() =>
|
|
329
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(3),
|
|
330
|
-
);
|
|
314
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(3));
|
|
331
315
|
expect(mocks.client.request.mock.calls[2][0].payload).toMatchObject({
|
|
332
316
|
keySortDirection: 'ascending',
|
|
333
317
|
});
|
|
334
318
|
|
|
335
319
|
await act(async () => sortKey());
|
|
336
|
-
await vi.waitFor(() =>
|
|
337
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(4),
|
|
338
|
-
);
|
|
320
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(4));
|
|
339
321
|
expect(mocks.client.request.mock.calls[3][0].payload).toMatchObject({
|
|
340
322
|
keySortDirection: 'descending',
|
|
341
323
|
});
|
|
@@ -368,17 +350,11 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
368
350
|
});
|
|
369
351
|
await act(async () =>
|
|
370
352
|
Array.from(container.querySelectorAll('button'))
|
|
371
|
-
.find(
|
|
372
|
-
(button) => button.getAttribute('aria-label') === 'Refresh storage',
|
|
373
|
-
)
|
|
353
|
+
.find((button) => button.getAttribute('aria-label') === 'Refresh storage')
|
|
374
354
|
?.click(),
|
|
375
355
|
);
|
|
376
|
-
await vi.waitFor(() =>
|
|
377
|
-
|
|
378
|
-
);
|
|
379
|
-
expect(
|
|
380
|
-
mocks.client.request.mock.calls[1][0].payload.cursor,
|
|
381
|
-
).toBeUndefined();
|
|
356
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
357
|
+
expect(mocks.client.request.mock.calls[1][0].payload.cursor).toBeUndefined();
|
|
382
358
|
await act(async () => root.unmount());
|
|
383
359
|
});
|
|
384
360
|
|
|
@@ -386,9 +362,7 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
386
362
|
const { root } = await renderPanel();
|
|
387
363
|
await discover();
|
|
388
364
|
await act(async () => {
|
|
389
|
-
await vi.waitFor(() =>
|
|
390
|
-
expect(mocks.client.request).toHaveBeenCalledTimes(1),
|
|
391
|
-
);
|
|
365
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(1));
|
|
392
366
|
mocks.emit('storage-invalidated', {
|
|
393
367
|
type: 'storage-invalidated',
|
|
394
368
|
target,
|
|
@@ -401,9 +375,22 @@ describe('StoragePanel preview query cutover', () => {
|
|
|
401
375
|
key: 'next',
|
|
402
376
|
});
|
|
403
377
|
});
|
|
404
|
-
await vi.waitFor(() =>
|
|
405
|
-
|
|
406
|
-
|
|
378
|
+
await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2));
|
|
379
|
+
await act(async () => root.unmount());
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// The panel is recreated on every app reload and asks for storages before the
|
|
383
|
+
// app's React tree has mounted the plugin, so its first request is dropped.
|
|
384
|
+
it('runs discovery again when the device announces itself', async () => {
|
|
385
|
+
const { root, container } = await renderPanel();
|
|
386
|
+
expect(discoveryRequests()).toHaveLength(1);
|
|
387
|
+
expect(container.textContent).toContain('Waiting for storages');
|
|
388
|
+
|
|
389
|
+
await act(async () => mocks.emit('device-ready', { type: 'device-ready' }));
|
|
390
|
+
expect(discoveryRequests()).toHaveLength(2);
|
|
391
|
+
await discover();
|
|
392
|
+
|
|
393
|
+
expect(container.textContent).toContain('First');
|
|
407
394
|
await act(async () => root.unmount());
|
|
408
395
|
});
|
|
409
396
|
});
|
|
@@ -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
|
});
|