Files
video-sync/desktop-client/test_fight_reactions.py
T

191 lines
7.5 KiB
Python

import time
import wave
from unittest.mock import Mock
import pytest
from PyQt6.QtCore import QEvent, QUrl
from PyQt6.QtMultimedia import QSoundEffect
from PyQt6.QtWidgets import QApplication
from fight_reactions import REACTIONS, write_reaction_sound
from test_chat import room
@pytest.mark.parametrize('kind', REACTIONS)
def test_command_dispatch(room, qtbot, kind):
with qtbot.waitSignal(room.fight_action_requested) as event:
room.chat_input.setText('/' + kind)
room.send_chat()
assert event.args == [{'type': 'reaction', 'kind': kind}]
assert room.chat_input.text() == ''
assert room.chat_content_layout.count() == 0 # Wait for the server echo.
@pytest.mark.parametrize('kind', REACTIONS)
def test_strip_is_native_positioned_and_does_not_steal_focus(room, qtbot, kind):
room.activateWindow()
room.chat_input.setFocus()
qtbot.wait(30)
room.handle_fight_reaction({'kind': kind, 'username': "<b>O'Brien</b>", 'position': 83})
strip = room.ringside_strip
assert strip.isWindow()
assert strip.isVisible()
assert strip.headline.text() == REACTIONS[kind][0]
assert "<b>O'Brien</b>" in strip.caption.text()
assert '01:23' in strip.metadata.text()
qtbot.wait(210)
assert QApplication.focusWidget() is room.chat_input
video = room.video_frame
rect = strip.geometry()
from PyQt6.QtCore import QRect, QPoint
bounds = QRect(video.mapToGlobal(QPoint(0, 0)), video.size())
assert bounds.contains(rect)
room.resize(1150, 650)
qtbot.wait(50)
bounds = QRect(video.mapToGlobal(QPoint(0, 0)), video.size())
assert bounds.contains(strip.geometry())
QApplication.sendEvent(room, QEvent(QEvent.Type.Hide))
assert not strip.isVisible()
def test_strip_auto_dismiss_and_cleanup(room, qtbot):
room.activateWindow()
qtbot.wait(30)
room.handle_fight_reaction({'kind': 'ko', 'username': 'Alex', 'position': 0})
assert room.ringside_strip.isVisible()
room.ringside_strip.dismiss_timer.start(30)
qtbot.waitUntil(lambda: not room.ringside_strip.isVisible())
room.handle_fight_reaction({'kind': 'bell', 'username': 'Alex', 'position': 0})
room.cleanup()
assert not room.ringside_strip.isVisible()
assert not room.ringside_strip.dismiss_timer.isActive()
assert not room._fight_poll_timer.isActive()
def test_reactions_work_with_chat_popout_focused(room, qtbot):
room.popout_chat()
popout = room.popout_window
qtbot.addWidget(popout)
popout.activateWindow()
room.chat_input.setFocus()
qtbot.wait(50)
room.handle_fight_reaction({'kind': 'ko', 'username': 'Other', 'position': 0})
assert room.ringside_strip.isVisible()
assert QApplication.focusWidget() is room.chat_input
popout.close()
def test_arcade_ko_covers_video_then_restores_strip(room, qtbot):
room.activateWindow()
qtbot.wait(30)
room.handle_fight_reaction({'kind': 'ko', 'position': 0})
overlay = room.ringside_strip
assert overlay.size() == room.video_frame.size()
assert overlay.panel.isHidden()
assert not overlay.ko_canvas.isHidden()
assert overlay.ko_animation.state() == overlay.ko_animation.State.Running
room.handle_fight_reaction({'kind': 'bell', 'position': 0})
assert overlay.ko_canvas.isHidden()
assert not overlay.panel.isHidden()
assert overlay.width() <= 540
assert overlay.height() < room.video_frame.height()
room.cleanup()
assert overlay.ko_animation.state() == overlay.ko_animation.State.Stopped
def test_sound_is_enabled_by_default_and_hidden_effects_are_suppressed(room, qtbot, monkeypatch):
play = Mock()
monkeypatch.setattr(room, '_play_reaction_sound', play)
room.activateWindow()
qtbot.wait(30)
room.handle_fight_reaction({'kind': 'ko', 'position': 0})
play.assert_called_once_with('ko')
play.reset_mock()
room.handle_fight_reaction({'kind': 'bell', 'position': 0})
play.assert_called_once_with('bell')
room.hide()
room.handle_fight_reaction({'kind': 'tap', 'position': 0})
play.assert_called_once_with('bell')
assert not room.ringside_strip.isVisible()
@pytest.mark.parametrize('kind', ['ko', 'bell'])
def test_receiving_client_shows_reaction_without_focus(room, qtbot, monkeypatch, kind):
from PyQt6.QtWidgets import QLineEdit
other_client = QLineEdit()
qtbot.addWidget(other_client)
other_client.show()
other_client.activateWindow()
other_client.setFocus()
qtbot.wait(30)
assert not room.isActiveWindow()
play = Mock()
monkeypatch.setattr(room, '_play_reaction_sound', play)
room.handle_fight_reaction({'kind': kind, 'username': 'Other client', 'position': 0})
assert room.ringside_strip.isVisible()
play.assert_called_once_with(kind)
assert QApplication.focusWidget() is other_client
QApplication.sendEvent(room, QEvent(QEvent.Type.WindowDeactivate))
assert room.ringside_strip.isVisible()
room.showMinimized()
qtbot.wait(30)
assert not room.ringside_strip.isVisible()
@pytest.mark.parametrize('kind', REACTIONS)
def test_original_sound_files_are_valid_and_loadable(qtbot, tmp_path, kind):
path = write_reaction_sound(tmp_path, kind)
assert write_reaction_sound(tmp_path, kind) == path
with wave.open(str(path)) as audio:
assert audio.getnchannels() == 1
assert audio.getsampwidth() == 2
assert audio.getframerate() == 22050
assert .35 <= audio.getnframes() / 22050 <= .9
assert any(audio.readframes(audio.getnframes()))
sound = QSoundEffect()
sound.setSource(QUrl.fromLocalFile(str(path)))
qtbot.waitUntil(lambda: sound.status() in (QSoundEffect.Status.Ready, QSoundEffect.Status.Error))
assert sound.status() == QSoundEffect.Status.Ready
def test_robbery_vote_controls_expiry_and_reconnect(room, qtbot):
poll = {'id': 'vote-1', 'active': True, 'expiresAt': time.time() * 1000 + 30000,
'votes': {'Other': 'yes'}}
room.update_fight_poll(poll)
assert not room.fight_poll_panel.isHidden()
assert 'Absolutely 1' in room.fight_poll_result.text()
with qtbot.waitSignal(room.fight_action_requested) as event:
room.fight_no_btn.click()
assert event.args == [{'type': 'fight_poll_vote', 'id': 'vote-1', 'choice': 'no'}]
poll['votes']['Me'] = 'no'
room.update_fight_poll(poll)
assert 'Fair decision 1' in room.fight_poll_result.text()
assert 'Your vote is in' in room.fight_poll_result.text()
room.update_connection_status(False)
assert not room.fight_yes_btn.isEnabled()
room.update_connection_status(True)
assert room.fight_yes_btn.isEnabled()
poll['expiresAt'] = time.time() * 1000 - 1000
room.update_fight_poll(poll)
assert 'Vote closed' in room.fight_poll_title.text()
assert not room.fight_yes_btn.isEnabled()
assert not room._fight_poll_timer.isActive()
room.reset_fight_reactions()
assert room.fight_poll_panel.isHidden()
def test_styled_vote_controls_fit_and_dismiss_stays_dismissed(room, qtbot):
from main import VlcSyncApp
VlcSyncApp.apply_stylesheet(room)
room.splitter.setSizes([750, 350])
poll = {'id': 'styled', 'active': True, 'expiresAt': time.time() * 1000 + 30000, 'votes': {}}
room.update_fight_poll(poll)
qtbot.wait(50)
for button in (room.fight_yes_btn, room.fight_no_btn, room.fight_poll_dismiss_btn):
assert button.width() >= button.fontMetrics().horizontalAdvance(button.text()) + 10
room.fight_poll_dismiss_btn.click()
room.update_fight_poll(poll)
assert room.fight_poll_panel.isHidden()
room.update_fight_poll({**poll, 'id': 'new-poll'})
assert not room.fight_poll_panel.isHidden()