Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/search-panels.test.mjs

Raw
import 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");