qa360 1.4.5 → 2.0.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/README.md +1 -1
- package/dist/commands/ai.d.ts +41 -0
- package/dist/commands/ai.js +499 -0
- package/dist/commands/ask.js +12 -12
- package/dist/commands/coverage.d.ts +8 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/explain.d.ts +27 -0
- package/dist/commands/explain.js +630 -0
- package/dist/commands/flakiness.d.ts +73 -0
- package/dist/commands/flakiness.js +435 -0
- package/dist/commands/generate.d.ts +66 -0
- package/dist/commands/generate.js +438 -0
- package/dist/commands/init.d.ts +56 -9
- package/dist/commands/init.js +217 -10
- package/dist/commands/monitor.d.ts +27 -0
- package/dist/commands/monitor.js +225 -0
- package/dist/commands/ollama.d.ts +40 -0
- package/dist/commands/ollama.js +301 -0
- package/dist/commands/pack.d.ts +37 -9
- package/dist/commands/pack.js +240 -141
- package/dist/commands/regression.d.ts +8 -0
- package/dist/commands/regression.js +340 -0
- package/dist/commands/repair.d.ts +26 -0
- package/dist/commands/repair.js +307 -0
- package/dist/commands/retry.d.ts +43 -0
- package/dist/commands/retry.js +275 -0
- package/dist/commands/run.d.ts +8 -3
- package/dist/commands/run.js +45 -31
- package/dist/commands/slo.d.ts +8 -0
- package/dist/commands/slo.js +327 -0
- package/dist/core/adapters/playwright-native-api.d.ts +183 -0
- package/dist/core/adapters/playwright-native-api.js +461 -0
- package/dist/core/adapters/playwright-ui.d.ts +7 -0
- package/dist/core/adapters/playwright-ui.js +29 -1
- package/dist/core/ai/anthropic-provider.d.ts +50 -0
- package/dist/core/ai/anthropic-provider.js +211 -0
- package/dist/core/ai/deepseek-provider.d.ts +81 -0
- package/dist/core/ai/deepseek-provider.js +254 -0
- package/dist/core/ai/index.d.ts +60 -0
- package/dist/core/ai/index.js +18 -0
- package/dist/core/ai/llm-client.d.ts +45 -0
- package/dist/core/ai/llm-client.js +7 -0
- package/dist/core/ai/mock-provider.d.ts +49 -0
- package/dist/core/ai/mock-provider.js +121 -0
- package/dist/core/ai/ollama-provider.d.ts +78 -0
- package/dist/core/ai/ollama-provider.js +192 -0
- package/dist/core/ai/openai-provider.d.ts +48 -0
- package/dist/core/ai/openai-provider.js +188 -0
- package/dist/core/ai/provider-factory.d.ts +160 -0
- package/dist/core/ai/provider-factory.js +269 -0
- package/dist/core/auth/api-key-provider.d.ts +16 -0
- package/dist/core/auth/api-key-provider.js +63 -0
- package/dist/core/auth/aws-iam-provider.d.ts +35 -0
- package/dist/core/auth/aws-iam-provider.js +177 -0
- package/dist/core/auth/azure-ad-provider.d.ts +15 -0
- package/dist/core/auth/azure-ad-provider.js +99 -0
- package/dist/core/auth/basic-auth-provider.d.ts +26 -0
- package/dist/core/auth/basic-auth-provider.js +111 -0
- package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
- package/dist/core/auth/gcp-adc-provider.js +126 -0
- package/dist/core/auth/index.d.ts +238 -0
- package/dist/core/auth/index.js +82 -0
- package/dist/core/auth/jwt-provider.d.ts +19 -0
- package/dist/core/auth/jwt-provider.js +160 -0
- package/dist/core/auth/manager.d.ts +84 -0
- package/dist/core/auth/manager.js +230 -0
- package/dist/core/auth/oauth2-provider.d.ts +17 -0
- package/dist/core/auth/oauth2-provider.js +114 -0
- package/dist/core/auth/totp-provider.d.ts +31 -0
- package/dist/core/auth/totp-provider.js +134 -0
- package/dist/core/auth/ui-login-provider.d.ts +26 -0
- package/dist/core/auth/ui-login-provider.js +198 -0
- package/dist/core/cache/index.d.ts +7 -0
- package/dist/core/cache/index.js +6 -0
- package/dist/core/cache/lru-cache.d.ts +203 -0
- package/dist/core/cache/lru-cache.js +397 -0
- package/dist/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/coverage/analyzer.js +415 -0
- package/dist/core/coverage/collector.d.ts +74 -0
- package/dist/core/coverage/collector.js +459 -0
- package/dist/core/coverage/config.d.ts +37 -0
- package/dist/core/coverage/config.js +156 -0
- package/dist/core/coverage/index.d.ts +11 -0
- package/dist/core/coverage/index.js +15 -0
- package/dist/core/coverage/types.d.ts +267 -0
- package/dist/core/coverage/types.js +6 -0
- package/dist/core/coverage/vault.d.ts +95 -0
- package/dist/core/coverage/vault.js +405 -0
- package/dist/core/dashboard/assets.d.ts +6 -0
- package/dist/core/dashboard/assets.js +690 -0
- package/dist/core/dashboard/index.d.ts +6 -0
- package/dist/core/dashboard/index.js +5 -0
- package/dist/core/dashboard/server.d.ts +72 -0
- package/dist/core/dashboard/server.js +354 -0
- package/dist/core/dashboard/types.d.ts +70 -0
- package/dist/core/dashboard/types.js +5 -0
- package/dist/core/discoverer/index.d.ts +115 -0
- package/dist/core/discoverer/index.js +250 -0
- package/dist/core/flakiness/index.d.ts +228 -0
- package/dist/core/flakiness/index.js +384 -0
- package/dist/core/generation/code-formatter.d.ts +111 -0
- package/dist/core/generation/code-formatter.js +307 -0
- package/dist/core/generation/code-generator.d.ts +144 -0
- package/dist/core/generation/code-generator.js +293 -0
- package/dist/core/generation/generator.d.ts +40 -0
- package/dist/core/generation/generator.js +76 -0
- package/dist/core/generation/index.d.ts +30 -0
- package/dist/core/generation/index.js +28 -0
- package/dist/core/generation/pack-generator.d.ts +107 -0
- package/dist/core/generation/pack-generator.js +416 -0
- package/dist/core/generation/prompt-builder.d.ts +132 -0
- package/dist/core/generation/prompt-builder.js +672 -0
- package/dist/core/generation/source-analyzer.d.ts +213 -0
- package/dist/core/generation/source-analyzer.js +657 -0
- package/dist/core/generation/test-optimizer.d.ts +117 -0
- package/dist/core/generation/test-optimizer.js +328 -0
- package/dist/core/generation/types.d.ts +214 -0
- package/dist/core/generation/types.js +4 -0
- package/dist/core/index.d.ts +23 -1
- package/dist/core/index.js +39 -0
- package/dist/core/pack/validator.js +31 -1
- package/dist/core/pack-v2/index.d.ts +9 -0
- package/dist/core/pack-v2/index.js +8 -0
- package/dist/core/pack-v2/loader.d.ts +62 -0
- package/dist/core/pack-v2/loader.js +231 -0
- package/dist/core/pack-v2/migrator.d.ts +56 -0
- package/dist/core/pack-v2/migrator.js +455 -0
- package/dist/core/pack-v2/validator.d.ts +61 -0
- package/dist/core/pack-v2/validator.js +577 -0
- package/dist/core/regression/detector.d.ts +107 -0
- package/dist/core/regression/detector.js +497 -0
- package/dist/core/regression/index.d.ts +9 -0
- package/dist/core/regression/index.js +11 -0
- package/dist/core/regression/trend-analyzer.d.ts +102 -0
- package/dist/core/regression/trend-analyzer.js +345 -0
- package/dist/core/regression/types.d.ts +222 -0
- package/dist/core/regression/types.js +7 -0
- package/dist/core/regression/vault.d.ts +87 -0
- package/dist/core/regression/vault.js +289 -0
- package/dist/core/repair/engine/fixer.d.ts +24 -0
- package/dist/core/repair/engine/fixer.js +226 -0
- package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
- package/dist/core/repair/engine/suggestion-engine.js +187 -0
- package/dist/core/repair/index.d.ts +10 -0
- package/dist/core/repair/index.js +13 -0
- package/dist/core/repair/repairer.d.ts +90 -0
- package/dist/core/repair/repairer.js +284 -0
- package/dist/core/repair/types.d.ts +91 -0
- package/dist/core/repair/types.js +6 -0
- package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
- package/dist/core/repair/utils/error-analyzer.js +264 -0
- package/dist/core/retry/flakiness-integration.d.ts +60 -0
- package/dist/core/retry/flakiness-integration.js +228 -0
- package/dist/core/retry/index.d.ts +14 -0
- package/dist/core/retry/index.js +16 -0
- package/dist/core/retry/retry-engine.d.ts +80 -0
- package/dist/core/retry/retry-engine.js +296 -0
- package/dist/core/retry/types.d.ts +178 -0
- package/dist/core/retry/types.js +52 -0
- package/dist/core/retry/vault.d.ts +77 -0
- package/dist/core/retry/vault.js +304 -0
- package/dist/core/runner/e2e-helpers.d.ts +102 -0
- package/dist/core/runner/e2e-helpers.js +153 -0
- package/dist/core/runner/phase3-runner.d.ts +101 -2
- package/dist/core/runner/phase3-runner.js +559 -24
- package/dist/core/self-healing/assertion-healer.d.ts +97 -0
- package/dist/core/self-healing/assertion-healer.js +371 -0
- package/dist/core/self-healing/engine.d.ts +122 -0
- package/dist/core/self-healing/engine.js +538 -0
- package/dist/core/self-healing/index.d.ts +10 -0
- package/dist/core/self-healing/index.js +11 -0
- package/dist/core/self-healing/selector-healer.d.ts +103 -0
- package/dist/core/self-healing/selector-healer.js +372 -0
- package/dist/core/self-healing/types.d.ts +152 -0
- package/dist/core/self-healing/types.js +6 -0
- package/dist/core/slo/config.d.ts +107 -0
- package/dist/core/slo/config.js +360 -0
- package/dist/core/slo/index.d.ts +11 -0
- package/dist/core/slo/index.js +15 -0
- package/dist/core/slo/sli-calculator.d.ts +92 -0
- package/dist/core/slo/sli-calculator.js +364 -0
- package/dist/core/slo/slo-tracker.d.ts +148 -0
- package/dist/core/slo/slo-tracker.js +379 -0
- package/dist/core/slo/types.d.ts +281 -0
- package/dist/core/slo/types.js +7 -0
- package/dist/core/slo/vault.d.ts +102 -0
- package/dist/core/slo/vault.js +427 -0
- package/dist/core/tui/index.d.ts +7 -0
- package/dist/core/tui/index.js +6 -0
- package/dist/core/tui/monitor.d.ts +92 -0
- package/dist/core/tui/monitor.js +271 -0
- package/dist/core/tui/renderer.d.ts +33 -0
- package/dist/core/tui/renderer.js +218 -0
- package/dist/core/tui/types.d.ts +63 -0
- package/dist/core/tui/types.js +5 -0
- package/dist/core/types/pack-v2.d.ts +425 -0
- package/dist/core/types/pack-v2.js +8 -0
- package/dist/core/vault/index.d.ts +116 -0
- package/dist/core/vault/index.js +400 -5
- package/dist/core/watch/index.d.ts +7 -0
- package/dist/core/watch/index.js +6 -0
- package/dist/core/watch/watch-mode.d.ts +213 -0
- package/dist/core/watch/watch-mode.js +389 -0
- package/dist/index.js +68 -68
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +136 -0
- package/package.json +5 -1
- package/dist/core/adapters/playwright-api.d.ts +0 -82
- package/dist/core/adapters/playwright-api.js +0 -264
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Vault Integration
|
|
3
|
+
*
|
|
4
|
+
* Integrates coverage data storage and retrieval with the Evidence Vault.
|
|
5
|
+
*/
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
/**
|
|
8
|
+
* Coverage Vault class
|
|
9
|
+
*/
|
|
10
|
+
export class CoverageVault {
|
|
11
|
+
db;
|
|
12
|
+
dbRun;
|
|
13
|
+
dbAll;
|
|
14
|
+
dbGet;
|
|
15
|
+
constructor(db) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.dbRun = promisify(db.run.bind(db));
|
|
18
|
+
this.dbAll = promisify(db.all.bind(db));
|
|
19
|
+
this.dbGet = promisify(db.get.bind(db));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize coverage tables
|
|
23
|
+
*/
|
|
24
|
+
async initialize() {
|
|
25
|
+
const schema = `
|
|
26
|
+
-- Coverage metrics table
|
|
27
|
+
CREATE TABLE IF NOT EXISTS coverage_metrics (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
30
|
+
gate TEXT NOT NULL,
|
|
31
|
+
file_path TEXT NOT NULL,
|
|
32
|
+
total_lines INTEGER DEFAULT 0,
|
|
33
|
+
covered_lines INTEGER DEFAULT 0,
|
|
34
|
+
branch_coverage REAL DEFAULT 0,
|
|
35
|
+
function_coverage REAL DEFAULT 0,
|
|
36
|
+
line_coverage REAL DEFAULT 0,
|
|
37
|
+
statement_coverage REAL DEFAULT 0,
|
|
38
|
+
total_branches INTEGER DEFAULT 0,
|
|
39
|
+
covered_branches INTEGER DEFAULT 0,
|
|
40
|
+
total_functions INTEGER DEFAULT 0,
|
|
41
|
+
covered_functions INTEGER DEFAULT 0,
|
|
42
|
+
total_statements INTEGER DEFAULT 0,
|
|
43
|
+
covered_statements INTEGER DEFAULT 0,
|
|
44
|
+
uncovered_lines TEXT,
|
|
45
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_metrics_run_id ON coverage_metrics(run_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_metrics_gate ON coverage_metrics(gate);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_metrics_file_path ON coverage_metrics(file_path);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_metrics_run_gate ON coverage_metrics(run_id, gate);
|
|
52
|
+
|
|
53
|
+
-- Coverage trends table
|
|
54
|
+
CREATE TABLE IF NOT EXISTS coverage_trends (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
test_id TEXT NOT NULL,
|
|
57
|
+
file_path TEXT NOT NULL,
|
|
58
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
59
|
+
coverage_type TEXT NOT NULL,
|
|
60
|
+
coverage_value REAL NOT NULL,
|
|
61
|
+
date INTEGER NOT NULL,
|
|
62
|
+
commit TEXT,
|
|
63
|
+
branch TEXT,
|
|
64
|
+
UNIQUE(test_id, file_path, coverage_type, date)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_trends_test_id ON coverage_trends(test_id);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_trends_file_path ON coverage_trends(file_path);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_trends_date ON coverage_trends(date);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_trends_type ON coverage_trends(coverage_type);
|
|
71
|
+
|
|
72
|
+
-- Coverage summary table (aggregate metrics per run)
|
|
73
|
+
CREATE TABLE IF NOT EXISTS coverage_summary (
|
|
74
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
|
|
75
|
+
line_coverage REAL NOT NULL DEFAULT 0,
|
|
76
|
+
branch_coverage REAL NOT NULL DEFAULT 0,
|
|
77
|
+
function_coverage REAL NOT NULL DEFAULT 0,
|
|
78
|
+
statement_coverage REAL NOT NULL DEFAULT 0,
|
|
79
|
+
total_files INTEGER DEFAULT 0,
|
|
80
|
+
files_meeting_threshold INTEGER DEFAULT 0,
|
|
81
|
+
total_lines INTEGER DEFAULT 0,
|
|
82
|
+
covered_lines INTEGER DEFAULT 0,
|
|
83
|
+
thresholds_met INTEGER DEFAULT 1,
|
|
84
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_coverage_summary_created_at ON coverage_summary(created_at);
|
|
88
|
+
`;
|
|
89
|
+
const statements = schema
|
|
90
|
+
.split(';')
|
|
91
|
+
.map(s => s.trim())
|
|
92
|
+
.filter(s => s.length > 0);
|
|
93
|
+
for (const statement of statements) {
|
|
94
|
+
await this.dbRun(statement, []);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Store coverage result for a run
|
|
99
|
+
*/
|
|
100
|
+
async storeCoverage(runId, result) {
|
|
101
|
+
// Store per-file metrics
|
|
102
|
+
for (const [filePath, fileCoverage] of Object.entries(result.files)) {
|
|
103
|
+
const record = {
|
|
104
|
+
run_id: runId,
|
|
105
|
+
gate: result.gate,
|
|
106
|
+
file_path: filePath,
|
|
107
|
+
total_lines: fileCoverage.totalLines,
|
|
108
|
+
covered_lines: fileCoverage.coveredLines,
|
|
109
|
+
branch_coverage: fileCoverage.branchCoverage,
|
|
110
|
+
function_coverage: fileCoverage.functionCoverage,
|
|
111
|
+
line_coverage: fileCoverage.lineCoverage,
|
|
112
|
+
statement_coverage: fileCoverage.statementCoverage,
|
|
113
|
+
total_branches: fileCoverage.totalBranches,
|
|
114
|
+
covered_branches: fileCoverage.coveredBranches,
|
|
115
|
+
total_functions: fileCoverage.totalFunctions,
|
|
116
|
+
covered_functions: fileCoverage.coveredFunctions,
|
|
117
|
+
total_statements: fileCoverage.totalStatements,
|
|
118
|
+
covered_statements: fileCoverage.coveredStatements,
|
|
119
|
+
uncovered_lines: JSON.stringify(fileCoverage.uncoveredLines),
|
|
120
|
+
created_at: Date.now()
|
|
121
|
+
};
|
|
122
|
+
await this.dbRun(`INSERT INTO coverage_metrics (
|
|
123
|
+
run_id, gate, file_path, total_lines, covered_lines,
|
|
124
|
+
branch_coverage, function_coverage, line_coverage, statement_coverage,
|
|
125
|
+
total_branches, covered_branches, total_functions, covered_functions,
|
|
126
|
+
total_statements, covered_statements, uncovered_lines, created_at
|
|
127
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
128
|
+
record.run_id, record.gate, record.file_path, record.total_lines, record.covered_lines,
|
|
129
|
+
record.branch_coverage, record.function_coverage, record.line_coverage, record.statement_coverage,
|
|
130
|
+
record.total_branches, record.covered_branches, record.total_functions, record.covered_functions,
|
|
131
|
+
record.total_statements, record.covered_statements, record.uncovered_lines, record.created_at
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
// Store aggregate summary
|
|
135
|
+
await this.storeSummary(runId, result.metrics);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Store coverage summary for a run
|
|
139
|
+
*/
|
|
140
|
+
async storeSummary(runId, metrics) {
|
|
141
|
+
await this.dbRun(`INSERT OR REPLACE INTO coverage_summary (
|
|
142
|
+
run_id, line_coverage, branch_coverage, function_coverage, statement_coverage,
|
|
143
|
+
total_files, files_meeting_threshold, total_lines, covered_lines, thresholds_met, created_at
|
|
144
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
145
|
+
runId,
|
|
146
|
+
metrics.lineCoverage,
|
|
147
|
+
metrics.branchCoverage,
|
|
148
|
+
metrics.functionCoverage,
|
|
149
|
+
metrics.statementCoverage,
|
|
150
|
+
metrics.totalFiles,
|
|
151
|
+
metrics.filesMeetingThreshold,
|
|
152
|
+
metrics.totalLines,
|
|
153
|
+
metrics.coveredLines,
|
|
154
|
+
1, // thresholds_met placeholder
|
|
155
|
+
Date.now()
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Retrieve coverage for a run
|
|
160
|
+
*/
|
|
161
|
+
async getCoverage(runId) {
|
|
162
|
+
const rows = await this.dbAll(`SELECT * FROM coverage_metrics WHERE run_id = ?`, [runId]);
|
|
163
|
+
if (rows.length === 0) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const files = {};
|
|
167
|
+
let gate = 'unknown';
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
gate = row.gate;
|
|
170
|
+
files[row.file_path] = {
|
|
171
|
+
path: row.file_path,
|
|
172
|
+
totalLines: row.total_lines,
|
|
173
|
+
coveredLines: row.covered_lines,
|
|
174
|
+
lineCoverage: row.line_coverage,
|
|
175
|
+
totalBranches: row.total_branches,
|
|
176
|
+
coveredBranches: row.covered_branches,
|
|
177
|
+
branchCoverage: row.branch_coverage,
|
|
178
|
+
totalFunctions: row.total_functions,
|
|
179
|
+
coveredFunctions: row.covered_functions,
|
|
180
|
+
functionCoverage: row.function_coverage,
|
|
181
|
+
totalStatements: row.total_statements,
|
|
182
|
+
coveredStatements: row.covered_statements,
|
|
183
|
+
statementCoverage: row.statement_coverage,
|
|
184
|
+
uncoveredLines: JSON.parse(row.uncovered_lines || '[]'),
|
|
185
|
+
partiallyCoveredLines: [],
|
|
186
|
+
branchesByLine: {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Get summary
|
|
190
|
+
const summary = await this.getSummary(runId);
|
|
191
|
+
const metrics = summary || {
|
|
192
|
+
lineCoverage: 0,
|
|
193
|
+
branchCoverage: 0,
|
|
194
|
+
functionCoverage: 0,
|
|
195
|
+
statementCoverage: 0,
|
|
196
|
+
totalFiles: Object.keys(files).length,
|
|
197
|
+
filesWithCoverage: Object.keys(files).length,
|
|
198
|
+
filesMeetingThreshold: 0,
|
|
199
|
+
totalLines: 0,
|
|
200
|
+
coveredLines: 0,
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
source: 'custom',
|
|
205
|
+
gate,
|
|
206
|
+
files,
|
|
207
|
+
metrics,
|
|
208
|
+
format: 'json',
|
|
209
|
+
timestamp: Date.now()
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get coverage summary for a run
|
|
214
|
+
*/
|
|
215
|
+
async getSummary(runId) {
|
|
216
|
+
const row = await this.dbGet(`SELECT * FROM coverage_summary WHERE run_id = ?`, [runId]);
|
|
217
|
+
if (!row) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
lineCoverage: row.line_coverage,
|
|
222
|
+
branchCoverage: row.branch_coverage,
|
|
223
|
+
functionCoverage: row.function_coverage,
|
|
224
|
+
statementCoverage: row.statement_coverage,
|
|
225
|
+
totalFiles: row.total_files,
|
|
226
|
+
filesWithCoverage: row.total_files,
|
|
227
|
+
filesMeetingThreshold: row.files_meeting_threshold,
|
|
228
|
+
totalLines: row.total_lines,
|
|
229
|
+
coveredLines: row.covered_lines,
|
|
230
|
+
timestamp: row.created_at
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get coverage trends for a file
|
|
235
|
+
*/
|
|
236
|
+
async getTrends(filePath, coverageType = 'line', limit = 30) {
|
|
237
|
+
const rows = await this.dbAll(`SELECT * FROM coverage_trends
|
|
238
|
+
WHERE file_path = ? AND coverage_type = ?
|
|
239
|
+
ORDER BY date DESC
|
|
240
|
+
LIMIT ?`, [filePath, coverageType, limit]);
|
|
241
|
+
const trends = [];
|
|
242
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
243
|
+
const row = rows[i];
|
|
244
|
+
const prev = i > 0 ? rows[i - 1] : null;
|
|
245
|
+
trends.push({
|
|
246
|
+
runId: row.run_id,
|
|
247
|
+
timestamp: row.date,
|
|
248
|
+
coverage: row.coverage_value,
|
|
249
|
+
type: coverageType,
|
|
250
|
+
change: prev ? row.coverage_value - prev.coverage_value : 0,
|
|
251
|
+
commit: row.commit,
|
|
252
|
+
branch: row.branch
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return trends;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get overall coverage trends
|
|
259
|
+
*/
|
|
260
|
+
async getOverallTrends(coverageType = 'line', limit = 30) {
|
|
261
|
+
const rows = await this.dbAll(`SELECT
|
|
262
|
+
run_id,
|
|
263
|
+
${coverageType}_coverage as coverage_value,
|
|
264
|
+
created_at as date
|
|
265
|
+
FROM coverage_summary
|
|
266
|
+
ORDER BY date DESC
|
|
267
|
+
LIMIT ?`, [limit]);
|
|
268
|
+
const trends = [];
|
|
269
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
270
|
+
const row = rows[i];
|
|
271
|
+
const prev = i > 0 ? rows[i - 1] : null;
|
|
272
|
+
trends.push({
|
|
273
|
+
runId: row.run_id,
|
|
274
|
+
timestamp: row.date,
|
|
275
|
+
coverage: row.coverage_value,
|
|
276
|
+
type: coverageType,
|
|
277
|
+
change: prev ? row.coverage_value - prev.coverage_value : 0
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return trends;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get coverage by gate
|
|
284
|
+
*/
|
|
285
|
+
async getCoverageByGate(gate, limit = 10) {
|
|
286
|
+
const rows = await this.dbAll(`SELECT
|
|
287
|
+
cs.line_coverage,
|
|
288
|
+
cs.branch_coverage,
|
|
289
|
+
cs.function_coverage,
|
|
290
|
+
cs.statement_coverage,
|
|
291
|
+
cs.total_files,
|
|
292
|
+
cs.files_meeting_threshold,
|
|
293
|
+
cs.total_lines,
|
|
294
|
+
cs.covered_lines,
|
|
295
|
+
cs.created_at as timestamp
|
|
296
|
+
FROM coverage_summary cs
|
|
297
|
+
JOIN runs r ON cs.run_id = r.id
|
|
298
|
+
JOIN gates g ON g.run_id = r.id
|
|
299
|
+
WHERE g.name = ?
|
|
300
|
+
ORDER BY cs.created_at DESC
|
|
301
|
+
LIMIT ?`, [gate, limit]);
|
|
302
|
+
return rows.map(row => ({
|
|
303
|
+
lineCoverage: row.line_coverage,
|
|
304
|
+
branchCoverage: row.branch_coverage,
|
|
305
|
+
functionCoverage: row.function_coverage,
|
|
306
|
+
statementCoverage: row.statement_coverage,
|
|
307
|
+
totalFiles: row.total_files,
|
|
308
|
+
filesWithCoverage: row.total_files,
|
|
309
|
+
filesMeetingThreshold: row.files_meeting_threshold,
|
|
310
|
+
totalLines: row.total_lines,
|
|
311
|
+
coveredLines: row.covered_lines,
|
|
312
|
+
timestamp: row.timestamp
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Get files with lowest coverage
|
|
317
|
+
*/
|
|
318
|
+
async getLowestCoverageFiles(limit = 10) {
|
|
319
|
+
const rows = await this.dbAll(`SELECT
|
|
320
|
+
file_path,
|
|
321
|
+
AVG(line_coverage) as avg_coverage,
|
|
322
|
+
gate
|
|
323
|
+
FROM coverage_metrics
|
|
324
|
+
GROUP BY file_path
|
|
325
|
+
ORDER BY avg_coverage ASC
|
|
326
|
+
LIMIT ?`, [limit]);
|
|
327
|
+
return rows.map(row => ({
|
|
328
|
+
filePath: row.file_path,
|
|
329
|
+
coverage: row.avg_coverage,
|
|
330
|
+
gate: row.gate
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Get files with highest coverage
|
|
335
|
+
*/
|
|
336
|
+
async getHighestCoverageFiles(limit = 10) {
|
|
337
|
+
const rows = await this.dbAll(`SELECT
|
|
338
|
+
file_path,
|
|
339
|
+
AVG(line_coverage) as avg_coverage,
|
|
340
|
+
gate
|
|
341
|
+
FROM coverage_metrics
|
|
342
|
+
GROUP BY file_path
|
|
343
|
+
ORDER BY avg_coverage DESC
|
|
344
|
+
LIMIT ?`, [limit]);
|
|
345
|
+
return rows.map(row => ({
|
|
346
|
+
filePath: row.file_path,
|
|
347
|
+
coverage: row.avg_coverage,
|
|
348
|
+
gate: row.gate
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get coverage statistics across all runs
|
|
353
|
+
*/
|
|
354
|
+
async getStatistics() {
|
|
355
|
+
const totalRuns = await this.dbGet(`SELECT COUNT(DISTINCT run_id) as count FROM coverage_summary`, []);
|
|
356
|
+
const averages = await this.dbGet(`SELECT
|
|
357
|
+
AVG(line_coverage) as line,
|
|
358
|
+
AVG(branch_coverage) as branch,
|
|
359
|
+
AVG(function_coverage) as function
|
|
360
|
+
FROM coverage_summary`, []);
|
|
361
|
+
const best = await this.dbGet(`SELECT run_id, line_coverage FROM coverage_summary ORDER BY line_coverage DESC LIMIT 1`, []);
|
|
362
|
+
const worst = await this.dbGet(`SELECT run_id, line_coverage FROM coverage_summary ORDER BY line_coverage ASC LIMIT 1`, []);
|
|
363
|
+
return {
|
|
364
|
+
totalRuns: totalRuns?.count || 0,
|
|
365
|
+
averageLineCoverage: averages?.line || 0,
|
|
366
|
+
averageBranchCoverage: averages?.branch || 0,
|
|
367
|
+
averageFunctionCoverage: averages?.function || 0,
|
|
368
|
+
bestCoverage: { runId: best?.run_id || '', coverage: best?.line_coverage || 0 },
|
|
369
|
+
worstCoverage: { runId: worst?.run_id || '', coverage: worst?.line_coverage || 0 }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Delete coverage data for a run
|
|
374
|
+
*/
|
|
375
|
+
async deleteCoverage(runId) {
|
|
376
|
+
await this.dbRun(`DELETE FROM coverage_metrics WHERE run_id = ?`, [runId]);
|
|
377
|
+
await this.dbRun(`DELETE FROM coverage_summary WHERE run_id = ?`, [runId]);
|
|
378
|
+
await this.dbRun(`DELETE FROM coverage_trends WHERE run_id = ?`, [runId]);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get recent coverage changes (comparison with previous run)
|
|
382
|
+
*/
|
|
383
|
+
async getRecentChanges(limit = 5) {
|
|
384
|
+
const rows = await this.dbAll(`SELECT
|
|
385
|
+
run_id,
|
|
386
|
+
created_at as timestamp,
|
|
387
|
+
line_coverage
|
|
388
|
+
FROM coverage_summary
|
|
389
|
+
ORDER BY created_at DESC
|
|
390
|
+
LIMIT ?`, [limit * 2] // Get extra to calculate changes
|
|
391
|
+
);
|
|
392
|
+
const result = [];
|
|
393
|
+
for (let i = 0; i < Math.min(rows.length - 1, limit); i++) {
|
|
394
|
+
const current = rows[i];
|
|
395
|
+
const previous = rows[i + 1];
|
|
396
|
+
result.push({
|
|
397
|
+
runId: current.run_id,
|
|
398
|
+
timestamp: current.timestamp,
|
|
399
|
+
lineCoverage: current.line_coverage,
|
|
400
|
+
change: previous ? current.line_coverage - previous.line_coverage : 0
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Assets - HTML/CSS/JS for the web dashboard
|
|
3
|
+
* Embedded as strings to avoid external file dependencies
|
|
4
|
+
*/
|
|
5
|
+
export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>QA360 - Test Execution Dashboard</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n :root {\n --bg-primary: #0d1117;\n --bg-secondary: #161b22;\n --bg-tertiary: #21262d;\n --border: #30363d;\n --text-primary: #c9d1d9;\n --text-secondary: #8b949e;\n --accent: #58a6ff;\n --success: #3fb950;\n --danger: #f85149;\n --warning: #d29922;\n --pending: #8b949e;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n line-height: 1.5;\n }\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 20px;\n }\n .header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n margin-bottom: 20px;\n }\n .header h1 {\n font-size: 24px;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n .badge {\n padding: 4px 12px;\n border-radius: 20px;\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n }\n .badge.idle { background: var(--bg-tertiary); color: var(--pending); }\n .badge.running { background: rgba(88, 166, 255, 0.15); color: var(--accent); animation: pulse 2s infinite; }\n .badge.completed { background: rgba(63, 185, 80, 0.15); color: var(--success); }\n .badge.failed { background: rgba(248, 81, 73, 0.15); color: var(--danger); }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.7; }\n }\n .stats {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 15px;\n margin-bottom: 20px;\n }\n .stat-card {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 20px;\n }\n .stat-label {\n font-size: 12px;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n .stat-value {\n font-size: 32px;\n font-weight: 700;\n margin-top: 5px;\n }\n .stat-value.success { color: var(--success); }\n .stat-value.danger { color: var(--danger); }\n .progress-section {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 20px;\n margin-bottom: 20px;\n }\n .progress-bar {\n height: 8px;\n background: var(--bg-tertiary);\n border-radius: 4px;\n overflow: hidden;\n margin-top: 10px;\n }\n .progress-fill {\n height: 100%;\n background: linear-gradient(90deg, var(--accent), var(--success));\n transition: width 0.3s ease;\n }\n .gates-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n gap: 15px;\n }\n .gate-card {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 15px;\n transition: all 0.2s ease;\n }\n .gate-card:hover {\n border-color: var(--accent);\n }\n .gate-card.running {\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n .gate-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 10px;\n }\n .gate-name {\n font-weight: 600;\n }\n .gate-status {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n }\n .gate-status.pending { background: var(--pending); }\n .gate-status.running { background: var(--accent); animation: pulse 1s infinite; }\n .gate-status.passed { background: var(--success); }\n .gate-status.failed { background: var(--danger); }\n .gate-status.skipped { background: var(--pending); }\n .gate-tests {\n font-size: 13px;\n color: var(--text-secondary);\n }\n .gate-duration {\n font-size: 12px;\n color: var(--text-secondary);\n margin-top: 5px;\n }\n .footer {\n text-align: center;\n padding: 20px;\n color: var(--text-secondary);\n font-size: 13px;\n }\n .reconnect-info {\n text-align: center;\n padding: 10px;\n background: var(--bg-tertiary);\n border-radius: 4px;\n margin-bottom: 20px;\n font-size: 12px;\n color: var(--text-secondary);\n }\n .flakiness-section {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 20px;\n margin-bottom: 20px;\n }\n .flakiness-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 15px;\n }\n .flakiness-title {\n font-size: 18px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .flakiness-score {\n font-size: 48px;\n font-weight: 700;\n text-align: center;\n padding: 15px;\n border-radius: 8px;\n background: var(--bg-tertiary);\n }\n .flakiness-score.legendary { color: #3fb950; }\n .flakiness-score.solid { color: #3fb950; }\n .flakiness-score.good { color: #d29922; }\n .flakiness-score.shaky { color: #d29922; }\n .flakiness-score.unstable { color: #f85149; }\n .flakiness-score-label {\n text-align: center;\n font-size: 14px;\n color: var(--text-secondary);\n margin-top: 5px;\n }\n .flakiness-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n gap: 10px;\n margin-top: 15px;\n }\n .flakiness-dist-item {\n background: var(--bg-tertiary);\n border-radius: 6px;\n padding: 10px;\n text-align: center;\n }\n .flakiness-dist-emoji {\n font-size: 20px;\n }\n .flakiness-dist-label {\n font-size: 11px;\n color: var(--text-secondary);\n margin-top: 4px;\n }\n .flakiness-dist-value {\n font-size: 20px;\n font-weight: 600;\n margin-top: 4px;\n }\n .quarantine-list {\n margin-top: 15px;\n }\n .quarantine-item {\n background: var(--bg-tertiary);\n border-left: 3px solid var(--danger);\n border-radius: 4px;\n padding: 12px;\n margin-bottom: 10px;\n }\n .quarantine-item.resolved {\n border-left-color: var(--success);\n opacity: 0.7;\n }\n .quarantine-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 6px;\n }\n .quarantine-test-name {\n font-weight: 600;\n }\n .quarantine-badge {\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n }\n .quarantine-badge.active {\n background: rgba(248, 81, 73, 0.15);\n color: var(--danger);\n }\n .quarantine-badge.resolved {\n background: rgba(63, 185, 80, 0.15);\n color: var(--success);\n }\n .quarantine-reason {\n font-size: 13px;\n color: var(--text-secondary);\n }\n .quarantine-meta {\n font-size: 11px;\n color: var(--text-secondary);\n margin-top: 6px;\n }\n .patterns-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 10px;\n margin-top: 15px;\n }\n .pattern-card {\n background: var(--bg-tertiary);\n border-radius: 6px;\n padding: 12px;\n }\n .pattern-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n }\n .pattern-type {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n color: var(--accent);\n }\n .pattern-confidence {\n font-size: 11px;\n padding: 2px 6px;\n border-radius: 4px;\n background: rgba(88, 166, 255, 0.15);\n color: var(--accent);\n }\n .pattern-test {\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 4px;\n }\n .pattern-description {\n font-size: 12px;\n color: var(--text-secondary);\n margin-bottom: 6px;\n }\n .pattern-fix {\n font-size: 11px;\n color: var(--success);\n }\n .section-tabs {\n display: flex;\n gap: 10px;\n margin-bottom: 15px;\n border-bottom: 1px solid var(--border);\n }\n .tab {\n padding: 8px 16px;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n color: var(--text-secondary);\n transition: all 0.2s;\n }\n .tab:hover {\n color: var(--text-primary);\n }\n .tab.active {\n color: var(--accent);\n border-bottom-color: var(--accent);\n }\n .tab-content {\n display: none;\n }\n .tab-content.active {\n display: block;\n }\n .empty-state {\n text-align: center;\n padding: 30px;\n color: var(--text-secondary);\n }\n .empty-state-icon {\n font-size: 40px;\n margin-bottom: 10px;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M12 2L2 7l10 5 10-5-10-5z\"/>\n <path d=\"M2 17l10 5 10-5\"/>\n <path d=\"M2 12l10 5 10-5\"/>\n </svg>\n QA360 Dashboard\n </h1>\n <span id=\"status-badge\" class=\"badge idle\">Idle</span>\n </div>\n\n <div class=\"reconnect-info\" id=\"connection-status\">\n Connecting...\n </div>\n\n <div class=\"stats\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Gates</div>\n <div class=\"stat-value\" id=\"total-gates\">0</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Passed</div>\n <div class=\"stat-value success\" id=\"passed-gates\">0</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Failed</div>\n <div class=\"stat-value danger\" id=\"failed-gates\">0</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Duration</div>\n <div class=\"stat-value\" id=\"duration\">0s</div>\n </div>\n </div>\n\n <div class=\"progress-section\">\n <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n <span id=\"phase-label\">Ready to start</span>\n <span id=\"progress-percent\">0%</span>\n </div>\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" id=\"progress-fill\" style=\"width: 0%\"></div>\n </div>\n </div>\n\n <!-- Flakiness Section -->\n <div class=\"flakiness-section\" id=\"flakiness-section\" style=\"display: none;\">\n <div class=\"flakiness-header\">\n <div class=\"flakiness-title\">\n <span>\uD83C\uDFB2</span>\n <span>Test Flakiness</span>\n </div>\n <span id=\"flakiness-badge\" class=\"badge\"></span>\n </div>\n\n <div class=\"section-tabs\">\n <div class=\"tab active\" data-tab=\"overview\">Overview</div>\n <div class=\"tab\" data-tab=\"quarantine\">Quarantine</div>\n <div class=\"tab\" data-tab=\"patterns\">Patterns</div>\n </div>\n\n <!-- Overview Tab -->\n <div class=\"tab-content active\" id=\"tab-overview\">\n <div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;\">\n <div>\n <div class=\"flakiness-score\" id=\"flakiness-score\">-</div>\n <div class=\"flakiness-score-label\">Average Score</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Tests</div>\n <div class=\"stat-value\" id=\"flakiness-total\">0</div>\n </div>\n </div>\n\n <div class=\"flakiness-grid\">\n <div class=\"flakiness-dist-item\">\n <div class=\"flakiness-dist-emoji\">\uD83D\uDFE2</div>\n <div class=\"flakiness-dist-label\">Legendary</div>\n <div class=\"flakiness-dist-value\" id=\"dist-legendary\">0</div>\n </div>\n <div class=\"flakiness-dist-item\">\n <div class=\"flakiness-dist-emoji\">\uD83D\uDFE2</div>\n <div class=\"flakiness-dist-label\">Solid</div>\n <div class=\"flakiness-dist-value\" id=\"dist-solid\">0</div>\n </div>\n <div class=\"flakiness-dist-item\">\n <div class=\"flakiness-dist-emoji\">\uD83D\uDFE1</div>\n <div class=\"flakiness-dist-label\">Good</div>\n <div class=\"flakiness-dist-value\" id=\"dist-good\">0</div>\n </div>\n <div class=\"flakiness-dist-item\">\n <div class=\"flakiness-dist-emoji\">\uD83D\uDFE0</div>\n <div class=\"flakiness-dist-label\">Shaky</div>\n <div class=\"flakiness-dist-value\" id=\"dist-shaky\">0</div>\n </div>\n <div class=\"flakiness-dist-item\">\n <div class=\"flakiness-dist-emoji\">\uD83D\uDD34</div>\n <div class=\"flakiness-dist-label\">Unstable</div>\n <div class=\"flakiness-dist-value\" id=\"dist-unstable\">0</div>\n </div>\n </div>\n </div>\n\n <!-- Quarantine Tab -->\n <div class=\"tab-content\" id=\"tab-quarantine\">\n <div id=\"quarantine-list\" class=\"quarantine-list\"></div>\n </div>\n\n <!-- Patterns Tab -->\n <div class=\"tab-content\" id=\"tab-patterns\">\n <div id=\"patterns-grid\" class=\"patterns-grid\"></div>\n </div>\n </div>\n\n <div class=\"gates-grid\" id=\"gates-container\"></div>\n\n <div class=\"footer\">\n QA360 v1.4.7 - Real-time test execution monitoring\n </div>\n </div>\n\n <script>\n let eventSource = null;\n let reconnectTimer = null;\n\n // Tab switching\n document.querySelectorAll('.tab').forEach(tab => {\n tab.addEventListener('click', () => {\n const tabName = tab.dataset.tab;\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n tab.classList.add('active');\n document.getElementById('tab-' + tabName).classList.add('active');\n });\n });\n\n function connect() {\n eventSource = new EventSource('/events');\n\n eventSource.onopen = () => {\n console.log('Connected to dashboard');\n document.getElementById('connection-status').textContent = 'Connected';\n document.getElementById('connection-status').style.display = 'none';\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n };\n\n eventSource.onmessage = (event) => {\n const state = JSON.parse(event.data);\n updateState(state);\n };\n\n eventSource.onerror = (error) => {\n console.error('SSE error:', error);\n document.getElementById('connection-status').textContent = 'Disconnected, reconnecting...';\n document.getElementById('connection-status').style.display = 'block';\n if (eventSource) {\n eventSource.close();\n }\n reconnectTimer = setTimeout(connect, 2000);\n };\n }\n\n function updateState(state) {\n // Update status badge\n const badge = document.getElementById('status-badge');\n badge.className = 'badge ' + state.status;\n badge.textContent = state.status;\n\n // Update stats\n document.getElementById('total-gates').textContent = state.gates.length;\n document.getElementById('passed-gates').textContent =\n state.gates.filter(g => g.status === 'passed').length;\n document.getElementById('failed-gates').textContent =\n state.gates.filter(g => g.status === 'failed').length;\n document.getElementById('duration').textContent = formatDuration(state.elapsed);\n\n // Update progress\n document.getElementById('phase-label').textContent = state.currentPhase || 'Ready';\n document.getElementById('progress-percent').textContent = state.progress.toFixed(1) + '%';\n document.getElementById('progress-fill').style.width = state.progress + '%';\n\n // Update flakiness section\n updateFlakiness(state.flakiness);\n\n // Update gates\n renderGates(state.gates);\n }\n\n function updateFlakiness(flakiness) {\n const section = document.getElementById('flakiness-section');\n if (!flakiness || !flakiness.enabled) {\n section.style.display = 'none';\n return;\n }\n\n section.style.display = 'block';\n\n // Update badge\n const badge = document.getElementById('flakiness-badge');\n const scoreClass = getScoreClass(flakiness.averageScore);\n badge.className = 'badge ' + scoreClass;\n badge.textContent = Math.round(flakiness.averageScore) + '%';\n\n // Update score display\n const scoreEl = document.getElementById('flakiness-score');\n scoreEl.textContent = Math.round(flakiness.averageScore) + '%';\n scoreEl.className = 'flakiness-score ' + scoreClass;\n\n // Update total tests\n document.getElementById('flakiness-total').textContent = flakiness.totalTests;\n\n // Update distribution\n document.getElementById('dist-legendary').textContent = flakiness.distribution.legendary;\n document.getElementById('dist-solid').textContent = flakiness.distribution.solid;\n document.getElementById('dist-good').textContent = flakiness.distribution.good;\n document.getElementById('dist-shaky').textContent = flakiness.distribution.shaky;\n document.getElementById('dist-unstable').textContent = flakiness.distribution.unstable;\n\n // Update quarantine list\n renderQuarantine(flakiness.quarantined);\n\n // Update patterns\n renderPatterns(flakiness.recentPatterns);\n }\n\n function getScoreClass(score) {\n if (score >= 90) return 'legendary';\n if (score >= 75) return 'good';\n if (score >= 50) return 'shaky';\n return 'unstable';\n }\n\n function renderQuarantine(quarantined) {\n const container = document.getElementById('quarantine-list');\n if (!quarantined || quarantined.length === 0) {\n container.innerHTML = `\n <div class=\"empty-state\">\n <div class=\"empty-state-icon\">\uD83D\uDEAB</div>\n <div>No tests in quarantine</div>\n </div>\n `;\n return;\n }\n\n container.innerHTML = quarantined.map(q => `\n <div class=\"quarantine-item ${q.resolved ? 'resolved' : ''}\">\n <div class=\"quarantine-header\">\n <span class=\"quarantine-test-name\">${q.testName}</span>\n <span class=\"quarantine-badge ${q.resolved ? 'resolved' : 'active'}\">\n ${q.resolved ? 'Resolved' : 'Active'}\n </span>\n </div>\n <div class=\"quarantine-reason\">${q.reason}</div>\n <div class=\"quarantine-meta\">\n ${q.gate} \u2022 Score: ${q.score}% \u2022 ${new Date(q.quarantinedAt).toLocaleDateString()}\n </div>\n </div>\n `).join('');\n }\n\n function renderPatterns(patterns) {\n const container = document.getElementById('patterns-grid');\n if (!patterns || patterns.length === 0) {\n container.innerHTML = `\n <div class=\"empty-state\" style=\"grid-column: 1 / -1;\">\n <div class=\"empty-state-icon\">\uD83D\uDD0D</div>\n <div>No flakiness patterns detected</div>\n </div>\n `;\n return;\n }\n\n container.innerHTML = patterns.map(p => `\n <div class=\"pattern-card\">\n <div class=\"pattern-header\">\n <span class=\"pattern-type\">${p.patternType.replace(/_/g, ' ')}</span>\n <span class=\"pattern-confidence\">${Math.round(p.confidence * 100)}% confidence</span>\n </div>\n <div class=\"pattern-test\">${p.testName}</div>\n <div class=\"pattern-description\">${p.description}</div>\n <div class=\"pattern-fix\">\uD83D\uDCA1 ${p.suggestedFix}</div>\n <div style=\"font-size: 10px; color: var(--text-secondary); margin-top: 6px;\">\n Detected ${p.detectionCount}x\n </div>\n </div>\n `).join('');\n }\n\n function renderGates(gates) {\n const container = document.getElementById('gates-container');\n container.innerHTML = gates.map(gate => `\n <div class=\"gate-card ${gate.status}\">\n <div class=\"gate-header\">\n <span class=\"gate-name\">${gate.name}</span>\n <span class=\"gate-status ${gate.status}\"></span>\n </div>\n <div class=\"gate-tests\">\n ${gate.testsTotal ? `${gate.testsPassed || 0}/${gate.testsTotal}` : '--'}\n ${gate.testsFailed ? ` (${gate.testsFailed} failed)` : ''}\n </div>\n ${gate.duration ? `<div class=\"gate-duration\">${formatDuration(gate.duration)}</div>` : ''}\n </div>\n `).join('');\n }\n\n function formatDuration(ms) {\n if (ms < 1000) return ms + 'ms';\n if (ms < 60000) return (ms / 1000).toFixed(1) + 's';\n const min = Math.floor(ms / 60000);\n const sec = Math.floor((ms % 60000) / 1000);\n return min + 'm ' + sec + 's';\n }\n\n // Auto-connect\n connect();\n </script>\n</body>\n</html>";
|
|
6
|
+
export declare const DASHBOARD_FAVICON = "data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><text y=\".9em\" font-size=\"90\">\uD83D\uDCCA</text></svg>";
|