diff --git a/frontend/e2e/base_data.py b/frontend/e2e/base_data.py index 2d374c6..2d31f59 100644 --- a/frontend/e2e/base_data.py +++ b/frontend/e2e/base_data.py @@ -35,12 +35,19 @@ from playwright.sync_api import Page, sync_playwright from common import ( E2EFailure, + click_query, collect_console_errors, + confirm_message_box, current_path, expect_toast, fail, + fill_filter, login, + open_menu, + select_filter_option, + table_rows, toast_text, + visible_dialog, wait_toast, ) @@ -68,65 +75,6 @@ console_errors: list[str] = [] # ==================== 页面操作辅助 ==================== -def open_menu(page: Page, title: str, group: str) -> None: - """点击侧边菜单项,必要时先展开指定分组。 - - 分组(el-sub-menu)已展开时再次点击会收拢,因此先判断菜单项是否可见。 - """ - menu = page.locator(".app-menu") - item = menu.locator(".el-menu-item", has_text=title).first - if not item.is_visible(): - menu.locator(".el-sub-menu__title", has_text=group).click() - page.wait_for_timeout(400) - item.click() - page.wait_for_timeout(800) - - -def fill_filter(page: Page, label: str, value: str) -> None: - """在**当前可见**的筛选表单中按标签填写输入框。 - - 选择器一律带 `:visible` —— 同页存在多个表单实例(隐藏未销毁)时, - 不带可见性过滤会命中隐藏元素。 - """ - form_item = page.locator( - f'.filter-form:visible .el-form-item:has(.el-form-item__label:text-is("{label}"))' - ).first - form_item.locator("input").first.fill(value) - - -def select_filter_option(page: Page, label: str, option: str) -> None: - """展开可见筛选表单中指定标签的下拉并选择选项(发货/回款页按业务筛选)。""" - item = page.locator( - f'.filter-form:visible .el-form-item:has(.el-form-item__label:text-is("{label}"))' - ) - item.locator(".el-select").click() - page.wait_for_timeout(300) - page.locator(".el-select-dropdown:visible .el-select-dropdown__item", has_text=option).first.click() - page.wait_for_timeout(200) - - -def click_query(page: Page) -> None: - """点击可见筛选表单中的查询按钮。""" - page.locator(".filter-form:visible button:has-text('查询')").first.click() - page.wait_for_timeout(800) - - -def table_rows(page: Page): - """当前可见的表格行(选择器带 :visible,避开隐藏实例)。""" - return page.locator(".el-table__body .el-table__row:visible") - - -def visible_dialog(page: Page): - """当前可见的对话框(同页可能存在多个 dialog 实例,按可见性取最后一个)。""" - return page.locator(".el-dialog:visible").last - - -def confirm_message_box(page: Page, button_text: str) -> None: - """点击确认框(ElMessageBox)中的指定按钮。""" - page.locator(".el-message-box:visible").locator(f"button:has-text('{button_text}')").click() - page.wait_for_timeout(800) - - def confirm_text(page: Page) -> str: """当前确认框的完整文本(业务口径文案断言用),轮询等待出现。""" deadline = time.monotonic() + 5 diff --git a/frontend/e2e/common.py b/frontend/e2e/common.py index 6642cc8..7b56dee 100644 --- a/frontend/e2e/common.py +++ b/frontend/e2e/common.py @@ -156,3 +156,66 @@ def login(page: Page, username: str, password: str) -> None: page.wait_for_load_state("networkidle") finally: page.remove_listener("response", on_response) + + +# ==================== 列表页 UI 操作(base_data/system_management 共用) ==================== + + +def open_menu(page: Page, title: str, group: str) -> None: + """点击侧边菜单项,必要时先展开指定分组。 + + 分组(el-sub-menu)已展开时再次点击会收拢,因此先判断菜单项是否可见。 + """ + menu = page.locator(".app-menu") + item = menu.locator(".el-menu-item", has_text=title).first + if not item.is_visible(): + menu.locator(".el-sub-menu__title", has_text=group).click() + page.wait_for_timeout(400) + item.click() + page.wait_for_timeout(800) + + +def fill_filter(page: Page, label: str, value: str) -> None: + """在**当前可见**的筛选表单中按标签填写输入框。 + + 标签用 `:text-is` 精确匹配 —— 子串匹配会让「操作」命中「操作人」; + 选择器带 `:visible` —— 同页存在多个表单实例(v-show 隐藏未销毁)时 + 会命中隐藏元素。 + """ + form_item = page.locator( + f'.filter-form:visible .el-form-item:has(.el-form-item__label:text-is("{label}"))' + ).first + form_item.locator("input").first.fill(value) + + +def select_filter_option(page: Page, label: str, option: str) -> None: + """展开可见筛选表单中指定标签的下拉并选择选项。""" + item = page.locator( + f'.filter-form:visible .el-form-item:has(.el-form-item__label:text-is("{label}"))' + ) + item.locator(".el-select").click() + page.wait_for_timeout(300) + page.locator(".el-select-dropdown:visible .el-select-dropdown__item", has_text=option).first.click() + page.wait_for_timeout(200) + + +def click_query(page: Page) -> None: + """点击可见筛选表单中的查询按钮。""" + page.locator(".filter-form:visible button:has-text('查询')").first.click() + page.wait_for_timeout(800) + + +def table_rows(page: Page): + """当前可见的表格行(选择器带 :visible,避开隐藏实例)。""" + return page.locator(".el-table__body .el-table__row:visible") + + +def visible_dialog(page: Page): + """当前可见的对话框(同页可能存在多个 dialog 实例,按可见性取最后一个)。""" + return page.locator(".el-dialog:visible").last + + +def confirm_message_box(page: Page, button_text: str) -> None: + """点击确认框(ElMessageBox)中的指定按钮。""" + page.locator(".el-message-box:visible").locator(f"button:has-text('{button_text}')").click() + page.wait_for_timeout(800) diff --git a/frontend/e2e/system_management.py b/frontend/e2e/system_management.py index 30fcb83..cb4ace2 100644 --- a/frontend/e2e/system_management.py +++ b/frontend/e2e/system_management.py @@ -29,6 +29,13 @@ import time from playwright.sync_api import Page, sync_playwright from common import ( + click_query, + confirm_message_box, + fill_filter, + open_menu, + select_filter_option, + table_rows, + visible_dialog, BASE_URL, E2EFailure, collect_console_errors, @@ -56,66 +63,6 @@ console_errors: list[str] = [] # ==================== 页面操作辅助 ==================== -def open_menu(page: Page, title: str) -> None: - """点击侧边菜单项,必要时先展开「系统管理」分组。 - - 分组已展开时再次点击会收拢,因此先判断菜单项是否已经可见。 - """ - menu = page.locator(".app-menu") - item = menu.get_by_text(title, exact=True) - if not item.is_visible(): - menu.get_by_text("系统管理", exact=True).click() - page.wait_for_timeout(400) - item.click() - page.wait_for_timeout(800) - - -def fill_filter(page: Page, label: str, value: str) -> None: - """在**当前可见**的筛选表单中按标签填写输入框。 - - 选择器一律带 `:visible` —— 日志页两个 tab 各有一个表单(非激活 tab - 用 v-show 隐藏),不带可见性过滤会命中隐藏表单的元素。 - """ - form_item = page.locator(".filter-form:visible .el-form-item", has_text=label).first - form_item.locator("input").first.fill(value) - - -def select_filter_option(page: Page, label: str, option: str) -> None: - """展开可见筛选表单中指定标签的下拉并选择选项。 - - 标签用 `:text-is` 精确匹配 —— 子串匹配会让「操作」命中「操作人」。 - """ - item = page.locator( - f'.filter-form:visible .el-form-item:has(.el-form-item__label:text-is("{label}"))' - ) - item.locator(".el-select").click() - page.wait_for_timeout(300) - page.locator(".el-select-dropdown:visible .el-select-dropdown__item", has_text=option).first.click() - page.wait_for_timeout(200) - - -def click_query(page: Page) -> None: - """点击可见筛选表单中的查询按钮。""" - page.locator(".filter-form:visible button:has-text('查询')").first.click() - page.wait_for_timeout(800) - - -def table_rows(page: Page): - """当前可见的表格行(日志页非激活 tab 的表格隐藏,需过滤)。""" - return page.locator(".el-table__body .el-table__row:visible") - - -def confirm_message_box(page: Page, button_text: str) -> None: - """点击确认框(ElMessageBox)中的指定按钮。""" - page.locator(".el-message-box").locator(f"button:has-text('{button_text}')").click() - page.wait_for_timeout(800) - - -def visible_dialog(page: Page): - """当前可见的对话框(同页存在多个 dialog 实例,按可见性取)。""" - return page.locator(".el-dialog:visible").last - - def create_user_via_ui( page: Page, username: str, real_name: str, expect_success: bool = True ) -> None: @@ -154,7 +101,7 @@ def find_user_row(page: Page, username: str): def step_user_create(page: Page, admin_password: str) -> None: """登录、菜单导航、新增用户与重名负例。""" login(page, ADMIN, admin_password) - open_menu(page, "用户管理") + open_menu(page, "用户管理", "系统管理") if current_path(page) != "/system/users": fail("菜单导航", f"未进入用户管理页,当前 {page.url}") print("✓ 1. 菜单导航 经侧边菜单进入用户管理") @@ -217,7 +164,7 @@ def step_user_edit_and_status(page: Page) -> None: def step_role_management(page: Page) -> None: """角色保护与角色 CRUD。""" - open_menu(page, "角色管理") + open_menu(page, "角色管理", "系统管理") if current_path(page) != "/system/roles": fail("菜单导航", f"未进入角色管理页,当前 {page.url}") @@ -259,7 +206,7 @@ def step_role_management(page: Page) -> None: def step_logs_query(page: Page) -> None: """操作日志(含详情懒加载与脱敏断言)与登录日志。""" - open_menu(page, "日志查询") + open_menu(page, "日志查询", "系统管理") if current_path(page) != "/system/logs": fail("菜单导航", f"未进入日志查询页,当前 {page.url}") @@ -297,7 +244,7 @@ def step_logs_query(page: Page) -> None: def step_permission_check(page: Page, admin_password: str) -> None: """普通用户菜单隐藏与路由守卫。""" - open_menu(page, "用户管理") + open_menu(page, "用户管理", "系统管理") create_user_via_ui(page, VIEWER_USERNAME, "E2E访客") page.click("text=退出登录") page.wait_for_url("**/login**", timeout=10_000) @@ -322,7 +269,7 @@ def step_permission_check(page: Page, admin_password: str) -> None: def step_cleanup(page: Page) -> None: """删除测试用户与角色(用 expect_toast 校验,失败不假绿)。""" - open_menu(page, "用户管理") + open_menu(page, "用户管理", "系统管理") for username in (NEW_USERNAME, VIEWER_USERNAME): row = find_user_row(page, username) if row.count() == 0: @@ -332,7 +279,7 @@ def step_cleanup(page: Page) -> None: confirm_message_box(page, "删除") expect_toast(page, "已删除", "数据清理") - open_menu(page, "角色管理") + open_menu(page, "角色管理", "系统管理") fill_filter(page, "关键词", NEW_ROLE_CODE) click_query(page) role_row = table_rows(page).filter(has_text=NEW_ROLE_CODE).first