test/framework/core/base_page.py

214 lines
9.9 KiB
Python

from framework.core.logger import get_logger
import time
logger = get_logger("BasePage")
class BasePage:
"""所有页面对象的基类,提供智能识别与常用操作封装"""
def __init__(self, page):
self.page = page
def smart_find(self, description, role=None, timeout=5000):
"""核心智能定位器:按优先级尝试语义定位"""
strategies = [
lambda: self.page.get_by_role(role, name=description, exact=False) if role else None,
lambda: self.page.get_by_text(description, exact=False),
# 新增:直接属性扫描以应对 aria-label 类按钮
lambda: self.page.locator(f'[aria-label*="{description}"]').first,
lambda: self.page.get_by_placeholder(description, exact=False),
lambda: self.page.get_by_label(description, exact=False),
lambda: self.page.get_by_title(description, exact=False)
]
for get_loc in strategies:
try:
loc = get_loc()
if loc and loc.count() > 0:
return loc.first
except:
continue
# 兜底:尝试 CSS
if any(c in description for c in [".", "#", "[", ">"]):
try:
return self.page.locator(description).first
except:
pass
raise Exception(f"❌ 无法在页面找到元素: '{description}'")
def smart_click(self, text, role=None, timeout=5000):
"""语义化点击:带重试与可见性校验"""
logger.info(f"👉 [SmartClick] 尝试点击: {text}")
try:
el = self.smart_find(text, role=role, timeout=timeout)
# 等待可见与稳定
el.wait_for(state="visible", timeout=timeout)
# 使用自带重试的点击逻辑,应对 SPA 瞬时刷新
try:
el.click(timeout=timeout)
except Exception as e:
if "detached" in str(e).lower():
logger.warning(f"⚠️ 节点失效,正在重新寻找并点击: {text}")
self.smart_find(text, role=role, timeout=timeout).click(timeout=timeout)
else:
raise e
except Exception as e:
# 暴力循环 JS 点击作为最终兜底
success = self.page.evaluate(f"""(txt) => {{
const target = Array.from(document.querySelectorAll('button, a, span, .p-button-label, label, input'))
.find(el => {{
const it = el.innerText.trim();
const al = el.getAttribute('aria-label') || "";
const ph = el.getAttribute('placeholder') || "";
return it.includes(txt) || al.includes(txt) || ph.includes(txt);
}});
if (target) {{
target.scrollIntoView({{behavior: "smooth", block: "center"}});
setTimeout(() => target.click(), 100);
return true;
}}
return false;
}}""", text)
if not success:
raise e
def smart_fill(self, label, value, timeout=5000):
"""语义化输入:支持 Vue/React 的深度绑定注入"""
logger.info(f"⌨️ [SmartFill] 在 [{label}] 填写: {value}")
try:
# 1. 尝试直接命中 (Label/Placeholder等原生属性)
el = self.smart_find(label, timeout=timeout)
el.wait_for(state="visible", timeout=timeout)
el.fill(value, timeout=timeout)
except Exception as first_err:
try:
# 2. 邻近注入方案:借助 JS 寻找关联输入框,并使用 Playwright 原生 fill 保障前端框架双向绑定
logger.info(f"🔍 [SmartFill] 常规规则未命中,正在执行深度邻近探测 [{label}]...")
success = self.page.evaluate(f"""(lbl) => {{
document.querySelectorAll('[data-smart-target]').forEach(e => e.removeAttribute('data-smart-target'));
// 1. 过滤:必须是有高度/宽度的真实可见节点
const allNodes = Array.from(document.querySelectorAll('label, span, div, p, legend'))
.filter(el => el.offsetWidth > 0 && el.offsetHeight > 0 && el.innerText && el.innerText.includes(lbl));
// 2. 最深层过滤
const tags = allNodes.filter(el => {{
return !Array.from(el.children).some(child => child.offsetWidth > 0 && child.innerText && child.innerText.includes(lbl));
}});
for (const t of tags) {{
const container = t.closest('.p-inputgroup, .field, .flex, div');
const input = container?.querySelector('input:not([type="hidden"]), textarea') ||
t.parentElement?.querySelector('input:not([type="hidden"]), textarea');
if (input && input.offsetWidth > 0) {{
input.setAttribute('data-smart-target', 'true');
return true;
}}
}}
return false;
}}""", label)
if success:
target = self.page.locator("[data-smart-target='true']").first
target.scroll_into_view_if_needed()
target.fill(value, timeout=timeout)
target.blur()
else:
logger.warning(f"⚠️ [SmartFill] JS 探测失败: 页面上可见区域内没找到 '{label}' 及其相邻输入框。")
raise first_err
except Exception as second_err:
logger.error(f"⚠️ [SmartFill] JS注入或执行时发生错误: {second_err}")
raise second_err
def smart_delete(self,text,role=None,timeout=5000):
logger.info(f"👉 [SmartDelete] 尝试删除: {text}")
try:
el = self.smart_find(text, role=role, timeout=timeout)
el.wait_for(state="visible", timeout=timeout)
el.click(timeout=timeout)
except Exception as e:
logger.error(f"⚠️ [SmartDelete] JS注入或执行时发生错误: {e}")
raise e
def smart_select(self, label, value):
"""智能下拉框选择 (适配 PrimeVue)"""
logger.info(f"📋 [SmartSelect] 在 [{label}] 下拉框寻找目标: {value}")
# 1. 寻找并打开下拉框
opened = self.page.evaluate("""([lbl, val]) => {
const root = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]') || document;
const elements = Array.from(root.querySelectorAll('label, span, div'));
const targetLabel = elements.find(el => {
const t = el.innerText?.trim();
return t && (t === lbl || t.includes(lbl)) && el.offsetWidth > 0;
});
if (!targetLabel) return 'NO_LABEL';
let container = targetLabel;
for (let i = 0; i < 8; i++) {
if (!container) break;
// 查找下拉框容器
const dd = container.querySelector('.p-select, .p-dropdown, [data-pc-name="select"], [data-pc-name="dropdown"]');
if (dd && dd.offsetWidth > 0) {
dd.scrollIntoView({block: 'center'});
dd.click();
return 'OPENED';
}
// 检查兄弟元素
if (container.parentElement) {
const sibling = container.parentElement.querySelector('.p-select, .p-dropdown, [data-pc-name="select"]');
if (sibling && sibling.offsetWidth > 0) {
sibling.scrollIntoView({block: 'center'});
sibling.click();
return 'OPENED';
}
}
container = container.parentElement;
}
return 'NO_DROPDOWN';
}""", [label, value])
if opened != 'OPENED':
logger.warning(f"⚠️ [SmartSelect] 无法精准找到标签 '{label}' 关联的下拉框,尝试直接搜索首个 visible 的下拉框...")
self.page.locator(".p-select, .p-dropdown").first.click()
time.sleep(1)
# 2. 尝试多种定位器点击选项
try:
# 优先精准文本
selectors = [
f"li[role='option']:has-text('{value}')",
f".p-select-option:has-text('{value}')",
f".p-dropdown-item:has-text('{value}')"
]
for sel in selectors:
loc = self.page.locator(sel).first
if loc.is_visible(timeout=500):
loc.click()
logger.info(f"✅ [SmartSelect] 成功通过选择器选中: {value}")
return True
except:
pass
# 3. JS 暴力匹配并点击
clicked = self.page.evaluate("""(val) => {
const options = Array.from(document.querySelectorAll('li[role="option"], .p-select-option, .p-dropdown-item'));
const target = options.find(opt => opt.offsetWidth > 0 && opt.innerText.includes(val));
if (target) {
target.scrollIntoView({block: 'center'});
target.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
return true;
}
return false;
}""", value)
if clicked:
logger.info(f"✅ [SmartSelect] 通过 JS 暴力匹配选中: {value}")
return True
raise Exception(f"❌ [SmartSelect] 无法在下拉框中选择选项: {value}")