@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@avalix/chroma",
3
3
  "type": "module",
4
- "version": "0.0.10",
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": "latest",
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
+ })