test/framework/business/dev_machine_page.py

216 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)