Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/live-panels.test.mjs

Raw
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import vm from "node:vm";
import { test } from "node:test";

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);

// Execute production QML JavaScript with synthetic services. This checks command
// routing and reconciliation, not Qt bindings, delegate lifetime or real input.
function panel(name, service, extra = {}) {
    const source = read(`${name}Panel.qml`);
    const serviceName = `${name.toLowerCase()}Service`;
    const context = vm.createContext({ [serviceName]: service, Util: util, ...extra });
    context.root = context;
    const expression = source.slice(source.indexOf("readonly property var panelState:") + "readonly property var panelState:".length,
        source.indexOf("\n    readonly property", source.indexOf("readonly property var panelState:") + 1));
    Object.defineProperty(context, "panelState", { get: () => vm.runInContext(`(${expression})`, context) });
    const start = source.indexOf("    function ");
    const end = source.indexOf("\n    width:", start);
    vm.runInContext(source.slice(start, end), context);
    return context;
}

function serviceFixture(initial) {
    const fixture = { current: initial, calls: [], stateJson() { return JSON.stringify(this.current); } };
    for (const name of ["setVolume", "toggleMute", "setDefaultSink", "setDefaultSource", "setAll", "setDisplay", "setupDdc", "action", "seek", "select", "sessionAction", "confirmAction", "cancelAction", "primaryActivate", "displayMenu", "secondaryActivate"])
        fixture[name] = (...args) => {
            fixture.calls.push([name, ...args]);
            return '{"staleCommandReturn":true}';
        };
    return fixture;
}

function stableModel() {
    const rows = [];
    const operations = [];
    const context = vm.createContext({
        get count() { return rows.length; },
        get: index => rows[index],
        insert: (index, row) => { rows.splice(index, 0, { ...row }); operations.push("insert"); },
        set: (index, row) => { Object.assign(rows[index], row); operations.push("set"); },
        move: (from, to, count) => { rows.splice(to, 0, ...rows.splice(from, count)); operations.push("move"); },
        remove: (index, count) => { rows.splice(index, count); operations.push("remove"); },
    });
    const source = read("StableListModel.qml");
    vm.runInContext(source.slice(source.indexOf("    function sync("), source.lastIndexOf("\n}")), context);
    return { rows, operations, sync: context.sync };
}

test("stable reconciliation updates and reorders surviving rows without removal", () => {
    const model = stableModel();
    model.sync([{ id: "a", level: 0.2 }, { id: "b", level: 0.8 }]);
    const [a, b] = model.rows;
    model.operations.length = 0;
    model.sync([{ id: "a", level: 0.2 }, { id: "b", level: 0.8 }]);
    assert.deepEqual(model.operations, [], "unchanged snapshots do not emit model mutations");
    model.sync([{ id: "b", level: 0.9 }, { id: "c", level: 0.5 }, { id: "a", level: 0.3 }]);
    assert.equal(model.rows[0], b);
    assert.equal(model.rows[2], a);
    assert.equal(a.rowData.level, 0.3);
    assert.equal(b.rowData.level, 0.9);
    assert(!model.operations.includes("remove"));
    model.sync([{ id: "a", level: 0.4 }]);
    assert.equal(model.rows[0], a);
    assert.equal(model.rows.length, 1);
    model.sync([]);
    assert.equal(model.rows.length, 0);
});

test("every volume step reconciles into the same row during a simulated drag", () => {
    const model = stableModel();
    const service = serviceFixture({ sinks: [{ id: "speaker", percent: 0 }] });
    const audio = panel("Audio", service);
    model.sync(audio.panelState.sinks);
    const row = model.rows[0];
    for (let step = 0; step <= 20; step++) {
        audio.setVolume("speaker", step * 0.075);
        service.current = { sinks: [{ id: "speaker", percent: Math.round(step * 7.5) }] };
        model.sync(audio.panelState.sinks);
        assert.equal(model.rows[0], row);
    }
    assert.equal(service.calls.length, 21);
    assert.equal(row.rowData.percent, 150);
    assert.equal(model.operations.filter(op => op === "insert").length, 1);
    assert(!model.operations.includes("remove"));
});

test("audio commands ignore returned snapshots and bound volume", () => {
    const service = serviceFixture({ marker: "initial" });
    const audio = panel("Audio", service);
    audio.setVolume("sink", -1);
    audio.setVolume("sink", 2);
    audio.toggleMute("sink");
    audio.selectSink("sink");
    audio.selectSource("mic");
    assert.deepEqual(service.calls, [["setVolume", "sink", 0], ["setVolume", "sink", 1.5], ["toggleMute", "sink"], ["setDefaultSink", "sink"], ["setDefaultSource", "mic"]]);
    service.current = { marker: "external" };
    assert.equal(audio.panelState.marker, "external");
});

test("brightness routes bounded global and per-display changes without snapshot ownership", () => {
    const service = serviceFixture({ global: { mode: "uniform", level: 0.1 } });
    const brightness = panel("Brightness", service, { hasGlobal: true });
    brightness.setAllAt(2, 1);
    brightness.setDisplayAt("backlight", -1, 1);
    brightness.hasGlobal = false;
    brightness.setAllAt(1, 1);
    assert.deepEqual(service.calls, [["setAll", 1], ["setDisplay", "backlight", 0]]);
    service.current = { global: { mode: "uniform", level: 0.7 } };
    assert.equal(brightness.panelState.global.level, 0.7);
});

test("media capability matrix gates every transport and seek command", () => {
    const service = serviceFixture({ activePlayer: { title: "first" } });
    const media = panel("Media", service, {
        nativePlayer: { canControl: true, canGoPrevious: false, canGoNext: false, canTogglePlaying: false },
        player: { length: 200, canSeek: false, canShuffle: false, canRepeat: false, playbackState: "playing" },
    });
    const seekBinding = read("MediaPanel.qml").match(/readonly property bool canSeek: (.+)/)[1];
    Object.defineProperty(media, "canSeek", { get: () => vm.runInContext(seekBinding, media) });
    const capabilities = { shuffle: "canShuffle", repeat: "canRepeat", previous: "canGoPrevious", next: "canGoNext", playPause: "canTogglePlaying" };
    for (const [action, flag] of Object.entries(capabilities)) {
        const owner = ["shuffle", "repeat"].includes(action) ? media.player : media.nativePlayer;
        assert.equal(media.actionEnabled(action), false);
        media.runAction(action);
        assert.equal(service.calls.length, 0);
        owner[flag] = true;
        assert.equal(media.actionEnabled(action), true);
        media.runAction(action);
        assert.deepEqual(service.calls.splice(0), [["action", action]]);
        media.nativePlayer.canControl = false;
        media.runAction(action);
        assert.equal(service.calls.length, 0);
        media.nativePlayer.canControl = true;
        owner[flag] = false;
    }
    media.seekByRatio(0.5);
    assert.equal(service.calls.length, 0);
    media.player.canSeek = true;
    media.nativePlayer.canControl = false;
    media.seekByRatio(0.5);
    media.nativePlayer.canControl = true;
    media.player.length = 0;
    media.seekByRatio(0.5);
    assert.equal(service.calls.length, 0, "seek requires control capability and a positive duration");
    media.player.length = 200;
    media.seekByRatio(-1);
    media.seekByRatio(2);
    media.selectPlayer("other");
    assert.deepEqual(service.calls, [["seek", 0], ["seek", 200], ["select", "other"]]);
    assert.equal(media.actionLabel("playPause"), "pause");
    media.player.playbackState = "paused";
    assert.equal(media.actionLabel("playPause"), "play");
    service.current = { activePlayer: { title: "external" } };
    assert.equal(media.panelState.activePlayer.title, "external");
    media.nativePlayer = null;
    assert.equal(media.actionEnabled("playPause"), false);
});

test("power dispatch does not take ownership of confirmation state", () => {
    const service = serviceFixture({ pendingConfirmation: "" });
    const focused = [];
    const power = panel("Power", service, {
        safeActions: ["lock", "suspend", "monitor-off"],
        destructiveActions: ["logout", "reboot", "poweroff"],
        Qt: { TabFocusReason: "tab" },
        actionRepeater: { itemAt: index => index < 0 ? null : { forceActiveFocus: reason => focused.push([index, reason]) } },
    });
    power.runSession("reboot");
    service.current = { pendingConfirmation: "reboot" };
    assert.equal(power.panelState.pendingConfirmation, "reboot");
    power.confirm("reboot");
    power.cancel();
    service.current = { pendingConfirmation: "", battery: { percent: 42 } };
    assert.equal(power.panelState.pendingConfirmation, "");
    assert.equal(power.panelState.battery.percent, 42);
    assert.deepEqual(service.calls, [["sessionAction", "reboot"], ["confirmAction", "reboot"], ["cancelAction"]]);
    assert.deepEqual(focused, [[4, "tab"], [4, "tab"]]);
});

test("tray primary, native menu and menu-only fallback route exactly once", () => {
    const service = serviceFixture({ items: [{ id: "app", title: "before" }] });
    const tray = panel("Tray", service);
    const source = read("TrayPanel.qml");
    const nativeMenu = { menu: null, anchor: {}, opens: 0, open() { this.opens++; } };
    const cell = vm.createContext({
        root: tray, rowData: { id: "app", onlyMenu: false }, width: 100, height: 60, nativeMenu,
        Qt: { rect: (x, y, width, height) => ({ x, y, width, height }) },
        QsWindow: { window: "window" },
        mapToItem: (_, x, y) => ({ x: x + 200, y: y + 100 }),
    });
    cell.itemCell = cell;
    const start = source.indexOf("                function openMenu(");
    vm.runInContext(source.slice(start, source.indexOf("\n                theme:", start)), cell);
    cell.activate();
    assert.deepEqual(service.calls.splice(0), [["primaryActivate", "app", "window", 250, 130]]);
    cell.rowData.onlyMenu = true;
    cell.activate();
    assert.deepEqual(service.calls.splice(0), [["displayMenu", "app", "window", 250, 130]]);
    nativeMenu.menu = {};
    cell.activate();
    assert.equal(nativeMenu.opens, 1);
    assert.deepEqual(nativeMenu.anchor.rect, { x: 50, y: 30, width: 1, height: 1 });
    assert.equal(service.calls.length, 0);
    tray.secondary("app");
    assert.deepEqual(service.calls, [["secondaryActivate", "app"]]);
    service.current = { items: [{ id: "app", title: "external" }] };
    assert.equal(tray.panelState.items[0].title, "external");
});

test("panel source contracts retain state bindings and host-bounded scrolling", () => {
    for (const name of ["Audio", "Brightness", "Media", "Power", "Tray"]) {
        const source = read(`${name}Panel.qml`);
        assert.match(source, /readonly property var panelState:/);
        assert.doesNotMatch(source, /panelState\s*=|refreshFrom\(/);
        assert.match(source, /Flickable\s*\{\s*id: root/);
        assert.match(source, /contentHeight:/);
        assert.match(source, /clip: true/);
    }
    for (const name of ["Audio", "Brightness", "Tray"])
        assert.match(read(`${name}Panel.qml`), /StableListModel\s*\{/);
    for (const name of ["Audio", "Brightness", "Media"])
        assert.match(read(`${name}Panel.qml`), /Accessible\.name:.*(?:volume|brightness|playback)/);
    assert.doesNotMatch(read("AudioPanel.qml"), /MouseArea\s*\{/);
    assert.match(read("TrayPanel.qml"), /acceptedButtons: Qt\.MiddleButton \| Qt\.RightButton/);
    assert.doesNotMatch(read("PowerPanel.qml"), /Keys\.on(?:Return|Enter|Escape)Pressed/);
});