diff --git a/changelog.d/4674.md b/changelog.d/4674.md new file mode 100644 index 000000000..71e07a940 --- /dev/null +++ b/changelog.d/4674.md @@ -0,0 +1 @@ +- A videónéző-próba elkülöníti a QTest-kattintásokat, és minden gesztus állapotát naplózza (#4674). diff --git a/tests/app/qml_video_probe.py b/tests/app/qml_video_probe.py index 8186c3b91..021cecf73 100644 --- a/tests/app/qml_video_probe.py +++ b/tests/app/qml_video_probe.py @@ -15,25 +15,39 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") -def varj_kattintasablak_vegere( - app, - utolso_kattintas: float, - intervallum_ms: int, +QTEST_EGER_DELAY_MS = 10 + + +def qtest_egerlepes( + qtest, + window, + point, + button, + double_click_interval_ms: int, *, - ora=None, - alvas=None, - tartalek_ms: int = 100, + dupla: bool, + elozo_gesztus_volt: bool, ) -> None: - """Várja meg a Qt duplakattintási ablakának végét, eseményeket kezelve.""" - ora = ora or time.monotonic - alvas = alvas or time.sleep - hatarido = utolso_kattintas + (intervallum_ms + tartalek_ms) / 1000 - while True: - app.processEvents() - hatralevo = hatarido - ora() - if hatralevo <= 0: - return - alvas(min(0.05, hatralevo)) + """Valós Qt-időbélyeggel választja el az egérgesztusokat. + + A QTest saját eseményórát léptet; a falióra szerinti várakozás nem hat rá. + Egy előző gesztus után ezért egy, a duplakattintási ablaknál hosszabb + időbélyegű egérmozgás zárja le a korábbi kattintást. A dupla kattintás + négy eseményén belül rövid, explicit késleltetés marad. + """ + if elozo_gesztus_volt: + from PySide6.QtCore import QPoint + + qtest.mouseMove( + window, point + QPoint(1, 0), delay=double_click_interval_ms + 1 + ) + fuggveny = qtest.mouseDClick if dupla else qtest.mouseClick + fuggveny( + window, + button, + pos=point, + delay=QTEST_EGER_DELAY_MS, + ) def main(work_dir: Path) -> None: @@ -41,6 +55,7 @@ def main(work_dir: Path) -> None: from picasapy.app.controller import AppController from picasapy.app.edit_controller import EditController from picasapy.app.edit_preview import EditPreviewProvider + from picasapy.app.effect_thumbnails import EffectThumbnailProvider from picasapy.app.fileops_controller import FileOpsController from picasapy.app.thumbnail_provider import ThumbnailProvider from picasapy.index import open_index, sync_tree @@ -54,7 +69,6 @@ def main(work_dir: Path) -> None: from support.jpeg_factory import make_jpeg app = QGuiApplication([]) - utolso_kattintas_ideje = None lib = work_dir / "kepek" lib.mkdir() @@ -68,6 +82,9 @@ def main(work_dir: Path) -> None: str(work_dir / "settings.ini"), QSettings.Format.IniFormat ) provider = ThumbnailProvider(ThumbnailCache(work_dir / "thumbs", size=32)) + effect_thumb_provider = EffectThumbnailProvider( + provider.photo_record, max_threads=1 + ) controller = AppController(db, (str(lib),), provider, settings=settings) edit_preview = EditPreviewProvider() edit_controller = EditController(edit_preview) @@ -76,19 +93,25 @@ def main(work_dir: Path) -> None: engine = QQmlApplicationEngine() engine.addImageProvider("thumbs", provider) engine.addImageProvider("editpreview", edit_preview) + engine.addImageProvider("effectthumb", effect_thumb_provider) engine.addImportPath(str(app_module._APP_DIR / "qml")) engine.rootContext().setContextProperty("controller", controller) engine.rootContext().setContextProperty("editController", edit_controller) + engine.rootContext().setContextProperty("discoveryController", None) + engine.rootContext().setContextProperty("timelineController", None) engine.rootContext().setContextProperty( "fileOpsController", fileops_controller ) engine.rootContext().setContextProperty("appVersion", version_string()) + print("PROBE-INIT before Main.qml load", flush=True) engine.load(str(app_module._APP_DIR / "qml" / "Main.qml")) + print("PROBE-INIT after Main.qml load", flush=True) assert engine.rootObjects(), "Main.qml betöltése sikertelen" window = engine.rootObjects()[0] controller._reload() controller.selectFolder(str(lib)) app.processEvents() + print("PROBE-INIT after initial processEvents", flush=True) def child(name): obj = window.findChild(QObject, name) @@ -144,87 +167,87 @@ def child(name): f"váratlan idő-címke: {time_label.property('text')!r}" ) - def kattintas(item, dupla=False): - nonlocal utolso_kattintas_ideje - if utolso_kattintas_ideje is not None: - varj_kattintasablak_vegere( - app, - utolso_kattintas_ideje, - QGuiApplication.styleHints().mouseDoubleClickInterval(), - ) - pont = item.mapToScene( - QPointF(item.property("width") / 2, item.property("height") / 2) - ).toPoint() - if dupla: - QTest.mouseDClick(window, Qt.MouseButton.LeftButton, pos=pont) - else: - QTest.mouseClick(window, Qt.MouseButton.LeftButton, pos=pont) - app.processEvents() - utolso_kattintas_ideje = time.monotonic() - - def naplozott_dupla_kattintas(item): - nonlocal utolso_kattintas_ideje - if utolso_kattintas_ideje is not None: - varj_kattintasablak_vegere( - app, - utolso_kattintas_ideje, - QGuiApplication.styleHints().mouseDoubleClickInterval(), - ) - pont = item.mapToScene( - QPointF(item.property("width") / 2, item.property("height") / 2) - ).toPoint() + aktiv_lepes = {"nev": "előkészítés", "elem": None} + + def elem_tulajdonsag(elem, nev): + if elem is None: + return None + try: + return elem.property(nev) + except RuntimeError: + return "" + + def elem_nev(elem): + if elem is None: + return None + try: + return elem.objectName() + except RuntimeError: + return "" + + def nezo_allapot(elem=None): + return ( + f"viewerOpen={window.property('viewerOpen')!r}, " + f"currentIndex={viewer.property('currentIndex')!r}, " + f"isCurrentVideo={viewer.property('isCurrentVideo')!r}, " + f"singleClickExit={controller.singleClickExitEnabled!r}, " + "exitAfterDoubleClick=" + f"{elem_tulajdonsag(elem, 'exitAfterDoubleClick')!r}, " + f"clickTarget={elem_nev(elem)!r}, " + f"targetEnabled={elem_tulajdonsag(elem, 'enabled')!r}, " + f"targetVisible={elem_tulajdonsag(elem, 'visible')!r}" + ) - def naploz(nev): - def allapot(*_args): + class QtEsemenyNaplozo(QObject): + def eventFilter(self, _cel, event): + nev = { + QEvent.Type.MouseButtonPress: "press", + QEvent.Type.MouseButtonRelease: "release", + QEvent.Type.MouseButtonDblClick: "double-click", + QEvent.Type.MouseMove: "move", + }.get(event.type()) + if nev is not None: + elem = aktiv_lepes["elem"] print( - "SingleClickExit állóképes dupla kattintás " - f"MouseArea.{nev} után: " - f"viewerOpen={window.property('viewerOpen')!r}, " - "exitAfterDoubleClick=" - f"{item.property('exitAfterDoubleClick')!r}", + "PROBE-MOUSE " + f"step={aktiv_lepes['nev']} event={nev} " + f"timestamp={event.timestamp()} " + f"monotonic={time.monotonic():.6f} " + f"{nezo_allapot(elem)}", flush=True, ) - - return allapot - - class QtEsemenyNaplozo(QObject): - def eventFilter(self, _cel, event): - nev = { - QEvent.Type.MouseButtonPress: "press", - QEvent.Type.MouseButtonRelease: "release", - QEvent.Type.MouseButtonDblClick: "double-click", - }.get(event.type()) - if nev is not None: - print( - "SingleClickExit Qt egéresemény érkezett " - f"({nev}); viewerOpen=" - f"{window.property('viewerOpen')!r}, " - "exitAfterDoubleClick=" - f"{item.property('exitAfterDoubleClick')!r}", - flush=True, - ) - return False - - item.pressed.connect(naploz("pressed")) - item.released.connect(naploz("released")) - item.doubleClicked.connect(naploz("doubleClicked")) - esemenynaplozo = QtEsemenyNaplozo(window) - window.installEventFilter(esemenynaplozo) - QTest.mouseDClick(window, Qt.MouseButton.LeftButton, pos=pont) - app.processEvents() - utolso_kattintas_ideje = time.monotonic() - window.removeEventFilter(esemenynaplozo) - print( - "SingleClickExit állóképes dupla kattintás után: " - f"viewerOpen={window.property('viewerOpen')!r}, " - "exitAfterDoubleClick=" - f"{item.property('exitAfterDoubleClick')!r}", - flush=True, - ) + return False + + esemenynaplozo = QtEsemenyNaplozo(window) + window.installEventFilter(esemenynaplozo) + elozo_gesztus_volt = False + + def naplozott_kattintas(nev, elem, *, dupla=False): + nonlocal elozo_gesztus_volt + aktiv_lepes.update(nev=nev, elem=elem) + print(f"PROBE-STEP START {nev} {nezo_allapot(elem)}", flush=True) + try: + pont = elem.mapToScene( + QPointF( + elem.property("width") / 2, + elem.property("height") / 2, + ) + ).toPoint() + qtest_egerlepes( + QTest, + window, + pont, + Qt.MouseButton.LeftButton, + QGuiApplication.styleHints().mouseDoubleClickInterval(), + dupla=dupla, + elozo_gesztus_volt=elozo_gesztus_volt, + ) + app.processEvents() + elozo_gesztus_volt = True + finally: + print(f"PROBE-STEP END {nev} {nezo_allapot(elem)}", flush=True) def varj(feltetel, timeout_s=3.0): - import time - hatarido = time.monotonic() + timeout_s while time.monotonic() < hatarido: app.processEvents() @@ -237,11 +260,11 @@ def varj(feltetel, timeout_s=3.0): assert controller.singleClickExitEnabled is False video_viewport = item.findChild(QObject, "videoViewport") assert video_viewport is not None - kattintas(video_viewport) + naplozott_kattintas("video-single-click", video_viewport) assert window.property("viewerOpen") is True, ( "alapállapotban az egyszeres videókattintás bezárta a szerkesztőt" ) - kattintas(video_viewport, dupla=True) + naplozott_kattintas("video-double-click", video_viewport, dupla=True) assert varj(lambda: window.property("viewerOpen") is False), ( "alapállapotban a videóablak dupla kattintása nem tért vissza a könyvtárba" ) @@ -254,7 +277,7 @@ def varj(feltetel, timeout_s=3.0): video_item = child("videoLoader").property("item") video_viewport = video_item.findChild(QObject, "videoViewport") assert video_viewport is not None - kattintas(video_viewport) + naplozott_kattintas("video-single-click-exit-click", video_viewport) assert varj(lambda: window.property("viewerOpen") is False), ( "a SingleClickExit bekapcsolva nem vitte vissza a könyvtárba" ) @@ -269,7 +292,7 @@ def varj(feltetel, timeout_s=3.0): assert varj(lambda: pan_area.property("enabled")), ( "a nagyított állóképes előnézet kattintási területe nem aktív" ) - kattintas(pan_area) + naplozott_kattintas("photo-single-click-exit-click", pan_area) assert varj(lambda: window.property("viewerOpen") is False), ( "a SingleClickExit bekapcsolva az állóképes egyszeres kattintásra " "nem tért vissza a könyvtárba" @@ -283,7 +306,9 @@ def varj(feltetel, timeout_s=3.0): viewer.setProperty("zoomValue", 1.0) pan_area = child("viewerPanArea") assert varj(lambda: pan_area.property("enabled")) - naplozott_dupla_kattintas(pan_area) + naplozott_kattintas( + "photo-double-click-exit-click", pan_area, dupla=True + ) assert varj(lambda: window.property("viewerOpen") is False), ( "SingleClickExit mellett a dupla kattintás után újranyílt vagy " "nyitva maradt az állóképes néző" diff --git a/tests/app/test_qml_video.py b/tests/app/test_qml_video.py index 658484bfe..ca206f721 100644 --- a/tests/app/test_qml_video.py +++ b/tests/app/test_qml_video.py @@ -13,6 +13,7 @@ """ import os +import re import socket import subprocess import sys @@ -70,25 +71,55 @@ def test_video_viewer_probe(tmp_path): ) if not _linux_hangkimenet_elerheto(): pytest.skip( - "a Qt Multimedia-próba kihagyva: Linuxon sem a PipeWire, sem a " - "PulseAudio kimeneti foglalata nem elérhető ebben a környezetben" + "a Qt Multimedia videópróba kihagyva: a MediaPlayer a " + "videónéző betöltésekor Linuxon hangkimenetet nyit; hang nélküli " + "gépen a videó-gesztust a VideoExitGestureArea külön próbája méri" ) probe = Path(__file__).parent / "qml_video_probe.py" repo_root = Path(__file__).resolve().parents[2] env = dict(os.environ) env["QT_QPA_PLATFORM"] = "offscreen" + env["QT_QUICK_BACKEND"] = "software" env["PYTHONPATH"] = os.pathsep.join( [str(repo_root / "src"), str(repo_root / "tests")] ) - result = subprocess.run( - [sys.executable, str(probe), str(tmp_path)], - capture_output=True, - text=True, encoding="utf-8", errors="replace", - timeout=120, - env=env, - ) + try: + result = subprocess.run( + [sys.executable, str(probe), str(tmp_path)], + capture_output=True, + text=True, encoding="utf-8", errors="replace", + timeout=120, + env=env, + ) + except subprocess.TimeoutExpired as exc: + def szoveg(kimenet): + if isinstance(kimenet, bytes): + return kimenet.decode("utf-8", errors="replace") + return kimenet or "" + + pytest.fail( + "a videónéző-próba 120 másodperc alatt nem fejeződött be\n" + f"stdout:\n{szoveg(exc.stdout)}\n" + f"stderr:\n{szoveg(exc.stderr)}", + pytrace=False, + ) assert result.returncode == 0, ( f"probe exit={result.returncode}\n" f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" ) assert "OK" in result.stdout + lepesek = ( + "video-single-click", + "video-double-click", + "video-single-click-exit-click", + "photo-single-click-exit-click", + "photo-double-click-exit-click", + ) + for lepes in lepesek: + assert f"PROBE-STEP START {lepes} " in result.stdout + assert f"PROBE-STEP END {lepes} " in result.stdout + assert re.search( + rf"PROBE-MOUSE step={re.escape(lepes)} " + r"event=press timestamp=\d+ monotonic=\d+\.\d+", + result.stdout, + ), f"{lepes}: nem került egéresemény-időbélyeg a próbanaplóba" diff --git a/tests/app/test_qml_video_probe_timing.py b/tests/app/test_qml_video_probe_timing.py index e76bd1bde..9cf9c17e2 100644 --- a/tests/app/test_qml_video_probe_timing.py +++ b/tests/app/test_qml_video_probe_timing.py @@ -1,8 +1,10 @@ -"""Időzítési regressziótesztek a külön processzben futó videópróbához.""" +"""A QTest egér-időbélyegeit használó videópróba-gesztusok (#4674).""" import importlib.util from pathlib import Path +from PySide6.QtCore import QPoint, Qt + _PROBE_UT = Path(__file__).with_name("qml_video_probe.py") _SPEC = importlib.util.spec_from_file_location("qml_video_probe_timing", _PROBE_UT) @@ -11,31 +13,47 @@ _SPEC.loader.exec_module(_PROBE) -def test_varj_kattintasablak_vege_a_duplakattintas_intervallum_es_tartalek_utan(): - ido = 10.0 - alvasok = [] +def test_az_elozo_kattintas_utan_a_qtest_idobelye_valasztja_el_a_kovetkezot(): + class QTestRecorder: + def __init__(self): + self.esemenyek = [] + + def mouseClick(self, window, button, *, pos, delay): + self.esemenyek.append(("click", window, button, pos, delay)) - class Alkalmazas: - feldolgozasok = 0 + def mouseDClick(self, window, button, *, pos, delay): + self.esemenyek.append(("double-click", window, button, pos, delay)) - def processEvents(self): - self.feldolgozasok += 1 + def mouseMove(self, window, pos, *, delay): + self.esemenyek.append(("move", window, pos, delay)) - def alvas(masodperc): - nonlocal ido - alvasok.append(masodperc) - ido += masodperc + qtest = QTestRecorder() + window = object() + point = QPoint(40, 50) + button = Qt.MouseButton.LeftButton + interval_ms = 400 - app = Alkalmazas() - _PROBE.varj_kattintasablak_vegere( - app, - utolso_kattintas=10.0, - intervallum_ms=500, - ora=lambda: ido, - alvas=alvas, - tartalek_ms=100, + _PROBE.qtest_egerlepes( + qtest, + window, + point, + button, + interval_ms, + dupla=False, + elozo_gesztus_volt=False, + ) + _PROBE.qtest_egerlepes( + qtest, + window, + point, + button, + interval_ms, + dupla=True, + elozo_gesztus_volt=True, ) - assert ido >= 10.6 - assert app.feldolgozasok > 0 - assert alvasok and max(alvasok) <= 0.05 + assert qtest.esemenyek == [ + ("click", window, button, point, 10), + ("move", window, point + QPoint(1, 0), interval_ms + 1), + ("double-click", window, button, point, 10), + ]