@lukso/core 0.1.0-dev.0f1bea5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +159 -0
  3. package/dist/chunk-3WGYJTN4.js +19 -0
  4. package/dist/chunk-3WGYJTN4.js.map +1 -0
  5. package/dist/chunk-4TNWG4ME.js +106 -0
  6. package/dist/chunk-4TNWG4ME.js.map +1 -0
  7. package/dist/chunk-AMRGSLR5.cjs +1 -0
  8. package/dist/chunk-AMRGSLR5.cjs.map +1 -0
  9. package/dist/chunk-CC3LFUYY.cjs +19 -0
  10. package/dist/chunk-CC3LFUYY.cjs.map +1 -0
  11. package/dist/chunk-DFMMMF62.cjs +1 -0
  12. package/dist/chunk-DFMMMF62.cjs.map +1 -0
  13. package/dist/chunk-DKEXQFNE.js +1 -0
  14. package/dist/chunk-DKEXQFNE.js.map +1 -0
  15. package/dist/chunk-DKXHVRHM.js +84 -0
  16. package/dist/chunk-DKXHVRHM.js.map +1 -0
  17. package/dist/chunk-FR74YPGJ.cjs +87 -0
  18. package/dist/chunk-FR74YPGJ.cjs.map +1 -0
  19. package/dist/chunk-LEL6VWU4.js +1 -0
  20. package/dist/chunk-LEL6VWU4.js.map +1 -0
  21. package/dist/chunk-MBIRTPNM.cjs +84 -0
  22. package/dist/chunk-MBIRTPNM.cjs.map +1 -0
  23. package/dist/chunk-NJQVWIZL.cjs +49 -0
  24. package/dist/chunk-NJQVWIZL.cjs.map +1 -0
  25. package/dist/chunk-RM42NG7E.cjs +106 -0
  26. package/dist/chunk-RM42NG7E.cjs.map +1 -0
  27. package/dist/chunk-SV4TVR2K.js +87 -0
  28. package/dist/chunk-SV4TVR2K.js.map +1 -0
  29. package/dist/chunk-X2QNFZU7.js +49 -0
  30. package/dist/chunk-X2QNFZU7.js.map +1 -0
  31. package/dist/index.cjs +37 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +8 -0
  34. package/dist/index.d.ts +8 -0
  35. package/dist/index.js +37 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mixins/device.cjs +8 -0
  38. package/dist/mixins/device.cjs.map +1 -0
  39. package/dist/mixins/device.d.cts +34 -0
  40. package/dist/mixins/device.d.ts +34 -0
  41. package/dist/mixins/device.js +8 -0
  42. package/dist/mixins/device.js.map +1 -0
  43. package/dist/mixins/index.cjs +14 -0
  44. package/dist/mixins/index.cjs.map +1 -0
  45. package/dist/mixins/index.d.cts +3 -0
  46. package/dist/mixins/index.d.ts +3 -0
  47. package/dist/mixins/index.js +14 -0
  48. package/dist/mixins/index.js.map +1 -0
  49. package/dist/mixins/intl.cjs +8 -0
  50. package/dist/mixins/intl.cjs.map +1 -0
  51. package/dist/mixins/intl.d.cts +37 -0
  52. package/dist/mixins/intl.d.ts +37 -0
  53. package/dist/mixins/intl.js +8 -0
  54. package/dist/mixins/intl.js.map +1 -0
  55. package/dist/services/device.cjs +7 -0
  56. package/dist/services/device.cjs.map +1 -0
  57. package/dist/services/device.d.cts +71 -0
  58. package/dist/services/device.d.ts +71 -0
  59. package/dist/services/device.js +7 -0
  60. package/dist/services/device.js.map +1 -0
  61. package/dist/services/index.cjs +20 -0
  62. package/dist/services/index.cjs.map +1 -0
  63. package/dist/services/index.d.cts +4 -0
  64. package/dist/services/index.d.ts +4 -0
  65. package/dist/services/index.js +20 -0
  66. package/dist/services/index.js.map +1 -0
  67. package/dist/services/intl.cjs +15 -0
  68. package/dist/services/intl.cjs.map +1 -0
  69. package/dist/services/intl.d.cts +170 -0
  70. package/dist/services/intl.d.ts +170 -0
  71. package/dist/services/intl.js +15 -0
  72. package/dist/services/intl.js.map +1 -0
  73. package/dist/utils/index.cjs +11 -0
  74. package/dist/utils/index.cjs.map +1 -0
  75. package/dist/utils/index.d.cts +32 -0
  76. package/dist/utils/index.d.ts +32 -0
  77. package/dist/utils/index.js +11 -0
  78. package/dist/utils/index.js.map +1 -0
  79. package/package.json +114 -0
  80. package/src/index.ts +10 -0
  81. package/src/mixins/__tests__/device.spec.ts +119 -0
  82. package/src/mixins/__tests__/intl.spec.ts +198 -0
  83. package/src/mixins/device.ts +48 -0
  84. package/src/mixins/index.ts +8 -0
  85. package/src/mixins/intl.ts +112 -0
  86. package/src/services/__tests__/device.spec.ts +36 -0
  87. package/src/services/__tests__/intl.spec.ts +536 -0
  88. package/src/services/device.ts +121 -0
  89. package/src/services/index.ts +17 -0
  90. package/src/services/intl.ts +326 -0
  91. package/src/utils/__tests__/browserInfo.spec.ts +310 -0
  92. package/src/utils/__tests__/slug.spec.ts +26 -0
  93. package/src/utils/browserInfo.ts +102 -0
  94. package/src/utils/index.ts +4 -0
  95. package/src/utils/slug.ts +13 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Internationalization Service
3
+ *
4
+ * Provides locale-aware formatting for messages, numbers, dates, and times.
5
+ */
6
+
7
+ import {
8
+ createIntl,
9
+ type FormatNumberOptions,
10
+ type IntlConfig,
11
+ } from '@formatjs/intl'
12
+ import { signal } from '@preact/signals-core'
13
+
14
+ /**
15
+ * Translation messages object type
16
+ * Maps translation keys to their corresponding string values
17
+ */
18
+ export type IntlMessages = Record<string, string>
19
+
20
+ /**
21
+ * Default format number options
22
+ * @see https://github.com/formatjs/formatjs/blob/main/packages/ecma402-abstract/types/number.ts
23
+ */
24
+ const formatNumberDefaultOptions = {
25
+ maximumFractionDigits: 18,
26
+ }
27
+
28
+ /**
29
+ * Intl service interface
30
+ */
31
+ export interface IntlService {
32
+ /**
33
+ * Translate a string based on the key
34
+ *
35
+ * @param key - translation key
36
+ * @param options - optional options for formatMessage (for variable interpolation)
37
+ * @returns - translated string
38
+ */
39
+ formatMessage: (key: string, options?: Record<string, string>) => string
40
+
41
+ /**
42
+ * Number formatting based on the locale
43
+ *
44
+ * @param value - number to format
45
+ * @param options - options for formatNumber
46
+ * @returns - formatted number
47
+ */
48
+ formatNumber: (
49
+ value: number | string | bigint,
50
+ options?: FormatNumberOptions
51
+ ) => string
52
+
53
+ /**
54
+ * Date formatting based on the locale
55
+ *
56
+ * @param date - date to format
57
+ * @returns - formatted date
58
+ */
59
+ formatDate: (date?: string | number | Date) => string | undefined
60
+
61
+ /**
62
+ * Time formatting based on the locale
63
+ *
64
+ * @param date - date to format
65
+ * @returns - formatted time
66
+ */
67
+ formatTime: (date?: string | number | Date) => string | undefined
68
+
69
+ /**
70
+ * Timestamp formatting based on the locale
71
+ *
72
+ * @param timestamp - UNIX timestamp in seconds or milliseconds
73
+ * @param options - formatting options (predefined format name or custom Intl.DateTimeFormat options)
74
+ * @returns - formatted date & time string
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const intl = getIntlService();
79
+ *
80
+ * // Use predefined shortDateTime format
81
+ * intl.formatTimestamp(1733251200, 'shortDateTime'); // "Wed, Dec 3, 2025, 16:07"
82
+ *
83
+ * // Use custom format options
84
+ * intl.formatTimestamp(1733251200, { year: '2-digit', month: '2-digit', day: '2-digit' });
85
+ * // "12/03/25"
86
+ *
87
+ * // No options (basic format)
88
+ * intl.formatTimestamp(1733251200);
89
+ * ```
90
+ */
91
+ formatTimestamp: (
92
+ timestamp?: number | string,
93
+ options?: Intl.DateTimeFormatOptions | string
94
+ ) => string
95
+
96
+ /**
97
+ * Change the locale and optionally update messages
98
+ *
99
+ * @param locale - new locale code (e.g., 'en-US', 'de-DE')
100
+ * @param messages - optional new messages object for the locale
101
+ */
102
+ setLocale: (locale: string, messages?: IntlMessages) => void
103
+
104
+ /**
105
+ * Get the current locale
106
+ *
107
+ * @returns - current locale code (e.g., 'en-US', 'de-DE')
108
+ */
109
+ getLocale: () => string
110
+
111
+ /**
112
+ * Set fallback translations to use when a translation key is missing
113
+ * Useful when the host app's intl service doesn't have all translations
114
+ *
115
+ * @param fallbackMessages - translations to use as fallback
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // After intl service is initialized by host app
120
+ * intl.setFallbackTranslations(defaultMessages);
121
+ * ```
122
+ */
123
+ setFallbackTranslations: (fallbackMessages: IntlMessages) => void
124
+
125
+ /**
126
+ * Signal that tracks locale changes
127
+ * Use this signal to reactively update UI when locale changes
128
+ */
129
+ localeChanged: { value: number }
130
+ }
131
+
132
+ /**
133
+ * Default configuration for intl
134
+ */
135
+ export const defaultConfig: IntlConfig = {
136
+ locale: 'en-US',
137
+ messages: {},
138
+ formats: {},
139
+ }
140
+
141
+ /**
142
+ * Global intl service instance
143
+ */
144
+ let intlService: IntlService | null = null
145
+
146
+ /**
147
+ * Create a new intl service instance
148
+ *
149
+ * @param config - intl configuration with locale and messages
150
+ * @returns IntlService instance with formatting methods
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * import { createIntlService } from '@lukso/core/services/intl';
155
+ *
156
+ * const intl = createIntlService({
157
+ * locale: 'en-US',
158
+ * messages: customMessages,
159
+ * });
160
+ *
161
+ * const translated = intl.formatMessage('app.title');
162
+ * const formatted = intl.formatNumber(1234.56);
163
+ * const currentLocale = intl.getLocale(); // 'en-US'
164
+ *
165
+ * // Change locale
166
+ * intl.setLocale('de-DE', germanMessages);
167
+ * console.log(intl.getLocale()); // 'de-DE'
168
+ * ```
169
+ */
170
+ export function createIntlService(
171
+ config: IntlConfig = defaultConfig
172
+ ): IntlService {
173
+ let currentConfig = { ...defaultConfig, ...config }
174
+ let intl = createIntl({ ...currentConfig, defaultLocale: 'en-US' })
175
+ const localeChanged = signal(0)
176
+ let fallbackMessages: IntlMessages = {}
177
+
178
+ return {
179
+ formatMessage: (key: string, options?: Record<string, string>): string => {
180
+ try {
181
+ const result = intl.formatMessage({ id: key }, options)
182
+
183
+ if (result && result !== key) {
184
+ return result
185
+ }
186
+
187
+ // If no translation found, check fallback
188
+ if (fallbackMessages[key]) {
189
+ return fallbackMessages[key]
190
+ }
191
+
192
+ return key
193
+ } catch {
194
+ // On error, try fallback
195
+ return fallbackMessages[key] || key
196
+ }
197
+ },
198
+
199
+ formatNumber: (
200
+ value: number | string | bigint,
201
+ options: FormatNumberOptions = {}
202
+ ): string => {
203
+ if (value === null || value === undefined) {
204
+ return '0'
205
+ }
206
+
207
+ const _value =
208
+ typeof value === 'string' ? Number.parseFloat(value) : value
209
+
210
+ const mergedOptions = {
211
+ ...formatNumberDefaultOptions,
212
+ ...options,
213
+ }
214
+
215
+ return intl.formatNumber(_value as number, mergedOptions) || ''
216
+ },
217
+
218
+ formatDate: (date?: string | number | Date): string => {
219
+ return intl.formatDate(date)
220
+ },
221
+
222
+ formatTime: (date?: string | number | Date): string => {
223
+ return intl.formatTime(date)
224
+ },
225
+
226
+ formatTimestamp: (
227
+ timestamp?: number | string,
228
+ options?: Intl.DateTimeFormatOptions | string
229
+ ): string => {
230
+ if (!timestamp) return ''
231
+
232
+ const time = typeof timestamp === 'string' ? Number(timestamp) : timestamp
233
+ const date =
234
+ time < 10_000_000_000
235
+ ? new Date(time * 1000) // seconds
236
+ : new Date(time) // milliseconds
237
+
238
+ // Handle predefined format
239
+ if (typeof options === 'string') {
240
+ const dateTimeOptions = currentConfig.formats?.date?.[
241
+ options
242
+ ] as Intl.DateTimeFormatOptions
243
+
244
+ if (dateTimeOptions) {
245
+ return new Intl.DateTimeFormat(
246
+ currentConfig.locale,
247
+ dateTimeOptions
248
+ ).format(date)
249
+ }
250
+
251
+ // Fallback to default format if predefined format not found
252
+ return intl.formatDate(date) || ''
253
+ }
254
+
255
+ // For normal @formatjs/intl options
256
+ return intl.formatDate(date, options) || ''
257
+ },
258
+
259
+ setLocale: (locale: string, messages?: IntlMessages): void => {
260
+ currentConfig = {
261
+ ...currentConfig,
262
+ locale,
263
+ messages: messages || currentConfig.messages,
264
+ }
265
+ intl = createIntl({
266
+ ...currentConfig,
267
+ defaultLocale: 'en-US',
268
+ })
269
+ localeChanged.value += 1
270
+ },
271
+
272
+ getLocale: (): string => {
273
+ return currentConfig.locale
274
+ },
275
+
276
+ setFallbackTranslations: (fallbackMessagesInput: IntlMessages): void => {
277
+ fallbackMessages = fallbackMessagesInput
278
+ },
279
+
280
+ localeChanged,
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Set the global intl service
286
+ * Call this once from your app initialization
287
+ *
288
+ * @param service - IntlService instance
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * import { setIntlService, createIntlService } from '@lukso/core/services/intl';
293
+ *
294
+ * const intl = createIntlService(config);
295
+ * setIntlService(intl);
296
+ * ```
297
+ */
298
+ export function setIntlService(service: IntlService): void {
299
+ intlService = service
300
+ }
301
+
302
+ /**
303
+ * Get the current global intl service
304
+ * Returns null if no service has been set
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * import { getIntlService } from '@lukso/core/services/intl';
309
+ *
310
+ * const intl = getIntlService();
311
+ * if (intl) {
312
+ * console.log(intl.getLocale());
313
+ * }
314
+ * ```
315
+ */
316
+ export function getIntlService(): IntlService | null {
317
+ return intlService
318
+ }
319
+
320
+ /**
321
+ * Clear the global intl service
322
+ * Useful for testing or cleanup
323
+ */
324
+ export function clearIntlService(): void {
325
+ intlService = null
326
+ }
@@ -0,0 +1,310 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { BrowserInfo, BrowserName } from '../browserInfo'
3
+ import { browserInfo, EXTENSION_STORE_LINKS } from '../browserInfo'
4
+
5
+ describe('browserInfo', () => {
6
+ describe('browserInfo function', () => {
7
+ it('should return a BrowserInfo object', () => {
8
+ const mockDeviceService = {
9
+ isChrome: true,
10
+ isBrave: false,
11
+ isFirefox: false,
12
+ isSafari: false,
13
+ isEdge: false,
14
+ isOpera: false,
15
+ }
16
+
17
+ const info = browserInfo(mockDeviceService as any)
18
+
19
+ expect(info).toBeDefined()
20
+ expect(typeof info).toBe('object')
21
+ expect(info).toHaveProperty('id')
22
+ expect(info).toHaveProperty('name')
23
+ expect(info).toHaveProperty('icon')
24
+ expect(info).toHaveProperty('storeLink')
25
+ })
26
+
27
+ it('should detect Chrome browser', () => {
28
+ const mockDeviceService = {
29
+ isChrome: true,
30
+ isBrave: false,
31
+ isFirefox: false,
32
+ isSafari: false,
33
+ isEdge: false,
34
+ isOpera: false,
35
+ }
36
+
37
+ const info = browserInfo(mockDeviceService as any)
38
+
39
+ expect(info.id).toBe('chrome')
40
+ expect(info.name).toBe('Chrome')
41
+ expect(info.icon).toBe('logo-chrome')
42
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.chrome)
43
+ })
44
+
45
+ it('should detect Brave browser', () => {
46
+ const mockDeviceService = {
47
+ isChrome: false,
48
+ isBrave: true,
49
+ isFirefox: false,
50
+ isSafari: false,
51
+ isEdge: false,
52
+ isOpera: false,
53
+ }
54
+
55
+ const info = browserInfo(mockDeviceService as any)
56
+
57
+ expect(info.id).toBe('brave')
58
+ expect(info.name).toBe('Brave')
59
+ expect(info.icon).toBe('logo-brave')
60
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.brave)
61
+ })
62
+
63
+ it('should detect Edge browser', () => {
64
+ const mockDeviceService = {
65
+ isChrome: false,
66
+ isBrave: false,
67
+ isFirefox: false,
68
+ isSafari: false,
69
+ isEdge: true,
70
+ isOpera: false,
71
+ }
72
+
73
+ const info = browserInfo(mockDeviceService as any)
74
+
75
+ expect(info.id).toBe('edge')
76
+ expect(info.name).toBe('Edge')
77
+ expect(info.icon).toBe('logo-edge')
78
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.edge)
79
+ })
80
+
81
+ it('should detect Opera browser', () => {
82
+ const mockDeviceService = {
83
+ isChrome: false,
84
+ isBrave: false,
85
+ isFirefox: false,
86
+ isSafari: false,
87
+ isEdge: false,
88
+ isOpera: true,
89
+ }
90
+
91
+ const info = browserInfo(mockDeviceService as any)
92
+
93
+ expect(info.id).toBe('opera')
94
+ expect(info.name).toBe('Opera')
95
+ expect(info.icon).toBe('logo-opera')
96
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.opera)
97
+ })
98
+
99
+ it('should detect Firefox browser', () => {
100
+ const mockDeviceService = {
101
+ isChrome: false,
102
+ isBrave: false,
103
+ isFirefox: true,
104
+ isSafari: false,
105
+ isEdge: false,
106
+ isOpera: false,
107
+ }
108
+
109
+ const info = browserInfo(mockDeviceService as any)
110
+
111
+ expect(info.id).toBe('firefox')
112
+ expect(info.name).toBe('Firefox')
113
+ expect(info.icon).toBe('logo-firefox')
114
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.firefox)
115
+ })
116
+
117
+ it('should detect Safari browser', () => {
118
+ const mockDeviceService = {
119
+ isChrome: false,
120
+ isBrave: false,
121
+ isFirefox: false,
122
+ isSafari: true,
123
+ isEdge: false,
124
+ isOpera: false,
125
+ }
126
+
127
+ const info = browserInfo(mockDeviceService as any)
128
+
129
+ expect(info.id).toBe('safari')
130
+ expect(info.name).toBe('Safari')
131
+ expect(info.icon).toBe('logo-safari')
132
+ expect(info.storeLink).toBe(EXTENSION_STORE_LINKS.safari)
133
+ })
134
+
135
+ it('should prioritize Brave over Chrome', () => {
136
+ const mockDeviceService = {
137
+ isChrome: true,
138
+ isBrave: true,
139
+ isFirefox: false,
140
+ isSafari: false,
141
+ isEdge: false,
142
+ isOpera: false,
143
+ }
144
+
145
+ const info = browserInfo(mockDeviceService as any)
146
+
147
+ expect(info.id).toBe('brave')
148
+ expect(info.name).toBe('Brave')
149
+ })
150
+
151
+ it('should prioritize Edge over Chrome', () => {
152
+ const mockDeviceService = {
153
+ isChrome: true,
154
+ isBrave: false,
155
+ isFirefox: false,
156
+ isSafari: false,
157
+ isEdge: true,
158
+ isOpera: false,
159
+ }
160
+
161
+ const info = browserInfo(mockDeviceService as any)
162
+
163
+ expect(info.id).toBe('edge')
164
+ expect(info.name).toBe('Edge')
165
+ })
166
+
167
+ it('should prioritize Opera over Chrome', () => {
168
+ const mockDeviceService = {
169
+ isChrome: true,
170
+ isBrave: false,
171
+ isFirefox: false,
172
+ isSafari: false,
173
+ isEdge: false,
174
+ isOpera: true,
175
+ }
176
+
177
+ const info = browserInfo(mockDeviceService as any)
178
+
179
+ expect(info.id).toBe('opera')
180
+ expect(info.name).toBe('Opera')
181
+ })
182
+
183
+ it('should return default browser when none detected', () => {
184
+ const mockDeviceService = {
185
+ isChrome: false,
186
+ isBrave: false,
187
+ isFirefox: false,
188
+ isSafari: false,
189
+ isEdge: false,
190
+ isOpera: false,
191
+ }
192
+
193
+ const info = browserInfo(mockDeviceService as any)
194
+
195
+ expect(info.id).toBe('chrome')
196
+ expect(info.name).toBe('')
197
+ expect(info.icon).toBe('')
198
+ })
199
+
200
+ it('should return correct BrowserName type', () => {
201
+ const mockDeviceService = {
202
+ isChrome: true,
203
+ isBrave: false,
204
+ isFirefox: false,
205
+ isSafari: false,
206
+ isEdge: false,
207
+ isOpera: false,
208
+ }
209
+
210
+ const info: BrowserInfo = browserInfo(mockDeviceService as any)
211
+ const validBrowserNames: BrowserName[] = [
212
+ 'chrome',
213
+ 'safari',
214
+ 'firefox',
215
+ 'edge',
216
+ 'opera',
217
+ 'brave',
218
+ ]
219
+
220
+ expect(validBrowserNames).toContain(info.id)
221
+ })
222
+
223
+ it('should merge defaults with detected browser info', () => {
224
+ const mockDeviceService = {
225
+ isChrome: false,
226
+ isBrave: true,
227
+ isFirefox: false,
228
+ isSafari: false,
229
+ isEdge: false,
230
+ isOpera: false,
231
+ }
232
+
233
+ const info = browserInfo(mockDeviceService as any)
234
+
235
+ // Should have all properties from defaults and detected browser
236
+ expect(info).toEqual({
237
+ id: 'brave',
238
+ name: 'Brave',
239
+ icon: 'logo-brave',
240
+ storeLink: EXTENSION_STORE_LINKS.brave,
241
+ })
242
+ })
243
+
244
+ it('should handle multiple calls consistently', () => {
245
+ const mockDeviceService = {
246
+ isChrome: true,
247
+ isBrave: false,
248
+ isFirefox: false,
249
+ isSafari: false,
250
+ isEdge: false,
251
+ isOpera: false,
252
+ }
253
+
254
+ const info1 = browserInfo(mockDeviceService as any)
255
+ const info2 = browserInfo(mockDeviceService as any)
256
+
257
+ expect(info1).toEqual(info2)
258
+ })
259
+
260
+ it('should have all required properties for BrowserInfo type', () => {
261
+ const mockDeviceService = {
262
+ isChrome: true,
263
+ isBrave: false,
264
+ isFirefox: false,
265
+ isSafari: false,
266
+ isEdge: false,
267
+ isOpera: false,
268
+ }
269
+
270
+ const info = browserInfo(mockDeviceService as any)
271
+
272
+ // Verify TypeScript would accept this as BrowserInfo
273
+ const expectedKeys: (keyof BrowserInfo)[] = [
274
+ 'id',
275
+ 'name',
276
+ 'icon',
277
+ 'storeLink',
278
+ ]
279
+ expectedKeys.forEach((key) => {
280
+ expect(info).toHaveProperty(key)
281
+ })
282
+ })
283
+
284
+ it('should detect all browser types independently', () => {
285
+ const browsers = [
286
+ { isChrome: true, expected: 'chrome' },
287
+ { isBrave: true, expected: 'brave' },
288
+ { isEdge: true, expected: 'edge' },
289
+ { isOpera: true, expected: 'opera' },
290
+ { isFirefox: true, expected: 'firefox' },
291
+ { isSafari: true, expected: 'safari' },
292
+ ]
293
+
294
+ browsers.forEach(({ expected, ...detection }) => {
295
+ const mockDeviceService = {
296
+ isChrome: false,
297
+ isBrave: false,
298
+ isFirefox: false,
299
+ isSafari: false,
300
+ isEdge: false,
301
+ isOpera: false,
302
+ ...detection,
303
+ }
304
+
305
+ const info = browserInfo(mockDeviceService as any)
306
+ expect(info.id).toBe(expected)
307
+ })
308
+ })
309
+ })
310
+ })
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { slug } from '../slug'
4
+
5
+ describe('slug', () => {
6
+ it('returns an empty string for undefined or empty input', () => {
7
+ expect(slug()).toBe('')
8
+ // @ts-expect-error
9
+ expect(slug(null)).toBe('')
10
+ expect(slug('')).toBe('')
11
+ })
12
+
13
+ it('converts all characters to lowercase', () => {
14
+ expect(slug('TEST')).toBe('test')
15
+ })
16
+
17
+ it('converts spaces to hyphens', () => {
18
+ expect(slug('some attribute')).toBe('some-attribute')
19
+ })
20
+
21
+ it('keeps same output as input', () => {
22
+ expect(slug('test@slug')).toBe('test@slug')
23
+ expect(slug('-test-slug-')).toBe('-test-slug-')
24
+ expect(slug('test---slug')).toBe('test---slug')
25
+ })
26
+ })