Files
meshcore-analyzer/test-channel-fluid-e2e.js
T
Kpa-clawbot 85b8c8115a feat(channels): fluid sidebar + container-query stacking (#1057) (#1095)
## 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>
2026-05-05 08:31:37 -07:00

112 lines
4.5 KiB
JavaScript
Raw 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)}`);
}
});
console.log(`\n${passed} passed, ${failed} failed`);
await browser.close();
process.exit(failed ? 1 : 0);
})().catch((e) => { console.error(e); process.exit(1); });