275 lines
9.7 KiB
Python
275 lines
9.7 KiB
Python
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 或查询条件"
|