mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-21 06:00:55 +00:00
## Summary Wires the shared `TableSort` helper (already used by the nodes table, #679) into the observers table at `#/observers`. Adds `data-sort-key` / `data-type` attrs on every `<th>`, `data-value` on every `<td>` with the raw sortable value (epoch-ms for times, integers for counts, abs-seconds for clock skew, derived health rank for the status dot), and initializes `TableSort` at the end of `render()` — after the new `tbody` is in the DOM — to avoid the #679 init race on async refresh. ## Before / after - **Before:** clicking any column header on `#/observers` does nothing — bare `<th>` cells, no click handlers, no `TableSort.init` call (per #1639 repro). - **After:** clicking a header toggles asc/desc with `aria-sort` indicator + ▲/▼ glyph. Numeric columns (Packet Health, Total Packets, Packets/Hour, Clock Offset, Uptime) sort numerically. Time columns (Last Status, Last Packet) sort by ISO timestamp, not the `"23d ago"` display string. Active column + direction persisted in `localStorage` under `meshcore-observers-sort`. Default sort: Last Status desc (matches existing default ordering). ## Test plan - TDD red commit `0dcd5304` — fails on assertion `Total Packets <th> must carry data-sort-key="packet_count"` against master. - Green commit `d4f0376f` — both assertions pass. - E2E assertion added: `test-issue-1639-observers-sort-e2e.js:46` (header has `data-sort-key`+`data-type`) and `:62` (click reorders rows numerically desc). - Local commands run from the worktree: - `cd cmd/migrate && go build -o ../../cs-migrate-1639 .` → `./cs-migrate-1639 -db test-fixtures/e2e-fixture.db` - `cd cmd/server && go build -o ../../cs-server-1639 .` → run on port 13581 against the fixture DB - `CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1639-observers-sort-e2e.js` → ✅ both tests pass - `node test-observers-headings.js` (#1039 regression) → ✅ still passes - Browser verified: headless chromium against the local fixture server. Clicked Total Packets header three times: first click → `aria-sort=descending` + ▼ glyph + rows ordered 139,261 → 5,791. Second click → `aria-sort=ascending` + ▲ glyph. Third click → back to descending. tbody re-renders correctly after the 30s `loadObservers` auto-refresh (no init race — the new TableSort controller binds to the fresh header). - pr-preflight: clean (all hard gates + warnings pass against `origin/master`). ## Files changed - `public/observers.js` — wire TableSort, add `data-sort-key`/`data-type`/`data-value`, init after render - `test-issue-1639-observers-sort-e2e.js` — new E2E (red→green) - `.github/workflows/deploy.yml` — run the new E2E alongside existing playwright group Fixes #1639 --------- Co-authored-by: openclaw-bot <bot@openclaw> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw.local>
175 lines
6.9 KiB
JavaScript
175 lines
6.9 KiB
JavaScript
/**
|
|
* E2E test (#1639): observers table at #/observers must be sortable.
|
|
*
|
|
* Same fix-pattern as #679 (nodes table) — wire TableSort with
|
|
* numeric / time column hints. Clicking a column header MUST reorder
|
|
* tbody rows.
|
|
*
|
|
* Round-1 test additions (PR #1641):
|
|
* - last_packet_at column reorder (regression guard for the round-0
|
|
* data-type=date → numeric fix; without this the one-line fix is
|
|
* unguarded).
|
|
* - strict monotonic non-increasing assertion (no more .some(changed),
|
|
* which silently passes even when order is mostly wrong).
|
|
* - toggle (second-click ascending) on packet_count.
|
|
* - second-column string sort (Name) to exercise localeCompare.
|
|
* - cells are addressed via data-testid (added in observers.js for
|
|
* testability) — finding #8 dropped the broader data-col-key on
|
|
* <td>s, and data-testid is the less-invasive replacement.
|
|
*
|
|
* Usage: BASE_URL=http://localhost:13581 node test-issue-1639-observers-sort-e2e.js
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:3000';
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` \u2705 ${name}`);
|
|
} catch (err) {
|
|
console.log(` \u274c ${name}: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function assert(cond, msg) {
|
|
if (!cond) throw new Error(msg || 'Assertion failed');
|
|
}
|
|
|
|
// finding #5: strict monotonic non-increasing across ALL rows
|
|
function assertNonIncreasing(arr, label) {
|
|
for (let i = 0; i < arr.length - 1; i++) {
|
|
// NaN slots are sorted last by the comparator — accept them at the tail.
|
|
const a = arr[i], b = arr[i + 1];
|
|
if (!Number.isFinite(a)) continue; // NaN tail
|
|
if (!Number.isFinite(b)) continue;
|
|
if (!(a >= b)) {
|
|
throw new Error(`${label}: not non-increasing at idx ${i}: ${a} < ${b}; full=${JSON.stringify(arr)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNonDecreasing(arr, label) {
|
|
for (let i = 0; i < arr.length - 1; i++) {
|
|
const a = arr[i], b = arr[i + 1];
|
|
if (!Number.isFinite(a)) continue;
|
|
if (!Number.isFinite(b)) continue;
|
|
if (!(a <= b)) {
|
|
throw new Error(`${label}: not non-decreasing at idx ${i}: ${a} > ${b}; full=${JSON.stringify(arr)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function gotoObservers(page) {
|
|
await page.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => { try { localStorage.removeItem('meshcore-observers-sort'); } catch (_) {} });
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('#obsTable', { timeout: 10000 });
|
|
await page.waitForSelector('#obsTable tbody tr', { timeout: 10000 });
|
|
}
|
|
|
|
async function readNumeric(page, testid) {
|
|
return await page.$$eval(`#obsTable tbody tr td[data-testid="${testid}"]`, (tds) => tds.map((td) => {
|
|
const dv = td.getAttribute('data-value');
|
|
const raw = dv != null && dv !== '' ? dv : td.textContent.replace(/[^0-9.-]/g, '');
|
|
return Number(raw);
|
|
}));
|
|
}
|
|
|
|
async function readStrings(page, testid) {
|
|
return await page.$$eval(`#obsTable tbody tr td[data-testid="${testid}"]`, (tds) =>
|
|
tds.map((td) => td.getAttribute('data-value') || ''));
|
|
}
|
|
|
|
async function run() {
|
|
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({ viewport: { width: 1400, height: 900 } });
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(15000);
|
|
|
|
console.log(`\nRunning #1639 observers-sort E2E tests against ${BASE}\n`);
|
|
|
|
await test('observers thead has data-sort-key + data-type on numeric columns', async () => {
|
|
await gotoObservers(page);
|
|
const totalPktsTh = await page.$('#obsTable thead th[data-sort-key="packet_count"]');
|
|
assert(totalPktsTh, 'Total Packets <th> must carry data-sort-key="packet_count"');
|
|
const type = await totalPktsTh.getAttribute('data-type');
|
|
assert(type === 'numeric',
|
|
`Total Packets <th> must have data-type="numeric", got "${type}"`);
|
|
});
|
|
|
|
await test('clicking Total Packets header reorders rows numerically (desc)', async () => {
|
|
await gotoObservers(page);
|
|
const before = await readNumeric(page, 'obs-cell-packet-count');
|
|
assert(before.length >= 2, `need >=2 rows, got ${before.length}`);
|
|
|
|
const th = await page.$('#obsTable thead th[data-sort-key="packet_count"]');
|
|
assert(th, 'Total Packets <th> not found');
|
|
await th.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
const after = await readNumeric(page, 'obs-cell-packet-count');
|
|
assert(after.length === before.length, `row count changed`);
|
|
// finding #5: strict non-increasing across ALL rows, not just first/last
|
|
assertNonIncreasing(after, 'packet_count desc');
|
|
});
|
|
|
|
// finding #13: toggle (second click on the same header → ascending)
|
|
await test('second click on Total Packets header toggles to ascending', async () => {
|
|
await gotoObservers(page);
|
|
const th = await page.$('#obsTable thead th[data-sort-key="packet_count"]');
|
|
await th.click(); // desc
|
|
await page.waitForTimeout(200);
|
|
await th.click(); // asc
|
|
await page.waitForTimeout(300);
|
|
const after = await readNumeric(page, 'obs-cell-packet-count');
|
|
assert(after.length >= 2, 'need >=2 rows');
|
|
assertNonDecreasing(after, 'packet_count asc (after toggle)');
|
|
});
|
|
|
|
// finding #4: regression guard for round-0 data-type=date → numeric fix
|
|
await test('clicking Last Packet header reorders rows by timestamp (desc)', async () => {
|
|
await gotoObservers(page);
|
|
const th = await page.$('#obsTable thead th[data-sort-key="last_packet_at"]');
|
|
assert(th, 'Last Packet <th> not found');
|
|
const type = await th.getAttribute('data-type');
|
|
assert(type === 'numeric',
|
|
`Last Packet <th> must have data-type="numeric" (round-0 fix), got "${type}"`);
|
|
await th.click();
|
|
await page.waitForTimeout(300);
|
|
const after = await readNumeric(page, 'obs-cell-last-packet');
|
|
assert(after.length >= 2, `need >=2 rows`);
|
|
assertNonIncreasing(after, 'last_packet_at desc');
|
|
});
|
|
|
|
// finding #13: second-column test — Name string sort via localeCompare
|
|
await test('clicking Name header sorts alphabetically (string localeCompare)', async () => {
|
|
await gotoObservers(page);
|
|
const th = await page.$('#obsTable thead th[data-sort-key="name"]');
|
|
assert(th, 'Name <th> not found');
|
|
await th.click();
|
|
await page.waitForTimeout(300);
|
|
const after = await readStrings(page, 'obs-cell-name');
|
|
assert(after.length >= 2, `need >=2 rows`);
|
|
// Default direction for text columns is asc — sorted A→Z
|
|
for (let i = 0; i < after.length - 1; i++) {
|
|
if (after[i] === '' || after[i + 1] === '') continue;
|
|
assert(after[i].localeCompare(after[i + 1]) <= 0,
|
|
`Name asc broken at idx ${i}: ${JSON.stringify(after[i])} > ${JSON.stringify(after[i + 1])}`);
|
|
}
|
|
});
|
|
|
|
await browser.close();
|
|
console.log('\n✅ all #1639 sort tests passed\n');
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|