init
This commit is contained in:
14
dev/click.sh
Executable file
14
dev/click.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Simuliert einen Klick in der nested Shell: dev/click.sh x y
|
||||
# Ermittelt den Actor an der Position wie bei einem echten Klick (Picking) und löst den
|
||||
# nächstliegenden St.Button aus. Gibt den getroffenen Actor aus.
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
"$DIR/eval.sh" "const Clutter = imports.gi.Clutter, St = imports.gi.St;
|
||||
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, $1, $2);
|
||||
const hit = actor ? actor.constructor.name + ' [' + (actor.style_class ?? '') + ']' : 'nichts';
|
||||
while (actor && !(actor instanceof St.Button))
|
||||
actor = actor.get_parent();
|
||||
if (actor)
|
||||
actor.emit('clicked', 1);
|
||||
hit + (actor ? ' -> clicked ' + actor.constructor.name + ' [' + actor.style_class + ']' : ' -> kein Button');
|
||||
"
|
||||
15
dev/enable.py
Executable file
15
dev/enable.py
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# Trägt eine Extension-UUID in die aktiven GNOME-Extensions ein, falls sie fehlt: dev/enable.py <uuid>
|
||||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
uuid = sys.argv[1]
|
||||
raw = subprocess.check_output(['gsettings', 'get', 'org.gnome.shell', 'enabled-extensions']).decode().strip()
|
||||
enabled = ast.literal_eval(raw.replace('@as ', ''))
|
||||
if uuid in enabled:
|
||||
print(f'{uuid} ist schon aktiviert.')
|
||||
else:
|
||||
enabled.append(uuid)
|
||||
subprocess.check_call(['gsettings', 'set', 'org.gnome.shell', 'enabled-extensions', str(enabled)])
|
||||
print(f'{uuid} aktiviert.')
|
||||
11
dev/eval.sh
Executable file
11
dev/eval.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# JavaScript in der nested Shell ausführen: dev/eval.sh 'code' oder dev/eval.sh datei.js
|
||||
SCRATCH=${SCRATCH:-/tmp/desktop-app-nested}
|
||||
export DBUS_SESSION_BUS_ADDRESS=$(cat "$SCRATCH/bus")
|
||||
if [ -f "$1" ]; then
|
||||
CODE="eval(new TextDecoder().decode(GLib.file_get_contents('$(realpath "$1")')[1]))"
|
||||
else
|
||||
CODE="$1"
|
||||
fi
|
||||
gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval "$CODE" \
|
||||
| sed -e 's/^(true, /OK /' -e 's/^(false, /FEHLER /'
|
||||
18
dev/fake-ding.py
Normal file
18
dev/fake-ding.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Fenster, das sich wie Desktop Icons (DING) ausgibt: Titel "Desktop Icons 1", bildschirmfüllend.
|
||||
# Start: WAYLAND_DISPLAY=wayland-1 DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/desktop-app-nested/bus) python3 dev/fake-ding.py
|
||||
import gi
|
||||
gi.require_version('Gtk', '4.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
app = Gtk.Application(application_id='com.rastersoft.dingtest')
|
||||
|
||||
def activate(app):
|
||||
win = Gtk.ApplicationWindow(application=app, title='Desktop Icons 1', default_width=1920, default_height=1080)
|
||||
label = Gtk.Label(label='FAKE DING')
|
||||
label.set_valign(Gtk.Align.END)
|
||||
label.set_halign(Gtk.Align.CENTER)
|
||||
win.set_child(label)
|
||||
win.present()
|
||||
|
||||
app.connect('activate', activate)
|
||||
app.run()
|
||||
35
dev/input.sh
Executable file
35
dev/input.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Echte Eingaben in der headless Test-Shell (virtuelle Geräte, nur dort gefahrlos):
|
||||
# dev/input.sh click X Y Linksklick
|
||||
# dev/input.sh rclick X Y Rechtsklick
|
||||
# dev/input.sh drag X1 Y1 X2 Y2 Linke Taste drücken, ziehen, loslassen
|
||||
# dev/input.sh move X Y Zeiger bewegen
|
||||
# dev/input.sh press X Y Linke Taste drücken (halten)
|
||||
# dev/input.sh release Linke Taste loslassen
|
||||
# dev/input.sh type "Text" Text tippen (ASCII)
|
||||
# dev/input.sh key Escape Taste (Clutter.KEY_*-Name ohne Präfix), z.B. Return, Escape, Tab
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PRE='const Clutter = imports.gi.Clutter;
|
||||
const seat = Clutter.get_default_backend().get_default_seat();
|
||||
globalThis._vp ??= seat.create_virtual_device(Clutter.InputDeviceType.POINTER_DEVICE);
|
||||
globalThis._vk ??= seat.create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);
|
||||
const t = () => GLib.get_monotonic_time();
|
||||
const move = (x, y) => globalThis._vp.notify_absolute_motion(t(), x, y);
|
||||
const button = (b, s) => globalThis._vp.notify_button(t(), b, s ? Clutter.ButtonState.PRESSED : Clutter.ButtonState.RELEASED);
|
||||
const key = (k, s) => globalThis._vk.notify_keyval(t(), k, s ? Clutter.KeyState.PRESSED : Clutter.KeyState.RELEASED);
|
||||
'
|
||||
case "$1" in
|
||||
click) CODE="move($2, $3); button(Clutter.BUTTON_PRIMARY, 1); button(Clutter.BUTTON_PRIMARY, 0);" ;;
|
||||
rclick) CODE="move($2, $3); button(Clutter.BUTTON_SECONDARY, 1); button(Clutter.BUTTON_SECONDARY, 0);" ;;
|
||||
drag) "$0" press "$2" "$3"; sleep 0.15
|
||||
for i in 1 2 3 4; do "$0" move $(( $2 + ($4 - $2) * i / 4 )) $(( $3 + ($5 - $3) * i / 4 )); sleep 0.1; done
|
||||
"$0" release; exit ;;
|
||||
move) CODE="move($2, $3);" ;;
|
||||
press) CODE="move($2, $3); button(Clutter.BUTTON_PRIMARY, 1);" ;;
|
||||
release) CODE="button(Clutter.BUTTON_PRIMARY, 0);" ;;
|
||||
type) TEXT=$(printf '%s' "$2" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
|
||||
CODE="for (const ch of $TEXT) { const k = ch.charCodeAt(0) === 10 ? Clutter.KEY_Return : Clutter.unicode_to_keysym(ch.codePointAt(0)); key(k, 1); key(k, 0); }" ;;
|
||||
key) CODE="key(Clutter.KEY_$2, 1); key(Clutter.KEY_$2, 0);" ;;
|
||||
*) echo "unbekannt: $1"; exit 1 ;;
|
||||
esac
|
||||
"$DIR/eval.sh" "$PRE $CODE 'ok'" | grep -v "^OK '\"ok\"')$"
|
||||
25
dev/nested.sh
Executable file
25
dev/nested.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Startet eine Test-GNOME-Shell mit der Extension.
|
||||
# dev/nested.sh Fenster auf dem Desktop (zum Selbst-Ausprobieren)
|
||||
# dev/nested.sh headless unsichtbar; echte Maus/Tastatur lassen sich gefahrlos simulieren
|
||||
# Eigene Settings (keyfile) und eigenes Datenverzeichnis, die echte Session bleibt unberührt.
|
||||
set -e
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
SCRATCH=${SCRATCH:-/tmp/desktop-app-nested}
|
||||
mkdir -p "$SCRATCH/data/gnome-shell/extensions" "$SCRATCH/config/glib-2.0/settings"
|
||||
ln -sfn "$ROOT/desktop@marha.de" "$SCRATCH/data/gnome-shell/extensions/desktop@marha.de"
|
||||
ln -sfn "$ROOT/dev/unsafe@dev" "$SCRATCH/data/gnome-shell/extensions/unsafe@dev"
|
||||
cat > "$SCRATCH/config/glib-2.0/settings/keyfile" <<KEYS
|
||||
[org/gnome/shell]
|
||||
enabled-extensions=['desktop@marha.de', 'unsafe@dev']
|
||||
disable-user-extensions=false
|
||||
KEYS
|
||||
export XDG_DATA_HOME="$SCRATCH/data" XDG_CONFIG_HOME="$SCRATCH/config" GSETTINGS_BACKEND=keyfile
|
||||
export MUTTER_DEBUG_DUMMY_MODE_SPECS=${MODE:-1920x1080}
|
||||
if [ "$1" = headless ]; then
|
||||
export GNOME_SHELL_SESSION_MODE=user
|
||||
ARGS="--headless --virtual-monitor 1920x1080"
|
||||
else
|
||||
ARGS="--nested --wayland"
|
||||
fi
|
||||
exec dbus-run-session -- bash -c "echo \$DBUS_SESSION_BUS_ADDRESS > '$SCRATCH/bus'; echo \$\$ > '$SCRATCH/pid'; exec gnome-shell $ARGS"
|
||||
20
dev/press.sh
Executable file
20
dev/press.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Drückt den ersten sichtbaren St.Button mit diesem Text (in der Extension oder in Dialogen): dev/press.sh "Text"
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
"$DIR/eval.sh" "
|
||||
const St = imports.gi.St;
|
||||
const wanted = $(printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))');
|
||||
const text = a => a.label ?? a.child?.text ?? a.get_child?.()?.text ?? '';
|
||||
function find(actor) {
|
||||
if (!actor.visible) return null;
|
||||
if (actor instanceof St.Button && text(actor) === wanted) return actor;
|
||||
for (const c of actor.get_children()) { const r = find(c); if (r) return r; }
|
||||
return null;
|
||||
}
|
||||
const root = Main.extensionManager.lookup('desktop@marha.de').stateObj._root;
|
||||
const b = find(Main.layoutManager.modalDialogGroup) ?? find(root);
|
||||
if (!b) throw new Error('kein Button: ' + wanted);
|
||||
const [x, y] = b.get_transformed_position();
|
||||
b.emit('clicked', 1);
|
||||
'pressed ' + wanted + ' @ ' + Math.round(x + b.width / 2) + ',' + Math.round(y + b.height / 2);
|
||||
"
|
||||
16
dev/restart.sh
Executable file
16
dev/restart.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Test-Shell neu starten und Fehler der Extension zeigen: dev/restart.sh [headless]
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
"$DIR/stop.sh"
|
||||
pgrep -f "^gnome-shell --(nested|headless)" | xargs -r kill 2>/dev/null
|
||||
rm -rf /tmp/desktop-app-nested/data/desktop-app
|
||||
sleep 2
|
||||
(nohup "$DIR/nested.sh" "$1" > /tmp/desktop-app-nested.log 2>&1 &)
|
||||
sleep 9
|
||||
grep -n "desktop@marha\|JS ERROR" /tmp/desktop-app-nested.log | head -30
|
||||
# Headless: Overview weg, virtuelle Eingabegeräte anlegen und vorwärmen (erste Events gehen sonst verloren)
|
||||
if [ "$1" = headless ]; then
|
||||
"$DIR/eval.sh" 'Main.overview.hide(); "overview hidden"' >/dev/null
|
||||
"$DIR/input.sh" move 960 540; sleep 0.5; "$DIR/input.sh" move 961 541; sleep 0.5
|
||||
fi
|
||||
"$DIR/eval.sh" 'JSON.stringify([...Main.extensionManager._extensions].map(([k,v]) => [k, v.state, v.error]))'
|
||||
43
dev/seed.js
Normal file
43
dev/seed.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const ext = Main.extensionManager.lookup("desktop@marha.de").stateObj;
|
||||
const s = ext._store;
|
||||
s.saveEvent({title: "Zahnarzt", date: "2026-09-17", start: "09:30", end: "10:30"});
|
||||
s.saveEvent({title: "Team-Meeting", date: "2026-09-17", start: "14:00", end: "15:00"});
|
||||
s.saveEvent({title: "Geburtstag Anna", date: "2026-09-19", start: null, end: null});
|
||||
s.saveEvent({title: "Sehr langer Termintitel der abgeschnitten wird", date: "2026-09-17", start: "16:00", end: null});
|
||||
s.saveEvent({title: "Messe", date: "2026-09-15", start: "14:00", end: "11:00", endDate: "2026-09-16"});
|
||||
s.saveTodo({title: "Einkaufen", date: null, time: null});
|
||||
s.saveTodo({title: "Steuer abgeben", date: "2026-09-15", time: null});
|
||||
s.saveTodo({title: "Anruf Bank", date: "2026-09-17", time: "11:00"});
|
||||
s.saveTodo({title: "Schon erledigt", date: null, time: null});
|
||||
s.toggleTodo(s.todos[3].id);
|
||||
GLib.mkdir_with_parents(s.notesDir + "/Projekte/Desktop-App", 0o755);
|
||||
GLib.mkdir_with_parents(s.notesDir + "/Privat", 0o755);
|
||||
GLib.file_set_contents(s.notesDir + "/Projekte/Desktop-App/Plan.md", `# Plan
|
||||
|
||||
Eine **fette** und *kursive* Zeile mit \`code\`.
|
||||
|
||||
## Schritte
|
||||
|
||||
- [x] Skelett
|
||||
- [ ] Kalender
|
||||
- Monat
|
||||
- Woche
|
||||
- [ ] Sync per [FastAPI](http://localhost:8000)
|
||||
|
||||
> Zitat: Alle hassen Chaos
|
||||
|
||||
\`\`\`
|
||||
curl -X POST /api/events
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
1. eins
|
||||
2. zwei
|
||||
|
||||
Siehe [[Ideen]]`);
|
||||
GLib.file_set_contents(s.notesDir + "/Projekte/Ideen.md", "# Ideen\n\nNoch leer.");
|
||||
GLib.file_set_contents(s.notesDir + "/Privat/Einkauf.md", "- Milch\n- Brot");
|
||||
GLib.file_set_contents(s.notesDir + "/Allgemein.md", "Lose Notiz ohne Ordner.");
|
||||
ext._root.get_child_at_index(0)._refresh();
|
||||
`seeded ${s.events.length} events, ${s.todos.length} todos`;
|
||||
7
dev/shot.sh
Executable file
7
dev/shot.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Screenshot der nested Shell: dev/shot.sh [datei.png]
|
||||
SCRATCH=${SCRATCH:-/tmp/desktop-app-nested}
|
||||
OUT=${1:-$SCRATCH/shot.png}
|
||||
export DBUS_SESSION_BUS_ADDRESS=$(cat "$SCRATCH/bus")
|
||||
gdbus call --session --dest org.gnome.Shell.Screenshot --object-path /org/gnome/Shell/Screenshot \
|
||||
--method org.gnome.Shell.Screenshot.Screenshot false false "$OUT" >/dev/null && echo "$OUT"
|
||||
5
dev/stop.sh
Executable file
5
dev/stop.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Beendet die Test-Shell.
|
||||
SCRATCH=${SCRATCH:-/tmp/desktop-app-nested}
|
||||
[ -f "$SCRATCH/pid" ] && kill "$(cat "$SCRATCH/pid")" 2>/dev/null
|
||||
rm -f "$SCRATCH/pid" "$SCRATCH/bus"
|
||||
262
dev/tests/input.sh
Executable file
262
dev/tests/input.sh
Executable file
@@ -0,0 +1,262 @@
|
||||
#!/bin/bash
|
||||
# Ablauf-Tests mit echten Maus- und Tastatureingaben in der headless Shell: dev/tests/input.sh
|
||||
# Startet die Shell neu, spielt Testdaten ein und klickt/tippt sich durch alle Funktionen.
|
||||
DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
E="$DIR/eval.sh"
|
||||
IN="$DIR/input.sh"
|
||||
OUT=/tmp/desktop-app-nested/out.txt
|
||||
SHOTS=/tmp/desktop-app-nested/shots
|
||||
mkdir -p "$SHOTS"
|
||||
FAILED=0
|
||||
|
||||
PRE='const Clutter = imports.gi.Clutter; const St = imports.gi.St;
|
||||
const ext = Main.extensionManager.lookup("desktop@marha.de").stateObj;
|
||||
const notes = ext._root.get_child_at_index(0);
|
||||
const cal = ext._root.get_child_at_index(1).get_child_at_index(0);
|
||||
const todos = ext._root.get_child_at_index(1).get_child_at_index(1);
|
||||
const store = ext._store;
|
||||
const grabbed = () => global.stage.get_grab_actor();
|
||||
const center = a => { const [x, y] = a.get_transformed_position(); const [w, h] = a.get_transformed_size(); return [Math.round(x + w / 2), Math.round(y + h / 2)]; };
|
||||
const find = (root, text) => { let found = null; const walk = a => { if (found || !a.visible) return; if (a instanceof St.Button && (a.label ?? a.child?.text ?? a.label_actor?.text) === text) found = a; a.get_children().forEach(walk); }; if (root) walk(root); return found; };
|
||||
const entries = a => { const out = []; const walk = n => { if (n instanceof St.Entry) out.push(n.get_text()); n.get_children().forEach(walk); }; if (a) walk(a); return out; };
|
||||
const labels = a => { const out = []; const walk = n => { if (n instanceof St.Label && n.text) out.push(n.text); n.get_children().forEach(walk); }; if (a) walk(a); return out; };
|
||||
const read = p => new TextDecoder().decode(GLib.file_get_contents(p)[1]);
|
||||
const bottom = a => { const [x, y] = a.get_transformed_position(); const [w, h] = a.get_transformed_size(); return [Math.round(x + w / 2), Math.round(y + h - 24)]; };
|
||||
const today = GLib.DateTime.new_now_local().format("%Y-%m-%d");
|
||||
'
|
||||
|
||||
# JS ausführen, Ergebnis als Text zurückgeben
|
||||
val() {
|
||||
"$E" "$PRE GLib.file_set_contents('$OUT', String($1)); 'w'" | grep -v "^OK '\"w\"')$"
|
||||
cat "$OUT"
|
||||
}
|
||||
# Mittelpunkt eines Actors: xy 'ausdruck' → "x y"
|
||||
xy() {
|
||||
val "center($1).join(' ')"
|
||||
}
|
||||
click() { $IN click $(xy "$1"); sleep 0.3; }
|
||||
rclick() { $IN rclick $(xy "$1"); sleep 0.3; }
|
||||
rclickb() { $IN rclick $(val "bottom($1).join(' ')"); sleep 0.3; }
|
||||
type_() { $IN type "$1"; sleep 0.2; }
|
||||
key() { $IN key "$1"; sleep 0.3; }
|
||||
shot() { "$DIR/shot.sh" "$SHOTS/$1.png" >/dev/null; }
|
||||
check() {
|
||||
local name=$1 actual=$2 expected=$3
|
||||
if [ "$actual" == "$expected" ]; then
|
||||
echo "ok $name"
|
||||
else
|
||||
echo "FAIL $name"; echo " ist: $actual"; echo " soll: $expected"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Start"
|
||||
"$DIR/restart.sh" headless >/dev/null
|
||||
"$E" "$DIR/seed.js" >/dev/null
|
||||
check "Extension aktiv" "$(val 'Main.extensionManager.lookup("desktop@marha.de").state')" "1"
|
||||
check "Notizen halbe Bildschirmbreite" "$(val 'notes.width + " " + Main.layoutManager.primaryMonitor.width / 2')" "960 960"
|
||||
check "Rechte Spalte füllt den Rest, Abstand wie außen" "$(val '(r => [r.x - (notes.x + notes.width), ext._root.width - (r.x + r.width)].join(" "))(ext._root.get_child_at_index(1))')" "24 0"
|
||||
|
||||
echo "=== Woche: Termin per Drag"
|
||||
click 'cal._viewButtons.week'
|
||||
check "Wochenansicht" "$(val 'cal._view')" "week"
|
||||
$IN drag $(xy 'cal._slots[4][10]') $(xy 'cal._slots[4][12]'); sleep 0.3
|
||||
check "Popover offen" "$(val 'grabbed()?.style_class')" "desktop-popover form"
|
||||
check "Zeitraum im Popover" "$(val '["Fr, 18.09.2026 ▾", "10:00 ▾", "13:00 ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja ja"
|
||||
check "Auswahl markiert" "$(val 'cal._slots[4].map((s, h) => s.has_style_class_name("selecting") ? h : null).filter(h => h !== null).join(",")')" "10,11,12"
|
||||
type_ "Sport"
|
||||
shot "01-drag-popover"
|
||||
key Return
|
||||
check "Termin gespeichert" "$(val 'JSON.stringify((({title, date, start, end, endDate}) => ({title, date, start, end, endDate}))(store.events.find(e => e.title === "Sport")))')" \
|
||||
'{"title":"Sport","date":"2026-09-18","start":"10:00","end":"13:00","endDate":null}'
|
||||
check "Popover zu, Markierung weg" "$(val '[grabbed() ? "grab" : "frei", cal._slots[4].filter(s => s.has_style_class_name("selecting")).length].join(" ")')" "frei 0"
|
||||
check "Ein Block über drei Stunden" "$(val '(b => b.length + " " + Math.round(b[0].block.height / cal._rowPitch()))(cal._blocks.filter(b => b.event.title === "Sport"))')" "1 3"
|
||||
shot "02-week"
|
||||
|
||||
echo "=== Woche: Termin bearbeiten, Escape, Löschen per Rechtsklick"
|
||||
click 'cal._blocks.find(b => b.event.title === "Sport").block'
|
||||
check "Bearbeiten-Popover zeigt Titel" "$(val 'entries(grabbed())[0]')" "Sport"
|
||||
key Escape
|
||||
check "Escape schließt" "$(val 'grabbed() ? "grab" : "frei"')" "frei"
|
||||
rclick 'cal._blocks.find(b => b.event.title === "Sport").block'
|
||||
check "Kontextmenü offen" "$(val 'labels(grabbed()).join("|")')" "Löschen"
|
||||
shot "03-context-menu"
|
||||
click 'find(grabbed(), "Löschen")'
|
||||
check "Termin gelöscht" "$(val 'store.events.filter(e => e.title === "Sport").length')" "0"
|
||||
|
||||
echo "=== Woche: ganztägiger Termin, Klick daneben schließt"
|
||||
click 'cal._alldaySlots[6]'
|
||||
check "Ganztags-Popover" "$(val '["So, 20.09.2026 ▾", "Keine Uhrzeit ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja"
|
||||
type_ "Frei"; key Return
|
||||
check "Ganztags gespeichert" "$(val 'JSON.stringify(store.events.filter(e => e.title === "Frei").map(e => [e.date, e.start]))')" '[["2026-09-20",null]]'
|
||||
$IN drag $(xy 'cal._slots[1][9]') $(xy 'cal._slots[1][9]'); sleep 0.3
|
||||
check "Klick ohne Ziehen = eine Stunde" "$(val '["Di, 15.09.2026 ▾", "09:00 ▾", "10:00 ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja ja"
|
||||
click 'todos.get_child_at_index(0).get_child_at_index(0)'
|
||||
check "Klick daneben schließt" "$(val '[grabbed() ? "grab" : "frei", Main.modalCount].join(" ")')" "frei 0"
|
||||
|
||||
echo "=== Todos: Popover mit Datum- und Zeitauswahl"
|
||||
rclickb 'todos'
|
||||
click 'find(grabbed(), "Erstellen")'
|
||||
check "Todo-Popover" "$(val 'labels(grabbed())[0]')" "Neues Todo"
|
||||
type_ "Milch kaufen"
|
||||
click 'find(grabbed(), "Kein Datum ▾")'
|
||||
check "Datumsauswahl offen" "$(val 'grabbed()?.style_class')" "desktop-popover menu datepicker"
|
||||
shot "04-datepicker"
|
||||
click 'find(grabbed(), "20")'
|
||||
check "Datum übernommen" "$(val 'find(grabbed(), "So, 20.09.2026 ▾") ? "ja" : "nein"')" "ja"
|
||||
click 'find(grabbed(), "Keine Uhrzeit ▾")'
|
||||
check "Zeitauswahl offen" "$(val 'grabbed()?.style_class')" "desktop-popover menu timepicker"
|
||||
click 'find(grabbed(), "09:30")'
|
||||
check "Uhrzeit übernommen" "$(val 'find(grabbed(), "09:30 ▾") ? "ja" : "nein"')" "ja"
|
||||
shot "05-todo-popover"
|
||||
check "Fokus nach Auswahl wieder im Textfeld" "$(val 'global.stage.get_key_focus() === entries(grabbed()) && "x" || (grabbed().contains(global.stage.get_key_focus()) && global.stage.get_key_focus().constructor.name)')" "Clutter_Text"
|
||||
key Return
|
||||
check "Todo gespeichert" "$(val 'JSON.stringify((({title, date, time, done}) => ({title, date, time, done}))(store.todos.find(t => t.title === "Milch kaufen")))')" \
|
||||
'{"title":"Milch kaufen","date":"2026-09-20","time":"09:30","done":false}'
|
||||
click 'find(todos, "Milch kaufen")'
|
||||
check "Todo bearbeiten zeigt Text" "$(val 'entries(grabbed())[0]')" "Milch kaufen"
|
||||
key Escape
|
||||
rclick 'find(todos, "Milch kaufen")'
|
||||
click 'find(grabbed(), "Löschen")'
|
||||
check "Todo gelöscht" "$(val 'store.todos.filter(t => t.title === "Milch kaufen").length')" "0"
|
||||
rclickb 'todos'
|
||||
check "Rechtsklick Todo-Fläche" "$(val 'labels(grabbed()).join("|")')" "Erstellen"
|
||||
click 'find(grabbed(), "Erstellen")'
|
||||
key Return
|
||||
check "Leeres Todo abgelehnt" "$(val 'labels(grabbed()).includes("Text fehlt") ? "Fehler gezeigt" : "kein Fehler"')" "Fehler gezeigt"
|
||||
key Escape
|
||||
|
||||
echo "=== Notizen: anlegen, direkt bearbeiten, umbenennen, löschen"
|
||||
rclickb 'notes.get_child_at_index(0)'
|
||||
check "Rechtsklick im Baum" "$(val 'labels(grabbed()).join("|")')" "Notiz erstellen|Ordner erstellen"
|
||||
click 'find(grabbed(), "Notiz erstellen")'
|
||||
type_ "Testnotiz"; key Return
|
||||
check "Notiz-Datei angelegt" "$(val 'GLib.file_test(store.notesDir + "/Testnotiz.md", GLib.FileTest.EXISTS)')" "true"
|
||||
check "Editor offen mit Fokus" "$(val '[notes._editing, notes._editScroll.visible, global.stage.get_key_focus() === notes._editor.clutter_text].join(" ")')" "true true true"
|
||||
type_ "# Hallo"; key Return; key Return; type_ "Erste **Notiz**"
|
||||
shot "06-inline-editor"
|
||||
sleep 4
|
||||
check "Autosave nach ein paar Sekunden" "$(val '[notes._editing, JSON.stringify(read(store.notesDir + "/Testnotiz.md"))].join(" ")')" 'true "# Hallo\n\nErste **Notiz**"'
|
||||
click 'cal._viewButtons.month'
|
||||
check "Fokusverlust beendet den Editor, Anzeige gerendert" "$(val '[notes._editing, notes._viewScroll.visible, notes._content.text].join("|")')" "false|true|Hallo
|
||||
|
||||
Erste Notiz"
|
||||
click 'notes._viewScroll'
|
||||
check "Klick in die Fläche startet den Editor" "$(val '[notes._editing, global.stage.get_key_focus() === notes._editor.clutter_text].join(" ")')" "true true"
|
||||
type_ " mehr"
|
||||
click 'find(notes, "Allgemein")'
|
||||
check "Wechsel speichert sofort" "$(val 'read(store.notesDir + "/Testnotiz.md").endsWith("**Notiz** mehr") + " " + notes._selected.name')" "true Allgemein"
|
||||
rclick 'find(notes, "Testnotiz")'
|
||||
check "Kontextmenü Notiz" "$(val 'labels(grabbed()).join("|")')" "Umbenennen|Löschen"
|
||||
click 'find(grabbed(), "Umbenennen")'
|
||||
type_ "Umbenannt"; key Return
|
||||
check "Notiz umbenannt" "$(val '[GLib.file_test(store.notesDir + "/Testnotiz.md", GLib.FileTest.EXISTS), GLib.file_test(store.notesDir + "/Umbenannt.md", GLib.FileTest.EXISTS)].join(" ")')" "false true"
|
||||
rclick 'find(notes, "▸ Projekte")'
|
||||
check "Voller Ordner nicht löschbar" "$(val '(b => b ? String(b.reactive) : "fehlt")(find(grabbed(), "Löschen (Ordner ist nicht leer)"))')" "false"
|
||||
key Escape
|
||||
rclickb 'notes.get_child_at_index(0)'
|
||||
click 'find(grabbed(), "Ordner erstellen")'
|
||||
type_ "Leer"; key Return
|
||||
check "Ordner angelegt und gewählt" "$(val '[GLib.file_test(store.notesDir + "/Leer", GLib.FileTest.IS_DIR), notes._selected?.name].join(" ")')" "true Leer"
|
||||
rclick 'find(notes, "▾ Leer")'
|
||||
click 'find(grabbed(), "Löschen")'
|
||||
check "Leerer Ordner gelöscht" "$(val 'GLib.file_test(store.notesDir + "/Leer", GLib.FileTest.EXISTS)')" "false"
|
||||
rclick 'find(notes, "Umbenannt")'
|
||||
click 'find(grabbed(), "Löschen")'
|
||||
check "Notiz gelöscht" "$(val '[GLib.file_test(store.notesDir + "/Umbenannt.md", GLib.FileTest.EXISTS), JSON.stringify(notes._noteTitle.text)].join(" ")')" 'false ""'
|
||||
check "Trennlinie zwischen Ordnern und Notizen" "$(val 'notes._tree.get_children().map(c => c.style_class).join("|")')" "tree-row folder|tree-row folder|tree-separator|tree-row note"
|
||||
|
||||
rclickb 'notes.get_child_at_index(0)'
|
||||
check "Rechtsklick Notiz-Fläche" "$(val 'labels(grabbed()).join("|")')" "Notiz erstellen|Ordner erstellen"
|
||||
click 'find(grabbed(), "Notiz erstellen")'
|
||||
type_ "Wurzel"; key Return
|
||||
check "Notiz im Wurzelordner, Editor offen" "$(val '[GLib.file_test(store.notesDir + "/Wurzel.md", GLib.FileTest.EXISTS), notes._editing].join(" ")')" "true true"
|
||||
click 'cal._viewButtons.month'
|
||||
rclickb 'notes'
|
||||
check "Rechtsklick im Inhaltsbereich" "$(val 'labels(grabbed()).join("|")')" "Notiz erstellen|Ordner erstellen"
|
||||
key Escape
|
||||
rclick 'find(notes, "▸ Privat")'
|
||||
check "Ordnermenü" "$(val 'labels(grabbed()).join("|")')" "Notiz erstellen|Ordner erstellen|Umbenennen|Löschen (Ordner ist nicht leer)"
|
||||
click 'find(grabbed(), "Ordner erstellen")'
|
||||
type_ "Sub"; key Return
|
||||
check "Unterordner im Ordner" "$(val '[GLib.file_test(store.notesDir + "/Privat/Sub", GLib.FileTest.IS_DIR), notes._selected?.name].join(" ")')" "true Sub"
|
||||
rclick 'find(notes, "▾ Privat")'
|
||||
click 'find(grabbed(), "Notiz erstellen")'
|
||||
type_ "InPrivat"; key Return
|
||||
check "Notiz im Ordner" "$(val 'GLib.file_test(store.notesDir + "/Privat/InPrivat.md", GLib.FileTest.EXISTS)')" "true"
|
||||
click 'find(notes, "▾ Privat")'
|
||||
check "Ordner gewählt zeigt nichts" "$(val '[notes._selected.type, JSON.stringify(notes._noteTitle.text), JSON.stringify(notes._content.text)].join(" ")')" 'folder "" ""'
|
||||
|
||||
echo "=== Woche: Rechtsklick, Termin ohne Ende, mehrtägig"
|
||||
click 'cal._viewButtons.week'
|
||||
rclick 'cal._slots[1][9]'
|
||||
check "Rechtsklick Stundenzelle" "$(val 'labels(grabbed()).join("|")')" "Termin erstellen|Todo erstellen"
|
||||
click 'find(grabbed(), "Todo erstellen")'
|
||||
check "Todo mit Tag und Uhrzeit vorbelegt" "$(val '["Di, 15.09.2026 ▾", "09:00 ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja"
|
||||
key Escape
|
||||
rclick 'cal._slots[1][9]'
|
||||
click 'find(grabbed(), "Termin erstellen")'
|
||||
check "Termin mit Tag und Startzeit vorbelegt" "$(val '["Di, 15.09.2026 ▾", "09:00 ▾", "Keine Uhrzeit ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja ja"
|
||||
type_ "Kurz"; key Return
|
||||
check "Ohne Ende gilt eine Stunde" "$(val 'JSON.stringify((({start, end}) => [start, end])(store.events.find(e => e.title === "Kurz")))')" '["09:00","10:00"]'
|
||||
check "Mehrtägiger Termin als zwei Blöcke" "$(val 'JSON.stringify(cal._blocks.filter(b => b.event.title === "Messe").map(b => [b.dayIndex, b.from, b.to]))')" "[[1,840,1440],[2,0,660]]"
|
||||
$IN drag $(xy 'cal._slots[5][12]') $(xy 'cal._slots[6][12]'); sleep 0.3
|
||||
check "Ziehen über zwei Tage markiert" "$(val '[cal._slots[5].filter(s => s.has_style_class_name("selecting")).length, cal._slots[6].filter(s => s.has_style_class_name("selecting")).length].join(" ")')" "12 13"
|
||||
check "Mehrtägig vorbelegt" "$(val '["Sa, 19.09.2026 ▾", "12:00 ▾", "So, 20.09.2026 ▾", "13:00 ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja ja ja"
|
||||
shot "08-multiday-drag"
|
||||
type_ "Lang"; key Return
|
||||
check "Mehrtägig gespeichert" "$(val 'JSON.stringify((({date, start, endDate, end}) => [date, start, endDate, end])(store.events.find(e => e.title === "Lang")))')" '["2026-09-19","12:00","2026-09-20","13:00"]'
|
||||
check "Mehrtägig als zwei Blöcke" "$(val 'JSON.stringify(cal._blocks.filter(b => b.event.title === "Lang").map(b => [b.dayIndex, b.from, b.to]))')" "[[5,720,1440],[6,0,780]]"
|
||||
$IN drag $(xy 'cal._alldaySlots[2]') $(xy 'cal._alldaySlots[4]'); sleep 0.3
|
||||
check "Ganztags über drei Tage markiert" "$(val 'cal._alldaySlots.map((s, d) => s.has_style_class_name("selecting") ? d : null).filter(d => d !== null).join(",")')" "2,3,4"
|
||||
check "Ganztags mehrtägig vorbelegt" "$(val '["Mi, 16.09.2026 ▾", "Keine Uhrzeit ▾", "Fr, 18.09.2026 ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja ja"
|
||||
type_ "Urlaub"; key Return
|
||||
check "Ganztags mehrtägig gespeichert" "$(val 'JSON.stringify((({date, start, endDate}) => [date, start, endDate])(store.events.find(e => e.title === "Urlaub")))')" '["2026-09-16",null,"2026-09-18"]'
|
||||
check "Ganztags als ein Block über drei Tage" "$(val '(b => b.length + " " + b[0].from + "-" + b[0].to + " " + Math.round(b[0].block.width / (cal._alldaySlots[1].x - cal._alldaySlots[0].x)))(cal._alldayBlocks.filter(b => b.event.title === "Urlaub"))')" "1 2-4 3"
|
||||
check "Ganztags-Zellen bündig mit Stundenzellen" "$(val '[0, 3, 6].map(d => Math.round(cal._alldaySlots[d].get_transformed_position()[0] - cal._slots[d][9].get_transformed_position()[0]) + "/" + Math.round(cal._alldaySlots[d].width - cal._slots[d][9].width)).join(" ")')" "0/0 0/0 0/0"
|
||||
check "Ganztags-Block bündig mit der Spalte" "$(val '(b => Math.abs(b.block.x - cal._alldaySlots[2].x) <= 1)(cal._alldayBlocks.find(b => b.event.title === "Urlaub"))')" "true"
|
||||
check "Zwei Ganztags-Termine untereinander" "$(val 'JSON.stringify(cal._alldayBlocks.filter(b => ["Urlaub", "Frei"].includes(b.event.title)).map(b => b.event.title + ":" + b.lane))')" '["Urlaub:0","Frei:0"]'
|
||||
shot "09-allday-blocks"
|
||||
click 'cal._blocks.find(b => b.event.title === "Kurz").block'
|
||||
click 'find(grabbed(), "09:00 ▾")'
|
||||
click 'find(grabbed(), "11:00")'
|
||||
check "Fokus nach Zeitauswahl im Titelfeld" "$(val 'global.stage.get_key_focus()?.constructor.name + " " + grabbed().contains(global.stage.get_key_focus())')" "Clutter_Text true"
|
||||
key Return
|
||||
check "Ende vor Anfang abgelehnt" "$(val 'labels(grabbed()).includes("Ende liegt vor dem Anfang") ? "Fehler gezeigt" : "kein Fehler"')" "Fehler gezeigt"
|
||||
key Escape
|
||||
|
||||
echo "=== Monat und Tag"
|
||||
click 'cal._viewButtons.month'
|
||||
check "Monat: Uhrzeit nur bei eintägigen Terminen" "$(val '(c => [c.get_child_at_index(17), c.get_child_at_index(19)].map(cell => cell.get_child().get_child_at_index(2).label_actor.text).join("|"))(cal._body.get_child().get_child_at_index(1))')" "09:30 Zahnarzt|Lang"
|
||||
rclick 'cal._body.get_child().get_child_at_index(1).get_child_at_index(21)'
|
||||
check "Rechtsklick Monatszelle" "$(val 'labels(grabbed()).join("|")')" "Termin erstellen|Todo erstellen"
|
||||
click 'find(grabbed(), "Termin erstellen")'
|
||||
check "Tag vorbelegt" "$(val '["Mo, 21.09.2026 ▾", "Keine Uhrzeit ▾"].map(t => find(grabbed(), t) ? "ja" : "nein").join(" ")')" "ja ja"
|
||||
key Escape
|
||||
click 'cal._viewButtons.month'
|
||||
click 'cal._body.get_child().get_child_at_index(1).get_child_at_index(17).get_child().get_child_at_index(2)'
|
||||
check "Monatseintrag öffnet Popover" "$(val 'entries(grabbed())[0]')" "Zahnarzt"
|
||||
key Escape
|
||||
click 'cal._body.get_child().get_child_at_index(1).get_child_at_index(17).get_child().get_child_at_index(0)'
|
||||
check "Tagesansicht" "$(val 'cal._view + " " + cal._cursor')" "day 2026-09-17"
|
||||
check "Keine Buttons in der Tagesansicht" "$(val '[find(cal, "+ Todo"), find(cal, "+ Termin"), find(cal, "Heute")].filter(Boolean).length')" "0"
|
||||
rclickb 'cal._body.get_child()'
|
||||
click 'find(grabbed(), "Todo erstellen")'
|
||||
check "Todo aus Tagesansicht hat Datum" "$(val 'find(grabbed(), "Do, 17.09.2026 ▾") ? "ja" : "nein"')" "ja"
|
||||
key Escape
|
||||
rclickb 'cal._body.get_child()'
|
||||
check "Rechtsklick Tag-Fläche" "$(val 'labels(grabbed()).join("|")')" "Termin erstellen|Todo erstellen"
|
||||
click 'find(grabbed(), "Termin erstellen")'
|
||||
check "Termin erstellen für den Tag" "$(val 'labels(grabbed())[0] + " " + (find(grabbed(), "Do, 17.09.2026 ▾") ? "ja" : "nein")')" "Neuer Termin ja"
|
||||
key Escape
|
||||
shot "07-day"
|
||||
click 'cal._viewButtons.month'
|
||||
click 'cal._title.get_parent().get_child_at_index(2)'
|
||||
check "Pfeil rechts blättert" "$(val 'cal._title.text')" "Oktober 2026"
|
||||
check "Pfeil rechts direkt am Titel" "$(val '(n => n.x - (cal._title.x + cal._title.width) < 20)(cal._title.get_parent().get_child_at_index(2))')" "true"
|
||||
click 'cal._viewButtons.day'
|
||||
check "Tag-Button nutzt Cursor" "$(val 'cal._view')" "day"
|
||||
|
||||
check "Keine JS-Fehler" "$(grep -c 'JS ERROR' /tmp/desktop-app-nested.log)" "0"
|
||||
echo
|
||||
if [ $FAILED -eq 0 ]; then echo "alle ok"; else echo "$FAILED FEHLER"; grep -n "JS ERROR" -A5 /tmp/desktop-app-nested.log | head -40; fi
|
||||
66
dev/tests/run.js
Normal file
66
dev/tests/run.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// Tests der reinen Module: gjs -m dev/tests/run.js
|
||||
import * as D from '../../desktop@marha.de/dates.js';
|
||||
import {renderMarkdown} from '../../desktop@marha.de/markdown.js';
|
||||
|
||||
let failed = 0;
|
||||
function eq(actual, expected, name) {
|
||||
const a = JSON.stringify(actual), e = JSON.stringify(expected);
|
||||
if (a === e)
|
||||
print(`ok ${name}`);
|
||||
else {
|
||||
failed++;
|
||||
print(`FAIL ${name}\n got ${a}\n expected ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
eq(D.toDateString(new Date(2026, 8, 17)), '2026-09-17', 'toDateString');
|
||||
eq(D.addDays('2026-09-30', 1), '2026-10-01', 'addDays über Monatsgrenze');
|
||||
eq(D.addDays('2026-01-01', -1), '2025-12-31', 'addDays rückwärts über Jahr');
|
||||
eq(D.startOfWeek('2026-09-17'), '2026-09-14', 'startOfWeek Do → Mo');
|
||||
eq(D.startOfWeek('2026-09-20'), '2026-09-14', 'startOfWeek So → Mo');
|
||||
eq(D.startOfWeek('2026-09-14'), '2026-09-14', 'startOfWeek Mo bleibt');
|
||||
eq(D.weekDays('2026-09-17').length, 7, 'weekDays 7 Tage');
|
||||
const grid = D.monthGrid(2026, 8);
|
||||
eq(grid[0], '2026-08-31', 'monthGrid startet Montag davor');
|
||||
eq(grid.length, 42, 'monthGrid 42 Zellen');
|
||||
eq(D.monthGrid(2026, 5)[0], '2026-06-01', 'monthGrid Monat beginnt Montag');
|
||||
eq(D.isoWeek('2026-09-17'), 38, 'isoWeek');
|
||||
eq(D.isoWeek('2027-01-01'), 53, 'isoWeek Jahreswechsel');
|
||||
eq(D.formatMonth(2026, 8), 'September 2026', 'formatMonth');
|
||||
eq(D.formatDate('2026-09-17'), 'Do, 17.09.2026', 'formatDate');
|
||||
eq(D.formatDayMonth('2026-09-17'), '17.09.', 'formatDayMonth');
|
||||
eq(D.parseDateInput('17.09.2026'), '2026-09-17', 'parseDateInput dd.mm.yyyy');
|
||||
eq(D.parseDateInput('1.9.26'), '2026-09-01', 'parseDateInput kurz');
|
||||
eq(D.parseDateInput('2026-09-17'), '2026-09-17', 'parseDateInput iso');
|
||||
eq(D.parseDateInput('31.02.2026'), null, 'parseDateInput ungültig');
|
||||
eq(D.parseDateInput('abc'), null, 'parseDateInput Müll');
|
||||
eq(D.parseTimeInput('9:05'), '09:05', 'parseTimeInput h:mm');
|
||||
eq(D.parseTimeInput('14'), '14:00', 'parseTimeInput nur Stunde');
|
||||
eq(D.parseTimeInput('9.30'), '09:30', 'parseTimeInput Punkt');
|
||||
eq(D.parseTimeInput('25:00'), null, 'parseTimeInput ungültig');
|
||||
eq(D.timeToMinutes('01:30'), 90, 'timeToMinutes');
|
||||
eq(D.minutesToTime(90), '01:30', 'minutesToTime');
|
||||
|
||||
eq(renderMarkdown('# Titel'), '<span size="18pt" weight="bold">Titel</span>', 'md h1');
|
||||
eq(renderMarkdown('## A **b** c'), '<span size="15pt" weight="bold">A <b>b</b> c</span>', 'md h2 bold');
|
||||
eq(renderMarkdown('a *kursiv* b'), 'a <i>kursiv</i> b', 'md italic');
|
||||
eq(renderMarkdown('snake_case_name'), 'snake_case_name', 'md underscore im Wort bleibt');
|
||||
eq(renderMarkdown('2 * 3 * 4'), '2 * 3 * 4', 'md einzelne Sterne bleiben');
|
||||
eq(renderMarkdown('- eins\n - zwei'), ' • eins\n • zwei', 'md verschachtelte Liste');
|
||||
eq(renderMarkdown('1. a\n2. b'), ' 1. a\n 2. b', 'md nummeriert');
|
||||
eq(renderMarkdown('- [ ] offen\n- [x] fertig'),
|
||||
' ☐ offen\n ☑ <span foreground="#888888"><s>fertig</s></span>', 'md checkbox');
|
||||
eq(renderMarkdown('a `x < y` b'), 'a <span font_family="monospace" background="#2b2b2b">x < y</span> b', 'md code escaped');
|
||||
eq(renderMarkdown('`**nicht fett**`'), '<span font_family="monospace" background="#2b2b2b">**nicht fett**</span>', 'md code schützt Sterne');
|
||||
eq(renderMarkdown('```\nlet a = 1 & 2;\n```'), '<span font_family="monospace" background="#2b2b2b">let a = 1 & 2;</span>', 'md codeblock');
|
||||
eq(renderMarkdown('[Link](http://x)'), '<u><span foreground="#8ab4f8">Link</span></u>', 'md link');
|
||||
eq(renderMarkdown('[[Wiki]]'), '<u><span foreground="#8ab4f8">Wiki</span></u>', 'md wikilink');
|
||||
eq(renderMarkdown(''), '[Bild: Bild]', 'md image');
|
||||
eq(renderMarkdown('> zitat'), '<span foreground="#b0b0b0">▎ <i>zitat</i></span>', 'md quote');
|
||||
eq(renderMarkdown('---'), '<span foreground="#666666">────────────────────────────</span>', 'md hr');
|
||||
eq(renderMarkdown('~~weg~~'), '<s>weg</s>', 'md strike');
|
||||
eq(renderMarkdown('a\n\nb'), 'a\n\nb', 'md leere Zeile bleibt');
|
||||
|
||||
print(failed ? `\n${failed} FEHLER` : '\nalle Tests ok');
|
||||
if (failed)
|
||||
imports.system.exit(1);
|
||||
11
dev/unsafe@dev/extension.js
Normal file
11
dev/unsafe@dev/extension.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
export default class UnsafeExtension extends Extension {
|
||||
enable() {
|
||||
global.context.unsafe_mode = true;
|
||||
}
|
||||
|
||||
disable() {
|
||||
global.context.unsafe_mode = false;
|
||||
}
|
||||
}
|
||||
6
dev/unsafe@dev/metadata.json
Normal file
6
dev/unsafe@dev/metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"uuid": "unsafe@dev",
|
||||
"name": "Dev Unsafe Mode",
|
||||
"description": "Nur für die nested Test-Shell: erlaubt Eval und Screenshots per D-Bus",
|
||||
"shell-version": ["45", "46", "47", "48", "49"]
|
||||
}
|
||||
Reference in New Issue
Block a user