Files
DankMaterialShell/quickshell/tests/plugin-ranking.test.mjs
T
2026-10-20 12:23:25 -04:00

269 lines
18 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import vm from "node:vm";
import test from "node:test";
const rank = vm.createContext({});
vm.runInContext(readFileSync(new URL("../Common/PluginRanking.js", import.meta.url), "utf8").replace(".pragma library", ""), rank);
const NOW = Date.UTC(2026, 9, 10);
const DAY = 86400000;
const ago = days => new Date(NOW - days * DAY).toISOString();
const plugin = (id, extra) => Object.assign({ id, name: id, author: "a-" + id, category: "utilities", upvotes: 0, status: ["reviewed"], compositors: ["any"], screenshot: "x.png", created_at: ago(100), updated_at: ago(100) }, extra || {});
const ids = list => Array.from(list, p => p.id);
test("installed, broken, unmaintained and wrong-compositor plugins are never recommended", () => {
const list = [plugin("mine", { installed: true, upvotes: 50 }), plugin("broken", { status: ["broken"], upvotes: 50 }), plugin("old", { status: ["unmaintained"], upvotes: 50 }), plugin("hypr", { compositors: ["hyprland"], upvotes: 50 }), plugin("ok", { upvotes: 1 })];
assert.deepEqual(ids(rank.recommend(list, { now: NOW, compositor: "niri" })), ["ok"]);
assert.deepEqual(ids(rank.recommend(list, { now: NOW, compositor: "hyprland" })), ["hypr", "ok"]);
});
test("a plugin similar to an installed one outranks a slightly more voted stranger", () => {
const list = [plugin("shot", { installed: true, similar: ["capture"] }), plugin("capture", { upvotes: 3 }), plugin("stranger", { upvotes: 6, category: "media" })];
assert.equal(rank.recommend(list, { now: NOW })[0].id, "capture");
assert.equal(rank.reason(list[1], rank.buildTaste(list), NOW), "similar");
});
test("votes still win when the gap is large", () => {
const list = [plugin("shot", { installed: true, similar: ["capture"] }), plugin("capture", { upvotes: 1 }), plugin("star", { upvotes: 400, category: "media" })];
assert.equal(rank.recommend(list, { now: NOW })[0].id, "star");
});
test("trending favours votes gathered fast over the same votes gathered slowly", () => {
const list = [plugin("veteran", { upvotes: 10, created_at: ago(400) }), plugin("rising", { upvotes: 10, created_at: ago(10) }), plugin("silent", { upvotes: 0, created_at: ago(1) })];
assert.deepEqual(ids(rank.trendingNow(list, { now: NOW })), ["rising", "veteran"]);
});
test("a single vote on a brand-new plugin does not beat an established one", () => {
const list = [plugin("fresh", { upvotes: 1, created_at: ago(0) }), plugin("solid", { upvotes: 30, created_at: ago(60) })];
assert.equal(rank.trendingNow(list, { now: NOW })[0].id, "solid");
});
test("one author gets at most two places", () => {
const list = ["a", "b", "c", "d"].map((id, i) => plugin(id, { author: "same", upvotes: 40 - i })).concat([plugin("other", { upvotes: 1 })]);
assert.deepEqual(ids(rank.recommend(list, { now: NOW, limit: 3 })), ["a", "b", "other"]);
});
test("a short shelf is filled with the entries the author limit skipped", () => {
const list = [1, 2, 3, 4].map(n => plugin("p" + n, { author: "same", upvotes: 10 - n }));
assert.deepEqual(ids(rank.recommend(list, { now: NOW, limit: 3 })), ["p1", "p2", "p3"]);
list.push(plugin("other", { upvotes: 1 }));
assert.deepEqual(ids(rank.recommend(list, { now: NOW, limit: 3 })), ["p1", "p2", "other"]);
});
test("excluded ids are left out of every shelf", () => {
const list = [plugin("a", { upvotes: 9, created_at: ago(2) }), plugin("b", { upvotes: 5, created_at: ago(3) })];
const options = { now: NOW, exclude: ["a"] };
assert.deepEqual(ids(rank.recommend(list, options)), ["b"]);
assert.deepEqual(ids(rank.trendingNow(list, options)), ["b"]);
assert.deepEqual(ids(rank.newArrivals(list, options)), ["b"]);
});
test("new arrivals are the last two weeks, newest first", () => {
const list = [plugin("month", { created_at: ago(30) }), plugin("week", { created_at: ago(6) }), plugin("today", { created_at: ago(0.2) }), plugin("undated", { created_at: "" })];
assert.deepEqual(ids(rank.newArrivals(list, { now: NOW })), ["today", "week"]);
});
test("picks are stable within their period, differ from each other and rotate", () => {
const list = [];
for (let i = 0; i < 60; i++)
list.push(plugin("p" + i, { upvotes: 60 - i }));
const day = rank.pickOfTheDay(list, NOW);
const week = rank.pickOfTheWeek(list, NOW);
assert.equal(rank.pickOfTheDay(list, NOW + 5 * 3600000).id, day.id);
assert.notEqual(day.id, week.id);
const monday = Date.UTC(2026, 9, 5);
const weekIds = new Set([0, 1, 2, 3, 4, 5, 6].map(d => rank.pickOfTheWeek(list, monday + d * DAY).id));
assert.equal(weekIds.size, 1);
const days = new Set();
for (let d = 0; d < 30; d++)
days.add(rank.pickOfTheDay(list, NOW + d * DAY).id);
assert.ok(days.size >= 12, "30 days gave only " + days.size + " different picks");
const weeks = new Set();
for (let w = 0; w < 12; w++)
weeks.add(rank.pickOfTheWeek(list, NOW + w * 7 * DAY).id);
assert.ok(weeks.size >= 5, "12 weeks gave only " + weeks.size + " different picks");
});
test("picks need a review and a screenshot, and survive an empty list", () => {
assert.equal(rank.pickOfTheDay([], NOW), null);
assert.equal(rank.pickOfTheWeek([plugin("raw", { status: [] }), plugin("blind", { screenshot: "" })], NOW), null);
});
test("no taste, no dates, no votes: still a deterministic order", () => {
const list = [{ id: "b" }, { id: "a" }, { id: "c", upvotes: 1 }];
assert.deepEqual(ids(rank.recommend(list, { now: NOW })), ["c", "a", "b"]);
});
test("an upvote is posted to the reactions of the plugin's GitHub issue only", () => {
assert.equal(rank.issueReactionsPath("https://github.com/duxlabs-dev/dux-plugin-registry/issues/42"), "repos/duxlabs-dev/dux-plugin-registry/issues/42/reactions");
assert.equal(rank.issueReactionsPath("https://github.com/a/b.c/issues/7#issuecomment-1"), "repos/a/b.c/issues/7/reactions");
for (const url of ["", undefined, "https://example.com/a/b/issues/1", "https://github.com/a/b/pull/1", "https://github.com/a/b/issues/1/../../x", "https://github.com/a/b/issues/1 -f x=y", "http://github.com/a/b/issues/1", "https://github.com/../b/issues/1", "https://github.com/a/../issues/1", "https://github.com/./b/issues/1", "https://github.com/a/./issues/1"])
assert.equal(rank.issueReactionsPath(url), "");
assert.equal(rank.issueReactionsPath("https://github.com/a/.github/issues/3"), "repos/a/.github/issues/3/reactions");
});
test("the upvote count follows each click without double counting a vote the registry already has", () => {
const click = (state, removed, created) => rank.nextUpvoteDelta(state, removed, created);
// New vote, taken back, voted again.
let delta = click(0, false, true);
assert.equal(rank.upvoteCount(10, delta), 11);
delta = click(delta, true, false);
assert.equal(rank.upvoteCount(10, delta), 10);
delta = click(delta, false, true);
assert.equal(rank.upvoteCount(10, delta), 11);
// The account had already voted before this session: GitHub answers 200, the registry count already holds it.
delta = click(0, false, false);
assert.equal(rank.upvoteCount(10, delta), 10);
delta = click(delta, true, false);
assert.equal(rank.upvoteCount(10, delta), 9);
// A stale registry count never shows below zero.
assert.equal(rank.upvoteCount(0, click(0, true, false)), 0);
assert.equal(rank.upvoteCount(undefined, undefined), 0);
});
test("a card is marked new for two weeks, then updated for a week after a release", () => {
assert.equal(rank.recency(plugin("a", { created_at: ago(3), updated_at: ago(1) }), NOW), "new");
assert.equal(rank.recency(plugin("b", { created_at: ago(90), updated_at: ago(2) }), NOW), "updated");
assert.equal(rank.recency(plugin("c", { created_at: ago(90), updated_at: ago(30) }), NOW), "");
assert.equal(rank.recency(plugin("d", { created_at: undefined, updated_at: undefined }), NOW), "");
});
test("the reaction id is read from a gh reply, so the vote can be taken back", () => {
const body = JSON.stringify({ id: 987, node_id: "x", user: { login: "someone", id: 5 }, content: "+1" });
assert.equal(rank.reactionId("HTTP/2.0 201 Created\r\nEtag: \"a{b\"\r\n\r\n" + body), 987);
assert.equal(rank.reactionId("HTTP/2.0 201 Created\r\nServer: github.com\r\n\r\n" + body), 987);
assert.equal(rank.reactionId("HTTP/2.0 200 OK\n\n" + body), 987);
for (const reply of ["", undefined, "HTTP/2.0 401 Unauthorized\n\n{\"message\":\"Bad credentials\"}", "HTTP/2.0 200 OK\n\n{\"id\":3,\"content\":\"heart\"}", "{not json"])
assert.equal(rank.reactionId(reply), 0);
});
test("a plugin limited to other compositors names them, a portable one names none", () => {
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("a", { compositors: ["hyprland", "sway"] }), "niri")), ["hyprland", "sway"]);
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("b", { compositors: ["niri"] }), "niri")), []);
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("c", { compositors: ["any"] }), "niri")), []);
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("d", { compositors: ["hyprland"] }), "")), []);
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("e", { compositors: undefined }), "niri")), []);
});
test("quick filters keep only the plugins matching every set filter", () => {
const here = plugin("a", { compositors: ["niri"], firstParty: true, created_at: ago(2), updated_at: ago(1) });
const elsewhere = plugin("b", { compositors: ["hyprland"], firstParty: false, created_at: ago(200), updated_at: ago(100) });
assert.equal(rank.matchesFilters(elsewhere, {}, "niri", NOW), true);
assert.equal(rank.matchesFilters(elsewhere, null, "niri", NOW), true);
assert.equal(rank.matchesFilters(here, { usable: true, fresh: true, official: true }, "niri", NOW), true);
assert.equal(rank.matchesFilters(elsewhere, { usable: true }, "niri", NOW), false);
assert.equal(rank.matchesFilters(elsewhere, { fresh: true }, "niri", NOW), false);
assert.equal(rank.matchesFilters(elsewhere, { official: true }, "niri", NOW), false);
assert.equal(rank.matchesFilters(here, { usable: true }, "hyprland", NOW), false);
});
test("lists that are not real arrays (QML sequences) are still read", () => {
const seq = items => Object.assign(Object.create(null), items, { length: items.length });
assert.equal(rank.isUsable(plugin("a", { compositors: seq(["hyprland"]) }), "niri"), false);
assert.equal(rank.isUsable(plugin("b", { status: seq(["broken"]) }), "niri"), false);
assert.deepEqual(Array.from(rank.unsupportedOn(plugin("c", { compositors: seq(["hyprland"]) }), "niri")), ["hyprland"]);
});
const found = (list, query) => ids(list.filter(p => rank.searchRank(p, rank.searchPlan(list, query)) > 0));
test("search reads name, author, category, capabilities and dependencies", () => {
const list = [plugin("clock", { name: "World Clock", author: "Ada" }), plugin("shot", { name: "Quick Capture", capabilities: ["launcher"], dependencies: ["grim"] }), plugin("music", { name: "Tunes", category: "media" })];
assert.deepEqual(found(list, "clock"), ["clock"]);
assert.deepEqual(found(list, "ada"), ["clock"]);
assert.deepEqual(found(list, "launcher"), ["shot"]);
assert.deepEqual(found(list, "grim"), ["shot"]);
assert.deepEqual(found(list, "media"), ["music"]);
assert.deepEqual(found(list, "quick launcher"), ["shot"]);
assert.deepEqual(found(list, ""), ["clock", "shot", "music"]);
});
test("a typo is forgiven, accents and camelCase do not matter", () => {
const list = [plugin("wallpaperCarousel", { name: "Wallpaper Carousel" }), plugin("cafe", { name: "Café Timer" }), plugin("dockerManager", { name: "dockerManager" })];
assert.deepEqual(found(list, "walpaper"), ["wallpaperCarousel"]);
assert.deepEqual(found(list, "wallpapre"), ["wallpaperCarousel"]);
assert.deepEqual(found(list, "carousle"), ["wallpaperCarousel"]);
assert.deepEqual(found(list, "cafe"), ["cafe"]);
assert.deepEqual(found(list, "manager"), ["dockerManager"]);
assert.deepEqual(found(list, "dokcer"), ["dockerManager"]);
});
test("a camelCase name, id or author typed as one word still matches", () => {
const list = [plugin("orbitBluetooth", { author: "AvengeMedia" }), plugin("clock", { name: "World Clock", author: "Ada" })];
assert.deepEqual(found(list, "avengemedia"), ["orbitBluetooth"]);
assert.deepEqual(found(list, "orbitbluetooth"), ["orbitBluetooth"]);
assert.deepEqual(found(list, "orbitbluetoth"), ["orbitBluetooth"]);
assert.deepEqual(found(list, "avenge"), ["orbitBluetooth"]);
assert.deepEqual(found(list, "media"), ["orbitBluetooth"]);
assert.deepEqual(found(list, "bluetooth"), ["orbitBluetooth"]);
});
test("a name without camelCase is indexed once", () => {
const words = Array.from(rank._searchText(plugin("clock", { name: "World Clock" })).words);
assert.equal(words.filter(w => w === "world").length, 1);
});
test("a huge query is cut to its first eight whole words of at most 100 characters", () => {
const list = [plugin("orbitBluetooth", { name: "Orbit Bluetooth" }), plugin("weather", { name: "Weather" }), plugin("clock", { name: "World Clock" })];
const tokens = query => Array.from(rank.searchPlan(list, query), t => t.token);
assert.equal(tokens("aA".repeat(5000)).length, 8);
assert.deepEqual(tokens("orbit " + "blutooth ".repeat(3000)), ["orbit", "blutooth", "blutooth", "blutooth", "blutooth", "blutooth", "blutooth", "blutooth"]);
assert.deepEqual(found(list, "orbit " + "blutooth ".repeat(3000)), ["orbitBluetooth"]);
assert.deepEqual(tokens("x".repeat(200)), ["x".repeat(100)]);
assert.deepEqual(tokens("clock" + " ".repeat(94) + "weather"), ["clock"]);
assert.deepEqual(ids(rank.closest(list, "weather ".repeat(8) + "qqqqqqqq qqqqqqqq", 4)), ["weather"]);
});
test("a correctly typed word never picks up look-alikes", () => {
const list = [plugin("dark", { name: "Dark Mode" }), plugin("dank", { name: "Dank Clock" }), plugin("lock", { name: "Lock" })];
assert.deepEqual(found(list, "dark"), ["dark"]);
assert.deepEqual(found(list, "lock"), ["dank", "lock"]);
assert.deepEqual(found(list, "zzzzzz"), []);
assert.deepEqual(found(list, "do"), []);
});
test("closest proposes the nearest names when nothing matches", () => {
const list = [plugin("weather", { name: "Weather", upvotes: 2 }), plugin("feather", { name: "Feather", upvotes: 9 }), plugin("notes", { name: "Notes" })];
assert.deepEqual(ids(rank.closest(list, "wether", 3)), ["weather", "feather"]);
assert.deepEqual(ids(rank.closest(list, "qqqqqqqq", 3)), []);
assert.deepEqual(ids(rank.closest(list, "", 3)), []);
assert.equal(rank.closest(list, "eather", 1).length, 1);
});
test("byAuthor lists one author's plugins whatever the case", () => {
const list = [plugin("a", { author: "Jane Example" }), plugin("b", { author: "jane example " }), plugin("c", { author: "Other" }), plugin("d", { author: "AvengeMedia" }), plugin("e", { author: "avengemedia" }), plugin("f", { author: "Avenge Media" }), plugin("g", { author: "示例 作者" }), plugin("h", { author: "Avenge-Media" })];
assert.deepEqual(ids(rank.byAuthor(list, "示例作者")), ["g"]);
assert.deepEqual(ids(rank.byAuthor(list, "Jane Example")), ["a", "b"]);
assert.deepEqual(ids(rank.byAuthor(list, "AvengeMedia")), ["d", "e", "f", "h"]);
assert.deepEqual(ids(rank.byAuthor(list, "avengemedia")), ["d", "e", "f", "h"]);
assert.deepEqual(ids(rank.byAuthor(list, "Avenge Media")), ["d", "e", "f", "h"]);
assert.deepEqual(ids(rank.byAuthor(list, "Avenge-Media")), ["d", "e", "f", "h"]);
assert.deepEqual(ids(rank.byAuthor(list, "")), []);
});
test("permissions that reach outside the plugin come first", () => {
assert.deepEqual(Array.from(rank.sortPermissions(["settings_read", "settings_write", "network", "process"])), ["process", "network", "settings_read", "settings_write"]);
assert.equal(rank.permissionIsSensitive("shell_execute"), true);
assert.equal(rank.permissionIsSensitive("settings_read"), false);
});
test("dependencies are checked by command, libraries stay unknown", () => {
const deps = ["curl", "jq", "wl-clipboard", "qt6-websockets", "imagemagick"];
const states = rank.dependencyStates(deps, ["curl", "wl-copy", "convert"], ["curl", "jq", "wl-copy", "magick", "convert"]);
assert.deepEqual(Array.from(states, s => s.name + ":" + s.state), ["curl:ok", "jq:missing", "wl-clipboard:ok", "qt6-websockets:unknown", "imagemagick:ok"]);
// Before the lookup answers, or when it fails, nothing is reported missing.
assert.deepEqual(Array.from(rank.dependencyStates(deps, [], []), s => s.state), ["unknown", "unknown", "unknown", "unknown", "unknown"]);
assert.deepEqual(Array.from(rank.dependencyStates(["imagemagick"], [], ["magick"]), s => s.state), ["unknown"]);
assert.deepEqual(Array.from(rank.dependencyCommands("lib; rm -rf")), []);
});
test("the report link is prefilled and only built for GitHub repositories", () => {
const url = rank.issueReportUrl({ id: "clock", name: "World Clock", version: "1.2.0", repo: "https://github.com/ada/clock" }, { dms: "1.4.0", compositor: "niri" });
assert.ok(url.startsWith("https://github.com/ada/clock/issues/new?title=%5BWorld%20Clock%5D%20&body="));
const body = decodeURIComponent(url.split("&body=")[1]);
assert.ok(body.includes("World Clock (`clock`) v1.2.0") && body.includes("**DMS:** 1.4.0") && body.includes("**Compositor:** niri"));
assert.equal(rank.issueReportUrl({ id: "x", repo: "https://example.org/x" }, {}), "");
assert.equal(rank.shareUrl({ issueUrl: "https://github.com/a/b/issues/1", repo: "https://github.com/a/b" }), "https://github.com/a/b/issues/1");
assert.equal(rank.shareUrl({ repo: "javascript:alert(1)" }), "");
});