Files
DankMaterialShell/quickshell/tests/admin-policy.test.mjs
T
purian23 d694107f36 feat(density): interface density setting for shell surfaces
- density setting (spacious, comfortable, compact, dense)
- bar, dock, island and frame sizes follow density presets and keep values the user changed
- launcher no longer rebuilds every frame while the action panel animates
- cc toggle rows match the single-height list rows
- ipc settings set runs change hooks and rejects unknown keys and density values
- state layer focus and pressed follow the library's m3 values
- bump dux-qml-common for density menu and file browser metrics

(cherry picked from commit 76025038df)
2026-10-27 20:49:36 -04:00

248 lines
14 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import vm from "node:vm";
import { loadScript } from "./qml-script.mjs";
function functionSource(file, name) {
const source = readFileSync(new URL(file, import.meta.url), "utf8");
const found = source.match(new RegExp(`^ function ${name}\\([^)]*\\)(?:: \\w+)? \\{\\n[\\s\\S]*?^ \\}`, "m"));
if (!found)
throw new Error(`${name} not found in ${file}`);
return found[0];
}
test("the lock IPC switch drops the lock demo keybind action and keeps the others", () => {
const getDmsActions = functionSource("../Services/KeybindsService.qml", "getDmsActions");
const Actions = loadScript(new URL("../Common/KeybindActions.js", import.meta.url));
for (const disableLockIpcUnlock of [false, true]) {
const context = vm.createContext({ Actions, CompositorService: { isNiri: false, isHyprland: false }, AdminPolicy: { disableLockIpcUnlock } });
vm.runInContext(getDmsActions, context);
const ids = context.getDmsActions().map(action => action.id);
assert.equal(ids.includes("spawn dms ipc call lock demo"), !disableLockIpcUnlock);
assert.ok(ids.includes("spawn dms ipc call lock lock"));
assert.equal(ids.length, Actions.getDmsActions(false, false).length - (disableLockIpcUnlock ? 1 : 0));
}
});
test("the user plugin switch keeps the user tree out of a scan and a resync", () => {
const user = "/home/user/.config/DankMaterialShell/plugins";
const system = "/etc/xdg/quickshell/dms-plugins";
const folder = dirs => ({ folder: "", count: dirs.length, get: (i, role) => role === "filePath" ? "file://" + dirs[i] : 0 });
for (const disableUserPlugins of [false, true]) {
const loaded = [];
const context = vm.createContext({
Date,
AdminPolicy: { disableUserPlugins },
Paths: { toFileUrl: path => "file://" + path },
pluginDirectory: user,
systemPluginDirectory: system,
userWatcher: folder([user + "/probe"]),
systemWatcher: folder([system + "/admitted"]),
resyncDebounce: { restart() {} },
checkPluginDirectoryExists() {},
knownManifests: {},
pathToPluginId: {},
directoryModifiedTimes: {},
loadPluginManifestFile: (path, source) => loaded.push([path, source])
});
context.root = context;
for (const name of ["snapshotModel", "resyncAll", "scanPlugins"])
vm.runInContext(functionSource("../Services/PluginService.qml", name), context);
context.scanPlugins();
assert.equal(context.userWatcher.folder, disableUserPlugins ? "" : "file://" + user);
assert.equal(context.systemWatcher.folder, "file://" + system);
context.resyncAll();
const userManifests = disableUserPlugins ? [] : [[user + "/probe/plugin.json", "user"]];
assert.deepEqual(loaded, [...userManifests, [system + "/admitted/plugin.json", "system"]]);
}
});
function settingsIpcHandlers() {
const source = readFileSync(new URL("../DMSShellIPC.qml", import.meta.url), "utf8");
const block = source.slice(source.indexOf(" function get(key: string)"), source.indexOf(' target: "settings"'));
return [...block.matchAll(/^ {8}function \w+\([^)]*\)(?:: \w+)? \{\n[\s\S]*?^ {8}\}$/gm)].map(match => match[0].replace(/\((.*?)\)(?:: \w+)? \{/, (_, params) => "(" + params.replace(/: \w+/g, "") + ") {"));
}
test("the settings IPC switch refuses set and keeps get", () => {
const handlers = settingsIpcHandlers();
for (const disableSettingsIpcSet of [false, true]) {
const SettingsData = { lockBeforeSuspend: false, hasSetting: key => key in SPEC, set(key, value) { this[key] = value; return true; }, isManaged: () => false };
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet }, SettingsData, log: { warn() {} } });
for (const handler of handlers)
vm.runInContext(handler, context);
assert.equal(context.set("lockBeforeSuspend", "true"), disableSettingsIpcSet ? "SETTINGS_SET_DISABLED" : "SETTINGS_SET_SUCCESS");
assert.equal(SettingsData.lockBeforeSuspend, !disableSettingsIpcSet);
assert.equal(context.get("lockBeforeSuspend"), String(!disableSettingsIpcSet));
}
});
const ManagedSettings = loadScript(new URL("../Common/settings/ManagedSettings.js", import.meta.url));
const SettingsStore = loadScript(new URL("../Common/settings/SettingsStore.js", import.meta.url));
const SPEC = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)).SPEC;
function settingsDataContext(globals, names) {
const context = vm.createContext(globals);
context.root = context;
for (const name of names)
vm.runInContext(functionSource("../Common/SettingsData.qml", name), context);
return context;
}
test("managed lock settings keep only values of the default's type", () => {
const warnings = [];
const settings = ManagedSettings.parse(JSON.stringify({
lockBeforeSuspend: true,
customPowerActionLock: "",
acLockTimeout: 300,
u2fMode: null,
enableFprint: "yes",
maxFprintTries: [3],
notASetting: true
}), (...args) => warnings.push(args[1]));
assert.deepEqual({ ...settings }, { lockBeforeSuspend: true, customPowerActionLock: "", acLockTimeout: 300 });
assert.deepEqual(warnings.sort(), ["enableFprint", "maxFprintTries", "notASetting", "u2fMode"]);
assert.deepEqual({ ...ManagedSettings.parse("", () => assert.fail("no policy, no warning")) }, {});
});
test("managed settings win over every settings file, on the first load and on each reload", () => {
const version = Number(readFileSync(new URL("../Common/SettingsData.qml", import.meta.url), "utf8").match(/settingsConfigVersion: (\d+)/)[1]);
let files = { configVersion: version, lockBeforeSuspend: false, customPowerActionLock: "true", acLockTimeout: 60 };
const context = settingsDataContext({
AdminPolicy: { lockSettings: { lockBeforeSuspend: true, customPowerActionLock: "" } },
SettingsData: { Stage: { Partial: 2, Ready: 3 } },
Store: SettingsStore,
settingsConfigVersion: version,
_hasLoaded: false,
_settingsStage: 3,
_parseError: false,
frameEnabled: false,
frameMode: "",
_getSettingsObjectFromFiles: () => JSON.parse(JSON.stringify(files)),
_mergeSessionState() {},
Qt: { callLater() {} },
checkIconThemeDrift() {},
applyStoredTheme() {},
updateCompositorCursor() {},
updateFrameCompositorLayout() {},
loadPluginSettings() {},
clearIslandBars() {}
}, ["_loadSettings"]);
for (const userValue of [false, true]) {
files = Object.assign({}, files, { lockBeforeSuspend: userValue, acLockTimeout: files.acLockTimeout * 2 });
assert.equal(context._loadSettings(), false, "managed values are no unsaved change");
assert.equal(context.lockBeforeSuspend, true);
assert.equal(context.customPowerActionLock, "");
assert.equal(context.acLockTimeout, files.acLockTimeout);
}
});
test("a save leaves managed keys out of the settings files, and a user's own value in place", () => {
const main = "/home/u/.config/dms/settings.json";
const overlay = "/home/u/.config/dms/config.d/lock.json";
const fileSettings = { [main]: { lockBeforeSuspend: false, acLockTimeout: 60 }, [overlay]: { customPowerActionLock: "true", fadeToLockEnabled: false } };
const context = settingsDataContext({
AdminPolicy: { lockSettings: { lockBeforeSuspend: true, customPowerActionLock: "", batteryLockTimeout: 300 } },
_settingsFilesPaths: [main, overlay],
_settingsFiles: new Map(Object.keys(fileSettings).map(path => [path, { getSettings: () => Object.assign({}, fileSettings[path]) }])),
defaultSettingsFile: { filePath: main }
}, ["isManaged", "_splitSettingsByFile"]);
const split = context._splitSettingsByFile({ configVersion: 42, lockBeforeSuspend: true, customPowerActionLock: "", batteryLockTimeout: 300, acLockTimeout: 120, fadeToLockEnabled: true, lockAtStartup: true });
assert.deepEqual(JSON.parse(JSON.stringify(split)), {
[main]: { lockBeforeSuspend: false, acLockTimeout: 120, configVersion: 42, lockAtStartup: true },
[overlay]: { customPowerActionLock: "true", fadeToLockEnabled: true }
});
});
test("set and reset refuse managed keys and change the others", () => {
const changes = [];
const warnings = [];
const context = settingsDataContext({
AdminPolicy: { lockSettings: { lockBeforeSuspend: true } },
Spec: { SPEC, set: (root, key, value) => changes.push([key, value]) },
DensityPresets: loadScript(new URL("../Common/settings/DensityPresets.js", import.meta.url)),
density: "comfortable",
log: { warn: (...args) => warnings.push(args.at(-1)) },
saveSettings() {},
_hooks: {},
lockBeforeSuspend: true
}, ["isManaged", "set", "resetToDefault"]);
context.set("lockBeforeSuspend", false);
context.set("lockAtStartup", true);
context.resetToDefault(["lockBeforeSuspend", "acLockTimeout"]);
assert.deepEqual(changes, [["lockAtStartup", true], ["acLockTimeout", null]]);
assert.deepEqual(warnings, ["lockBeforeSuspend"]);
});
test("settings IPC set refuses a managed key, also with the settings IPC switch off", () => {
const handlers = settingsIpcHandlers();
const managed = new Set(["lockBeforeSuspend"]);
for (const disableSettingsIpcSet of [false, true]) {
const SettingsData = { lockBeforeSuspend: true, lockAtStartup: false, hasSetting: key => key in SPEC, set(key, value) { this[key] = value; return true; }, isManaged: key => managed.has(key) };
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet }, SettingsData, log: { warn() {} } });
for (const handler of handlers)
vm.runInContext(handler, context);
assert.equal(context.set("lockBeforeSuspend", "false"), disableSettingsIpcSet ? "SETTINGS_SET_DISABLED" : "SETTINGS_SET_MANAGED");
assert.equal(SettingsData.lockBeforeSuspend, true);
assert.equal(context.set("lockAtStartup", "true"), disableSettingsIpcSet ? "SETTINGS_SET_DISABLED" : "SETTINGS_SET_SUCCESS");
assert.equal(SettingsData.lockAtStartup, !disableSettingsIpcSet);
}
});
test("settings IPC set runs the change hook with the previous value, saves, and refuses non-settings", () => {
const calls = [];
let saves = 0;
const SettingsData = settingsDataContext({
AdminPolicy: { lockSettings: {} },
Spec: loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)),
log: { warn() {} },
saveSettings: () => saves++,
_hooks: { regenSystemThemes: (root, key, previous) => calls.push([key, previous, root[key]]) },
matugenScheme: "scheme-tonal-spot",
matugenContrast: 0,
lockAtStartup: false,
isReadOnly: false
}, ["isManaged", "hasSetting", "set"]);
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet: false }, SettingsData, log: { warn() {} } });
for (const handler of settingsIpcHandlers())
vm.runInContext(handler, context);
assert.equal(context.set("matugenScheme", "scheme-fidelity"), "SETTINGS_SET_SUCCESS");
assert.equal(context.set("matugenContrast", "0.5"), "SETTINGS_SET_SUCCESS");
assert.equal(context.set("lockAtStartup", "true"), "SETTINGS_SET_SUCCESS");
assert.deepEqual(calls, [["matugenScheme", "scheme-tonal-spot", "scheme-fidelity"], ["matugenContrast", 0, 0.5]]);
assert.equal(context.set("isReadOnly", "true"), "SETTINGS_INVALID_KEY");
assert.equal(SettingsData.lockAtStartup, true);
assert.equal(SettingsData.isReadOnly, false);
assert.equal(saves, 3);
});
test("settings set refuses an unknown density, from IPC too, without saving or moving sizes", () => {
const moves = [];
let saves = 0;
const SettingsData = settingsDataContext({
AdminPolicy: { lockSettings: {} },
Spec: loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)),
DensityPresets: loadScript(new URL("../Common/settings/DensityPresets.js", import.meta.url)),
log: { warn() {} },
saveSettings: () => saves++,
_hooks: { applyDensityPresets: (root, key, previous) => moves.push([previous, root.density]) },
density: "comfortable"
}, ["isManaged", "hasSetting", "set"]);
const context = vm.createContext({ AdminPolicy: { disableSettingsIpcSet: false }, SettingsData, log: { warn() {} } });
for (const handler of settingsIpcHandlers())
vm.runInContext(handler, context);
for (const value of ["Compact", "tiny", ""])
assert.equal(context.set("density", value), "SETTINGS_SET_FAILURE", value);
assert.equal(context.set("density", "dense"), "SETTINGS_SET_SUCCESS");
assert.deepEqual([moves, saves, SettingsData.density], [[["comfortable", "dense"]], 1, "dense"]);
});
test("the legacy session migration leaves managed lock settings alone", () => {
const managed = new Set(["acLockTimeout", "lockBeforeSuspend"]);
const changes = [];
const context = vm.createContext({ SettingsData: { isManaged: key => managed.has(key), set: (key, value) => changes.push([key, value]) } });
vm.runInContext(functionSource("../Common/SessionData.qml", "migrateFromUndefinedToV1"), context);
context.migrateFromUndefinedToV1({ acMonitorTimeout: 30, acLockTimeout: 60, lockBeforeSuspend: false, loginctlLockIntegration: false, launchPrefix: "uwsm app --" });
assert.deepEqual(changes, [["acMonitorTimeout", 30], ["loginctlLockIntegration", false], ["launchPrefix", "uwsm app --"]]);
});