Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 91fa734

Browse files
committed
optimize test
1 parent 7c5da36 commit 91fa734

2 files changed

Lines changed: 136 additions & 172 deletions

File tree

scripts/generate-test-city-3d.js

Lines changed: 132 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,47 @@
11
#!/usr/bin/env node
22
/**
3-
* generate-test-city-3d.js
4-
* Creates an interactive 3D city visualization from actual Playwright test results
5-
* Each building represents a test, grouped by test suite
3+
* generate-test-city-3d.js (patched 2025‑08‑02)
4+
* Creates an interactive 3D city visualization from Playwright test results.
5+
* Each building represents a test, grouped by test suite.
66
*/
77

8-
const fs = require('fs');
8+
/* eslint-disable no-console */
9+
10+
const fs = require('fs');
911
const path = require('path');
1012

11-
const ART = 'artifacts';
13+
// ────────────────────────────────────────────────────────────
14+
// Constants / helpers
15+
// ────────────────────────────────────────────────────────────
16+
const ART = 'artifacts'; // dashboard output folder
17+
const HISTORY_FILE = path.join(ART, 'test-history-insights.json'); // output from track-test-history.js
1218

13-
// Helper to read JSON safely
14-
const readJSON = (filepath, defaultValue = null) => {
19+
// Safe JSON reader
20+
function readJSON (filepath, def = null) {
1521
try {
1622
return JSON.parse(fs.readFileSync(filepath, 'utf8'));
17-
} catch (e) {
18-
console.warn(`Could not read ${filepath}:`, e.message);
19-
return defaultValue;
23+
} catch (err) {
24+
if (err.code !== 'ENOENT') {
25+
console.warn(`⚠️ Could not parse ${filepath}:`, err.message);
26+
}
27+
return def;
2028
}
21-
};
29+
}
2230

23-
// Extract comprehensive test data from Playwright metrics
24-
function extractTestData() {
25-
const possiblePaths = [
31+
// ────────────────────────────────────────────────────────────
32+
// Extract all test data we have (metrics + history)
33+
// ────────────────────────────────────────────────────────────
34+
function extractTestData () {
35+
// Prefer the rich metrics file emitted by Playwright reporter
36+
const candidatePaths = [
2637
'playwright-metrics.json',
2738
path.join(ART, 'playwright-metrics.json'),
28-
path.join(ART, 'playwright-summary-pr.json'),
29-
path.join(ART, 'playwright-summary.json')
39+
path.join(ART, 'playwright-summary-pr.json'), // minimal
40+
path.join(ART, 'playwright-summary.json') // alias
3041
];
31-
42+
3243
let metrics = null;
33-
for (const p of possiblePaths) {
44+
for (const p of candidatePaths) {
3445
if (fs.existsSync(p)) {
3546
metrics = readJSON(p);
3647
if (metrics && (metrics.suites || metrics.total)) {
@@ -39,157 +50,116 @@ function extractTestData() {
3950
}
4051
}
4152
}
42-
43-
// If no detailed metrics found, try to use summary
53+
54+
// Fall‑back: if we only have a summary, return one pseudo‑suite so the UI still works
4455
if (!metrics || !metrics.suites) {
4556
const summary = readJSON(path.join(ART, 'playwright-summary-pr.json'));
4657
if (summary && summary.total) {
47-
// Create a minimal structure from summary
4858
console.log('Using summary data for visualization');
4959
return [{
50-
id: 'summary-tests',
51-
suite: 'All Tests',
52-
describe: 'Summary',
53-
name: `${summary.passed} passed, ${summary.failed} failed`,
54-
duration: summary.duration || 0,
55-
passRate: summary.pass_rate || 0,
56-
passed: summary.passed || 0,
57-
failed: summary.failed || 0,
58-
total: summary.total || 0,
59-
lastStatus: summary.failed > 0 ? 'failed' : 'passed',
60-
priority: 1
60+
id: 'summary-tests',
61+
suite: 'All Tests',
62+
describe: 'Summary',
63+
name: `${summary.passed} passed, ${summary.failed} failed`,
64+
duration: summary.duration || 0,
65+
passRate: summary.pass_rate || 0,
66+
passed: summary.passed || 0,
67+
failed: summary.failed || 0,
68+
total: summary.total || 0,
69+
lastStatus: summary.failed > 0 ? 'failed' : 'passed',
70+
priority: 1
6171
}];
6272
}
73+
// absolutely nothing to show
74+
return [];
6375
}
64-
76+
77+
// Load history if it exists (for flakiness calc)
78+
const historyData = readJSON(HISTORY_FILE, { tests: {} });
79+
const historyMap = historyData.tests || {};
80+
6581
const testData = [];
66-
const testHistoryMap = {};
67-
68-
// Build history map for flakiness calculation
69-
if (history && history.tests) {
70-
Object.entries(history.tests).forEach(([testName, data]) => {
71-
testHistoryMap[testName] = data;
72-
});
73-
}
74-
75-
// Process each test suite
76-
metrics.suites.forEach((suite, suiteIdx) => {
77-
const suiteName = path.basename(suite.file || `suite-${suiteIdx}`)
78-
.replace(/\.(spec|test)\.(js|ts|jsx|tsx)$/, '');
79-
80-
// Get suite location info
81-
const line = suite.line || 0;
82-
const column = suite.column || 0;
83-
84-
suite.suites.forEach((describe, describeIdx) => {
85-
describe.specs.forEach((spec, specIdx) => {
86-
const fullTestName = `${suiteName} > ${describe.title} > ${spec.title}`;
87-
88-
// Get all test attempts
89-
const attempts = spec.tests || [];
90-
let totalDuration = 0;
91-
let passCount = 0;
92-
let failCount = 0;
93-
let skipCount = 0;
94-
let lastStatus = 'unknown';
95-
let errors = [];
96-
97-
attempts.forEach(test => {
98-
test.results.forEach(result => {
99-
totalDuration += result.duration || 0;
100-
101-
if (result.status === 'passed' || result.status === 'expected') {
102-
passCount++;
103-
lastStatus = 'passed';
104-
} else if (result.status === 'failed' || result.status === 'unexpected') {
105-
failCount++;
106-
lastStatus = 'failed';
107-
if (result.error) {
108-
errors.push({
109-
message: result.error.message,
110-
stack: result.error.stack
111-
});
112-
}
113-
} else if (result.status === 'skipped') {
114-
skipCount++;
115-
lastStatus = 'skipped';
116-
}
82+
83+
metrics.suites.forEach((suite, sIdx) => {
84+
const suiteName = path.basename(suite.file || `suite-${sIdx}`)
85+
.replace(/\.(spec|test)\.(jsx?|tsx?)$/, '');
86+
87+
suite.suites.forEach((describe, dIdx) => {
88+
describe.specs.forEach((spec, tIdx) => {
89+
const fullName = `${suiteName} > ${describe.title} > ${spec.title}`;
90+
const attempts = spec.tests || [];
91+
92+
let durationTotal = 0;
93+
let passed = 0, failed = 0, skipped = 0, lastStatus = 'unknown';
94+
const errors = [];
95+
96+
attempts.forEach(attempt => {
97+
attempt.results.forEach(r => {
98+
durationTotal += r.duration || 0;
99+
if (r.status === 'passed' || r.status === 'expected') { passed++; lastStatus = 'passed'; }
100+
else if (r.status === 'failed' || r.status === 'unexpected') { failed++; lastStatus = 'failed'; }
101+
else if (r.status === 'skipped') { skipped++; lastStatus = 'skipped'; }
102+
if (r.error) errors.push({ message: r.error.message, stack: r.error.stack });
117103
});
118104
});
119-
120-
const totalRuns = passCount + failCount + skipCount;
121-
const avgDuration = totalRuns > 0 ? totalDuration / totalRuns : 0;
122-
const passRate = totalRuns > 0 ? (passCount / totalRuns) * 100 : 0;
123-
124-
// Calculate flakiness from history or current runs
105+
106+
const runs = passed + failed + skipped;
107+
const avgDur = runs ? durationTotal / runs : 0;
108+
const passRate = runs ? (passed / runs) * 100 : 0;
109+
110+
// flakiness: prefer history, else derive from current attempts
125111
let flakiness = 0;
126-
const historyData = testHistoryMap[fullTestName];
127-
if (historyData) {
128-
flakiness = historyData.flakiness || 0;
129-
} else if (totalRuns > 1 && passCount > 0 && failCount > 0) {
130-
// Test both passed and failed in current run = flaky
131-
flakiness = 50;
112+
if (historyMap[fullName]) {
113+
flakiness = historyMap[fullName].flakiness || 0;
114+
} else if (runs > 1 && passed > 0 && failed > 0) {
115+
flakiness = 50; // simplistic
132116
}
133-
134-
// Determine test category
117+
118+
// tag category
135119
let category = 'standard';
136-
if (spec.title.toLowerCase().includes('critical') ||
137-
describe.title.toLowerCase().includes('critical')) {
138-
category = 'critical';
139-
} else if (spec.title.toLowerCase().includes('smoke') ||
140-
describe.title.toLowerCase().includes('smoke')) {
141-
category = 'smoke';
142-
} else if (spec.title.toLowerCase().includes('regression')) {
143-
category = 'regression';
144-
}
145-
120+
const loTitle = spec.title.toLowerCase();
121+
const loDesc = describe.title.toLowerCase();
122+
if (loTitle.includes('critical') || loDesc.includes('critical')) category = 'critical';
123+
else if (loTitle.includes('smoke') || loDesc.includes('smoke')) category = 'smoke';
124+
else if (loTitle.includes('regression')) category = 'regression';
125+
146126
testData.push({
147-
id: `${suiteName}-${describeIdx}-${specIdx}`,
127+
id: `${suiteName}-${dIdx}-${tIdx}`,
148128
suite: suiteName,
149129
describe: describe.title,
150130
name: spec.title,
151-
fullName: fullTestName,
152-
duration: avgDuration,
153-
totalDuration: totalDuration,
154-
passRate: passRate,
155-
runs: totalRuns,
156-
passed: passCount,
157-
failed: failCount,
158-
skipped: skipCount,
159-
lastStatus: lastStatus,
160-
flakiness: flakiness,
161-
category: category,
162-
line: spec.line || line,
163-
column: spec.column || column,
164-
errors: errors,
165-
// Calculate priority for height
166-
priority: calculateTestPriority(avgDuration, passRate, flakiness, category)
131+
fullName,
132+
duration: avgDur,
133+
totalDuration: durationTotal,
134+
passRate,
135+
runs,
136+
passed,
137+
failed,
138+
skipped,
139+
lastStatus,
140+
flakiness,
141+
category,
142+
line: spec.line || suite.line || 0,
143+
column: spec.column || suite.column || 0,
144+
errors,
145+
priority: calcPriority(avgDur, passRate, flakiness, category)
167146
});
168147
});
169148
});
170149
});
171-
150+
172151
return testData;
173152
}
174153

175-
// Calculate test priority (affects building height)
176-
function calculateTestPriority(duration, passRate, flakiness, category) {
177-
let priority = 1;
178-
179-
// Category weight
180-
if (category === 'critical') priority *= 2;
181-
else if (category === 'smoke') priority *= 1.5;
182-
183-
// Duration weight (longer = higher priority)
184-
priority *= (1 + duration / 5000);
185-
186-
// Failure weight (lower pass rate = higher priority)
187-
priority *= (2 - passRate / 100);
188-
189-
// Flakiness weight
190-
if (flakiness > 30) priority *= 1.5;
191-
192-
return priority;
154+
function calcPriority (duration, passRate, flakiness, category) {
155+
let p = 1;
156+
if (category === 'critical') p *= 2;
157+
else if (category === 'smoke') p *= 1.5;
158+
159+
p *= 1 + duration / 5_000; // slower → taller
160+
p *= 2 - passRate / 100; // failing → taller
161+
if (flakiness > 30) p *= 1.5; // flaky → taller
162+
return p;
193163
}
194164

195165
// Generate the 3D city HTML
@@ -858,40 +828,30 @@ init();
858828
}
859829

860830
// Main execution
861-
console.log('🏙️ Generating 3D Test City visualization...');
862-
831+
console.log('🏙️ Generating 3D Test City visualization…');
863832
const testData = extractTestData();
864-
if (testData.length === 0) {
865-
console.error('❌ No test data found to visualize');
833+
if (!testData.length) {
834+
console.error('❌ No test data found — nothing to visualise.');
866835
process.exit(1);
867836
}
837+
console.log(`📊 Visualising ${testData.length} tests`);
868838

869-
console.log(`📊 Found ${testData.length} tests to visualize`);
870-
871-
// Generate HTML
839+
// Generate HTML (using original giant template function)
872840
const html = generate3DCityHTML(testData);
873841

874-
// Save files
842+
// Ensure output dirs
875843
fs.mkdirSync(path.join(ART, 'web-report'), { recursive: true });
844+
876845
fs.writeFileSync(path.join(ART, 'web-report', 'test-city-3d.html'), html);
846+
fs.writeFileSync(path.join(ART, 'test-city-data.json'), JSON.stringify({
847+
generated: new Date().toISOString(),
848+
stats: {
849+
total: testData.length,
850+
passed: testData.filter(t => t.lastStatus === 'passed').length,
851+
failed: testData.filter(t => t.lastStatus === 'failed').length,
852+
flaky: testData.filter(t => t.flakiness > 30 ).length
853+
},
854+
tests: testData
855+
}, null, 2));
877856

878-
// Also save the processed test data for other tools
879-
fs.writeFileSync(
880-
path.join(ART, 'test-city-data.json'),
881-
JSON.stringify({
882-
generated: new Date().toISOString(),
883-
stats: {
884-
total: testData.length,
885-
passed: testData.filter(t => t.lastStatus === 'passed').length,
886-
failed: testData.filter(t => t.lastStatus === 'failed').length,
887-
flaky: testData.filter(t => t.flakiness > 30).length
888-
},
889-
tests: testData
890-
}, null, 2)
891-
);
892-
893-
console.log('✅ Test City 3D visualization generated');
894-
console.log('📍 Location: artifacts/web-report/test-city-3d.html');
895-
console.log('🏢 Building colors: Green (passing) → Orange (unstable) → Red (failing)');
896-
console.log('✨ Glowing buildings indicate flaky tests');
897-
console.log('⭐ Stars indicate critical tests');
857+
console.log('✅ 3D Test City generated → artifacts/web-report/test-city-3d.html');

tests/demo-todo-app.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ test.describe('New Todo', () => {
1919
// Create 1st todo.
2020
await newTodo.fill(TODO_ITEMS[0]);
2121
await newTodo.press('Enter');
22+
23+
await page.evaluate(() => {
24+
document.body.style.borderTop = '12px solid hotpink';
25+
});
2226

2327
// DELIBERATE FAILURE: Expect wrong text to generate a failed test screenshot
2428
await expect(page.getByTestId('todo-title')).toHaveText([

0 commit comments

Comments
 (0)