test(#1056): RED — slide-over panel E2E for narrow widths

Adds test-slideover-1056-e2e.js asserting that, at 800x800, clicking a
row in the Packets/Nodes/Observers tables opens a .slide-over-panel
with .slide-over-backdrop and a .slide-over-close X button, and that the
panel closes via Escape, backdrop click, and X click. Wide viewport
regression check: at 1440 the slide-over MUST NOT appear.

This commit is intentionally RED — the slide-over has not been
implemented yet, so the assertions on .slide-over-panel will fail.
This commit is contained in:
openclaw-bot
2026-05-08 16:35:14 +00:00
parent 79bf673fa3
commit 8ac568bac3
2 changed files with 175 additions and 0 deletions
+1
View File
@@ -231,6 +231,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-more-floor-1139-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-fluid-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-slideover-1056-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1122-packets-filter-ux-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1128-packets-layout-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1128-multi-viewport-e2e.js 2>&1 | tee -a e2e-output.txt
+174
View File
@@ -0,0 +1,174 @@
/**
* E2E (#1056 AC #4): Row-detail slide-over panel at narrow widths.
*
* At viewports <=1023, clicking a row in the Packets, Nodes, or Observers
* tables must open the row's detail in a slide-over panel
* (`.slide-over-panel`) with a backdrop (`.slide-over-backdrop`), instead of
* pushing layout to a separate page. The panel must close via the X button,
* a backdrop click, and the Escape key.
*
* Wide viewports (>=1280) MUST NOT trigger the slide-over — the existing
* right-side detail panel behavior is preserved.
*
* Usage: BASE_URL=http://localhost:13581 node test-slideover-1056-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', rowSel: '#pktTable tbody tr[data-id], #pktTable tbody tr', name: 'packets' },
{ hash: '#/nodes', tableSel: '#nodesTable', rowSel: '#nodesTable tbody tr[data-value]', name: 'nodes' },
{ hash: '#/observers', tableSel: '#obsTable', rowSel: '#obsTable tbody tr[data-action="navigate"]', name: 'observers' },
];
(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 AC#4 slide-over E2E against ${BASE} ===`);
// ---- Narrow viewport: slide-over MUST appear ----
for (const p of PAGES) {
const ctx = await browser.newContext({ viewport: { width: 800, height: 800 } });
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
const tag = `${p.name}@800`;
await step(`${tag}: page renders + first row exists`, async () => {
await page.goto(BASE + '/' + p.hash, { waitUntil: 'domcontentloaded' });
await page.waitForSelector(p.tableSel, { timeout: 8000 });
// wait for at least one tbody row
await page.waitForFunction((sel) => {
const t = document.querySelector(sel);
return t && t.querySelectorAll('tbody tr').length > 0;
}, p.tableSel, { timeout: 8000 });
});
await step(`${tag}: clicking row opens slide-over with backdrop`, async () => {
// Click the first body row.
await page.evaluate((sel) => {
const t = document.querySelector(sel);
const row = t && t.querySelector('tbody tr');
if (row) row.click();
}, p.tableSel);
await page.waitForTimeout(250);
const info = await page.evaluate(() => {
const panel = document.querySelector('.slide-over-panel');
const back = document.querySelector('.slide-over-backdrop');
const closeBtn = panel && panel.querySelector('.slide-over-close');
return {
panelPresent: !!panel,
panelVisible: !!(panel && panel.offsetParent !== null && !panel.hidden),
backdropPresent: !!back,
backdropVisible: !!(back && back.offsetParent !== null && !back.hidden),
hasCloseBtn: !!closeBtn,
};
});
assert(info.panelPresent, 'slide-over panel not in DOM');
assert(info.panelVisible, 'slide-over panel not visible');
assert(info.backdropPresent, 'slide-over backdrop not in DOM');
assert(info.backdropVisible, 'slide-over backdrop not visible');
assert(info.hasCloseBtn, 'slide-over panel missing .slide-over-close X button');
});
await step(`${tag}: Escape closes slide-over`, async () => {
await page.keyboard.press('Escape');
await page.waitForTimeout(200);
const info = await page.evaluate(() => {
const panel = document.querySelector('.slide-over-panel');
const back = document.querySelector('.slide-over-backdrop');
return {
panelGone: !panel || panel.offsetParent === null || panel.hidden,
backGone: !back || back.offsetParent === null || back.hidden,
};
});
assert(info.panelGone, 'slide-over panel still visible after Escape');
assert(info.backGone, 'slide-over backdrop still visible after Escape');
});
await step(`${tag}: backdrop click closes slide-over`, async () => {
await page.evaluate((sel) => {
const t = document.querySelector(sel);
const row = t && t.querySelector('tbody tr');
if (row) row.click();
}, p.tableSel);
await page.waitForTimeout(250);
// Click the backdrop directly.
await page.evaluate(() => {
const b = document.querySelector('.slide-over-backdrop');
if (b) b.click();
});
await page.waitForTimeout(200);
const gone = await page.evaluate(() => {
const panel = document.querySelector('.slide-over-panel');
return !panel || panel.offsetParent === null || panel.hidden;
});
assert(gone, 'slide-over still visible after backdrop click');
});
await step(`${tag}: X button closes slide-over`, async () => {
await page.evaluate((sel) => {
const t = document.querySelector(sel);
const row = t && t.querySelector('tbody tr');
if (row) row.click();
}, p.tableSel);
await page.waitForTimeout(250);
await page.evaluate(() => {
const x = document.querySelector('.slide-over-panel .slide-over-close');
if (x) x.click();
});
await page.waitForTimeout(200);
const gone = await page.evaluate(() => {
const panel = document.querySelector('.slide-over-panel');
return !panel || panel.offsetParent === null || panel.hidden;
});
assert(gone, 'slide-over still visible after X click');
});
await ctx.close();
}
// ---- Wide viewport: slide-over MUST NOT appear (regression guard) ----
{
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
await step('wide@1440 packets: row click does NOT open slide-over', async () => {
await page.goto(BASE + '/#/packets', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#pktTable', { timeout: 8000 });
await page.waitForFunction(() => document.querySelectorAll('#pktTable tbody tr').length > 0, null, { timeout: 8000 });
await page.evaluate(() => {
const r = document.querySelector('#pktTable tbody tr');
if (r) r.click();
});
await page.waitForTimeout(300);
const slideOverShown = await page.evaluate(() => {
const p = document.querySelector('.slide-over-panel');
return !!(p && p.offsetParent !== null && !p.hidden);
});
assert(!slideOverShown, 'slide-over should NOT appear at 1440px width');
});
await ctx.close();
}
await browser.close();
console.log(`\n=== #1056 AC#4 slide-over E2E: ${passed} passed, ${failed} failed ===`);
process.exit(failed ? 1 : 0);
})();