Dotfiles_ArchAP/tests/clipboard/ui_quickshell_smoke.py

204 lines
9.2 KiB
Python

import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
# Exécuter dans une session Wayland ; base et wl-copy restent privés au test.
REPO = Path(__file__).resolve().parents[2]
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
db = root / "db"
output = root / "copied"
mime = root / "mime"
fake = root / "wl-copy"
png = subprocess.run(["magick", "-size", "2x2", "xc:blue", "png:-"],
capture_output=True, check=True).stdout
for index in range(14):
subprocess.run(["cliphist", "-db-path", str(db), "store"],
input=("fixture-%02d" % index).encode(), check=True)
fake.write_text("#!/usr/bin/env python3\nimport os,sys,pathlib\n"
"pathlib.Path(os.environ['TEST_OUT']).write_bytes(sys.stdin.buffer.read())\n"
"pathlib.Path(os.environ['TEST_MIME']).write_text(sys.argv[2])\n")
fake.chmod(0o700)
qml = root / "shell.qml"
qml.write_text('''import QtQuick
import QtTest
import Quickshell
import Quickshell.Io
import "file:/home/fy59/.dotfiles/quickshell/.config/quickshell/labfy-sway/status" as S
import "file:/home/fy59/.dotfiles/quickshell/.config/quickshell/labfy-sway/clipboard" as C
ShellRoot {
id: root
PanelWindow {
id: bar
screen: Quickshell.screens[0]
anchors { top: true; right: true }
implicitWidth: 100
implicitHeight: 34
S.ClipboardIndicator { id: icon; open: loader.active; anchors.centerIn: parent;
onToggled: { if (loader.active) loader.item.dismiss(true); else loader.active = true; } }
}
LazyLoader { id: loader; active: false; C.ClipboardPanel { barWindow: bar; onCleanupFinished: loader.active = false } }
TestCase { id: tester; name: "Fixture"; when: false }
IpcHandler {
target: "clipboardMouseFixture"
function state(): string {
const p = loader.item;
return JSON.stringify({loaded: loader.active, visible: !!p && p.visible, focused: !!p && p.searchFocused,
results: p ? p.results.length : 0, images: p ? p.results.filter(x => x.image).length : 0,
thumbnail: p ? p.thumbnail : "", operation: p ? p.operation : "",
message: p ? p.message : "", selected: p ? p.selected : -1,
confirming: !!p && p.confirming, listY: p ? p.resultList.contentY : 0,
contentHeight: p ? p.resultList.contentHeight : 0, listHeight: p ? p.resultList.height : 0,
bounds: p ? {x:p.cardX,y:p.cardY,w:p.cardWidth,h:p.cardHeight} : null});
}
function clickIcon(): bool { tester.mouseClick(icon, 13, 13); return true; }
function clickItem(index: int): bool {
const item = loader.item.resultList.itemAtIndex(index);
if (!item) return false;
tester.mouseClick(item, 30, item.height / 2);
return true;
}
function clickClear(): bool {
const item = loader.item.clearActionItem;
tester.mouseClick(item, item.width / 2, item.height / 2);
return true;
}
function clickConfirm(): bool {
const item = loader.item.confirmActionItem;
tester.mouseClick(item, item.width / 2, item.height / 2);
return true;
}
function clickCancel(): bool {
const item = loader.item.cancelActionItem;
tester.mouseClick(item, item.width / 2, item.height / 2);
return true;
}
function clickRemove(): bool {
const item = loader.item.removeActionItem;
tester.mouseClick(item, item.width / 2, item.height / 2);
return true;
}
function wheel(): bool {
const item = loader.item.resultList;
tester.mouseWheel(item, item.width / 2, item.height / 2, 0, -120);
return true;
}
function clickOutside(): bool {
tester.mouseClick(loader.item.interactionRoot, 2, 2);
return true;
}
function refresh(): bool { loader.item.refresh(); return true; }
}
}
'''.replace("file:/home/fy59/.dotfiles", REPO.as_uri()))
environment = dict(os.environ, LABFY_CLIPHIST_TEST_DB=str(db),
LABFY_CLIPHIST_TEST_CACHE_DIR=str(root),
LABFY_CLIPHIST_TEST_WL_COPY=str(fake), TEST_OUT=str(output),
TEST_MIME=str(mime))
# CONTRACT: le chemin absolu injecté dans QML doit viser le collecteur factice,
# car un vrai wl-copy dans la session écraserait les données personnelles.
assert Path(environment["LABFY_CLIPHIST_TEST_WL_COPY"]).resolve() == fake.resolve()
assert fake.is_file() and os.access(fake, os.X_OK)
process = subprocess.Popen(["quickshell", "-p", str(qml)], env=environment,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
def call(name, *args):
result = subprocess.run(["quickshell", "ipc", "-p", str(qml), "call",
"clipboardMouseFixture", name, *map(str, args)],
capture_output=True, text=True)
if result.returncode:
raise AssertionError((name, result.stdout[:200], result.stderr[:200]))
return json.loads(result.stdout)
def wait_for(predicate, seconds=3):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
value = call("state")
if predicate(value):
return value
time.sleep(0.04)
raise AssertionError(("state timeout", call("state")))
try:
time.sleep(0.3)
assert call("clickIcon")
opened = wait_for(lambda state: state["loaded"] and state["focused"] and state["results"] == 14)
assert opened["bounds"]["w"] == 540 and opened["bounds"]["y"] == 46
if os.environ.get("CLIPBOARD_UI_CAPTURE"):
box = opened["bounds"]
# Le recadrage intérieur ne montre que la carte alimentée par les données factices.
geometry = f"{box['x'] + 2},{box['y'] + 2} {box['w'] - 4}x{box['h'] - 4}"
subprocess.run(["grim", "-g", geometry, os.environ["CLIPBOARD_UI_CAPTURE"]], check=True)
assert call("clickIcon")
wait_for(lambda state: not state["loaded"])
assert call("clickIcon")
wait_for(lambda state: state["results"] == 14)
assert call("wheel")
scrolled = wait_for(lambda state: state["listY"] > 0)
assert scrolled["contentHeight"] > scrolled["listHeight"]
time.sleep(0.4)
assert call("clickItem", 5)
wait_for(lambda state: not state["loaded"])
assert output.read_bytes() == b"fixture-08"
assert call("clickIcon")
wait_for(lambda state: state["results"] == 14)
assert call("clickRemove")
wait_for(lambda state: state["results"] == 13)
current = subprocess.run(["cliphist", "-db-path", str(db), "list"],
capture_output=True, check=True).stdout.splitlines()
assert len(current) == 13
assert call("clickClear")
assert call("state")["confirming"]
assert call("clickCancel")
assert not call("state")["confirming"]
assert len(subprocess.run(["cliphist", "-db-path", str(db), "list"],
capture_output=True, check=True).stdout.splitlines()) == 13
assert call("clickClear")
subprocess.run(["wtype", "-k", "Escape"], check=True)
wait_for(lambda state: not state["confirming"] and state["visible"])
assert call("clickClear")
assert call("clickIcon")
wait_for(lambda state: not state["loaded"])
assert call("clickIcon")
wait_for(lambda state: state["results"] == 13)
assert call("clickClear")
assert call("clickConfirm")
wait_for(lambda state: state["results"] == 0)
print("icon_click_toggle=true geometry=true focus=true mouse_selection=true click_restore=true"
" scroll=true delete=true purge_confirm=true purge_cancel=true escape_cancel=true"
" purge_forced_close=true empty=true")
subprocess.run(["cliphist", "-db-path", str(db), "store"], input=png, check=True)
assert call("refresh")
state = wait_for(lambda state: state["results"] == 1 and state["images"] == 1
and state["thumbnail"].startswith("file:"))
assert state["thumbnail"]
assert call("clickItem", 0)
wait_for(lambda state: not state["loaded"])
assert output.read_bytes() == png and mime.read_text() == "image/png"
print("image_mouse_restore=true image_mime=true")
assert call("clickIcon")
wait_for(lambda state: state["results"] == 1)
subprocess.run(["ydotool", "key", "28:1", "28:0"], check=True)
wait_for(lambda state: not state["loaded"])
assert output.read_bytes() == png and mime.read_text() == "image/png"
print("image_visible=true thumbnail=true image_keyboard_restore=true")
assert call("clickIcon")
wait_for(lambda state: state["results"] == 1)
assert call("clickOutside")
wait_for(lambda state: not state["loaded"])
assert not list(root.glob("labfy-cliphist-panel-*/*.png"))
print("outside_click_close=true")
finally:
process.terminate()
try:
log = process.communicate(timeout=4)[0]
except subprocess.TimeoutExpired:
process.kill()
log = process.communicate()[0]
errors = [line for line in log.splitlines() if "ERROR" in line]
if errors:
raise AssertionError("QuickShell reported errors: " + str(errors[-2:]))