@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.
- package/LICENSE +201 -0
- package/README.md +159 -0
- package/dist/chunk-3WGYJTN4.js +19 -0
- package/dist/chunk-3WGYJTN4.js.map +1 -0
- package/dist/chunk-4TNWG4ME.js +106 -0
- package/dist/chunk-4TNWG4ME.js.map +1 -0
- package/dist/chunk-AMRGSLR5.cjs +1 -0
- package/dist/chunk-AMRGSLR5.cjs.map +1 -0
- package/dist/chunk-CC3LFUYY.cjs +19 -0
- package/dist/chunk-CC3LFUYY.cjs.map +1 -0
- package/dist/chunk-DFMMMF62.cjs +1 -0
- package/dist/chunk-DFMMMF62.cjs.map +1 -0
- package/dist/chunk-DKEXQFNE.js +1 -0
- package/dist/chunk-DKEXQFNE.js.map +1 -0
- package/dist/chunk-DKXHVRHM.js +84 -0
- package/dist/chunk-DKXHVRHM.js.map +1 -0
- package/dist/chunk-FR74YPGJ.cjs +87 -0
- package/dist/chunk-FR74YPGJ.cjs.map +1 -0
- package/dist/chunk-LEL6VWU4.js +1 -0
- package/dist/chunk-LEL6VWU4.js.map +1 -0
- package/dist/chunk-MBIRTPNM.cjs +84 -0
- package/dist/chunk-MBIRTPNM.cjs.map +1 -0
- package/dist/chunk-NJQVWIZL.cjs +49 -0
- package/dist/chunk-NJQVWIZL.cjs.map +1 -0
- package/dist/chunk-RM42NG7E.cjs +106 -0
- package/dist/chunk-RM42NG7E.cjs.map +1 -0
- package/dist/chunk-SV4TVR2K.js +87 -0
- package/dist/chunk-SV4TVR2K.js.map +1 -0
- package/dist/chunk-X2QNFZU7.js +49 -0
- package/dist/chunk-X2QNFZU7.js.map +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/mixins/device.cjs +8 -0
- package/dist/mixins/device.cjs.map +1 -0
- package/dist/mixins/device.d.cts +34 -0
- package/dist/mixins/device.d.ts +34 -0
- package/dist/mixins/device.js +8 -0
- package/dist/mixins/device.js.map +1 -0
- package/dist/mixins/index.cjs +14 -0
- package/dist/mixins/index.cjs.map +1 -0
- package/dist/mixins/index.d.cts +3 -0
- package/dist/mixins/index.d.ts +3 -0
- package/dist/mixins/index.js +14 -0
- package/dist/mixins/index.js.map +1 -0
- package/dist/mixins/intl.cjs +8 -0
- package/dist/mixins/intl.cjs.map +1 -0
- package/dist/mixins/intl.d.cts +37 -0
- package/dist/mixins/intl.d.ts +37 -0
- package/dist/mixins/intl.js +8 -0
- package/dist/mixins/intl.js.map +1 -0
- package/dist/services/device.cjs +7 -0
- package/dist/services/device.cjs.map +1 -0
- package/dist/services/device.d.cts +71 -0
- package/dist/services/device.d.ts +71 -0
- package/dist/services/device.js +7 -0
- package/dist/services/device.js.map +1 -0
- package/dist/services/index.cjs +20 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.cts +4 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +20 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/intl.cjs +15 -0
- package/dist/services/intl.cjs.map +1 -0
- package/dist/services/intl.d.cts +170 -0
- package/dist/services/intl.d.ts +170 -0
- package/dist/services/intl.js +15 -0
- package/dist/services/intl.js.map +1 -0
- package/dist/utils/index.cjs +11 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +32 -0
- package/dist/utils/index.d.ts +32 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +114 -0
- package/src/index.ts +10 -0
- package/src/mixins/__tests__/device.spec.ts +119 -0
- package/src/mixins/__tests__/intl.spec.ts +198 -0
- package/src/mixins/device.ts +48 -0
- package/src/mixins/index.ts +8 -0
- package/src/mixins/intl.ts +112 -0
- package/src/services/__tests__/device.spec.ts +36 -0
- package/src/services/__tests__/intl.spec.ts +536 -0
- package/src/services/device.ts +121 -0
- package/src/services/index.ts +17 -0
- package/src/services/intl.ts +326 -0
- package/src/utils/__tests__/browserInfo.spec.ts +310 -0
- package/src/utils/__tests__/slug.spec.ts +26 -0
- package/src/utils/browserInfo.ts +102 -0
- package/src/utils/index.ts +4 -0
- 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
|
+
})
|