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': "O'Brien", 'position': 83}) strip = room.ringside_strip assert strip.isWindow() assert strip.isVisible() assert strip.headline.text() == REACTIONS[kind][0] assert "O'Brien" 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()