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}")