/**
* E2E (#1056): Fluid table columns + "+N hidden" pill.
*
* Boots Chromium against a local corescope-server and verifies that the
* primary tables (Packets, Nodes, Observers) collapse priority-tagged
* columns at narrow viewports, render a "+N hidden" pill in the header
* showing the count, and that clicking the pill reveals the hidden columns.
*
* Tested viewports: 768, 1080, 1440 (parent task: 768/1080/1440/1920).
*
* Usage: BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js
*/
'use strict';
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:13581';
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 PAGES = [
{ hash: '#/packets', tableSel: '#pktTable', name: 'packets' },
{ hash: '#/nodes', tableSel: '#nodesTable', name: 'nodes' },
{ hash: '#/observers', tableSel: '#obsTable', name: 'observers' },
];
const VIEWPORTS = [
{ w: 768, h: 900, expectHidden: true },
{ w: 1080, h: 900, expectHidden: true },
{ w: 1440, h: 900, expectHidden: false }, // wide enough — no hide expected (or 0)
{ w: 1920, h: 900, expectHidden: false }, // AC #5: also exercise 1920px
];
// Per-(page, viewport) override map. After #1415 the packets table's
// column priorities were rewritten to the locked spec — at 1080px (>1024)
// every packets column is visible (data-priority 3 columns reveal above
// 1024px, priority 5 above 768px). The /nodes and /observers tables still
// match the original #1056 contract. Default falls through to VIEWPORTS.
function expectHidden(pageName, w) {
if (pageName === 'packets' && w >= 1080) return false;
const vp = VIEWPORTS.find(v => v.w === w);
return vp ? vp.expectHidden : false;
}
async function waitForAnalyticsTables(page) {
// The initial theme-refresh replaces the Nodes tab's tables. Wait for that
// lifecycle event and the handles on the resulting render before measuring.
await page.waitForFunction(() => window.__columnResizeThemeReady &&
document.querySelector('#analyticsContent .analytics-table .col-resize-handle'));
}
(async () => {
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=== #1056 fluid tables E2E against ${BASE} ===`);
for (const width of [1250, 375]) {
await step(`#2217: analytics resize handles stay on their headers at ${width}px`, async () => {
const ctx = await browser.newContext({ viewport: { width, height: 900 } });
try {
await ctx.addInitScript(() => {
window.addEventListener('theme-refresh', () => { window.__columnResizeThemeReady = true; }, { once: true });
});
const page = await ctx.newPage();
await page.goto(BASE + '/#/analytics?tab=nodes', { waitUntil: 'domcontentloaded' });
await waitForAnalyticsTables(page);
const geometry = await page.evaluate(() => ({
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
handles: Array.from(document.querySelectorAll('#analyticsContent .col-resize-handle')).map(handle => {
const header = handle.parentElement;
const h = header.getBoundingClientRect(), r = handle.getBoundingClientRect();
return { anchored: handle.offsetParent === header, height: r.height,
top: r.top - h.top, edge: r.left + r.width / 2 - h.right };
}),
}));
assert(geometry.handles.length > 0, 'expected analytics resize handles');
assert(geometry.handles.every(h => h.anchored), 'resize handle has the page as its containing block');
assert(geometry.handles.every(h => h.height > 0 && Math.abs(h.top) <= 2 && Math.abs(h.edge) <= 2),
'resize handles must follow the header edge: ' + JSON.stringify(geometry.handles));
assert(geometry.overflow <= 1, `analytics page overflows by ${geometry.overflow}px`);
if (width === 1250) {
const selector = '#analyticsContent .analytics-table thead th:first-child';
const header = page.locator(selector).first();
await header.scrollIntoViewIfNeeded();
const before = await header.evaluate(th => {
const r = th.getBoundingClientRect();
return { width: th.offsetWidth, x: r.right - 2, y: r.top + r.height / 2 };
});
await page.mouse.move(before.x, before.y);
await page.mouse.down();
await page.mouse.move(before.x + 40, before.y, { steps: 4 });
await page.mouse.up();
const dragged = await header.evaluate(th => ({
width: th.offsetWidth,
percent: th.offsetWidth / th.closest('table').offsetWidth * 100,
saved: JSON.parse(localStorage.getItem('meshcore-analytics-nodes-0-col-widths')),
}));
assert(dragged.width > before.width + 20, 'dragging must resize the analytics column');
assert(Array.isArray(dragged.saved), 'drag must save column widths');
assert(Math.abs(dragged.saved[0] - dragged.percent) < 1, 'saved percentage must match dragged width');
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForAnalyticsTables(page);
const restored = await header.evaluate(th => ({
percent: th.offsetWidth / th.closest('table').offsetWidth * 100,
anchored: th.querySelector('.col-resize-handle').offsetParent === th,
}));
assert(Math.abs(restored.percent - dragged.saved[0]) < 1, 'reload must restore the saved column width');
assert(restored.anchored, 'restored widths must still anchor the resize handle to its header');
}
} finally {
await ctx.close();
}
});
}
await step('#2079: responsive columns preserve full-width status and spacer rows', async () => {
const ctx = await browser.newContext({ viewport: { width: 375, height: 900 } });
try {
const page = await ctx.newPage();
await page.goto(BASE + '/#/home', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!window.TableResponsive);
const result = await page.evaluate(() => {
const table = document.createElement('table');
table.id = 'responsiveSpanFixture';
table.className = 'data-table';
table.innerHTML = ' ' +
'Kept Hidden ' +
'Hidden Kept