@open-webapp/calendar 0.0.0-stage → 0.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.
@@ -0,0 +1,503 @@
1
+ .calendar-pane {
2
+ --calendar-footer-height: 64px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ min-height: 0;
6
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
7
+ }
8
+
9
+ .calendar-pane-scroll {
10
+ flex: 1;
11
+ min-height: 0;
12
+ overflow-y: auto;
13
+ }
14
+
15
+ .calendar-pane:has(.calendar-pane-footer) .calendar-pane-scroll {
16
+ padding-bottom: var(--calendar-footer-height);
17
+ }
18
+
19
+ .calendar-pane-footer {
20
+ position: sticky;
21
+ bottom: 0;
22
+ z-index: 3;
23
+ box-sizing: border-box;
24
+ min-height: var(--calendar-footer-height);
25
+ padding: 12px 16px;
26
+ border-top: 1px solid #eee;
27
+ background: #ffffff;
28
+ }
29
+
30
+ .calendar-pane-prompt {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: flex-start;
34
+ gap: 12px;
35
+ padding: 24px 16px;
36
+ }
37
+
38
+ .calendar-pane-prompt p {
39
+ margin: 0;
40
+ color: #555;
41
+ font-size: 14px;
42
+ }
43
+
44
+ .calendar-connect-button {
45
+ padding: 10px 16px;
46
+ font-size: 14px;
47
+ font-weight: 500;
48
+ background: #ffffff;
49
+ color: #00a9ce;
50
+ border: 1px solid #00a9ce;
51
+ border-radius: 6px;
52
+ cursor: pointer;
53
+ }
54
+
55
+ .calendar-connect-button:hover {
56
+ background: #f0f8fb;
57
+ }
58
+
59
+ .calendar-rsvp-chips {
60
+ display: flex;
61
+ gap: 8px;
62
+ padding: 12px 16px;
63
+ flex-wrap: wrap;
64
+ position: sticky;
65
+ top: 0;
66
+ background: #ffffff;
67
+ z-index: 2;
68
+ }
69
+
70
+ .calendar-rsvp-chip {
71
+ width: 32px;
72
+ height: 32px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ padding: 0;
77
+ background: transparent;
78
+ color: #333;
79
+ border: 1px solid #ccc;
80
+ border-radius: 999px;
81
+ cursor: pointer;
82
+ }
83
+
84
+ .calendar-scroll-today-btn {
85
+ margin-right: 4px;
86
+ padding-right: 4px;
87
+ border-right: 1px solid #ccc;
88
+ }
89
+
90
+ .calendar-rsvp-chip:hover {
91
+ border-color: #00a9ce;
92
+ }
93
+
94
+ .calendar-rsvp-chip.selected {
95
+ background: #081a59;
96
+ color: #ffffff;
97
+ border-color: #081a59;
98
+ }
99
+
100
+ .calendar-empty-state {
101
+ padding: 24px 16px;
102
+ color: #999;
103
+ font-size: 14px;
104
+ }
105
+
106
+ .calendar-day-groups {
107
+ display: flex;
108
+ flex-direction: column;
109
+ }
110
+
111
+ .calendar-day-group {
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+
116
+ .calendar-day-header {
117
+ position: sticky;
118
+ top: var(--chip-row-height, 44px);
119
+ background: #f5f5f5;
120
+ color: #333;
121
+ font-size: 13px;
122
+ font-weight: 600;
123
+ padding: 8px 16px;
124
+ z-index: 1;
125
+ }
126
+
127
+ .calendar-event-allday {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 8px;
131
+ padding: 10px 16px;
132
+ border-left: 3px solid var(--rsvp-bar, transparent);
133
+ border-bottom: 1px solid #eee;
134
+ }
135
+
136
+ .calendar-event-row {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 8px;
140
+ padding: 10px 16px;
141
+ border-left: 3px solid var(--rsvp-bar, transparent);
142
+ border-bottom: 1px solid #eee;
143
+ }
144
+
145
+ .calendar-event-allday {
146
+ background: #fdf6e3;
147
+ font-style: italic;
148
+ }
149
+
150
+ .calendar-event-row--rsvp-accepted {
151
+ --rsvp-bar: #2e7d32;
152
+ }
153
+
154
+ .calendar-event-row--rsvp-tentative {
155
+ --rsvp-bar: #d97706;
156
+ }
157
+
158
+ .calendar-event-row--rsvp-needsAction {
159
+ --rsvp-bar: #2563eb;
160
+ }
161
+
162
+ .calendar-event-row--rsvp-declined {
163
+ --rsvp-bar: #dc2626;
164
+ }
165
+
166
+ .calendar-event-header {
167
+ display: flex;
168
+ justify-content: space-between;
169
+ align-items: center;
170
+ gap: 8px;
171
+ }
172
+
173
+ .calendar-event-title-link {
174
+ align-self: flex-start;
175
+ text-decoration: none;
176
+ color: inherit;
177
+ cursor: pointer;
178
+ }
179
+
180
+ .calendar-event-title-link:hover .calendar-event-title {
181
+ text-decoration: underline;
182
+ }
183
+
184
+ .calendar-event-title-wrap {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 6px;
188
+ min-width: 0;
189
+ align-self: flex-start;
190
+ }
191
+
192
+ .calendar-event-insert-link {
193
+ display: inline-flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ flex-shrink: 0;
197
+ width: 24px;
198
+ height: 24px;
199
+ padding: 0;
200
+ border: none;
201
+ border-radius: 4px;
202
+ background: transparent;
203
+ color: #888;
204
+ cursor: pointer;
205
+ }
206
+
207
+ .calendar-event-insert-link:hover {
208
+ color: #222;
209
+ background: rgba(0, 0, 0, 0.06);
210
+ }
211
+
212
+ .calendar-event-insert-link:focus-visible {
213
+ outline: 2px solid #1a73e8;
214
+ outline-offset: 1px;
215
+ color: #222;
216
+ }
217
+
218
+ .calendar-event-time {
219
+ font-size: 12px;
220
+ color: #666;
221
+ min-width: 110px;
222
+ flex-shrink: 0;
223
+ }
224
+
225
+ .calendar-event-title {
226
+ font-size: 14px;
227
+ color: #222;
228
+ }
229
+
230
+ .calendar-event-organizer {
231
+ font-size: 12px;
232
+ color: #888;
233
+ }
234
+
235
+ .calendar-event-calendar {
236
+ display: inline-flex;
237
+ align-items: center;
238
+ gap: 5px;
239
+ font-size: 12px;
240
+ color: #888;
241
+ }
242
+
243
+ .calendar-event-calendar-dot {
244
+ width: 8px;
245
+ height: 8px;
246
+ flex: 0 0 auto;
247
+ border-radius: 50%;
248
+ }
249
+
250
+ .calendar-event-join {
251
+ font-size: 12px;
252
+ font-weight: 500;
253
+ color: #00a9ce;
254
+ cursor: pointer;
255
+ text-decoration: none;
256
+ padding: 4px 8px;
257
+ border: 1px solid #00a9ce;
258
+ border-radius: 4px;
259
+ }
260
+
261
+ .calendar-event-join:hover {
262
+ background: #f0f8fb;
263
+ }
264
+
265
+ .calendar-event-row--current,
266
+ .calendar-event-row--selected {
267
+ background: #f0f8fb;
268
+ }
269
+
270
+ @media (prefers-color-scheme: dark) {
271
+ .calendar-pane-prompt p {
272
+ color: #ccc;
273
+ }
274
+
275
+ .calendar-connect-button {
276
+ background: #1a1a1a;
277
+ color: #22d0ef;
278
+ border-color: #22d0ef;
279
+ }
280
+
281
+ .calendar-connect-button:hover {
282
+ background: #2a2a2a;
283
+ }
284
+
285
+ .calendar-rsvp-chips {
286
+ background: #1a1a1a;
287
+ }
288
+
289
+ .calendar-pane-footer {
290
+ border-top-color: #333;
291
+ background: #1a1a1a;
292
+ }
293
+
294
+ .calendar-rsvp-chip {
295
+ color: #eee;
296
+ border-color: #444;
297
+ }
298
+
299
+ .calendar-scroll-today-btn {
300
+ border-right-color: #444;
301
+ }
302
+
303
+ .calendar-rsvp-chip.selected {
304
+ background: #22d0ef;
305
+ color: #081a59;
306
+ border-color: #22d0ef;
307
+ }
308
+
309
+ .calendar-day-header {
310
+ background: #222;
311
+ color: #eee;
312
+ }
313
+
314
+ .calendar-event-allday {
315
+ border-bottom-color: #333;
316
+ }
317
+
318
+ .calendar-event-row {
319
+ border-bottom-color: #333;
320
+ }
321
+
322
+ .calendar-event-allday {
323
+ background: #2a2510;
324
+ }
325
+
326
+ .calendar-event-row--rsvp-accepted {
327
+ --rsvp-bar: #4ade80;
328
+ }
329
+
330
+ .calendar-event-row--rsvp-tentative {
331
+ --rsvp-bar: #fbbf24;
332
+ }
333
+
334
+ .calendar-event-row--rsvp-needsAction {
335
+ --rsvp-bar: #60a5fa;
336
+ }
337
+
338
+ .calendar-event-row--rsvp-declined {
339
+ --rsvp-bar: #f87171;
340
+ }
341
+
342
+ .calendar-event-title {
343
+ color: #eee;
344
+ }
345
+
346
+ .calendar-event-join {
347
+ color: #22d0ef;
348
+ border-color: #22d0ef;
349
+ }
350
+
351
+ .calendar-event-join:hover {
352
+ background: #2a2a2a;
353
+ }
354
+
355
+ .calendar-event-row--current,
356
+ .calendar-event-row--selected {
357
+ background: #10262b;
358
+ }
359
+
360
+ .calendar-event-insert-link {
361
+ color: #999;
362
+ }
363
+
364
+ .calendar-event-insert-link:hover {
365
+ color: #eee;
366
+ background: rgba(255, 255, 255, 0.1);
367
+ }
368
+
369
+ .calendar-event-insert-link:focus-visible {
370
+ outline-color: #22d0ef;
371
+ color: #eee;
372
+ }
373
+ }
374
+
375
+ .calendar-selector {
376
+ position: relative;
377
+ }
378
+
379
+ .calendar-selector-dropdown {
380
+ position: absolute;
381
+ z-index: 3;
382
+ box-sizing: border-box;
383
+ min-width: 220px;
384
+ max-width: min(320px, calc(100vw - 32px));
385
+ padding: 8px;
386
+ border: 1px solid #ccc;
387
+ border-radius: 6px;
388
+ background: #fff;
389
+ color: #1a1a1a;
390
+ box-shadow: 0 4px 14px rgb(0 0 0 / 15%);
391
+ }
392
+
393
+ .calendar-selector-dropdown--upward {
394
+ right: 0;
395
+ bottom: calc(100% + 6px);
396
+ }
397
+
398
+ .calendar-selector-dropdown input[type='search'] {
399
+ box-sizing: border-box;
400
+ width: 100%;
401
+ }
402
+
403
+ .calendar-selector-checkbox {
404
+ flex: none;
405
+ margin: 0;
406
+ pointer-events: none;
407
+ accent-color: #22a6c0;
408
+ }
409
+
410
+ .calendar-selector-actions {
411
+ display: flex;
412
+ gap: 8px;
413
+ margin: 8px 0;
414
+ }
415
+
416
+ .calendar-selector-dropdown [role='listbox'] {
417
+ display: grid;
418
+ gap: 2px;
419
+ max-height: 240px;
420
+ overflow-y: auto;
421
+ }
422
+
423
+ .calendar-selector-dropdown [role='option'] {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 8px;
427
+ width: 100%;
428
+ padding: 6px;
429
+ border: 0;
430
+ background: transparent;
431
+ text-align: left;
432
+ }
433
+
434
+ .calendar-selector-dropdown [role='option'][data-active='true'] {
435
+ background: #e8f5f8;
436
+ }
437
+
438
+ .calendar-selector-dot {
439
+ flex: none;
440
+ width: 10px;
441
+ height: 10px;
442
+ border-radius: 50%;
443
+ }
444
+
445
+ .calendar-selector-name {
446
+ min-width: 0;
447
+ overflow: hidden;
448
+ text-overflow: ellipsis;
449
+ white-space: nowrap;
450
+ }
451
+
452
+ @media (prefers-color-scheme: dark) {
453
+ .calendar-selector-dropdown {
454
+ border-color: #333;
455
+ background: #1a1a1a;
456
+ color: #eee;
457
+ box-shadow: 0 4px 14px rgb(0 0 0 / 50%);
458
+ }
459
+
460
+ .calendar-selector-dropdown input[type='search'] {
461
+ border: 1px solid #444;
462
+ background: #2a2a2a;
463
+ color: #eee;
464
+ }
465
+
466
+ .calendar-selector-dropdown button {
467
+ border: 1px solid #444;
468
+ background: #2a2a2a;
469
+ color: #eee;
470
+ }
471
+
472
+ .calendar-selector-dropdown [role='option'] {
473
+ border: 0;
474
+ background: transparent;
475
+ color: #eee;
476
+ }
477
+
478
+ .calendar-selector-dropdown [role='option']:hover {
479
+ background: #2a2a2a;
480
+ }
481
+
482
+ .calendar-selector-dropdown [role='option'][data-active='true'] {
483
+ background: #10262b;
484
+ }
485
+ }
486
+
487
+ .calendar-event-source-dot {
488
+ display: inline-block;
489
+ width: 8px;
490
+ height: 8px;
491
+ border-radius: 50%;
492
+ margin-right: 5px;
493
+ flex: none;
494
+ }
495
+
496
+ .calendar-selector-heading {
497
+ margin: 0;
498
+ padding: 6px 8px 2px;
499
+ font-size: 0.75rem;
500
+ font-weight: 600;
501
+ opacity: 0.7;
502
+ overflow-wrap: anywhere;
503
+ }
@@ -0,0 +1,21 @@
1
+ import type { CryptoAdapter, StorageAdapter, StorageRow } from '../core/storage.js';
2
+ export declare function createMemoryStorage(): StorageAdapter & {
3
+ rows: Map<string, StorageRow>;
4
+ };
5
+ export declare const identityCrypto: CryptoAdapter;
6
+ export type FakeReply = {
7
+ status: number;
8
+ body?: unknown;
9
+ } | 'network-error';
10
+ /** Stubs global fetch with a router; returns the request log. Restore via the returned `restore`. */
11
+ export declare function installFakeFetch(router: (req: {
12
+ url: string;
13
+ token: string;
14
+ }, u: URL) => FakeReply): {
15
+ log: {
16
+ url: string;
17
+ token: string;
18
+ }[];
19
+ restore: () => void;
20
+ };
21
+ export * from './storageContract.js';
@@ -0,0 +1,42 @@
1
+ export function createMemoryStorage() {
2
+ const rows = new Map();
3
+ return {
4
+ rows,
5
+ async get(id) {
6
+ return rows.get(id) ?? null;
7
+ },
8
+ async put(id, row) {
9
+ rows.set(id, row);
10
+ },
11
+ async delete(id) {
12
+ rows.delete(id);
13
+ },
14
+ async clear(id) {
15
+ rows.delete(id);
16
+ },
17
+ };
18
+ }
19
+ export const identityCrypto = {
20
+ encrypt: async (s) => s,
21
+ decrypt: async (s) => s,
22
+ };
23
+ /** Stubs global fetch with a router; returns the request log. Restore via the returned `restore`. */
24
+ export function installFakeFetch(router) {
25
+ const original = globalThis.fetch;
26
+ const log = [];
27
+ globalThis.fetch = (async (input, init) => {
28
+ const url = String(input);
29
+ const auth = init?.headers?.Authorization ?? init?.headers?.authorization ?? '';
30
+ const req = { url, token: auth.replace(/^Bearer /, '') };
31
+ log.push(req);
32
+ const reply = router(req, new URL(url));
33
+ if (reply === 'network-error')
34
+ throw new TypeError('Failed to fetch');
35
+ return new Response(JSON.stringify(reply.body ?? {}), {
36
+ status: reply.status,
37
+ headers: { 'content-type': 'application/json' },
38
+ });
39
+ });
40
+ return { log, restore: () => void (globalThis.fetch = original) };
41
+ }
42
+ export * from './storageContract.js';
@@ -0,0 +1,27 @@
1
+ import type { CryptoAdapter, StorageAdapter, StorageRow } from '../core/storage.js';
2
+ import type { CalendarEvent } from '../core/types.js';
3
+ /**
4
+ * What a host supplies to run the contract.
5
+ *
6
+ * - `storage` / `crypto`: the adapters under test (fresh, empty per factory call).
7
+ * - `seedRaw` (optional): writes a row EXACTLY as given, bypassing any normalisation the
8
+ * adapter's `put` performs. Hosts whose adapter stores a different on-disk shape (e.g. an
9
+ * IndexedDB store) use this to plant legacy-format and corrupt rows. Defaults to `storage.put`.
10
+ * - `wrongKeyCrypto` (optional): a crypto adapter whose decrypt fails (or yields garbage) for
11
+ * data written by `crypto`. Omit when `crypto` is an identity transform (wrong-key case skipped).
12
+ * - `encryptedFixture` (optional): a LITERAL ciphertext string (copied from a real legacy
13
+ * capture, never generated here) that `crypto.decrypt` turns into JSON of `[LEGACY_EVENT]`.
14
+ * Defaults to the plain JSON string, which is correct for identity crypto.
15
+ * - `corruptRow` (optional): a row the host considers corrupt on disk. Defaults to garbage strings.
16
+ */
17
+ export interface StorageContractHarness {
18
+ storage: StorageAdapter;
19
+ crypto: CryptoAdapter;
20
+ seedRaw?: (sourceId: string, row: StorageRow) => Promise<void>;
21
+ wrongKeyCrypto?: CryptoAdapter;
22
+ encryptedFixture?: string;
23
+ corruptRow?: StorageRow;
24
+ }
25
+ /** Literal fixture event (values copied from the notesdiary calendarCache test row()). */
26
+ export declare const LEGACY_EVENT: CalendarEvent;
27
+ export declare function runStorageAdapterContract(factory: () => StorageContractHarness | Promise<StorageContractHarness>): void;
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createCalendarController } from '../core/controller.js';
3
+ import { installFakeFetch } from './index.js';
4
+ /** Literal fixture event (values copied from the notesdiary calendarCache test row()). */
5
+ export const LEGACY_EVENT = {
6
+ id: 'instanceId',
7
+ sourceId: 'src',
8
+ calendarId: 'cal',
9
+ summary: 'Original',
10
+ start: { dateTime: '2026-10-09T15:00:00.000Z' },
11
+ end: { dateTime: '2026-10-09T16:00:00.000Z' },
12
+ isAllDay: false,
13
+ selfResponseStatus: 'accepted',
14
+ joinUrl: null,
15
+ organizer: { self: false },
16
+ htmlLink: '',
17
+ };
18
+ const LEGACY_JSON = '[{"id":"instanceId","sourceId":"src","calendarId":"cal","summary":"Original","start":{"dateTime":"2026-10-09T15:00:00.000Z"},"end":{"dateTime":"2026-10-09T16:00:00.000Z"},"isAllDay":false,"selfResponseStatus":"accepted","joinUrl":null,"organizer":{"self":false},"htmlLink":""}]';
19
+ export function runStorageAdapterContract(factory) {
20
+ describe('StorageAdapter contract', () => {
21
+ const ev = (id) => ({ ...LEGACY_EVENT, id });
22
+ const row = (ids, tokens, selectedIds) => ({
23
+ events: ids.map(ev),
24
+ syncState: { tokens },
25
+ selectedIds,
26
+ });
27
+ /** Loads a source through the real controller (network down) and returns what it read back. */
28
+ async function readBack(h, crypto = h.crypto) {
29
+ const f = installFakeFetch(() => 'network-error');
30
+ try {
31
+ const c = createCalendarController({
32
+ sources: [{ id: 'src', email: 'a@b.c' }],
33
+ getToken: async () => 't',
34
+ storage: h.storage,
35
+ crypto,
36
+ });
37
+ await c.refresh();
38
+ return { events: c.getEvents(), selected: c.getSelectedIds('src') };
39
+ }
40
+ finally {
41
+ f.restore();
42
+ }
43
+ }
44
+ const seed = (h, id, r) => (h.seedRaw ?? ((i, x) => h.storage.put(i, x)))(id, r);
45
+ it('put/get round-trips events, syncState and selectedIds', async () => {
46
+ const { storage } = await factory();
47
+ const r = row(['a', 'b'], { cal: 'tok' }, ['cal']);
48
+ await storage.put('s1', r);
49
+ expect(await storage.get('s1')).toEqual(r);
50
+ });
51
+ it('isolates sources from each other', async () => {
52
+ const { storage } = await factory();
53
+ await storage.put('s1', row(['a'], { c: '1' }, ['c']));
54
+ await storage.put('s2', row(['b'], { c: '2' }, []));
55
+ await storage.delete('s1');
56
+ expect(await storage.get('s1')).toBeNull();
57
+ expect(await storage.get('s2')).toEqual(row(['b'], { c: '2' }, []));
58
+ });
59
+ it('delete and clear remove only the given source', async () => {
60
+ const { storage } = await factory();
61
+ await storage.put('s1', row(['a'], {}, []));
62
+ await storage.put('s2', row(['b'], {}, []));
63
+ await storage.clear('s1');
64
+ expect(await storage.get('s1')).toBeNull();
65
+ expect(await storage.get('s2')).not.toBeNull();
66
+ await storage.delete('s2');
67
+ expect(await storage.get('s2')).toBeNull();
68
+ });
69
+ it('get on a missing source resolves null', async () => {
70
+ const { storage } = await factory();
71
+ expect(await storage.get('nope')).toBeNull();
72
+ });
73
+ it('missing source reads back as empty', async () => {
74
+ const h = await factory();
75
+ expect(await readBack(h)).toEqual({ events: [], selected: ['primary'] });
76
+ });
77
+ it('reads a legacy plain (unencrypted) row', async () => {
78
+ const h = await factory();
79
+ await seed(h, 'src', { events: [LEGACY_EVENT], syncState: { tokens: { cal: 't' } }, selectedIds: ['cal'] });
80
+ expect(await readBack(h)).toEqual({ events: [LEGACY_EVENT], selected: ['cal'] });
81
+ });
82
+ it('reads a legacy encrypted row from a literal fixture', async () => {
83
+ const h = await factory();
84
+ await seed(h, 'src', {
85
+ events: h.encryptedFixture ?? LEGACY_JSON,
86
+ syncState: h.encryptedFixture ? null : '{"tokens":{"cal":"t"}}',
87
+ selectedIds: ['cal'],
88
+ });
89
+ const out = await readBack(h);
90
+ expect(out.events).toEqual([LEGACY_EVENT]);
91
+ expect(out.selected).toEqual(['cal']);
92
+ });
93
+ it('reads a 0.1.0 row that has no lastFullSyncAt/lastSyncAt fields', async () => {
94
+ const h = await factory();
95
+ await seed(h, 'src', { events: [LEGACY_EVENT], syncState: { tokens: { cal: 't' } }, selectedIds: ['cal'] });
96
+ const out = await readBack(h);
97
+ expect(out.events).toEqual([LEGACY_EVENT]);
98
+ expect(out.selected).toEqual(['cal']);
99
+ });
100
+ it('a corrupt row reads back empty without throwing', async () => {
101
+ const h = await factory();
102
+ await seed(h, 'src', h.corruptRow ?? { events: '%%not-json%%', syncState: '{{', selectedIds: 'bad' });
103
+ await expect(readBack(h)).resolves.toEqual({ events: [], selected: ['primary'] });
104
+ });
105
+ it('a wrong key reads back empty without throwing', async () => {
106
+ const h = await factory();
107
+ if (!h.wrongKeyCrypto)
108
+ return;
109
+ await h.storage.put('src', {
110
+ events: await h.crypto.encrypt(JSON.stringify([LEGACY_EVENT])),
111
+ syncState: await h.crypto.encrypt(JSON.stringify({ tokens: {} })),
112
+ selectedIds: [],
113
+ });
114
+ await expect(readBack(h, h.wrongKeyCrypto)).resolves.toEqual({ events: [], selected: ['primary'] });
115
+ });
116
+ });
117
+ }