Update UI automation test files
This commit is contained in:
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001.png
Normal file
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001_01_open_page.png
Normal file
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001_01_open_page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001_02_after_login.png
Normal file
BIN
zhyy/screenshots/TC-zhyy-xsxt-hngl-001_02_after_login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
0
zhyy/test_case/Resource/UI/__init__.py
Normal file
0
zhyy/test_case/Resource/UI/__init__.py
Normal file
78
zhyy/test_case/Resource/UI/base_page.py
Normal file
78
zhyy/test_case/Resource/UI/base_page.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
import allure
|
||||
from playwright.sync_api import Page, Locator, expect
|
||||
|
||||
|
||||
class BasePage:
|
||||
"""公共页面基类:封装稳定等待、截图、候选定位器选择等通用能力。"""
|
||||
|
||||
def __init__(self, page: Page, screenshot_dir: str):
|
||||
self.page = page
|
||||
self.screenshot_dir = screenshot_dir
|
||||
os.makedirs(self.screenshot_dir, exist_ok=True)
|
||||
|
||||
def goto(self, url: str):
|
||||
self.page.goto(url, wait_until="domcontentloaded")
|
||||
try:
|
||||
self.page.wait_for_load_state("networkidle", timeout=5000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
path = os.path.join(self.screenshot_dir, name)
|
||||
self.page.screenshot(path=path, full_page=True)
|
||||
allure.attach.file(path, name=name, attachment_type=allure.attachment_type.PNG)
|
||||
return path
|
||||
|
||||
def first_visible_locator(
|
||||
self,
|
||||
selectors: list[str],
|
||||
timeout: int = 5000,
|
||||
description: str = "元素",
|
||||
) -> Locator:
|
||||
"""
|
||||
从候选 CSS/XPath/Text selector 中返回第一个可见元素。
|
||||
注意:页面侦察结果为空时,候选 selector 需在项目落地时根据真实 DOM 调整。
|
||||
"""
|
||||
last_error = None
|
||||
for selector in selectors:
|
||||
try:
|
||||
locator = self.page.locator(selector).first
|
||||
locator.wait_for(state="visible", timeout=timeout)
|
||||
return locator
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
|
||||
self.screenshot(f"not_found_{description}.png")
|
||||
raise AssertionError(f"未找到可见{description},请根据真实页面 DOM 更新 selector。候选:{selectors}") from last_error
|
||||
|
||||
def click_first_visible(
|
||||
self,
|
||||
selectors: list[str],
|
||||
timeout: int = 5000,
|
||||
description: str = "按钮",
|
||||
):
|
||||
locator = self.first_visible_locator(selectors, timeout=timeout, description=description)
|
||||
locator.click()
|
||||
|
||||
def fill_first_visible(
|
||||
self,
|
||||
selectors: list[str],
|
||||
value: str,
|
||||
timeout: int = 5000,
|
||||
description: str = "输入框",
|
||||
):
|
||||
locator = self.first_visible_locator(selectors, timeout=timeout, description=description)
|
||||
locator.fill(value)
|
||||
|
||||
def wait_network_idle(self):
|
||||
try:
|
||||
self.page.wait_for_load_state("networkidle", timeout=5000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def assert_url_contains(self, expected_part: str):
|
||||
expect(self.page).to_have_url(lambda url: expected_part in url)
|
||||
|
||||
def wait_for_timeout(self, ms: int):
|
||||
self.page.wait_for_timeout(ms)
|
||||
194
zhyy/test_case/Resource/UI/contract_manage_page.py
Normal file
194
zhyy/test_case/Resource/UI/contract_manage_page.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import re
|
||||
from playwright.sync_api import expect
|
||||
from zhyy.test_case.Resource.UI.base_page import BasePage
|
||||
|
||||
|
||||
class SmartManagementLoginPage(BasePage):
|
||||
"""
|
||||
智慧运营登录页对象。
|
||||
|
||||
TODO:
|
||||
当前未提供登录页侦察结果,以下 selector 为常见候选。
|
||||
落地时请优先使用真实 DOM 中稳定属性,例如 data-testid、id、name。
|
||||
"""
|
||||
|
||||
USERNAME_INPUT_SELECTORS = [
|
||||
"input[name='username']",
|
||||
"input[name='userName']",
|
||||
"input[id*='username']",
|
||||
"input[placeholder*='用户名']",
|
||||
"input[placeholder*='账号']",
|
||||
]
|
||||
|
||||
PASSWORD_INPUT_SELECTORS = [
|
||||
"input[name='password']",
|
||||
"input[type='password']",
|
||||
"input[placeholder*='密码']",
|
||||
]
|
||||
|
||||
LOGIN_BUTTON_SELECTORS = [
|
||||
"button:has-text('登录')",
|
||||
"button:has-text('登 录')",
|
||||
"[role='button']:has-text('登录')",
|
||||
]
|
||||
|
||||
def login_if_needed(self, username: str, password: str):
|
||||
"""
|
||||
如果当前页面出现登录表单则登录;如果已登录或 SSO 已生效则跳过。
|
||||
"""
|
||||
username_input = self.page.locator(",".join(self.USERNAME_INPUT_SELECTORS)).first
|
||||
try:
|
||||
username_input.wait_for(state="visible", timeout=5000)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
self.fill_first_visible(self.USERNAME_INPUT_SELECTORS, username, description="用户名输入框")
|
||||
self.fill_first_visible(self.PASSWORD_INPUT_SELECTORS, password, description="密码输入框")
|
||||
self.click_first_visible(self.LOGIN_BUTTON_SELECTORS, description="登录按钮")
|
||||
self.wait_network_idle()
|
||||
self.screenshot("after_login.png")
|
||||
|
||||
|
||||
class ContractManagePage(BasePage):
|
||||
"""
|
||||
合同管理页面对象。
|
||||
|
||||
被测页面:
|
||||
https://smart-management-web-st.best-envision.com/SZPurchase/PurchaseManage/PurchaseOrderManage
|
||||
|
||||
TODO:
|
||||
reconnaissanceResult 为空,缺少真实 DOM。
|
||||
以下 selector 采用“清晰候选 + TODO”方式,落地执行前需通过页面侦察结果替换为真实稳定 selector。
|
||||
"""
|
||||
|
||||
CONTRACT_MENU_SELECTORS = [
|
||||
# TODO: 使用真实菜单 DOM 替换,例如 [data-testid='contract-menu']
|
||||
"text=合同管理",
|
||||
"a:has-text('合同管理')",
|
||||
"li:has-text('合同管理')",
|
||||
"[role='menuitem']:has-text('合同管理')",
|
||||
]
|
||||
|
||||
CONTRACT_NO_INPUT_SELECTORS = [
|
||||
# TODO: 使用真实合同编号输入框 selector 替换
|
||||
"input[placeholder*='合同编号']",
|
||||
"input[aria-label*='合同编号']",
|
||||
".ant-form-item:has-text('合同编号') input",
|
||||
".el-form-item:has-text('合同编号') input",
|
||||
"label:has-text('合同编号') + div input",
|
||||
]
|
||||
|
||||
QUERY_BUTTON_SELECTORS = [
|
||||
"button:has-text('查询')",
|
||||
"button:has-text('搜索')",
|
||||
"[role='button']:has-text('查询')",
|
||||
"[role='button']:has-text('搜索')",
|
||||
]
|
||||
|
||||
TABLE_ROW_SELECTORS = [
|
||||
# Ant Design Table
|
||||
".ant-table-tbody > tr:not(.ant-table-placeholder)",
|
||||
# Element Plus / Element UI Table
|
||||
".el-table__body tbody tr",
|
||||
# 原生 table
|
||||
"table tbody tr",
|
||||
]
|
||||
|
||||
EMPTY_SELECTORS = [
|
||||
".ant-empty",
|
||||
".el-empty",
|
||||
"text=暂无数据",
|
||||
"text=无数据",
|
||||
"text=No Data",
|
||||
]
|
||||
|
||||
def open(self, url: str):
|
||||
self.goto(url)
|
||||
self.screenshot("contract_manage_opened.png")
|
||||
|
||||
def open_contract_menu_if_visible(self):
|
||||
"""
|
||||
用户步骤要求“找到合同菜单”。
|
||||
如果当前 URL 已直达合同管理页,则菜单可能无需点击。
|
||||
若菜单可见则点击;不可见不强制失败,避免直达 URL 场景误报。
|
||||
"""
|
||||
for selector in self.CONTRACT_MENU_SELECTORS:
|
||||
locator = self.page.locator(selector).first
|
||||
try:
|
||||
locator.wait_for(state="visible", timeout=2000)
|
||||
locator.click()
|
||||
self.wait_network_idle()
|
||||
self.screenshot("contract_menu_clicked.png")
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def search_by_contract_no_keyword(self, keyword: str):
|
||||
self.fill_first_visible(
|
||||
self.CONTRACT_NO_INPUT_SELECTORS,
|
||||
keyword,
|
||||
description="合同编号搜索框",
|
||||
)
|
||||
self.click_first_visible(
|
||||
self.QUERY_BUTTON_SELECTORS,
|
||||
description="查询按钮",
|
||||
)
|
||||
self.wait_network_idle()
|
||||
self.wait_for_timeout(1000)
|
||||
self.screenshot("contract_no_search_result.png")
|
||||
|
||||
def _table_rows_locator(self):
|
||||
for selector in self.TABLE_ROW_SELECTORS:
|
||||
rows = self.page.locator(selector)
|
||||
try:
|
||||
if rows.count() > 0 and rows.first.is_visible(timeout=2000):
|
||||
return rows
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def has_empty_result(self) -> bool:
|
||||
for selector in self.EMPTY_SELECTORS:
|
||||
try:
|
||||
locator = self.page.locator(selector).first
|
||||
if locator.is_visible(timeout=1000):
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def get_result_row_texts(self) -> list[str]:
|
||||
rows = self._table_rows_locator()
|
||||
if rows is None:
|
||||
return []
|
||||
|
||||
row_texts = []
|
||||
count = rows.count()
|
||||
for index in range(count):
|
||||
row = rows.nth(index)
|
||||
text = re.sub(r"\s+", " ", row.inner_text()).strip()
|
||||
if text:
|
||||
row_texts.append(text)
|
||||
return row_texts
|
||||
|
||||
def assert_contract_no_fuzzy_match(self, keyword: str):
|
||||
"""
|
||||
断言查询结果与合同编号模糊匹配条件一致。
|
||||
|
||||
说明:
|
||||
由于缺少真实表格列 selector,当前按整行文本包含 keyword 断言。
|
||||
落地后建议替换为“合同编号列单元格”精确读取:
|
||||
- AntD: 根据 th 文本定位列 index,再读取 tbody tr td[index]
|
||||
- Element: 使用列 prop 或 class 定位
|
||||
"""
|
||||
row_texts = self.get_result_row_texts()
|
||||
|
||||
assert row_texts, (
|
||||
f"合同编号关键字【{keyword}】查询后未获取到表格数据。"
|
||||
f"若业务允许无结果,请调整测试数据为存在的合同编号片段。"
|
||||
)
|
||||
|
||||
unmatched_rows = [text for text in row_texts if keyword not in text]
|
||||
assert not unmatched_rows, (
|
||||
f"存在与合同编号关键字【{keyword}】不匹配的查询结果:{unmatched_rows}"
|
||||
)
|
||||
208
zhyy/test_case/Resource/UI/contract_management_page.py
Normal file
208
zhyy/test_case/Resource/UI/contract_management_page.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import allure
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
from zhyy.test_case.Resource.UI.base_page import BasePage
|
||||
|
||||
|
||||
class ContractManagementPage(BasePage):
|
||||
"""合同管理页面对象。
|
||||
|
||||
注意:
|
||||
- 当前未提供页面侦察结果,以下 selector 采用“候选定位 + TODO”方式。
|
||||
- 落地执行前,建议通过侦察脚本确认真实 DOM 后替换 TODO selector。
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, screenshot_dir: str):
|
||||
super().__init__(page, screenshot_dir)
|
||||
|
||||
def open(self, url: str) -> None:
|
||||
with allure.step("打开合同管理被测页面"):
|
||||
self.goto(url)
|
||||
self.attach_screenshot("01_open_contract_management_page.png")
|
||||
|
||||
def login_if_needed(self, username: str, password: str) -> None:
|
||||
"""如页面跳转到登录页,则执行登录。
|
||||
|
||||
TODO:
|
||||
请根据真实登录页 DOM 替换用户名、密码、登录按钮 selector。
|
||||
当前保留常见中文系统候选定位,避免在无侦察结果时硬编码单一 selector。
|
||||
"""
|
||||
with allure.step("如需要则登录系统"):
|
||||
username_candidates = [
|
||||
self.page.get_by_placeholder(re.compile("用户名|账号|请输入用户名|请输入账号")),
|
||||
self.page.locator("input[name='username']"),
|
||||
self.page.locator("input[type='text']").first,
|
||||
# TODO: 替换为真实用户名输入框 selector,例如:self.page.locator("#username")
|
||||
]
|
||||
|
||||
password_candidates = [
|
||||
self.page.get_by_placeholder(re.compile("密码|请输入密码")),
|
||||
self.page.locator("input[name='password']"),
|
||||
self.page.locator("input[type='password']"),
|
||||
# TODO: 替换为真实密码输入框 selector,例如:self.page.locator("#password")
|
||||
]
|
||||
|
||||
login_button_candidates = [
|
||||
self.page.get_by_role("button", name=re.compile("登录|登 录|Login", re.I)),
|
||||
self.page.locator("button[type='submit']"),
|
||||
self.page.locator("text=登录"),
|
||||
# TODO: 替换为真实登录按钮 selector
|
||||
]
|
||||
|
||||
login_form_visible = any(
|
||||
locator.first.is_visible()
|
||||
for locator in username_candidates + password_candidates
|
||||
)
|
||||
|
||||
if not login_form_visible:
|
||||
return
|
||||
|
||||
self.fill_first_visible(username_candidates, username, description="用户名输入框")
|
||||
self.fill_first_visible(password_candidates, password, description="密码输入框")
|
||||
self.attach_screenshot("02_before_login.png")
|
||||
|
||||
self.click_first_visible(login_button_candidates, description="登录按钮")
|
||||
self.wait_network_idle()
|
||||
self.attach_screenshot("03_after_login.png")
|
||||
|
||||
def ensure_contract_menu_selected(self) -> None:
|
||||
"""进入/确认合同菜单。
|
||||
|
||||
若 URL 已直接进入合同管理页面,本方法不会强制点击菜单。
|
||||
TODO:
|
||||
如系统必须通过左侧菜单进入,请用页面真实菜单 selector 替换候选定位。
|
||||
"""
|
||||
with allure.step("找到并选择合同菜单"):
|
||||
menu_candidates = [
|
||||
self.page.get_by_role("menuitem", name=re.compile("合同管理|合同")),
|
||||
self.page.get_by_text(re.compile("^合同管理$|^合同$"), exact=False),
|
||||
self.page.locator("text=合同管理"),
|
||||
self.page.locator("text=合同"),
|
||||
# TODO: 替换为真实合同菜单 selector
|
||||
]
|
||||
|
||||
try:
|
||||
menu = self.first_visible_locator(
|
||||
menu_candidates,
|
||||
timeout=3000,
|
||||
description="合同菜单",
|
||||
)
|
||||
menu.click()
|
||||
self.wait_network_idle()
|
||||
except AssertionError:
|
||||
# 直接 URL 进入时可能没有可见菜单或菜单已选中,不阻断用例。
|
||||
pass
|
||||
|
||||
self.attach_screenshot("04_contract_menu_selected.png")
|
||||
|
||||
def search_by_contract_no_fuzzy(self, partial_contract_no: str) -> None:
|
||||
"""按合同编号进行模糊查询。"""
|
||||
with allure.step(f"输入合同编号部分字符并点击查询:{partial_contract_no}"):
|
||||
contract_no_input_candidates = [
|
||||
self.page.get_by_placeholder(re.compile("合同编号|请输入合同编号")),
|
||||
self.page.locator("input[placeholder*='合同编号']"),
|
||||
self.page.locator("label:has-text('合同编号')").locator("xpath=following::input[1]"),
|
||||
self.page.locator("text=合同编号").locator("xpath=following::input[1]"),
|
||||
# TODO: 替换为真实合同编号搜索框 selector
|
||||
]
|
||||
|
||||
query_button_candidates = [
|
||||
self.page.get_by_role("button", name=re.compile("^查询$|搜索")),
|
||||
self.page.locator("button:has-text('查询')"),
|
||||
self.page.locator("text=查询"),
|
||||
# TODO: 替换为真实查询按钮 selector
|
||||
]
|
||||
|
||||
self.fill_first_visible(
|
||||
contract_no_input_candidates,
|
||||
partial_contract_no,
|
||||
description="合同编号搜索框",
|
||||
)
|
||||
self.attach_screenshot("05_filled_contract_no.png")
|
||||
|
||||
self.click_first_visible(
|
||||
query_button_candidates,
|
||||
description="查询按钮",
|
||||
)
|
||||
self.wait_network_idle()
|
||||
|
||||
# 等待常见表格渲染完成
|
||||
self.wait_for_result_table()
|
||||
self.attach_screenshot("06_after_contract_no_search.png")
|
||||
|
||||
def wait_for_result_table(self) -> None:
|
||||
"""等待查询结果表格出现。
|
||||
|
||||
TODO:
|
||||
根据真实表格框架替换为唯一稳定 selector。
|
||||
"""
|
||||
table_candidates = [
|
||||
self.page.locator(".el-table__body-wrapper"),
|
||||
self.page.locator(".el-table__body"),
|
||||
self.page.locator(".ant-table-tbody"),
|
||||
self.page.locator("table tbody"),
|
||||
self.page.locator("[role='table']"),
|
||||
# TODO: 替换为真实结果表格 selector
|
||||
]
|
||||
self.first_visible_locator(table_candidates, timeout=10000, description="合同结果表格")
|
||||
|
||||
def result_rows_locator(self):
|
||||
"""结果表格行候选。
|
||||
|
||||
TODO:
|
||||
建议替换为合同列表真实数据行 selector。
|
||||
"""
|
||||
candidates = [
|
||||
self.page.locator(".el-table__body tbody tr"),
|
||||
self.page.locator(".ant-table-tbody tr"),
|
||||
self.page.locator("table tbody tr"),
|
||||
self.page.locator("[role='row']"),
|
||||
# TODO: 替换为真实合同列表行 selector
|
||||
]
|
||||
|
||||
for locator in candidates:
|
||||
try:
|
||||
if locator.count() > 0:
|
||||
return locator
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
raise AssertionError("未找到合同列表数据行,请根据真实页面补充 result_rows_locator selector。")
|
||||
|
||||
def get_result_row_texts(self) -> list[str]:
|
||||
with allure.step("获取合同列表查询结果"):
|
||||
rows = self.result_rows_locator()
|
||||
texts = self.text_contents(rows)
|
||||
# 过滤空行、表头、加载占位行
|
||||
return [
|
||||
text
|
||||
for text in texts
|
||||
if text and "暂无数据" not in text and "No Data" not in text
|
||||
]
|
||||
|
||||
def assert_contract_no_fuzzy_match(self, partial_contract_no: str) -> None:
|
||||
"""断言查询结果与合同编号模糊查询条件一致。
|
||||
|
||||
当前无页面侦察结果,无法确认“合同编号”列的真实 selector。
|
||||
因此先断言每条结果行文本中包含输入的合同编号部分字符。
|
||||
如果后续确认合同编号列 selector,请改为仅校验合同编号列文本。
|
||||
"""
|
||||
with allure.step("断言列表仅展示合同编号与输入内容模糊匹配的数据"):
|
||||
row_texts = self.get_result_row_texts()
|
||||
|
||||
assert row_texts, "查询结果为空,无法验证合同编号模糊匹配。请确认测试数据或查询条件。"
|
||||
|
||||
unmatched_rows = [
|
||||
row_text
|
||||
for row_text in row_texts
|
||||
if partial_contract_no not in row_text
|
||||
]
|
||||
|
||||
assert not unmatched_rows, (
|
||||
f"存在与合同编号查询条件不匹配的结果。\n"
|
||||
f"查询条件:{partial_contract_no}\n"
|
||||
f"不匹配行:{unmatched_rows}"
|
||||
)
|
||||
274
zhyy/test_case/Resource/UI/hngl_page.py
Normal file
274
zhyy/test_case/Resource/UI/hngl_page.py
Normal file
@@ -0,0 +1,274 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import allure
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
from zhyy.test_case.Resource.UI.base_page import BasePage
|
||||
|
||||
|
||||
class SmartManagementLoginPage(BasePage):
|
||||
"""智慧运营登录页。"""
|
||||
|
||||
# TODO:如项目已有页面侦察结果,请替换为真实 selector
|
||||
USERNAME_INPUT_SELECTORS = [
|
||||
'input[name="username"]',
|
||||
'input[id="username"]',
|
||||
'input[placeholder*="用户名"]',
|
||||
'input[placeholder*="账号"]',
|
||||
'input[type="text"]',
|
||||
]
|
||||
|
||||
PASSWORD_INPUT_SELECTORS = [
|
||||
'input[name="password"]',
|
||||
'input[id="password"]',
|
||||
'input[placeholder*="密码"]',
|
||||
'input[type="password"]',
|
||||
]
|
||||
|
||||
LOGIN_BUTTON_SELECTORS = [
|
||||
'button:has-text("登录")',
|
||||
'button:has-text("登 录")',
|
||||
'button[type="submit"]',
|
||||
]
|
||||
|
||||
def is_login_page(self) -> bool:
|
||||
url = self.page.url.lower()
|
||||
if "login" in url:
|
||||
return True
|
||||
try:
|
||||
self.first_visible_locator(self.PASSWORD_INPUT_SELECTORS, timeout=2000, description="密码输入框")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@allure.step("登录智慧运营系统")
|
||||
def login_if_required(self, username: str, password: str):
|
||||
if not self.is_login_page():
|
||||
return
|
||||
|
||||
self.fill_first_visible(self.USERNAME_INPUT_SELECTORS, username, description="用户名输入框")
|
||||
self.fill_first_visible(self.PASSWORD_INPUT_SELECTORS, password, description="密码输入框")
|
||||
self.click_first_visible(self.LOGIN_BUTTON_SELECTORS, description="登录按钮")
|
||||
self.wait_for_page_ready()
|
||||
|
||||
|
||||
class ContractManagementPage(BasePage):
|
||||
"""合同管理页面:封装合同编号模糊查询能力。"""
|
||||
|
||||
PAGE_URL = "https://smart-management-web-st.best-envision.com/SZPurchase/PurchaseManage/PurchaseOrderManage"
|
||||
|
||||
# TODO:页面侦察结果为空,以下为基于业务语义的候选定位;落地时建议替换为真实稳定 selector/data-testid
|
||||
CONTRACT_MENU_SELECTORS = [
|
||||
'a:has-text("合同管理")',
|
||||
'li:has-text("合同管理")',
|
||||
'span:has-text("合同管理")',
|
||||
'div:has-text("合同管理")',
|
||||
]
|
||||
|
||||
CONTRACT_NO_INPUT_SELECTORS = [
|
||||
# Ant Design 常见结构:label 为“合同编号”后紧邻输入框
|
||||
'xpath=//*[normalize-space()="合同编号"]/following::input[1]',
|
||||
'input[placeholder*="合同编号"]',
|
||||
'input[name*="contract"]',
|
||||
'input[id*="contract"]',
|
||||
# TODO:补充页面侦察得到的合同编号输入框 selector
|
||||
]
|
||||
|
||||
QUERY_BUTTON_SELECTORS = [
|
||||
'button:has-text("查询")',
|
||||
'button:has-text("搜索")',
|
||||
# TODO:补充页面侦察得到的查询按钮 selector
|
||||
]
|
||||
|
||||
RESET_BUTTON_SELECTORS = [
|
||||
'button:has-text("重置")',
|
||||
'button:has-text("清空")',
|
||||
]
|
||||
|
||||
TABLE_ROW_SELECTORS = [
|
||||
# Ant Design 表格常见结构
|
||||
'.ant-table-tbody > tr:not(.ant-table-placeholder)',
|
||||
'table tbody tr',
|
||||
# TODO:补充页面侦察得到的列表行 selector
|
||||
]
|
||||
|
||||
TABLE_HEADER_CELL_SELECTORS = [
|
||||
'.ant-table-thead th',
|
||||
'table thead th',
|
||||
# TODO:补充页面侦察得到的表头 selector
|
||||
]
|
||||
|
||||
EMPTY_SELECTORS = [
|
||||
'.ant-empty',
|
||||
'text=暂无数据',
|
||||
'text=无数据',
|
||||
]
|
||||
|
||||
def __init__(self, page: Page):
|
||||
super().__init__(page)
|
||||
self.login_page = SmartManagementLoginPage(page)
|
||||
|
||||
@allure.step("打开合同管理页面")
|
||||
def open(self, username: str, password: str):
|
||||
self.goto(self.PAGE_URL)
|
||||
self.login_page.login_if_required(username, password)
|
||||
|
||||
# 登录后如回到首页,则再次进入目标页面
|
||||
if "PurchaseOrderManage" not in self.page.url:
|
||||
self.goto(self.PAGE_URL)
|
||||
self.wait_for_page_ready()
|
||||
|
||||
self.ensure_contract_management_page_loaded()
|
||||
|
||||
@allure.step("确认合同管理页面加载完成")
|
||||
def ensure_contract_management_page_loaded(self):
|
||||
"""
|
||||
优先确认页面 URL,其次确认页面上存在合同相关筛选项。
|
||||
"""
|
||||
if "PurchaseOrderManage" in self.page.url:
|
||||
return
|
||||
|
||||
try:
|
||||
self.first_visible_locator(self.CONTRACT_MENU_SELECTORS, timeout=5000, description="合同管理菜单")
|
||||
return
|
||||
except Exception as exc:
|
||||
raise AssertionError(f"合同管理页面未成功加载,当前 URL={self.page.url}") from exc
|
||||
|
||||
@allure.step("点击合同管理菜单")
|
||||
def click_contract_menu_if_visible(self):
|
||||
"""
|
||||
用户步骤要求“找到合同菜单”;若当前已在目标页面,则不强制点击。
|
||||
"""
|
||||
if "PurchaseOrderManage" in self.page.url:
|
||||
return
|
||||
|
||||
self.click_first_visible(self.CONTRACT_MENU_SELECTORS, timeout=5000, description="合同管理菜单")
|
||||
self.wait_for_page_ready()
|
||||
|
||||
@allure.step("输入合同编号查询条件:{contract_no_keyword}")
|
||||
def input_contract_no_keyword(self, contract_no_keyword: str):
|
||||
try:
|
||||
self.page.get_by_label("合同编号", exact=False).fill(contract_no_keyword)
|
||||
except Exception:
|
||||
self.fill_first_visible(
|
||||
self.CONTRACT_NO_INPUT_SELECTORS,
|
||||
contract_no_keyword,
|
||||
description="合同编号搜索框",
|
||||
)
|
||||
|
||||
@allure.step("点击查询按钮")
|
||||
def click_query(self):
|
||||
try:
|
||||
self.page.get_by_role("button", name="查询").click()
|
||||
self.wait_for_page_ready()
|
||||
except Exception:
|
||||
self.click_first_visible(self.QUERY_BUTTON_SELECTORS, description="查询按钮")
|
||||
|
||||
# 等待列表刷新完成
|
||||
self.page.wait_for_timeout(1000)
|
||||
|
||||
@allure.step("按合同编号模糊查询:{contract_no_keyword}")
|
||||
def search_by_contract_no(self, contract_no_keyword: str):
|
||||
self.input_contract_no_keyword(contract_no_keyword)
|
||||
self.click_query()
|
||||
|
||||
def _get_contract_no_column_index(self) -> Optional[int]:
|
||||
"""
|
||||
根据表头文本动态识别“合同编号”列索引。
|
||||
识别失败时返回 None,断言时退化为行文本包含校验。
|
||||
"""
|
||||
for selector in self.TABLE_HEADER_CELL_SELECTORS:
|
||||
headers = self.page.locator(selector)
|
||||
try:
|
||||
count = headers.count()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for index in range(count):
|
||||
text = headers.nth(index).inner_text(timeout=2000).strip()
|
||||
if "合同编号" in text:
|
||||
return index
|
||||
return None
|
||||
|
||||
def get_result_rows_text(self) -> List[str]:
|
||||
for selector in self.TABLE_ROW_SELECTORS:
|
||||
rows = self.page.locator(selector)
|
||||
try:
|
||||
count = rows.count()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
row_texts = []
|
||||
for index in range(count):
|
||||
row = rows.nth(index)
|
||||
text = row.inner_text(timeout=3000).strip()
|
||||
if text:
|
||||
row_texts.append(text)
|
||||
if row_texts:
|
||||
return row_texts
|
||||
return []
|
||||
|
||||
def get_result_contract_numbers(self) -> List[str]:
|
||||
"""
|
||||
返回结果列表中的合同编号列文本。
|
||||
若无法识别合同编号列,则返回空列表,由上层断言决定是否退化为行文本校验。
|
||||
"""
|
||||
column_index = self._get_contract_no_column_index()
|
||||
if column_index is None:
|
||||
return []
|
||||
|
||||
for selector in self.TABLE_ROW_SELECTORS:
|
||||
rows = self.page.locator(selector)
|
||||
try:
|
||||
row_count = rows.count()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
contract_numbers = []
|
||||
for row_index in range(row_count):
|
||||
row = rows.nth(row_index)
|
||||
cells = row.locator("td")
|
||||
if cells.count() > column_index:
|
||||
contract_no = cells.nth(column_index).inner_text(timeout=3000).strip()
|
||||
if contract_no:
|
||||
contract_numbers.append(contract_no)
|
||||
|
||||
if contract_numbers:
|
||||
return contract_numbers
|
||||
|
||||
return []
|
||||
|
||||
def is_empty_result(self) -> bool:
|
||||
for selector in self.EMPTY_SELECTORS:
|
||||
try:
|
||||
if self.page.locator(selector).first.is_visible(timeout=2000):
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
@allure.step("断言列表仅展示合同编号模糊匹配:{contract_no_keyword}")
|
||||
def assert_contract_no_fuzzy_match(self, contract_no_keyword: str):
|
||||
"""
|
||||
优先按“合同编号”列断言;
|
||||
如因页面侦察缺失无法识别列,则退化为断言每行文本均包含查询关键字。
|
||||
"""
|
||||
expect(self.page.locator("body")).to_be_visible()
|
||||
|
||||
contract_numbers = self.get_result_contract_numbers()
|
||||
if contract_numbers:
|
||||
assert all(contract_no_keyword in item for item in contract_numbers), (
|
||||
f"存在合同编号不匹配查询条件:keyword={contract_no_keyword}, "
|
||||
f"contract_numbers={contract_numbers}"
|
||||
)
|
||||
return
|
||||
|
||||
row_texts = self.get_result_rows_text()
|
||||
if row_texts:
|
||||
assert all(contract_no_keyword in row_text for row_text in row_texts), (
|
||||
f"无法识别合同编号列,已退化为行文本校验;存在行不包含查询条件:"
|
||||
f"keyword={contract_no_keyword}, rows={row_texts}"
|
||||
)
|
||||
return
|
||||
|
||||
assert self.is_empty_result(), "查询后列表无数据且未识别到空数据提示,请检查表格 selector 或查询条件"
|
||||
47
zhyy/test_case/Resource/UI/login_page.py
Normal file
47
zhyy/test_case/Resource/UI/login_page.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from zhyy.test_case.Resource.UI.base_page import BasePage
|
||||
|
||||
|
||||
class LoginPage(BasePage):
|
||||
"""
|
||||
登录页对象。
|
||||
|
||||
注意:
|
||||
当前用例未提供登录页侦察结果,因此以下 selector 使用 TODO 占位。
|
||||
落地执行前请通过页面侦察补充真实 selector。
|
||||
"""
|
||||
|
||||
USERNAME_INPUT = "TODO_LOGIN_USERNAME_INPUT_SELECTOR"
|
||||
PASSWORD_INPUT = "TODO_LOGIN_PASSWORD_INPUT_SELECTOR"
|
||||
LOGIN_BUTTON = "TODO_LOGIN_BUTTON_SELECTOR"
|
||||
|
||||
def __init__(self, page: Page):
|
||||
super().__init__(page)
|
||||
|
||||
def is_login_page(self) -> bool:
|
||||
"""
|
||||
判断当前是否处于登录页。
|
||||
由于缺少真实登录页 selector,这里通过 TODO selector 判断。
|
||||
若系统已通过 SSO 或已有登录态直接进入业务页,可返回 False。
|
||||
"""
|
||||
if "TODO" in self.USERNAME_INPUT:
|
||||
# 未补充登录页定位时,不主动执行登录,避免误判。
|
||||
return False
|
||||
|
||||
try:
|
||||
return self.page.locator(self.USERNAME_INPUT).first.is_visible(timeout=3_000)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def login_if_needed(self, username: str, password: str):
|
||||
"""
|
||||
如当前页面需要登录,则执行登录。
|
||||
"""
|
||||
if not self.is_login_page():
|
||||
return
|
||||
|
||||
self.fill(self.USERNAME_INPUT, username)
|
||||
self.fill(self.PASSWORD_INPUT, password)
|
||||
self.click(self.LOGIN_BUTTON)
|
||||
self.wait_for_network_idle()
|
||||
0
zhyy/test_case/Resource/__init__.py
Normal file
0
zhyy/test_case/Resource/__init__.py
Normal file
@@ -1,6 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import pytest
|
||||
|
||||
|
||||
def test_tc_zhyy_ui_api_verify_001():
|
||||
assert "智慧运营"
|
||||
Reference in New Issue
Block a user