Skip to content

Commit b81ea09

Browse files
authored
UI integration tests setup (#31)
* add find child util function * create test fixtures for UI testing * Create test_window_gui.py * Update pytest.yml * uxe xvfb * Update pytest.yml * Update pytest.yml * Update pytest.yml * Update pytest.yml * temp break * repair * Update conftest.py * rename test_app_controls * make find_child_by_attribute generic * StyleTextEdit debounce ms attribute * Create test_initiative_controls.py * Update test_initiative_controls.py * rename app_instance * use a pyside6 Signal for text debouncing * use qtbot.waitSignal in test
1 parent fe21cd1 commit b81ea09

File tree

8 files changed

+109
-20
lines changed

8 files changed

+109
-20
lines changed

.github/workflows/pytest.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,37 @@ permissions:
1111

1212
jobs:
1313
build:
14-
1514
runs-on: ubuntu-latest
15+
env:
16+
# Display must be available globally for linux to know where xvfb is
17+
DISPLAY: ":99.0"
18+
QT_SELECT: "qt6"
1619

1720
steps:
1821
- uses: actions/checkout@v3
1922

20-
- name: Install Pyside6 dependencies
23+
- name: Install Linux dependencies
2124
run: |
22-
sudo apt update && sudo apt install -y libegl1-mesa-dev
25+
# Copied from https://stackoverflow.com/a/77480795/4082914
26+
sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 \
27+
libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 \
28+
libxcb-xfixes0 libxcb-shape0 libglib2.0-0 libgl1-mesa-dev
29+
30+
sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev \
31+
libxi-dev libxkbcommon-dev libxkbcommon-x11-dev
32+
33+
# start xvfb in the background
34+
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
2335
2436
- name: Set up Python 3
2537
uses: actions/setup-python@v3
2638

27-
- name: Install dependencies
39+
- name: Install Python dependencies
2840
run: |
2941
python -m pip install --upgrade pip
3042
pip install -r requirements.txt
31-
pip install pytest
43+
pip install pytest pytest-qt
3244
3345
- name: Run tests with Pytest
3446
run: |
35-
pytest .
47+
pytest tests

battle_map_tv/layouts/initiative_controls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self):
1818
super().__init__()
1919
self.image_window = get_image_window()
2020
self.setPlaceholderText("Display initiative order")
21-
self.on_text_changed(self.callback)
21+
self.textChangedDebounced.connect(self.callback)
2222

2323
def callback(self):
2424
text = self.toPlainText().strip()

battle_map_tv/utils.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import TYPE_CHECKING, Optional, Tuple
1+
from typing import TYPE_CHECKING, Optional, Tuple, Type
22

3-
from PySide6.QtCore import QSize
3+
from PySide6.QtCore import QObject, QSize
44
from PySide6.QtWidgets import QApplication
55

66
if TYPE_CHECKING:
@@ -31,3 +31,16 @@ def get_image_window_size_px() -> Tuple[int, int]:
3131
def get_image_filename() -> Optional[str]:
3232
image_window = get_image_window()
3333
return image_window.image.image_filename if image_window.image else None
34+
35+
36+
def find_child_by_attribute(parent: QObject, child_type: Type[QObject], text: Optional[str] = None):
37+
child: QObject
38+
for child in parent.findChildren(child_type):
39+
if text:
40+
if child.text() == text: # type: ignore
41+
return child
42+
else:
43+
return child
44+
raise AttributeError(
45+
f"Could not find child of type {child_type} with text '{text}' in {parent}"
46+
)

battle_map_tv/widgets/text_based.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
from typing import Callable
2-
3-
from PySide6.QtCore import QTimer
1+
from PySide6.QtCore import QTimer, Signal
42
from PySide6.QtWidgets import QLineEdit, QTextEdit
53

64

@@ -35,6 +33,9 @@ def __init__(
3533

3634

3735
class StyledTextEdit(QTextEdit):
36+
text_changed_debounce_ms = 700
37+
textChangedDebounced = Signal()
38+
3839
def __init__(self):
3940
super().__init__()
4041
self.setStyleSheet(
@@ -46,13 +47,11 @@ def __init__(self):
4647
border-radius: 6px;
4748
"""
4849
)
50+
self._typing_timer = QTimer()
51+
self._typing_timer.setSingleShot(True)
52+
self._typing_timer.timeout.connect(self.textChangedDebounced.emit)
4953

50-
def on_text_changed(self, callback: Callable):
51-
typing_timer = QTimer()
52-
typing_timer.setSingleShot(True)
53-
typing_timer.timeout.connect(callback)
54-
55-
def reset_typing_timer():
56-
typing_timer.start(700)
54+
self.textChanged.connect(self._reset_typing_timer)
5755

58-
self.textChanged.connect(reset_typing_timer)
56+
def _reset_typing_timer(self):
57+
self._typing_timer.start(self.text_changed_debounce_ms)

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ hatch-vcs
33
mypy
44
pre-commit
55
pytest
6+
pytest-qt
67
ruff

tests/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
from PySide6.QtWidgets import QApplication
3+
4+
from battle_map_tv.window_gui import GuiWindow
5+
from battle_map_tv.window_image import ImageWindow
6+
7+
8+
@pytest.fixture
9+
def app_instance(qtbot):
10+
app = QApplication.instance() or QApplication([])
11+
return app
12+
13+
14+
@pytest.fixture
15+
def image_window(app_instance, qtbot):
16+
image_window = ImageWindow()
17+
qtbot.addWidget(image_window)
18+
image_window.move(image_window.screen().geometry().center())
19+
yield image_window
20+
image_window.close()
21+
22+
23+
@pytest.fixture
24+
def gui_window(app_instance, image_window, qtbot):
25+
gui_window = GuiWindow()
26+
qtbot.addWidget(gui_window)
27+
gui_window.move(gui_window.screen().geometry().topLeft())
28+
yield gui_window
29+
gui_window.close()

tests/ui/test_app_controls.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from PySide6.QtCore import Qt
2+
from PySide6.QtWidgets import QPushButton
3+
4+
from battle_map_tv.utils import find_child_by_attribute
5+
6+
7+
def test_fullscreen_button(image_window, gui_window, qtbot):
8+
fullscreen_button = find_child_by_attribute(gui_window, QPushButton, "Fullscreen")
9+
assert fullscreen_button
10+
11+
# Check if the fullscreen mode is toggled
12+
qtbot.mouseClick(fullscreen_button, Qt.LeftButton) # type: ignore
13+
assert image_window.isFullScreen()
14+
15+
# now click again to make it normal again
16+
qtbot.mouseClick(fullscreen_button, Qt.LeftButton) # type: ignore
17+
assert not image_window.isFullScreen()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from battle_map_tv.layouts.initiative_controls import InitiativeTextArea
2+
from battle_map_tv.utils import find_child_by_attribute
3+
4+
5+
def test_initiative_text_area_updates_image_window(image_window, gui_window, qtbot):
6+
text_area = find_child_by_attribute(parent=gui_window, child_type=InitiativeTextArea)
7+
text_area.setFocus()
8+
assert len(image_window.initiative_overlay_manager.overlays) == 0
9+
10+
# Simulate writing text in the text area
11+
with qtbot.waitSignal(text_area.textChangedDebounced, timeout=5000):
12+
qtbot.keyClicks(text_area, "Test Initiative Order")
13+
assert len(image_window.initiative_overlay_manager.overlays) == 2
14+
15+
# Simulate clearing the text area
16+
with qtbot.waitSignal(text_area.textChangedDebounced, timeout=5000):
17+
qtbot.keyClicks(text_area, "\b" * len("Test Initiative Order"))
18+
assert len(image_window.initiative_overlay_manager.overlays) == 0

0 commit comments

Comments
 (0)