@tuturuuu/utils 0.0.3 → 0.6.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.
- package/CHANGELOG.md +313 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
capitalize,
|
|
4
|
+
formatBytes,
|
|
5
|
+
formatCurrency,
|
|
6
|
+
formatDuration,
|
|
7
|
+
getCurrencyLocale,
|
|
8
|
+
isValidBlobUrl,
|
|
9
|
+
isValidHttpUrl,
|
|
10
|
+
} from '../format';
|
|
11
|
+
|
|
12
|
+
describe('Format Utilities', () => {
|
|
13
|
+
describe('capitalize', () => {
|
|
14
|
+
it('capitalizes first letter of a word', () => {
|
|
15
|
+
expect(capitalize('hello')).toBe('Hello');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('handles single character strings', () => {
|
|
19
|
+
expect(capitalize('a')).toBe('A');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('handles empty string', () => {
|
|
23
|
+
expect(capitalize('')).toBe('');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('handles null input', () => {
|
|
27
|
+
expect(capitalize(null)).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('handles undefined input', () => {
|
|
31
|
+
expect(capitalize(undefined)).toBe('');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does not change already capitalized strings', () => {
|
|
35
|
+
expect(capitalize('Hello')).toBe('Hello');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('preserves rest of the string', () => {
|
|
39
|
+
expect(capitalize('hELLO')).toBe('HELLO');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles strings with spaces', () => {
|
|
43
|
+
expect(capitalize('hello world')).toBe('Hello world');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles numbers at the start', () => {
|
|
47
|
+
expect(capitalize('123abc')).toBe('123abc');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('formatBytes', () => {
|
|
52
|
+
it('formats 0 bytes', () => {
|
|
53
|
+
expect(formatBytes(0)).toBe('0 Byte');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('formats bytes', () => {
|
|
57
|
+
expect(formatBytes(500)).toBe('500 Bytes');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('formats kilobytes', () => {
|
|
61
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('formats megabytes', () => {
|
|
65
|
+
expect(formatBytes(1024 * 1024)).toBe('1 MB');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('formats gigabytes', () => {
|
|
69
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('formats terabytes', () => {
|
|
73
|
+
expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe('1 TB');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('respects decimal places option', () => {
|
|
77
|
+
expect(formatBytes(1536, { decimals: 2 })).toBe('1.50 KB');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('uses accurate sizes when specified', () => {
|
|
81
|
+
expect(formatBytes(1024, { sizeType: 'accurate' })).toBe('1 KiB');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('uses accurate sizes for MiB', () => {
|
|
85
|
+
expect(formatBytes(1024 * 1024, { sizeType: 'accurate' })).toBe('1 MiB');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles large numbers', () => {
|
|
89
|
+
expect(formatBytes(5 * 1024 * 1024 * 1024, { decimals: 1 })).toBe(
|
|
90
|
+
'5.0 GB'
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('formatDuration', () => {
|
|
96
|
+
it('formats seconds only', () => {
|
|
97
|
+
expect(formatDuration(30)).toBe('30 seconds');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('formats 1 second singular', () => {
|
|
101
|
+
expect(formatDuration(1)).toBe('1 seconds');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('formats minutes only', () => {
|
|
105
|
+
expect(formatDuration(120)).toBe('2 minutes');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('formats 1 minute singular', () => {
|
|
109
|
+
expect(formatDuration(60)).toBe('1 minute');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('formats minutes and seconds', () => {
|
|
113
|
+
expect(formatDuration(90)).toBe('1 minute 30 seconds');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('formats hours only', () => {
|
|
117
|
+
expect(formatDuration(3600)).toBe('1 hour');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('formats multiple hours', () => {
|
|
121
|
+
expect(formatDuration(7200)).toBe('2 hours');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('formats hours and minutes', () => {
|
|
125
|
+
expect(formatDuration(3720)).toBe('1 hour 2 minutes');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('does not show seconds when hours are present', () => {
|
|
129
|
+
expect(formatDuration(3661)).toBe('1 hour 1 minute');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('formats complex duration', () => {
|
|
133
|
+
expect(formatDuration(7320)).toBe('2 hours 2 minutes');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles 59 seconds', () => {
|
|
137
|
+
expect(formatDuration(59)).toBe('59 seconds');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles exactly 60 seconds', () => {
|
|
141
|
+
expect(formatDuration(60)).toBe('1 minute');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('isValidBlobUrl', () => {
|
|
146
|
+
it('returns true for valid blob URL', () => {
|
|
147
|
+
expect(isValidBlobUrl('blob:http://localhost:3000/abc123')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns true for blob URL with uppercase', () => {
|
|
151
|
+
expect(isValidBlobUrl('BLOB:http://localhost:3000/abc123')).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns true for blob URL with leading whitespace', () => {
|
|
155
|
+
expect(isValidBlobUrl(' blob:http://localhost:3000/abc123')).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns false for http URL', () => {
|
|
159
|
+
expect(isValidBlobUrl('http://example.com')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns false for https URL', () => {
|
|
163
|
+
expect(isValidBlobUrl('https://example.com')).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns false for null', () => {
|
|
167
|
+
expect(isValidBlobUrl(null)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns false for undefined', () => {
|
|
171
|
+
expect(isValidBlobUrl(undefined)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns false for empty string', () => {
|
|
175
|
+
expect(isValidBlobUrl('')).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns false for data URL', () => {
|
|
179
|
+
expect(isValidBlobUrl('data:image/png;base64,abc')).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('isValidHttpUrl', () => {
|
|
184
|
+
it('returns true for http URL', () => {
|
|
185
|
+
expect(isValidHttpUrl('http://example.com')).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns true for https URL', () => {
|
|
189
|
+
expect(isValidHttpUrl('https://example.com')).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns true for URL with path', () => {
|
|
193
|
+
expect(isValidHttpUrl('https://example.com/path/to/resource')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('returns true for URL with query string', () => {
|
|
197
|
+
expect(isValidHttpUrl('https://example.com?foo=bar')).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns true for URL with port', () => {
|
|
201
|
+
expect(isValidHttpUrl('http://localhost:3000')).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns false for ftp URL', () => {
|
|
205
|
+
expect(isValidHttpUrl('ftp://example.com')).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns false for file URL', () => {
|
|
209
|
+
expect(isValidHttpUrl('file:///path/to/file')).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns false for blob URL', () => {
|
|
213
|
+
expect(isValidHttpUrl('blob:http://localhost:3000/abc')).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns false for null', () => {
|
|
217
|
+
expect(isValidHttpUrl(null)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('returns false for undefined', () => {
|
|
221
|
+
expect(isValidHttpUrl(undefined)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('returns false for empty string', () => {
|
|
225
|
+
expect(isValidHttpUrl('')).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns false for invalid URL', () => {
|
|
229
|
+
expect(isValidHttpUrl('not-a-url')).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('returns false for javascript protocol', () => {
|
|
233
|
+
expect(isValidHttpUrl('javascript:alert(1)')).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('handles URL with whitespace', () => {
|
|
237
|
+
expect(isValidHttpUrl(' https://example.com ')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('getCurrencyLocale', () => {
|
|
242
|
+
it('returns vi-VN for VND', () => {
|
|
243
|
+
expect(getCurrencyLocale('VND')).toBe('vi-VN');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns en-US for USD', () => {
|
|
247
|
+
expect(getCurrencyLocale('USD')).toBe('en-US');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns en-PH for PHP', () => {
|
|
251
|
+
expect(getCurrencyLocale('PHP')).toBe('en-PH');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('defaults to en-US for unknown currency', () => {
|
|
255
|
+
expect(getCurrencyLocale('UNKNOWN')).toBe('en-US');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('is case insensitive', () => {
|
|
259
|
+
expect(getCurrencyLocale('php')).toBe('en-PH');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('formatCurrency', () => {
|
|
264
|
+
it('formats VND currency by default', () => {
|
|
265
|
+
const result = formatCurrency(1000000);
|
|
266
|
+
expect(result).toContain('1.000.000');
|
|
267
|
+
expect(result).toContain('₫');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('formats negative amounts with sign', () => {
|
|
271
|
+
const result = formatCurrency(-50000);
|
|
272
|
+
expect(result).toContain('-');
|
|
273
|
+
expect(result).toContain('50.000');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('formats positive amounts without sign by default', () => {
|
|
277
|
+
const result = formatCurrency(50000);
|
|
278
|
+
expect(result).not.toContain('+');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('formats positive amounts with sign when specified', () => {
|
|
282
|
+
const result = formatCurrency(50000, 'VND', 'vi-VN', {
|
|
283
|
+
signDisplay: 'always',
|
|
284
|
+
});
|
|
285
|
+
expect(result).toContain('+');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('formats USD currency', () => {
|
|
289
|
+
const result = formatCurrency(1000, 'USD');
|
|
290
|
+
expect(result).toContain('$');
|
|
291
|
+
expect(result).toContain('1,000');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('formats EUR currency', () => {
|
|
295
|
+
const result = formatCurrency(1000, 'EUR');
|
|
296
|
+
expect(result).toContain('€');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('formats PHP currency', () => {
|
|
300
|
+
const result = formatCurrency(1000, 'PHP');
|
|
301
|
+
expect(result).toContain('₱');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('respects custom options', () => {
|
|
305
|
+
const result = formatCurrency(1000.5, 'USD', 'en-US', {
|
|
306
|
+
minimumFractionDigits: 2,
|
|
307
|
+
maximumFractionDigits: 2,
|
|
308
|
+
});
|
|
309
|
+
expect(result).toContain('1,000.50');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('handles zero amount', () => {
|
|
313
|
+
const result = formatCurrency(0);
|
|
314
|
+
expect(result).toContain('0');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { containsHtml, sanitizeHtml, textToHtml } from '../html-sanitizer';
|
|
6
|
+
|
|
7
|
+
describe('sanitizeHtml', () => {
|
|
8
|
+
describe('basic functionality', () => {
|
|
9
|
+
it('should return empty string for empty input', () => {
|
|
10
|
+
expect(sanitizeHtml('')).toBe('');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return empty string for null/undefined-like input', () => {
|
|
14
|
+
expect(sanitizeHtml(null as unknown as string)).toBe('');
|
|
15
|
+
expect(sanitizeHtml(undefined as unknown as string)).toBe('');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should preserve plain text', () => {
|
|
19
|
+
expect(sanitizeHtml('Hello World')).toBe('Hello World');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should preserve text with special characters', () => {
|
|
23
|
+
expect(sanitizeHtml('Hello & World < Test > Quote "test"')).toBe(
|
|
24
|
+
'Hello & World < Test > Quote "test"'
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('allowed tags', () => {
|
|
30
|
+
it('should allow <p> tags', () => {
|
|
31
|
+
expect(sanitizeHtml('<p>Paragraph</p>')).toBe('<p>Paragraph</p>');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should allow <br> tags', () => {
|
|
35
|
+
expect(sanitizeHtml('Line 1<br>Line 2')).toBe('Line 1<br>Line 2');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should allow <strong> and <b> tags', () => {
|
|
39
|
+
expect(sanitizeHtml('<strong>Bold</strong>')).toBe(
|
|
40
|
+
'<strong>Bold</strong>'
|
|
41
|
+
);
|
|
42
|
+
expect(sanitizeHtml('<b>Bold</b>')).toBe('<b>Bold</b>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should allow <em> and <i> tags', () => {
|
|
46
|
+
expect(sanitizeHtml('<em>Italic</em>')).toBe('<em>Italic</em>');
|
|
47
|
+
expect(sanitizeHtml('<i>Italic</i>')).toBe('<i>Italic</i>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should allow <u> tags', () => {
|
|
51
|
+
expect(sanitizeHtml('<u>Underline</u>')).toBe('<u>Underline</u>');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should allow list tags', () => {
|
|
55
|
+
expect(sanitizeHtml('<ul><li>Item 1</li><li>Item 2</li></ul>')).toBe(
|
|
56
|
+
'<ul><li>Item 1</li><li>Item 2</li></ul>'
|
|
57
|
+
);
|
|
58
|
+
expect(sanitizeHtml('<ol><li>Item 1</li><li>Item 2</li></ol>')).toBe(
|
|
59
|
+
'<ol><li>Item 1</li><li>Item 2</li></ol>'
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should allow <span> tags with class attribute', () => {
|
|
64
|
+
expect(sanitizeHtml('<span class="highlight">Text</span>')).toBe(
|
|
65
|
+
'<span class="highlight">Text</span>'
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should allow <div> tags with class attribute', () => {
|
|
70
|
+
expect(sanitizeHtml('<div class="container">Content</div>')).toBe(
|
|
71
|
+
'<div class="container">Content</div>'
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should allow nested allowed tags', () => {
|
|
76
|
+
const input = '<p><strong>Bold <em>and italic</em></strong></p>';
|
|
77
|
+
expect(sanitizeHtml(input)).toBe(
|
|
78
|
+
'<p><strong>Bold <em>and italic</em></strong></p>'
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('link sanitization', () => {
|
|
84
|
+
it('should allow <a> tags with safe href', () => {
|
|
85
|
+
const result = sanitizeHtml('<a href="https://example.com">Link</a>');
|
|
86
|
+
expect(result).toContain('href="https://example.com"');
|
|
87
|
+
expect(result).toContain('rel="noopener noreferrer"');
|
|
88
|
+
expect(result).toContain('target="_blank"');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should allow http links', () => {
|
|
92
|
+
const result = sanitizeHtml('<a href="http://example.com">Link</a>');
|
|
93
|
+
expect(result).toContain('href="http://example.com"');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should allow mailto links', () => {
|
|
97
|
+
const result = sanitizeHtml(
|
|
98
|
+
'<a href="mailto:test@example.com">Email</a>'
|
|
99
|
+
);
|
|
100
|
+
expect(result).toContain('href="mailto:test@example.com"');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should allow relative URLs', () => {
|
|
104
|
+
expect(sanitizeHtml('<a href="/page">Link</a>')).toContain(
|
|
105
|
+
'href="/page"'
|
|
106
|
+
);
|
|
107
|
+
expect(sanitizeHtml('<a href="./page">Link</a>')).toContain(
|
|
108
|
+
'href="./page"'
|
|
109
|
+
);
|
|
110
|
+
expect(sanitizeHtml('<a href="../page">Link</a>')).toContain(
|
|
111
|
+
'href="../page"'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should preserve title attribute on links', () => {
|
|
116
|
+
const result = sanitizeHtml(
|
|
117
|
+
'<a href="https://example.com" title="Example">Link</a>'
|
|
118
|
+
);
|
|
119
|
+
expect(result).toContain('title="Example"');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('XSS prevention', () => {
|
|
124
|
+
it('should remove javascript: URLs', () => {
|
|
125
|
+
const result = sanitizeHtml('<a href="javascript:alert(1)">Click</a>');
|
|
126
|
+
expect(result).not.toContain('javascript:');
|
|
127
|
+
expect(result).not.toContain('href');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should remove data: URLs', () => {
|
|
131
|
+
const result = sanitizeHtml(
|
|
132
|
+
'<a href="data:text/html,<script>alert(1)</script>">Click</a>'
|
|
133
|
+
);
|
|
134
|
+
expect(result).not.toContain('data:');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should remove vbscript: URLs', () => {
|
|
138
|
+
const result = sanitizeHtml('<a href="vbscript:msgbox(1)">Click</a>');
|
|
139
|
+
expect(result).not.toContain('vbscript:');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should remove <script> tags', () => {
|
|
143
|
+
const result = sanitizeHtml('<script>alert("xss")</script>');
|
|
144
|
+
expect(result).not.toContain('<script>');
|
|
145
|
+
expect(result).not.toContain('</script>');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should remove <script> tags and preserve text content', () => {
|
|
149
|
+
const result = sanitizeHtml(
|
|
150
|
+
'<p>Before</p><script>alert(1)</script><p>After</p>'
|
|
151
|
+
);
|
|
152
|
+
expect(result).toContain('Before');
|
|
153
|
+
expect(result).toContain('After');
|
|
154
|
+
expect(result).not.toContain('<script>');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should remove event handler attributes', () => {
|
|
158
|
+
// onclick is not in allowed attributes, so it should be removed
|
|
159
|
+
const result = sanitizeHtml('<div onclick="alert(1)">Click me</div>');
|
|
160
|
+
expect(result).not.toContain('onclick');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should remove onerror handlers', () => {
|
|
164
|
+
const result = sanitizeHtml('<img src="x" onerror="alert(1)">');
|
|
165
|
+
expect(result).not.toContain('onerror');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should remove <iframe> tags', () => {
|
|
169
|
+
const result = sanitizeHtml('<iframe src="https://evil.com"></iframe>');
|
|
170
|
+
expect(result).not.toContain('<iframe');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should remove <object> tags', () => {
|
|
174
|
+
const result = sanitizeHtml('<object data="evil.swf"></object>');
|
|
175
|
+
expect(result).not.toContain('<object');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should remove <embed> tags', () => {
|
|
179
|
+
const result = sanitizeHtml('<embed src="evil.swf">');
|
|
180
|
+
expect(result).not.toContain('<embed');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should remove <form> tags', () => {
|
|
184
|
+
const result = sanitizeHtml(
|
|
185
|
+
'<form action="https://evil.com"><input></form>'
|
|
186
|
+
);
|
|
187
|
+
expect(result).not.toContain('<form');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should remove <style> tags', () => {
|
|
191
|
+
const result = sanitizeHtml(
|
|
192
|
+
'<style>body { background: red; }</style><p>Text</p>'
|
|
193
|
+
);
|
|
194
|
+
expect(result).not.toContain('<style>');
|
|
195
|
+
expect(result).toContain('<p>Text</p>');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle case-insensitive dangerous tags', () => {
|
|
199
|
+
const result = sanitizeHtml('<SCRIPT>alert(1)</SCRIPT>');
|
|
200
|
+
expect(result).not.toContain('<SCRIPT>');
|
|
201
|
+
expect(result).not.toContain('<script>');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle mixed case javascript URLs', () => {
|
|
205
|
+
const result = sanitizeHtml('<a href="JaVaScRiPt:alert(1)">Click</a>');
|
|
206
|
+
expect(result).not.toContain('javascript');
|
|
207
|
+
expect(result).not.toContain('JaVaScRiPt');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('disallowed tags', () => {
|
|
212
|
+
it('should remove <img> tags but keep alt text', () => {
|
|
213
|
+
const result = sanitizeHtml('<img src="image.jpg" alt="Description">');
|
|
214
|
+
expect(result).not.toContain('<img');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should remove <video> tags', () => {
|
|
218
|
+
const result = sanitizeHtml(
|
|
219
|
+
'<video src="video.mp4">Video content</video>'
|
|
220
|
+
);
|
|
221
|
+
expect(result).not.toContain('<video');
|
|
222
|
+
expect(result).toContain('Video content');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should remove <audio> tags', () => {
|
|
226
|
+
const result = sanitizeHtml(
|
|
227
|
+
'<audio src="audio.mp3">Audio content</audio>'
|
|
228
|
+
);
|
|
229
|
+
expect(result).not.toContain('<audio');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should remove <table> tags but keep content', () => {
|
|
233
|
+
const result = sanitizeHtml('<table><tr><td>Cell</td></tr></table>');
|
|
234
|
+
expect(result).not.toContain('<table');
|
|
235
|
+
expect(result).toContain('Cell');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('attribute filtering', () => {
|
|
240
|
+
it('should remove style attributes', () => {
|
|
241
|
+
const result = sanitizeHtml(
|
|
242
|
+
'<p style="color: red; background: url(evil.com)">Text</p>'
|
|
243
|
+
);
|
|
244
|
+
expect(result).not.toContain('style=');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should remove id attributes', () => {
|
|
248
|
+
const result = sanitizeHtml('<p id="test">Text</p>');
|
|
249
|
+
expect(result).not.toContain('id=');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should only allow class on span and div', () => {
|
|
253
|
+
const result1 = sanitizeHtml('<span class="test">Text</span>');
|
|
254
|
+
expect(result1).toContain('class="test"');
|
|
255
|
+
|
|
256
|
+
const result2 = sanitizeHtml('<p class="test">Text</p>');
|
|
257
|
+
expect(result2).not.toContain('class=');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('complex scenarios', () => {
|
|
262
|
+
it('should handle deeply nested content', () => {
|
|
263
|
+
const input =
|
|
264
|
+
'<div><p><strong><em><u>Deeply nested</u></em></strong></p></div>';
|
|
265
|
+
const result = sanitizeHtml(input);
|
|
266
|
+
expect(result).toContain('Deeply nested');
|
|
267
|
+
expect(result).toContain('<strong>');
|
|
268
|
+
expect(result).toContain('<em>');
|
|
269
|
+
expect(result).toContain('<u>');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should handle mixed allowed and disallowed content', () => {
|
|
273
|
+
const input =
|
|
274
|
+
'<p>Safe</p><script>evil()</script><strong>Also safe</strong>';
|
|
275
|
+
const result = sanitizeHtml(input);
|
|
276
|
+
expect(result).toContain('<p>Safe</p>');
|
|
277
|
+
expect(result).toContain('<strong>Also safe</strong>');
|
|
278
|
+
expect(result).not.toContain('<script>');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle malformed HTML gracefully', () => {
|
|
282
|
+
const input = '<p>Unclosed paragraph<strong>Bold without close';
|
|
283
|
+
const result = sanitizeHtml(input);
|
|
284
|
+
expect(result).toContain('Unclosed paragraph');
|
|
285
|
+
expect(result).toContain('Bold without close');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('textToHtml', () => {
|
|
291
|
+
it('should convert newlines to <br> tags', () => {
|
|
292
|
+
expect(textToHtml('Line 1\nLine 2')).toBe('Line 1<br>Line 2');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should convert multiple newlines', () => {
|
|
296
|
+
expect(textToHtml('Line 1\nLine 2\nLine 3')).toBe(
|
|
297
|
+
'Line 1<br>Line 2<br>Line 3'
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should handle empty string', () => {
|
|
302
|
+
expect(textToHtml('')).toBe('');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should handle string without newlines', () => {
|
|
306
|
+
expect(textToHtml('No newlines here')).toBe('No newlines here');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should handle consecutive newlines', () => {
|
|
310
|
+
expect(textToHtml('Line 1\n\nLine 3')).toBe('Line 1<br><br>Line 3');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should handle Windows-style line endings (CRLF)', () => {
|
|
314
|
+
// Note: This function only handles \n, not \r\n
|
|
315
|
+
expect(textToHtml('Line 1\r\nLine 2')).toBe('Line 1\r<br>Line 2');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('containsHtml', () => {
|
|
320
|
+
it('should return true for strings with HTML tags', () => {
|
|
321
|
+
expect(containsHtml('<p>Text</p>')).toBe(true);
|
|
322
|
+
expect(containsHtml('<br>')).toBe(true);
|
|
323
|
+
expect(containsHtml('<div class="test">Content</div>')).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should return true for self-closing tags', () => {
|
|
327
|
+
expect(containsHtml('<br/>')).toBe(true);
|
|
328
|
+
expect(containsHtml('<img src="test.jpg"/>')).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return false for plain text', () => {
|
|
332
|
+
expect(containsHtml('Plain text')).toBe(false);
|
|
333
|
+
expect(containsHtml('Hello World')).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should return false for empty string', () => {
|
|
337
|
+
expect(containsHtml('')).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should return true for angle brackets that form tags', () => {
|
|
341
|
+
// Note: The simple regex /<[^>]+>/g matches anything that looks like a tag
|
|
342
|
+
// This includes "5 < 10 and 20 >" because "< 10 and 20 >" matches the pattern
|
|
343
|
+
expect(containsHtml('5 < 10 and 20 > 15')).toBe(true); // Regex sees this as a tag
|
|
344
|
+
expect(containsHtml('Use <tag> for markup')).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle incomplete angle brackets', () => {
|
|
348
|
+
// The simple regex matches anything between < and >
|
|
349
|
+
expect(containsHtml('Less than < greater than >')).toBe(true); // Matches "< greater than >"
|
|
350
|
+
expect(containsHtml('Arrow -> direction')).toBe(false); // No < before >
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should return true for script tags', () => {
|
|
354
|
+
expect(containsHtml('<script>alert(1)</script>')).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should return true for tags with attributes', () => {
|
|
358
|
+
expect(containsHtml('<a href="https://example.com">Link</a>')).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
});
|