5050 MARKER_VIDEO_TABLE ,
5151)
5252from OTAnalytics .plugin_ui .nicegui_gui .pages .analysis_form .container import (
53+ MARKER_BUTTON_EXPORT_COUNTS ,
5354 MARKER_BUTTON_EXPORT_TRACK_STATISTICS ,
5455)
5556from 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+
580692def 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