@avalix/chroma 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.d.mts +1712 -45712
- package/package.json +8 -4
- package/src/utils/download-extension.integration.test.ts +95 -0
- package/src/utils/download-extension.test.ts +173 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avalix/chroma",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.11",
|
|
5
5
|
"description": "End-to-end testing library for Polkadot wallet interactions",
|
|
6
6
|
"author": "Avalix Labs",
|
|
7
7
|
"license": "MIT",
|
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
"lint": "eslint --fix .",
|
|
45
45
|
"prepublishOnly": "npm run build",
|
|
46
46
|
"download-extensions": "rm -rf .chroma && tsx scripts/download-extensions.ts",
|
|
47
|
-
"test": "playwright test --ui"
|
|
47
|
+
"test": "playwright test --ui",
|
|
48
|
+
"test:unit": "vitest",
|
|
49
|
+
"test:unit:run": "vitest run",
|
|
50
|
+
"test:unit:coverage": "vitest run --coverage"
|
|
48
51
|
},
|
|
49
52
|
"peerDependencies": {
|
|
50
53
|
"@playwright/test": "^1.57.0"
|
|
@@ -58,8 +61,9 @@
|
|
|
58
61
|
"@types/node": "^24.10.2",
|
|
59
62
|
"@types/unzipper": "^0.10.11",
|
|
60
63
|
"eslint": "^9.39.1",
|
|
61
|
-
"tsdown": "
|
|
62
|
-
"tsx": "^4.21.0"
|
|
64
|
+
"tsdown": "^0.20.1",
|
|
65
|
+
"tsx": "^4.21.0",
|
|
66
|
+
"vitest": "^4.0.18"
|
|
63
67
|
},
|
|
64
68
|
"publishConfig": {
|
|
65
69
|
"access": "public"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { DownloadExtensionOptions } from './download-extension.js'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { downloadAndExtractExtension } from './download-extension.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Integration Tests (real download) - Requires network
|
|
10
|
+
* Tests actual download and extraction with real extension files
|
|
11
|
+
*
|
|
12
|
+
* Important test cases:
|
|
13
|
+
* - Nested zip extraction (Talisman has a zip inside a zip)
|
|
14
|
+
* - Standard zip extraction (Polkadot-JS)
|
|
15
|
+
* - Skip download if already exists
|
|
16
|
+
* - Error handling for invalid URLs
|
|
17
|
+
*/
|
|
18
|
+
describe('downloadAndExtractExtension (integration tests)', () => {
|
|
19
|
+
let tempDir: string
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tempDir = path.join(os.tmpdir(), `chroma-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
23
|
+
await fs.promises.mkdir(tempDir, { recursive: true })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
28
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true })
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should handle nested zip extraction (Talisman)', async () => {
|
|
33
|
+
// Talisman extension has a nested zip structure
|
|
34
|
+
const VERSION = '3.1.13'
|
|
35
|
+
const options: DownloadExtensionOptions = {
|
|
36
|
+
downloadUrl: `https://github.com/avalix-labs/polkadot-wallets/raw/refs/heads/main/talisman/talisman-${VERSION}.zip`,
|
|
37
|
+
extensionName: `talisman-extension-${VERSION}`,
|
|
38
|
+
targetDir: tempDir,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await downloadAndExtractExtension(options)
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(path.join(tempDir, `talisman-extension-${VERSION}`))
|
|
44
|
+
expect(fs.existsSync(result)).toBe(true)
|
|
45
|
+
|
|
46
|
+
// Talisman should have manifest.json after nested extraction
|
|
47
|
+
const files = await fs.promises.readdir(result)
|
|
48
|
+
expect(files).toContain('manifest.json')
|
|
49
|
+
}, 60000)
|
|
50
|
+
|
|
51
|
+
it('should handle standard zip extraction (Polkadot-JS)', async () => {
|
|
52
|
+
const VERSION = '0.62.6'
|
|
53
|
+
const options: DownloadExtensionOptions = {
|
|
54
|
+
downloadUrl: `https://github.com/polkadot-js/extension/releases/download/v${VERSION}/master-chrome-build.zip`,
|
|
55
|
+
extensionName: `polkadot-extension-${VERSION}`,
|
|
56
|
+
targetDir: tempDir,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await downloadAndExtractExtension(options)
|
|
60
|
+
|
|
61
|
+
expect(result).toBe(path.join(tempDir, `polkadot-extension-${VERSION}`))
|
|
62
|
+
expect(fs.existsSync(result)).toBe(true)
|
|
63
|
+
|
|
64
|
+
const files = await fs.promises.readdir(result)
|
|
65
|
+
expect(files).toContain('manifest.json')
|
|
66
|
+
}, 60000)
|
|
67
|
+
|
|
68
|
+
it('should skip download if extension already exists', async () => {
|
|
69
|
+
const extensionDir = path.join(tempDir, 'existing-extension')
|
|
70
|
+
await fs.promises.mkdir(extensionDir, { recursive: true })
|
|
71
|
+
await fs.promises.writeFile(path.join(extensionDir, 'manifest.json'), '{"name": "test"}')
|
|
72
|
+
|
|
73
|
+
const options: DownloadExtensionOptions = {
|
|
74
|
+
downloadUrl: 'https://example.com/should-not-be-called.zip',
|
|
75
|
+
extensionName: 'existing-extension',
|
|
76
|
+
targetDir: tempDir,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await downloadAndExtractExtension(options)
|
|
80
|
+
|
|
81
|
+
expect(result).toBe(extensionDir)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should throw error for invalid URL', async () => {
|
|
85
|
+
const options: DownloadExtensionOptions = {
|
|
86
|
+
downloadUrl: 'https://github.com/invalid-user-12345/nonexistent-repo/releases/download/v0.0.0/nonexistent.zip',
|
|
87
|
+
extensionName: 'invalid-extension',
|
|
88
|
+
targetDir: tempDir,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await expect(downloadAndExtractExtension(options)).rejects.toThrow(
|
|
92
|
+
/Failed to download\/extract invalid-extension/,
|
|
93
|
+
)
|
|
94
|
+
}, 15000)
|
|
95
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { DownloadExtensionOptions } from './download-extension.js'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { downloadAndExtractExtension } from './download-extension.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unit Tests (with mocks) - Fast, no network required
|
|
9
|
+
* Tests error handling, cleanup logic, and path resolution
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Mock adm-zip
|
|
13
|
+
const mockExtractAllTo = vi.fn()
|
|
14
|
+
vi.mock('adm-zip', () => {
|
|
15
|
+
return {
|
|
16
|
+
default: vi.fn().mockImplementation(() => ({
|
|
17
|
+
extractAllTo: mockExtractAllTo,
|
|
18
|
+
})),
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Mock fetch
|
|
23
|
+
const mockFetch = vi.fn()
|
|
24
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
25
|
+
|
|
26
|
+
// Mock stream/promises pipeline
|
|
27
|
+
vi.mock('node:stream/promises', () => ({
|
|
28
|
+
pipeline: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
// Mock fs module
|
|
32
|
+
vi.mock('node:fs', async () => {
|
|
33
|
+
const actual = await vi.importActual<typeof import('node:fs')>('node:fs')
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
default: {
|
|
37
|
+
...actual,
|
|
38
|
+
existsSync: vi.fn(),
|
|
39
|
+
readdirSync: vi.fn(),
|
|
40
|
+
promises: {
|
|
41
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
rm: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
rename: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
unlink: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
readdir: vi.fn().mockResolvedValue([]),
|
|
46
|
+
},
|
|
47
|
+
createWriteStream: vi.fn().mockReturnValue({
|
|
48
|
+
on: vi.fn(),
|
|
49
|
+
write: vi.fn(),
|
|
50
|
+
end: vi.fn(),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
existsSync: vi.fn(),
|
|
54
|
+
readdirSync: vi.fn(),
|
|
55
|
+
createWriteStream: vi.fn().mockReturnValue({
|
|
56
|
+
on: vi.fn(),
|
|
57
|
+
write: vi.fn(),
|
|
58
|
+
end: vi.fn(),
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('downloadAndExtractExtension (unit tests)', () => {
|
|
64
|
+
const mockOptions: DownloadExtensionOptions = {
|
|
65
|
+
downloadUrl: 'https://example.com/extension.zip',
|
|
66
|
+
extensionName: 'test-extension',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
vi.clearAllMocks()
|
|
71
|
+
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
vi.restoreAllMocks()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('when extension already exists', () => {
|
|
79
|
+
it('should skip download and return existing path', async () => {
|
|
80
|
+
const mockedFs = vi.mocked(fs)
|
|
81
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
82
|
+
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
83
|
+
|
|
84
|
+
const result = await downloadAndExtractExtension(mockOptions)
|
|
85
|
+
|
|
86
|
+
expect(result).toContain('test-extension')
|
|
87
|
+
expect(mockFetch).not.toHaveBeenCalled()
|
|
88
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
89
|
+
expect.stringContaining('already exists'),
|
|
90
|
+
expect.any(String),
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('when extension does not exist', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
const mockedFs = vi.mocked(fs)
|
|
98
|
+
mockedFs.existsSync.mockReturnValue(false)
|
|
99
|
+
mockedFs.readdirSync.mockReturnValue([])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should throw error when download fails with bad status', async () => {
|
|
103
|
+
mockFetch.mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 404,
|
|
106
|
+
statusText: 'Not Found',
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await expect(downloadAndExtractExtension(mockOptions)).rejects.toThrow(
|
|
110
|
+
'Failed to download/extract test-extension',
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should throw error when fetch throws network error', async () => {
|
|
115
|
+
mockFetch.mockRejectedValue(new Error('Network error'))
|
|
116
|
+
|
|
117
|
+
await expect(downloadAndExtractExtension(mockOptions)).rejects.toThrow(
|
|
118
|
+
'Failed to download/extract test-extension: Network error',
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should cleanup files on error', async () => {
|
|
123
|
+
const mockedFs = vi.mocked(fs)
|
|
124
|
+
|
|
125
|
+
mockFetch.mockResolvedValue({
|
|
126
|
+
ok: false,
|
|
127
|
+
status: 500,
|
|
128
|
+
statusText: 'Internal Server Error',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
mockedFs.existsSync.mockImplementation((p: fs.PathLike) => {
|
|
132
|
+
const pathStr = p.toString()
|
|
133
|
+
if (pathStr.endsWith('test-extension') && !pathStr.includes('.zip')) {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
return true
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await expect(downloadAndExtractExtension(mockOptions)).rejects.toThrow()
|
|
140
|
+
|
|
141
|
+
expect(mockedFs.promises.unlink).toHaveBeenCalled()
|
|
142
|
+
expect(mockedFs.promises.rm).toHaveBeenCalled()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('targetDir option', () => {
|
|
147
|
+
it('should use custom targetDir when provided', async () => {
|
|
148
|
+
const mockedFs = vi.mocked(fs)
|
|
149
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
150
|
+
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
151
|
+
|
|
152
|
+
const customOptions: DownloadExtensionOptions = {
|
|
153
|
+
...mockOptions,
|
|
154
|
+
targetDir: '/custom/path',
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = await downloadAndExtractExtension(customOptions)
|
|
158
|
+
|
|
159
|
+
expect(result).toBe(path.join('/custom/path', 'test-extension'))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should use default .chroma directory when targetDir not provided', async () => {
|
|
163
|
+
const mockedFs = vi.mocked(fs)
|
|
164
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
165
|
+
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
166
|
+
|
|
167
|
+
const result = await downloadAndExtractExtension(mockOptions)
|
|
168
|
+
|
|
169
|
+
expect(result).toContain('.chroma')
|
|
170
|
+
expect(result).toContain('test-extension')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|