snice 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/snice.js +11 -5
- package/bin/templates/CLAUDE.md +5 -5
- package/bin/templates/pwa/README.md +31 -16
- package/bin/templates/pwa/index.html +0 -1
- package/bin/templates/pwa/package.json +9 -2
- package/bin/templates/pwa/src/fetcher.ts +15 -0
- package/bin/templates/pwa/src/guards/auth.ts +6 -4
- package/bin/templates/pwa/src/middleware/auth.ts +7 -6
- package/bin/templates/pwa/src/middleware/error.ts +16 -5
- package/bin/templates/pwa/src/middleware/retry.ts +7 -3
- package/bin/templates/pwa/src/pages/dashboard.ts +4 -3
- package/bin/templates/pwa/src/pages/login.ts +7 -7
- package/bin/templates/pwa/src/pages/notifications.ts +2 -2
- package/bin/templates/pwa/src/pages/profile.ts +9 -8
- package/bin/templates/pwa/src/router.ts +8 -4
- package/bin/templates/pwa/src/types/auth.ts +1 -1
- package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
- package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
- package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
- package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
- package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
- package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
- package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
- package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
- package/dist/components/file-gallery/snice-file-gallery.d.ts +87 -0
- package/dist/components/file-gallery/snice-file-gallery.js +892 -0
- package/dist/components/file-gallery/snice-file-gallery.js.map +1 -0
- package/dist/components/file-gallery/snice-file-gallery.types.d.ts +72 -0
- package/dist/components/qr-reader/qr-decoder.d.ts +20 -0
- package/dist/components/qr-reader/qr-decoder.js +49 -0
- package/dist/components/qr-reader/qr-decoder.js.map +1 -0
- package/dist/components/qr-reader/qr-worker.d.ts +6 -0
- package/dist/components/qr-reader/qr-worker.js +64 -0
- package/dist/components/qr-reader/qr-worker.js.map +1 -0
- package/dist/components/qr-reader/snice-qr-reader.d.ts +39 -0
- package/dist/components/qr-reader/snice-qr-reader.js +436 -0
- package/dist/components/qr-reader/snice-qr-reader.js.map +1 -0
- package/dist/components/qr-reader/snice-qr-reader.types.d.ts +17 -0
- package/dist/components/qr-reader/zxing-reader.mjs +1582 -0
- package/dist/components/qr-reader/zxing-share.mjs +305 -0
- package/dist/components/qr-reader/zxing_reader.wasm +0 -0
- package/dist/components/zxing-reader-B3Rfebg9.js +1771 -0
- package/dist/components/zxing-reader-B3Rfebg9.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/docs/ai/README.md +1 -1
- package/docs/ai/components/file-gallery.md +206 -0
- package/docs/ai/components/qr-reader.md +80 -0
- package/docs/ai/patterns.md +1 -1
- package/docs/components/file-gallery.md +692 -0
- package/docs/components/qr-reader.md +327 -0
- package/docs/routing.md +9 -8
- package/package.json +1 -1
- package/bin/templates/pwa/public/manifest.json +0 -24
- package/bin/templates/pwa/src/utils/fetch.ts +0 -39
- package/bin/templates/social/README.md +0 -42
- package/bin/templates/social/global.d.ts +0 -14
- package/bin/templates/social/index.html +0 -13
- package/bin/templates/social/package.json +0 -21
- package/bin/templates/social/public/vite.svg +0 -1
- package/bin/templates/social/src/main.ts +0 -33
- package/bin/templates/social/src/pages/feed-page.ts +0 -111
- package/bin/templates/social/src/pages/messages-page.ts +0 -102
- package/bin/templates/social/src/pages/not-found-page.ts +0 -46
- package/bin/templates/social/src/pages/profile-page.ts +0 -99
- package/bin/templates/social/src/pages/settings-page.ts +0 -119
- package/bin/templates/social/src/router.ts +0 -9
- package/bin/templates/social/src/styles/global.css +0 -156
- package/bin/templates/social/tsconfig.json +0 -22
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { Context } from 'snice';
|
|
3
|
+
import { errorMiddleware } from '../../src/middleware/error';
|
|
4
|
+
import * as storage from '../../src/services/storage';
|
|
5
|
+
import type { Principal } from '../../src/types/auth';
|
|
6
|
+
|
|
7
|
+
describe('Error Middleware', () => {
|
|
8
|
+
let mockContext: Context;
|
|
9
|
+
let mockNext: ReturnType<typeof vi.fn>;
|
|
10
|
+
let originalLocation: Location;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
localStorage.clear();
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
|
|
16
|
+
// Save original location
|
|
17
|
+
originalLocation = window.location;
|
|
18
|
+
|
|
19
|
+
// Mock window.location
|
|
20
|
+
delete (window as any).location;
|
|
21
|
+
window.location = { href: '' } as Location;
|
|
22
|
+
|
|
23
|
+
// Create mock context
|
|
24
|
+
mockContext = {
|
|
25
|
+
application: {
|
|
26
|
+
principal: {
|
|
27
|
+
user: { id: '1', name: 'Test', email: 'test@example.com' },
|
|
28
|
+
isAuthenticated: true,
|
|
29
|
+
} as Principal,
|
|
30
|
+
},
|
|
31
|
+
navigation: {
|
|
32
|
+
route: '/',
|
|
33
|
+
params: {},
|
|
34
|
+
},
|
|
35
|
+
update: vi.fn(),
|
|
36
|
+
} as unknown as Context;
|
|
37
|
+
|
|
38
|
+
// Create mock next function
|
|
39
|
+
const mockResponse = new Response('{}', { status: 200 });
|
|
40
|
+
mockNext = vi.fn(() => Promise.resolve(mockResponse));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
window.location = originalLocation;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should pass through successful responses', async () => {
|
|
48
|
+
const mockResponse = new Response('{"data":"ok"}', { status: 200 });
|
|
49
|
+
const response = await errorMiddleware.call(mockContext, mockResponse, mockNext);
|
|
50
|
+
|
|
51
|
+
expect(mockNext).toHaveBeenCalled();
|
|
52
|
+
expect(response).toBeInstanceOf(Response);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle 401 unauthorized by clearing token and redirecting', async () => {
|
|
56
|
+
storage.setToken('test-token');
|
|
57
|
+
const mockResponse = new Response('Unauthorized', { status: 401 });
|
|
58
|
+
|
|
59
|
+
await expect(
|
|
60
|
+
errorMiddleware.call(mockContext, mockResponse, mockNext)
|
|
61
|
+
).rejects.toThrow('Unauthorized - redirecting to login');
|
|
62
|
+
|
|
63
|
+
expect(storage.getToken()).toBeNull();
|
|
64
|
+
const principal = mockContext.application.principal as Principal;
|
|
65
|
+
expect(principal.user).toBeNull();
|
|
66
|
+
expect(principal.isAuthenticated).toBe(false);
|
|
67
|
+
expect(window.location.href).toBe('#/login');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should throw error for failed requests with JSON error message', async () => {
|
|
71
|
+
const mockResponse = new Response(
|
|
72
|
+
JSON.stringify({ message: 'Custom error message' }),
|
|
73
|
+
{
|
|
74
|
+
status: 400,
|
|
75
|
+
headers: { 'content-type': 'application/json' },
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
errorMiddleware.call(mockContext, mockResponse, mockNext)
|
|
81
|
+
).rejects.toThrow('Custom error message');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw error for failed requests without JSON body', async () => {
|
|
85
|
+
const mockResponse = new Response('Internal Server Error', {
|
|
86
|
+
status: 500,
|
|
87
|
+
headers: { 'content-type': 'text/plain' },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
errorMiddleware.call(mockContext, mockResponse, mockNext)
|
|
92
|
+
).rejects.toThrow('Request failed with status 500');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw generic error for failed JSON requests without message', async () => {
|
|
96
|
+
const mockResponse = new Response(JSON.stringify({}), {
|
|
97
|
+
status: 400,
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(
|
|
102
|
+
errorMiddleware.call(mockContext, mockResponse, mockNext)
|
|
103
|
+
).rejects.toThrow('Request failed with status 400');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { Context } from 'snice';
|
|
3
|
+
import { createRetryMiddleware } from '../../src/middleware/retry';
|
|
4
|
+
|
|
5
|
+
describe('Retry Middleware', () => {
|
|
6
|
+
let mockContext: Context;
|
|
7
|
+
let mockRequest: Request;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
|
|
12
|
+
mockContext = {
|
|
13
|
+
application: {
|
|
14
|
+
principal: {
|
|
15
|
+
user: null,
|
|
16
|
+
isAuthenticated: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
navigation: {
|
|
20
|
+
route: '/',
|
|
21
|
+
params: {},
|
|
22
|
+
},
|
|
23
|
+
update: vi.fn(),
|
|
24
|
+
} as unknown as Context;
|
|
25
|
+
|
|
26
|
+
mockRequest = new Request('https://api.example.com/data');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return response on first successful attempt', async () => {
|
|
30
|
+
const retryMiddleware = createRetryMiddleware(3, 10);
|
|
31
|
+
const mockResponse = new Response('{}', { status: 200 });
|
|
32
|
+
const mockNext = vi.fn(() => Promise.resolve(mockResponse));
|
|
33
|
+
|
|
34
|
+
const response = await retryMiddleware.call(mockContext, mockRequest, mockNext);
|
|
35
|
+
|
|
36
|
+
expect(mockNext).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(response).toBe(mockResponse);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should retry on failure and eventually succeed', async () => {
|
|
41
|
+
const retryMiddleware = createRetryMiddleware(3, 10);
|
|
42
|
+
const mockResponse = new Response('{}', { status: 200 });
|
|
43
|
+
const mockNext = vi
|
|
44
|
+
.fn()
|
|
45
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
46
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
47
|
+
.mockResolvedValueOnce(mockResponse);
|
|
48
|
+
|
|
49
|
+
const response = await retryMiddleware.call(mockContext, mockRequest, mockNext);
|
|
50
|
+
|
|
51
|
+
expect(mockNext).toHaveBeenCalledTimes(3);
|
|
52
|
+
expect(response).toBe(mockResponse);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw error after max retries', async () => {
|
|
56
|
+
const retryMiddleware = createRetryMiddleware(3, 10);
|
|
57
|
+
const error = new Error('Network error');
|
|
58
|
+
const mockNext = vi.fn(() => Promise.reject(error));
|
|
59
|
+
|
|
60
|
+
await expect(
|
|
61
|
+
retryMiddleware.call(mockContext, mockRequest, mockNext)
|
|
62
|
+
).rejects.toThrow('Network error');
|
|
63
|
+
|
|
64
|
+
expect(mockNext).toHaveBeenCalledTimes(3);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not retry on Unauthorized errors', async () => {
|
|
68
|
+
const retryMiddleware = createRetryMiddleware(3, 10);
|
|
69
|
+
const error = new Error('Unauthorized');
|
|
70
|
+
const mockNext = vi.fn(() => Promise.reject(error));
|
|
71
|
+
|
|
72
|
+
await expect(
|
|
73
|
+
retryMiddleware.call(mockContext, mockRequest, mockNext)
|
|
74
|
+
).rejects.toThrow('Unauthorized');
|
|
75
|
+
|
|
76
|
+
// Should only try once, no retries
|
|
77
|
+
expect(mockNext).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use exponential backoff', async () => {
|
|
81
|
+
vi.useFakeTimers();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const retryMiddleware = createRetryMiddleware(3, 100);
|
|
85
|
+
const mockNext = vi.fn(() => Promise.reject(new Error('Network error')));
|
|
86
|
+
|
|
87
|
+
const promise = retryMiddleware.call(mockContext, mockRequest, mockNext).catch(err => err);
|
|
88
|
+
|
|
89
|
+
// Run all timers to completion
|
|
90
|
+
await vi.runAllTimersAsync();
|
|
91
|
+
|
|
92
|
+
// All 3 attempts should have been made
|
|
93
|
+
expect(mockNext).toHaveBeenCalledTimes(3);
|
|
94
|
+
|
|
95
|
+
// Promise should have rejected with network error
|
|
96
|
+
const result = await promise;
|
|
97
|
+
expect(result).toBeInstanceOf(Error);
|
|
98
|
+
expect(result.message).toBe('Network error');
|
|
99
|
+
} finally {
|
|
100
|
+
vi.useRealTimers();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { login, logout, isAuthenticated, refreshToken } from '../../src/services/auth';
|
|
3
|
+
import * as storage from '../../src/services/storage';
|
|
4
|
+
|
|
5
|
+
describe('Auth Service', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
localStorage.clear();
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('login', () => {
|
|
12
|
+
it('should login with valid credentials', async () => {
|
|
13
|
+
const credentials = { email: 'demo@example.com', password: 'demo' };
|
|
14
|
+
const result = await login(credentials);
|
|
15
|
+
|
|
16
|
+
expect(result).toHaveProperty('token');
|
|
17
|
+
expect(result).toHaveProperty('user');
|
|
18
|
+
expect(result.user.email).toBe('demo@example.com');
|
|
19
|
+
expect(result.user.name).toBe('Demo User');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should store token and user on successful login', async () => {
|
|
23
|
+
const credentials = { email: 'demo@example.com', password: 'demo' };
|
|
24
|
+
await login(credentials);
|
|
25
|
+
|
|
26
|
+
expect(storage.getToken()).toBeTruthy();
|
|
27
|
+
expect(storage.getUser()).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw error for invalid credentials', async () => {
|
|
31
|
+
const credentials = { email: 'wrong@example.com', password: 'wrong' };
|
|
32
|
+
|
|
33
|
+
await expect(login(credentials)).rejects.toThrow('Invalid credentials');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should throw error for wrong password', async () => {
|
|
37
|
+
const credentials = { email: 'demo@example.com', password: 'wrong' };
|
|
38
|
+
|
|
39
|
+
await expect(login(credentials)).rejects.toThrow('Invalid credentials');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('logout', () => {
|
|
44
|
+
it('should clear token on logout', async () => {
|
|
45
|
+
storage.setToken('test-token');
|
|
46
|
+
storage.setUser({ id: '1', name: 'Test' });
|
|
47
|
+
|
|
48
|
+
await logout();
|
|
49
|
+
|
|
50
|
+
expect(storage.getToken()).toBeNull();
|
|
51
|
+
expect(storage.getUser()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isAuthenticated', () => {
|
|
56
|
+
it('should return true when valid token exists', () => {
|
|
57
|
+
// Non-expired token
|
|
58
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJkZW1vQGV4YW1wbGUuY29tIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.mock';
|
|
59
|
+
storage.setToken(token);
|
|
60
|
+
|
|
61
|
+
expect(isAuthenticated()).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return false when no token exists', () => {
|
|
65
|
+
expect(isAuthenticated()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return false when token is expired', () => {
|
|
69
|
+
// Expired token
|
|
70
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNjAwMDAwMDAwfQ.mock';
|
|
71
|
+
storage.setToken(token);
|
|
72
|
+
|
|
73
|
+
expect(isAuthenticated()).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('refreshToken', () => {
|
|
78
|
+
it('should return new token', async () => {
|
|
79
|
+
const newToken = await refreshToken();
|
|
80
|
+
expect(newToken).toBeTruthy();
|
|
81
|
+
expect(typeof newToken).toBe('string');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should update stored token', async () => {
|
|
85
|
+
await refreshToken();
|
|
86
|
+
expect(storage.getToken()).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { decodeJWT, isTokenExpired, getTokenExpiration } from '../../src/services/jwt';
|
|
3
|
+
|
|
4
|
+
describe('JWT Service', () => {
|
|
5
|
+
describe('decodeJWT', () => {
|
|
6
|
+
it('should decode a valid JWT', () => {
|
|
7
|
+
// Valid JWT: {"sub":"123","name":"Test","exp":9999999999}
|
|
8
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiVGVzdCIsImV4cCI6OTk5OTk5OTk5OX0.mock';
|
|
9
|
+
const payload = decodeJWT(token);
|
|
10
|
+
|
|
11
|
+
expect(payload).toBeTruthy();
|
|
12
|
+
expect(payload?.sub).toBe('123');
|
|
13
|
+
expect(payload?.name).toBe('Test');
|
|
14
|
+
expect(payload?.exp).toBe(9999999999);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return null for invalid JWT format', () => {
|
|
18
|
+
const payload = decodeJWT('invalid-token');
|
|
19
|
+
expect(payload).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return null for malformed JWT', () => {
|
|
23
|
+
const payload = decodeJWT('part1.part2');
|
|
24
|
+
expect(payload).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return null for JWT with invalid base64', () => {
|
|
28
|
+
const payload = decodeJWT('header.!!!invalid!!!.signature');
|
|
29
|
+
expect(payload).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('isTokenExpired', () => {
|
|
34
|
+
it('should return false for non-expired token', () => {
|
|
35
|
+
// Token expires in year 2286
|
|
36
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock';
|
|
37
|
+
expect(isTokenExpired(token)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return true for expired token', () => {
|
|
41
|
+
// Token expired in 2020
|
|
42
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE2MDAwMDAwMDB9.mock';
|
|
43
|
+
expect(isTokenExpired(token)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return true for token without exp claim', () => {
|
|
47
|
+
// Token without exp: {"sub":"123"}
|
|
48
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.mock';
|
|
49
|
+
expect(isTokenExpired(token)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return true for invalid token', () => {
|
|
53
|
+
expect(isTokenExpired('invalid')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getTokenExpiration', () => {
|
|
58
|
+
it('should return expiration date for valid token', () => {
|
|
59
|
+
// Token with exp: 9999999999 (Sat Nov 20 2286)
|
|
60
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock';
|
|
61
|
+
const expiration = getTokenExpiration(token);
|
|
62
|
+
|
|
63
|
+
expect(expiration).toBeInstanceOf(Date);
|
|
64
|
+
expect(expiration?.getTime()).toBe(9999999999 * 1000);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return null for token without exp', () => {
|
|
68
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.mock';
|
|
69
|
+
expect(getTokenExpiration(token)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return null for invalid token', () => {
|
|
73
|
+
expect(getTokenExpiration('invalid')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { getToken, setToken, clearToken, getUser, setUser } from '../../src/services/storage';
|
|
3
|
+
|
|
4
|
+
describe('Storage Service', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Token Management', () => {
|
|
11
|
+
it('should set and get token', () => {
|
|
12
|
+
const token = 'test-token-123';
|
|
13
|
+
setToken(token);
|
|
14
|
+
expect(getToken()).toBe(token);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return null when no token exists', () => {
|
|
18
|
+
expect(getToken()).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should clear token and user', () => {
|
|
22
|
+
setToken('test-token');
|
|
23
|
+
setUser({ id: '1', name: 'Test' });
|
|
24
|
+
|
|
25
|
+
clearToken();
|
|
26
|
+
|
|
27
|
+
expect(getToken()).toBeNull();
|
|
28
|
+
expect(getUser()).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('User Management', () => {
|
|
33
|
+
it('should set and get user object', () => {
|
|
34
|
+
const user = { id: '1', name: 'Test User', email: 'test@example.com' };
|
|
35
|
+
setUser(user);
|
|
36
|
+
expect(getUser()).toEqual(user);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return null when no user exists', () => {
|
|
40
|
+
expect(getUser()).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle complex user objects', () => {
|
|
44
|
+
const user = {
|
|
45
|
+
id: '1',
|
|
46
|
+
name: 'Test User',
|
|
47
|
+
email: 'test@example.com',
|
|
48
|
+
metadata: {
|
|
49
|
+
preferences: {
|
|
50
|
+
theme: 'dark',
|
|
51
|
+
notifications: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
setUser(user);
|
|
57
|
+
expect(getUser()).toEqual(user);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should overwrite existing user', () => {
|
|
61
|
+
setUser({ id: '1', name: 'User 1' });
|
|
62
|
+
setUser({ id: '2', name: 'User 2' });
|
|
63
|
+
|
|
64
|
+
const user = getUser<{ id: string; name: string }>();
|
|
65
|
+
expect(user?.id).toBe('2');
|
|
66
|
+
expect(user?.name).toBe('User 2');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineConfig } from '
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
2
|
import swc from 'unplugin-swc';
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
@@ -18,21 +18,16 @@ export default defineConfig({
|
|
|
18
18
|
},
|
|
19
19
|
}),
|
|
20
20
|
],
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
test: {
|
|
22
|
+
environment: 'happy-dom',
|
|
23
|
+
globals: true,
|
|
24
|
+
coverage: {
|
|
25
|
+
reporter: ['text', 'json', 'html'],
|
|
26
|
+
exclude: [
|
|
27
|
+
'node_modules/',
|
|
28
|
+
'dist/',
|
|
29
|
+
'tests/**',
|
|
30
|
+
],
|
|
31
31
|
},
|
|
32
|
-
sourcemap: true,
|
|
33
|
-
chunkSizeWarningLimit: 500
|
|
34
32
|
},
|
|
35
|
-
|
|
36
|
-
drop: process.env.NODE_ENV === 'production' ? ['debugger'] : []
|
|
37
|
-
}
|
|
38
|
-
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { FileGalleryView, GalleryFile, CustomAction, UploadRequest, SniceFileGalleryElement } from './snice-file-gallery.types';
|
|
2
|
+
export declare class SniceFileGallery extends HTMLElement implements SniceFileGalleryElement {
|
|
3
|
+
accept: string;
|
|
4
|
+
multiple: boolean;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
maxSize: number;
|
|
7
|
+
maxFiles: number;
|
|
8
|
+
view: FileGalleryView;
|
|
9
|
+
showProgress: boolean;
|
|
10
|
+
allowPause: boolean;
|
|
11
|
+
allowDelete: boolean;
|
|
12
|
+
autoUpload: boolean;
|
|
13
|
+
showAddButton: boolean;
|
|
14
|
+
hideAddButton: boolean;
|
|
15
|
+
input?: HTMLInputElement;
|
|
16
|
+
dropZone?: HTMLElement;
|
|
17
|
+
private galleryFiles;
|
|
18
|
+
private galleryCustomActions;
|
|
19
|
+
private isDragOver;
|
|
20
|
+
private uploadAbortControllers;
|
|
21
|
+
private container?;
|
|
22
|
+
private galleryContainer?;
|
|
23
|
+
private galleryHeader?;
|
|
24
|
+
uploadFile(request: UploadRequest): any;
|
|
25
|
+
get files(): GalleryFile[];
|
|
26
|
+
get customActions(): CustomAction[];
|
|
27
|
+
getFile(fileId: string): GalleryFile | undefined;
|
|
28
|
+
getCustomAction(actionId: string): CustomAction | undefined;
|
|
29
|
+
isPending(fileId: string): boolean;
|
|
30
|
+
isUploading(fileId: string): boolean;
|
|
31
|
+
isPaused(fileId: string): boolean;
|
|
32
|
+
isCompleted(fileId: string): boolean;
|
|
33
|
+
hasError(fileId: string): boolean;
|
|
34
|
+
canAddFiles(): boolean;
|
|
35
|
+
setupComponent(): void;
|
|
36
|
+
private updateGalleryDOM;
|
|
37
|
+
private updateHeaderDOM;
|
|
38
|
+
private updateFileItemDOM;
|
|
39
|
+
private createFileItem;
|
|
40
|
+
private createAddButton;
|
|
41
|
+
private createCustomAction;
|
|
42
|
+
renderContent(): import("snice").TemplateResult;
|
|
43
|
+
componentStyles(): import("snice").CSSResult;
|
|
44
|
+
private handleDropZoneClick;
|
|
45
|
+
private handleFileSelect;
|
|
46
|
+
private handleDragEnter;
|
|
47
|
+
private handleDragLeave;
|
|
48
|
+
private handleDragOver;
|
|
49
|
+
private handleDrop;
|
|
50
|
+
private handleViewToggle;
|
|
51
|
+
private handleClearAll;
|
|
52
|
+
addFiles(files: FileList | File[]): void;
|
|
53
|
+
addFileWithPreview(file: File, previewDataUrl: string): void;
|
|
54
|
+
removeFile(fileId: string): void;
|
|
55
|
+
pauseUpload(fileId: string): void;
|
|
56
|
+
resumeUpload(fileId: string): void;
|
|
57
|
+
retryUpload(fileId: string): void;
|
|
58
|
+
clear(): void;
|
|
59
|
+
addCustomAction(icon: string, text: string): string;
|
|
60
|
+
removeCustomAction(actionId: string): void;
|
|
61
|
+
clearCustomActions(): void;
|
|
62
|
+
clearCompleted(): void;
|
|
63
|
+
clearErrors(): void;
|
|
64
|
+
pauseAll(): void;
|
|
65
|
+
resumeAll(): void;
|
|
66
|
+
retryAll(): void;
|
|
67
|
+
cancelUpload(fileId: string): void;
|
|
68
|
+
cancelAll(): void;
|
|
69
|
+
openFilePicker(): void;
|
|
70
|
+
setFileBadge(fileId: string, badge: string, position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'): void;
|
|
71
|
+
removeFileBadge(fileId: string): void;
|
|
72
|
+
private handleCustomActionClick;
|
|
73
|
+
private startUpload;
|
|
74
|
+
private generatePreview;
|
|
75
|
+
private generateFileId;
|
|
76
|
+
private isAcceptedType;
|
|
77
|
+
private formatAcceptTypes;
|
|
78
|
+
private formatFileSize;
|
|
79
|
+
private emitFilesChange;
|
|
80
|
+
private emitFileRemove;
|
|
81
|
+
private emitUploadProgress;
|
|
82
|
+
private emitUploadComplete;
|
|
83
|
+
private emitUploadError;
|
|
84
|
+
private emitUploadPause;
|
|
85
|
+
private emitError;
|
|
86
|
+
cleanup(): void;
|
|
87
|
+
}
|