@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  3. package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/chunks/index.require.js +1 -3
  6. package/dist/react-native/chunks/secure-storage.require.js +2 -6
  7. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
  9. package/dist/react-native/index.d.ts +14 -2
  10. package/dist/react-native/index.js +2 -10
  11. package/dist/rozenite.json +1 -1
  12. package/package.json +29 -29
  13. package/react-native.ts +11 -36
  14. package/rozenite.config.ts +2 -6
  15. package/sdk.ts +1 -5
  16. package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
  17. package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
  18. package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
  19. package/src/react-native/__tests__/import.test.ts +9 -8
  20. package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
  21. package/src/react-native/__tests__/stress-verification.test.ts +5 -18
  22. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
  23. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  24. package/src/react-native/adapters/async-storage.ts +2 -5
  25. package/src/react-native/adapters/index.ts +2 -8
  26. package/src/react-native/adapters/mmkv.ts +7 -24
  27. package/src/react-native/adapters/secure-storage.ts +2 -6
  28. package/src/react-native/entry-preview-pagination.ts +11 -27
  29. package/src/react-native/export-snapshot.ts +3 -9
  30. package/src/react-native/full-entry-request.ts +3 -8
  31. package/src/react-native/import.ts +10 -21
  32. package/src/react-native/storage-discovery.ts +2 -6
  33. package/src/react-native/storage-view.ts +2 -6
  34. package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
  35. package/src/react-native/useStorageAgentTools.ts +8 -24
  36. package/src/shared/__tests__/entry-preview.test.ts +9 -27
  37. package/src/shared/__tests__/snapshot.test.ts +10 -32
  38. package/src/shared/agent-tools.ts +9 -30
  39. package/src/shared/entry-preview.ts +3 -8
  40. package/src/shared/messaging.ts +15 -11
  41. package/src/shared/snapshot.ts +4 -17
  42. package/src/shared/types.ts +2 -4
  43. package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
  44. package/src/ui/__tests__/binary.test.ts +13 -33
  45. package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
  46. package/src/ui/__tests__/panel.test.tsx +41 -68
  47. package/src/ui/__tests__/storage-groups.test.ts +1 -4
  48. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  49. package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
  50. package/src/ui/add-entry-dialog.tsx +4 -15
  51. package/src/ui/binary-value-editor-state.ts +6 -24
  52. package/src/ui/binary-value-editor.tsx +5 -17
  53. package/src/ui/binary.ts +6 -19
  54. package/src/ui/edit-entry-dialog.tsx +3 -11
  55. package/src/ui/editor-switcher.tsx +2 -11
  56. package/src/ui/entry-detail-dialog.tsx +5 -14
  57. package/src/ui/format-value.tsx +2 -9
  58. package/src/ui/import-dialog.tsx +7 -19
  59. package/src/ui/large-value-viewer-state.ts +2 -9
  60. package/src/ui/large-value-viewer.tsx +3 -9
  61. package/src/ui/panel.tsx +65 -153
  62. package/src/ui/query-client.tsx +4 -15
  63. package/src/ui/type-conversion.ts +2 -5
  64. package/src/ui/typed-value-editor.tsx +1 -5
  65. package/src/ui/use-storage-entries.ts +6 -21
  66. package/src/ui/utils.ts +1 -4
  67. package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
  68. package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
@@ -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
@@ -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: {
@@ -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: StorageCapabilities,
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
- 0x49, 0x48, 0x44, 0x52,
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
- 0x49, 0x48, 0x44, 0x52,
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
- 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
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
- 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
219
- 0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a,
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
- onClick,
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 = mocks.client.send.mock.calls.find(
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
- expect(mocks.client.request).toHaveBeenCalledTimes(2),
276
- );
277
- expect(
278
- mocks.client.request.mock.calls.map(
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
- expect(mocks.client.request).toHaveBeenCalledTimes(2),
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
- expect(mocks.client.request).toHaveBeenCalledTimes(2),
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
- 0x74, 0x72, 0x75, 0x65,
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
- true,
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
  });