@membranehq/cli 0.1.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.
- package/.turbo/turbo-build.log +9 -0
- package/CHANGELOG.md +7 -0
- package/README.md +54 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +335 -0
- package/package.json +46 -0
- package/scripts/add-shebang.sh +6 -0
- package/scripts/prepare-package-json.ts +29 -0
- package/src/agent.tsx +50 -0
- package/src/cli.ts +72 -0
- package/src/commands/open.command.ts +51 -0
- package/src/commands/pull.command.ts +75 -0
- package/src/commands/push.command.ts +79 -0
- package/src/commands/test.command.ts +99 -0
- package/src/components/AddMcpServerScreen.tsx +215 -0
- package/src/components/AgentStatus.tsx +15 -0
- package/src/components/Main.tsx +64 -0
- package/src/components/OverviewSection.tsx +24 -0
- package/src/components/PersonalAccessTokenInput.tsx +56 -0
- package/src/components/RecentChanges.tsx +65 -0
- package/src/components/SelectWorkspace.tsx +112 -0
- package/src/components/Setup.tsx +121 -0
- package/src/components/WorkspaceStatus.tsx +61 -0
- package/src/contexts/FileWatcherContext.tsx +81 -0
- package/src/index.ts +27 -0
- package/src/legacy/commands/pullWorkspace.ts +70 -0
- package/src/legacy/commands/pushWorkspace.ts +246 -0
- package/src/legacy/integrationElements.ts +78 -0
- package/src/legacy/push/types.ts +17 -0
- package/src/legacy/reader/index.ts +113 -0
- package/src/legacy/types.ts +17 -0
- package/src/legacy/util.ts +149 -0
- package/src/legacy/workspace-elements/connectors.ts +397 -0
- package/src/legacy/workspace-elements/index.ts +27 -0
- package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
- package/src/legacy/workspace-tools/integrationElements.ts +78 -0
- package/src/legacy/workspace-tools/util.ts +149 -0
- package/src/mcp/server-status.ts +27 -0
- package/src/mcp/server.ts +36 -0
- package/src/mcp/tools/getTestAccessToken.ts +32 -0
- package/src/modules/api/account-api-client.ts +89 -0
- package/src/modules/api/index.ts +3 -0
- package/src/modules/api/membrane-api-client.ts +116 -0
- package/src/modules/api/workspace-api-client.ts +11 -0
- package/src/modules/config/cwd-context.tsx +11 -0
- package/src/modules/config/project/getAgentVersion.ts +16 -0
- package/src/modules/config/project/index.ts +8 -0
- package/src/modules/config/project/paths.ts +25 -0
- package/src/modules/config/project/readProjectConfig.ts +27 -0
- package/src/modules/config/project/useProjectConfig.tsx +103 -0
- package/src/modules/config/system/index.ts +35 -0
- package/src/modules/file-watcher/index.ts +166 -0
- package/src/modules/file-watcher/types.ts +14 -0
- package/src/modules/setup/steps.ts +9 -0
- package/src/modules/setup/useSetup.ts +16 -0
- package/src/modules/status/useStatus.ts +16 -0
- package/src/modules/workspace-element-service/constants.ts +121 -0
- package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
- package/src/modules/workspace-element-service/index.ts +304 -0
- package/src/testing/environment.ts +172 -0
- package/src/testing/runners/base.runner.ts +27 -0
- package/src/testing/runners/test.runner.ts +123 -0
- package/src/testing/scripts/generate-test-report.ts +757 -0
- package/src/testing/test-suites/base.ts +92 -0
- package/src/testing/test-suites/data-collection.ts +128 -0
- package/src/testing/testers/base.ts +115 -0
- package/src/testing/testers/create.ts +273 -0
- package/src/testing/testers/delete.ts +155 -0
- package/src/testing/testers/find-by-id.ts +135 -0
- package/src/testing/testers/list.ts +110 -0
- package/src/testing/testers/match.ts +149 -0
- package/src/testing/testers/search.ts +148 -0
- package/src/testing/testers/spec.ts +30 -0
- package/src/testing/testers/update.ts +284 -0
- package/src/utils/auth.ts +19 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/fields.ts +83 -0
- package/src/utils/logger.ts +106 -0
- package/src/utils/templating.ts +50 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import yaml from 'js-yaml'
|
|
5
|
+
|
|
6
|
+
interface TestNode {
|
|
7
|
+
name: string
|
|
8
|
+
path: string
|
|
9
|
+
content?: string
|
|
10
|
+
parsedContent?: unknown
|
|
11
|
+
children: TestNode[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TestResult {
|
|
15
|
+
logs: any[]
|
|
16
|
+
assertions: TestAssertion[]
|
|
17
|
+
error?: any
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TestAssertion {
|
|
21
|
+
message: string
|
|
22
|
+
result: boolean
|
|
23
|
+
details?: any
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TestSummary {
|
|
27
|
+
totalDataCollections: number
|
|
28
|
+
fullyFunctional: number
|
|
29
|
+
partiallyFunctional: number
|
|
30
|
+
failed: number
|
|
31
|
+
untested: number
|
|
32
|
+
// Test-level statistics
|
|
33
|
+
totalTests: number
|
|
34
|
+
successfulTests: number
|
|
35
|
+
warningTests: number
|
|
36
|
+
errorTests: number
|
|
37
|
+
untestedTests: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function calculateTestStatus(node: TestNode): 'success' | 'warning' | 'error' | 'untested' {
|
|
41
|
+
if (!node.parsedContent || typeof node.parsedContent !== 'object') {
|
|
42
|
+
return 'untested'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = node.parsedContent as any
|
|
46
|
+
const result = parsed.result as TestResult | undefined
|
|
47
|
+
|
|
48
|
+
if (!result) {
|
|
49
|
+
return 'untested'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.error) {
|
|
53
|
+
return 'error'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!result.assertions || result.assertions.length === 0) {
|
|
57
|
+
return 'untested'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hasFailures = result.assertions.some((assertion: TestAssertion) => !assertion.result)
|
|
61
|
+
const allPassed = result.assertions.every((assertion: TestAssertion) => assertion.result)
|
|
62
|
+
|
|
63
|
+
if (allPassed) {
|
|
64
|
+
return 'success'
|
|
65
|
+
} else if (hasFailures) {
|
|
66
|
+
return 'warning'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'untested'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calculateDataCollectionSummary(nodes: TestNode[]): TestSummary {
|
|
73
|
+
let totalDataCollections = 0
|
|
74
|
+
let fullyFunctional = 0
|
|
75
|
+
let partiallyFunctional = 0
|
|
76
|
+
let failed = 0
|
|
77
|
+
let untested = 0
|
|
78
|
+
let totalTests = 0
|
|
79
|
+
let successfulTests = 0
|
|
80
|
+
let warningTests = 0
|
|
81
|
+
let errorTests = 0
|
|
82
|
+
let untestedTests = 0
|
|
83
|
+
|
|
84
|
+
function processNodes(nodeList: TestNode[]) {
|
|
85
|
+
for (const node of nodeList) {
|
|
86
|
+
if (node.children.length > 0) {
|
|
87
|
+
// Check if this is a data collection folder (has test files)
|
|
88
|
+
const hasTestFiles = node.children.some(
|
|
89
|
+
(child) => child.name.endsWith('.test.yml') || child.name.endsWith('.test.yaml'),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (hasTestFiles) {
|
|
93
|
+
totalDataCollections++
|
|
94
|
+
|
|
95
|
+
const testResults = node.children
|
|
96
|
+
.filter((child) => child.name.endsWith('.test.yml') || child.name.endsWith('.test.yaml'))
|
|
97
|
+
.map((child) => calculateTestStatus(child))
|
|
98
|
+
|
|
99
|
+
totalTests += testResults.length
|
|
100
|
+
const successCount = testResults.filter((status) => status === 'success').length
|
|
101
|
+
const errorCount = testResults.filter((status) => status === 'error').length
|
|
102
|
+
const warningCount = testResults.filter((status) => status === 'warning').length
|
|
103
|
+
const untestedCount = testResults.filter((status) => status === 'untested').length
|
|
104
|
+
|
|
105
|
+
successfulTests += successCount
|
|
106
|
+
errorTests += errorCount
|
|
107
|
+
warningTests += warningCount
|
|
108
|
+
untestedTests += untestedCount
|
|
109
|
+
|
|
110
|
+
if (errorCount > 0) {
|
|
111
|
+
failed++
|
|
112
|
+
} else if (untestedCount > 0) {
|
|
113
|
+
untested++
|
|
114
|
+
} else if (warningCount > 0) {
|
|
115
|
+
partiallyFunctional++
|
|
116
|
+
} else if (successCount > 0) {
|
|
117
|
+
fullyFunctional++
|
|
118
|
+
} else {
|
|
119
|
+
untested++
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
processNodes(node.children)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
processNodes(nodes)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
totalDataCollections,
|
|
132
|
+
fullyFunctional,
|
|
133
|
+
partiallyFunctional,
|
|
134
|
+
failed,
|
|
135
|
+
untested,
|
|
136
|
+
totalTests,
|
|
137
|
+
successfulTests,
|
|
138
|
+
warningTests,
|
|
139
|
+
errorTests,
|
|
140
|
+
untestedTests,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildTestTree(dir: string, basePath: string = ''): TestNode[] {
|
|
145
|
+
const nodes: TestNode[] = []
|
|
146
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
147
|
+
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const fullPath = path.join(dir, entry.name)
|
|
150
|
+
const relativePath = path.join(basePath, entry.name)
|
|
151
|
+
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
nodes.push({
|
|
154
|
+
name: entry.name,
|
|
155
|
+
path: relativePath,
|
|
156
|
+
children: buildTestTree(fullPath, relativePath),
|
|
157
|
+
})
|
|
158
|
+
} else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
159
|
+
const content = fs.readFileSync(fullPath, 'utf8')
|
|
160
|
+
let parsedContent: unknown
|
|
161
|
+
try {
|
|
162
|
+
parsedContent = yaml.load(content)
|
|
163
|
+
console.warn(`Successfully loaded YAML file: ${fullPath}`)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`Error parsing YAML file ${fullPath}:`, error)
|
|
166
|
+
}
|
|
167
|
+
nodes.push({
|
|
168
|
+
name: entry.name,
|
|
169
|
+
path: relativePath,
|
|
170
|
+
content,
|
|
171
|
+
parsedContent,
|
|
172
|
+
children: [],
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return nodes.sort((a, b) => {
|
|
178
|
+
// Directories first, then files
|
|
179
|
+
if (a.children.length > 0 && b.children.length === 0) return -1
|
|
180
|
+
if (a.children.length === 0 && b.children.length > 0) return 1
|
|
181
|
+
return a.name.localeCompare(b.name)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let folderIdCounter = 0
|
|
186
|
+
|
|
187
|
+
function getStatusIcon(status: string): string {
|
|
188
|
+
switch (status) {
|
|
189
|
+
case 'success':
|
|
190
|
+
return '✅'
|
|
191
|
+
case 'warning':
|
|
192
|
+
return '⚠️'
|
|
193
|
+
case 'error':
|
|
194
|
+
return '❌'
|
|
195
|
+
case 'untested':
|
|
196
|
+
return '⭕'
|
|
197
|
+
default:
|
|
198
|
+
return '📄'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getStatusClass(status: string): string {
|
|
203
|
+
switch (status) {
|
|
204
|
+
case 'success':
|
|
205
|
+
return 'status-success'
|
|
206
|
+
case 'warning':
|
|
207
|
+
return 'status-warning'
|
|
208
|
+
case 'error':
|
|
209
|
+
return 'status-error'
|
|
210
|
+
case 'untested':
|
|
211
|
+
return 'status-untested'
|
|
212
|
+
default:
|
|
213
|
+
return ''
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function generateTreeHtml(nodes: TestNode[], parentId = 'root', depth = 0): string {
|
|
218
|
+
return nodes
|
|
219
|
+
.map((node) => {
|
|
220
|
+
const hasChildren = node.children.length > 0
|
|
221
|
+
const isYaml = node.name.endsWith('.yaml') || node.name.endsWith('.yml')
|
|
222
|
+
const isTestYaml = node.name.endsWith('.test.yaml') || node.name.endsWith('.test.yml')
|
|
223
|
+
let content = ''
|
|
224
|
+
let folderId = ''
|
|
225
|
+
if (hasChildren) {
|
|
226
|
+
folderId = `folder-${parentId}-${folderIdCounter++}`
|
|
227
|
+
content = `
|
|
228
|
+
<div id="${folderId}" class="folder-children" style="display: none;">
|
|
229
|
+
${generateTreeHtml(node.children, folderId, depth + 1)}
|
|
230
|
+
</div>
|
|
231
|
+
`
|
|
232
|
+
}
|
|
233
|
+
let displayName = node.name
|
|
234
|
+
let icon = ''
|
|
235
|
+
let statusClass = ''
|
|
236
|
+
|
|
237
|
+
if (hasChildren) {
|
|
238
|
+
icon = '📁'
|
|
239
|
+
} else if (isTestYaml) {
|
|
240
|
+
displayName = node.name.replace(/\.test\.ya?ml$/, '')
|
|
241
|
+
const status = calculateTestStatus(node)
|
|
242
|
+
icon = getStatusIcon(status)
|
|
243
|
+
statusClass = getStatusClass(status)
|
|
244
|
+
} else if (isYaml) {
|
|
245
|
+
icon = '📄'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const chevron = hasChildren ? `<span class="folder-chevron" data-target="${folderId}">▶</span>` : ''
|
|
249
|
+
const className = hasChildren
|
|
250
|
+
? 'tree-node folder-node'
|
|
251
|
+
: isYaml
|
|
252
|
+
? `tree-node yaml-file ${statusClass}`
|
|
253
|
+
: 'tree-node'
|
|
254
|
+
const dataContent = isYaml && node.content ? `data-content="${escapeHtml(node.content)}"` : ''
|
|
255
|
+
|
|
256
|
+
return `
|
|
257
|
+
<div class="${className}" ${dataContent}>
|
|
258
|
+
<span class="tree-line" style="padding-left: ${depth * 1.25}rem">
|
|
259
|
+
${chevron} ${icon} <span class="file-name">${displayName}</span>
|
|
260
|
+
</span>
|
|
261
|
+
${content}
|
|
262
|
+
</div>
|
|
263
|
+
`
|
|
264
|
+
})
|
|
265
|
+
.join('')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function generateSummaryHtml(summary: TestSummary): string {
|
|
269
|
+
const total = summary.totalDataCollections
|
|
270
|
+
const successPercent = total > 0 ? Math.round((summary.fullyFunctional / total) * 100) : 0
|
|
271
|
+
const warningPercent = total > 0 ? Math.round((summary.partiallyFunctional / total) * 100) : 0
|
|
272
|
+
const errorPercent = total > 0 ? Math.round((summary.failed / total) * 100) : 0
|
|
273
|
+
const untestedPercent = total > 0 ? Math.round((summary.untested / total) * 100) : 0
|
|
274
|
+
|
|
275
|
+
const testTotal = summary.totalTests
|
|
276
|
+
const testSuccessPercent = testTotal > 0 ? Math.round((summary.successfulTests / testTotal) * 100) : 0
|
|
277
|
+
const testWarningPercent = testTotal > 0 ? Math.round((summary.warningTests / testTotal) * 100) : 0
|
|
278
|
+
const testErrorPercent = testTotal > 0 ? Math.round((summary.errorTests / testTotal) * 100) : 0
|
|
279
|
+
const testUntestedPercent = testTotal > 0 ? Math.round((summary.untestedTests / testTotal) * 100) : 0
|
|
280
|
+
|
|
281
|
+
return `
|
|
282
|
+
<div class="summary-dashboard">
|
|
283
|
+
<h2 class="summary-title">Test Summary</h2>
|
|
284
|
+
|
|
285
|
+
<h3 class="summary-subtitle">Data Collections Overview</h3>
|
|
286
|
+
<div class="summary-grid">
|
|
287
|
+
<div class="summary-card total">
|
|
288
|
+
<div class="summary-number">${total}</div>
|
|
289
|
+
<div class="summary-label">Total Data Collections</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="summary-card success">
|
|
292
|
+
<div class="summary-number">${summary.fullyFunctional}</div>
|
|
293
|
+
<div class="summary-label">Fully Functional (${successPercent}%)</div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="summary-card warning">
|
|
296
|
+
<div class="summary-number">${summary.partiallyFunctional}</div>
|
|
297
|
+
<div class="summary-label">Partially Functional (${warningPercent}%)</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="summary-card error">
|
|
300
|
+
<div class="summary-number">${summary.failed}</div>
|
|
301
|
+
<div class="summary-label">Failed (${errorPercent}%)</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="summary-card untested">
|
|
304
|
+
<div class="summary-number">${summary.untested}</div>
|
|
305
|
+
<div class="summary-label">Untested (${untestedPercent}%)</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<h3 class="summary-subtitle">Individual Tests Breakdown</h3>
|
|
310
|
+
<div class="summary-grid">
|
|
311
|
+
<div class="summary-card total">
|
|
312
|
+
<div class="summary-number">${testTotal}</div>
|
|
313
|
+
<div class="summary-label">Total Tests</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="summary-card success">
|
|
316
|
+
<div class="summary-number">${summary.successfulTests}</div>
|
|
317
|
+
<div class="summary-label">Successful (${testSuccessPercent}%)</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="summary-card warning">
|
|
320
|
+
<div class="summary-number">${summary.warningTests}</div>
|
|
321
|
+
<div class="summary-label">Warnings (${testWarningPercent}%)</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="summary-card error">
|
|
324
|
+
<div class="summary-number">${summary.errorTests}</div>
|
|
325
|
+
<div class="summary-label">Errors (${testErrorPercent}%)</div>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="summary-card untested">
|
|
328
|
+
<div class="summary-number">${summary.untestedTests}</div>
|
|
329
|
+
<div class="summary-label">Untested (${testUntestedPercent}%)</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="legend">
|
|
333
|
+
<h3>Status Legend</h3>
|
|
334
|
+
<div class="legend-items">
|
|
335
|
+
<div class="legend-item">
|
|
336
|
+
<span class="legend-icon">✅</span>
|
|
337
|
+
<span>Fully Functional - All tests pass, operation works correctly</span>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="legend-item">
|
|
340
|
+
<span class="legend-icon">⚠️</span>
|
|
341
|
+
<span>Partially Functional - Some tests fail, may have issues</span>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="legend-item">
|
|
344
|
+
<span class="legend-icon">❌</span>
|
|
345
|
+
<span>Failed - Tests failed or errors occurred</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="legend-item">
|
|
348
|
+
<span class="legend-icon">⭕</span>
|
|
349
|
+
<span>Untested - No test results available</span>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function generateHtml(nodes: TestNode[]): string {
|
|
358
|
+
folderIdCounter = 0
|
|
359
|
+
const summary = calculateDataCollectionSummary(nodes)
|
|
360
|
+
|
|
361
|
+
const html = `
|
|
362
|
+
<!DOCTYPE html>
|
|
363
|
+
<html lang="en">
|
|
364
|
+
<head>
|
|
365
|
+
<meta charset="UTF-8">
|
|
366
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
367
|
+
<title>Integration Tests Report</title>
|
|
368
|
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
369
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
|
|
370
|
+
<style>
|
|
371
|
+
.tree-node {
|
|
372
|
+
padding: 0.25rem 0;
|
|
373
|
+
}
|
|
374
|
+
.tree-line {
|
|
375
|
+
display: block;
|
|
376
|
+
width: 100%;
|
|
377
|
+
box-sizing: border-box;
|
|
378
|
+
padding: 0.25rem 0.5rem;
|
|
379
|
+
border-radius: 0.25rem;
|
|
380
|
+
transition: background 0.2s;
|
|
381
|
+
}
|
|
382
|
+
.tree-line:hover {
|
|
383
|
+
background-color: #f3f4f6;
|
|
384
|
+
}
|
|
385
|
+
.folder-node {
|
|
386
|
+
font-weight: 500;
|
|
387
|
+
}
|
|
388
|
+
.folder-chevron {
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
user-select: none;
|
|
391
|
+
display: inline-block;
|
|
392
|
+
width: 1.5rem;
|
|
393
|
+
text-align: center;
|
|
394
|
+
}
|
|
395
|
+
.folder-chevron:hover {
|
|
396
|
+
color: #2563eb;
|
|
397
|
+
}
|
|
398
|
+
.yaml-file {
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
}
|
|
401
|
+
.file-name {
|
|
402
|
+
margin-left: 0.25rem;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Status-based styling */
|
|
406
|
+
.status-success .tree-line {
|
|
407
|
+
background-color: #f0fdf4;
|
|
408
|
+
border-left: 4px solid #22c55e;
|
|
409
|
+
}
|
|
410
|
+
.status-warning .tree-line {
|
|
411
|
+
background-color: #fefce8;
|
|
412
|
+
border-left: 4px solid #eab308;
|
|
413
|
+
}
|
|
414
|
+
.status-error .tree-line {
|
|
415
|
+
background-color: #fef2f2;
|
|
416
|
+
border-left: 4px solid #ef4444;
|
|
417
|
+
}
|
|
418
|
+
.status-untested .tree-line {
|
|
419
|
+
background-color: #f8fafc;
|
|
420
|
+
border-left: 4px solid #94a3b8;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* Summary Dashboard */
|
|
424
|
+
.summary-dashboard {
|
|
425
|
+
background: white;
|
|
426
|
+
border-radius: 0.5rem;
|
|
427
|
+
padding: 2rem;
|
|
428
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
429
|
+
margin-bottom: 2rem;
|
|
430
|
+
}
|
|
431
|
+
.summary-title {
|
|
432
|
+
font-size: 1.5rem;
|
|
433
|
+
font-weight: 600;
|
|
434
|
+
margin-bottom: 1.5rem;
|
|
435
|
+
color: #1f2937;
|
|
436
|
+
}
|
|
437
|
+
.summary-subtitle {
|
|
438
|
+
font-size: 1.25rem;
|
|
439
|
+
font-weight: 600;
|
|
440
|
+
margin: 1.5rem 0 1rem 0;
|
|
441
|
+
color: #374151;
|
|
442
|
+
border-bottom: 2px solid #e5e7eb;
|
|
443
|
+
padding-bottom: 0.5rem;
|
|
444
|
+
}
|
|
445
|
+
.summary-grid {
|
|
446
|
+
display: grid;
|
|
447
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
448
|
+
gap: 1rem;
|
|
449
|
+
margin-bottom: 2rem;
|
|
450
|
+
}
|
|
451
|
+
.summary-card {
|
|
452
|
+
padding: 1.5rem;
|
|
453
|
+
border-radius: 0.5rem;
|
|
454
|
+
text-align: center;
|
|
455
|
+
}
|
|
456
|
+
.summary-card.total {
|
|
457
|
+
background-color: #f8fafc;
|
|
458
|
+
border: 2px solid #e2e8f0;
|
|
459
|
+
}
|
|
460
|
+
.summary-card.success {
|
|
461
|
+
background-color: #f0fdf4;
|
|
462
|
+
border: 2px solid #22c55e;
|
|
463
|
+
}
|
|
464
|
+
.summary-card.warning {
|
|
465
|
+
background-color: #fefce8;
|
|
466
|
+
border: 2px solid #eab308;
|
|
467
|
+
}
|
|
468
|
+
.summary-card.error {
|
|
469
|
+
background-color: #fef2f2;
|
|
470
|
+
border: 2px solid #ef4444;
|
|
471
|
+
}
|
|
472
|
+
.summary-card.untested {
|
|
473
|
+
background-color: #f1f5f9;
|
|
474
|
+
border: 2px solid #94a3b8;
|
|
475
|
+
}
|
|
476
|
+
.summary-number {
|
|
477
|
+
font-size: 2rem;
|
|
478
|
+
font-weight: 700;
|
|
479
|
+
margin-bottom: 0.5rem;
|
|
480
|
+
}
|
|
481
|
+
.summary-label {
|
|
482
|
+
font-size: 0.875rem;
|
|
483
|
+
font-weight: 500;
|
|
484
|
+
opacity: 0.8;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* Legend */
|
|
488
|
+
.legend {
|
|
489
|
+
border-top: 1px solid #e2e8f0;
|
|
490
|
+
padding-top: 1.5rem;
|
|
491
|
+
}
|
|
492
|
+
.legend h3 {
|
|
493
|
+
font-size: 1.125rem;
|
|
494
|
+
font-weight: 600;
|
|
495
|
+
margin-bottom: 1rem;
|
|
496
|
+
color: #374151;
|
|
497
|
+
}
|
|
498
|
+
.legend-items {
|
|
499
|
+
display: grid;
|
|
500
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
501
|
+
gap: 0.75rem;
|
|
502
|
+
}
|
|
503
|
+
.legend-item {
|
|
504
|
+
display: flex;
|
|
505
|
+
align-items: center;
|
|
506
|
+
gap: 0.75rem;
|
|
507
|
+
}
|
|
508
|
+
.legend-icon {
|
|
509
|
+
font-size: 1.25rem;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.modal {
|
|
513
|
+
display: none;
|
|
514
|
+
position: fixed;
|
|
515
|
+
top: 0;
|
|
516
|
+
left: 0;
|
|
517
|
+
width: 100%;
|
|
518
|
+
height: 100%;
|
|
519
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
520
|
+
z-index: 1000;
|
|
521
|
+
}
|
|
522
|
+
.modal-content {
|
|
523
|
+
position: relative;
|
|
524
|
+
background-color: #2d2d2d;
|
|
525
|
+
margin: 5% auto;
|
|
526
|
+
padding: 1.5rem;
|
|
527
|
+
width: 80%;
|
|
528
|
+
max-width: 1000px;
|
|
529
|
+
max-height: 80vh;
|
|
530
|
+
border-radius: 0.5rem;
|
|
531
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
532
|
+
overflow: auto;
|
|
533
|
+
}
|
|
534
|
+
.modal-header {
|
|
535
|
+
display: flex;
|
|
536
|
+
justify-content: space-between;
|
|
537
|
+
align-items: center;
|
|
538
|
+
margin-bottom: 1rem;
|
|
539
|
+
padding-bottom: 0.5rem;
|
|
540
|
+
border-bottom: 1px solid #4a4a4a;
|
|
541
|
+
}
|
|
542
|
+
.modal-title {
|
|
543
|
+
color: #e6e6e6;
|
|
544
|
+
font-size: 1.25rem;
|
|
545
|
+
font-weight: 600;
|
|
546
|
+
}
|
|
547
|
+
.modal-close {
|
|
548
|
+
color: #e6e6e6;
|
|
549
|
+
font-size: 1.5rem;
|
|
550
|
+
cursor: pointer;
|
|
551
|
+
padding: 0.25rem;
|
|
552
|
+
}
|
|
553
|
+
.modal-close:hover {
|
|
554
|
+
color: #fff;
|
|
555
|
+
}
|
|
556
|
+
.modal-body {
|
|
557
|
+
background-color: #2d2d2d;
|
|
558
|
+
border-radius: 0.25rem;
|
|
559
|
+
overflow: auto;
|
|
560
|
+
}
|
|
561
|
+
.modal-body pre {
|
|
562
|
+
margin: 0;
|
|
563
|
+
padding: 0;
|
|
564
|
+
}
|
|
565
|
+
.modal-body code {
|
|
566
|
+
font-family: 'Fira Code', monospace;
|
|
567
|
+
font-size: 0.9rem;
|
|
568
|
+
line-height: 1.5;
|
|
569
|
+
}
|
|
570
|
+
.tree-controls {
|
|
571
|
+
display: flex;
|
|
572
|
+
gap: 1rem;
|
|
573
|
+
margin-bottom: 1.5rem;
|
|
574
|
+
}
|
|
575
|
+
.tree-btn {
|
|
576
|
+
background: #2563eb;
|
|
577
|
+
color: #fff;
|
|
578
|
+
border: none;
|
|
579
|
+
border-radius: 0.375rem;
|
|
580
|
+
padding: 0.5rem 1.25rem;
|
|
581
|
+
font-size: 1rem;
|
|
582
|
+
font-weight: 500;
|
|
583
|
+
cursor: pointer;
|
|
584
|
+
transition: background 0.2s;
|
|
585
|
+
}
|
|
586
|
+
.tree-btn:hover {
|
|
587
|
+
background: #1d4ed8;
|
|
588
|
+
}
|
|
589
|
+
</style>
|
|
590
|
+
</head>
|
|
591
|
+
<body class="bg-gray-50 min-h-screen p-8">
|
|
592
|
+
<div class="max-w-7xl mx-auto">
|
|
593
|
+
<h1 class="text-3xl font-bold mb-8 text-gray-800">Integration Tests Report</h1>
|
|
594
|
+
|
|
595
|
+
${generateSummaryHtml(summary)}
|
|
596
|
+
|
|
597
|
+
<div class="bg-white rounded-lg shadow-lg p-6">
|
|
598
|
+
<h2 class="text-xl font-semibold mb-4 text-gray-800">Detailed Test Results</h2>
|
|
599
|
+
<div class="tree-controls">
|
|
600
|
+
<button id="expandAllBtn" class="tree-btn">Expand All</button>
|
|
601
|
+
<button id="collapseAllBtn" class="tree-btn">Collapse All</button>
|
|
602
|
+
</div>
|
|
603
|
+
<div id="folder-root" class="folder-children" style="display: block;">
|
|
604
|
+
${generateTreeHtml(nodes, 'root')}
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
<!-- Modal -->
|
|
610
|
+
<div id="yamlModal" class="modal">
|
|
611
|
+
<div class="modal-content">
|
|
612
|
+
<div class="modal-header">
|
|
613
|
+
<h2 class="modal-title"></h2>
|
|
614
|
+
<span class="modal-close">×</span>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="modal-body">
|
|
617
|
+
<pre><code class="language-yaml"></code></pre>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
|
623
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
|
|
624
|
+
<script>
|
|
625
|
+
const modal = document.getElementById('yamlModal');
|
|
626
|
+
const modalTitle = modal.querySelector('.modal-title');
|
|
627
|
+
const modalCode = modal.querySelector('code');
|
|
628
|
+
const modalClose = modal.querySelector('.modal-close');
|
|
629
|
+
|
|
630
|
+
function showYamlContent(fileName, content) {
|
|
631
|
+
modalTitle.textContent = fileName;
|
|
632
|
+
modalCode.textContent = content;
|
|
633
|
+
modal.style.display = 'block';
|
|
634
|
+
Prism.highlightElement(modalCode);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function closeModal() {
|
|
638
|
+
modal.style.display = 'none';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Folder toggle logic
|
|
642
|
+
function toggleFolder(chevron, children) {
|
|
643
|
+
if (!chevron || !children) return;
|
|
644
|
+
const isOpen = children.style.display === 'block';
|
|
645
|
+
children.style.display = isOpen ? 'none' : 'block';
|
|
646
|
+
chevron.textContent = isOpen ? '▶' : '▼';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Close modal when clicking the close button or outside the modal
|
|
650
|
+
modalClose.addEventListener('click', closeModal);
|
|
651
|
+
window.addEventListener('click', (e) => {
|
|
652
|
+
if (e.target === modal) {
|
|
653
|
+
closeModal();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Close modal with Escape key
|
|
658
|
+
document.addEventListener('keydown', (e) => {
|
|
659
|
+
if (e.key === 'Escape') {
|
|
660
|
+
closeModal();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
665
|
+
// Folder chevrons
|
|
666
|
+
document.querySelectorAll('.folder-chevron').forEach(chevron => {
|
|
667
|
+
chevron.addEventListener('click', (e) => {
|
|
668
|
+
e.stopPropagation();
|
|
669
|
+
const targetId = chevron.getAttribute('data-target');
|
|
670
|
+
const children = document.getElementById(targetId);
|
|
671
|
+
toggleFolder(chevron, children);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
// Folder line click (expand/collapse)
|
|
675
|
+
document.querySelectorAll('.folder-node .tree-line').forEach(line => {
|
|
676
|
+
line.addEventListener('click', (e) => {
|
|
677
|
+
// Prevent double toggle if chevron was clicked
|
|
678
|
+
if ((e.target).classList.contains('folder-chevron')) return;
|
|
679
|
+
const chevron = line.querySelector('.folder-chevron');
|
|
680
|
+
if (chevron) {
|
|
681
|
+
const targetId = chevron.getAttribute('data-target');
|
|
682
|
+
const children = document.getElementById(targetId);
|
|
683
|
+
toggleFolder(chevron, children);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
// YAML file modal
|
|
688
|
+
document.querySelectorAll('.yaml-file').forEach(node => {
|
|
689
|
+
node.addEventListener('click', (e) => {
|
|
690
|
+
const fileName = node.querySelector('.file-name').textContent;
|
|
691
|
+
const content = node.getAttribute('data-content');
|
|
692
|
+
if (content) {
|
|
693
|
+
showYamlContent(fileName, content);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
// Expand All
|
|
698
|
+
document.getElementById('expandAllBtn').addEventListener('click', () => {
|
|
699
|
+
document.querySelectorAll('.folder-children').forEach(el => {
|
|
700
|
+
el.style.display = 'block';
|
|
701
|
+
});
|
|
702
|
+
document.querySelectorAll('.folder-chevron').forEach(chevron => {
|
|
703
|
+
chevron.textContent = '▼';
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
// Collapse All
|
|
707
|
+
document.getElementById('collapseAllBtn').addEventListener('click', () => {
|
|
708
|
+
document.querySelectorAll('.folder-children').forEach(el => {
|
|
709
|
+
if (el.id !== 'folder-root') {
|
|
710
|
+
el.style.display = 'none';
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
document.querySelectorAll('.folder-chevron').forEach(chevron => {
|
|
714
|
+
chevron.textContent = '▶';
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
</script>
|
|
719
|
+
</body>
|
|
720
|
+
</html>
|
|
721
|
+
`
|
|
722
|
+
|
|
723
|
+
return html
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function escapeHtml(unsafe: string): string {
|
|
727
|
+
return unsafe
|
|
728
|
+
.replace(/&/g, '&')
|
|
729
|
+
.replace(/</g, '<')
|
|
730
|
+
.replace(/>/g, '>')
|
|
731
|
+
.replace(/"/g, '"')
|
|
732
|
+
.replace(/'/g, ''')
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function generateTestReport(testsDir: string, outputFile: string): void {
|
|
736
|
+
try {
|
|
737
|
+
const tree = buildTestTree(testsDir)
|
|
738
|
+
const html = generateHtml(tree)
|
|
739
|
+
fs.writeFileSync(outputFile, html)
|
|
740
|
+
console.warn(`Report generated successfully at: ${outputFile}`)
|
|
741
|
+
} catch (error) {
|
|
742
|
+
console.error('Error generating report:', error)
|
|
743
|
+
throw error
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function main() {
|
|
748
|
+
const testsDir = path.join(__dirname, '..', 'tests')
|
|
749
|
+
const outputFile = path.join(__dirname, '..', 'test-report.html')
|
|
750
|
+
|
|
751
|
+
generateTestReport(testsDir, outputFile)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Only run main if this file is executed directly
|
|
755
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
756
|
+
main()
|
|
757
|
+
}
|