mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-26 11:57:21 +00:00
Compare commits
7 Commits
fix/ci-she
...
fix/readme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f2ef199b | ||
|
|
12d1174e39 | ||
|
|
96ed40a3e0 | ||
|
|
fb03fa80cf | ||
|
|
113f68aea7 | ||
|
|
f4cccdff2f | ||
|
|
5536b4c67c |
102
.github/workflows/deploy.yml
vendored
102
.github/workflows/deploy.yml
vendored
@@ -10,11 +10,6 @@ on:
|
|||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- 'LICENSE'
|
|
||||||
- '.gitignore'
|
|
||||||
- 'docs/**'
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -42,8 +37,23 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Skip if docs-only change
|
||||||
|
id: docs-check
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||||
|
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||||
|
if [ "$NON_DOCS" -eq 0 ]; then
|
||||||
|
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📄 Docs-only PR — skipping heavy CI"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up Go 1.22
|
- name: Set up Go 1.22
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.22'
|
||||||
@@ -52,6 +62,7 @@ jobs:
|
|||||||
cmd/ingestor/go.sum
|
cmd/ingestor/go.sum
|
||||||
|
|
||||||
- name: Build and test Go server (with coverage)
|
- name: Build and test Go server (with coverage)
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
run: |
|
run: |
|
||||||
set -e -o pipefail
|
set -e -o pipefail
|
||||||
cd cmd/server
|
cd cmd/server
|
||||||
@@ -61,6 +72,7 @@ jobs:
|
|||||||
go tool cover -func=server-coverage.out | tail -1
|
go tool cover -func=server-coverage.out | tail -1
|
||||||
|
|
||||||
- name: Build and test Go ingestor (with coverage)
|
- name: Build and test Go ingestor (with coverage)
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
run: |
|
run: |
|
||||||
set -e -o pipefail
|
set -e -o pipefail
|
||||||
cd cmd/ingestor
|
cd cmd/ingestor
|
||||||
@@ -70,6 +82,7 @@ jobs:
|
|||||||
go tool cover -func=ingestor-coverage.out | tail -1
|
go tool cover -func=ingestor-coverage.out | tail -1
|
||||||
|
|
||||||
- name: Verify proto syntax (all .proto files compile)
|
- name: Verify proto syntax (all .proto files compile)
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
echo "Installing protoc..."
|
echo "Installing protoc..."
|
||||||
@@ -84,7 +97,7 @@ jobs:
|
|||||||
echo "✅ All .proto files are syntactically valid"
|
echo "✅ All .proto files are syntactically valid"
|
||||||
|
|
||||||
- name: Generate Go coverage badges
|
- name: Generate Go coverage badges
|
||||||
if: always()
|
if: always() && steps.docs-check.outputs.docs_only != 'true'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .badges
|
mkdir -p .badges
|
||||||
|
|
||||||
@@ -144,21 +157,39 @@ jobs:
|
|||||||
node-test:
|
node-test:
|
||||||
name: "🧪 Node.js Tests"
|
name: "🧪 Node.js Tests"
|
||||||
runs-on: [self-hosted, Linux]
|
runs-on: [self-hosted, Linux]
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Skip if docs-only change
|
||||||
|
id: docs-check
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||||
|
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||||
|
if [ "$NON_DOCS" -eq 0 ]; then
|
||||||
|
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📄 Docs-only PR — skipping heavy CI"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up Node.js 22
|
- name: Set up Node.js 22
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
run: npm ci --production=false
|
run: npm ci --production=false
|
||||||
|
|
||||||
- name: Detect changed files
|
- name: Detect changed files
|
||||||
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
id: changes
|
id: changes
|
||||||
run: |
|
run: |
|
||||||
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
||||||
@@ -174,7 +205,7 @@ jobs:
|
|||||||
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
||||||
|
|
||||||
- name: Run backend tests with coverage
|
- name: Run backend tests with coverage
|
||||||
if: steps.changes.outputs.backend == 'true'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'true'
|
||||||
run: |
|
run: |
|
||||||
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
||||||
|
|
||||||
@@ -192,19 +223,23 @@ jobs:
|
|||||||
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: Run backend tests (quick, no coverage)
|
- name: Run backend tests (quick, no coverage)
|
||||||
if: steps.changes.outputs.backend == 'false'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'false'
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
|
|
||||||
- name: Install Playwright browser
|
- name: Install Playwright browser
|
||||||
if: steps.changes.outputs.frontend == 'true'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: npx playwright install chromium --with-deps 2>/dev/null || true
|
run: |
|
||||||
|
# Install chromium (skips download if already cached on self-hosted runner)
|
||||||
|
npx playwright install chromium 2>/dev/null || true
|
||||||
|
# Install system deps only if missing (apt-get is slow)
|
||||||
|
npx playwright install-deps chromium 2>/dev/null || true
|
||||||
|
|
||||||
- name: Instrument frontend JS for coverage
|
- name: Instrument frontend JS for coverage
|
||||||
if: steps.changes.outputs.frontend == 'true'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: sh scripts/instrument-frontend.sh
|
run: sh scripts/instrument-frontend.sh
|
||||||
|
|
||||||
- name: Start instrumented test server on port 13581
|
- name: Start instrumented test server on port 13581
|
||||||
if: steps.changes.outputs.frontend == 'true'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Kill any stale server on 13581
|
# Kill any stale server on 13581
|
||||||
fuser -k 13581/tcp 2>/dev/null || true
|
fuser -k 13581/tcp 2>/dev/null || true
|
||||||
@@ -227,19 +262,32 @@ jobs:
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Run Playwright E2E tests
|
- name: Run Playwright E2E + coverage collection concurrently
|
||||||
if: steps.changes.outputs.frontend == 'true'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
|
run: |
|
||||||
|
# Run E2E tests and coverage collection in parallel — both use the same server
|
||||||
- name: Collect frontend coverage report
|
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt &
|
||||||
if: always() && steps.changes.outputs.frontend == 'true'
|
E2E_PID=$!
|
||||||
|
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt &
|
||||||
|
COV_PID=$!
|
||||||
|
|
||||||
|
# Wait for both — E2E must pass, coverage is best-effort
|
||||||
|
E2E_EXIT=0
|
||||||
|
wait $E2E_PID || E2E_EXIT=$?
|
||||||
|
wait $COV_PID || true
|
||||||
|
|
||||||
|
# Fail if E2E failed
|
||||||
|
[ $E2E_EXIT -ne 0 ] && exit $E2E_EXIT
|
||||||
|
true
|
||||||
|
|
||||||
|
- name: Generate frontend coverage badges
|
||||||
|
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: |
|
run: |
|
||||||
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt
|
|
||||||
|
|
||||||
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
||||||
|
|
||||||
mkdir -p .badges
|
mkdir -p .badges
|
||||||
if [ -f .nyc_output/frontend-coverage.json ]; then
|
# Merge E2E + coverage collector data if both exist
|
||||||
|
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
|
||||||
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
||||||
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
||||||
FE_COVERAGE=${FE_COVERAGE:-0}
|
FE_COVERAGE=${FE_COVERAGE:-0}
|
||||||
@@ -252,7 +300,7 @@ jobs:
|
|||||||
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
||||||
|
|
||||||
- name: Stop test server
|
- name: Stop test server
|
||||||
if: always() && steps.changes.outputs.frontend == 'true'
|
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||||
run: |
|
run: |
|
||||||
if [ -f .server.pid ]; then
|
if [ -f .server.pid ]; then
|
||||||
kill $(cat .server.pid) 2>/dev/null || true
|
kill $(cat .server.pid) 2>/dev/null || true
|
||||||
@@ -261,12 +309,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run frontend E2E (quick, no coverage)
|
- name: Run frontend E2E (quick, no coverage)
|
||||||
if: steps.changes.outputs.frontend == 'false'
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'false'
|
||||||
run: |
|
run: |
|
||||||
fuser -k 13581/tcp 2>/dev/null || true
|
fuser -k 13581/tcp 2>/dev/null || true
|
||||||
PORT=13581 node server.js &
|
PORT=13581 node server.js &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 5
|
# Wait for server to be ready (up to 15s)
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
curl -sf http://localhost:13581/api/stats > /dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
||||||
kill $SERVER_PID 2>/dev/null || true
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
> High-performance mesh network analyzer powered by Go. Sub-millisecond packet queries, ~300 MB memory for 56K+ packets, real-time WebSocket broadcast, full channel decryption.
|
> High-performance mesh network analyzer powered by Go. Sub-millisecond packet queries, ~300 MB memory for 56K+ packets, real-time WebSocket broadcast, full channel decryption.
|
||||||
|
|
||||||
Self-hosted, open-source MeshCore packet analyzer — a community alternative to the closed-source `analyzer.letsmesh.net`. Collects MeshCore packets via MQTT, decodes them in real time, and presents a full web UI with live packet feed, interactive maps, channel chat, packet tracing, and per-node analytics.
|
Self-hosted, open-source MeshCore packet analyzer. Collects MeshCore packets via MQTT, decodes them in real time, and presents a full web UI with live packet feed, interactive maps, channel chat, packet tracing, and per-node analytics.
|
||||||
|
|
||||||
## ⚡ Performance
|
## ⚡ Performance
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ async function collectCoverage() {
|
|||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Home page — chooser...');
|
console.log(' [coverage] Home page — chooser...');
|
||||||
// Clear localStorage to get chooser
|
// Clear localStorage to get chooser
|
||||||
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
||||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Click "I'm new"
|
// Click "I'm new"
|
||||||
await safeClick('#chooseNew');
|
await safeClick('#chooseNew');
|
||||||
@@ -105,7 +105,7 @@ async function collectCoverage() {
|
|||||||
|
|
||||||
// Switch to experienced mode
|
// Switch to experienced mode
|
||||||
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
||||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await safeClick('#chooseExp');
|
await safeClick('#chooseExp');
|
||||||
|
|
||||||
// Interact with experienced home page
|
// Interact with experienced home page
|
||||||
@@ -120,7 +120,7 @@ async function collectCoverage() {
|
|||||||
// NODES PAGE
|
// NODES PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Nodes page...');
|
console.log(' [coverage] Nodes page...');
|
||||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Sort by EVERY column
|
// Sort by EVERY column
|
||||||
for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) {
|
for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) {
|
||||||
@@ -168,7 +168,7 @@ async function collectCoverage() {
|
|||||||
try {
|
try {
|
||||||
const firstNodeKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim());
|
const firstNodeKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim());
|
||||||
if (firstNodeKey) {
|
if (firstNodeKey) {
|
||||||
await page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Click tabs on detail page
|
// Click tabs on detail page
|
||||||
await clickAll('.tab-btn, [data-tab]', 10);
|
await clickAll('.tab-btn, [data-tab]', 10);
|
||||||
@@ -191,7 +191,7 @@ async function collectCoverage() {
|
|||||||
try {
|
try {
|
||||||
const firstKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => null);
|
const firstKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => null);
|
||||||
if (firstKey) {
|
if (firstKey) {
|
||||||
await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ async function collectCoverage() {
|
|||||||
// PACKETS PAGE
|
// PACKETS PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Packets page...');
|
console.log(' [coverage] Packets page...');
|
||||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Open filter bar
|
// Open filter bar
|
||||||
await safeClick('#filterToggleBtn');
|
await safeClick('#filterToggleBtn');
|
||||||
@@ -285,13 +285,13 @@ async function collectCoverage() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Navigate to specific packet by hash
|
// Navigate to specific packet by hash
|
||||||
await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
// MAP PAGE
|
// MAP PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Map page...');
|
console.log(' [coverage] Map page...');
|
||||||
await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/map`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Toggle controls panel
|
// Toggle controls panel
|
||||||
await safeClick('#mapControlsToggle');
|
await safeClick('#mapControlsToggle');
|
||||||
@@ -345,7 +345,7 @@ async function collectCoverage() {
|
|||||||
// ANALYTICS PAGE
|
// ANALYTICS PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Analytics page...');
|
console.log(' [coverage] Analytics page...');
|
||||||
await page.goto(`${BASE}/#/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/analytics`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Click EVERY analytics tab
|
// Click EVERY analytics tab
|
||||||
const analyticsTabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance'];
|
const analyticsTabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance'];
|
||||||
@@ -383,7 +383,7 @@ async function collectCoverage() {
|
|||||||
|
|
||||||
// Deep-link to each analytics tab via URL
|
// Deep-link to each analytics tab via URL
|
||||||
for (const tab of analyticsTabs) {
|
for (const tab of analyticsTabs) {
|
||||||
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Region filter on analytics
|
// Region filter on analytics
|
||||||
@@ -396,7 +396,7 @@ async function collectCoverage() {
|
|||||||
// CUSTOMIZE
|
// CUSTOMIZE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Customizer...');
|
console.log(' [coverage] Customizer...');
|
||||||
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await safeClick('#customizeToggle');
|
await safeClick('#customizeToggle');
|
||||||
|
|
||||||
// Click EVERY customizer tab
|
// Click EVERY customizer tab
|
||||||
@@ -503,7 +503,7 @@ async function collectCoverage() {
|
|||||||
// CHANNELS PAGE
|
// CHANNELS PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Channels page...');
|
console.log(' [coverage] Channels page...');
|
||||||
await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/channels`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
// Click channel rows/items
|
// Click channel rows/items
|
||||||
await clickAll('.channel-item, .channel-row, .channel-card', 3);
|
await clickAll('.channel-item, .channel-row, .channel-card', 3);
|
||||||
await clickAll('table tbody tr', 3);
|
await clickAll('table tbody tr', 3);
|
||||||
@@ -512,7 +512,7 @@ async function collectCoverage() {
|
|||||||
try {
|
try {
|
||||||
const channelHash = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()).catch(() => null);
|
const channelHash = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()).catch(() => null);
|
||||||
if (channelHash) {
|
if (channelHash) {
|
||||||
await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
@@ -520,7 +520,7 @@ async function collectCoverage() {
|
|||||||
// LIVE PAGE
|
// LIVE PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Live page...');
|
console.log(' [coverage] Live page...');
|
||||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// VCR controls
|
// VCR controls
|
||||||
await safeClick('#vcrPauseBtn');
|
await safeClick('#vcrPauseBtn');
|
||||||
@@ -603,14 +603,14 @@ async function collectCoverage() {
|
|||||||
// TRACES PAGE
|
// TRACES PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Traces page...');
|
console.log(' [coverage] Traces page...');
|
||||||
await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/traces`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await clickAll('table tbody tr', 3);
|
await clickAll('table tbody tr', 3);
|
||||||
|
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
// OBSERVERS PAGE
|
// OBSERVERS PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Observers page...');
|
console.log(' [coverage] Observers page...');
|
||||||
await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
// Click observer rows
|
// Click observer rows
|
||||||
const obsRows = await page.$$('table tbody tr, .observer-card, .observer-row');
|
const obsRows = await page.$$('table tbody tr, .observer-card, .observer-row');
|
||||||
for (let i = 0; i < Math.min(obsRows.length, 3); i++) {
|
for (let i = 0; i < Math.min(obsRows.length, 3); i++) {
|
||||||
@@ -631,7 +631,7 @@ async function collectCoverage() {
|
|||||||
// PERF PAGE
|
// PERF PAGE
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
console.log(' [coverage] Perf page...');
|
console.log(' [coverage] Perf page...');
|
||||||
await page.goto(`${BASE}/#/perf`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/perf`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await safeClick('#perfRefresh');
|
await safeClick('#perfRefresh');
|
||||||
await safeClick('#perfReset');
|
await safeClick('#perfReset');
|
||||||
|
|
||||||
@@ -641,14 +641,14 @@ async function collectCoverage() {
|
|||||||
console.log(' [coverage] App.js — router + global...');
|
console.log(' [coverage] App.js — router + global...');
|
||||||
|
|
||||||
// Navigate to bad route to trigger error/404
|
// Navigate to bad route to trigger error/404
|
||||||
await page.goto(`${BASE}/#/nonexistent-route`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/nonexistent-route`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
|
|
||||||
// Navigate to every route via hash
|
// Navigate to every route via hash
|
||||||
const allRoutes = ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf'];
|
const allRoutes = ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf'];
|
||||||
for (const route of allRoutes) {
|
for (const route of allRoutes) {
|
||||||
try {
|
try {
|
||||||
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
await new Promise(r => setTimeout(r, 200));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,14 +788,14 @@ async function collectCoverage() {
|
|||||||
console.log(' [coverage] Region filter...');
|
console.log(' [coverage] Region filter...');
|
||||||
try {
|
try {
|
||||||
// Open region filter on nodes page
|
// Open region filter on nodes page
|
||||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await safeClick('#nodesRegionFilter');
|
await safeClick('#nodesRegionFilter');
|
||||||
await clickAll('#nodesRegionFilter input[type="checkbox"]', 3);
|
await clickAll('#nodesRegionFilter input[type="checkbox"]', 3);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Region filter on packets
|
// Region filter on packets
|
||||||
try {
|
try {
|
||||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||||
await safeClick('#packetsRegionFilter');
|
await safeClick('#packetsRegionFilter');
|
||||||
await clickAll('#packetsRegionFilter input[type="checkbox"]', 3);
|
await clickAll('#packetsRegionFilter input[type="checkbox"]', 3);
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -807,7 +807,7 @@ async function collectCoverage() {
|
|||||||
for (const route of allRoutes) {
|
for (const route of allRoutes) {
|
||||||
try {
|
try {
|
||||||
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
||||||
await page.waitForLoadState('networkidle').catch(() => {});
|
await new Promise(r => setTimeout(r, 200));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -354,10 +354,17 @@ async function run() {
|
|||||||
await test('Packets clicking row shows detail pane', async () => {
|
await test('Packets clicking row shows detail pane', async () => {
|
||||||
// Fresh navigation to avoid stale row references from previous test
|
// Fresh navigation to avoid stale row references from previous test
|
||||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||||
|
// Wait for table rows AND initial API data to settle
|
||||||
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
const firstRow = await page.$('table tbody tr[data-action]');
|
const firstRow = await page.$('table tbody tr[data-action]');
|
||||||
assert(firstRow, 'No clickable packet rows found');
|
assert(firstRow, 'No clickable packet rows found');
|
||||||
await firstRow.click();
|
// Click the row and wait for the /packets/{hash} API response
|
||||||
|
const [response] = await Promise.all([
|
||||||
|
page.waitForResponse(resp => resp.url().includes('/packets/') && resp.status() === 200, { timeout: 15000 }),
|
||||||
|
firstRow.click(),
|
||||||
|
]);
|
||||||
|
assert(response, 'API response for packet detail not received');
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const panel = document.getElementById('pktRight');
|
const panel = document.getElementById('pktRight');
|
||||||
return panel && !panel.classList.contains('empty');
|
return panel && !panel.classList.contains('empty');
|
||||||
@@ -375,12 +382,16 @@ async function run() {
|
|||||||
if (!pktRight) {
|
if (!pktRight) {
|
||||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
}
|
}
|
||||||
const panelOpen = await page.$eval('#pktRight', el => !el.classList.contains('empty'));
|
const panelOpen = await page.$eval('#pktRight', el => !el.classList.contains('empty'));
|
||||||
if (!panelOpen) {
|
if (!panelOpen) {
|
||||||
const firstRow = await page.$('table tbody tr[data-action]');
|
const firstRow = await page.$('table tbody tr[data-action]');
|
||||||
if (!firstRow) { console.log(' ⏭️ Skipped (no clickable rows)'); return; }
|
if (!firstRow) { console.log(' ⏭️ Skipped (no clickable rows)'); return; }
|
||||||
await firstRow.click();
|
await Promise.all([
|
||||||
|
page.waitForResponse(resp => resp.url().includes('/packets/') && resp.status() === 200, { timeout: 15000 }),
|
||||||
|
firstRow.click(),
|
||||||
|
]);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const panel = document.getElementById('pktRight');
|
const panel = document.getElementById('pktRight');
|
||||||
return panel && !panel.classList.contains('empty');
|
return panel && !panel.classList.contains('empty');
|
||||||
@@ -909,6 +920,19 @@ async function run() {
|
|||||||
assert(hexDump, 'Hex dump should be visible after selecting a packet');
|
assert(hexDump, 'Hex dump should be visible after selecting a packet');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extract frontend coverage if instrumented server is running
|
||||||
|
try {
|
||||||
|
const coverage = await page.evaluate(() => window.__coverage__);
|
||||||
|
if (coverage) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const outDir = path.join(__dirname, '.nyc_output');
|
||||||
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(outDir, 'e2e-coverage.json'), JSON.stringify(coverage));
|
||||||
|
console.log(`Frontend coverage from E2E: ${Object.keys(coverage).length} files`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
|
|||||||
@@ -1322,7 +1322,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
|||||||
assert.ok(result.includes('>v2.6.0</a>'), 'version text has v prefix');
|
assert.ok(result.includes('>v2.6.0</a>'), 'version text has v prefix');
|
||||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit links to full hash');
|
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit links to full hash');
|
||||||
assert.ok(result.includes('>abc1234</a>'), 'commit display is truncated to 7');
|
assert.ok(result.includes('>abc1234</a>'), 'commit display is truncated to 7');
|
||||||
assert.ok(result.includes('[node]'), 'should show engine');
|
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>node<'), 'should show engine name');
|
||||||
});
|
});
|
||||||
test('prod port 80: shows version', () => {
|
test('prod port 80: shows version', () => {
|
||||||
const { formatVersionBadge } = makeBadgeSandbox('80');
|
const { formatVersionBadge } = makeBadgeSandbox('80');
|
||||||
@@ -1348,7 +1348,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
|||||||
assert.ok(!result.includes('v2.6.0'), 'staging should NOT show version');
|
assert.ok(!result.includes('v2.6.0'), 'staging should NOT show version');
|
||||||
assert.ok(result.includes('>abc1234</a>'), 'should show commit hash');
|
assert.ok(result.includes('>abc1234</a>'), 'should show commit hash');
|
||||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit is linked');
|
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit is linked');
|
||||||
assert.ok(result.includes('[go]'), 'should show engine');
|
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||||
});
|
});
|
||||||
test('staging port 81: hides version', () => {
|
test('staging port 81: hides version', () => {
|
||||||
const { formatVersionBadge } = makeBadgeSandbox('81');
|
const { formatVersionBadge } = makeBadgeSandbox('81');
|
||||||
@@ -1369,18 +1369,18 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
|||||||
const result = formatVersionBadge('2.6.0', 'unknown', 'node');
|
const result = formatVersionBadge('2.6.0', 'unknown', 'node');
|
||||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||||
assert.ok(!result.includes('unknown'), 'should not show unknown commit');
|
assert.ok(!result.includes('unknown'), 'should not show unknown commit');
|
||||||
assert.ok(result.includes('[node]'), 'should show engine');
|
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>node<'), 'should show engine name');
|
||||||
});
|
});
|
||||||
test('skips commit when missing', () => {
|
test('skips commit when missing', () => {
|
||||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||||
assert.ok(result.includes('[go]'), 'should show engine');
|
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||||
});
|
});
|
||||||
test('shows only engine when version/commit missing', () => {
|
test('shows only engine when version/commit missing', () => {
|
||||||
const { formatVersionBadge } = makeBadgeSandbox('3000');
|
const { formatVersionBadge } = makeBadgeSandbox('3000');
|
||||||
const result = formatVersionBadge(null, null, 'go');
|
const result = formatVersionBadge(null, null, 'go');
|
||||||
assert.ok(result.includes('[go]'), 'should show engine');
|
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||||
assert.ok(result.includes('version-badge'), 'should use version-badge class');
|
assert.ok(result.includes('version-badge'), 'should use version-badge class');
|
||||||
});
|
});
|
||||||
test('short commit not truncated in display', () => {
|
test('short commit not truncated in display', () => {
|
||||||
@@ -1398,7 +1398,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
|||||||
const { formatVersionBadge } = makeBadgeSandbox('8080');
|
const { formatVersionBadge } = makeBadgeSandbox('8080');
|
||||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||||
assert.ok(!result.includes('2.6.0'), 'no version on staging');
|
assert.ok(!result.includes('2.6.0'), 'no version on staging');
|
||||||
assert.ok(result.includes('[go]'), 'engine shown');
|
assert.ok(result.includes('engine-badge'), 'engine badge shown'); assert.ok(result.includes('>go<'), 'engine name shown');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user