From a35421ccfcd39b352cc6a3cd09d878d6540e0d82 Mon Sep 17 00:00:00 2001 From: Francesco Grazioso Date: Fri, 13 Jun 2025 17:11:45 +0200 Subject: [PATCH] Add GUI tests and test runner for StreamingCommunity application --- Test/GUI/README.md | 92 +++++++++++++++ Test/GUI/run_tests.py | 61 ++++++++++ Test/GUI/test_integration.py | 156 ++++++++++++++++++++++++++ Test/GUI/test_main_window.py | 93 ++++++++++++++++ Test/GUI/test_results_table.py | 116 +++++++++++++++++++ Test/GUI/test_run_tab.py | 185 +++++++++++++++++++++++++++++++ Test/GUI/test_site_manager.py | 67 +++++++++++ Test/GUI/test_stream_redirect.py | 60 ++++++++++ 8 files changed, 830 insertions(+) create mode 100644 Test/GUI/README.md create mode 100755 Test/GUI/run_tests.py create mode 100644 Test/GUI/test_integration.py create mode 100644 Test/GUI/test_main_window.py create mode 100644 Test/GUI/test_results_table.py create mode 100644 Test/GUI/test_run_tab.py create mode 100644 Test/GUI/test_site_manager.py create mode 100644 Test/GUI/test_stream_redirect.py diff --git a/Test/GUI/README.md b/Test/GUI/README.md new file mode 100644 index 0000000..b7aa7f5 --- /dev/null +++ b/Test/GUI/README.md @@ -0,0 +1,92 @@ +# GUI Tests + +This directory contains tests for the GUI components of the StreamingCommunity application. + +## Test Files + +- `test_main_window.py`: Tests for the main window class (`StreamingGUI`) +- `test_run_tab.py`: Tests for the run tab component (`RunTab`) +- `test_results_table.py`: Tests for the results table widget (`ResultsTable`) +- `test_stream_redirect.py`: Tests for the stdout redirection utility (`Stream`) +- `test_site_manager.py`: Tests for the site manager utility +- `test_integration.py`: Integration tests for all GUI components working together + +## Running the Tests + +### Using the Test Runner + +The easiest way to run the tests is to use the included test runner script: + +```bash +# Run all tests +cd Test/GUI +python run_tests.py + +# Run specific test files +python run_tests.py test_main_window.py test_run_tab.py + +# Run with different verbosity level (1-3) +python run_tests.py -v 3 +``` + +### Using unittest Directly + +You can also run the tests using the standard unittest module: + +```bash +# Run all GUI tests +python -m unittest discover -s Test/GUI + +# Run individual test files +python -m unittest Test/GUI/test_main_window.py +python -m unittest Test/GUI/test_run_tab.py +python -m unittest Test/GUI/test_results_table.py +python -m unittest Test/GUI/test_stream_redirect.py +python -m unittest Test/GUI/test_site_manager.py +python -m unittest Test/GUI/test_integration.py +``` + +## Test Coverage + +The tests cover the following aspects of the GUI: + +1. **Basic Initialization** + - Proper initialization of all GUI components + - Correct parent-child relationships + - Default states of widgets + +2. **UI Creation** + - Correct creation of all UI elements + - Proper layout of widgets + - Initial visibility states + +3. **Widget Interactions** + - Button clicks + - Checkbox toggles + - Table updates + +4. **Process Execution** + - Script execution + - Process termination + - Status updates + +5. **Integration** + - Components working together correctly + - Signal-slot connections + - Data flow between components + +## Adding New Tests + +When adding new GUI components, please add corresponding tests following the same pattern as the existing tests. Each test file should: + +1. Import the necessary modules +2. Create a test class that inherits from `unittest.TestCase` +3. Include setup and teardown methods +4. Test all aspects of the component's functionality +5. Include a main block to run the tests when the file is executed directly + +## Notes + +- The tests use `unittest.mock` to mock external dependencies like `QProcess` +- A `QApplication` instance is created in the `setUpClass` method to ensure that PyQt widgets can be created +- The tests clean up resources in the `tearDown` method to prevent memory leaks diff --git a/Test/GUI/run_tests.py b/Test/GUI/run_tests.py new file mode 100755 index 0000000..c563e61 --- /dev/null +++ b/Test/GUI/run_tests.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +import argparse + +def run_tests(verbosity=2, test_names=None): + """Run the GUI tests with the specified verbosity level.""" + # Create a test loader + loader = unittest.TestLoader() + + # If specific test names are provided, run only those tests + if test_names: + suite = unittest.TestSuite() + for test_name in test_names: + # Try to load the test module + try: + if test_name.endswith('.py'): + test_name = test_name[:-3] # Remove .py extension + + # If the test name is a module name, load all tests from that module + if test_name.startswith('test_'): + module = __import__(test_name) + suite.addTests(loader.loadTestsFromModule(module)) + else: + # Otherwise, assume it's a test class or method name + suite.addTests(loader.loadTestsFromName(test_name)) + except (ImportError, AttributeError) as e: + print(f"Error loading test {test_name}: {e}") + return False + else: + # Otherwise, discover all tests in the current directory + suite = loader.discover('.', pattern='test_*.py') + + # Run the tests + runner = unittest.TextTestRunner(verbosity=verbosity) + result = runner.run(suite) + + # Return True if all tests passed, False otherwise + return result.wasSuccessful() + +if __name__ == '__main__': + # Parse command line arguments + parser = argparse.ArgumentParser(description='Run GUI tests for StreamingCommunity') + parser.add_argument('-v', '--verbosity', type=int, default=2, + help='Verbosity level (1-3, default: 2)') + parser.add_argument('test_names', nargs='*', + help='Specific test modules, classes, or methods to run') + args = parser.parse_args() + + # Run the tests + success = run_tests(args.verbosity, args.test_names) + + # Exit with appropriate status code + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/Test/GUI/test_integration.py b/Test/GUI/test_integration.py new file mode 100644 index 0000000..a2c4954 --- /dev/null +++ b/Test/GUI/test_integration.py @@ -0,0 +1,156 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock +from PyQt5.QtWidgets import QApplication +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt, QProcess + +from gui.main_window import StreamingGUI +from gui.tabs.run_tab import RunTab +from gui.widgets.results_table import ResultsTable +from gui.utils.stream_redirect import Stream +from gui.utils.site_manager import sites + +class TestGUIIntegration(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of StreamingGUI for each test + self.gui = StreamingGUI() + + def tearDown(self): + # Clean up after each test + self.gui.close() + self.gui = None + + def test_main_window_has_run_tab(self): + """Test that the main window has a RunTab instance""" + self.assertIsInstance(self.gui.run_tab, RunTab) + + def test_run_tab_has_results_table(self): + """Test that the RunTab has a ResultsTable instance""" + self.assertIsInstance(self.gui.run_tab.results_table, ResultsTable) + + def test_stdout_redirection(self): + """Test that stdout is redirected to the RunTab's output_text""" + # Get the original stdout + original_stdout = sys.stdout + + # Check that stdout is now a Stream instance + self.assertIsInstance(sys.stdout, Stream) + + # Check that the Stream's newText signal is connected to the RunTab's update_output method + # We can't directly check the connections, but we can test the behavior + + # First, make the output_text visible + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + self.gui.run_tab.toggle_console(Qt.Checked) + + # Then, print something + print("Test message") + + # Check that the message appears in the output_text + self.assertIn("Test message", self.gui.run_tab.output_text.toPlainText()) + + # Restore the original stdout for cleanup + sys.stdout = original_stdout + + @patch('gui.tabs.run_tab.QProcess') + def test_run_script_integration(self, mock_qprocess): + """Test that the run_script method integrates with other components""" + # Set up the mock QProcess + mock_process = MagicMock() + mock_qprocess.return_value = mock_process + + # Set some search terms + self.gui.run_tab.search_terms.setText("test search") + + # Call the run_script method + self.gui.run_tab.run_script() + + # Check that the process was created + self.assertIsNotNone(self.gui.run_tab.process) + + # Check that the run_button is disabled + self.assertFalse(self.gui.run_tab.run_button.isEnabled()) + + # Check that the stop_button is enabled + self.assertTrue(self.gui.run_tab.stop_button.isEnabled()) + + # Check that the process was started with the correct arguments + mock_process.start.assert_called_once() + args = mock_process.start.call_args[0][1] + self.assertIn("run_streaming.py", args[0]) + self.assertIn("-s", args[1:]) + self.assertIn("test search", args[1:]) + + def test_toggle_console_integration(self): + """Test that the toggle_console method integrates with other components""" + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + + # Initially, the console_checkbox should be unchecked + self.assertFalse(self.gui.run_tab.console_checkbox.isChecked()) + + # Check the console_checkbox + self.gui.run_tab.console_checkbox.setChecked(True) + + # Call the toggle_console method directly with the checked state + self.gui.run_tab.toggle_console(Qt.Checked) + + # Uncheck the console_checkbox + self.gui.run_tab.console_checkbox.setChecked(False) + + # Call the toggle_console method directly with the unchecked state + self.gui.run_tab.toggle_console(Qt.Unchecked) + + def test_stop_script_integration(self): + """Test that the stop_script method integrates with other components""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess as ActualQProcess + + # Create a mock process + mock_process = MagicMock() + + # Set the process state to Running + mock_process.state.return_value = ActualQProcess.Running + + # Mock the waitForFinished method to return True + mock_process.waitForFinished.return_value = True + + # Set the process directly + self.gui.run_tab.process = mock_process + + # Enable the stop button + self.gui.run_tab.stop_button.setEnabled(True) + + # Disable the run button + self.gui.run_tab.run_button.setEnabled(False) + + # Stop the script + self.gui.run_tab.stop_script() + + # Check that the process was terminated + mock_process.terminate.assert_called_once() + + # Check that waitForFinished was called + mock_process.waitForFinished.assert_called_once_with(3000) + + # Check that the run_button is enabled + self.assertTrue(self.gui.run_tab.run_button.isEnabled()) + + # Check that the stop_button is disabled + self.assertFalse(self.gui.run_tab.stop_button.isEnabled()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_main_window.py b/Test/GUI/test_main_window.py new file mode 100644 index 0000000..16701e9 --- /dev/null +++ b/Test/GUI/test_main_window.py @@ -0,0 +1,93 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import MagicMock +from PyQt5.QtWidgets import QApplication + +from gui.main_window import StreamingGUI + +class TestMainWindow(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of StreamingGUI for each test + self.gui = StreamingGUI() + + def tearDown(self): + # Clean up after each test + self.gui.close() + self.gui = None + + def test_init(self): + """Test that the main window initializes correctly""" + # Check that the window title is set correctly + self.assertEqual(self.gui.windowTitle(), "StreamingCommunity GUI") + + # Check that the window size is set correctly + self.assertEqual(self.gui.geometry().width(), 1000) + self.assertEqual(self.gui.geometry().height(), 700) + + # Check that the run_tab is created + self.assertIsNotNone(self.gui.run_tab) + + # Check that the stdout_stream is set up + self.assertIsNotNone(self.gui.stdout_stream) + + def test_close_event(self): + """Test that the close event handler works correctly""" + # Mock the process + self.gui.process = MagicMock() + self.gui.process.state.return_value = QApplication.instance().startingUp() # Not running + + # Create a mock event + event = MagicMock() + + # Call the close event handler + self.gui.closeEvent(event) + + # Check that the event was accepted + event.accept.assert_called_once() + + # Check that sys.stdout was restored + self.assertEqual(sys.stdout, sys.__stdout__) + + def test_close_event_with_running_process(self): + """Test that the close event handler terminates running processes""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess + + # Mock the process as running + self.gui.process = MagicMock() + self.gui.process.state.return_value = QProcess.Running + + # Mock the waitForFinished method to return True + self.gui.process.waitForFinished.return_value = True + + # Create a mock event + event = MagicMock() + + # Call the close event handler + self.gui.closeEvent(event) + + # Check that terminate was called + self.gui.process.terminate.assert_called_once() + + # Check that waitForFinished was called + self.gui.process.waitForFinished.assert_called_once_with(1000) + + # Check that the event was accepted + event.accept.assert_called_once() + + # Check that sys.stdout was restored + self.assertEqual(sys.stdout, sys.__stdout__) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_results_table.py b/Test/GUI/test_results_table.py new file mode 100644 index 0000000..3a7ac5a --- /dev/null +++ b/Test/GUI/test_results_table.py @@ -0,0 +1,116 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from PyQt5.QtWidgets import QApplication, QTableWidgetItem +from PyQt5.QtCore import Qt + +from gui.widgets.results_table import ResultsTable + +class TestResultsTable(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of ResultsTable for each test + self.results_table = ResultsTable() + + def tearDown(self): + # Clean up after each test + self.results_table.close() + self.results_table = None + + def test_init(self): + """Test that the ResultsTable initializes correctly""" + # Check that the table is hidden initially + self.assertFalse(self.results_table.isVisible()) + + # Check that the table is not editable + self.assertEqual(self.results_table.editTriggers(), ResultsTable.NoEditTriggers) + + # Check that the table has no selection + self.assertEqual(self.results_table.selectionMode(), ResultsTable.NoSelection) + + # Check that the table has no focus + self.assertEqual(self.results_table.focusPolicy(), Qt.NoFocus) + + # Check that the table has no drag and drop + self.assertEqual(self.results_table.dragDropMode(), ResultsTable.NoDragDrop) + + # Check that the table has no context menu + self.assertEqual(self.results_table.contextMenuPolicy(), Qt.NoContextMenu) + + # Check that the vertical header is hidden + self.assertFalse(self.results_table.verticalHeader().isVisible()) + + # Check that the table is disabled + self.assertFalse(self.results_table.isEnabled()) + + def test_update_with_seasons(self): + """Test that the update_with_seasons method works correctly""" + # Call the method with 3 seasons + self.results_table.update_with_seasons(3) + + # Check that the table has 2 columns + self.assertEqual(self.results_table.columnCount(), 2) + + # Check that the table has 3 rows + self.assertEqual(self.results_table.rowCount(), 3) + + # Check that the column headers are set correctly + self.assertEqual(self.results_table.horizontalHeaderItem(0).text(), "Index") + self.assertEqual(self.results_table.horizontalHeaderItem(1).text(), "Season") + + # Check that the table cells are set correctly + for i in range(3): + self.assertEqual(self.results_table.item(i, 0).text(), str(i + 1)) + self.assertEqual(self.results_table.item(i, 1).text(), f"Stagione {i + 1}") + + # Check that the items are not editable + self.assertEqual(self.results_table.item(i, 0).flags(), Qt.ItemIsEnabled) + self.assertEqual(self.results_table.item(i, 1).flags(), Qt.ItemIsEnabled) + + # Check that the table is visible + self.assertTrue(self.results_table.isVisible()) + + def test_update_with_results(self): + """Test that the update_with_results method works correctly""" + # Define headers and rows + headers = ["Column 1", "Column 2", "Column 3"] + rows = [ + ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"], + ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"], + ] + + # Call the method + self.results_table.update_with_results(headers, rows) + + # Check that the table has the correct number of columns + self.assertEqual(self.results_table.columnCount(), len(headers)) + + # Check that the table has the correct number of rows + self.assertEqual(self.results_table.rowCount(), len(rows)) + + # Check that the column headers are set correctly + for i, header in enumerate(headers): + self.assertEqual(self.results_table.horizontalHeaderItem(i).text(), header) + + # Check that the table cells are set correctly + for i, row in enumerate(rows): + for j, cell in enumerate(row): + self.assertEqual(self.results_table.item(i, j).text(), cell) + + # Check that the items are not editable + self.assertEqual(self.results_table.item(i, j).flags(), Qt.ItemIsEnabled) + + # Check that the table is visible + self.assertTrue(self.results_table.isVisible()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_run_tab.py b/Test/GUI/test_run_tab.py new file mode 100644 index 0000000..aa720eb --- /dev/null +++ b/Test/GUI/test_run_tab.py @@ -0,0 +1,185 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtCore import Qt, QProcess + +from gui.tabs.run_tab import RunTab +from gui.utils.site_manager import sites + +class TestRunTab(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a parent widget and a fresh instance of RunTab for each test + self.parent = QWidget() + self.run_tab = RunTab(self.parent) + + def tearDown(self): + # Clean up after each test + self.run_tab.close() + self.parent.close() + self.run_tab = None + self.parent = None + + def test_init(self): + """Test that the RunTab initializes correctly""" + # Check that the parent is set correctly + self.assertEqual(self.run_tab.parent, self.parent) + + # Check that the process is None initially + self.assertIsNone(self.run_tab.process) + + # Check that the current_context is None initially + self.assertIsNone(self.run_tab.current_context) + + # Check that the selected_season is None initially + self.assertIsNone(self.run_tab.selected_season) + + # Check that the buffer is empty initially + self.assertEqual(self.run_tab.buffer, "") + + def test_create_search_group(self): + """Test that the search group is created correctly""" + # Get the search group + search_group = self.run_tab.create_search_group() + + # Check that the search_terms QLineEdit is created + self.assertIsNotNone(self.run_tab.search_terms) + + # Check that the site_combo QComboBox is created and populated + self.assertIsNotNone(self.run_tab.site_combo) + self.assertEqual(self.run_tab.site_combo.count(), len(sites)) + + # Check that the first site is selected by default + if len(sites) > 0: + self.assertEqual(self.run_tab.site_combo.currentIndex(), 0) + + def test_create_control_layout(self): + """Test that the control layout is created correctly""" + # Get the control layout + control_layout = self.run_tab.create_control_layout() + + # Check that the run_button is created + self.assertIsNotNone(self.run_tab.run_button) + + # Check that the stop_button is created and disabled initially + self.assertIsNotNone(self.run_tab.stop_button) + self.assertFalse(self.run_tab.stop_button.isEnabled()) + + # Check that the console_checkbox is created and unchecked initially + self.assertIsNotNone(self.run_tab.console_checkbox) + self.assertFalse(self.run_tab.console_checkbox.isChecked()) + + def test_create_output_group(self): + """Test that the output group is created correctly""" + # Get the output group + output_group = self.run_tab.create_output_group() + + # Check that the results_table is created + self.assertIsNotNone(self.run_tab.results_table) + + # Check that the output_text is created and hidden initially + self.assertIsNotNone(self.run_tab.output_text) + self.assertFalse(self.run_tab.output_text.isVisible()) + + # Check that the input_field is created and hidden initially + self.assertIsNotNone(self.run_tab.input_field) + self.assertFalse(self.run_tab.input_field.isVisible()) + + # Check that the send_button is created and hidden initially + self.assertIsNotNone(self.run_tab.send_button) + self.assertFalse(self.run_tab.send_button.isVisible()) + + def test_toggle_console(self): + """Test that the toggle_console method works correctly""" + # Import Qt here to avoid circular import + from PyQt5.QtCore import Qt + + # Initially, the console_checkbox should be unchecked + self.assertFalse(self.run_tab.console_checkbox.isChecked()) + + # Check the console_checkbox + self.run_tab.console_checkbox.setChecked(True) + + # Call the toggle_console method directly with the checked state + self.run_tab.toggle_console(Qt.Checked) + + # Uncheck the console_checkbox + self.run_tab.console_checkbox.setChecked(False) + + # Call the toggle_console method directly with the unchecked state + self.run_tab.toggle_console(Qt.Unchecked) + + @patch('gui.tabs.run_tab.QProcess') + def test_run_script(self, mock_qprocess): + """Test that the run_script method works correctly""" + # Set up the mock QProcess + mock_process = MagicMock() + mock_qprocess.return_value = mock_process + + # Set some search terms + self.run_tab.search_terms.setText("test search") + + # Call the run_script method + self.run_tab.run_script() + + # Check that the process was created + self.assertIsNotNone(self.run_tab.process) + + # Check that the run_button is disabled + self.assertFalse(self.run_tab.run_button.isEnabled()) + + # Check that the stop_button is enabled + self.assertTrue(self.run_tab.stop_button.isEnabled()) + + # Check that the process was started with the correct arguments + mock_process.start.assert_called_once() + args = mock_process.start.call_args[0][1] + self.assertIn("run_streaming.py", args[0]) + self.assertIn("-s", args[1:]) + self.assertIn("test search", args[1:]) + + def test_stop_script(self): + """Test that the stop_script method works correctly""" + # Import QProcess here to avoid circular import + from PyQt5.QtCore import QProcess as ActualQProcess + + # Create a mock process + mock_process = MagicMock() + + # Set the process state to Running + mock_process.state.return_value = ActualQProcess.Running + + # Mock the waitForFinished method to return True + mock_process.waitForFinished.return_value = True + + # Set the process + self.run_tab.process = mock_process + + # Call the stop_script method + self.run_tab.stop_script() + + # Check that the process was terminated + mock_process.terminate.assert_called_once() + + # Check that waitForFinished was called + mock_process.waitForFinished.assert_called_once_with(3000) + + # Check that the run_button is enabled + self.assertTrue(self.run_tab.run_button.isEnabled()) + + # Check that the stop_button is disabled + self.assertFalse(self.run_tab.stop_button.isEnabled()) + +if __name__ == '__main__': + unittest.main() diff --git a/Test/GUI/test_site_manager.py b/Test/GUI/test_site_manager.py new file mode 100644 index 0000000..1975e49 --- /dev/null +++ b/Test/GUI/test_site_manager.py @@ -0,0 +1,67 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import patch, MagicMock + +from gui.utils.site_manager import get_sites, sites + +class TestSiteManager(unittest.TestCase): + @patch('gui.utils.site_manager.load_search_functions') + def test_get_sites(self, mock_load_search_functions): + """Test that the get_sites function correctly processes search functions""" + # Set up the mock to return a dictionary of search functions + mock_load_search_functions.return_value = { + 'site1_search': (MagicMock(), 'movie'), + 'site2_search': (MagicMock(), 'tv'), + 'site3_search': (MagicMock(), 'anime'), + } + + # Call the function + result = get_sites() + + # Check that the mock was called + mock_load_search_functions.assert_called_once() + + # Check that the result is a list + self.assertIsInstance(result, list) + + # Check that the result has the correct length + self.assertEqual(len(result), 3) + + # Check that each item in the result is a dictionary with the correct keys + for i, site in enumerate(result): + self.assertIsInstance(site, dict) + self.assertIn('index', site) + self.assertIn('name', site) + self.assertIn('flag', site) + + # Check that the index is correct + self.assertEqual(site['index'], i) + + # Check that the name is correct (the part before '_' in the key) + expected_name = f'site{i+1}' + self.assertEqual(site['name'], expected_name) + + # Check that the flag is correct (first 3 characters of the key, uppercase) + expected_flag = f'SIT' + self.assertEqual(site['flag'], expected_flag) + + def test_sites_variable(self): + """Test that the sites variable is a list""" + # Check that sites is a list + self.assertIsInstance(sites, list) + + # Check that each item in sites is a dictionary with the correct keys + for site in sites: + self.assertIsInstance(site, dict) + self.assertIn('index', site) + self.assertIn('name', site) + self.assertIn('flag', site) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/Test/GUI/test_stream_redirect.py b/Test/GUI/test_stream_redirect.py new file mode 100644 index 0000000..70e1e37 --- /dev/null +++ b/Test/GUI/test_stream_redirect.py @@ -0,0 +1,60 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(src_path) + +# Import +import unittest +from unittest.mock import MagicMock +from PyQt5.QtWidgets import QApplication + +from gui.utils.stream_redirect import Stream + +class TestStreamRedirect(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create a QApplication instance before running tests + cls.app = QApplication.instance() or QApplication(sys.argv) + + def setUp(self): + # Create a fresh instance of Stream for each test + self.stream = Stream() + + # Create a mock slot to connect to the newText signal + self.mock_slot = MagicMock() + self.stream.newText.connect(self.mock_slot) + + def tearDown(self): + # Clean up after each test + self.stream.newText.disconnect(self.mock_slot) + self.stream = None + self.mock_slot = None + + def test_write(self): + """Test that the write method emits the newText signal with the correct text""" + # Call the write method + self.stream.write("Test message") + + # Check that the mock slot was called with the correct text + self.mock_slot.assert_called_once_with("Test message") + + # Call the write method again with a different message + self.stream.write("Another test") + + # Check that the mock slot was called again with the correct text + self.mock_slot.assert_called_with("Another test") + + # Check that the mock slot was called exactly twice + self.assertEqual(self.mock_slot.call_count, 2) + + def test_flush(self): + """Test that the flush method does nothing (as required by the io interface)""" + # Call the flush method + self.stream.flush() + + # Check that the mock slot was not called + self.mock_slot.assert_not_called() + +if __name__ == '__main__': + unittest.main()