import time from framework.core.base_page import BasePage from framework.core.logger import get_logger logger = get_logger("DevMachinePage") class DevMachinePage(BasePage): """开发机页面 POM - 纯原子操作层""" MENU_TEXT = "开发机" URL_FRAGMENT = "/dev-machine" def __init__(self, page): self.page = page def navigate_to(self): """进入开发机管理页面 (含强力跳转)""" logger.info("正在切换到【开发机】页面...") if self.URL_FRAGMENT in self.page.url: return for i in range(3): self.page.evaluate("document.querySelectorAll('.p-dialog-mask').forEach(el => el.style.display='none')") self.page.evaluate(f"""() => {{ const target = Array.from(document.querySelectorAll('.p-menuitem-link, a, span')) .find(el => el.innerText.trim() === '{self.MENU_TEXT}'); if (target) target.click(); }}""") time.sleep(3) if self.URL_FRAGMENT in self.page.url: logger.info("✅ 成功进入开发机页面") return # 强制重定向兜底 if self.URL_FRAGMENT not in self.page.url: target_url = self.page.url.split('#')[0].split('?')[0].replace('/file-manager', self.URL_FRAGMENT) self.page.goto(target_url) time.sleep(3) def get_first_machine_status(self): """查询首台实例状态""" try: self.page.wait_for_selector("tr", timeout=5000) status = self.page.evaluate("""() => { const rows = document.querySelectorAll('tr'); if (rows.length < 2) return 'None'; const cells = rows[1].querySelectorAll('td'); return cells.length > 2 ? cells[2].innerText.trim() : 'Unknown'; }""") logger.info(f"✅ 探测到首台开发机状态: {status}") return status except: return "Empty" def open_apply_dialog(self): """点击申请按钮""" logger.info("👉 点击 [申请开发机] 按钮") self.smart_click("申请开发机") time.sleep(1) def fill_name(self, name): """填写开发机名称""" logger.info(f"⌨️ 正在输入名称: {name}") self.smart_fill("名称", name) def select_sku(self, sku_id): """选择配置 SKU""" logger.info(f"🎯 尝试选择 SKU: {sku_id}") self.page.evaluate(f"""(id) => {{ const input = document.getElementById(id + '-input') || document.getElementById(id); if (input) {{ input.click(); return true; }} return false; }}""", sku_id) def select_image(self, image_keyword="CUDA"): """选择镜像""" logger.info(f"💿 正在选择镜像: {image_keyword}") self.smart_click("选择镜像") time.sleep(1) self.smart_click(image_keyword) time.sleep(1) self.smart_click("确定", role="button") time.sleep(1) def fill_ssh_key(self, ssh_key): """填写 SSH 公钥 (使用增强加固后的智能填写)""" logger.info(f"⌨️ 正在输入 SSH 公钥") self.smart_fill("SSH公钥", ssh_key) def fill_system_disk(self, size="100"): """填写系统盘大小""" logger.info(f"⌨️ 正在输入系统盘大小: {size}") self.smart_fill("请输入系统盘大小", size) def submit_application(self): """点击申请创建按钮""" logger.info("🚀 提交申请创建") self.smart_click("申请创建") time.sleep(2) def get_machine_status(self, machine_name): """查找指定名称机器的真实行文字 (精准锚点)""" return self.page.evaluate(f"""(name) => {{ // 优先查找表格行 const rows = Array.from(document.querySelectorAll('tr')); const dataRow = rows.find(r => {{ const text = r.innerText; // 必须包含名字,且为了避免误中面包屑,行内字符数应该较多 return text.includes(name) && text.trim().length > name.length + 5; }}); if (dataRow) return dataRow.innerText.trim(); // 如果没找到表格行,尝试找包含该名字的最近的一个容器类元素 const anyRow = Array.from(document.querySelectorAll('.p-datatable-row, .p-selectable-row')) .find(r => r.innerText.includes(name)); return anyRow ? anyRow.innerText.trim() : 'Not Found'; }}""", machine_name) def wait_for_status(self, name, expected_status, timeout=600): """带心跳的状态等待器 (增强版)""" logger.info(f"⏳ 等待开发机 {name} 状态变为: {expected_status}...") start = time.time() last_log = 0 while time.time() - start < timeout: current = self.get_machine_status(name) # 命中状态 if expected_status in current: logger.info(f"✅ 状态达标: {current}") return True # 心跳日志 if time.time() - last_log > 10: # 截取前 50 个字符避免日志过长 snippet = (current[:50] + '...') if len(current) > 50 else current logger.info(f" [状态巡检] {int(time.time()-start)}s | 当前实时内容: {snippet}") last_log = time.time() time.sleep(5) raise TimeoutError(f"超时: 巡检 400s 仍未发现关键字 [{expected_status}]。当前最后看到的内容: {current}") def _click_row_action(self, machine_name, action_label): """通用:点击指定机器行内的操作按钮(关机/删除等)""" logger.info(f"👉 在 {machine_name} 行内寻找并点击 [{action_label}] 按钮") # 策略1:在包含该机器名的行内,找 aria-label 匹配的按钮 clicked = self.page.evaluate(f"""(args) => {{ const [name, action] = args; const rows = Array.from(document.querySelectorAll('tr')); const row = rows.find(r => r.innerText.includes(name) && r.querySelectorAll('td').length > 0); if (!row) return 'NO_ROW'; // 尝试 aria-label 匹配 let btn = row.querySelector(`button[aria-label="${{action}}"], [aria-label="${{action}}"]`); // 尝试按钮文本匹配 if (!btn) {{ btn = Array.from(row.querySelectorAll('button, .p-button, a, span.p-button-label')) .find(el => el.innerText.trim() === action || (el.getAttribute('aria-label') || '').includes(action)); }} if (btn) {{ btn.scrollIntoView({{block: 'center'}}); btn.click(); return 'CLICKED'; }} return 'NO_BTN'; }}""", [machine_name, action_label]) logger.info(f" 行内按钮点击结果: {clicked}") if clicked == 'NO_ROW': raise Exception(f"❌ 表格中未找到包含 '{machine_name}' 的行") if clicked == 'NO_BTN': # 最后兜底:用 Playwright 原生按钮定位 logger.info(f" JS未命中,尝试 Playwright role 定位...") self.page.get_by_role("button", name=action_label).first.click() def _confirm_dialog(self): """确认弹窗""" time.sleep(0.5) try: # PrimeVue 确认弹窗 confirm = self.page.locator('.p-confirm-dialog-accept, .p-dialog-footer button.p-button-danger, .p-dialog-footer button.p-button-primary').first confirm.wait_for(state="visible", timeout=3000) confirm.click() except: try: self.smart_click("确定", role="button", timeout=3000) except: self.page.evaluate("""() => { const btn = Array.from(document.querySelectorAll('button')) .find(b => b.innerText.includes('确定') || b.innerText.includes('确认')); btn?.click(); }""") time.sleep(1) def delete_machine(self, machine_name): """删除开发机""" self.wait_for_status(machine_name, "已关机") logger.info(f"🎯 尝试删除开发机 {machine_name}") self._click_row_action(machine_name, "删除") self._confirm_dialog() time.sleep(2) def stop_machine(self, machine_name): """停止/关机 开发机""" self.wait_for_status(machine_name, "运行中") logger.info(f"🎯 尝试下发关机指令: {machine_name}") self._click_row_action(machine_name, "关机") self._confirm_dialog() time.sleep(2)