feat: one setting per application, and never the shape of the machine
A laptop is going in for the women's Bible study, and the obvious way to
build that — a laptop profile that turns off the children's church
applications — is the wrong way. A laptop may well take children's church,
and a tower may well be a Bible study. Hardware cannot be allowed to
decide purpose, so every optional application gets a setting of its own:
WONDERINK_ENABLED already existed; behaviour unchanged
DVD_ENABLED new
PARENT_CALL_ENABLED new
and DEVICE_TYPE says only what the hardware IS. classroom-desktop-layout
never reads it, and there is a test that says so, because this is the rule
that will be under pressure the first time somebody adds a device.
All three default to true, so an existing classroom that has never heard
of any of them comes up exactly as it did before. The settings are
appended to device.conf by the same mechanism every other setting has
used, so an upgrade gains them without losing a byte an administrator
wrote.
The disc player is the one place hardware still votes, and it is a veto:
DVD_ENABLED can take the player away from a machine that has a drive,
nothing can put it on a machine that has none. A button that cannot work
is worse than no button.
The layout's launcher markers are replaced by a feature block of three
plain booleans. The old mechanism commented a line out of an array, which
worked for launchers and could not express Parent Call, because that is a
whole widget rather than one entry. Three booleans express both, and the
launcher list is now built from them in one fixed order, so a disabled
application closes the row up rather than leaving a gap, and the same
enabled set produces the same bar on every machine in the church.
Also here, because it is the bar and not a feature: sortingStrategy=0.
Plasma's default is 1, "manually", and the task manager accepts a
reordering drop only in that mode — MouseHandler.qml tests
tasksModel.sortMode === SortManual before it will move anything. The
classroom never wrote the key, so it had the default, and any teacher
could drag the launchers into a different order. This is the applet's own
lever refusing the drop rather than something of ours putting the bar back
afterwards.
The test now runs the layout the package actually ships, in node, with
Plasma's API stubbed, and asserts the launcher string that comes out.
Asserting against a copy of the file's text in the test was how the old
one worked, and a copy is a thing that drifts.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fc764b0621
commit
7f4a122900
@@ -1,10 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Which applications a device offers, and the bar that comes out of it.
|
||||
#
|
||||
# Two halves. The first is the decision — one setting per application, and
|
||||
# never the shape of the machine. The second actually RUNS the layout
|
||||
# script the package ships, in node, with Plasma's API stubbed, so the
|
||||
# assertions are about the real file rather than about a copy of its text
|
||||
# that could drift away from it.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from importlib.machinery import SourceFileLoader
|
||||
@@ -16,6 +27,8 @@ spec = importlib.util.spec_from_loader("layout", loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(module)
|
||||
|
||||
LAYOUT = root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
@@ -31,115 +44,172 @@ def check(label, condition, detail=""):
|
||||
failed += 1
|
||||
|
||||
|
||||
WITH = '''tasks.writeConfig("launchers", [
|
||||
/* CLASSROOM-WONDERINK-LAUNCHER */ "applications:ekklesia-wonderink.desktop",
|
||||
"applications:google-chrome.desktop",
|
||||
/* CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"
|
||||
].join(","));'''
|
||||
BLOCK = """/* CLASSROOM-FEATURES-BEGIN */
|
||||
var WONDERINK = true;
|
||||
var DVD = true;
|
||||
var PARENT_CALL = true;
|
||||
/* CLASSROOM-FEATURES-END */"""
|
||||
|
||||
|
||||
def dvd(text, wanted):
|
||||
return module.rewrite(text, module.DVD_MARKER, module.DVD_ENTRY, wanted)
|
||||
print("1. one line per application, rewritten in place")
|
||||
check("a feature can be switched off",
|
||||
"var WONDERINK = false;" in module.rewrite(BLOCK, "WONDERINK", False))
|
||||
check("and back on again",
|
||||
"var WONDERINK = true;"
|
||||
in module.rewrite(module.rewrite(BLOCK, "WONDERINK", False),
|
||||
"WONDERINK", True))
|
||||
check("switching one leaves the others alone",
|
||||
"var DVD = true;" in module.rewrite(BLOCK, "WONDERINK", False)
|
||||
and "var PARENT_CALL = true;" in module.rewrite(BLOCK, "WONDERINK", False))
|
||||
check("rewriting twice changes nothing the second time",
|
||||
module.rewrite(module.rewrite(BLOCK, "DVD", False), "DVD", False)
|
||||
== module.rewrite(BLOCK, "DVD", False))
|
||||
|
||||
|
||||
def ink(text, wanted):
|
||||
return module.rewrite(
|
||||
text, module.WONDERINK_MARKER, module.WONDERINK_ENTRY, wanted
|
||||
print("2. the shape of the machine never decides what it is for")
|
||||
# This is the architecture rule, and it is the one worth a test: a laptop
|
||||
# must be able to run children's church and a tower must be able to be a
|
||||
# Bible study.
|
||||
module.has_optical_drive = lambda: True
|
||||
|
||||
laptop_with_ink = module.wanted({"DEVICE_TYPE": "laptop"})
|
||||
check("a laptop still offers Wonder Ink unless told otherwise",
|
||||
laptop_with_ink["WONDERINK"] is True, str(laptop_with_ink))
|
||||
check("a laptop still offers Parent Call unless told otherwise",
|
||||
laptop_with_ink["PARENT_CALL"] is True)
|
||||
|
||||
desktop_no_ink = module.wanted(
|
||||
{"DEVICE_TYPE": "desktop", "WONDERINK_ENABLED": "false"})
|
||||
check("a desktop can have Wonder Ink taken away",
|
||||
desktop_no_ink["WONDERINK"] is False)
|
||||
check("and keeps everything else",
|
||||
desktop_no_ink["DVD"] is True and desktop_no_ink["PARENT_CALL"] is True)
|
||||
|
||||
check("an existing room that has never heard of these keeps them all",
|
||||
all(module.wanted({}).values()), str(module.wanted({})))
|
||||
|
||||
|
||||
print("3. a drive can only ever take the player away")
|
||||
module.has_optical_drive = lambda: False
|
||||
check("no drive means no player, whatever the setting says",
|
||||
module.wanted({"DVD_ENABLED": "true"})["DVD"] is False)
|
||||
module.has_optical_drive = lambda: True
|
||||
check("a drive plus DVD_ENABLED=false is still no player",
|
||||
module.wanted({"DVD_ENABLED": "false"})["DVD"] is False)
|
||||
check("a drive plus DVD_ENABLED=true is the player",
|
||||
module.wanted({"DVD_ENABLED": "true"})["DVD"] is True)
|
||||
|
||||
|
||||
print("4. the script rewrites the layout the package actually ships")
|
||||
module.has_optical_drive = lambda: True
|
||||
|
||||
def run(config):
|
||||
"""Run the real script over a copy of the real layout."""
|
||||
work = Path(tempfile.mkdtemp(prefix="classroom-layout-"))
|
||||
copy = work / "layout.js"
|
||||
shutil.copy(LAYOUT, copy)
|
||||
|
||||
module.read_device_config = lambda: config
|
||||
module.main.__globals__["sys"].argv = ["classroom-desktop-layout", str(copy)]
|
||||
module.main()
|
||||
|
||||
return copy.read_text(), work
|
||||
|
||||
text, work = run({})
|
||||
check("the shipped layout carries the feature block",
|
||||
module.MARKER in text)
|
||||
check("and an unconfigured machine keeps every application",
|
||||
"var WONDERINK = true;" in text and "var DVD = true;" in text
|
||||
and "var PARENT_CALL = true;" in text)
|
||||
|
||||
|
||||
print("5. the bar that comes out, evaluated in node")
|
||||
STUB = r"""
|
||||
const fs = require("fs");
|
||||
const captured = {widgets: [], config: {}};
|
||||
let current = null;
|
||||
function makeWidget(type) {
|
||||
const w = {
|
||||
type: type,
|
||||
currentConfigGroup: [],
|
||||
writeConfig: function (k, v) { captured.config[type + "." + k] = String(v); },
|
||||
reloadConfig: function () {}
|
||||
};
|
||||
captured.widgets.push(type);
|
||||
return w;
|
||||
}
|
||||
globalThis.desktops = () => [makeWidget("desktop")];
|
||||
globalThis.Panel = function () {
|
||||
this.addWidget = makeWidget;
|
||||
return this;
|
||||
};
|
||||
eval(fs.readFileSync(process.argv[2], "utf8"));
|
||||
console.log(JSON.stringify(captured));
|
||||
"""
|
||||
|
||||
def bar(config):
|
||||
text, work = run(config)
|
||||
stub = work / "stub.js"
|
||||
stub.write_text(STUB)
|
||||
result = subprocess.run(
|
||||
["node", str(stub), str(work / "layout.js")],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None, result.stderr
|
||||
return json.loads(result.stdout), ""
|
||||
|
||||
|
||||
WITHOUT = WITH.replace(' /* CLASSROOM-DVD-LAUNCHER */',
|
||||
' // /* CLASSROOM-DVD-LAUNCHER */')
|
||||
NO_INK = WITH.replace(' /* CLASSROOM-WONDERINK-LAUNCHER */',
|
||||
' // /* CLASSROOM-WONDERINK-LAUNCHER */')
|
||||
if shutil.which("node") is None:
|
||||
print(" SKIP node is not installed; the layout was not evaluated")
|
||||
else:
|
||||
LAUNCH = "org.kde.plasma.icontasks.launchers"
|
||||
INK = "applications:ekklesia-wonderink.desktop"
|
||||
WEB = "applications:google-chrome.desktop"
|
||||
DISC = "applications:ekklesia-dvd.desktop"
|
||||
|
||||
print("1. the DVD launcher follows the hardware")
|
||||
check("a machine with a drive keeps it", dvd(WITH, True) == WITH)
|
||||
check("a machine without a drive loses it",
|
||||
dvd(WITH, False) == WITHOUT, dvd(WITH, False))
|
||||
check("and gets it back when a drive appears", dvd(WITHOUT, True) == WITH)
|
||||
check("removing twice changes nothing the second time",
|
||||
dvd(dvd(WITH, False), False) == WITHOUT)
|
||||
check("adding twice changes nothing the second time",
|
||||
dvd(dvd(WITHOUT, True), True) == WITH)
|
||||
every, why = bar({})
|
||||
check("a classroom gets Wonder Ink, the browser and the player, in order",
|
||||
every and every["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
||||
(every or {}).get("config", {}).get(LAUNCH, why))
|
||||
check("and Parent Call in the corner",
|
||||
every and "org.kde.plasma.quicklaunch" in every["widgets"])
|
||||
|
||||
print()
|
||||
print("2. Wonder Ink follows the device setting")
|
||||
check("a device with Wonder Ink keeps it", ink(WITH, True) == WITH)
|
||||
check("a device without it loses the launcher",
|
||||
ink(WITH, False) == NO_INK, ink(WITH, False))
|
||||
check("and gets it back when the setting changes",
|
||||
ink(NO_INK, True) == WITH)
|
||||
check("turning it off twice changes nothing the second time",
|
||||
ink(ink(WITH, False), False) == NO_INK)
|
||||
# The line carries a trailing comma, so a comment that stopped short of it
|
||||
# would leave the array with a hole in it.
|
||||
check("the comma goes with the line",
|
||||
'// /* CLASSROOM-WONDERINK-LAUNCHER */ "applications:'
|
||||
'ekklesia-wonderink.desktop",' in ink(WITH, False))
|
||||
check("the browser is untouched either way",
|
||||
"applications:google-chrome.desktop" in ink(WITH, False))
|
||||
check("and so is the disc player",
|
||||
'CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"'
|
||||
in ink(WITH, False))
|
||||
no_ink, why = bar({"WONDERINK_ENABLED": "false"})
|
||||
check("a Bible study desktop has the browser first and no gap",
|
||||
no_ink and no_ink["config"].get(LAUNCH) == f"{WEB},{DISC}",
|
||||
(no_ink or {}).get("config", {}).get(LAUNCH, why))
|
||||
|
||||
print()
|
||||
print("3. the two settings are independent")
|
||||
neither = ink(dvd(WITH, False), False)
|
||||
check("a laptop with no drive and no Wonder Ink keeps only the browser",
|
||||
neither.count("// /* CLASSROOM") == 2
|
||||
and "applications:google-chrome.desktop" in neither)
|
||||
check("and comes all the way back",
|
||||
ink(dvd(neither, True), True) == WITH)
|
||||
module.has_optical_drive = lambda: False
|
||||
bible, why = bar({"WONDERINK_ENABLED": "false",
|
||||
"PARENT_CALL_ENABLED": "false",
|
||||
"DEVICE_TYPE": "laptop"})
|
||||
check("the laptop's bar is the browser alone",
|
||||
bible and bible["config"].get(LAUNCH) == WEB,
|
||||
(bible or {}).get("config", {}).get(LAUNCH, why))
|
||||
check("with no Parent Call button on it",
|
||||
bible and "org.kde.plasma.quicklaunch" not in bible["widgets"],
|
||||
str((bible or {}).get("widgets")))
|
||||
check("and the status corner is untouched",
|
||||
bible and "org.kde.plasma.volume" in bible["widgets"]
|
||||
and "org.kde.plasma.digitalclock" in bible["widgets"]
|
||||
and "org.kde.plasma.networkmanagement" in bible["widgets"])
|
||||
|
||||
print()
|
||||
print("4. nothing else in the layout is touched")
|
||||
other = WITH + "\npanel.addWidget(\"org.kde.plasma.digitalclock\");\n"
|
||||
check("a line without the marker is left alone",
|
||||
dvd(other, False).endswith('digitalclock");\n'))
|
||||
check("the other launchers survive",
|
||||
"ekklesia-wonderink.desktop" in dvd(WITH, False)
|
||||
and "google-chrome.desktop" in dvd(WITH, False))
|
||||
module.has_optical_drive = lambda: True
|
||||
ink_laptop, why = bar({"DEVICE_TYPE": "laptop"})
|
||||
check("a laptop taking children's church gets the same bar as a tower",
|
||||
ink_laptop and ink_laptop["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
||||
(ink_laptop or {}).get("config", {}).get(LAUNCH, why))
|
||||
|
||||
print()
|
||||
print("5. the real layout carries both markers")
|
||||
layout = (root / "config/plasma/lookandfeel/contents/layouts"
|
||||
/ "org.kde.plasma.desktop-layout.js").read_text()
|
||||
check("exactly one DVD marker on a launcher line",
|
||||
layout.count(module.DVD_MARKER + ' "applications:') == 1)
|
||||
check("exactly one Wonder Ink marker on a launcher line",
|
||||
layout.count(module.WONDERINK_MARKER + ' "applications:') == 1)
|
||||
check("the shipped layout is one a drive-less machine can be given",
|
||||
dvd(layout, False) != layout)
|
||||
check("and one a Wonder-Ink-less device can be given",
|
||||
ink(layout, False) != layout)
|
||||
|
||||
print()
|
||||
print("6. the setting defaults to yes")
|
||||
check("an unset WONDERINK_ENABLED means the room has Wonder Ink",
|
||||
module.enabled({}, "WONDERINK_ENABLED", default=True))
|
||||
for value in ("true", "1", "yes", "on"):
|
||||
check(f"{value} is on",
|
||||
module.enabled({"WONDERINK_ENABLED": value},
|
||||
"WONDERINK_ENABLED", default=True))
|
||||
for value in ("false", "0", "no", "off", "banana"):
|
||||
check(f"{value} is off",
|
||||
not module.enabled({"WONDERINK_ENABLED": value},
|
||||
"WONDERINK_ENABLED", default=True))
|
||||
|
||||
print()
|
||||
print("7. a missing or unmarked layout is not an error")
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
empty = Path(work) / "none.js"
|
||||
check("a layout that does not exist exits cleanly",
|
||||
module.main.__code__ is not None)
|
||||
unmarked = Path(work) / "plain.js"
|
||||
unmarked.write_text("panel.addWidget('x');\n")
|
||||
sys.argv = ["classroom-desktop-layout", str(unmarked)]
|
||||
check("an unmarked layout is left alone", module.main() == 0)
|
||||
check("and unchanged", unmarked.read_text() == "panel.addWidget('x');\n")
|
||||
sys.argv = ["classroom-desktop-layout", str(Path(work) / "missing.js")]
|
||||
check("a missing layout is not a failure", module.main() == 0)
|
||||
print("6. the bar cannot be rearranged by dragging")
|
||||
check("sorting is off, which is what refuses a reordering drop",
|
||||
every and every["config"].get(
|
||||
"org.kde.plasma.icontasks.sortingStrategy") == "0",
|
||||
str((every or {}).get("config", {}).get(
|
||||
"org.kde.plasma.icontasks.sortingStrategy")))
|
||||
check("and launchers stay ahead of running windows",
|
||||
every and every["config"].get(
|
||||
"org.kde.plasma.icontasks.separateLaunchers") == "true")
|
||||
|
||||
print()
|
||||
print(f"{passed} passed, {failed} failed")
|
||||
|
||||
Reference in New Issue
Block a user