mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
236 lines
11 KiB
JavaScript
236 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import vm from "node:vm";
|
|
import test from "node:test";
|
|
|
|
const source = readFileSync(new URL("../Services/PluginService.qml", import.meta.url), "utf8");
|
|
|
|
function service({ enabled = true, type = "widget" } = {}) {
|
|
const compiled = new Map();
|
|
const files = new Map();
|
|
const requests = [];
|
|
const pending = [];
|
|
const failures = [];
|
|
const destroyed = [];
|
|
const manifest = { id: "fixture", name: "Fixture", type, component: "./Widget.qml", settings: "./Settings.qml" };
|
|
const context = vm.createContext({
|
|
Date, Object,
|
|
availablePlugins: {}, loadedPlugins: {}, knownManifests: {}, pathToPluginId: {},
|
|
pluginWidgetComponents: {}, pluginDesktopComponents: {}, pluginDaemonComponents: {},
|
|
pluginLauncherComponents: {}, pluginDashComponents: {}, pluginDashCardComponents: {},
|
|
pluginInstances: {}, pluginDaemonInstances: {}, installingPlugins: {}, _componentRevisions: {},
|
|
_daemonSpawnQueue: [], _daemonSpawnTimer: { restart() {} },
|
|
pluginSurfaceKeys: ["widget", "desktop", "daemon", "launcher", "dash", "dashCard"],
|
|
log: { error() {}, warn() {} },
|
|
Paths: { toFileUrl: path => path.startsWith("file://") ? path : "file://" + path },
|
|
SettingsData: { getPluginSetting: () => enabled },
|
|
DMSService: { update: (id, callback) => requests.push(callback), installedPlugins: [] },
|
|
ShellVersionService: { checkVersionRequirement: requirement => !requirement.startsWith(">=99"), getParsedShellVersion: () => ({}) },
|
|
Component: { Error: 3, PreferSynchronous: 0 },
|
|
I18n: { tr: text => ({ arg: value => text.replace("%1", value) }) },
|
|
ToastService: { showError() {} },
|
|
Qt: { createComponent(url) {
|
|
if (!compiled.has(url))
|
|
compiled.set(url, { value: files.get(url.split("?")[0]), createObject() { return this.value; }, destroy() { destroyed.push(url); } });
|
|
return compiled.get(url);
|
|
} },
|
|
pluginLoaded() {}, pluginUnloaded() {}, pluginListUpdated() {},
|
|
pluginLoadFailed: (id, error) => failures.push(error),
|
|
_clearLoadError() {}, _setLoadError() {}, _updateAvailablePluginsList() {},
|
|
_loadPluginTranslations() {}, _cleanupPluginStateWriter() {},
|
|
isPluginLoaded: id => !!context.loadedPlugins[id],
|
|
loadPluginManifestFile: (path, source, time) => pending.push(() => context._onManifestParsed(path, manifest, source, time))
|
|
});
|
|
context.root = context;
|
|
for (const match of source.matchAll(/^ function (\w+)\([^\n]*\)(?:: \w+)? \{\n[\s\S]*?^ \}/gm)) {
|
|
if (match[1] in context)
|
|
continue;
|
|
vm.runInContext(match[0], context);
|
|
}
|
|
files.set("file:///plugins/fixture/Widget.qml", 12);
|
|
context._onManifestParsed("/plugins/fixture/plugin.json", manifest, "user", 1);
|
|
return { context, files, requests, pending, failures, manifest, destroyed };
|
|
}
|
|
|
|
test("successful updates replace cached components and reread the manifest", () => {
|
|
const { context, files, requests, pending, manifest } = service();
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, 12);
|
|
files.set("file:///plugins/fixture/Widget.qml", 16);
|
|
manifest.version = "2.0.0";
|
|
let response;
|
|
context.updatePlugin("fixture", value => response = value);
|
|
const result = { result: { success: true } };
|
|
requests.shift()(result);
|
|
assert.equal(response, result);
|
|
assert.equal(pending.length, 1);
|
|
pending.shift()();
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, 16);
|
|
assert.equal(context.availablePlugins.fixture.version, "2.0.0");
|
|
});
|
|
|
|
test("an update whose registry entry needs a newer shell is refused before the download", () => {
|
|
const { context, requests, pending } = service();
|
|
context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=99.0.0" }];
|
|
let response;
|
|
context.updatePlugin("fixture", value => response = value);
|
|
assert.equal(requests.length, 0);
|
|
assert.equal(pending.length, 0);
|
|
assert.match(response.error, /99\.0\.0/);
|
|
context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=1.0.0" }];
|
|
context.updatePlugin("fixture");
|
|
assert.equal(requests.length, 1);
|
|
});
|
|
|
|
test("failed updates preserve the running plugin and forward the error", () => {
|
|
const { context, requests, pending } = service();
|
|
const component = context.pluginWidgetComponents.fixture;
|
|
let response;
|
|
context.updatePlugin("fixture", value => response = value);
|
|
requests.shift()({ error: "download failed" });
|
|
assert.equal(response.error, "download failed");
|
|
assert.equal(pending.length, 0);
|
|
assert.equal(context.pluginWidgetComponents.fixture, component);
|
|
});
|
|
|
|
test("updating a disabled plugin keeps it disabled and its next load is fresh", () => {
|
|
const { context, files, requests, pending } = service({ enabled: false });
|
|
context.loadPlugin("fixture");
|
|
context.unloadPlugin("fixture");
|
|
files.set("file:///plugins/fixture/Widget.qml", 18);
|
|
context.updatePlugin("fixture");
|
|
requests.shift()({});
|
|
pending.shift()();
|
|
assert.equal(context.isPluginLoaded("fixture"), false);
|
|
context.loadPlugin("fixture");
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, 18);
|
|
});
|
|
|
|
test("reload and rescan retain fresh components across disable and enable", () => {
|
|
const { context, files, pending } = service();
|
|
for (let value = 13; value <= 16; value++) {
|
|
files.set("file:///plugins/fixture/Widget.qml", value);
|
|
assert.equal(context.reloadPlugin("fixture"), true);
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, value);
|
|
context.unloadPlugin("fixture");
|
|
context.loadPlugin("fixture");
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, value);
|
|
}
|
|
files.set("file:///plugins/fixture/Widget.qml", 20);
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
assert.equal(context.pluginWidgetComponents.fixture.value, 20);
|
|
});
|
|
|
|
test("rescan picks up changed surfaces and preserves automatic desktop loading", () => {
|
|
const { context, files, pending, manifest } = service({ enabled: false, type: "desktop" });
|
|
assert.equal(context.pluginDesktopComponents.fixture.value, 12);
|
|
manifest.component = "./Desktop.qml";
|
|
files.set("file:///plugins/fixture/Desktop.qml", 24);
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
assert.equal(context.pluginDesktopComponents.fixture.value, 24);
|
|
});
|
|
|
|
test("single component dash plugins load onto their dash surface", () => {
|
|
for (const [type, key] of [["dash", "pluginDashComponents"], ["dashCard", "pluginDashCardComponents"]]) {
|
|
const { context } = service({ type });
|
|
assert.deepEqual(context.availablePlugins.fixture.surfaces, [type]);
|
|
assert.equal(context[key].fixture.value, 12);
|
|
assert.equal(context.pluginWidgetComponents.fixture, undefined);
|
|
}
|
|
});
|
|
|
|
test("unloading destroys the plugin's components", () => {
|
|
const { context, destroyed } = service();
|
|
assert.equal(context.unloadPlugin("fixture"), true);
|
|
assert.deepEqual(destroyed, ["file:///plugins/fixture/Widget.qml"]);
|
|
assert.equal(context.pluginWidgetComponents.fixture, undefined);
|
|
});
|
|
|
|
test("settings and startup checks use the current plugin revision", () => {
|
|
const { context, files } = service();
|
|
const path = "/plugins/fixture/Settings.qml";
|
|
files.set("file://" + path, 12);
|
|
const before = context.pluginComponentUrl("fixture", path);
|
|
assert.equal(context.Qt.createComponent(before).value, 12);
|
|
files.set("file://" + path, 18);
|
|
context.reloadPlugin("fixture");
|
|
const after = context.pluginComponentUrl("fixture", path);
|
|
assert.equal(context.Qt.createComponent(after).value, 18);
|
|
assert.equal(context.pluginComponentUrl("fixture", "file://" + path), after);
|
|
assert.equal(context.pluginComponentUrl("fixture", null), "");
|
|
assert.equal(context._makeStartupCheckObject("fixture", { startupCheckPath: path }), 18);
|
|
});
|
|
|
|
test("unload failures abort reload and rescan without discarding the manifest", () => {
|
|
const { context, pending } = service();
|
|
const plugin = context.availablePlugins.fixture;
|
|
context.unloadPlugin = () => false;
|
|
assert.equal(context.reloadPlugin("fixture"), false);
|
|
context.forceRescanPlugin("fixture");
|
|
assert.equal(context.availablePlugins.fixture, plugin);
|
|
assert.equal(pending.length, 0);
|
|
});
|
|
|
|
test("updated startup checks still prevent activation on failure", () => {
|
|
const { context, files, pending, manifest, failures } = service();
|
|
manifest.startupCheck = "./Check.qml";
|
|
files.set("file:///plugins/fixture/Check.qml", {
|
|
check: () => "dependency unavailable",
|
|
destroy() {}
|
|
});
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
assert.equal(context.isPluginLoaded("fixture"), false);
|
|
assert.equal(failures.at(-1), "dependency unavailable");
|
|
files.set("file:///plugins/fixture/Check.qml", {
|
|
check: () => null,
|
|
destroy() {}
|
|
});
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
assert.equal(context.isPluginLoaded("fixture"), true);
|
|
files.set("file:///plugins/fixture/Check.qml", {
|
|
check: () => "dependency removed",
|
|
destroy() {}
|
|
});
|
|
context.reloadPlugin("fixture");
|
|
assert.equal(context.isPluginLoaded("fixture"), false);
|
|
assert.equal(failures.at(-1), "dependency removed");
|
|
});
|
|
|
|
test("startup checks that cannot run prevent activation", () => {
|
|
const { context, files, pending, manifest, failures } = service();
|
|
const createComponent = context.Qt.createComponent;
|
|
context.Qt.createComponent = url => url.startsWith("file:///plugins/fixture/Broken.qml") ? { status: 3, errorString: () => "module missing" } : createComponent(url);
|
|
const cases = [
|
|
["./Broken.qml", null, "module missing"],
|
|
["./Check.qml", { destroy() {} }, "startupCheck has no check function"],
|
|
["./Check.qml", { check() { throw new Error("sync throw"); }, destroy() {} }, "sync throw"],
|
|
["./Check.qml", { check(done) { throw new Error("async throw"); }, destroy() {} }, "async throw"]
|
|
];
|
|
for (const [path, probe, error] of cases) {
|
|
manifest.startupCheck = path;
|
|
files.set("file:///plugins/fixture/Check.qml", probe);
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
assert.equal(context.isPluginLoaded("fixture"), false, error);
|
|
assert.equal(failures.at(-1), error);
|
|
}
|
|
});
|
|
|
|
test("composite updates refresh every surface without creating daemons synchronously", () => {
|
|
const { context, files, pending, manifest } = service();
|
|
delete manifest.component;
|
|
manifest.components = Object.fromEntries(context.pluginSurfaceKeys.map(surface => [surface, "./" + surface + ".qml"]));
|
|
for (const value of [12, 20]) {
|
|
for (const surface of context.pluginSurfaceKeys)
|
|
files.set("file:///plugins/fixture/" + surface + ".qml", value);
|
|
context.forceRescanPlugin("fixture");
|
|
pending.shift()();
|
|
for (const key of ["pluginWidgetComponents", "pluginDesktopComponents", "pluginDaemonComponents", "pluginLauncherComponents", "pluginDashComponents", "pluginDashCardComponents"])
|
|
assert.equal(context[key].fixture.value, value);
|
|
assert.equal(context.pluginDaemonInstances.fixture, undefined);
|
|
}
|
|
});
|