@bendyline/squisq 0.1.1

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 (102) hide show
  1. package/dist/Types-sh2VRxfo.d.ts +15 -0
  2. package/dist/chunk-7UDSRZKG.js +103 -0
  3. package/dist/chunk-7UDSRZKG.js.map +1 -0
  4. package/dist/chunk-O5HCIUAD.js +577 -0
  5. package/dist/chunk-O5HCIUAD.js.map +1 -0
  6. package/dist/chunk-PJ7AODIM.js +3040 -0
  7. package/dist/chunk-PJ7AODIM.js.map +1 -0
  8. package/dist/chunk-QWVRN6I4.js +81 -0
  9. package/dist/chunk-QWVRN6I4.js.map +1 -0
  10. package/dist/chunk-S4F2RY37.js +92 -0
  11. package/dist/chunk-S4F2RY37.js.map +1 -0
  12. package/dist/chunk-URU6QMLY.js +888 -0
  13. package/dist/chunk-URU6QMLY.js.map +1 -0
  14. package/dist/chunk-VJN7UB2Z.js +145 -0
  15. package/dist/chunk-VJN7UB2Z.js.map +1 -0
  16. package/dist/doc/index.d.ts +3 -0
  17. package/dist/doc/index.js +126 -0
  18. package/dist/doc/index.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.js +217 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/markdown/index.d.ts +237 -0
  23. package/dist/markdown/index.js +33 -0
  24. package/dist/markdown/index.js.map +1 -0
  25. package/dist/schemas/index.d.ts +70 -0
  26. package/dist/schemas/index.js +61 -0
  27. package/dist/schemas/index.js.map +1 -0
  28. package/dist/spatial/index.d.ts +111 -0
  29. package/dist/spatial/index.js +25 -0
  30. package/dist/spatial/index.js.map +1 -0
  31. package/dist/storage/index.d.ts +91 -0
  32. package/dist/storage/index.js +11 -0
  33. package/dist/storage/index.js.map +1 -0
  34. package/dist/story/index.d.ts +691 -0
  35. package/dist/story/index.js +126 -0
  36. package/dist/story/index.js.map +1 -0
  37. package/dist/themeLibrary-DySHPcgj.d.ts +1352 -0
  38. package/dist/types-DHiv_Pnm.d.ts +496 -0
  39. package/package.json +91 -0
  40. package/src/__tests__/animationUtils.test.ts +77 -0
  41. package/src/__tests__/geohash.test.ts +109 -0
  42. package/src/__tests__/getLayers.test.ts +200 -0
  43. package/src/__tests__/haversine.test.ts +63 -0
  44. package/src/__tests__/localForageAdapter.test.ts +155 -0
  45. package/src/__tests__/markdown.test.ts +806 -0
  46. package/src/__tests__/markdownToDoc.test.ts +374 -0
  47. package/src/__tests__/storage.test.ts +60 -0
  48. package/src/__tests__/templates.test.ts +247 -0
  49. package/src/doc/docToMarkdown.ts +98 -0
  50. package/src/doc/getLayers.ts +152 -0
  51. package/src/doc/index.ts +8 -0
  52. package/src/doc/markdownToDoc.ts +277 -0
  53. package/src/doc/templates/accentImage.ts +338 -0
  54. package/src/doc/templates/captionUtils.ts +33 -0
  55. package/src/doc/templates/comparisonBar.ts +140 -0
  56. package/src/doc/templates/coverBlock.ts +156 -0
  57. package/src/doc/templates/dateEvent.ts +122 -0
  58. package/src/doc/templates/definitionCard.ts +129 -0
  59. package/src/doc/templates/factCard.ts +114 -0
  60. package/src/doc/templates/fullBleedQuote.ts +59 -0
  61. package/src/doc/templates/imageWithCaption.ts +167 -0
  62. package/src/doc/templates/index.ts +571 -0
  63. package/src/doc/templates/listBlock.ts +101 -0
  64. package/src/doc/templates/mapBlock.ts +132 -0
  65. package/src/doc/templates/persistentLayers.ts +522 -0
  66. package/src/doc/templates/photoGrid.ts +199 -0
  67. package/src/doc/templates/pullQuote.ts +119 -0
  68. package/src/doc/templates/quoteBlock.ts +112 -0
  69. package/src/doc/templates/sectionHeader.ts +105 -0
  70. package/src/doc/templates/statHighlight.ts +113 -0
  71. package/src/doc/templates/titleBlock.ts +100 -0
  72. package/src/doc/templates/twoColumn.ts +239 -0
  73. package/src/doc/templates/videoPullQuote.ts +128 -0
  74. package/src/doc/templates/videoWithCaption.ts +126 -0
  75. package/src/doc/utils/animationUtils.ts +135 -0
  76. package/src/doc/utils/themeUtils.ts +139 -0
  77. package/src/index.ts +5 -0
  78. package/src/markdown/convert.ts +897 -0
  79. package/src/markdown/htmlParse.ts +212 -0
  80. package/src/markdown/index.ts +110 -0
  81. package/src/markdown/parse.ts +103 -0
  82. package/src/markdown/stringify.ts +122 -0
  83. package/src/markdown/types.ts +605 -0
  84. package/src/markdown/utils.ts +165 -0
  85. package/src/ngeohash.d.ts +11 -0
  86. package/src/schemas/BlockTemplates.ts +706 -0
  87. package/src/schemas/Doc.ts +565 -0
  88. package/src/schemas/LayoutStrategy.ts +178 -0
  89. package/src/schemas/MediaProvider.ts +70 -0
  90. package/src/schemas/Theme.ts +235 -0
  91. package/src/schemas/Types.ts +15 -0
  92. package/src/schemas/Viewport.ts +91 -0
  93. package/src/schemas/index.ts +14 -0
  94. package/src/schemas/themeLibrary.ts +492 -0
  95. package/src/spatial/Geohash.ts +168 -0
  96. package/src/spatial/Haversine.ts +59 -0
  97. package/src/spatial/index.ts +2 -0
  98. package/src/storage/LocalForageAdapter.ts +103 -0
  99. package/src/storage/LocalStorageAdapter.ts +57 -0
  100. package/src/storage/MemoryStorageAdapter.ts +37 -0
  101. package/src/storage/Storage.ts +28 -0
  102. package/src/storage/index.ts +5 -0
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ encodeGeohash,
4
+ decodeGeohash,
5
+ getNeighbors,
6
+ getGeohash4Neighbors,
7
+ getGeohashPrefix,
8
+ geohashToHierarchicalPath,
9
+ getGeohashPath,
10
+ geohashOverlapsBounds,
11
+ } from '../spatial/Geohash';
12
+
13
+ describe('encodeGeohash / decodeGeohash round-trip', () => {
14
+ it('encodes and decodes Seattle', () => {
15
+ const lat = 47.6062;
16
+ const lng = -122.3321;
17
+ const hash = encodeGeohash(lat, lng, 6);
18
+ expect(hash).toHaveLength(6);
19
+
20
+ const decoded = decodeGeohash(hash);
21
+ expect(decoded.lat).toBeCloseTo(lat, 1);
22
+ expect(decoded.lng).toBeCloseTo(lng, 1);
23
+ });
24
+
25
+ it('encodes and decodes equator/prime meridian', () => {
26
+ const hash = encodeGeohash(0, 0, 4);
27
+ expect(hash).toHaveLength(4);
28
+ const decoded = decodeGeohash(hash);
29
+ expect(decoded.lat).toBeCloseTo(0, 0);
30
+ expect(decoded.lng).toBeCloseTo(0, 0);
31
+ });
32
+
33
+ it('defaults to precision 9', () => {
34
+ const hash = encodeGeohash(47.6, -122.3);
35
+ expect(hash).toHaveLength(9);
36
+ });
37
+ });
38
+
39
+ describe('getNeighbors', () => {
40
+ it('returns 8 neighbors', () => {
41
+ const neighbors = getNeighbors('c23n');
42
+ expect(neighbors).toHaveLength(8);
43
+ neighbors.forEach((n) => expect(n).toHaveLength(4));
44
+ });
45
+ });
46
+
47
+ describe('getGeohash4Neighbors', () => {
48
+ it('returns center + 8 neighbors (9 total)', () => {
49
+ const result = getGeohash4Neighbors('c23n');
50
+ expect(result).toHaveLength(9);
51
+ expect(result[0]).toBe('c23n');
52
+ });
53
+
54
+ it('throws for non-4-char input', () => {
55
+ expect(() => getGeohash4Neighbors('c2')).toThrow();
56
+ expect(() => getGeohash4Neighbors('c23nn')).toThrow();
57
+ });
58
+ });
59
+
60
+ describe('getGeohashPrefix', () => {
61
+ it('extracts prefix', () => {
62
+ expect(getGeohashPrefix('c23nxy', 4)).toBe('c23n');
63
+ expect(getGeohashPrefix('c23nxy', 2)).toBe('c2');
64
+ });
65
+ });
66
+
67
+ describe('geohashToHierarchicalPath', () => {
68
+ it('converts 4-char geohash to path', () => {
69
+ expect(geohashToHierarchicalPath('c23n')).toBe('c/2/3/n');
70
+ expect(geohashToHierarchicalPath('9q8y')).toBe('9/q/8/y');
71
+ });
72
+
73
+ it('throws for non-4-char input', () => {
74
+ expect(() => geohashToHierarchicalPath('c2')).toThrow();
75
+ });
76
+ });
77
+
78
+ describe('getGeohashPath', () => {
79
+ it('returns empty for same cell', () => {
80
+ expect(getGeohashPath('c23n', 'c23n')).toEqual([]);
81
+ });
82
+
83
+ it('returns intermediate cells between two geohashes', () => {
84
+ const path = getGeohashPath('c23n', 'c24k');
85
+ expect(path.length).toBeGreaterThan(0);
86
+ path.forEach((h) => {
87
+ expect(h).toHaveLength(4);
88
+ expect(h).not.toBe('c23n');
89
+ expect(h).not.toBe('c24k');
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('geohashOverlapsBounds', () => {
95
+ it('detects overlap', () => {
96
+ // Seattle area: c23n
97
+ const bounds = { north: 48, south: 47, east: -122, west: -123 };
98
+ const hash = encodeGeohash(47.6, -122.3, 4);
99
+ expect(geohashOverlapsBounds(hash, bounds)).toBe(true);
100
+ });
101
+
102
+ it('detects non-overlap', () => {
103
+ // Tokyo area bounds
104
+ const bounds = { north: 36, south: 35, east: 140, west: 139 };
105
+ // Seattle geohash
106
+ const hash = encodeGeohash(47.6, -122.3, 4);
107
+ expect(geohashOverlapsBounds(hash, bounds)).toBe(false);
108
+ });
109
+ });
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { getLayers, RenderContext } from '../doc/getLayers.js';
3
+ import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
4
+ import { TemplateBlock, PersistentLayerConfig } from '../schemas/BlockTemplates.js';
5
+ import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
6
+ import type { Block, Layer, TextLayer, ShapeLayer } from '../schemas/Doc.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ function makeTemplateBlock(overrides: Partial<TemplateBlock> = {}): TemplateBlock {
13
+ return {
14
+ template: 'titleBlock',
15
+ id: 'test-title-1',
16
+ duration: 5,
17
+ audioSegment: 0,
18
+ title: 'Hello World',
19
+ ...overrides,
20
+ } as TemplateBlock;
21
+ }
22
+
23
+ function makeRawBlock(layers: Layer[]): Block {
24
+ return {
25
+ id: 'raw-block-1',
26
+ duration: 5,
27
+ startTime: 0,
28
+ audioSegment: 0,
29
+ layers,
30
+ };
31
+ }
32
+
33
+ const defaultContext: RenderContext = {
34
+ theme: DEFAULT_THEME,
35
+ viewport: VIEWPORT_PRESETS.landscape,
36
+ blockIndex: 0,
37
+ totalBlocks: 5,
38
+ };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Tests
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe('getLayers', () => {
45
+ it('generates layers for a known template block', () => {
46
+ const block = makeTemplateBlock({ template: 'titleBlock', title: 'Test Title' });
47
+ const layers = getLayers(block, defaultContext);
48
+
49
+ expect(Array.isArray(layers)).toBe(true);
50
+ expect(layers.length).toBeGreaterThan(0);
51
+
52
+ // titleBlock always produces at least a text layer with the title
53
+ const textLayer = layers.find((l) => l.type === 'text');
54
+ expect(textLayer).toBeDefined();
55
+ expect((textLayer as TextLayer).content.text).toContain('Test Title');
56
+ });
57
+
58
+ it('generates layers for statHighlight template', () => {
59
+ const block = makeTemplateBlock({
60
+ template: 'statHighlight',
61
+ stat: '42%',
62
+ description: 'of users',
63
+ });
64
+ const layers = getLayers(block, defaultContext);
65
+
66
+ expect(layers.length).toBeGreaterThan(0);
67
+ const texts = layers.filter((l) => l.type === 'text');
68
+ expect(texts.length).toBeGreaterThanOrEqual(1);
69
+ });
70
+
71
+ it('returns raw block layers when block has pre-computed layers', () => {
72
+ const existingLayers: Layer[] = [
73
+ {
74
+ type: 'shape',
75
+ id: 'test-shape',
76
+ content: { shape: 'rect', fill: '#ff0000' },
77
+ position: { x: 0, y: 0, width: 100, height: 100 },
78
+ } as ShapeLayer,
79
+ ];
80
+ const block = makeRawBlock(existingLayers);
81
+ const layers = getLayers(block, defaultContext);
82
+
83
+ expect(layers).toEqual(existingLayers);
84
+ });
85
+
86
+ it('returns empty array for unknown template', () => {
87
+ const block = {
88
+ template: 'totally_nonexistent',
89
+ id: 'unknown-1',
90
+ duration: 5,
91
+ audioSegment: 0,
92
+ } as unknown as TemplateBlock;
93
+ const layers = getLayers(block, defaultContext);
94
+
95
+ expect(layers).toEqual([]);
96
+ });
97
+
98
+ it('returns empty array for block with no template and no layers', () => {
99
+ const block: Block = { id: 'empty-1', duration: 5, startTime: 0, audioSegment: 0 };
100
+ const layers = getLayers(block, defaultContext);
101
+
102
+ expect(layers).toEqual([]);
103
+ });
104
+
105
+ it('uses default context values when context is omitted', () => {
106
+ const block = makeTemplateBlock({ template: 'titleBlock', title: 'Defaults' });
107
+ const layers = getLayers(block);
108
+
109
+ expect(Array.isArray(layers)).toBe(true);
110
+ expect(layers.length).toBeGreaterThan(0);
111
+ });
112
+
113
+ it('handles template that throws an error gracefully', () => {
114
+ // We spy on console.error to suppress noise and verify it was called
115
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
116
+
117
+ // comparisonBar requires numeric values; passing invalid data may throw
118
+ // but getLayers should catch and return []
119
+ const block = makeTemplateBlock({
120
+ template: 'comparisonBar',
121
+ // Missing required fields
122
+ });
123
+ const layers = getLayers(block, defaultContext);
124
+
125
+ expect(Array.isArray(layers)).toBe(true);
126
+ consoleSpy.mockRestore();
127
+ });
128
+
129
+ describe('persistent layers injection', () => {
130
+ const bottomLayer: Layer = {
131
+ type: 'shape',
132
+ id: 'bottom',
133
+ content: { shape: 'rect', fill: '#000' },
134
+ position: { x: 0, y: 0, width: 100, height: 100 },
135
+ } as ShapeLayer;
136
+
137
+ const topLayer: Layer = {
138
+ type: 'shape',
139
+ id: 'top',
140
+ content: { shape: 'rect', fill: '#fff' },
141
+ position: { x: 0, y: 0, width: 100, height: 100 },
142
+ } as ShapeLayer;
143
+
144
+ const persistentConfig: PersistentLayerConfig = {
145
+ bottomLayers: [bottomLayer],
146
+ topLayers: [topLayer],
147
+ };
148
+
149
+ it('injects persistent bottom and top layers around block layers', () => {
150
+ const block = makeTemplateBlock({ template: 'titleBlock', title: 'With Persistent' });
151
+ const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
152
+
153
+ // First layer should be the bottom persistent layer
154
+ expect(layers[0]).toEqual(bottomLayer);
155
+ // Last layer should be the top persistent layer
156
+ expect(layers[layers.length - 1]).toEqual(topLayer);
157
+ // Middle layers (from template) should exist
158
+ expect(layers.length).toBeGreaterThan(2);
159
+ });
160
+
161
+ it('respects useBottomLayer: false opt-out', () => {
162
+ const block = makeTemplateBlock({
163
+ template: 'titleBlock',
164
+ title: 'No Bottom',
165
+ useBottomLayer: false,
166
+ });
167
+ const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
168
+
169
+ // Should NOT start with the bottom persistent layer
170
+ expect(layers[0]).not.toEqual(bottomLayer);
171
+ // Should still end with the top persistent layer
172
+ expect(layers[layers.length - 1]).toEqual(topLayer);
173
+ });
174
+
175
+ it('respects useTopLayer: false opt-out', () => {
176
+ const block = makeTemplateBlock({
177
+ template: 'titleBlock',
178
+ title: 'No Top',
179
+ useTopLayer: false,
180
+ });
181
+ const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
182
+
183
+ // Should start with the bottom persistent layer
184
+ expect(layers[0]).toEqual(bottomLayer);
185
+ // Should NOT end with the top persistent layer
186
+ expect(layers[layers.length - 1]).not.toEqual(topLayer);
187
+ });
188
+
189
+ it('skips injection when no persistent layers are configured', () => {
190
+ const block = makeTemplateBlock({ template: 'titleBlock', title: 'No Persistent' });
191
+ const withoutPersistent = getLayers(block, defaultContext);
192
+ const withEmptyPersistent = getLayers(block, {
193
+ ...defaultContext,
194
+ persistentLayers: { bottomLayers: [], topLayers: [] },
195
+ });
196
+
197
+ expect(withoutPersistent).toEqual(withEmptyPersistent);
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { haversineDistance, calculateBearing } from '../spatial/Haversine';
3
+
4
+ describe('haversineDistance', () => {
5
+ it('returns 0 for same point', () => {
6
+ const p = { lat: 47.6062, lng: -122.3321 };
7
+ expect(haversineDistance(p, p)).toBe(0);
8
+ });
9
+
10
+ it('calculates Seattle to Portland (~233 km)', () => {
11
+ const seattle = { lat: 47.6062, lng: -122.3321 };
12
+ const portland = { lat: 45.5152, lng: -122.6784 };
13
+ const dist = haversineDistance(seattle, portland);
14
+ expect(dist).toBeGreaterThan(230);
15
+ expect(dist).toBeLessThan(240);
16
+ });
17
+
18
+ it('calculates London to Paris (~344 km)', () => {
19
+ const london = { lat: 51.5074, lng: -0.1278 };
20
+ const paris = { lat: 48.8566, lng: 2.3522 };
21
+ const dist = haversineDistance(london, paris);
22
+ expect(dist).toBeGreaterThan(340);
23
+ expect(dist).toBeLessThan(350);
24
+ });
25
+
26
+ it('calculates antipodal points (~20,000 km)', () => {
27
+ const p1 = { lat: 0, lng: 0 };
28
+ const p2 = { lat: 0, lng: 180 };
29
+ const dist = haversineDistance(p1, p2);
30
+ expect(dist).toBeGreaterThan(20000);
31
+ expect(dist).toBeLessThan(20100);
32
+ });
33
+ });
34
+
35
+ describe('calculateBearing', () => {
36
+ it('returns ~0 for due north', () => {
37
+ const from = { lat: 0, lng: 0 };
38
+ const to = { lat: 10, lng: 0 };
39
+ const bearing = calculateBearing(from, to);
40
+ expect(bearing).toBeCloseTo(0, 0);
41
+ });
42
+
43
+ it('returns ~90 for due east', () => {
44
+ const from = { lat: 0, lng: 0 };
45
+ const to = { lat: 0, lng: 10 };
46
+ const bearing = calculateBearing(from, to);
47
+ expect(bearing).toBeCloseTo(90, 0);
48
+ });
49
+
50
+ it('returns ~180 for due south', () => {
51
+ const from = { lat: 10, lng: 0 };
52
+ const to = { lat: 0, lng: 0 };
53
+ const bearing = calculateBearing(from, to);
54
+ expect(bearing).toBeCloseTo(180, 0);
55
+ });
56
+
57
+ it('returns ~270 for due west', () => {
58
+ const from = { lat: 0, lng: 10 };
59
+ const to = { lat: 0, lng: 0 };
60
+ const bearing = calculateBearing(from, to);
61
+ expect(bearing).toBeCloseTo(270, 0);
62
+ });
63
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * LocalForageAdapter tests
3
+ *
4
+ * Uses the localforage library (which falls back to an in-memory driver
5
+ * in jsdom/Node environments) to verify the adapter contract.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach } from 'vitest';
9
+ import { LocalForageAdapter } from '../storage/LocalForageAdapter';
10
+
11
+ describe('LocalForageAdapter', () => {
12
+ let storage: LocalForageAdapter;
13
+
14
+ beforeEach(async () => {
15
+ storage = new LocalForageAdapter({ name: 'test-db', storeName: 'test-store' });
16
+ await storage.clear();
17
+ });
18
+
19
+ it('supports enumeration', () => {
20
+ expect(storage.supportsEnumeration).toBe(true);
21
+ });
22
+
23
+ it('get returns null for missing key', async () => {
24
+ expect(await storage.get('nonexistent')).toBeNull();
25
+ });
26
+
27
+ it('set and get round-trip', async () => {
28
+ await storage.set('key', { hello: 'world' });
29
+ expect(await storage.get('key')).toEqual({ hello: 'world' });
30
+ });
31
+
32
+ it('stores primitives', async () => {
33
+ await storage.set('str', 'hello');
34
+ await storage.set('num', 42);
35
+ await storage.set('bool', true);
36
+ expect(await storage.get('str')).toBe('hello');
37
+ expect(await storage.get('num')).toBe(42);
38
+ expect(await storage.get('bool')).toBe(true);
39
+ });
40
+
41
+ it('overwrites existing values', async () => {
42
+ await storage.set('key', 'first');
43
+ await storage.set('key', 'second');
44
+ expect(await storage.get('key')).toBe('second');
45
+ });
46
+
47
+ it('remove deletes a key', async () => {
48
+ await storage.set('key', 'value');
49
+ await storage.remove('key');
50
+ expect(await storage.get('key')).toBeNull();
51
+ });
52
+
53
+ it('clear removes all keys', async () => {
54
+ await storage.set('a', 1);
55
+ await storage.set('b', 2);
56
+ await storage.clear();
57
+ expect(await storage.get('a')).toBeNull();
58
+ expect(await storage.get('b')).toBeNull();
59
+ expect(await storage.keys()).toEqual([]);
60
+ });
61
+
62
+ it('keys returns all stored keys', async () => {
63
+ await storage.set('alpha', 1);
64
+ await storage.set('beta', 2);
65
+ const keys = await storage.keys();
66
+ expect(keys.sort()).toEqual(['alpha', 'beta']);
67
+ });
68
+
69
+ it('stores binary data (ArrayBuffer)', async () => {
70
+ const buffer = new Uint8Array([1, 2, 3, 4]).buffer;
71
+ await storage.set('binary', buffer);
72
+ const result = await storage.get<ArrayBuffer>('binary');
73
+ expect(result).toBeInstanceOf(ArrayBuffer);
74
+ expect(new Uint8Array(result!)).toEqual(new Uint8Array([1, 2, 3, 4]));
75
+ });
76
+
77
+ it('stores Uint8Array', async () => {
78
+ const data = new Uint8Array([10, 20, 30]);
79
+ await storage.set('bytes', data);
80
+ const result = await storage.get<Uint8Array>('bytes');
81
+ expect(new Uint8Array(result!)).toEqual(new Uint8Array([10, 20, 30]));
82
+ });
83
+ });
84
+
85
+ describe('LocalForageAdapter with prefix', () => {
86
+ let storage: LocalForageAdapter;
87
+
88
+ beforeEach(async () => {
89
+ storage = new LocalForageAdapter({
90
+ name: 'test-db-prefix',
91
+ storeName: 'test-store',
92
+ prefix: 'slot1:',
93
+ });
94
+ await storage.clear();
95
+ });
96
+
97
+ it('get/set respects prefix', async () => {
98
+ await storage.set('doc', { title: 'My Doc' });
99
+ expect(await storage.get('doc')).toEqual({ title: 'My Doc' });
100
+ });
101
+
102
+ it('keys strips prefix', async () => {
103
+ await storage.set('alpha', 1);
104
+ await storage.set('beta', 2);
105
+ const keys = await storage.keys();
106
+ expect(keys.sort()).toEqual(['alpha', 'beta']);
107
+ });
108
+
109
+ it('clear only removes prefixed keys', async () => {
110
+ // Write directly to a separate adapter (no prefix) to verify isolation
111
+ const other = new LocalForageAdapter({
112
+ name: 'test-db-prefix',
113
+ storeName: 'test-store',
114
+ prefix: 'slot2:',
115
+ });
116
+ await other.set('other-key', 'should-survive');
117
+ await storage.set('my-key', 'will-be-cleared');
118
+
119
+ await storage.clear();
120
+
121
+ expect(await storage.get('my-key')).toBeNull();
122
+ expect(await other.get('other-key')).toBe('should-survive');
123
+
124
+ // Cleanup
125
+ await other.clear();
126
+ });
127
+
128
+ it('different prefixes are isolated', async () => {
129
+ const slot1 = new LocalForageAdapter({
130
+ name: 'test-isolation',
131
+ storeName: 'test-store',
132
+ prefix: 'slot1:',
133
+ });
134
+ const slot2 = new LocalForageAdapter({
135
+ name: 'test-isolation',
136
+ storeName: 'test-store',
137
+ prefix: 'slot2:',
138
+ });
139
+
140
+ await slot1.set('doc', 'slot1-doc');
141
+ await slot2.set('doc', 'slot2-doc');
142
+
143
+ expect(await slot1.get('doc')).toBe('slot1-doc');
144
+ expect(await slot2.get('doc')).toBe('slot2-doc');
145
+
146
+ const keys1 = await slot1.keys();
147
+ const keys2 = await slot2.keys();
148
+ expect(keys1).toEqual(['doc']);
149
+ expect(keys2).toEqual(['doc']);
150
+
151
+ // Cleanup
152
+ await slot1.clear();
153
+ await slot2.clear();
154
+ });
155
+ });