# -*- coding: utf-8 -*- """ QwenPaw Sessions module P0 end-to-end test cases. P0 definition: - Core user flows - Multi-feature combined coverage - Real user scenarios - High-priority functionality Stack: pytest + Playwright + Page Object Pattern Run with: pytest tests/test_sessions_p0.py -v """ from __future__ import annotations import logging import time import pytest from playwright.sync_api import Page, expect, TimeoutError from pages.sessions_page import SessionsPage from config.settings import config from utils.helpers import ( log_test_step, log_test_result, take_screenshot, assert_text_contains, ) logger = logging.getLogger(__name__) # ============================================================================ # SESS-001: Session list display + filter + detail view # ============================================================================ @pytest.mark.integration @pytest.mark.p0 @pytest.mark.sessions_core class TestSessionListFilterAndDetail: """ SESS-001: Session list display + filter + detail view. Combined coverage: 1. Sessions page navigation and load 2. Session list table display (column verification) 3. UserID filter 4. Channel filter 5. List sorting 6. Session detail view Scenario: A user opens the Sessions management page, browses the list, narrows down via filter and sort, then views session detail. """ @pytest.mark.test_id("SESS-001") def test_session_list_filter_and_detail(self, sessions_page: SessionsPage, request: pytest.FixtureRequest): """ Verify session list display, filtering, sorting and detail view. Steps: 1. Visit the Sessions page and verify the table loads 2. Verify key columns (ID / UserID / Channel / Created) 3. Verify filters are available (UserID / Channel) 4. Verify sorting (table header is clickable) 5. View detail for the first session """ test_name = request.node.name log_test_step("1. Visit the Sessions page and verify the table loads") sessions_page.open() session_count = sessions_page.get_session_count() logger.info(f"Session count: {session_count}") log_test_step("2. Verify key table columns") table_header = sessions_page.page.locator("thead th") header_count = table_header.count() assert header_count >= 3, f"Table should have at least 3 columns, got {header_count}" logger.info(f"Table column count: {header_count}") log_test_step("3. Verify filters and run a filter operation") userid_input = sessions_page.page.locator(sessions_page.FILTER_USER_ID_INPUT).first if userid_input.count() > 0 and userid_input.is_visible(timeout=3000): # Type a filter value and verify userid_input.fill("e2e_test_filter") sessions_page.page.wait_for_timeout(1500) filtered_count = sessions_page.get_session_count() logger.info(f"Session count after filter: {filtered_count}") assert filtered_count <= session_count, \ f"Filtered count ({filtered_count}) should not exceed original count ({session_count})" logger.info("UserID filter works") # Clear filter userid_input.fill("") sessions_page.page.wait_for_timeout(1000) else: logger.info("UserID filter input not found") log_test_step("4. Verify sorting (click table header)") headers = sessions_page.page.locator("thead th") assert headers.count() > 0, "Table headers should exist" # Click the first sortable header first_header = headers.first first_header.click() sessions_page.page.wait_for_timeout(1000) logger.info("Clicked header to sort") log_test_step("5. View detail for the first session") refreshed_count = sessions_page.get_session_count() if refreshed_count > 0: visible_rows = sessions_page.page.locator("tbody tr:not([aria-hidden='true'])") assert visible_rows.count() > 0, "Visible session rows should exist" first_row = visible_rows.first first_row.click() sessions_page.page.wait_for_timeout(2000) # Verify the detail panel or drawer opens detail_panel = sessions_page.page.locator('.qwenpaw-drawer, .qwenpaw-modal, [class*="detail"]').first if detail_panel.count() > 0 and detail_panel.is_visible(timeout=3000): detail_text = detail_panel.text_content() or "" assert len(detail_text) > 10, "Session detail content should not be empty" logger.info(f"Session detail panel opened, content length: {len(detail_text)}") sessions_page.page.keyboard.press("Escape") sessions_page.page.wait_for_timeout(500) else: logger.info("Clicking row did not open a detail panel (may use route navigation)") logger.info(f"Visible session rows: {visible_rows.count()}") else: logger.info("No session data, skipping detail view") log_test_result(test_name, True, 0) logger.info(f"Test {test_name} passed - session list display, filter, sort and detail OK") # ============================================================================ # SESS-002: Edit session + delete session + batch delete # ============================================================================ @pytest.mark.integration @pytest.mark.p0 @pytest.mark.sessions_edit class TestEditAndDeleteSession: """ SESS-002: Edit session + delete session + batch delete. Combined coverage: 1. Click the edit button to open the edit dialog 2. Verify the edit dialog and form 3. Cancel editing and close the dialog 4. Verify the delete button is available 5. Verify batch selection and batch delete Scenario: Admin opens the edit dialog to inspect a session, cancels, then verifies single delete and batch delete are available. """ @pytest.mark.test_id("SESS-002") def test_edit_and_delete_session(self, sessions_page: SessionsPage, ensure_session_data, request: pytest.FixtureRequest): """ Verify edit, delete and batch-delete features. Steps: 1. Visit the Sessions page 2. Verify operable sessions exist 3. Click the edit button and verify the dialog opens 4. Cancel editing and verify the dialog closes 5. Verify the delete button is available 6. Verify batch selection and batch-delete """ test_name = request.node.name log_test_step("1. Visit the Sessions page") sessions_page.open() sessions_page.step_shot("01_sessions_page_opened") log_test_step("2. Verify operable sessions exist") session_count = sessions_page.get_session_count() assert session_count > 0, ( "ensure_session_data fixture should have created test data, but the page shows 0 sessions" ) first_row = sessions_page.page.locator(sessions_page.SESSION_TABLE_ROW).first # --- Edit: must actually open and close the drawer --- log_test_step("3. Click the edit button and verify the dialog opens") # Note: SUT has Action column fixed="right"; in-row lookup may miss the button. # Use the page-level EDIT_BTN selector and take the first match (covers fixed-column shadow row). page_edit_btns = sessions_page.page.locator(sessions_page.EDIT_BTN) edit_btn_count = page_edit_btns.count() assert edit_btn_count > 0, ( f"No edit button found on the page (rows={session_count}). " f"Page object EDIT_BTN selector may not cover real DOM; check fixed-right column structure." ) edit_btn = page_edit_btns.first edit_btn.scroll_into_view_if_needed() edit_btn.click() sessions_page.step_shot("02_edit_btn_clicked") expect( sessions_page.page.locator(sessions_page.SESSION_DRAWER).first ).to_be_visible(timeout=5000) logger.info("Edit dialog opened") sessions_page.step_shot("03_edit_drawer_opened") log_test_step("4. Cancel editing and verify the dialog closes") cancel_btns = sessions_page.page.locator(sessions_page.FORM_CANCEL_BTN) if cancel_btns.count() > 0 and cancel_btns.first.is_visible(): cancel_btns.first.click() else: # Fallback: press ESC to close the drawer sessions_page.page.keyboard.press("Escape") expect( sessions_page.page.locator(sessions_page.SESSION_DRAWER).first ).to_be_hidden(timeout=5000) sessions_page.step_shot("04_edit_drawer_closed") logger.info("Edit dialog closed") # --- Delete: the button must exist and be enabled --- log_test_step("5. Verify the delete button is available") page_delete_btns = sessions_page.page.locator(sessions_page.DELETE_BTN) del_count = page_delete_btns.count() assert del_count > 0, "No delete button found on the page" first_delete = page_delete_btns.first assert first_delete.is_enabled(), "First delete button should be enabled" logger.info(f"Delete button verified ({del_count} delete buttons)") sessions_page.step_shot("05_delete_btn_visible") # --- Batch delete: checkbox must actually be selectable --- log_test_step("6. Verify batch-selection checkbox is selectable") row_checkboxes = sessions_page.page.locator( 'tbody tr .qwenpaw-checkbox-input, ' 'tbody tr .ant-checkbox-input, ' 'tbody tr input[type="checkbox"]' ) cb_count = row_checkboxes.count() assert cb_count > 0, "No row checkboxes found (batch selection should be available)" first_cb = row_checkboxes.first first_cb.click(force=True) sessions_page.page.wait_for_timeout(800) sessions_page.step_shot("06_first_checkbox_checked") # After selection, a batch-delete button should appear (fixed bar or toolbar) batch_btns = sessions_page.page.locator( 'button.qwenpaw-btn-dangerous:has-text("Delete"), ' 'button:has-text("Batch Delete")' ) # This is a soft check: batch-delete button text/class varies by frontend; any dangerous button is fine assert batch_btns.count() > 0, ( "A batch-delete / dangerous button should appear after selecting a checkbox, but none found." ) logger.info(f"Batch-delete button appeared after selecting checkbox ({batch_btns.count()})") # Unselect to avoid polluting the next test first_cb.click(force=True) sessions_page.page.wait_for_timeout(300) log_test_result(test_name, True, 0) logger.info(f"Test {test_name} passed - edit, delete and batch-delete verified") # ============================================================================ # SESS-003: Session name edit and save # ============================================================================ @pytest.mark.integration @pytest.mark.p0 @pytest.mark.sessions_edit class TestSessionEditAndSave: """ SESS-003: Session name edit and save. Coverage: 1. Click the edit button to open the edit drawer 2. Verify the edit drawer opens 3. Modify the session name 4. Save and verify the name updated 5. Restore the original name Scenario: A user edits the session name to better identify the conversation, saves it and verifies the name update, then restores the original name to keep the environment clean. """ @pytest.mark.test_id("SESS-003") def test_session_edit_name_and_save( self, sessions_page: SessionsPage, ensure_session_data, request: pytest.FixtureRequest, ): """ Verify session name edit-and-save flow. Steps: 1. Visit the Sessions page 2. Verify operable sessions exist 3. Click the edit button on the first session 4. Verify the edit drawer opens 5. Change the session name to "E2E_Test_Renamed_xxx" 6. Click save 7. Verify the drawer closes 8. Verify the new name appears in the list 9. Restore the original name (edit and save again) """ test_name = request.node.name log_test_step("1. Visit the Sessions page") sessions_page.open() log_test_step("2. Verify operable sessions exist") session_count = sessions_page.get_session_count() if session_count == 0: pytest.skip("No operable sessions") first_row = sessions_page.page.locator(sessions_page.SESSION_TABLE_ROW).first # Get the original session name original_name_cell = first_row.locator('td').nth(1) # Assume the name is in the 2nd column original_name = original_name_cell.text_content().strip() if original_name_cell.count() > 0 else "" logger.info(f"Original session name: {original_name}") log_test_step("3. Click the edit button on the first session") # Source: Action column fixed="right"; button is Button type="link" size="small" # Need to find the edit button within the fixed column edit_btn = first_row.locator('button:has-text("Edit")').first if not edit_btn.is_visible(): # Try the fixed column fixed_row = sessions_page.page.locator('.qwenpaw-table-cell-fix-right button:has-text("Edit")').first if fixed_row.is_visible(): edit_btn = fixed_row if not edit_btn.is_visible(): pytest.skip("Edit button not available") edit_btn.click() log_test_step("4. Verify the edit drawer opens") expect(sessions_page.page.locator(sessions_page.SESSION_DRAWER).first).to_be_visible(timeout=5000) logger.info("Edit drawer opened") log_test_step("5. Modify the session name") new_name = f"E2E_Test_Renamed_{request.node.name[-8:]}" name_input = sessions_page.page.locator(sessions_page.SESSION_DRAWER).first.locator('input').first if name_input.count() > 0 and name_input.is_visible(): name_input.fill(new_name) logger.info(f"Entered new name: {new_name}") else: # Try other possible input selectors name_input = sessions_page.page.locator('#sessionName, [name="sessionName"]').first if name_input.count() > 0: name_input.fill(new_name) logger.info(f"Entered new name: {new_name}") else: pytest.skip("Name input not found") log_test_step("6. Click the save button") save_btn = sessions_page.page.locator(sessions_page.FORM_SUBMIT_BTN).first if save_btn.count() == 0: save_btn = sessions_page.page.locator('button:has-text("Save"), button.qwenpaw-btn-primary').first if save_btn.count() > 0 and save_btn.is_visible(): save_btn.click() logger.info("Clicked save button") else: pytest.skip("Save button not found") log_test_step("7. Verify the drawer closes") expect(sessions_page.page.locator(sessions_page.SESSION_DRAWER).first).to_be_hidden(timeout=5000) logger.info("Edit drawer closed") log_test_step("8. Verify the new name appears in the list (hard assert)") sessions_page.page.wait_for_timeout(1500) # Reload the page to ensure we see the latest data (avoid optimistic-update cache illusion) sessions_page.page.reload() sessions_page.page.wait_for_load_state("domcontentloaded") sessions_page.page.wait_for_timeout(2000) sessions_page.step_shot("08_after_save_reloaded") # Search for new_name across all rows in the table (not just the first, sort order may change) page_text = sessions_page.page.locator("tbody").inner_text() or "" assert new_name in page_text, ( f"New name '{new_name}' not found in session list text after save; " f"save may not have taken effect. Table preview: {page_text[:300]}" ) logger.info(f"Found new name in session list: {new_name}") log_test_step("9. Restore the original name") if original_name: edit_btn.click() expect(sessions_page.page.locator(sessions_page.SESSION_DRAWER).first).to_be_visible(timeout=5000) name_input = sessions_page.page.locator(sessions_page.SESSION_DRAWER).first.locator('input').first if name_input.count() > 0: name_input.fill(original_name) save_btn = sessions_page.page.locator(sessions_page.FORM_SUBMIT_BTN).first if save_btn.count() == 0: save_btn = sessions_page.page.locator('button:has-text("Save"), button.qwenpaw-btn-primary').first if save_btn.count() > 0: save_btn.click() expect(sessions_page.page.locator(sessions_page.SESSION_DRAWER).first).to_be_hidden(timeout=5000) logger.info(f"Restored original name: {original_name}") log_test_result(test_name, True, 0) logger.info(f"Test {test_name} passed") # ============================================================================ # SESS-004: Batch delete sessions # ============================================================================ @pytest.mark.integration @pytest.mark.p1 @pytest.mark.sessions_batch class TestSessionBatchDelete: """ SESS-004: Batch delete sessions. Coverage: 1. Tick checkboxes for multiple sessions 2. Verify the batch-delete button becomes available 3. Click the batch-delete button 4. Confirm deletion 5. Verify the session count decreased Scenario: A user batch-selects and deletes multiple unwanted sessions for efficiency. Note: this is destructive, skip if there are insufficient sessions. """ @pytest.mark.test_id("SESS-004") def test_session_batch_delete( self, sessions_page: SessionsPage, ensure_session_data, request: pytest.FixtureRequest, ): """ Verify batch delete sessions. Steps: 1. Visit the Sessions page 2. Verify at least 2 sessions exist 3. Tick checkboxes for the first two sessions 4. Verify the batch-delete button becomes available 5. Click the batch-delete button 6. Confirm deletion (if a confirm dialog appears) 7. Verify the session count decreased """ test_name = request.node.name log_test_step("1. Visit the Sessions page") sessions_page.open() log_test_step("2. Verify at least 2 sessions exist") session_count = sessions_page.get_session_count() if session_count < 2: pytest.skip(f"Insufficient sessions; need at least 2, have {session_count}") log_test_step("3. Tick checkboxes for the first two sessions") # Source: Table rowSelection; each tbody tr has a checkbox. # Don't tick the select-all (in thead); tick the tbody row checkboxes. # Use broad selectors to cover both antd and qwenpaw prefixes row_checkboxes = sessions_page.page.locator( 'tbody tr .qwenpaw-checkbox-input, ' 'tbody tr .ant-checkbox-input, ' 'tbody tr input[type="checkbox"]' ).all() if len(row_checkboxes) < 2: pytest.skip(f"Not enough row checkboxes found; got {len(row_checkboxes)}") checked_count = 0 for i in range(min(2, len(row_checkboxes))): checkbox = row_checkboxes[i] if checkbox.is_visible(): checkbox.click(force=True) sessions_page.page.wait_for_timeout(800) checked_count += 1 logger.info(f"Ticked checkbox for session #{i + 1}") assert checked_count >= 1, "At least 1 session checkbox should be ticked" logger.info(f"Ticked {checked_count} session checkbox(es)") log_test_step("4. Verify the batch-delete button appears") # Source: button Type="primary" danger renders only when selectedRowKeys.length > 0 # antd Button type="primary" danger may have classes qwenpaw-btn-primary + qwenpaw-btn-dangerous batch_delete_btn = None batch_btn_selectors = [ 'button.qwenpaw-btn-dangerous:has-text("Delete")', 'button:has-text("Batch Delete")', 'button:has-text("Delete")', ] for selector in batch_btn_selectors: btn = sessions_page.page.locator(selector).first if btn.count() > 0 and btn.is_visible(timeout=3000): batch_delete_btn = btn logger.info(f"Found batch-delete button: {selector}") break assert batch_delete_btn is not None, "Batch-delete button not found" logger.info("Batch-delete button appeared") log_test_step("5. Record session count before deletion") count_before = sessions_page.get_session_count() logger.info(f"Session count before deletion: {count_before}") log_test_step("6. Click the batch-delete button") batch_delete_btn.click() sessions_page.page.wait_for_timeout(1500) log_test_step("7. Confirm deletion (if a confirm dialog appears)") # Source: Modal.confirm, okType="danger" # Explicitly wait for the dialog to appear modal_visible = False try: modal = sessions_page.page.locator('.qwenpaw-modal, .ant-modal').first modal.wait_for(state="visible", timeout=5000) modal_visible = True logger.info("Confirm dialog appeared") except Exception: logger.warning("No dialog detected, trying to match the confirm button directly") sessions_page.page.wait_for_timeout(500) # Try multiple selectors to match the confirm button confirm_btn = None confirm_selectors = [ # Modal.confirm okType="danger" buttons '.qwenpaw-modal-confirm-btns .qwenpaw-btn-dangerous', '.qwenpaw-modal-confirm-btns .qwenpaw-btn-primary', '.qwenpaw-modal .qwenpaw-btn-dangerous', '.qwenpaw-modal .qwenpaw-btn-primary', # Original antd prefixes '.ant-modal-confirm-btns .ant-btn-dangerous', '.ant-modal-confirm-btns .ant-btn-primary', '.ant-modal .ant-btn-primary', # Generic text matchers '.qwenpaw-modal button:has-text("OK")', '.qwenpaw-modal button:has-text("Delete")', '.ant-modal button:has-text("OK")', # Popconfirm or other confirm components '.qwenpaw-popconfirm button.qwenpaw-btn-primary', 'button:has-text("OK")', ] for selector in confirm_selectors: try: btn = sessions_page.page.locator(selector).first if btn.count() > 0 and btn.is_visible(timeout=2000): confirm_btn = btn logger.info(f"Found confirm button: {selector}") break except Exception: continue if confirm_btn is not None: confirm_btn.click() logger.info("Clicked confirm-delete button") else: logger.warning("Confirm-dialog button not found, trying to confirm via Enter") sessions_page.page.keyboard.press("Enter") # Wait for the delete API to finish and the list to refresh sessions_page.page.wait_for_timeout(5000) log_test_step("8. Verify session count decreased") # Wait for the list to refresh sessions_page.page.reload() sessions_page.page.wait_for_load_state("domcontentloaded") sessions_page.page.wait_for_timeout(3000) count_after = sessions_page.get_session_count() logger.info(f"Session count after deletion: {count_after}") assert count_after < count_before, \ f"Session count did not decrease: before={count_before}, after={count_after}" logger.info(f"Session count went from {count_before} down to {count_after}") log_test_result(test_name, True, 0) logger.info(f"Test {test_name} passed") # ============================================================================ # Fixtures # ============================================================================ @pytest.fixture(scope="function") def sessions_page(page: Page) -> SessionsPage: """Create a SessionsPage instance.""" return SessionsPage(page) @pytest.fixture(scope="function") def ensure_session_data(page: Page): """Ensure the Sessions page has enough test data (at least 3 entries). Sends messages via POST /api/console/chat to auto-create sessions, each with a different session_id and user_id. After the test, the created test sessions are deleted via the API. """ base_url = config.base_url page.goto(f"{base_url}/sessions") page.wait_for_timeout(2000) existing_count = page.locator( "tbody tr:not([aria-hidden='true'])" ":not(.qwenpaw-table-placeholder)" ":not(.qwenpaw-table-measure-row)" ).count() needed = max(0, 3 - existing_count) created_session_ids = [] if needed == 0: logger.info(f"Already have {existing_count} sessions; no creation needed") yield created_session_ids return logger.info(f"Currently {existing_count} sessions; need to create {needed} more") for i in range(needed): session_id = f"e2e_sess_{int(time.time() * 1000)}_{i}" user_id = f"e2e_user_{int(time.time() * 1000)}_{i}" result = page.evaluate( """async ([sessionId, userId, idx]) => { try { const resp = await fetch('/api/console/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: [{ role: 'user', type: 'message', content: [{ type: 'text', text: 'E2E test session ' + idx, status: 'created' }] }], session_id: sessionId, user_id: userId, channel: 'console', stream: false }) }); return { ok: resp.ok, status: resp.status }; } catch (e) { return { ok: false, error: e.message }; } }""", [session_id, user_id, str(i)], ) if result.get("ok"): created_session_ids.append(session_id) else: logger.warning(f" Failed to create session: {result}") page.wait_for_timeout(1500) logger.info(f" Created session: sid={session_id}, uid={user_id}") # Reload page to verify data was created page.reload() page.wait_for_timeout(2000) logger.info(f"Created {needed} test sessions") yield created_session_ids # ---- teardown: clean up created test sessions ---- if not created_session_ids: return logger.info(f"Starting cleanup of {len(created_session_ids)} test sessions") try: # Fetch /api/chats to map session_id -> UUID (id field) chat_list = page.evaluate( """async (targetSessionIds) => { try { const resp = await fetch('/api/chats'); if (!resp.ok) return { ok: false, status: resp.status }; const chats = await resp.json(); const matches = chats .filter(c => targetSessionIds.includes(c.session_id)) .map(c => ({ id: c.id, session_id: c.session_id })); return { ok: true, matches }; } catch (e) { return { ok: false, error: e.message }; } }""", created_session_ids, ) if not chat_list.get("ok"): logger.warning(f" Failed to fetch session list: {chat_list}") return for match in chat_list.get("matches", []): uuid = match["id"] sid = match["session_id"] try: delete_result = page.evaluate( """async (uuid) => { try { const resp = await fetch('/api/chats/' + uuid, { method: 'DELETE', }); return { ok: resp.ok, status: resp.status }; } catch (e) { return { ok: false, error: e.message }; } }""", uuid, ) if delete_result.get("ok"): logger.info(f" Deleted session: {sid} (uuid={uuid})") else: logger.warning(f" Delete failed: {sid} -> {delete_result}") except Exception as delete_error: logger.warning(f" Delete exception: {sid} -> {delete_error}") except Exception as cleanup_error: logger.warning(f" Cleanup process exception: {cleanup_error}") logger.info("Test session cleanup complete") # ============================================================================ # P1 test case: combined session filtering # ============================================================================ @pytest.mark.integration @pytest.mark.p1 @pytest.mark.sessions_filter class TestSessionFilterByUseridAndChannel: """ SESS-P1-001: Filter sessions by UserID and Channel combined. Coverage: 1. Filter by UserID input 2. Filter by Channel dropdown 3. Verify combined filter results 4. Clear filters and verify the list is restored """ def test_session_filter_by_userid_and_channel(self, page: Page, ensure_session_data): """Test combined session filter by UserID and Channel.""" log_test_step("Navigate to the session management page") page.goto(f"{config.base_url}/sessions") page.wait_for_load_state("domcontentloaded") page.wait_for_timeout(3000) log_test_step("Find filter controls") # Source FilterBar.tsx: UserID input placeholder is t("sessions.filterUserId"); # Channel selector placeholder is t("sessions.filterChannel") userid_input = page.locator( "input[placeholder*='user'], input[placeholder*='User'], " "input[placeholder*='userid'], input[placeholder*='UserId'], " "input[placeholder*='ID'], input[placeholder*='id']" ).first channel_select = page.locator(".qwenpaw-select, .ant-select").first # At least one filter control must exist has_userid_input = userid_input.count() > 0 has_channel_select = channel_select.count() > 0 assert has_userid_input or has_channel_select, \ "No filter controls found (UserID input or Channel selector)" logger.info(f"Filter controls: UserID input={'yes' if has_userid_input else 'no'}, Channel selector={'yes' if has_channel_select else 'no'}") log_test_step("Get the initial session list") session_row_selector = "tbody tr:not(.qwenpaw-table-placeholder):not(.qwenpaw-table-measure-row)" initial_sessions = page.locator(session_row_selector).all() initial_count = len(initial_sessions) assert initial_count > 0, "ensure_session_data fixture should have created test data, but session list is still empty" logger.info(f"Initial session count: {initial_count}") if has_userid_input: log_test_step("Extract UserID from the first session") first_session = initial_sessions[0] cells = first_session.locator("td").all() assert len(cells) >= 2, "Session row has too few columns" test_userid = cells[1].inner_text().strip() assert len(test_userid) > 0, "Could not extract UserID" logger.info(f"Using test UserID: {test_userid}") log_test_step(f"Enter UserID filter: {test_userid}") userid_input.fill(test_userid) page.wait_for_timeout(2000) log_test_step("Verify filter results") filtered_sessions = page.locator(session_row_selector).all() filtered_count = len(filtered_sessions) assert filtered_count <= initial_count, \ f"Filtered session count ({filtered_count}) should not exceed initial ({initial_count})" logger.info(f"After UserID filter: {filtered_count} sessions (initial: {initial_count})") # Verify the filtered results contain the target UserID if filtered_count > 0: first_filtered_cells = filtered_sessions[0].locator("td").all() if len(first_filtered_cells) >= 2: result_userid = first_filtered_cells[1].inner_text().strip() assert test_userid in result_userid or result_userid in test_userid, \ f"Filtered UserID ({result_userid}) does not match input ({test_userid})" logger.info(f"Filtered UserID matches: {result_userid}") log_test_step("Clear filter and verify list is restored") userid_input.fill("") page.wait_for_timeout(2000) restored_sessions = page.locator(session_row_selector).all() restored_count = len(restored_sessions) assert abs(restored_count - initial_count) <= 2, \ f"Restored count is off: initial {initial_count}, restored {restored_count}" logger.info(f"After clearing filter, restored to {restored_count} sessions (initial {initial_count})") logger.info("Session filter test complete")