Add frontend code coverage via Istanbul instrumentation + Playwright

- Install nyc for Istanbul instrumentation
- Add scripts/instrument-frontend.sh to instrument public/*.js
- Add scripts/collect-frontend-coverage.js to extract window.__coverage__
- Add scripts/combined-coverage.sh for combined server+frontend coverage
- Make server.js serve public-instrumented/ when COVERAGE=1 is set
- Add test:full-coverage npm script
- Add public-instrumented/ and .nyc_output/ to .gitignore
This commit is contained in:
you
2026-03-24 03:11:13 +00:00
parent 2040b36a63
commit d7faa4d978
7 changed files with 1783 additions and 2 deletions

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ config-lincomatic.json
theme.json
firmware/
coverage/
public-instrumented/
.nyc_output/

1694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"scripts": {
"test": "npx c8 --reporter=text --reporter=text-summary sh test-all.sh",
"test:unit": "node test-packet-filter.js && node test-aging.js && node test-regional-filter.js",
"test:coverage": "npx c8 --reporter=text --reporter=html sh test-all.sh"
"test:coverage": "npx c8 --reporter=text --reporter=html sh test-all.sh",
"test:full-coverage": "sh scripts/combined-coverage.sh"
},
"keywords": [],
"author": "",
@@ -19,6 +20,7 @@
"ws": "^8.19.0"
},
"devDependencies": {
"nyc": "^18.0.0",
"playwright": "^1.58.2",
"supertest": "^7.2.2"
}

View File

@@ -0,0 +1,46 @@
// After Playwright tests, this script:
// 1. Connects to the running test server
// 2. Extracts window.__coverage__ from the browser
// 3. Writes it to .nyc_output/ for merging
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
async function collectCoverage() {
const browser = await chromium.launch({
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
args: ['--no-sandbox'],
headless: true
});
const page = await browser.newPage();
const BASE = process.env.BASE_URL || 'http://localhost:13581';
// Visit every major page to exercise the code
const pages = ['#/home', '#/nodes', '#/map', '#/packets', '#/channels', '#/analytics', '#/live', '#/traces', '#/observers'];
for (const hash of pages) {
await page.goto(`${BASE}/${hash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
}
// Exercise some interactions
try {
await page.click('#customizeToggle');
await page.waitForTimeout(1000);
} catch {}
// Extract coverage
const coverage = await page.evaluate(() => window.__coverage__);
await browser.close();
if (coverage) {
const outDir = path.join(__dirname, '..', '.nyc_output');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'frontend-coverage.json'), JSON.stringify(coverage));
console.log('Frontend coverage collected: ' + Object.keys(coverage).length + ' files');
} else {
console.log('WARNING: No __coverage__ object found — instrumentation may have failed');
}
}
collectCoverage().catch(e => { console.error(e); process.exit(1); });

View File

@@ -0,0 +1,26 @@
#!/bin/sh
# Run server-side tests with c8, then frontend coverage with nyc
set -e
# 1. Server-side coverage (existing)
npx c8 --reporter=json --reports-dir=.nyc_output node tools/e2e-test.js
# 2. Instrument frontend
sh scripts/instrument-frontend.sh
# 3. Start instrumented server
COVERAGE=1 PORT=13581 node server.js &
SERVER_PID=$!
sleep 5
# 4. Run Playwright tests (exercises frontend code)
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
# 5. Collect browser coverage
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js
# 6. Kill server
kill $SERVER_PID 2>/dev/null || true
# 7. Generate combined report
npx nyc report --reporter=text-summary --reporter=text

View File

@@ -0,0 +1,10 @@
#!/bin/sh
# Instrument frontend JS for coverage tracking
rm -rf public-instrumented
npx nyc instrument public/ public-instrumented/ --compact=false
# Copy non-JS files (CSS, HTML, images) as-is
cp public/*.css public-instrumented/ 2>/dev/null
cp public/*.html public-instrumented/ 2>/dev/null
cp public/*.svg public-instrumented/ 2>/dev/null
cp public/*.png public-instrumented/ 2>/dev/null
echo "Frontend instrumented successfully"

View File

@@ -2891,7 +2891,8 @@ app.get('/api/audio-lab/buckets', (req, res) => {
});
// Static files + SPA fallback
app.use(express.static(path.join(__dirname, 'public'), {
const publicDir = process.env.COVERAGE === '1' ? 'public-instrumented' : 'public';
app.use(express.static(path.join(__dirname, publicDir), {
etag: false,
lastModified: false,
setHeaders: (res, filePath) => {