@gravito/cosmos 3.1.0 → 3.2.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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @file packages/cosmos/tests/unit/memory-loader.test.ts
3
+ * @description MemoryLoader 單元測試
4
+ */
5
+
6
+ import { describe, expect, it } from 'bun:test'
7
+ import { MemoryLoader } from '../../src/loaders/MemoryLoader'
8
+
9
+ describe('MemoryLoader', () => {
10
+ it('should load translations from memory', async () => {
11
+ const loader = new MemoryLoader({
12
+ translations: {
13
+ en: { hello: 'Hello', welcome: 'Welcome' },
14
+ 'zh-TW': { hello: '你好', welcome: '歡迎' },
15
+ },
16
+ })
17
+
18
+ const en = await loader.load('en')
19
+ expect(en).toEqual({ hello: 'Hello', welcome: 'Welcome' })
20
+
21
+ const zhTW = await loader.load('zh-TW')
22
+ expect(zhTW).toEqual({ hello: '你好', welcome: '歡迎' })
23
+ })
24
+
25
+ it('should return null for missing locale', async () => {
26
+ const loader = new MemoryLoader({
27
+ translations: { en: { hello: 'Hello' } },
28
+ })
29
+
30
+ const result = await loader.load('fr')
31
+ expect(result).toBeNull()
32
+ })
33
+
34
+ it('should support fallback chain', async () => {
35
+ const primary = new MemoryLoader({
36
+ translations: { en: { hello: 'Hello' } },
37
+ })
38
+
39
+ const fallback = new MemoryLoader({
40
+ translations: { fr: { hello: 'Bonjour' } },
41
+ })
42
+
43
+ primary.fallback(fallback)
44
+
45
+ const result = await primary.load('fr')
46
+ expect(result).toEqual({ hello: 'Bonjour' })
47
+ })
48
+
49
+ it('should allow adding translations dynamically', async () => {
50
+ const loader = new MemoryLoader({
51
+ translations: { en: { hello: 'Hello' } },
52
+ })
53
+
54
+ loader.addTranslations('fr', { hello: 'Bonjour' })
55
+
56
+ const result = await loader.load('fr')
57
+ expect(result).toEqual({ hello: 'Bonjour' })
58
+ })
59
+
60
+ it('should merge when adding translations to existing locale', async () => {
61
+ const loader = new MemoryLoader({
62
+ translations: { en: { hello: 'Hello' } },
63
+ })
64
+
65
+ loader.addTranslations('en', { goodbye: 'Goodbye' })
66
+
67
+ const result = await loader.load('en')
68
+ expect(result).toEqual({ hello: 'Hello', goodbye: 'Goodbye' })
69
+ })
70
+
71
+ it('should allow removing translations', async () => {
72
+ const loader = new MemoryLoader({
73
+ translations: {
74
+ en: { hello: 'Hello' },
75
+ fr: { hello: 'Bonjour' },
76
+ },
77
+ })
78
+
79
+ loader.removeTranslations('fr')
80
+
81
+ const result = await loader.load('fr')
82
+ expect(result).toBeNull()
83
+ })
84
+
85
+ it('should return loaded locales', () => {
86
+ const loader = new MemoryLoader({
87
+ translations: {
88
+ en: { hello: 'Hello' },
89
+ 'zh-TW': { hello: '你好' },
90
+ fr: { hello: 'Bonjour' },
91
+ },
92
+ })
93
+
94
+ const locales = loader.getLoadedLocales()
95
+ expect(locales).toContain('en')
96
+ expect(locales).toContain('zh-TW')
97
+ expect(locales).toContain('fr')
98
+ expect(locales.length).toBe(3)
99
+ })
100
+
101
+ it('should check if locale exists', () => {
102
+ const loader = new MemoryLoader({
103
+ translations: { en: { hello: 'Hello' } },
104
+ })
105
+
106
+ expect(loader.hasLocale('en')).toBe(true)
107
+ expect(loader.hasLocale('fr')).toBe(false)
108
+ })
109
+
110
+ it('should handle nested translations', async () => {
111
+ const loader = new MemoryLoader({
112
+ translations: {
113
+ en: {
114
+ auth: {
115
+ login: 'Login',
116
+ logout: 'Logout',
117
+ },
118
+ },
119
+ },
120
+ })
121
+
122
+ const result = await loader.load('en')
123
+ expect(result).toEqual({
124
+ auth: {
125
+ login: 'Login',
126
+ logout: 'Logout',
127
+ },
128
+ })
129
+ })
130
+ })
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @file packages/cosmos/tests/unit/path-utils.test.ts
3
+ * @description Path utils 單元測試
4
+ */
5
+
6
+ import { describe, expect, it } from 'bun:test'
7
+ import { pathUtils } from '../../src/runtime/path-utils'
8
+
9
+ describe('Path Utils', () => {
10
+ describe('join', () => {
11
+ it('should join path segments', () => {
12
+ expect(pathUtils.join('foo', 'bar', 'baz.txt')).toBe('foo/bar/baz.txt')
13
+ })
14
+
15
+ it('should handle leading slashes', () => {
16
+ expect(pathUtils.join('/foo', 'bar')).toBe('/foo/bar')
17
+ })
18
+
19
+ it('should handle trailing slashes', () => {
20
+ expect(pathUtils.join('foo/', 'bar/')).toBe('foo/bar')
21
+ })
22
+
23
+ it('should filter empty segments', () => {
24
+ expect(pathUtils.join('', 'foo', '', 'bar')).toBe('foo/bar')
25
+ })
26
+
27
+ it('should handle multiple slashes', () => {
28
+ expect(pathUtils.join('foo//bar///baz')).toBe('foo/bar/baz')
29
+ })
30
+ })
31
+
32
+ describe('basename', () => {
33
+ it('should return base name', () => {
34
+ expect(pathUtils.basename('/foo/bar/baz.txt')).toBe('baz.txt')
35
+ })
36
+
37
+ it('should remove extension if provided', () => {
38
+ expect(pathUtils.basename('/foo/bar/baz.txt', '.txt')).toBe('baz')
39
+ })
40
+
41
+ it('should handle trailing slash', () => {
42
+ expect(pathUtils.basename('/foo/bar/')).toBe('bar')
43
+ })
44
+
45
+ it('should handle root path', () => {
46
+ expect(pathUtils.basename('/')).toBe('')
47
+ })
48
+ })
49
+
50
+ describe('dirname', () => {
51
+ it('should return directory name', () => {
52
+ expect(pathUtils.dirname('/foo/bar/baz.txt')).toBe('/foo/bar')
53
+ })
54
+
55
+ it('should handle root path', () => {
56
+ expect(pathUtils.dirname('/foo')).toBe('/')
57
+ })
58
+
59
+ it('should handle relative path', () => {
60
+ expect(pathUtils.dirname('foo/bar')).toBe('foo')
61
+ })
62
+
63
+ it('should handle single segment', () => {
64
+ expect(pathUtils.dirname('foo')).toBe('.')
65
+ })
66
+ })
67
+
68
+ describe('extname', () => {
69
+ it('should return file extension', () => {
70
+ expect(pathUtils.extname('index.html')).toBe('.html')
71
+ })
72
+
73
+ it('should handle multiple dots', () => {
74
+ expect(pathUtils.extname('index.coffee.md')).toBe('.md')
75
+ })
76
+
77
+ it('should handle dot at end', () => {
78
+ expect(pathUtils.extname('index.')).toBe('.')
79
+ })
80
+
81
+ it('should return empty for no extension', () => {
82
+ expect(pathUtils.extname('index')).toBe('')
83
+ })
84
+
85
+ it('should handle hidden files', () => {
86
+ // .gitignore 整個被視為副檔名
87
+ expect(pathUtils.extname('.gitignore')).toBe('.gitignore')
88
+ })
89
+ })
90
+
91
+ describe('isAbsolute', () => {
92
+ it('should detect Unix absolute path', () => {
93
+ expect(pathUtils.isAbsolute('/foo/bar')).toBe(true)
94
+ })
95
+
96
+ it('should detect relative path', () => {
97
+ expect(pathUtils.isAbsolute('foo/bar')).toBe(false)
98
+ })
99
+
100
+ it('should detect Windows absolute path', () => {
101
+ expect(pathUtils.isAbsolute('C:/foo/bar')).toBe(true)
102
+ expect(pathUtils.isAbsolute('D:\\foo\\bar')).toBe(true)
103
+ })
104
+ })
105
+
106
+ describe('normalize', () => {
107
+ it('should normalize path with dots', () => {
108
+ expect(pathUtils.normalize('/foo/bar//baz/./qux/../quux')).toBe('/foo/bar/baz/quux')
109
+ })
110
+
111
+ it('should handle relative path with ..', () => {
112
+ expect(pathUtils.normalize('foo/../bar')).toBe('bar')
113
+ })
114
+
115
+ it('should preserve leading .. in relative paths', () => {
116
+ expect(pathUtils.normalize('../foo/bar')).toBe('../foo/bar')
117
+ })
118
+
119
+ it('should remove . segments', () => {
120
+ expect(pathUtils.normalize('./foo/./bar/.')).toBe('foo/bar')
121
+ })
122
+
123
+ it('should handle absolute path', () => {
124
+ expect(pathUtils.normalize('/foo/bar')).toBe('/foo/bar')
125
+ })
126
+
127
+ it('should return . for empty normalized relative path', () => {
128
+ expect(pathUtils.normalize('foo/..')).toBe('.')
129
+ })
130
+
131
+ it('should return / for empty normalized absolute path', () => {
132
+ expect(pathUtils.normalize('/foo/..')).toBe('/')
133
+ })
134
+ })
135
+ })
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @file packages/cosmos/tests/unit/runtime-detector.test.ts
3
+ * @description Runtime detector 單元測試
4
+ */
5
+
6
+ import { describe, expect, it } from 'bun:test'
7
+ import { detectRuntime, isEdge, isNode } from '../../src/runtime/detector'
8
+
9
+ describe('Runtime Detector', () => {
10
+ it('should detect node runtime', () => {
11
+ const runtime = detectRuntime()
12
+ // Bun 環境會被檢測為 node-compatible
13
+ expect(runtime).toBe('node')
14
+ })
15
+
16
+ it('should return true for isNode()', () => {
17
+ expect(isNode()).toBe(true)
18
+ })
19
+
20
+ it('should return false for isEdge()', () => {
21
+ expect(isEdge()).toBe(false)
22
+ })
23
+
24
+ // 以下測試模擬 Edge 環境 (需要 mock globalThis)
25
+ it('should detect Cloudflare Workers', () => {
26
+ const originalCaches = globalThis.caches
27
+ const originalWSP = (globalThis as any).WebSocketPair
28
+
29
+ // Mock Cloudflare Workers 環境
30
+ ;(globalThis as any).caches = {}
31
+ ;(globalThis as any).WebSocketPair = class {}
32
+
33
+ const runtime = detectRuntime()
34
+
35
+ // Restore
36
+ if (originalCaches) {
37
+ globalThis.caches = originalCaches
38
+ } else {
39
+ delete (globalThis as any).caches
40
+ }
41
+
42
+ if (originalWSP) {
43
+ ;(globalThis as any).WebSocketPair = originalWSP
44
+ } else {
45
+ delete (globalThis as any).WebSocketPair
46
+ }
47
+
48
+ expect(runtime).toBe('edge')
49
+ })
50
+
51
+ it('should detect Vercel Edge Runtime', () => {
52
+ const originalEdgeRuntime = (globalThis as any).EdgeRuntime
53
+
54
+ // Mock Vercel Edge Runtime 環境
55
+ ;(globalThis as any).EdgeRuntime = 'vercel'
56
+
57
+ const runtime = detectRuntime()
58
+
59
+ // Restore
60
+ if (originalEdgeRuntime !== undefined) {
61
+ ;(globalThis as any).EdgeRuntime = originalEdgeRuntime
62
+ } else {
63
+ delete (globalThis as any).EdgeRuntime
64
+ }
65
+
66
+ expect(runtime).toBe('edge')
67
+ })
68
+
69
+ it('should detect Deno', () => {
70
+ const originalDeno = (globalThis as any).Deno
71
+
72
+ // Mock Deno 環境
73
+ ;(globalThis as any).Deno = { version: { deno: '1.0.0' } }
74
+
75
+ const runtime = detectRuntime()
76
+
77
+ // Restore
78
+ if (originalDeno !== undefined) {
79
+ ;(globalThis as any).Deno = originalDeno
80
+ } else {
81
+ delete (globalThis as any).Deno
82
+ }
83
+
84
+ expect(runtime).toBe('edge')
85
+ })
86
+ })