@getmicdrop/svelte-components 5.3.13 → 5.3.14

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.
@@ -1,447 +1,139 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { get } from 'svelte/store';
3
- import {
4
- auth,
5
- isPerformer,
6
- setAuthState,
7
- clearAuthState,
8
- initializeAuthState,
9
- updateTokens,
10
- setPermissions,
11
- setContext,
12
- hasPermission,
13
- getPermissions,
14
- isTokenExpiringSoon
15
- } from './auth';
16
-
17
- // Mock jwt-decode
18
- vi.mock('jwt-decode', () => ({
19
- jwtDecode: vi.fn((token) => {
20
- // Parse mock tokens in format: "token-exp-{timestamp}"
21
- if (token.startsWith('token-exp-')) {
22
- const exp = parseInt(token.replace('token-exp-', ''), 10);
23
- return { exp: exp / 1000 }; // Convert ms to seconds (JWT uses seconds)
24
- }
25
- // Default: return token that expires in 1 hour
26
- return { exp: (Date.now() + 3600000) / 1000 };
27
- })
28
- }));
29
-
30
- describe('auth store', () => {
31
- beforeEach(() => {
32
- // Reset auth state before each test
33
- clearAuthState();
34
- // Clear cookies
35
- Object.defineProperty(document, 'cookie', {
36
- writable: true,
37
- value: '',
38
- });
39
- });
40
-
41
- describe('initial state', () => {
42
- it('starts with isAuthenticated as false', () => {
43
- expect(get(auth).isAuthenticated).toBe(false);
44
- });
45
-
46
- it('starts with token as null', () => {
47
- expect(get(auth).token).toBe(null);
48
- });
49
-
50
- it('starts with userDetails as null', () => {
51
- expect(get(auth).userDetails).toBe(null);
52
- });
53
- });
54
-
55
- describe('setAuthState', () => {
56
- it('sets authentication state', () => {
57
- setAuthState({
58
- isAuthenticated: true,
59
- token: 'test-token',
60
- userDetails: { email: 'test@test.com' },
61
- });
62
-
63
- const state = get(auth);
64
- expect(state.isAuthenticated).toBe(true);
65
- expect(state.token).toBe('test-token');
66
- expect(state.userDetails).toEqual({ email: 'test@test.com' });
67
- });
68
-
69
- it('can update partial state', () => {
70
- setAuthState({ isAuthenticated: true, token: 'token1', userDetails: null });
71
- setAuthState({ isAuthenticated: true, token: 'token2', userDetails: { name: 'User' } });
72
-
73
- const state = get(auth);
74
- expect(state.token).toBe('token2');
75
- expect(state.userDetails).toEqual({ name: 'User' });
76
- });
77
- });
78
-
79
- describe('clearAuthState', () => {
80
- it('clears authentication state', () => {
81
- setAuthState({
82
- isAuthenticated: true,
83
- token: 'test-token',
84
- userDetails: { email: 'test@test.com' },
85
- });
86
-
87
- clearAuthState();
88
-
89
- const state = get(auth);
90
- expect(state.isAuthenticated).toBe(false);
91
- expect(state.token).toBe(null);
92
- expect(state.userDetails).toBe(null);
93
- });
94
- });
95
-
96
- describe('initializeAuthState', () => {
97
- it('does nothing when no performer_token cookie exists', () => {
98
- Object.defineProperty(document, 'cookie', {
99
- writable: true,
100
- value: '',
101
- });
102
-
103
- initializeAuthState();
104
-
105
- const state = get(auth);
106
- expect(state.isAuthenticated).toBe(false);
107
- });
108
-
109
- it('sets auth state from performer_token cookie', () => {
110
- Object.defineProperty(document, 'cookie', {
111
- writable: true,
112
- value: 'performer_token=my-token',
113
- });
114
-
115
- initializeAuthState();
116
-
117
- const state = get(auth);
118
- expect(state.isAuthenticated).toBe(true);
119
- expect(state.token).toBe('my-token');
120
- });
121
-
122
- it('parses userDetails cookie when present', () => {
123
- const userDetails = JSON.stringify({ email: 'test@test.com', firstName: 'Test' });
124
- Object.defineProperty(document, 'cookie', {
125
- writable: true,
126
- value: `performer_token=my-token; userDetails=${encodeURIComponent(userDetails)}`,
127
- });
128
-
129
- initializeAuthState();
130
-
131
- const state = get(auth);
132
- expect(state.userDetails).toEqual({ email: 'test@test.com', firstName: 'Test' });
133
- });
134
-
135
- it('handles missing userDetails cookie', () => {
136
- Object.defineProperty(document, 'cookie', {
137
- writable: true,
138
- value: 'performer_token=my-token',
139
- });
140
-
141
- initializeAuthState();
142
-
143
- const state = get(auth);
144
- expect(state.isAuthenticated).toBe(true);
145
- expect(state.userDetails).toBe(null);
146
- });
147
- });
148
-
149
- describe('store subscription', () => {
150
- it('can subscribe to auth changes', () => {
151
- const values = [];
152
- const unsubscribe = auth.subscribe((value) => {
153
- values.push(value.isAuthenticated);
154
- });
155
-
156
- setAuthState({ isAuthenticated: true, token: 'token', userDetails: null });
157
- clearAuthState();
158
-
159
- unsubscribe();
160
-
161
- expect(values).toEqual([false, true, false]);
162
- });
163
- });
164
-
165
- describe('updateTokens', () => {
166
- it('updates access token and computes new expiry', () => {
167
- setAuthState({
168
- isAuthenticated: true,
169
- token: 'old-token',
170
- refreshToken: 'old-refresh',
171
- userDetails: null,
172
- });
173
-
174
- updateTokens('new-access-token', 'new-refresh-token');
175
-
176
- const state = get(auth);
177
- expect(state.token).toBe('new-access-token');
178
- expect(state.refreshToken).toBe('new-refresh-token');
179
- expect(state.tokenExpiry).toBeDefined();
180
- });
181
-
182
- it('preserves existing refresh token if not provided', () => {
183
- setAuthState({
184
- isAuthenticated: true,
185
- token: 'old-token',
186
- refreshToken: 'existing-refresh',
187
- userDetails: null,
188
- });
189
-
190
- updateTokens('new-access-token');
191
-
192
- const state = get(auth);
193
- expect(state.token).toBe('new-access-token');
194
- expect(state.refreshToken).toBe('existing-refresh');
195
- });
196
-
197
- it('preserves other state properties when updating tokens', () => {
198
- setAuthState({
199
- isAuthenticated: true,
200
- token: 'old-token',
201
- refreshToken: 'refresh',
202
- userDetails: { email: 'test@test.com' },
203
- permissions: { events: { read: true } },
204
- context: { contextType: 'performer' },
205
- });
206
-
207
- updateTokens('new-token');
208
-
209
- const state = get(auth);
210
- expect(state.userDetails).toEqual({ email: 'test@test.com' });
211
- expect(state.permissions).toEqual({ events: { read: true } });
212
- expect(state.context).toEqual({ contextType: 'performer' });
213
- });
214
- });
215
-
216
- describe('isTokenExpiringSoon', () => {
217
- it('returns true when no token expiry is set', () => {
218
- clearAuthState();
219
- expect(isTokenExpiringSoon()).toBe(true);
220
- });
221
-
222
- it('returns true when token is expired', () => {
223
- const pastTime = Date.now() - 10000; // 10 seconds ago
224
- setAuthState({
225
- isAuthenticated: true,
226
- token: 'token',
227
- tokenExpiry: pastTime,
228
- });
229
-
230
- expect(isTokenExpiringSoon()).toBe(true);
231
- });
232
-
233
- it('returns true when token expires within buffer time', () => {
234
- const soonExpiry = Date.now() + (2 * 60 * 1000); // 2 minutes from now
235
- setAuthState({
236
- isAuthenticated: true,
237
- token: 'token',
238
- tokenExpiry: soonExpiry,
239
- });
240
-
241
- // Default buffer is 5 minutes, so 2 minutes is within buffer
242
- expect(isTokenExpiringSoon()).toBe(true);
243
- });
244
-
245
- it('returns false when token has plenty of time left', () => {
246
- const futureExpiry = Date.now() + (30 * 60 * 1000); // 30 minutes from now
247
- setAuthState({
248
- isAuthenticated: true,
249
- token: 'token',
250
- tokenExpiry: futureExpiry,
251
- });
252
-
253
- expect(isTokenExpiringSoon()).toBe(false);
254
- });
255
-
256
- it('accepts custom buffer time', () => {
257
- const expiry = Date.now() + (10 * 60 * 1000); // 10 minutes from now
258
- setAuthState({
259
- isAuthenticated: true,
260
- token: 'token',
261
- tokenExpiry: expiry,
262
- });
263
-
264
- // With 5 minute buffer (default), should be fine
265
- expect(isTokenExpiringSoon(5 * 60 * 1000)).toBe(false);
266
-
267
- // With 15 minute buffer, should be expiring soon
268
- expect(isTokenExpiringSoon(15 * 60 * 1000)).toBe(true);
269
- });
270
- });
271
-
272
- describe('setPermissions and getPermissions', () => {
273
- it('sets permissions on auth state', () => {
274
- const permissions = {
275
- events: { read: true, write: false },
276
- shows: { read: true, write: true },
277
- };
278
-
279
- setPermissions(permissions);
280
-
281
- const state = get(auth);
282
- expect(state.permissions).toEqual(permissions);
283
- });
284
-
285
- it('getPermissions returns current permissions', () => {
286
- const permissions = {
287
- events: { read: true },
288
- };
289
-
290
- setPermissions(permissions);
291
-
292
- expect(getPermissions()).toEqual(permissions);
293
- });
294
-
295
- it('getPermissions returns null when no permissions set', () => {
296
- clearAuthState();
297
- expect(getPermissions()).toBe(null);
298
- });
299
-
300
- it('preserves other state when setting permissions', () => {
301
- setAuthState({
302
- isAuthenticated: true,
303
- token: 'test-token',
304
- userDetails: { email: 'test@test.com' },
305
- });
306
-
307
- setPermissions({ events: { read: true } });
308
-
309
- const state = get(auth);
310
- expect(state.isAuthenticated).toBe(true);
311
- expect(state.token).toBe('test-token');
312
- expect(state.userDetails).toEqual({ email: 'test@test.com' });
313
- });
314
- });
315
-
316
- describe('setContext', () => {
317
- it('sets user context on auth state', () => {
318
- const context = {
319
- contextType: 'performer',
320
- performerId: 'perf-123',
321
- };
322
-
323
- setContext(context);
324
-
325
- const state = get(auth);
326
- expect(state.context).toEqual(context);
327
- });
328
-
329
- it('preserves other state when setting context', () => {
330
- setAuthState({
331
- isAuthenticated: true,
332
- token: 'test-token',
333
- permissions: { events: { read: true } },
334
- });
335
-
336
- setContext({ contextType: 'performer' });
337
-
338
- const state = get(auth);
339
- expect(state.isAuthenticated).toBe(true);
340
- expect(state.token).toBe('test-token');
341
- expect(state.permissions).toEqual({ events: { read: true } });
342
- });
343
-
344
- it('can update existing context', () => {
345
- setContext({ contextType: 'performer', performerId: 'old' });
346
- setContext({ contextType: 'performer', performerId: 'new' });
347
-
348
- const state = get(auth);
349
- expect(state.context.performerId).toBe('new');
350
- });
351
- });
352
-
353
- describe('hasPermission', () => {
354
- it('returns false when no permissions are set', () => {
355
- clearAuthState();
356
- expect(hasPermission('events', 'read')).toBe(false);
357
- });
358
-
359
- it('returns true when permission exists and is true', () => {
360
- setPermissions({
361
- events: { read: true, write: false },
362
- });
363
-
364
- expect(hasPermission('events', 'read')).toBe(true);
365
- });
366
-
367
- it('returns false when permission exists and is false', () => {
368
- setPermissions({
369
- events: { read: true, write: false },
370
- });
371
-
372
- expect(hasPermission('events', 'write')).toBe(false);
373
- });
374
-
375
- it('returns false when category does not exist', () => {
376
- setPermissions({
377
- events: { read: true },
378
- });
379
-
380
- expect(hasPermission('shows', 'read')).toBe(false);
381
- });
382
-
383
- it('returns false when permission does not exist in category', () => {
384
- setPermissions({
385
- events: { read: true },
386
- });
387
-
388
- expect(hasPermission('events', 'delete')).toBe(false);
389
- });
390
-
391
- it('works with nested permission categories', () => {
392
- setPermissions({
393
- events: { read: true, write: true },
394
- shows: { read: true, write: false, delete: false },
395
- admin: { read: false },
396
- });
397
-
398
- expect(hasPermission('events', 'read')).toBe(true);
399
- expect(hasPermission('events', 'write')).toBe(true);
400
- expect(hasPermission('shows', 'read')).toBe(true);
401
- expect(hasPermission('shows', 'write')).toBe(false);
402
- expect(hasPermission('admin', 'read')).toBe(false);
403
- });
404
- });
405
-
406
- describe('isPerformer derived store', () => {
407
- it('returns true when context type is performer', () => {
408
- setContext({ contextType: 'performer' });
409
-
410
- expect(get(isPerformer)).toBe(true);
411
- });
412
-
413
- it('returns true when user is authenticated (fallback)', () => {
414
- setAuthState({
415
- isAuthenticated: true,
416
- token: 'token',
417
- userDetails: null,
418
- });
419
-
420
- expect(get(isPerformer)).toBe(true);
421
- });
422
-
423
- it('returns false when not authenticated and no performer context', () => {
424
- clearAuthState();
425
-
426
- expect(get(isPerformer)).toBe(false);
427
- });
428
-
429
- it('updates reactively when context changes', () => {
430
- const values = [];
431
- const unsubscribe = isPerformer.subscribe((value) => {
432
- values.push(value);
433
- });
434
-
435
- clearAuthState();
436
- setContext({ contextType: 'performer' });
437
- setContext({ contextType: 'staff' });
438
- setAuthState({ isAuthenticated: true, token: 'token' });
439
-
440
- unsubscribe();
441
-
442
- // false (initial), true (performer context), false (staff context, not authenticated), true (authenticated)
443
- expect(values).toContain(false);
444
- expect(values).toContain(true);
445
- });
446
- });
447
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { get } from 'svelte/store';
3
+ import { auth, setAuthState, clearAuthState, initializeAuthState } from './auth';
4
+
5
+ describe('auth store', () => {
6
+ beforeEach(() => {
7
+ // Reset auth state before each test
8
+ clearAuthState();
9
+ // Clear cookies
10
+ Object.defineProperty(document, 'cookie', {
11
+ writable: true,
12
+ value: '',
13
+ });
14
+ });
15
+
16
+ describe('initial state', () => {
17
+ it('starts with isAuthenticated as false', () => {
18
+ expect(get(auth).isAuthenticated).toBe(false);
19
+ });
20
+
21
+ it('starts with token as null', () => {
22
+ expect(get(auth).token).toBe(null);
23
+ });
24
+
25
+ it('starts with userDetails as null', () => {
26
+ expect(get(auth).userDetails).toBe(null);
27
+ });
28
+ });
29
+
30
+ describe('setAuthState', () => {
31
+ it('sets authentication state', () => {
32
+ setAuthState({
33
+ isAuthenticated: true,
34
+ token: 'test-token',
35
+ userDetails: { email: 'test@test.com' },
36
+ });
37
+
38
+ const state = get(auth);
39
+ expect(state.isAuthenticated).toBe(true);
40
+ expect(state.token).toBe('test-token');
41
+ expect(state.userDetails).toEqual({ email: 'test@test.com' });
42
+ });
43
+
44
+ it('can update partial state', () => {
45
+ setAuthState({ isAuthenticated: true, token: 'token1', userDetails: null });
46
+ setAuthState({ isAuthenticated: true, token: 'token2', userDetails: { name: 'User' } });
47
+
48
+ const state = get(auth);
49
+ expect(state.token).toBe('token2');
50
+ expect(state.userDetails).toEqual({ name: 'User' });
51
+ });
52
+ });
53
+
54
+ describe('clearAuthState', () => {
55
+ it('clears authentication state', () => {
56
+ setAuthState({
57
+ isAuthenticated: true,
58
+ token: 'test-token',
59
+ userDetails: { email: 'test@test.com' },
60
+ });
61
+
62
+ clearAuthState();
63
+
64
+ const state = get(auth);
65
+ expect(state.isAuthenticated).toBe(false);
66
+ expect(state.token).toBe(null);
67
+ expect(state.userDetails).toBe(null);
68
+ });
69
+ });
70
+
71
+ describe('initializeAuthState', () => {
72
+ it('does nothing when no performer_token cookie exists', () => {
73
+ Object.defineProperty(document, 'cookie', {
74
+ writable: true,
75
+ value: '',
76
+ });
77
+
78
+ initializeAuthState();
79
+
80
+ const state = get(auth);
81
+ expect(state.isAuthenticated).toBe(false);
82
+ });
83
+
84
+ it('sets auth state from performer_token cookie', () => {
85
+ Object.defineProperty(document, 'cookie', {
86
+ writable: true,
87
+ value: 'performer_token=my-token',
88
+ });
89
+
90
+ initializeAuthState();
91
+
92
+ const state = get(auth);
93
+ expect(state.isAuthenticated).toBe(true);
94
+ expect(state.token).toBe('my-token');
95
+ });
96
+
97
+ it('parses userDetails cookie when present', () => {
98
+ const userDetails = JSON.stringify({ email: 'test@test.com', firstName: 'Test' });
99
+ Object.defineProperty(document, 'cookie', {
100
+ writable: true,
101
+ value: `performer_token=my-token; userDetails=${encodeURIComponent(userDetails)}`,
102
+ });
103
+
104
+ initializeAuthState();
105
+
106
+ const state = get(auth);
107
+ expect(state.userDetails).toEqual({ email: 'test@test.com', firstName: 'Test' });
108
+ });
109
+
110
+ it('handles missing userDetails cookie', () => {
111
+ Object.defineProperty(document, 'cookie', {
112
+ writable: true,
113
+ value: 'performer_token=my-token',
114
+ });
115
+
116
+ initializeAuthState();
117
+
118
+ const state = get(auth);
119
+ expect(state.isAuthenticated).toBe(true);
120
+ expect(state.userDetails).toBe(null);
121
+ });
122
+ });
123
+
124
+ describe('store subscription', () => {
125
+ it('can subscribe to auth changes', () => {
126
+ const values = [];
127
+ const unsubscribe = auth.subscribe((value) => {
128
+ values.push(value.isAuthenticated);
129
+ });
130
+
131
+ setAuthState({ isAuthenticated: true, token: 'token', userDetails: null });
132
+ clearAuthState();
133
+
134
+ unsubscribe();
135
+
136
+ expect(values).toEqual([false, true, false]);
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @typedef {Object} FormStoreOptions
3
+ * @property {Object} initialData - Initial form data
4
+ * @property {string} [endpoint] - API endpoint for saving
5
+ * @property {string} [successMessage] - Message to show on success
6
+ * @property {string} [errorMessage] - Message to show on error
7
+ * @property {Function} [validate] - Validation function returning { isValid, errors }
8
+ * @property {Function} [transformData] - Transform data before saving
9
+ * @property {Function} [onSuccess] - Callback after successful save
10
+ * @property {Function} [onError] - Callback after error
11
+ * @property {boolean} [showToasts=true] - Show toast notifications
12
+ */
13
+ /**
14
+ * Creates a comprehensive form store with state management, dirty tracking,
15
+ * validation, and save handling.
16
+ *
17
+ * @param {FormStoreOptions} options - Configuration options
18
+ * @returns {Object} Form store with all state and handlers
19
+ *
20
+ * @example
21
+ * const form = createFormStore({
22
+ * initialData: { name: '', email: '' },
23
+ * endpoint: '/api/profile',
24
+ * validate: (data) => {
25
+ * const errors = {};
26
+ * if (!data.name) errors.name = 'Name is required';
27
+ * return { isValid: Object.keys(errors).length === 0, errors };
28
+ * }
29
+ * });
30
+ *
31
+ * // In Svelte component:
32
+ * <input bind:value={$form.data.name} on:input={form.checkDirty} />
33
+ * <button on:click={form.submit} disabled={$form.isLoading}>Save</button>
34
+ */
35
+ export function createFormStore(options?: FormStoreOptions): Object;
36
+ export type FormStoreOptions = {
37
+ /**
38
+ * - Initial form data
39
+ */
40
+ initialData: Object;
41
+ /**
42
+ * - API endpoint for saving
43
+ */
44
+ endpoint?: string | undefined;
45
+ /**
46
+ * - Message to show on success
47
+ */
48
+ successMessage?: string | undefined;
49
+ /**
50
+ * - Message to show on error
51
+ */
52
+ errorMessage?: string | undefined;
53
+ /**
54
+ * - Validation function returning { isValid, errors }
55
+ */
56
+ validate?: Function | undefined;
57
+ /**
58
+ * - Transform data before saving
59
+ */
60
+ transformData?: Function | undefined;
61
+ /**
62
+ * - Callback after successful save
63
+ */
64
+ onSuccess?: Function | undefined;
65
+ /**
66
+ * - Callback after error
67
+ */
68
+ onError?: Function | undefined;
69
+ /**
70
+ * - Show toast notifications
71
+ */
72
+ showToasts?: boolean | undefined;
73
+ };
74
+ //# sourceMappingURL=createFormStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFormStore.d.ts","sourceRoot":"","sources":["../../src/lib/stores/createFormStore.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,0CAlBW,gBAAgB,GACd,MAAM,CA4WlB;;;;;iBA5Xa,MAAM"}