Skip to content

Commit dbce28c

Browse files
authored
Merge pull request #771 from OpenTrafficCam/task/8376-write-test-to-filter-displayed-trackss
Task/8376 write test to filter displayed trackss
2 parents b12710e + 3ece14b commit dbce28c

30 files changed

Lines changed: 1974 additions & 96 deletions

OTAnalytics/adapter_ui/dummy_viewmodel.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from pathlib import Path
55
from time import sleep
6-
from typing import Any, Dict, Iterable, List, Optional
6+
from typing import Any, Dict, Iterable, List, Optional, cast
77

88
from OTAnalytics.adapter_ui.abstract_button_quick_save_config import (
99
AbstractButtonQuickSaveConfig,
@@ -370,7 +370,7 @@ def _update_enabled_general_buttons(self) -> None:
370370
frame.set_enabled_general_buttons(general_buttons_enabled)
371371

372372
def _get_frames(self) -> list[AbstractFrame | AbstractFrameProject]:
373-
return [
373+
frames: list[AbstractFrame | AbstractFrameProject] = [
374374
self.frame_tracks,
375375
self.frame_offset,
376376
self.frame_videos,
@@ -379,6 +379,9 @@ def _get_frames(self) -> list[AbstractFrame | AbstractFrameProject]:
379379
self.frame_flows,
380380
self.frame_analysis,
381381
]
382+
if self._frame_filter is not None:
383+
frames.append(cast(AbstractFrame, self._frame_filter))
384+
return frames
382385

383386
def _update_enabled_track_buttons(self) -> None:
384387
action_running = self._application.action_state.action_running.get()
@@ -814,7 +817,10 @@ def get_selected_section_ids(self) -> list[str]:
814817
@action
815818
async def load_tracks(self) -> None:
816819
track_files = await self._ui_factory.askopenfilenames(
817-
title="Load track files", filetypes=[("tracks file", "*.ottrk")]
820+
title="Load track files",
821+
filetypes=[
822+
("tracks file", "*.ottrk"),
823+
],
818824
)
819825
if not track_files:
820826
return

OTAnalytics/helpers/file_helpers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ def get_all_files_with_correct_file_ending_in_directory(
1212
for file in files:
1313
file_to_save = file.expanduser()
1414
if file_to_save.is_dir():
15+
# Treat file_type as a glob pattern (e.g., "*.ottrk", "*.tracks.csv")
1516
files_in_directory = file_to_save.rglob(file_type)
1617
files_to_save.update(files_in_directory)
1718
continue
1819

19-
if not file_to_save.exists() or file_to_save.suffix != file_type:
20+
if not file_to_save.exists():
21+
logger().warning(f"File '{file_to_save}' does not exist. Skipping.")
22+
continue
23+
24+
if not file_to_save.match(file_type):
2025
logger().warning(
21-
f"Ottrk file'{file_to_save}' does not exist. Skipping file."
26+
f"File '{file_to_save}' does not match expected pattern '{file_type}'. Skipping." # noqa
2227
)
2328
continue
2429

OTAnalytics/plugin_ui/nicegui_gui/dialogs/file_picker.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def __init__(
7777
# Current selected extension filter from dropdown
7878
self.current_extension_filter: Optional[List[str]] = None
7979
self.current_selected_option: str = "All File Endings"
80+
if extension_options is not None and "All Files" in self.extension_options:
81+
self.current_selected_option = "All Files"
8082

8183
# Set up current_extension_filter based on provided parameters
8284
if self.show_extension_select:
@@ -97,9 +99,10 @@ def __init__(
9799
self.current_selected_option = option_name
98100
break
99101
else:
100-
# Apply the default "All File Endings" filter
101-
self.current_extension_filter = self.extension_options.get(
102-
"All File Endings"
102+
self.current_extension_filter = (
103+
self.extension_options.get("All Files")
104+
if "All Files" in self.extension_options
105+
else self.extension_options.get("All File Endings")
103106
)
104107

105108
with self, ui.card().style("max-width: 70%; width: 100%"):
@@ -199,13 +202,12 @@ def update_grid(self) -> None:
199202
):
200203
# Filter based on current_extension_filter (handles all extension filtering)
201204
# Only apply filter if current_extension_filter is not empty
202-
paths = [
203-
p
204-
for p in paths
205-
if p.is_file()
206-
and p.suffix in self.current_extension_filter
207-
or p.is_dir()
208-
]
205+
exts: List[str] = list(self.current_extension_filter)
206+
207+
def matches_any(file_path: Path) -> bool:
208+
return any(file_path.name.endswith(ext) for ext in exts)
209+
210+
paths = [p for p in paths if (p.is_file() and matches_any(p)) or p.is_dir()]
209211
paths.sort(key=lambda p: p.name.lower())
210212
paths.sort(key=lambda p: not p.is_dir())
211213

@@ -239,8 +241,15 @@ def handle_double_click(self, e: events.GenericEventArguments) -> None:
239241
self.submit([self.path])
240242

241243
async def _handle_ok(self) -> None:
242-
rows: List[Dict[str, Any]] = await self.grid.get_selected_rows()
243-
paths: List[Path] = [self._map_to_domain(r[PATH]) for r in rows]
244+
# NiceGUI may return None when nothing is selected; normalize to an empty list
245+
rows: List[Dict[str, Any]] | list = await self.grid.get_selected_rows() or []
246+
247+
# Build paths only from valid row dicts that contain PATH
248+
paths: List[Path] = [
249+
self._map_to_domain(r[PATH])
250+
for r in rows
251+
if isinstance(r, dict) and (PATH in r)
252+
]
244253

245254
# If no rows are selected and we're in directory-only mode, use the current directory # noqa
246255
if not paths and (self.show_only_directories or self.multiple):

OTAnalytics/plugin_ui/nicegui_gui/nicegui/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,12 @@ class Alignment:
2323
center: Literal["center"] = "center"
2424
baseline: Literal["baseline"] = "baseline"
2525
stretch: Literal["stretch"] = "stretch"
26+
27+
28+
class TestIdAttributes:
29+
"""
30+
Helper class providing constants for test ID attribute names.
31+
"""
32+
33+
TEST_ID: str = "test-id"
34+
DATA_TESTID: str = "data-testid"

OTAnalytics/plugin_ui/nicegui_gui/nicegui/elements/forms.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from nicegui.elements.select import Select
1212
from nicegui.events import ValueChangeEventArguments
1313

14+
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.constants import TestIdAttributes
1415
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.table import (
1516
MissingInstanceError,
1617
)
17-
from OTAnalytics.plugin_ui.nicegui_gui.test_constants import TEST_ID
1818

1919
YEAR_MONTH_DAY_FORMAT = "%Y-%m-%d"
2020
DAY_MONTH_YEAR_FORMAT = "%d.%m.%Y"
@@ -116,7 +116,13 @@ def _apply_marker(self, element: Element) -> None:
116116
# so that Playwright-based acceptance tests can reliably select the
117117
# input elements. This mirrors the pattern used for buttons in the UI.
118118
element.mark(self.marker)
119-
element.props(f"{TEST_ID}={self.marker}")
119+
try:
120+
element.props(f"{TestIdAttributes.TEST_ID}={self.marker}")
121+
element.props(f"{TestIdAttributes.DATA_TESTID}={self.marker}")
122+
except Exception:
123+
# In case a specific element type doesn't support props, ignore.
124+
pass
125+
element.props(f"{TestIdAttributes.TEST_ID}={self.marker}")
120126

121127
def validate(self) -> bool:
122128
"""Handles the validation logic for an element.
@@ -746,8 +752,14 @@ def __internal_update(self, event: ValueChangeEventArguments) -> None:
746752
def build(self) -> None:
747753
"""Builds the UI form element."""
748754
self._instance = ui.checkbox(text=self._label_text, value=self._initial_value)
755+
# Apply marker and data-testid for Playwright's get_by_test_id compatibility
749756
if self._marker:
750757
self._instance.mark(self._marker)
758+
self._instance.props(f"{TestIdAttributes.DATA_TESTID}={self._marker}")
759+
# Apply any additional props passed in
760+
if self._props:
761+
for prop in self._props:
762+
self._instance.props(prop)
751763
if self._on_value_change:
752764
self._instance.on_value_change(self._on_value_change)
753765

OTAnalytics/plugin_ui/nicegui_gui/pages/add_track_form/container.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from OTAnalytics.plugin_ui.nicegui_gui.test_constants import TEST_ID
1717

1818
MARKER_VIDEO_TAB = "marker-video-tab"
19+
MARKER_TRACK_TAB = "marker-track-tab"
1920

2021

2122
class TrackForm:
@@ -41,7 +42,8 @@ def build(self) -> None:
4142
TrackFormKeys.TAB_VIDEO,
4243
)
4344
)
44-
# Expose a stable test-id for Playwright to open the Videos tab reliably
45+
# Expose stable test-ids for Playwright to open tabs reliably
46+
track_tab.props(f"{TEST_ID}={MARKER_TRACK_TAB}")
4547
video_tab.props(f"{TEST_ID}={MARKER_VIDEO_TAB}")
4648
with ui.tab_panels(tabs, value=track_tab).classes("w-full"):
4749
with ui.tab_panel(track_tab):

OTAnalytics/plugin_ui/nicegui_gui/pages/add_track_form/visualization_offset_slider_form.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
)
1212
from OTAnalytics.domain.geometry import RelativeOffsetCoordinate
1313
from OTAnalytics.plugin_ui.nicegui_gui.forms.offset_slider_form import OffsetSliderForm
14+
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.constants import TestIdAttributes
1415
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.button_form import ButtonForm
1516

17+
MARKER_UPDATE_OFFSET = "marker-update-offset"
18+
1619

1720
class VisualizationOffSetSliderForm(AbstractFrameOffset, ButtonForm):
1821
def __init__(
@@ -39,6 +42,10 @@ def build(self) -> Self:
3942
),
4043
on_click=self._view_model.change_track_offset_to_section_offset,
4144
)
45+
self.update_offset_button.mark(MARKER_UPDATE_OFFSET)
46+
self.update_offset_button.props(
47+
f"{TestIdAttributes.DATA_TESTID}={MARKER_UPDATE_OFFSET}"
48+
)
4249
return self
4350

4451
def on_offset_change(self) -> None:

OTAnalytics/plugin_ui/nicegui_gui/pages/visualization_filters_form/container.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from OTAnalytics.plugin_ui.customtkinter_gui.frame_filter import (
1717
InvalidDatetimeFormatError,
1818
)
19+
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.constants import TestIdAttributes
1920
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.button_form import ButtonForm
2021
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.forms import (
2122
DateTimeForm,
@@ -24,6 +25,23 @@
2425
)
2526

2627
MARKER_FILTER_BY_DATE_CHECKBOX = "marker-filter-by-date-checkbox"
28+
MARKER_FILTER_BY_DATE_BUTTON = "marker-filter-by-date-button"
29+
MARKER_FILTER_BY_DATE_APPLY_BUTTON = "marker-filter-by-date-apply"
30+
MARKER_FILTER_START_DATE_INPUT = "marker-filter-start-date"
31+
MARKER_FILTER_START_TIME_INPUT = "marker-filter-start-time"
32+
MARKER_FILTER_END_DATE_INPUT = "marker-filter-end-date"
33+
MARKER_FILTER_END_TIME_INPUT = "marker-filter-end-time"
34+
MARKER_FILTER_RANGE_LABEL = "marker-filter-range-label"
35+
36+
# Additional navigation markers for Playwright tests
37+
MARKER_PREV_DATE_BUTTON = "marker-prev-date"
38+
MARKER_NEXT_DATE_BUTTON = "marker-next-date"
39+
MARKER_PREV_SECONDS_BUTTON = "marker-prev-seconds"
40+
MARKER_NEXT_SECONDS_BUTTON = "marker-next-seconds"
41+
MARKER_PREV_FRAMES_BUTTON = "marker-prev-frames"
42+
MARKER_NEXT_FRAMES_BUTTON = "marker-next-frames"
43+
MARKER_PREV_EVENT_BUTTON = "marker-prev-event"
44+
MARKER_NEXT_EVENT_BUTTON = "marker-next-event"
2745

2846

2947
class FilterDateRangeForm:
@@ -37,10 +55,14 @@ def __init__(
3755
self._start_date = DateTimeForm(
3856
self._resource_manager.get(VisualizationFiltersKeys.LABEL_START_DATE),
3957
self._resource_manager.get(VisualizationFiltersKeys.LABEL_START_TIME),
58+
marker_date=MARKER_FILTER_START_DATE_INPUT,
59+
marker_time=MARKER_FILTER_START_TIME_INPUT,
4060
)
4161
self._end_date = DateTimeForm(
4262
self._resource_manager.get(VisualizationFiltersKeys.LABEL_END_DATE),
4363
self._resource_manager.get(VisualizationFiltersKeys.LABEL_END_TIME),
64+
marker_date=MARKER_FILTER_END_DATE_INPUT,
65+
marker_time=MARKER_FILTER_END_TIME_INPUT,
4466
)
4567

4668
def open(self) -> None:
@@ -89,10 +111,15 @@ def open(self) -> None:
89111
ui.label(first_occurrence_info_text)
90112
ui.label(last_occurrence_info_text)
91113
with ui.row():
92-
ui.button(
114+
apply_button = ui.button(
93115
self._resource_manager.get(GeneralKeys.LABEL_APPLY),
94116
on_click=self._on_apply_button_clicked,
95117
)
118+
# marker for Playwright: Apply button inside the filter-by-date dialog
119+
apply_button.mark(MARKER_FILTER_BY_DATE_APPLY_BUTTON)
120+
apply_button.props(
121+
f"{TestIdAttributes.DATA_TESTID}={MARKER_FILTER_BY_DATE_APPLY_BUTTON}" # noqa
122+
)
96123
ui.button(
97124
self._resource_manager.get(GeneralKeys.LABEL_RESET),
98125
on_click=self._on_reset_button_clicked,
@@ -174,26 +201,56 @@ def build(self) -> None:
174201
),
175202
on_click=self.open_range_dialog,
176203
)
204+
# Attach a stable test marker for Playwright selection
205+
self._filter_by_date_button.mark(MARKER_FILTER_BY_DATE_BUTTON)
206+
self._filter_by_date_button.props(
207+
f"{TestIdAttributes.DATA_TESTID}={MARKER_FILTER_BY_DATE_BUTTON}"
208+
)
177209
self._filter_by_date_button.disable()
178210

179211
self._button_previous_second = ui.button(
180212
"<", on_click=self._viewmodel.previous_second
181213
)
214+
self._button_previous_second.mark(MARKER_PREV_SECONDS_BUTTON)
215+
self._button_previous_second.props(
216+
f"{TestIdAttributes.DATA_TESTID}={MARKER_PREV_SECONDS_BUTTON}"
217+
)
182218
self._button_previous_frame = ui.button(
183219
"<", on_click=self._viewmodel.previous_frame
184220
)
221+
self._button_previous_frame.mark(MARKER_PREV_FRAMES_BUTTON)
222+
self._button_previous_frame.props(
223+
f"{TestIdAttributes.DATA_TESTID}={MARKER_PREV_FRAMES_BUTTON}"
224+
)
185225
self._button_previous_event = ui.button(
186226
"<", on_click=self._viewmodel.previous_event
187227
)
228+
self._button_previous_event.mark(MARKER_PREV_EVENT_BUTTON)
229+
self._button_previous_event.props(
230+
f"{TestIdAttributes.DATA_TESTID}={MARKER_PREV_EVENT_BUTTON}"
231+
)
188232
with ui.column():
189233
with ui.row():
190234
self._filter_by_date_button_left = ui.button(
191235
"<", on_click=self._viewmodel.switch_to_prev_date_range
192236
)
237+
self._filter_by_date_button_left.mark(MARKER_PREV_DATE_BUTTON)
238+
self._filter_by_date_button_left.props(
239+
f"{TestIdAttributes.DATA_TESTID}={MARKER_PREV_DATE_BUTTON}"
240+
)
193241
self._label_filter_by_date = ui.label()
242+
# Expose the current applied date range for Playwright assertions
243+
self._label_filter_by_date.mark(MARKER_FILTER_RANGE_LABEL)
244+
self._label_filter_by_date.props(
245+
f"{TestIdAttributes.DATA_TESTID}={MARKER_FILTER_RANGE_LABEL}"
246+
)
194247
self._filter_by_date_button_right = ui.button(
195248
">", on_click=self._viewmodel.switch_to_next_date_range
196249
)
250+
self._filter_by_date_button_right.mark(MARKER_NEXT_DATE_BUTTON)
251+
self._filter_by_date_button_right.props(
252+
f"{TestIdAttributes.DATA_TESTID}={MARKER_NEXT_DATE_BUTTON}"
253+
)
197254
self._filter_by_date_button_left.disable()
198255
self._filter_by_date_button_right.disable()
199256
self._input_seconds.build()
@@ -216,12 +273,24 @@ def build(self) -> None:
216273
self._button_next_second = ui.button(
217274
">", on_click=self._viewmodel.next_second
218275
)
276+
self._button_next_second.mark(MARKER_NEXT_SECONDS_BUTTON)
277+
self._button_next_second.props(
278+
f"{TestIdAttributes.DATA_TESTID}={MARKER_NEXT_SECONDS_BUTTON}"
279+
)
219280
self._button_next_frame = ui.button(
220281
">", on_click=self._viewmodel.next_frame
221282
)
283+
self._button_next_frame.mark(MARKER_NEXT_FRAMES_BUTTON)
284+
self._button_next_frame.props(
285+
f"{TestIdAttributes.DATA_TESTID}={MARKER_NEXT_FRAMES_BUTTON}"
286+
)
222287
self._button_next_event = ui.button(
223288
">", on_click=self._viewmodel.next_event
224289
)
290+
self._button_next_event.mark(MARKER_NEXT_EVENT_BUTTON)
291+
self._button_next_event.props(
292+
f"{TestIdAttributes.DATA_TESTID}={MARKER_NEXT_EVENT_BUTTON}"
293+
)
225294
self.set_enabled_general_buttons(False)
226295

227296
def change_button_date(self, checkbox_status: bool) -> None:
@@ -275,9 +344,13 @@ def open_range_dialog(self) -> None:
275344

276345
def set_active_color_on_filter_by_date_button(self) -> None:
277346
self._filter_by_date_button.props("color=orange")
347+
# Expose active state for Playwright assertions
348+
self._filter_by_date_button.props("data-filter-by-date-active=true")
278349

279350
def set_inactive_color_on_filter_by_date_button(self) -> None:
280351
self._filter_by_date_button.props("color=primary")
352+
# Expose inactive state for Playwright assertions
353+
self._filter_by_date_button.props("data-filter-by-date-active=false")
281354

282355
def set_active_color_on_filter_by_class_button(self) -> None:
283356
self._filter_by_class_button.props("color=orange")

0 commit comments

Comments
 (0)