Add shell bar runtime regression coverage
This commit is contained in:
@@ -351,7 +351,7 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onClicked: function(mouse) {
|
onDoubleClicked: function(mouse) {
|
||||||
if (mouse.button === Qt.RightButton) root.openThemeSwitcher()
|
if (mouse.button === Qt.RightButton) root.openThemeSwitcher()
|
||||||
else root.openSelector()
|
else root.openSelector()
|
||||||
mouse.accepted = true
|
mouse.accepted = true
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ Item {
|
|||||||
property int tooltipRequest: 0
|
property int tooltipRequest: 0
|
||||||
property var activePopout: null
|
property var activePopout: null
|
||||||
property var clickTargets: []
|
property var clickTargets: []
|
||||||
|
property var debugModuleSlots: []
|
||||||
|
|
||||||
function registerClickTarget(target) {
|
function registerClickTarget(target) {
|
||||||
if (!target || clickTargets.indexOf(target) !== -1) return
|
if (!target || clickTargets.indexOf(target) !== -1) return
|
||||||
var next = clickTargets.slice()
|
var next = clickTargets.slice()
|
||||||
@@ -73,6 +75,44 @@ Item {
|
|||||||
clickTargets = next
|
clickTargets = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerDebugModuleSlot(slot) {
|
||||||
|
if (!slot || debugModuleSlots.indexOf(slot) !== -1) return
|
||||||
|
var next = debugModuleSlots.slice()
|
||||||
|
next.push(slot)
|
||||||
|
debugModuleSlots = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterDebugModuleSlot(slot) {
|
||||||
|
var next = debugModuleSlots.filter(function(item) { return item !== slot })
|
||||||
|
debugModuleSlots = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugBarGeometry() {
|
||||||
|
var out = []
|
||||||
|
for (var i = 0; i < debugModuleSlots.length; i++) {
|
||||||
|
var slot = debugModuleSlots[i]
|
||||||
|
if (!slot || !slot.activeItem) continue
|
||||||
|
var point = { x: slot.x, y: slot.y }
|
||||||
|
try {
|
||||||
|
point = slot.mapToItem(null, 0, 0)
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
id: slot.moduleName,
|
||||||
|
section: slot.region,
|
||||||
|
x: Math.round(point.x),
|
||||||
|
y: Math.round(point.y),
|
||||||
|
width: Math.round(slot.width),
|
||||||
|
height: Math.round(slot.height),
|
||||||
|
visible: slot.visible === true && slot.width > 0 && slot.height > 0,
|
||||||
|
itemVisible: slot.activeItem.visible === true,
|
||||||
|
itemWidth: Math.round(slot.activeItem.implicitWidth || 0),
|
||||||
|
itemHeight: Math.round(slot.activeItem.implicitHeight || 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
function targetWindow(target) {
|
function targetWindow(target) {
|
||||||
return target && target.QsWindow ? target.QsWindow.window : null
|
return target && target.QsWindow ? target.QsWindow.window : null
|
||||||
}
|
}
|
||||||
@@ -452,10 +492,12 @@ Item {
|
|||||||
|
|
||||||
component LeftModules: ModuleList {
|
component LeftModules: ModuleList {
|
||||||
entries: root.layoutEntries("left")
|
entries: root.layoutEntries("left")
|
||||||
|
region: "left"
|
||||||
}
|
}
|
||||||
|
|
||||||
component RightModules: ModuleList {
|
component RightModules: ModuleList {
|
||||||
entries: root.layoutEntries("right")
|
entries: root.layoutEntries("right")
|
||||||
|
region: "right"
|
||||||
}
|
}
|
||||||
|
|
||||||
component CenterModules: Item {
|
component CenterModules: Item {
|
||||||
@@ -481,12 +523,14 @@ Item {
|
|||||||
ModuleList {
|
ModuleList {
|
||||||
visible: !centerRoot.hasAnchor
|
visible: !centerRoot.hasAnchor
|
||||||
entries: centerRoot.entries
|
entries: centerRoot.entries
|
||||||
|
region: "center"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleList {
|
ModuleList {
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
||||||
|
region: "center"
|
||||||
anchors.right: centerAnchorModule.left
|
anchors.right: centerAnchorModule.left
|
||||||
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
||||||
}
|
}
|
||||||
@@ -495,12 +539,14 @@ Item {
|
|||||||
id: centerAnchorModule
|
id: centerAnchorModule
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entry: centerRoot.anchorEntry
|
entry: centerRoot.anchorEntry
|
||||||
|
region: "center"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleList {
|
ModuleList {
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
||||||
|
region: "center"
|
||||||
anchors.left: centerAnchorModule.right
|
anchors.left: centerAnchorModule.right
|
||||||
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
||||||
}
|
}
|
||||||
@@ -518,12 +564,14 @@ Item {
|
|||||||
ModuleList {
|
ModuleList {
|
||||||
visible: !centerRoot.hasAnchor
|
visible: !centerRoot.hasAnchor
|
||||||
entries: centerRoot.entries
|
entries: centerRoot.entries
|
||||||
|
region: "center"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleList {
|
ModuleList {
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
||||||
|
region: "center"
|
||||||
anchors.bottom: centerAnchorModule.top
|
anchors.bottom: centerAnchorModule.top
|
||||||
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
||||||
}
|
}
|
||||||
@@ -532,12 +580,14 @@ Item {
|
|||||||
id: centerAnchorModule
|
id: centerAnchorModule
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entry: centerRoot.anchorEntry
|
entry: centerRoot.anchorEntry
|
||||||
|
region: "center"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleList {
|
ModuleList {
|
||||||
visible: centerRoot.hasAnchor
|
visible: centerRoot.hasAnchor
|
||||||
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
||||||
|
region: "center"
|
||||||
anchors.top: centerAnchorModule.bottom
|
anchors.top: centerAnchorModule.bottom
|
||||||
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
||||||
}
|
}
|
||||||
@@ -567,6 +617,7 @@ Item {
|
|||||||
id: moduleListRoot
|
id: moduleListRoot
|
||||||
|
|
||||||
property var entries: []
|
property var entries: []
|
||||||
|
property string region: ""
|
||||||
|
|
||||||
visible: entries.length > 0
|
visible: entries.length > 0
|
||||||
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
|
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
|
||||||
@@ -585,6 +636,7 @@ Item {
|
|||||||
ModuleSlot {
|
ModuleSlot {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
entry: modelData
|
entry: modelData
|
||||||
|
region: moduleListRoot.region
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -602,6 +654,7 @@ Item {
|
|||||||
ModuleSlot {
|
ModuleSlot {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
entry: modelData
|
entry: modelData
|
||||||
|
region: moduleListRoot.region
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,6 +665,7 @@ Item {
|
|||||||
id: slot
|
id: slot
|
||||||
|
|
||||||
required property var entry
|
required property var entry
|
||||||
|
property string region: ""
|
||||||
readonly property string moduleName: root.entryId(entry)
|
readonly property string moduleName: root.entryId(entry)
|
||||||
readonly property var moduleSettings: root.entrySettings(entry)
|
readonly property var moduleSettings: root.entrySettings(entry)
|
||||||
readonly property string customType: root.customModuleType(entry)
|
readonly property string customType: root.customModuleType(entry)
|
||||||
@@ -638,6 +692,9 @@ Item {
|
|||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
|
|
||||||
|
Component.onCompleted: root.registerDebugModuleSlot(slot)
|
||||||
|
Component.onDestruction: root.unregisterDebugModuleSlot(slot)
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: componentLoader
|
id: componentLoader
|
||||||
active: !slot.qmlCustom && !slot.registered
|
active: !slot.qmlCustom && !slot.registered
|
||||||
|
|||||||
@@ -647,6 +647,10 @@ ShellRoot {
|
|||||||
return JSON.stringify(shell.shellConfig || {})
|
return JSON.stringify(shell.shellConfig || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debugBarGeometry(): string {
|
||||||
|
return JSON.stringify(shell.bar && shell.bar.debugBarGeometry ? shell.bar.debugBarGeometry() : [])
|
||||||
|
}
|
||||||
|
|
||||||
function summon(id: string, payloadJson: string): string {
|
function summon(id: string, payloadJson: string): string {
|
||||||
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+149
@@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
TMPDIR=""
|
||||||
|
|
||||||
|
export PATH="$ROOT/bin:$PATH"
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf 'ok - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'not ok - %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
jq empty "$ROOT/config/omarchy/shell.json"
|
||||||
|
pass "default shell.json is valid JSON"
|
||||||
|
|
||||||
|
jq -e '.version == 1 and (.bar.layout.left | type == "array") and (.bar.layout.center | type == "array") and (.bar.layout.right | type == "array")' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||||
|
pass "default shell.json has versioned bar layout"
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
def ids: map(.id // .);
|
||||||
|
.bar.layout.center | ids == [
|
||||||
|
"omarchy.clock",
|
||||||
|
"omarchy.weather",
|
||||||
|
"omarchy.system-update",
|
||||||
|
"omarchy.indicators"
|
||||||
|
]
|
||||||
|
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||||
|
pass "default center layout keeps update next to weather"
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
(.bar.centerAnchor // "") as $anchor |
|
||||||
|
any(.bar.layout.center[]; (.id // .) == $anchor)
|
||||||
|
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||||
|
pass "default center anchor exists in center layout"
|
||||||
|
|
||||||
|
ROOT="$ROOT" python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
root = Path(os.environ["ROOT"])
|
||||||
|
config = json.loads((root / "config/omarchy/shell.json").read_text())
|
||||||
|
manifests = {}
|
||||||
|
for manifest_path in (root / "shell/plugins").glob("**/*.manifest.json"):
|
||||||
|
data = json.loads(manifest_path.read_text())
|
||||||
|
manifests[data.get("id", "")] = (manifest_path, data)
|
||||||
|
for manifest_path in (root / "shell/plugins").glob("**/manifest.json"):
|
||||||
|
data = json.loads(manifest_path.read_text())
|
||||||
|
manifests[data.get("id", "")] = (manifest_path, data)
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for section in ("left", "center", "right"):
|
||||||
|
entries.extend(config["bar"]["layout"][section])
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
bad = []
|
||||||
|
for entry in entries:
|
||||||
|
widget_id = entry["id"] if isinstance(entry, dict) else str(entry)
|
||||||
|
if not widget_id.startswith("omarchy."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = manifests.get(widget_id)
|
||||||
|
if row is None:
|
||||||
|
missing.append(widget_id)
|
||||||
|
continue
|
||||||
|
manifest_path, manifest = row
|
||||||
|
|
||||||
|
if "bar-widget" not in manifest.get("kinds", []):
|
||||||
|
bad.append(f"{widget_id}: missing bar-widget kind")
|
||||||
|
entry_point = manifest.get("entryPoints", {}).get("barWidget")
|
||||||
|
if not entry_point:
|
||||||
|
bad.append(f"{widget_id}: missing barWidget entry point")
|
||||||
|
elif not (manifest_path.parent / entry_point).exists():
|
||||||
|
bad.append(f"{widget_id}: missing {entry_point}")
|
||||||
|
|
||||||
|
if missing or bad:
|
||||||
|
for item in missing:
|
||||||
|
print(f"missing manifest for {item}", file=sys.stderr)
|
||||||
|
for item in bad:
|
||||||
|
print(item, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
pass "default bar widget ids resolve to manifests and entry points"
|
||||||
|
|
||||||
|
migration=$(grep -rl 'Place the system update indicator next to weather in the bar' "$ROOT/migrations" | head -n 1)
|
||||||
|
[[ -n $migration ]] || fail "update placement migration exists"
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
mkdir -p "$TMPDIR/home/.config/omarchy"
|
||||||
|
cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"bar": {
|
||||||
|
"position": "top",
|
||||||
|
"transparent": false,
|
||||||
|
"centerAnchor": "omarchy.clock",
|
||||||
|
"layout": {
|
||||||
|
"left": [{ "id": "omarchy.menu" }],
|
||||||
|
"center": [
|
||||||
|
{ "id": "omarchy.clock", "format": "HH:mm" },
|
||||||
|
{ "id": "omarchy.weather", "refreshMinutes": 30 },
|
||||||
|
{ "id": "omarchy.indicators", "items": ["Dnd", "NightLight"] },
|
||||||
|
{ "id": "omarchy.system-update", "custom": true }
|
||||||
|
],
|
||||||
|
"right": [{ "id": "omarchy.audio" }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [{ "id": "custom.plugin", "enabled": true }]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
HOME="$TMPDIR/home" bash "$migration"
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
def ids: map(.id // .);
|
||||||
|
.bar.layout.center | ids == [
|
||||||
|
"omarchy.clock",
|
||||||
|
"omarchy.weather",
|
||||||
|
"omarchy.system-update",
|
||||||
|
"omarchy.indicators"
|
||||||
|
]
|
||||||
|
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||||
|
pass "update placement migration moves update after weather"
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
.bar.layout.center[1].refreshMinutes == 30 and
|
||||||
|
.bar.layout.center[2].custom == true and
|
||||||
|
.bar.layout.center[3].items == ["Dnd", "NightLight"] and
|
||||||
|
.plugins == [{ "id": "custom.plugin", "enabled": true }]
|
||||||
|
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||||
|
pass "update placement migration preserves unrelated settings"
|
||||||
|
|
||||||
|
before=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
|
||||||
|
HOME="$TMPDIR/home" bash "$migration"
|
||||||
|
after=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
|
||||||
|
[[ $before == "$after" ]] || fail "update placement migration is idempotent"
|
||||||
|
pass "update placement migration is idempotent"
|
||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
TMPDIR=""
|
||||||
|
QS_PID=""
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf 'ok - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'not ok - %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
kill "$QS_PID" 2>/dev/null || true
|
||||||
|
wait "$QS_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if ! command -v quickshell >/dev/null 2>&1; then
|
||||||
|
pass "quickshell not installed; skipping QML contract test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
result="$TMPDIR/result.json"
|
||||||
|
log="$TMPDIR/quickshell.log"
|
||||||
|
config_dir="$TMPDIR/indicator-contract"
|
||||||
|
mkdir -p "$config_dir"
|
||||||
|
cp "$ROOT/test/qml/indicator-contract/shell.qml" "$config_dir/shell.qml"
|
||||||
|
ln -s "$ROOT/shell/Ui" "$config_dir/Ui"
|
||||||
|
ln -s "$ROOT/shell/Commons" "$config_dir/Commons"
|
||||||
|
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
OMARCHY_QML_TEST_RESULT="$result" \
|
||||||
|
HOME="$TMPDIR/home" \
|
||||||
|
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
|
||||||
|
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
|
||||||
|
PATH="$ROOT/bin:$PATH" \
|
||||||
|
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
|
||||||
|
QS_PID=$!
|
||||||
|
|
||||||
|
for _ in {1..50}; do
|
||||||
|
[[ -s $result ]] && break
|
||||||
|
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
sed -n '1,160p' "$log" >&2
|
||||||
|
fail "QML contract quickshell exited before writing result"
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -s $result ]] || {
|
||||||
|
sed -n '1,160p' "$log" >&2
|
||||||
|
fail "QML contract test timed out"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! jq -e '.ok == true' "$result" >/dev/null; then
|
||||||
|
printf 'QML contract result:\n' >&2
|
||||||
|
jq . "$result" >&2
|
||||||
|
printf 'QML contract log:\n' >&2
|
||||||
|
sed -n '1,160p' "$log" >&2
|
||||||
|
fail "QML indicator contract checks pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass "QML indicator contract checks pass"
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
TMPDIR=""
|
||||||
|
QS_PID=""
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf 'ok - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'not ok - %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
kill "$QS_PID" 2>/dev/null || true
|
||||||
|
wait "$QS_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
|
||||||
|
pass "no Wayland compositor; skipping shell runtime smoke test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v quickshell >/dev/null 2>&1; then
|
||||||
|
pass "quickshell not installed; skipping shell runtime smoke test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
test_root="$TMPDIR/omarchy"
|
||||||
|
test_home="$TMPDIR/home"
|
||||||
|
stub_bin="$TMPDIR/bin"
|
||||||
|
log="$TMPDIR/quickshell.log"
|
||||||
|
mkdir -p "$test_root" "$test_home" "$stub_bin"
|
||||||
|
cp -a "$ROOT/shell" "$test_root/shell"
|
||||||
|
ln -s "$ROOT/config" "$test_root/config"
|
||||||
|
ln -s "$ROOT/bin" "$test_root/bin"
|
||||||
|
|
||||||
|
cat >"$stub_bin/omarchy-update-available" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "Omarchy update available (test)"
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$stub_bin/omarchy-update-available"
|
||||||
|
|
||||||
|
OMARCHY_PATH="$test_root" \
|
||||||
|
HOME="$test_home" \
|
||||||
|
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||||
|
quickshell -p "$test_root/shell" --no-color >"$log" 2>&1 &
|
||||||
|
QS_PID=$!
|
||||||
|
|
||||||
|
for _ in {1..80}; do
|
||||||
|
if OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" -q shell ping >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
sed -n '1,200p' "$log" >&2
|
||||||
|
fail "test shell exited before IPC became available"
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" -q omarchy.system-update refresh >/dev/null 2>&1 || true
|
||||||
|
sleep 0.8
|
||||||
|
|
||||||
|
geometry=$(OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" shell debugBarGeometry)
|
||||||
|
if [[ -z $geometry ]]; then
|
||||||
|
sed -n '1,200p' "$log" >&2
|
||||||
|
fail "debug bar geometry returned output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
map(select(.section == "center" and .visible == true)) as $center |
|
||||||
|
($center | map(select(.id == "omarchy.weather" and .visible == true)) | length) >= 1 and
|
||||||
|
($center | map(select(.id == "omarchy.system-update" and .visible == true and .width > 0)) | length) >= 1 and
|
||||||
|
($center | map(select(.id == "omarchy.indicators" and .visible == true)) | length) >= 1 and
|
||||||
|
(
|
||||||
|
($center | map(select(.id == "omarchy.weather")) | first | .x) <
|
||||||
|
($center | map(select(.id == "omarchy.system-update")) | first | .x)
|
||||||
|
) and
|
||||||
|
(
|
||||||
|
($center | map(select(.id == "omarchy.system-update")) | first | .x) <
|
||||||
|
($center | map(select(.id == "omarchy.indicators")) | first | .x)
|
||||||
|
)
|
||||||
|
' <<<"$geometry" >/dev/null || {
|
||||||
|
printf 'Geometry:\n%s\n' "$geometry" | jq . >&2
|
||||||
|
sed -n '1,200p' "$log" >&2
|
||||||
|
fail "runtime geometry places visible update between weather and indicators"
|
||||||
|
}
|
||||||
|
|
||||||
|
pass "runtime geometry places visible update between weather and indicators"
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
||||||
|
property var failures: []
|
||||||
|
property var commands: []
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
failures.push(String(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTrue(condition, message) {
|
||||||
|
if (!condition) fail(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: notificationService
|
||||||
|
property bool doNotDisturb: false
|
||||||
|
function setDoNotDisturb(value) {
|
||||||
|
doNotDisturb = !!value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: mockShell
|
||||||
|
function firstPartyServiceFor(id) {
|
||||||
|
return id === "omarchy.notifications" ? notificationService : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: mockBar
|
||||||
|
property bool vertical: false
|
||||||
|
property int barSize: 26
|
||||||
|
property string fontFamily: "monospace"
|
||||||
|
property var shell: mockShell
|
||||||
|
function run(command) {
|
||||||
|
root.commands.push(String(command))
|
||||||
|
}
|
||||||
|
function showTooltip(target, text) {}
|
||||||
|
function hideTooltip(target) {}
|
||||||
|
function registerClickTarget(target) {}
|
||||||
|
function unregisterClickTarget(target) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: indicatorHost
|
||||||
|
property bool revealInactiveIndicators: true
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIndicator(name) {
|
||||||
|
var component = Qt.createComponent("file://" + rootPath + "/shell/plugins/bar/indicators/" + name + ".qml")
|
||||||
|
if (component.status !== Component.Ready) {
|
||||||
|
fail(name + " failed to load: " + component.errorString())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = component.createObject(root, {
|
||||||
|
indicatorHost: indicatorHost,
|
||||||
|
indicatorBlock: "inactive",
|
||||||
|
activeOverride: null
|
||||||
|
})
|
||||||
|
if (!item) {
|
||||||
|
fail(name + " failed to instantiate: " + component.errorString())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectBar(item) {
|
||||||
|
assertTrue(item.bar === null || item.bar === undefined, item.moduleName + " starts without bar")
|
||||||
|
item.bar = mockBar
|
||||||
|
assertTrue(item.bar === mockBar, item.moduleName + " accepts delayed bar injection")
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandCount(command) {
|
||||||
|
var count = 0
|
||||||
|
for (var i = 0; i < commands.length; i++) {
|
||||||
|
if (commands[i] === command) count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeResult() {
|
||||||
|
var payload = JSON.stringify({
|
||||||
|
ok: failures.length === 0,
|
||||||
|
failures: failures,
|
||||||
|
commands: commands,
|
||||||
|
dnd: notificationService.doNotDisturb
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resultPath) {
|
||||||
|
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value) {
|
||||||
|
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string rootPath: Quickshell.env("OMARCHY_PATH")
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1
|
||||||
|
running: true
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
var dnd = root.createIndicator("Dnd")
|
||||||
|
if (dnd) {
|
||||||
|
dnd.moduleName = "Dnd"
|
||||||
|
root.injectBar(dnd)
|
||||||
|
notificationService.doNotDisturb = false
|
||||||
|
dnd.triggerPress(Qt.LeftButton)
|
||||||
|
root.assertTrue(notificationService.doNotDisturb === true, "DND left click toggles notification service")
|
||||||
|
}
|
||||||
|
|
||||||
|
var nightLight = root.createIndicator("NightLight")
|
||||||
|
if (nightLight) {
|
||||||
|
nightLight.moduleName = "NightLight"
|
||||||
|
root.injectBar(nightLight)
|
||||||
|
nightLight.triggerPress(Qt.LeftButton)
|
||||||
|
root.assertTrue(root.commandCount("omarchy-toggle-nightlight") === 1, "Night Light left click runs toggle command")
|
||||||
|
}
|
||||||
|
|
||||||
|
var screenRecording = root.createIndicator("ScreenRecording")
|
||||||
|
if (screenRecording) {
|
||||||
|
screenRecording.moduleName = "ScreenRecording"
|
||||||
|
root.injectBar(screenRecording)
|
||||||
|
screenRecording.triggerPress(Qt.LeftButton)
|
||||||
|
root.assertTrue(root.commandCount("omarchy-capture-screenrecording") === 1, "Screen Recording left click runs capture command")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictation = root.createIndicator("Dictation")
|
||||||
|
if (dictation) {
|
||||||
|
dictation.moduleName = "Dictation"
|
||||||
|
root.injectBar(dictation)
|
||||||
|
dictation.triggerPress(Qt.LeftButton)
|
||||||
|
dictation.triggerPress(Qt.RightButton)
|
||||||
|
root.assertTrue(root.commandCount("omarchy-voxtype-model") === 1, "Dictation left click runs model command")
|
||||||
|
root.assertTrue(root.commandCount("omarchy-voxtype-config") === 1, "Dictation right click runs config command")
|
||||||
|
}
|
||||||
|
|
||||||
|
var stayAwake = root.createIndicator("StayAwake")
|
||||||
|
if (stayAwake) {
|
||||||
|
stayAwake.moduleName = "StayAwake"
|
||||||
|
root.injectBar(stayAwake)
|
||||||
|
stayAwake.triggerPress(Qt.LeftButton)
|
||||||
|
root.assertTrue(root.commandCount("omarchy-toggle-idle") === 1, "Stay Awake left click runs idle toggle command")
|
||||||
|
}
|
||||||
|
|
||||||
|
root.writeResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user