|
| 1 | +# test_ingest_window.py |
| 2 | +# |
| 3 | +# Imports |
| 4 | +import pytest |
| 5 | +from pytest_mock import MockerFixture # For mocking |
| 6 | +from pathlib import Path |
| 7 | +# |
| 8 | +# Third-party Libraries |
| 9 | +from textual.app import App, ComposeResult |
| 10 | +from textual.widgets import Button, Input, Select, Checkbox, TextArea, RadioSet, RadioButton, Collapsible, ListView, \ |
| 11 | + ListItem, Markdown, LoadingIndicator, Label, Static |
| 12 | +from textual.containers import Container, VerticalScroll, Horizontal, Vertical |
| 13 | +from textual.pilot import Pilot |
| 14 | +from textual.css.query import QueryError |
| 15 | +# |
| 16 | +# Local Imports |
| 17 | +from tldw_chatbook.app import TldwCli # The main app |
| 18 | +from tldw_chatbook.UI.Ingest_Window import IngestWindow, MEDIA_TYPES # Import MEDIA_TYPES |
| 19 | +from tldw_chatbook.tldw_api.schemas import ProcessVideoRequest, ProcessAudioRequest, ProcessPDFRequest, \ |
| 20 | + ProcessEbookRequest, ProcessDocumentRequest, ProcessXMLRequest, ProcessMediaWikiRequest |
| 21 | +# |
| 22 | +# |
| 23 | +######################################################################################################################## |
| 24 | +# |
| 25 | +# Fixtures and Helper Functions |
| 26 | + |
| 27 | +# Helper to get the IngestWindow instance from the app |
| 28 | +async def get_ingest_window(pilot: Pilot) -> IngestWindow: |
| 29 | + ingest_window_query = pilot.app.query(IngestWindow) |
| 30 | + assert ingest_window_query.is_empty is False, "IngestWindow not found" |
| 31 | + return ingest_window_query.first() |
| 32 | + |
| 33 | + |
| 34 | +@pytest.fixture |
| 35 | +async def app_pilot() -> Pilot: |
| 36 | + app = TldwCli() |
| 37 | + async with app.run_test() as pilot: |
| 38 | + # Ensure the Ingest tab is active. Default is Chat. |
| 39 | + # Switching tabs is handled by app.py's on_button_pressed for tab buttons. |
| 40 | + # We need to find the Ingest tab button and click it. |
| 41 | + # Assuming tab IDs are like "tab-ingest" |
| 42 | + try: |
| 43 | + await pilot.click("#tab-ingest") |
| 44 | + except QueryError: |
| 45 | + # Fallback if direct ID click isn't working as expected in test setup |
| 46 | + # This might indicate an issue with tab IDs or pilot interaction timing |
| 47 | + all_buttons = pilot.app.query(Button) |
| 48 | + ingest_tab_button = None |
| 49 | + for btn in all_buttons: |
| 50 | + if btn.id == "tab-ingest": |
| 51 | + ingest_tab_button = btn |
| 52 | + break |
| 53 | + assert ingest_tab_button is not None, "Ingest tab button not found" |
| 54 | + await pilot.click(ingest_tab_button) |
| 55 | + |
| 56 | + # Verify IngestWindow is present and active |
| 57 | + ingest_window = await get_ingest_window(pilot) |
| 58 | + assert ingest_window is not None |
| 59 | + assert ingest_window.display is True, "IngestWindow is not visible after switching to Ingest tab" |
| 60 | + # Also check the app's current_tab reactive variable |
| 61 | + assert pilot.app.current_tab == "ingest", "App's current_tab is not set to 'ingest'" |
| 62 | + yield pilot |
| 63 | + |
| 64 | + |
| 65 | +# Test Class |
| 66 | +class TestIngestWindowTLDWAPI: |
| 67 | + |
| 68 | + async def test_initial_tldw_api_nav_buttons_and_views(self, app_pilot: Pilot): |
| 69 | + ingest_window = await get_ingest_window(app_pilot) |
| 70 | + # The IngestWindow itself is a container, nav buttons are direct children of its "ingest-nav-pane" |
| 71 | + nav_pane = ingest_window.query_one("#ingest-nav-pane") |
| 72 | + |
| 73 | + for mt in MEDIA_TYPES: |
| 74 | + nav_button_id = f"ingest-nav-tldw-api-{mt.replace('_', '-')}" # IDs don't have # |
| 75 | + view_id = f"ingest-view-tldw-api-{mt.replace('_', '-')}" |
| 76 | + |
| 77 | + # Check navigation button exists |
| 78 | + nav_button = nav_pane.query_one(f"#{nav_button_id}", Button) |
| 79 | + assert nav_button is not None, f"Navigation button {nav_button_id} not found" |
| 80 | + expected_label_part = mt.replace('_', ' ').title() |
| 81 | + if mt == "mediawiki_dump": |
| 82 | + expected_label_part = "MediaWiki Dump" |
| 83 | + assert expected_label_part in str(nav_button.label), f"Label for {nav_button_id} incorrect" |
| 84 | + |
| 85 | + # Check view area exists |
| 86 | + view_area = ingest_window.query_one(f"#{view_id}", Container) |
| 87 | + assert view_area is not None, f"View area {view_id} not found" |
| 88 | + |
| 89 | + # Check initial visibility based on app's active ingest view |
| 90 | + # This assumes that after switching to Ingest tab, a default sub-view *within* Ingest is activated. |
| 91 | + # If `ingest_active_view` is set (e.g. to "ingest-view-prompts" by default), then |
| 92 | + # all tldw-api views should be hidden. |
| 93 | + active_ingest_view_on_app = app_pilot.app.ingest_active_view |
| 94 | + if view_id != active_ingest_view_on_app: |
| 95 | + assert view_area.display is False, f"{view_id} should be hidden if not the active ingest view ('{active_ingest_view_on_app}')" |
| 96 | + else: |
| 97 | + assert view_area.display is True, f"{view_id} should be visible as it's the active ingest view ('{active_ingest_view_on_app}')" |
| 98 | + |
| 99 | + @pytest.mark.parametrize("media_type", MEDIA_TYPES) |
| 100 | + async def test_tldw_api_navigation_and_view_display(self, app_pilot: Pilot, media_type: str): |
| 101 | + ingest_window = await get_ingest_window(app_pilot) |
| 102 | + nav_button_id = f"ingest-nav-tldw-api-{media_type.replace('_', '-')}" |
| 103 | + target_view_id = f"ingest-view-tldw-api-{media_type.replace('_', '-')}" |
| 104 | + |
| 105 | + await app_pilot.click(f"#{nav_button_id}") |
| 106 | + await app_pilot.pause() # Allow watchers to update display properties |
| 107 | + |
| 108 | + # Verify target view is visible |
| 109 | + target_view_area = ingest_window.query_one(f"#{target_view_id}", Container) |
| 110 | + assert target_view_area.display is True, f"{target_view_id} should be visible after clicking {nav_button_id}" |
| 111 | + assert app_pilot.app.ingest_active_view == target_view_id, f"App's active ingest view should be {target_view_id}" |
| 112 | + |
| 113 | + # Verify other TLDW API views are hidden |
| 114 | + for other_mt in MEDIA_TYPES: |
| 115 | + if other_mt != media_type: |
| 116 | + other_view_id = f"ingest-view-tldw-api-{other_mt.replace('_', '-')}" |
| 117 | + other_view_area = ingest_window.query_one(f"#{other_view_id}", Container) |
| 118 | + assert other_view_area.display is False, f"{other_view_id} should be hidden when {target_view_id} is active" |
| 119 | + |
| 120 | + # Verify common form elements exist with dynamic IDs |
| 121 | + common_endpoint_input = target_view_area.query_one(f"#tldw-api-endpoint-url-{media_type}", Input) |
| 122 | + assert common_endpoint_input is not None |
| 123 | + |
| 124 | + common_submit_button = target_view_area.query_one(f"#tldw-api-submit-{media_type}", Button) |
| 125 | + assert common_submit_button is not None |
| 126 | + |
| 127 | + # Verify media-specific options container and its widgets |
| 128 | + if media_type == "video": |
| 129 | + opts_container = target_view_area.query_one("#tldw-api-video-options", Container) |
| 130 | + assert opts_container.display is True |
| 131 | + widget = opts_container.query_one(f"#tldw-api-video-transcription-model-{media_type}", Input) |
| 132 | + assert widget is not None |
| 133 | + elif media_type == "audio": |
| 134 | + opts_container = target_view_area.query_one("#tldw-api-audio-options", Container) |
| 135 | + assert opts_container.display is True |
| 136 | + widget = opts_container.query_one(f"#tldw-api-audio-transcription-model-{media_type}", Input) |
| 137 | + assert widget is not None |
| 138 | + elif media_type == "pdf": |
| 139 | + opts_container = target_view_area.query_one("#tldw-api-pdf-options", Container) |
| 140 | + assert opts_container.display is True |
| 141 | + widget = opts_container.query_one(f"#tldw-api-pdf-engine-{media_type}", Select) |
| 142 | + assert widget is not None |
| 143 | + elif media_type == "ebook": |
| 144 | + opts_container = target_view_area.query_one("#tldw-api-ebook-options", Container) |
| 145 | + assert opts_container.display is True |
| 146 | + widget = opts_container.query_one(f"#tldw-api-ebook-extraction-method-{media_type}", Select) |
| 147 | + assert widget is not None |
| 148 | + elif media_type == "document": # Has minimal specific options currently |
| 149 | + opts_container = target_view_area.query_one("#tldw-api-document-options", Container) |
| 150 | + assert opts_container.display is True |
| 151 | + # Example: find the label if one exists |
| 152 | + try: |
| 153 | + label = opts_container.query_one(Label) # Assuming there's at least one label |
| 154 | + assert label is not None |
| 155 | + except QueryError: # If no labels, this is fine for doc |
| 156 | + pass |
| 157 | + elif media_type == "xml": |
| 158 | + opts_container = target_view_area.query_one("#tldw-api-xml-options", Container) |
| 159 | + assert opts_container.display is True |
| 160 | + widget = opts_container.query_one(f"#tldw-api-xml-auto-summarize-{media_type}", Checkbox) |
| 161 | + assert widget is not None |
| 162 | + elif media_type == "mediawiki_dump": |
| 163 | + opts_container = target_view_area.query_one("#tldw-api-mediawiki-options", Container) |
| 164 | + assert opts_container.display is True |
| 165 | + widget = opts_container.query_one(f"#tldw-api-mediawiki-wiki-name-{media_type}", Input) |
| 166 | + assert widget is not None |
| 167 | + |
| 168 | + async def test_tldw_api_video_submission_data_collection(self, app_pilot: Pilot, mocker: MockerFixture): |
| 169 | + media_type = "video" |
| 170 | + ingest_window = await get_ingest_window(app_pilot) |
| 171 | + |
| 172 | + # Navigate to video tab by clicking its nav button |
| 173 | + nav_button_id = f"ingest-nav-tldw-api-{media_type}" |
| 174 | + await app_pilot.click(f"#{nav_button_id}") |
| 175 | + await app_pilot.pause() # Allow UI to update |
| 176 | + |
| 177 | + target_view_id = f"ingest-view-tldw-api-{media_type}" |
| 178 | + target_view_area = ingest_window.query_one(f"#{target_view_id}", Container) |
| 179 | + assert target_view_area.display is True, "Video view area not displayed after click" |
| 180 | + |
| 181 | + # Mock the API client and its methods |
| 182 | + mock_api_client_instance = mocker.MagicMock() |
| 183 | + # Make process_video an async mock |
| 184 | + mock_process_video = mocker.AsyncMock(return_value=mocker.MagicMock()) |
| 185 | + mock_api_client_instance.process_video = mock_process_video |
| 186 | + mock_api_client_instance.close = mocker.AsyncMock() |
| 187 | + |
| 188 | + mocker.patch("tldw_chatbook.Event_Handlers.ingest_events.TLDWAPIClient", return_value=mock_api_client_instance) |
| 189 | + |
| 190 | + # Set form values |
| 191 | + endpoint_url_input = target_view_area.query_one(f"#tldw-api-endpoint-url-{media_type}", Input) |
| 192 | + urls_textarea = target_view_area.query_one(f"#tldw-api-urls-{media_type}", TextArea) |
| 193 | + video_trans_model_input = target_view_area.query_one(f"#tldw-api-video-transcription-model-{media_type}", Input) |
| 194 | + auth_method_select = target_view_area.query_one(f"#tldw-api-auth-method-{media_type}", Select) |
| 195 | + |
| 196 | + endpoint_url_input.value = "http://fakeapi.com" |
| 197 | + urls_textarea.text = "http://example.com/video.mp4" |
| 198 | + video_trans_model_input.value = "test_video_model" |
| 199 | + auth_method_select.value = "config_token" |
| 200 | + |
| 201 | + app_pilot.app.app_config = {"tldw_api": {"auth_token_config": "fake_token"}} |
| 202 | + |
| 203 | + submit_button_id = f"tldw-api-submit-{media_type}" |
| 204 | + await app_pilot.click(f"#{submit_button_id}") |
| 205 | + await app_pilot.pause(delay=0.5) |
| 206 | + |
| 207 | + mock_process_video.assert_called_once() |
| 208 | + call_args = mock_process_video.call_args[0] |
| 209 | + |
| 210 | + assert len(call_args) >= 1, "process_video not called with request_model" |
| 211 | + request_model_arg = call_args[0] |
| 212 | + |
| 213 | + assert isinstance(request_model_arg, ProcessVideoRequest) |
| 214 | + assert request_model_arg.urls == ["http://example.com/video.mp4"] |
| 215 | + assert request_model_arg.transcription_model == "test_video_model" |
| 216 | + assert request_model_arg.api_key == "fake_token" |
| 217 | + |
| 218 | + # Example for local_file_paths if it's the second argument |
| 219 | + if len(call_args) > 1: |
| 220 | + local_files_arg = call_args[1] |
| 221 | + assert local_files_arg == [], "local_files_arg was not empty" |
| 222 | + else: |
| 223 | + # This case implies process_video might not have received local_file_paths, |
| 224 | + # which could be an issue if it's expected. For now, let's assume it's optional. |
| 225 | + pass |
0 commit comments