202 lines
8.9 KiB
Python
202 lines
8.9 KiB
Python
import time
|
||
from framework.core.base_page import BasePage
|
||
from framework.core.logger import get_logger
|
||
from framework.config.settings import Config
|
||
|
||
logger = get_logger("MirrorAssetsPage")
|
||
|
||
class MirrorAssetsPage(BasePage):
|
||
"""镜像资产页面操作"""
|
||
|
||
def __init__(self, page):
|
||
super().__init__(page)
|
||
|
||
def navigate_to(self):
|
||
"""导航到镜像资产页面"""
|
||
logger.info("🚀 导航到镜像资产页面")
|
||
# 根据截图修正 URL
|
||
self.page.goto(f"{Config.BASE_URL}/mirror-center/private")
|
||
time.sleep(3)
|
||
|
||
def click_my_mirror(self):
|
||
"""进入镜像资产并确保切换到 [我的镜像] (Prod 强化版)"""
|
||
logger.info("👉 准备切换至 [我的镜像] 标签")
|
||
|
||
# 1. 等待页面基础渲染
|
||
timeout = 15
|
||
start_time = time.time()
|
||
while time.time() - start_time < timeout:
|
||
# 尝试定位任何包含“我的镜像”的可见元素
|
||
loc = self.page.locator("text='我的镜像'").filter(has_not=self.page.locator(".p-hidden")).first
|
||
if loc.is_visible():
|
||
loc.click()
|
||
logger.info("✅ 已点击 [我的镜像]")
|
||
break
|
||
time.sleep(1)
|
||
else:
|
||
logger.error(f"❌ 15s 后仍未看到 [我的镜像] 标签。URL: {self.page.url}")
|
||
# 这里的 JS 诊断会更彻底一些
|
||
diag_log = self.page.evaluate("""() => {
|
||
const all = Array.from(document.querySelectorAll('*'));
|
||
return all.filter(el => el.innerText && el.innerText.includes('镜像') && el.offsetWidth > 0)
|
||
.map(el => ({ tag: el.tagName, text: el.innerText.trim().slice(0, 20), id: el.id, class: el.className }))
|
||
.slice(0, 10);
|
||
}""")
|
||
logger.info(f"🔍 故障诊断 - 包含'镜像'关键词的可见元素: {diag_log}")
|
||
raise Exception("未能定位到 [我的镜像] 标签页")
|
||
|
||
# 2. 确认切换成功 (等待高亮)
|
||
try:
|
||
self.page.wait_for_selector(".p-highlight, [aria-selected='true'], .active", timeout=5000)
|
||
logger.info("✅ 标签页高亮确认成功")
|
||
except:
|
||
logger.warning("⚠️ 未能确认高亮状态,但已尝试点击")
|
||
|
||
# 3. 记录当前 Tab 状态
|
||
tabs = self.page.evaluate("""() => {
|
||
return Array.from(document.querySelectorAll('li, a, span, div'))
|
||
.filter(el => ['我的镜像', '群组镜像'].some(t => el.innerText?.includes(t)) && el.offsetWidth > 0)
|
||
.map(el => el.innerText.trim().slice(0,10));
|
||
}""")
|
||
logger.info(f"📊 当前侦测到的标签页: {list(set(tabs))}")
|
||
time.sleep(2)
|
||
|
||
def click_add_button(self):
|
||
"""点击 [添加镜像] 按钮"""
|
||
logger.info("👉 点击 [添加镜像] 按钮")
|
||
self.smart_click("添加镜像")
|
||
|
||
def click_mirror_in_list(self, mirror_name=None):
|
||
"""点击列表末尾的可用镜像 (不限名称)"""
|
||
# 调试日志:探测当前所有镜像状态
|
||
try:
|
||
dom_info = self.page.evaluate("""() => {
|
||
const rows = Array.from(document.querySelectorAll('.p-datatable-row, tr, .p-card'));
|
||
return rows.filter(r => r.innerText.includes('可用') || r.innerText.includes('Available'))
|
||
.map(r => r.innerText.slice(0, 50).replace(/\\n/g, ' '));
|
||
}""")
|
||
logger.info(f"📊 当前页面可用镜像列表: {dom_info}")
|
||
except:
|
||
pass
|
||
|
||
logger.info("👉 尝试开启【可用】镜像列表中最后一个详情页")
|
||
# 直接通过状态文案定位行或卡片
|
||
selector = "tr:has-text('可用'), div.p-card:has-text('可用'), .p-datatable-row:has-text('可用')"
|
||
|
||
try:
|
||
# 等待至少一个可用镜像
|
||
self.page.wait_for_selector(selector, timeout=10000)
|
||
locators = self.page.locator(selector)
|
||
count = locators.count()
|
||
if count > 0:
|
||
logger.info(f"✅ 找到 {count} 个可用镜像,准备点击最后一个...")
|
||
target = locators.nth(count - 1)
|
||
target.scroll_into_view_if_needed()
|
||
target.click()
|
||
else:
|
||
raise Exception("未找到处于 '可用' 状态的镜像")
|
||
except Exception as e:
|
||
logger.error(f"❌ 镜像列表尝试点击失败: {e}")
|
||
# 暴力兜底:点击表格最后一行的第一个单元格
|
||
self.page.evaluate("""() => {
|
||
const rows = document.querySelectorAll('.p-datatable-row, tr');
|
||
if (rows.length > 0) rows[rows.length - 1].click();
|
||
}""")
|
||
|
||
# 跳转监控
|
||
logger.info("⏳ 等待跳转至详情页 (检测 [快速创建] 按钮)...")
|
||
try:
|
||
self.page.wait_for_selector("text='快速创建'", timeout=15000)
|
||
logger.info("✅ 已进入详情页")
|
||
except:
|
||
logger.error("❌ 跳转详情页失败,可能点击未奏效")
|
||
raise Exception("进入详情页超时")
|
||
|
||
def click_quick_create(self):
|
||
"""点击 [快速创建] 按钮 (增加 JS 兜底)"""
|
||
logger.info("👉 点击 [快速创建] 按钮")
|
||
try:
|
||
# 策略1:Playwright 智能点击
|
||
self.smart_click("快速创建", timeout=5000)
|
||
except:
|
||
# 策略2:JS 暴力点击
|
||
logger.warning("⚠️ smart_click 失败,正在执行 JS [快速创建] 强制点击")
|
||
self.page.evaluate("""() => {
|
||
const buttons = Array.from(document.querySelectorAll('button, a, .p-button'));
|
||
const target = buttons.find(b => b.innerText && b.innerText.includes('快速创建'));
|
||
if (target) {
|
||
target.scrollIntoView({block: 'center'});
|
||
target.click();
|
||
}
|
||
}""")
|
||
|
||
time.sleep(3) # 给弹窗充足的渲染时间
|
||
|
||
def fill_mirror_name(self, name):
|
||
"""输入名称 (增加深度探测与日志)"""
|
||
logger.info(f"⌨️ 正在尝试定位 [名称] 输入框...")
|
||
|
||
# 1. 尝试常见业务 Label (智能探测模式)
|
||
for lbl in ["名称", "桌面名称", "实例名称"]:
|
||
try:
|
||
# 尝试极短的时间验证,快进快出
|
||
self.smart_fill(lbl, name, timeout=2000)
|
||
logger.info(f"✅ 成功命中标签 [{lbl}]")
|
||
return
|
||
except:
|
||
continue
|
||
|
||
# 2. 尝试 Placeholder 模式
|
||
try:
|
||
loc = self.page.locator("input[placeholder*='名称'], input[placeholder*='name']").first
|
||
if loc.is_visible():
|
||
loc.fill(name)
|
||
logger.info("✅ 通过 Placeholder 定位成功")
|
||
return
|
||
except:
|
||
pass
|
||
|
||
# 3. 终极兜底:弹窗内的第一个可见 input (针对弹窗内 label 定位偏移的情况)
|
||
try:
|
||
# 找到当前可见的 dialog
|
||
dialog = self.page.locator(".p-dialog, [role='dialog']").first
|
||
if dialog.is_visible():
|
||
target_input = dialog.locator("input:not([type='hidden'])").first
|
||
target_input.fill(name)
|
||
logger.info("✅ 通过 [弹窗内首个输入框] 定位成功")
|
||
return
|
||
except:
|
||
pass
|
||
|
||
# 4. 诊断日志:如果走到这里说明彻底找不到,获取当前页面所有输入框上下文,发往日志
|
||
try:
|
||
dom_report = self.page.evaluate("""() => {
|
||
const inputs = Array.from(document.querySelectorAll('input, select, textarea'));
|
||
return inputs.map(i => {
|
||
const visible = i.offsetWidth > 0;
|
||
if(!visible) return null;
|
||
return {
|
||
tag: i.tagName,
|
||
placeholder: i.placeholder,
|
||
id: i.id,
|
||
className: i.className,
|
||
parentText: i.parentElement?.innerText?.slice(0, 30)
|
||
};
|
||
}).filter(x => x);
|
||
}""")
|
||
logger.error(f"❌ 无法定位输入框。当前页面所有可见输入框报告: {dom_report}")
|
||
except:
|
||
pass
|
||
|
||
raise Exception(f"❌ 无法在镜像创建弹窗找到名称输入框,报告已记录")
|
||
|
||
def select_sku(self, sku_id):
|
||
"""选择资源规格 (复用智能下拉框逻辑)"""
|
||
logger.info(f"🎯 镜像资产规格选择: {sku_id}")
|
||
self.smart_select("资源规格", sku_id)
|
||
|
||
def click_create_button(self):
|
||
"""点击 创建并开机 按钮"""
|
||
logger.info("👉 点击 [创建并开机] 按钮")
|
||
# 通常弹窗下方是 确定 或 立即创建
|
||
self.smart_click("创建并开机") |