mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-11 22:54:44 +00:00
85b8c8115a
## Summary Makes the channels page sidebar + message area fluid as part of the parent #1050 fluid-layout effort. Replaces the hardcoded `.ch-sidebar { width: 280px; min-width: 280px }` with `width: clamp(220px, 22vw, 320px); min-width: 220px`. Adds an `@container` query (via `container-type: inline-size` on `.ch-layout`) that stacks the sidebar above the message area when the channels page itself is narrow (≤700px container width) — independent of the global viewport, so it adapts even when an outer panel is consuming width. Removes the legacy `@media (max-width: 900px)` fixed 220px override; the clamp + container query handle that range. `.ch-main` already used `flex: 1`, so it absorbs all remaining width including ultrawides. The existing mobile (≤640px) overlay rules and the JS resize handle in `channels.js` are untouched and still work (user drag still wins via inline width). Fixes #1057. ## Scope - `public/style.css` — channels section only - (no `public/channels.js` changes needed) ## Tests TDD: red commit (failing tests) → green commit (implementation). - `test-channel-fluid-layout.js` (new): static CSS assertions - `.ch-sidebar` uses `clamp()` for width (not fixed px) - `.ch-sidebar` keeps a sane `min-width` (200–280px) - `.ch-main` keeps `flex: 1` - `.ch-layout` declares `container-type` (container query root) - `@container` rule scopes channels stacking - legacy `@media (max-width: 900px) .ch-sidebar { width: 220px }` is gone - `test-channel-fluid-e2e.js` (new): Playwright E2E at 768 / 1080 / 1440 / 1920 (wide) and 480 (narrow). Asserts: - no horizontal scroll on the body - sidebar AND message area both visible side-by-side at ≥768px - sidebar consumes ≤45% of viewport, main ≥40% - at 480px the layout stacks (or overlays) — no overflow Wired into `test-all.sh` and the unit + e2e steps of `.github/workflows/deploy.yml`. ## Verification - Static unit test: 6/6 pass on the green commit, 4/6 fail on the red commit (only the two trivially-true assertions pass). - Local Go server boot: `corescope-server` serves the updated `style.css` containing `container-type: inline-size`, `clamp(220px, 22vw, 320px)`, and `@container chlayout (max-width: 700px)`. - Local Chromium on the dev sandbox is musl-incompatible (Playwright fallback build crashes with `Error relocating ...: posix_fallocate64: symbol not found`), so the E2E was not run locally. CI will run it on Ubuntu runners. --------- Co-authored-by: clawbot <clawbot@example.com> Co-authored-by: meshcore-bot <bot@meshcore.local>
112 lines
4.5 KiB
JavaScript
112 lines
4.5 KiB
JavaScript
/**
|
||
* 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)}`);
|
||
}
|
||
});
|
||
|
||
console.log(`\n${passed} passed, ${failed} failed`);
|
||
await browser.close();
|
||
process.exit(failed ? 1 : 0);
|
||
})().catch((e) => { console.error(e); process.exit(1); });
|