Files
meshcore-analyzer/tests/e2e/test-channel-fluid-e2e.js
n30nex 410c82c02c fix(ui): use consistent 48px navigation and channel buttons (#2078)
Fixes #2052.

Navigation (`.nav-btn`) and channel icon (`.ch-icon-btn`) buttons now
use the shared 48px house minimum. Remove competing 44px
component/coarse-pointer rules and the legacy 32px mobile rule, and
correct comments that confused the house preference with accessibility
requirements.

At 768px, user-added channel rows already clipped Remove; the larger
Share target exposed the same constraint. Allow only those rows'
controls to wrap so both actions remain within the sidebar. Navigation
height and normal network-channel rows are unchanged.

- Red `7787d9b`: rendered target assertions fail at 375/390/768/1280px.
Green `e6ed9c8` changes CSS only.
- Validation: all 183 standalone suites; 33 computed touch-target
checks; 15 actual-page layout/click checks. Broader browser suite: 131
passed, 3 fixture-dependent skips. Three independent reviews found no
required changes.
- Browser verified: local Chromium and fixture-backed Go server;
screenshots `coverage/2052-targets-375.png` and
`coverage/2052-targets-768.png`.
- E2E assertion added: `tests/e2e/test-channel-fluid-e2e.js:112`,
covering dimensions, containment, overflow and normal Filter/Share
clicks. Existing CI selection is retained; Chromium-required runs cannot
silently skip the touch suite.
- Test-only follow-up `5144503` waits for packet readiness before
measuring navbar controls. Five fresh mobile contexts passed; size and
click assertions remain intact.
- No new settings, requests, dependencies or runtime JavaScript.

## Preflight overrides

- External `run-all.sh` unavailable; repository checks run directly.
- Local unit runner uses UTF-8 and `GITHUB_REF_NAME=local-validation`
for the existing release harness. Browser navigation uses a 60s local
budget for slow assets; repository assertions/timeouts are unchanged.
2026-09-30 11:40:22 +02:00

170 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Issue #1057 — Channels page fluid layout E2E.
*
* For each viewport asserts:
* - No horizontal scroll on the body.
* - At ≥768px wide: both .ch-sidebar and .ch-main are visible AND occupy
* non-overlapping horizontal regions (true side-by-side).
* - At narrow (<700px) widths: layout stacks (sidebar above OR overlay).
*
* Usage: BASE_URL=http://localhost:13581 node test-channel-fluid-e2e.js
*/
'use strict';
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:3000';
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'); }
(async () => {
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
const ctx = await browser.newContext();
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
console.log(`\n=== #1057 Channels fluid layout E2E against ${BASE} ===`);
async function loadChannels(w, h) {
await page.setViewportSize({ width: w, height: h });
await page.goto(BASE + '/#/channels', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.ch-sidebar', { timeout: 8000 });
// Allow CSS layout/paint to settle.
await page.waitForTimeout(150);
}
async function noBodyHScroll() {
return page.evaluate(() => {
// Allow ≤1px tolerance for sub-pixel rounding.
return (document.documentElement.scrollWidth - document.documentElement.clientWidth) <= 1;
});
}
async function rectOf(sel) {
return page.evaluate((s) => {
const el = document.querySelector(s);
if (!el) return null;
const r = el.getBoundingClientRect();
const cs = window.getComputedStyle(el);
return {
x: r.x, y: r.y, w: r.width, h: r.height,
visible: r.width > 0 && r.height > 0 && cs.display !== 'none' && cs.visibility !== 'hidden',
};
}, sel);
}
// Wide viewports — true side-by-side. Includes 2560×1440 ultrawide (AC4).
for (const [w, h] of [[768, 900], [1080, 900], [1440, 900], [1920, 1080], [2560, 1440]]) {
await step(`viewport ${w}×${h}: no horizontal scroll`, async () => {
await loadChannels(w, h);
assert(await noBodyHScroll(), 'document scrollWidth > clientWidth (horizontal scroll)');
});
await step(`viewport ${w}×${h}: sidebar AND message area both visible`, async () => {
const sb = await rectOf('.ch-sidebar');
const main = await rectOf('.ch-main');
assert(sb && sb.visible, '.ch-sidebar not visible');
assert(main && main.visible, '.ch-main not visible');
// Sidebar should not consume more than ~45% of viewport width on wide screens.
assert(sb.w <= w * 0.45 + 1,
`sidebar too wide: ${sb.w}px / ${w}px viewport (>45%)`);
// Message area should occupy meaningful remaining width (≥40% of viewport).
assert(main.w >= w * 0.40,
`message area too narrow: ${main.w}px / ${w}px viewport (<40%)`);
// Side-by-side: main starts at/after sidebar's right edge (no overlap).
assert(main.x + 1 >= sb.x + sb.w,
`sidebar (x=${sb.x},w=${sb.w}) overlaps main (x=${main.x})`);
});
}
// Narrow viewport — stacking (sidebar above main, or overlay/single-pane).
await step('viewport 480×800: layout stacks (no side-by-side overflow)', async () => {
await loadChannels(480, 800);
assert(await noBodyHScroll(), 'narrow viewport caused horizontal scroll');
const sb = await rectOf('.ch-sidebar');
const main = await rectOf('.ch-main');
assert(sb, '.ch-sidebar missing');
// Either main is hidden/overlayed (single-pane mobile mode), OR
// main is stacked below the sidebar (main.y >= sb.y + sb.h - tolerance).
if (main && main.visible) {
const stacked = main.y + 1 >= sb.y + sb.h
|| sb.y + 1 >= main.y + main.h;
const overlay = Math.abs(main.x - sb.x) < 5 && Math.abs(main.w - sb.w) < 5;
assert(stacked || overlay,
`narrow layout not stacked/overlayed: sb=${JSON.stringify(sb)} main=${JSON.stringify(main)}`);
}
});
// #2052: exercise the actual controls in both pointer modes, not just
// standalone CSS samples. Mobile channels use rows without inline icons;
// the packets navbar supplies the visible .nav-btn mirrors on phones.
for (const width of [375, 390, 768, 1280]) {
await step(`viewport ${width}: 48px controls fit and respond to clicks`, async () => {
const mobile = width < 768;
const touchContext = await browser.newContext({
viewport: { width, height: 900 }, hasTouch: mobile, isMobile: mobile,
});
const target = await touchContext.newPage();
try {
await target.addInitScript(() => localStorage.setItem('corescope_channel_keys',
JSON.stringify({ Touch2052: '00112233445566778899aabbccddeeff' })));
await target.goto(BASE + (mobile ? '/#/packets' : '/#/channels'),
{ waitUntil: 'domcontentloaded' });
const action = mobile ? '.filter-toggle-btn-mirror' : '[data-share-channel]';
await target.waitForSelector(action);
await target.evaluate(() => document.fonts.ready);
// Mobile page-actions rebuilds the mirror while packets initializes.
// Query and measure in one browser turn so a detached selector snapshot
// cannot look like missing controls. Bad dimensions still fail below.
const controlsHandle = await target.waitForFunction(({ mobile, action }) => {
if (mobile && !document.querySelector('#pktLeft[data-loaded="true"] #pktPauseBtn')) return false;
const els = Array.from(document.querySelectorAll('.top-nav .nav-btn, #chList .ch-icon-btn'))
.filter(el => el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden');
if (!els.length || !els.some(el => el.matches(action))) return false;
return els.map(el => {
const r = el.getBoundingClientRect();
const container = el.closest('.top-nav, .ch-item').getBoundingClientRect();
return { name: el.id || el.getAttribute('aria-label'), w: r.width, h: r.height,
fits: r.left >= container.left - 1 && r.right <= container.right + 1
&& r.top >= container.top - 1 && r.bottom <= container.bottom + 1 };
});
}, { mobile, action }, { timeout: 8000 });
const controls = await controlsHandle.jsonValue();
await controlsHandle.dispose();
assert(controls.length > 0, 'expected visible navbar or channel controls');
for (const control of controls) {
assert(control.w >= 48 && control.h >= 48,
`${control.name}: expected >=48x48, got ${control.w}x${control.h}`);
assert(control.fits, `${control.name}: clipped outside its navbar/channel row`);
}
assert(await target.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1),
'controls caused horizontal overflow');
await target.locator(action).click();
if (mobile) {
await target.waitForSelector('.filter-bar.filters-expanded');
} else {
await target.waitForSelector('#chShareModal:not(.hidden)');
assert((await target.inputValue('#chShareKey')) === '00112233445566778899aabbccddeeff',
'Share must open the selected channel key');
}
} finally {
await touchContext.close();
}
});
}
console.log(`\n${passed} passed, ${failed} failed`);
await browser.close();
process.exit(failed ? 1 : 0);
})().catch((e) => { console.error(e); process.exit(1); });