mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-08 21:17:15 +00:00
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.
Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.
Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.
Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.
Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
305 lines
13 KiB
JavaScript
305 lines
13 KiB
JavaScript
/**
|
||
* E2E (#1128 final): Multi-viewport layout collision + z-scale enforcement.
|
||
*
|
||
* Sister of test-issue-1128-packets-layout-e2e.js. That file asserts
|
||
* individual component properties; this one closes the original
|
||
* acceptance criterion: SCREENSHOTS at multiple viewports + bounding-rect
|
||
* collision detection on visible interactive elements.
|
||
*
|
||
* What this test asserts (on top of the sibling):
|
||
*
|
||
* A. At each of three viewports (1280×900, 1080×800, 768×1024), no
|
||
* two `.filter-group` siblings vertically overlap. (Bug 3
|
||
* regression guard — row-gap must be large enough for 34px
|
||
* controls when the bar wraps.)
|
||
*
|
||
* B. (Removed during self-review — was conceptually flawed; see the
|
||
* block comment inside the per-viewport loop. Z-scale band check
|
||
* C and the check-css-vars lint together gate the actual Bug 4
|
||
* regression — dropdowns rendering transparent or behind rows.)
|
||
*
|
||
* C. Z-scale enforcement (audit Section 2): every dropdown selector
|
||
* (`.col-toggle-menu`, `.multi-select-menu`,
|
||
* `.region-dropdown-menu`, `.fux-saved-menu`,
|
||
* `.fux-ac-dropdown`) must compute a z-index inside the
|
||
* `--z-dropdown` band [100,199] — NOT 50, 90, or 9200.
|
||
*
|
||
* D. Path chip line-height lock: `.path-hops .hop, .path-hops
|
||
* .hop-named, .path-hops .arrow` must all compute line-height
|
||
* ≤ 18px so chips never spill the 22px host (Bug 1 belt-
|
||
* and-suspenders, audit Section 3).
|
||
*
|
||
* E. `.col-path` must use `height: 28px`, not `max-height` (table
|
||
* cells ignore max-height per audit).
|
||
*
|
||
* F. Screenshots saved under e2e-screenshots/ for the PR record.
|
||
*
|
||
* Usage: BASE_URL=http://localhost:13581 node \
|
||
* test-issue-1128-multi-viewport-e2e.js
|
||
*/
|
||
'use strict';
|
||
const { chromium } = require('playwright');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||
const SHOT_DIR = 'e2e-screenshots';
|
||
|
||
let passed = 0, failed = 0;
|
||
async function step(name, fn) {
|
||
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
||
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
||
}
|
||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||
|
||
const VIEWPORTS = [
|
||
{ w: 1280, h: 900, name: 'desktop-1280' },
|
||
{ w: 1080, h: 800, name: 'laptop-1080' },
|
||
{ w: 768, h: 1024, name: 'tablet-768' },
|
||
];
|
||
|
||
function vOverlap(a, b) {
|
||
// Reject sub-pixel rounding noise.
|
||
if (a.bottom <= b.top + 1) return false;
|
||
if (b.bottom <= a.top + 1) return false;
|
||
return true;
|
||
}
|
||
|
||
async function gotoPackets(page) {
|
||
await page.goto(BASE + '/#/packets', { waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('#packetFilterInput', { timeout: 8000 });
|
||
await page.waitForFunction(() => !!document.querySelector('#filterUxBar'), { timeout: 8000 });
|
||
await page.evaluate(() => {
|
||
const sel = document.getElementById('fTimeWindow');
|
||
if (sel) { sel.value = '0'; sel.dispatchEvent(new Event('change', { bubbles: true })); }
|
||
});
|
||
await page.waitForFunction(
|
||
() => Array.from(document.querySelectorAll('#pktBody tr'))
|
||
.filter(r => r.id !== 'vscroll-top' && r.id !== 'vscroll-bottom').length > 0,
|
||
{ timeout: 8000 });
|
||
await page.waitForTimeout(400); // hop-resolver
|
||
}
|
||
|
||
(async () => {
|
||
if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||
|
||
const browser = await chromium.launch({
|
||
headless: true,
|
||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||
});
|
||
|
||
console.log(`\n=== #1128 multi-viewport E2E against ${BASE} ===`);
|
||
|
||
for (const vp of VIEWPORTS) {
|
||
const ctx = await browser.newContext({ viewport: { width: vp.w, height: vp.h } });
|
||
const page = await ctx.newPage();
|
||
page.setDefaultTimeout(8000);
|
||
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||
|
||
await step(`[${vp.name}] navigate to /packets`, async () => {
|
||
await gotoPackets(page);
|
||
});
|
||
|
||
await step(`[${vp.name}] screenshot toolbar`, async () => {
|
||
await page.screenshot({
|
||
path: path.join(SHOT_DIR, `issue-1128-${vp.name}.png`),
|
||
fullPage: false,
|
||
});
|
||
});
|
||
|
||
await step(`[${vp.name}] no two .filter-group siblings vertically overlap`, async () => {
|
||
const result = await page.evaluate(() => {
|
||
const groups = Array.from(document.querySelectorAll('.filter-bar > .filter-group'))
|
||
.filter(el => {
|
||
const cs = getComputedStyle(el);
|
||
return cs.display !== 'none' && cs.visibility !== 'hidden';
|
||
});
|
||
const rects = groups.map(g => {
|
||
const r = g.getBoundingClientRect();
|
||
return { top: r.top, bottom: r.bottom, left: r.left, right: r.right, w: r.width, h: r.height };
|
||
});
|
||
// Pairs that share x-overlap (same row attempt) but vertical overlap = bug.
|
||
const offenders = [];
|
||
for (let i = 0; i < rects.length; i++) {
|
||
for (let j = i + 1; j < rects.length; j++) {
|
||
const a = rects[i], b = rects[j];
|
||
const xOverlap = !(a.right <= b.left + 1 || b.right <= a.left + 1);
|
||
const yOverlap = !(a.bottom <= b.top + 1 || b.bottom <= a.top + 1);
|
||
if (xOverlap && yOverlap) offenders.push({ i, j, a, b });
|
||
}
|
||
}
|
||
return { count: groups.length, offenders };
|
||
});
|
||
assert(result.count > 0, 'no .filter-group elements found');
|
||
assert(result.offenders.length === 0,
|
||
`${result.offenders.length} .filter-group sibling pairs overlap: ` +
|
||
JSON.stringify(result.offenders[0]));
|
||
});
|
||
|
||
// #1128 Bug 5 — toolbar reorder gate: toggles MUST appear before dropdowns
|
||
// in document order. packets.js was reordered so the most-frequently-used
|
||
// toggles (Group/My Nodes/time) sit next to the search input. A revert
|
||
// would reintroduce the original eye-trail problem; this assertion fails
|
||
// if the order is swapped back.
|
||
await step(`[${vp.name}] Bug 5: filter-group-toggles precedes filter-group-dropdowns`, async () => {
|
||
const order = await page.evaluate(() => {
|
||
const groups = Array.from(document.querySelectorAll('.filter-bar .filter-group'));
|
||
const togIdx = groups.findIndex(g => g.classList.contains('filter-group-toggles'));
|
||
const dropIdx = groups.findIndex(g => g.classList.contains('filter-group-dropdowns'));
|
||
return { togIdx, dropIdx, total: groups.length };
|
||
});
|
||
assert(order.togIdx >= 0, 'no .filter-group-toggles found in toolbar');
|
||
assert(order.dropIdx >= 0, 'no .filter-group-dropdowns found in toolbar');
|
||
assert(order.togIdx < order.dropIdx,
|
||
`toggles (idx ${order.togIdx}) must precede dropdowns (idx ${order.dropIdx})`);
|
||
});
|
||
|
||
// NOTE on removed sub-tests (#1128 self-review): earlier drafts had
|
||
// "[vp] Saved menu does not overlap toolbar groups below it" and
|
||
// "[vp] Types multi-select dropdown does not overlap toolbar groups
|
||
// below it". Those sub-tests had a fundamentally wrong premise — a
|
||
// position:absolute dropdown opened from a wrapped toolbar row will
|
||
// ALWAYS overlap toolbar rows below it; that's by design. What
|
||
// matters for #1128 Bug 4 is that the dropdown (a) paints on top
|
||
// (z-index) and (b) is opaque (no transparent --surface). Both are
|
||
// already gated independently:
|
||
// - z-index band: "Z-scale: dropdown selectors compute z-index in
|
||
// [100,199] band" (below)
|
||
// - opacity / undefined vars: scripts/check-css-vars.js (CI lint,
|
||
// wired in deploy.yml)
|
||
// Reintroducing a "no rect overlap" assertion would require pinning
|
||
// the toolbar to a single non-wrapping row, which contradicts the
|
||
// responsive design the rest of this file exercises.
|
||
void vp;
|
||
|
||
await ctx.close();
|
||
}
|
||
|
||
// Single z-scale + line-height + col-path checks are viewport-agnostic.
|
||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||
const page = await ctx.newPage();
|
||
page.setDefaultTimeout(8000);
|
||
await gotoPackets(page);
|
||
|
||
await step('Z-scale: dropdown selectors compute z-index in [100,199] band', async () => {
|
||
const checks = await page.evaluate(() => {
|
||
// We can't always force every menu to render, so we test the
|
||
// computed z-index by inserting throwaway DOM nodes that match the
|
||
// selector and reading getComputedStyle. The browser applies the
|
||
// CSS rule by selector, regardless of whether the menu is the
|
||
// "real" one — what matters is the rule's declared z-index.
|
||
const selectors = [
|
||
'.col-toggle-menu',
|
||
'.multi-select-menu',
|
||
'.region-dropdown-menu',
|
||
'.fux-saved-menu',
|
||
'.fux-ac-dropdown',
|
||
];
|
||
const results = [];
|
||
for (const sel of selectors) {
|
||
const el = document.createElement('div');
|
||
// Strip leading dot, attach class.
|
||
el.className = sel.replace(/^\./, '');
|
||
// Force visible so getComputedStyle returns the matched rule.
|
||
el.style.position = 'absolute';
|
||
el.style.top = '-9999px';
|
||
el.style.display = 'block';
|
||
document.body.appendChild(el);
|
||
const z = parseInt(getComputedStyle(el).zIndex, 10);
|
||
results.push({ sel, z: isNaN(z) ? null : z });
|
||
el.remove();
|
||
}
|
||
return results;
|
||
});
|
||
const offenders = checks.filter(c => c.z === null || c.z < 100 || c.z > 199);
|
||
assert(offenders.length === 0,
|
||
'dropdown z-index outside [100,199] band: ' + JSON.stringify(offenders));
|
||
});
|
||
|
||
await step('Bug 1 polish: .path-hops chip children compute line-height ≤ 18px', async () => {
|
||
const offenders = await page.evaluate(() => {
|
||
const probe = (cls) => {
|
||
const host = document.createElement('div');
|
||
host.className = 'path-hops';
|
||
const child = document.createElement('span');
|
||
child.className = cls;
|
||
child.textContent = 'x';
|
||
host.appendChild(child);
|
||
document.body.appendChild(host);
|
||
const lh = parseFloat(getComputedStyle(child).lineHeight);
|
||
host.remove();
|
||
return { cls, lh };
|
||
};
|
||
const probed = ['hop', 'hop-named', 'arrow'].map(probe);
|
||
return probed.filter(p => !(p.lh <= 18.5));
|
||
});
|
||
assert(offenders.length === 0,
|
||
'.path-hops chip line-height > 18px: ' + JSON.stringify(offenders));
|
||
});
|
||
|
||
await step('Bug 1 polish: .col-path uses fixed height (not max-height) ≤ 28px', async () => {
|
||
const result = await page.evaluate(() => {
|
||
// Build a fake table cell to read the computed rule.
|
||
const tbl = document.createElement('table');
|
||
tbl.className = 'data-table';
|
||
const tr = document.createElement('tr');
|
||
const td = document.createElement('td');
|
||
td.className = 'col-path';
|
||
tr.appendChild(td);
|
||
const tbody = document.createElement('tbody');
|
||
tbody.appendChild(tr);
|
||
tbl.appendChild(tbody);
|
||
tbl.style.position = 'absolute';
|
||
tbl.style.top = '-9999px';
|
||
document.body.appendChild(tbl);
|
||
const cs = getComputedStyle(td);
|
||
const height = parseFloat(cs.height);
|
||
const maxHeight = cs.maxHeight;
|
||
tbl.remove();
|
||
// Walk the loaded stylesheets to find the rule(s) that target
|
||
// .data-table td.col-path and report which physical property
|
||
// (height vs max-height) is declared. The audit fix REQUIRES
|
||
// `height: 28px`; `max-height: 28px` is the original Bug 1
|
||
// regression and must fail this test.
|
||
const declared = { height: null, maxHeight: null, ruleSrc: null };
|
||
for (const sheet of document.styleSheets) {
|
||
let rules;
|
||
try { rules = sheet.cssRules; } catch (e) { continue; }
|
||
if (!rules) continue;
|
||
for (const rule of rules) {
|
||
if (!rule.selectorText) continue;
|
||
// Match selectors that include both .data-table and .col-path
|
||
// (allow combinator variations like ".data-table td.col-path").
|
||
if (/\.data-table[\s\S]*\.col-path/.test(rule.selectorText)) {
|
||
const h = rule.style && rule.style.getPropertyValue('height');
|
||
const mh = rule.style && rule.style.getPropertyValue('max-height');
|
||
if (h) declared.height = h.trim();
|
||
if (mh) declared.maxHeight = mh.trim();
|
||
if (h || mh) declared.ruleSrc = rule.selectorText;
|
||
}
|
||
}
|
||
}
|
||
return { height, maxHeight, declared };
|
||
});
|
||
// Computed height must be exactly 28px.
|
||
assert(Math.abs(result.height - 28) < 0.5,
|
||
'.col-path computed height not 28px: ' + JSON.stringify(result));
|
||
// Audit fix gate: rule must declare `height: 28px`. If the regression
|
||
// is reverted to `max-height: 28px`, declared.height is null and this
|
||
// assertion fails.
|
||
assert(result.declared.height && /^28px$/.test(result.declared.height),
|
||
'.col-path rule must declare `height: 28px` (audit fix), got: ' +
|
||
JSON.stringify(result.declared));
|
||
assert(!result.declared.maxHeight,
|
||
'.col-path rule must NOT declare max-height (original Bug 1 regression): ' +
|
||
JSON.stringify(result.declared));
|
||
});
|
||
|
||
await ctx.close();
|
||
await browser.close();
|
||
|
||
console.log(`\n=== Results: passed ${passed} failed ${failed} ===`);
|
||
process.exit(failed > 0 ? 1 : 0);
|
||
})().catch(e => { console.error(e); process.exit(1); });
|