204 lines
9.2 KiB
Python
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:]))
|