#!/usr/bin/env node // JS-only regression of the Stored Logs sorting interaction (no Rust server). import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const read = name => fs.readFileSync(name, 'utf8'); const ui = read('src/admin/web/file-logs.js'); const css = read('src/admin/web/style.css'); const api = JSON.parse(read('src/admin/openapi.json')); const params = api.paths['/api/file-logs/query'].get.parameters.map(param => param.name); assert.ok(params.includes('sort_by') && params.includes('sort_descending')); assert.ok(api.components.schemas.AdminPreferences.properties.file_log_sort); assert.ok(api.components.schemas.AdminPreferences.properties.file_log_descending); assert.match(ui, /params\.set\("sort_by", sortBy\)/); assert.match(ui, /params\.set\("sort_descending", String\(sortDescending\)\)/); assert.match(css, /file-log-sort-button/); class FakeNode { constructor(tag, text = '', cls = '') { this.tag = tag; this.textContent = text; this.className = cls; this.children = []; this.dataset = {}; this.attrs = {}; this.events = {}; } setAttribute(name, value) { this.attrs[name] = value; } append(...nodes) { this.children.push(...nodes); } replaceChildren(...nodes) { this.children = nodes; } addEventListener(name, handler) { this.events[name] = handler; } click() { this.events.click?.(); } } const row = new FakeNode('tr'); const sort = ui.slice(ui.indexOf(' function syncHeaders() {'), ui.indexOf(' function syncColumnPicker()')); assert.ok(sort.startsWith(' function syncHeaders() {') && sort.includes('file-log-sort-button')); const ctx = { el: (tag, text, cls) => new FakeNode(tag, text, cls), $: () => ({querySelector: () => row}), orderedColumns: () => ['time', 'status'], visibleColumns: new Set(['time', 'status']), specialLabels: new Map([['time', 'time'], ['status', 'status']]), columnLabel: id => id, t: key => key, sortBy: 'time', sortDescending: true, selectedIndex: 3, offset: 100, saved: 0, refreshed: 0, savePreferences() { this.saved++; }, refresh() { this.refreshed++; }, }; // Methods capture explicit counters rather than relying on vm's `this` binding. ctx.savePreferences = () => { ctx.saved++; }; ctx.refresh = reset => { if (reset) ctx.refreshed++; }; vm.createContext(ctx); vm.runInContext(`${sort}\nglobalThis.draw = syncHeaders;`, ctx); ctx.draw(); assert.equal(row.children.length, 2); assert.equal(row.children[0].attrs['aria-sort'], 'descending'); assert.equal(row.children[1].attrs['aria-sort'], 'none'); row.children[1].children[0].click(); assert.equal(ctx.sortBy, 'status'); assert.equal(ctx.sortDescending, false); assert.equal(ctx.offset, 0); assert.equal(ctx.saved, 1); assert.equal(ctx.refreshed, 1); assert.equal(row.children[1].attrs['aria-sort'], 'ascending'); row.children[1].children[0].click(); assert.equal(ctx.sortDescending, true); assert.equal(row.children[1].attrs['aria-sort'], 'descending'); assert.equal(ctx.saved, 2); assert.equal(ctx.refreshed, 2); console.log('PASS: 0.31.8 Stored Logs headers sort by column, toggle direction, reset pagination, persist preferences and request server sorting.');