Skip to content

Commit 2b82165

Browse files
Add Playwright export_counts helper and compare_counts_csv_files utility; implement test for generating counts reference file.
1 parent 9bb468f commit 2b82165

3 files changed

Lines changed: 164 additions & 3 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.button_form import ButtonForm
1313
from OTAnalytics.plugin_ui.nicegui_gui.test_constants import TEST_ID
1414

15+
MARKER_BUTTON_EXPORT_COUNTS = "button-export-counts"
1516
MARKER_BUTTON_EXPORT_TRACK_STATISTICS = "button-export-track-statistics"
1617

1718

@@ -46,7 +47,7 @@ def build(self) -> None:
4647
self._button_export_counts = ui.button(
4748
self._resource_manager.get(AnalysisKeys.BUTTON_TEXT_EXPORT_COUNTS),
4849
on_click=self._viewmodel.export_counts,
49-
)
50+
).props(f"{TEST_ID}={MARKER_BUTTON_EXPORT_COUNTS}")
5051
with ui.row():
5152
self._button_export_road_user_assignment = ui.button(
5253
self._resource_manager.get(

tests/acceptance/test_create_important_test_data.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
click_update_offset_button,
4848
create_flow,
4949
create_section,
50+
export_counts,
5051
export_track_statistics,
5152
get_loaded_tracks_canvas_from_otconfig,
5253
load_main_page,
@@ -311,3 +312,36 @@ def test_generate_track_statistics_reference_file(
311312
# Copy to the main test data directory for reuse
312313
permanent_path = data_dir / "track_statistics_reference.csv"
313314
shutil.copy(output_path, permanent_path)
315+
316+
@pytest.mark.timeout(ACCEPTANCE_TEST_PYTEST_TIMEOUT)
317+
@pytest.mark.playwright
318+
@pytest.mark.usefixtures("external_app")
319+
def test_generate_counts_reference_file(
320+
self,
321+
external_app: NiceGUITestServer,
322+
page: Page,
323+
resource_manager: ResourceManager,
324+
test_data_tmp_dir: Path,
325+
) -> None:
326+
"""Generate reference counts.csv file using Desktop GUI.
327+
328+
This test:
329+
- Loads a pre-configured project with video, tracks, sections, and flows
330+
- Clicks "Export counts ..."
331+
- Uses default values in the export dialog
332+
- Saves the file
333+
- Copies it to the test data directory as reference
334+
335+
The resulting file can be used by other tests to compare exported output.
336+
"""
337+
data_dir = Path(__file__).parents[1] / "data"
338+
otconfig_path = data_dir / "sections_created_test_file.otconfig"
339+
340+
# Export counts
341+
output_path = export_counts(
342+
page, external_app, resource_manager, test_data_tmp_dir, otconfig_path
343+
)
344+
345+
# Copy to the main test data directory for reuse
346+
permanent_path = data_dir / "counts_reference.csv"
347+
shutil.copy(output_path, permanent_path)

tests/utils/playwright_helpers.py

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
MARKER_VIDEO_TABLE,
5151
)
5252
from OTAnalytics.plugin_ui.nicegui_gui.pages.analysis_form.container import (
53+
MARKER_BUTTON_EXPORT_COUNTS,
5354
MARKER_BUTTON_EXPORT_TRACK_STATISTICS,
5455
)
5556
from OTAnalytics.plugin_ui.nicegui_gui.pages.canvas_and_files_form.canvas_form import (
@@ -572,14 +573,128 @@ def export_track_statistics(
572573
return output_path
573574

574575

576+
def export_counts(
577+
page: Page,
578+
external_app: Any,
579+
resource_manager: Any,
580+
test_data_tmp_dir: Path,
581+
otconfig_path: Path,
582+
) -> Path:
583+
"""Export counts from a pre-configured project.
584+
585+
This function:
586+
- Loads the main page
587+
- Opens the specified otconfig file
588+
- Clicks the export counts button
589+
- Handles the export dialog
590+
- Returns the path to the exported file
591+
592+
Args:
593+
page: The Playwright page object
594+
external_app: The NiceGUI test server
595+
resource_manager: The resource manager for localized text
596+
test_data_tmp_dir: Directory where the file should be exported
597+
otconfig_path: Path to the otconfig file to load
598+
599+
Returns:
600+
Path to the exported counts file
601+
"""
602+
# Setup: Load tracks with preconfigured file
603+
load_main_page(page, external_app)
604+
605+
# Load the otconfig file
606+
open_project_otconfig(page, resource_manager, otconfig_path)
607+
608+
# Click "Export counts ..." button
609+
export_button = search_for_marker_element(page, MARKER_BUTTON_EXPORT_COUNTS).first
610+
export_button.click()
611+
612+
# Handle export dialog and get output path
613+
output_path = export_file_via_dialog(page, test_data_tmp_dir)
614+
615+
# Verify the file was created
616+
assert output_path.exists(), f"Counts file not created: {output_path}"
617+
618+
return output_path
619+
620+
575621
# ----------------------
576622
# File comparison helpers
577623
# ----------------------
578624

579625

626+
def compare_counts_csv_files(exported_path: Path, reference_path: Path) -> None:
627+
"""Compare two counts CSV files, ignoring the classification column.
628+
629+
The classification column is ignored because the export may return different
630+
classifications on each run depending on the track data aggregation.
631+
632+
Args:
633+
exported_path: Path to the exported CSV file
634+
reference_path: Path to the reference CSV file
635+
636+
Raises:
637+
AssertionError: If files differ (excluding classification)
638+
"""
639+
import csv
640+
641+
assert exported_path.exists(), f"Exported file not found: {exported_path}"
642+
assert reference_path.exists(), f"Reference file not found: {reference_path}"
643+
644+
with open(exported_path, "r", encoding="utf-8") as exported_file:
645+
exported_reader = csv.DictReader(exported_file)
646+
exported_rows = list(exported_reader)
647+
648+
with open(reference_path, "r", encoding="utf-8") as reference_file:
649+
reference_reader = csv.DictReader(reference_file)
650+
reference_rows = list(reference_reader)
651+
652+
# Compare headers
653+
assert exported_reader.fieldnames == reference_reader.fieldnames, (
654+
f"Header mismatch:\n"
655+
f" Exported: {exported_reader.fieldnames}\n"
656+
f" Reference: {reference_reader.fieldnames}"
657+
)
658+
659+
# Ensure fieldnames is not None
660+
assert exported_reader.fieldnames is not None, "Exported CSV has no header"
661+
assert reference_reader.fieldnames is not None, "Reference CSV has no header"
662+
663+
# Compare row count
664+
assert len(exported_rows) == len(reference_rows), (
665+
f"Row count mismatch: exported={len(exported_rows)}, "
666+
f"reference={len(reference_rows)}"
667+
)
668+
669+
# Compare all columns except 'classification'
670+
columns_to_compare = [
671+
col for col in exported_reader.fieldnames if col != "classification"
672+
]
673+
674+
# Create tuples of values for comparison (excluding classification)
675+
exported_data = sorted(
676+
[tuple(row[col] for col in columns_to_compare) for row in exported_rows]
677+
)
678+
reference_data = sorted(
679+
[tuple(row[col] for col in columns_to_compare) for row in reference_rows]
680+
)
681+
682+
for i, (exported_vals, reference_vals) in enumerate(
683+
zip(exported_data, reference_data)
684+
):
685+
assert exported_vals == reference_vals, (
686+
f"Row {i + 1} mismatch (after sorting, excluding classification):\n"
687+
f" Exported: {dict(zip(columns_to_compare, exported_vals))}\n"
688+
f" Reference: {dict(zip(columns_to_compare, reference_vals))}"
689+
)
690+
691+
580692
def compare_csv_files(exported_path: Path, reference_path: Path) -> None:
581693
"""Compare two CSV files for equality.
582694
695+
Compares CSV files by sorting rows (excluding header) before comparison
696+
to handle potential ordering differences between exports.
697+
583698
Args:
584699
exported_path: Path to the exported CSV file
585700
reference_path: Path to the reference CSV file
@@ -605,11 +720,22 @@ def compare_csv_files(exported_path: Path, reference_path: Path) -> None:
605720
f"reference={len(reference_rows)}"
606721
)
607722

723+
# Compare headers (first row)
724+
assert exported_rows[0] == reference_rows[0], (
725+
f"Header mismatch:\n"
726+
f" Exported: {exported_rows[0]}\n"
727+
f" Reference: {reference_rows[0]}"
728+
)
729+
730+
# Sort data rows (excluding header) for consistent comparison
731+
exported_data = sorted(exported_rows[1:])
732+
reference_data = sorted(reference_rows[1:])
733+
608734
for i, (exported_row, reference_row) in enumerate(
609-
zip(exported_rows, reference_rows)
735+
zip(exported_data, reference_data)
610736
):
611737
assert exported_row == reference_row, (
612-
f"Row {i} mismatch:\n"
738+
f"Row {i + 1} mismatch (after sorting):\n"
613739
f" Exported: {exported_row}\n"
614740
f" Reference: {reference_row}"
615741
)

0 commit comments

Comments
 (0)