repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/search-panels.test.mjs
Rawimport assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import vm from "node:vm";
// Execute the panels' actual JS methods and binding expressions against synthetic
// services. This checks state/action logic, not Qt focus, rendering or input delivery.
const read = name => readFileSync(new URL(name, import.meta.url), "utf8");
const util = vm.createContext({});
vm.runInContext(read("Util.js").replace(/^\.pragma library\s*/, ""), util);
function panel(name, service, sessionState = { query: "", selectedId: "", scrollY: 0 }) {
const source = read(`${name}Panel.qml`);
const launcher = name === "Launcher";
const rowsName = launcher ? "results" : "rows";
const calls = [];
const context = vm.createContext({
sessionState, stateReady: false, Util: util,
launcherService: launcher ? service : null,
clipboardService: launcher ? null : service,
rowHeight: launcher ? 52 : 56,
rowStride: launcher ? 56 : 62,
Qt: { ControlModifier: 1 },
surfaces: { closePopout: reason => calls.push(["close", reason]) },
pasteRequested: id => calls.push(["paste", id]),
sessionStateChanged() {},
detailContent: {}, detailFlick: { contentY: 0, height: 120 },
});
const start = source.indexOf(" function updateState(");
const end = source.indexOf(" width: parent ?", start);
assert(start >= 0 && end > start, "method block exists");
vm.runInContext(source.slice(start, end), context);
const expressionStart = source.indexOf("readonly property var panelState:") + "readonly property var panelState:".length;
const expressionEnd = source.indexOf(` readonly property var ${rowsName}:`, expressionStart);
const expression = source.slice(expressionStart, expressionEnd).trim();
Object.defineProperties(context, {
query: { get: () => String(sessionState.query || "") },
panelState: { get: () => vm.runInContext(`(${expression})`, context) },
[rowsName]: { get: () => context.panelState[rowsName] || [] },
selectedIndex: { get: () => context.selectionIndex() },
selectedRow: { get: () => context[rowsName][context.selectedIndex] || {} },
selectedActions: { get: () => context.selectedRow.actions || [] },
});
const viewport = { height: 120 };
Object.defineProperty(viewport, "contentY", {
get: () => Math.max(0, Math.min(sessionState.scrollY, context[rowsName].length * context.rowStride - 4 - viewport.height)),
});
context[launcher ? "resultsFlick" : "list"] = viewport;
return { context, calls, source, viewport };
}
const entries = Array.from({ length: 80 }, (_, i) => ({
id: `item-${i}`, title: `match ${i}`, pinned: false,
actions: Array.from({ length: 8 }, (_, n) => ({ id: `action-${n}` })),
}));
for (const name of ["Launcher", "Clipboard"]) {
const key = name === "Launcher" ? "results" : "rows";
const service = {
entries: entries.slice(),
search(query) { return JSON.stringify({ [key]: this.entries.filter(row => row.title.includes(query)) }); },
};
const state = { query: "match", selectedId: "item-20", scrollY: 1000 };
const { context: first } = panel(name, service, state);
first.reconcileSelection();
assert.equal(first.selectedRow.id, "item-20");
assert.equal(state.scrollY, 1000, "initial reconciliation preserves saved scroll");
const viewportName = name === "Launcher" ? "resultsFlick" : "list";
const viewport = first[viewportName];
first[viewportName] = { contentY: 37, height: 120 };
first.rememberScroll();
assert.equal(state.scrollY, 1000, "initial layout cannot overwrite saved scroll");
first.stateReady = true;
first.rememberScroll();
assert.equal(state.scrollY, 37, "user scrolling updates the retained owner");
first[viewportName] = viewport;
first.select(79);
assert.equal(state.selectedId, "item-79", "all rows remain selectable, beyond the old 60-row cap");
assert.equal(state.scrollY, 79 * first.rowStride + first.rowHeight - 120);
first.select(-20);
assert.equal(state.selectedId, "item-0");
assert.equal(state.scrollY, 0);
first.select(20);
const saved = { ...state };
const { context: reopened } = panel(name, service, state);
reopened.reconcileSelection();
assert.equal(reopened.query, saved.query);
assert.equal(reopened.selectedRow.id, saved.selectedId);
assert.equal(state.scrollY, saved.scrollY, "recreated panel uses owner scroll");
service.entries = service.entries.toReversed();
reopened.reconcileSelection();
assert.equal(reopened.selectedRow.id, "item-20", "service reorder preserves selected ID");
service.entries = service.entries.filter(row => row.id !== "item-20");
reopened.reconcileSelection();
assert.equal(reopened.selectedRow.id, "item-79", "removed selection falls back to first match");
reopened[name === "Launcher" ? "setQuery" : "search"]("absent");
reopened.reconcileSelection();
assert.equal(reopened.selectedIndex, -1);
assert.equal(state.selectedId, "");
assert.equal(state.query, "absent", "zero matches do not clear the query");
reopened[name === "Launcher" ? "setQuery" : "search"]("");
reopened.reconcileSelection();
assert.equal(state.query, "", "explicit clear remains available");
}
const writes = [];
const clipboard = {
entries: entries.slice(0, 3), privateMode: false, lastAction: {},
search(query) {
return JSON.stringify({ rows: this.entries.filter(row => row.title.includes(query)), privateMode: this.privateMode, lastAction: this.lastAction });
},
copy(id) { writes.push(["copy", id]); this.lastAction = { kind: "copy", id }; return this.search(""); },
togglePin(id) { writes.push(["pin", id]); this.entries = this.entries.map(row => row.id === id ? { ...row, pinned: !row.pinned } : row); return this.search(""); },
deleteRow(id) { writes.push(["delete", id]); this.entries = this.entries.filter(row => row.id !== id); return this.search(""); },
setPrivateMode(value) { writes.push(["private", value]); this.privateMode = value; return this.search(""); },
paste() { assert.fail("panel must not dispatch service paste"); },
};
const { context: clip, calls: clipCalls } = panel("Clipboard", clipboard, { query: "match 1", selectedId: "item-1", scrollY: 0 });
clip.copy("item-1");
clip.togglePin("item-1");
clip.setPrivateMode(true);
assert.equal(clip.rows.length, 1, "unfiltered command return values never replace the search binding");
assert.equal(clip.rows[0].pinned, true);
assert.equal(clip.panelState.privateMode, true);
assert.equal(clip.panelState.lastAction.kind, "copy");
clip.paste("item-1");
clip.paste("absent");
assert.deepEqual(clipCalls, [["paste", "item-1"]], "paste is intent only and rejects missing rows");
clipboard.entries.push({ id: "new", title: "match 1 new" });
assert.equal(clip.rows.length, 2, "later captures remain reactive after actions");
clip.deleteRow("item-1");
clip.reconcileSelection();
assert.equal(clip.query, "match 1");
assert.equal(clip.selectedRow.id, "new");
assert.deepEqual(writes, [["copy", "item-1"], ["pin", "item-1"], ["private", true], ["delete", "item-1"]]);
const launchWrites = [];
const launcher = {
search: () => JSON.stringify({ results: entries }),
activate: row => launchWrites.push(["activate", JSON.parse(row).id]),
activateAction: (row, index) => launchWrites.push(["action", JSON.parse(row).id, index]),
};
const { context: launch, calls: launchCalls } = panel("Launcher", launcher);
launch.select(4);
launch.activateSelected();
launch.activateAction(7);
launch.activateAction(8);
launch.keyAction({ modifiers: 1, text: "2", isAutoRepeat: true });
launch.keyAction({ modifiers: 1, text: "2", isAutoRepeat: false });
assert.deepEqual(launchWrites, [["activate", "item-4"], ["action", "item-4", 7], ["action", "item-4", 1]]);
assert.equal(launchCalls.length, 3, "successful dispatch closes once");
launch.revealAction({ mapToItem: () => ({ y: 400 }), height: 42 });
assert.equal(launch.detailFlick.contentY, 322, "lower action is revealed completely");
launch.revealAction({ mapToItem: () => ({ y: 10 }), height: 42 });
assert.equal(launch.detailFlick.contentY, 10, "reverse traversal reveals upper action");
for (const name of ["Launcher", "Clipboard"]) {
const { context } = panel(name, null);
context.reconcileSelection();
assert.equal(context.selectedIndex, -1);
context.select(0);
if (name === "Launcher") {
context.activateSelected();
context.activateAction(0);
} else {
context.copy("missing"); context.paste("missing");
context.togglePin("missing"); context.deleteRow("missing"); context.setPrivateMode(true);
}
}
console.log("search-panels.test.mjs: state and command logic pass; Qt interaction and visual validation not run");