- Preview Sync button builds a short (8s) lossless clip around the marked events with the current offset applied, and plays it in a small popup PreviewDialog (its own independent PlayerWidget/mpv instance) -- lets you check the fix sounds/looks right before committing to a full export. ExportWorker gained optional clip_start/clip_duration so both the real export and this preview share the same underlying export_synced() call. - Detect All Claps is back as a button in the Sync markers panel (alongside Mark Audio/Video Event), in addition to staying in the Tools menu -- both widgets' enabled state are kept in sync. - Sync markers panel now shows Mark Audio Event before Mark Video Event. The clip-mode branch of ffmpeg_export.py (the -ss/-t-on-both-inputs path ffdelay's own notes flag as easy to get subtly wrong) hadn't been covered by the existing roundtrip test, which only exercised full-file export -- added a clip-mode case there, independently verified via the same scene-change/silence-detection technique as the other cases: 29.4ms drift after the fix, well under one frame. All of this was built and verified entirely on a virtual Xvfb display (DISPLAY=:99) per the user's request to stop interrupting their real desktop -- real mouse clicks and a real (non-monkeypatched) preview dialog were exercised there, screenshotted, and confirmed correct, without ever touching the real screen.
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
"""Verifies the four interaction changes requested after the first review:
|
|
- clap detection moved to a Tools menu (no more buttons in the layout)
|
|
- clicking anywhere on the slider track jumps straight there
|
|
- dragging the slider seeks live (via sliderMoved), not just on release
|
|
- dropping a file onto the player widget loads it
|
|
|
|
Drives real Qt events (QMouseEvent, QDropEvent) against the actual
|
|
MainWindow/PlayerWidget classes rather than calling internal methods
|
|
directly, so this proves the actual event wiring, not just the logic
|
|
behind it.
|
|
"""
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtCore import Qt, QPointF, QUrl, QMimeData
|
|
from PyQt6.QtGui import QMouseEvent, QDropEvent
|
|
from PyQt6.QtWidgets import QApplication
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
import main as app_main
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
CLIP = str(ROOT / "test_media" / "sync_test_INSYNC.mp4")
|
|
|
|
|
|
def main():
|
|
qapp = QApplication(sys.argv)
|
|
win = app_main.MainWindow()
|
|
win.show()
|
|
qapp.processEvents()
|
|
|
|
# -- Tools menu has the clap detection actions, AND Detect All Claps is
|
|
# also back as a button in the Sync markers panel (both should reflect
|
|
# the same enabled state, kept in sync explicitly since they're two
|
|
# separate widgets).
|
|
menu_titles = [a.text() for a in win.menuBar().actions()]
|
|
assert "&Tools" in menu_titles, f"expected a Tools menu, got {menu_titles}"
|
|
assert win.detect_all_action.text() == "Detect All Claps"
|
|
assert win.detect_next_action.text() == "Detect Next Clap"
|
|
assert win.detect_all_button.text() == "Detect All Claps"
|
|
assert win.detect_all_action.isEnabled() == win.detect_all_button.isEnabled()
|
|
print("PASS: Tools menu has plain-labeled clap detection actions, button back in sync markers panel")
|
|
|
|
# -- Load via drag-and-drop simulation on the player widget
|
|
mime = QMimeData()
|
|
mime.setUrls([QUrl.fromLocalFile(CLIP)])
|
|
drop_event = QDropEvent(
|
|
QPointF(10, 10), Qt.DropAction.CopyAction, mime,
|
|
Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier,
|
|
QDropEvent.Type.Drop,
|
|
)
|
|
win.player.dropEvent(drop_event)
|
|
qapp.processEvents()
|
|
time.sleep(0.3)
|
|
qapp.processEvents()
|
|
assert win.input_path == CLIP, f"drag-and-drop should have loaded the file, got {win.input_path}"
|
|
print(f"PASS: drag-and-drop loaded {Path(win.input_path).name}")
|
|
|
|
# -- Clicking directly on the slider track (not the handle) jumps there
|
|
duration_ms = win.seek_slider.maximum()
|
|
assert duration_ms > 0
|
|
target_x = win.seek_slider.width() * 0.5
|
|
click_pos = QPointF(target_x, win.seek_slider.height() / 2)
|
|
press_event = QMouseEvent(
|
|
QMouseEvent.Type.MouseButtonPress, click_pos, click_pos,
|
|
Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier,
|
|
)
|
|
before = win.player.get_position()
|
|
win.seek_slider.mousePressEvent(press_event)
|
|
qapp.processEvents()
|
|
time.sleep(0.2)
|
|
qapp.processEvents()
|
|
after = win.player.get_position()
|
|
expected = 0.5 * (duration_ms / 1000.0)
|
|
print(f"clicked slider track at 50%: before={before:.3f}s after={after:.3f}s expected~{expected:.3f}s")
|
|
assert abs(after - expected) < 0.5, "clicking the track should jump the player near that position"
|
|
print("PASS: click-anywhere-on-track seeking works")
|
|
|
|
# -- Dragging (sliderMoved) seeks live, without waiting for release
|
|
win.seek_slider.sliderPressed.emit()
|
|
qapp.processEvents()
|
|
drag_value = int(0.2 * duration_ms)
|
|
win.seek_slider.setValue(drag_value)
|
|
win.seek_slider.sliderMoved.emit(drag_value)
|
|
qapp.processEvents()
|
|
time.sleep(0.2)
|
|
qapp.processEvents()
|
|
live_pos = win.player.get_position()
|
|
expected_drag = drag_value / 1000.0
|
|
print(f"dragged to 20% (still 'pressed', not released): live player position={live_pos:.3f}s expected~{expected_drag:.3f}s")
|
|
assert abs(live_pos - expected_drag) < 0.5, "dragging should seek immediately, before release"
|
|
win.seek_slider.sliderReleased.emit()
|
|
print("PASS: live-seek during drag works")
|
|
|
|
win.player.shutdown()
|
|
print("\nALL INTERACTION SMOKE TESTS PASSED")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|