615 lines
28 KiB
Python
615 lines
28 KiB
Python
import time
|
||
from framework.core.base_page import BasePage
|
||
from framework.core.logger import get_logger
|
||
|
||
logger = get_logger("CloudDesktopPage")
|
||
|
||
class CloudDesktopPage(BasePage):
|
||
"""云桌面页面 POM - 纯原子操作层"""
|
||
|
||
MENU_TEXT = "地瓜桌面"
|
||
|
||
STATUS = {
|
||
"RUNNING": "运行中",
|
||
"STOPPED": "已关机",
|
||
"CREATING": "创建中",
|
||
"STARTING": "开机中"
|
||
}
|
||
|
||
def __init__(self, page):
|
||
self.page = page
|
||
|
||
def get_desktop_status(self, desktop_name):
|
||
"""获取指定名称桌面的全行内容"""
|
||
return self.page.evaluate("""(name) => {
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
for (const row of rows) {
|
||
const cells = row.querySelectorAll('td');
|
||
if (cells.length === 0) continue;
|
||
// 逐列精确匹配名称
|
||
for (const cell of cells) {
|
||
const t = cell.innerText.trim();
|
||
if (t === name) return row.innerText.trim();
|
||
}
|
||
}
|
||
// 兜底: 模糊匹配
|
||
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';
|
||
}""", desktop_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_desktop_status(name)
|
||
if expected_status in current:
|
||
logger.info(f"✅ 状态已达标: {expected_status}")
|
||
return True
|
||
if time.time() - last_log > 10:
|
||
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"桌面 {name} 在 {timeout}s 内未达到 {expected_status} 状态。最后看到: {current}")
|
||
|
||
def navigate_to(self):
|
||
"""进入云桌面管理页面"""
|
||
logger.info("正在切换到【地瓜桌面】页面...")
|
||
try:
|
||
self.smart_click(self.MENU_TEXT, timeout=3000)
|
||
try:
|
||
self.page.wait_for_url("**/*cloud*", timeout=3000)
|
||
except:
|
||
pass
|
||
except Exception as e:
|
||
logger.warning(f"原生导航失败,尝试兜底... {e}")
|
||
self.page.evaluate(f"""() => {{
|
||
const target = Array.from(document.querySelectorAll('a, span, li, div'))
|
||
.find(el => el.innerText && el.innerText.trim() === '{self.MENU_TEXT}');
|
||
if (target) {{
|
||
target.click();
|
||
if(target.parentElement) target.parentElement.click();
|
||
}}
|
||
}}""")
|
||
time.sleep(3)
|
||
|
||
def get_first_desktop_name(self):
|
||
"""获取列表首行名称"""
|
||
return self.page.evaluate("""() => {
|
||
const cells = document.querySelectorAll('td, .card-name');
|
||
return cells.length > 0 ? cells[0].innerText.trim() : 'N/A';
|
||
}""")
|
||
|
||
def open_create_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):
|
||
"""选择资源规格 (复用基类智能下拉框逻辑)"""
|
||
logger.info(f"🎯 云桌面规格选择: {sku_id}")
|
||
self.smart_select("资源规格", sku_id)
|
||
|
||
def select_image(self, image_keyword="Ubuntu"):
|
||
"""选择镜像 (增加搜索与精准点击)"""
|
||
logger.info(f"💿 选择镜像关键词: {image_keyword}")
|
||
self.smart_click("选择镜像")
|
||
time.sleep(2) # 等待列表对话框弹出
|
||
|
||
# 尝试使用搜索框
|
||
try:
|
||
search_input = self.page.locator("input[placeholder*='搜索'], .p-inputtext").first
|
||
if search_input.is_visible():
|
||
search_input.fill(image_keyword)
|
||
self.page.keyboard.press("Enter")
|
||
time.sleep(1.5)
|
||
except:
|
||
pass
|
||
|
||
# 定位并点击包含关键词的镜像卡片/行
|
||
try:
|
||
# 策略:在 dialog 中寻找包含文本的元素
|
||
target_selector = f".p-dialog :text-is('{image_keyword}'), .p-dialog :text('{image_keyword}')"
|
||
self.page.locator(target_selector).first.click(timeout=5000)
|
||
except:
|
||
# 备选:智能点击
|
||
self.smart_click(image_keyword, timeout=3000)
|
||
|
||
time.sleep(1)
|
||
# 点击确定按钮 (弹窗中的)
|
||
try:
|
||
self.page.locator(".p-dialog-footer button:has-text('确定')").click()
|
||
except:
|
||
self.smart_click("确定", role="button")
|
||
time.sleep(1)
|
||
|
||
def submit_creation(self):
|
||
"""提交创建"""
|
||
logger.info("🚀 提交创建请求")
|
||
self.smart_click("创建并开机")
|
||
time.sleep(3)
|
||
|
||
# ── 行内操作通用方法 ─────────────────────────────────────────────
|
||
|
||
def _click_row_action(self, desktop_name, action_label):
|
||
"""通用:点击指定桌面行内的操作按钮(精确行匹配版)"""
|
||
logger.info(f"👉 在 {desktop_name} 行内寻找并点击 [{action_label}]")
|
||
|
||
clicked = self.page.evaluate("""(args) => {
|
||
const [name, action] = args;
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
let targetRow = null;
|
||
|
||
// 精确匹配:遍历每行的每个 td,要求某列文字完全等于 name
|
||
for (const row of rows) {
|
||
const cells = row.querySelectorAll('td');
|
||
if (cells.length === 0) continue;
|
||
for (const cell of cells) {
|
||
const t = cell.innerText.trim();
|
||
if (t === name) {
|
||
targetRow = row;
|
||
break;
|
||
}
|
||
}
|
||
if (targetRow) break;
|
||
}
|
||
|
||
// 兜底:如果精确匹配失败,再用 includes(仅第一列)
|
||
if (!targetRow) {
|
||
for (const row of rows) {
|
||
const cells = row.querySelectorAll('td');
|
||
if (cells.length === 0) continue;
|
||
const firstCell = cells[0].innerText.trim();
|
||
if (firstCell === name || firstCell.includes(name)) {
|
||
targetRow = row;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!targetRow) return 'NO_ROW';
|
||
|
||
// 策略1:aria-label
|
||
let btn = targetRow.querySelector(`button[aria-label="${action}"], [aria-label="${action}"]`);
|
||
|
||
// 策略2:按钮/链接文字精确匹配
|
||
if (!btn) {
|
||
btn = Array.from(targetRow.querySelectorAll('button, a, span, .p-button'))
|
||
.find(el => {
|
||
const txt = el.innerText.trim();
|
||
const al = el.getAttribute('aria-label') || '';
|
||
return txt === action || al === action;
|
||
});
|
||
}
|
||
|
||
if (btn) {
|
||
btn.scrollIntoView({block: 'center'});
|
||
btn.click();
|
||
return 'CLICKED';
|
||
}
|
||
return 'NO_BTN';
|
||
}""", [desktop_name, action_label])
|
||
|
||
logger.info(f" 行内按钮点击结果: {clicked}")
|
||
|
||
if clicked == 'NO_ROW':
|
||
raise Exception(f"❌ 表格中未找到包含 '{desktop_name}' 的行")
|
||
if clicked == 'NO_BTN':
|
||
logger.info(f" JS未命中,尝试 Playwright 定位(限定行内)...")
|
||
# 用 Playwright 定位到目标行,再在行内找按钮
|
||
row_locator = self.page.locator(f"tr:has(td:text-is('{desktop_name}'))")
|
||
try:
|
||
row_locator.get_by_role("button", name=action_label).first.click(timeout=3000)
|
||
except:
|
||
# 最后兜底:在行内找包含操作文字的可点击元素
|
||
row_locator.locator(f"text='{action_label}'").first.click(timeout=3000)
|
||
|
||
def _confirm_dialog(self, confirm_text="确定"):
|
||
"""确认弹窗 (支持 '预约', '确认', '确定' 等)"""
|
||
logger.info(f"🔍 正在寻找确认按钮 (目标: {confirm_text})...")
|
||
time.sleep(1.5) # 给弹窗动画留出充足时间
|
||
|
||
# 1. 优先尝试显式的定位器 (含截图中的 aria-label="预约")
|
||
task_specific_selectors = [
|
||
"button[aria-label='预约']",
|
||
"button[aria-label='确定']",
|
||
".p-confirm-dialog-accept",
|
||
".p-dialog-footer button:not(.p-button-secondary):not(.p-button-text)"
|
||
]
|
||
for sel in task_specific_selectors:
|
||
try:
|
||
btn = self.page.locator(sel).first
|
||
if btn.is_visible(timeout=500):
|
||
btn.click()
|
||
logger.info(f"✅ 通过选择器成功点击: {sel}")
|
||
return
|
||
except:
|
||
continue
|
||
|
||
# 2. 按文案智能匹配
|
||
keywords = [confirm_text, "预约", "确定", "确认", "提交", "立即"]
|
||
for kw in keywords:
|
||
try:
|
||
# 尝试直接定位包含文字的 PrimeVue 按钮
|
||
btn = self.page.locator(f"button .p-button-label:text-is('{kw}'), button:has-text('{kw}')").first
|
||
if btn.is_visible(timeout=500):
|
||
btn.click()
|
||
logger.info(f"✅ 通过文案成功点击: {kw}")
|
||
return
|
||
except:
|
||
pass
|
||
|
||
# JS 强力匹配
|
||
clicked = self.page.evaluate(f"""(txt) => {{
|
||
const btns = Array.from(document.querySelectorAll('.p-dialog button, button'))
|
||
.filter(b => (b.innerText.includes(txt) || b.getAttribute('aria-label') === txt) && b.offsetWidth > 0);
|
||
if (btns.length > 0) {{
|
||
btns[0].scrollIntoView({{block: 'center'}});
|
||
btns[0].click();
|
||
return true;
|
||
}}
|
||
return false;
|
||
}}""", kw)
|
||
if clicked:
|
||
logger.info(f"✅ 通过 JS 模糊匹配成功点击: {kw}")
|
||
return
|
||
|
||
logger.warning(f"⚠️ 即使使用 '预约' 兜底也未能关闭确认窗口")
|
||
time.sleep(1)
|
||
|
||
# ── 业务操作 ─────────────────────────────────────────────────────
|
||
def convert_to_monthly(self, desktop_name):
|
||
"""按量付费转包月 —— 精确匹配目标行内的按量付费图标"""
|
||
self.wait_for_status(desktop_name, self.STATUS["RUNNING"])
|
||
logger.info(f"🎯 尝试转换桌面 {desktop_name} 为包月")
|
||
|
||
clicked = self.page.evaluate("""(name) => {
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
let targetRow = null;
|
||
|
||
// 精确匹配:遍历每行的每个 td,要求某列文字完全等于 name
|
||
for (const row of rows) {
|
||
const cells = row.querySelectorAll('td');
|
||
if (cells.length === 0) continue;
|
||
for (const cell of cells) {
|
||
const t = cell.innerText.trim();
|
||
if (t === name) {
|
||
targetRow = row;
|
||
break;
|
||
}
|
||
}
|
||
if (targetRow) break;
|
||
}
|
||
|
||
// 兜底
|
||
if (!targetRow) {
|
||
for (const row of rows) {
|
||
const cells = row.querySelectorAll('td');
|
||
if (cells.length === 0) continue;
|
||
const firstCell = cells[0].innerText.trim();
|
||
if (firstCell === name || firstCell.includes(name)) {
|
||
targetRow = row;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!targetRow) return 'NO_ROW';
|
||
|
||
// 在目标行内找到"按量付费"所在的单元格
|
||
const cells = Array.from(targetRow.querySelectorAll('td'));
|
||
for (const cell of cells) {
|
||
if (cell.innerText.includes('按量')) {
|
||
// 优先点击内部的可交互元素(图标/链接/按钮)
|
||
// 注意:svg 元素没有 .click(),统一用 dispatchEvent
|
||
const clickable = cell.querySelector('a, button, i, span[role="button"], .p-button')
|
||
|| cell.querySelector('svg')
|
||
|| cell.querySelector('span, div');
|
||
if (clickable) {
|
||
clickable.scrollIntoView({block: 'center'});
|
||
clickable.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
|
||
return 'CLICKED_INNER';
|
||
}
|
||
// 兜底:点击整个单元格
|
||
cell.scrollIntoView({block: 'center'});
|
||
cell.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
|
||
return 'CLICKED_CELL';
|
||
}
|
||
}
|
||
return 'NO_CELL';
|
||
}""", desktop_name)
|
||
|
||
logger.info(f" 按量付费点击结果: {clicked}")
|
||
if clicked == 'NO_ROW':
|
||
raise Exception(f"❌ 表格中未找到 {desktop_name}")
|
||
if clicked == 'NO_CELL':
|
||
logger.warning(" 目标行内未找到按量付费,尝试全局搜索...")
|
||
self.smart_click("按量付费", timeout=3000)
|
||
|
||
time.sleep(1)
|
||
self._confirm_dialog("确定")
|
||
time.sleep(2)
|
||
|
||
def open_desktop(self, desktop_name):
|
||
"""打开/开机桌面 (处理新开标签页)"""
|
||
# 显式等待状态变为运行中,防止操作过快导致按钮没出现
|
||
self.wait_for_status(desktop_name, self.STATUS["RUNNING"])
|
||
logger.info(f"🎯 尝试打开桌面 {desktop_name}")
|
||
|
||
# 捕获可能打开的新标签页
|
||
with self.page.context.expect_page(timeout=10000) as new_page_info:
|
||
self._click_row_action(desktop_name, "打开桌面")
|
||
|
||
new_page = new_page_info.value
|
||
logger.info(f"🌐 检测到桌面已在新标签页打开: {new_page.url}")
|
||
|
||
# 停留一会儿确保桌面加载,然后关掉它,回到控制台继续后续流程
|
||
time.sleep(60)
|
||
new_page.close()
|
||
logger.info("🔙 已关闭桌面标签页,返回控制台进行后续流程")
|
||
time.sleep(5)
|
||
|
||
def stop_desktop(self, desktop_name):
|
||
"""关机"""
|
||
self.wait_for_status(desktop_name, self.STATUS["RUNNING"])
|
||
logger.info(f"🎯 尝试关机桌面 {desktop_name}")
|
||
self._click_row_action(desktop_name, "关机")
|
||
time.sleep(1)
|
||
|
||
def delete_desktop(self, desktop_name):
|
||
"""删除桌面"""
|
||
self.wait_for_status(desktop_name, self.STATUS["STOPPED"])
|
||
logger.info(f"🎯 尝试删除桌面 {desktop_name}")
|
||
self._click_row_action(desktop_name, "删除")
|
||
time.sleep(0.5)
|
||
self._confirm_dialog("确定删除")
|
||
time.sleep(2)
|
||
|
||
def _select_dropdown(self, label_text, option_text):
|
||
"""通用下拉框选择 (强力触发 + 强力点击)"""
|
||
logger.info(f"📋 下拉框 [{label_text}] 寻找目标: {option_text}")
|
||
|
||
# 1. 寻找触发器并点击 (多重探测逻辑)
|
||
triggered = self.page.evaluate(f"""(label) => {{
|
||
const dialog = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]') || document.body;
|
||
|
||
// A. 通过 placeholder 文案尝试直接点击最明显的选择框
|
||
const placeholders = ["请选择", "选择", label];
|
||
for (const p of placeholders) {{
|
||
const boxes = Array.from(dialog.querySelectorAll('.p-select, .p-dropdown, [role="combobox"]'));
|
||
const targetBox = boxes.find(b => b.innerText.includes(p) && b.offsetWidth > 0);
|
||
if (targetBox) {{
|
||
targetBox.scrollIntoView({{block: 'center'}});
|
||
targetBox.click();
|
||
return 'BOX_CLICKED';
|
||
}}
|
||
}}
|
||
|
||
// B. 通过 Label 文字向上查找再向下查找
|
||
const labels = Array.from(dialog.querySelectorAll('label, .form-item-label, span'));
|
||
const lbl = labels.find(el => el.innerText.trim().includes(label) && el.offsetWidth > 0);
|
||
if (lbl) {{
|
||
let p = lbl;
|
||
for (let i = 0; i < 5; i++) {{
|
||
const dd = p.querySelector('.p-select, .p-dropdown, [role="combobox"]');
|
||
if (dd) {{
|
||
dd.scrollIntoView({{block: 'center'}});
|
||
dd.click();
|
||
return 'LABEL_CLICKED';
|
||
}}
|
||
p = p.parentElement;
|
||
if (!p) break;
|
||
}}
|
||
}}
|
||
return 'NOT_FOUND';
|
||
}}""", label_text)
|
||
|
||
logger.info(f" 下拉框触发结果: {triggered}")
|
||
time.sleep(1)
|
||
|
||
# 2. 在【专属容器】内寻找并匹配选项
|
||
success = False
|
||
keywords = [option_text]
|
||
if "其他" in option_text or "Other" in option_text:
|
||
keywords = ["其他", "其它", "Other", "Others", "other"]
|
||
|
||
# 尝试 2 次,给渲染留时间
|
||
for attempt in range(2):
|
||
clicked_res = self.page.evaluate(f"""(txts) => {{
|
||
const overlays = Array.from(document.querySelectorAll('.p-select-overlay, .p-select-panel, .p-dropdown-panel, .p-popover, .p-connected-overlay'));
|
||
const containers = overlays.length > 0 ? overlays : [document.body];
|
||
|
||
for (const container of containers) {{
|
||
const options = Array.from(container.querySelectorAll('li, [role="option"], .p-select-option, .p-dropdown-item'));
|
||
for (const opt of options) {{
|
||
const opText = (opt.innerText || opt.textContent).trim();
|
||
// 命中关键词
|
||
if (txts.some(t => opText === t || opText.includes(t))) {{
|
||
opt.scrollIntoView({{block: 'center', behavior: 'instant'}});
|
||
// 强力点击
|
||
opt.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true }}));
|
||
opt.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true }}));
|
||
opt.dispatchEvent(new MouseEvent('click', {{ bubbles: true }}));
|
||
return true;
|
||
}}
|
||
}}
|
||
}}
|
||
return false;
|
||
}}""", keywords)
|
||
|
||
if clicked_res:
|
||
logger.info(f" ✅ 已成功选中目标选项")
|
||
success = True
|
||
break
|
||
time.sleep(1)
|
||
|
||
if not success:
|
||
logger.error(f"❌ 流程卡在下拉框选项点击: {label_text} -> {option_text}")
|
||
# Playwright 原生点击兜底
|
||
try:
|
||
self.page.locator(f"li:has-text('{option_text}'), [role='option']:has-text('{option_text}')").last.click(timeout=1000)
|
||
success = True
|
||
except: pass
|
||
time.sleep(0.5)
|
||
|
||
def _fill_tag_input(self, label_text, tag_value):
|
||
"""填写标签输入"""
|
||
logger.info(f"🏷️ 标签输入 [{label_text}]: {tag_value}")
|
||
# 找到标签区域的 "+" 按钮或输入框
|
||
found = self.page.evaluate("""(label) => {
|
||
const dialog = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]');
|
||
if (!dialog) return 'NO_DIALOG';
|
||
const labels = Array.from(dialog.querySelectorAll('label, span, div'));
|
||
const lbl = labels.find(el => el.innerText.trim().includes(label) && el.offsetWidth > 0);
|
||
if (!lbl) return 'NO_LABEL';
|
||
let container = lbl;
|
||
for (let i = 0; i < 6; i++) {
|
||
if (!container) break;
|
||
const addBtn = container.querySelector('button, .p-button, [role="button"]');
|
||
if (addBtn && addBtn.offsetWidth > 0) { addBtn.click(); return 'FOUND'; }
|
||
container = container.parentElement;
|
||
}
|
||
return 'NO_BTN';
|
||
}""", label_text)
|
||
logger.info(f" 标签输入框查找结果: {found}")
|
||
time.sleep(0.8)
|
||
|
||
# "+" 按钮点击后应该出现 input,填入标签值并回车
|
||
tag_filled = self.page.evaluate("""(tag) => {
|
||
const dialog = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]');
|
||
if (!dialog) return 'NO_DIALOG';
|
||
const inputs = Array.from(dialog.querySelectorAll('input[type="text"], input:not([type])'))
|
||
.filter(inp => !inp.readOnly && !inp.disabled && inp.offsetWidth > 0);
|
||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||
const inp = inputs[i];
|
||
if (inp.value === '' || inp.placeholder?.includes('标签') || inp.placeholder?.includes('tag')) {
|
||
const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
||
nativeSet.call(inp, tag);
|
||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||
inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
||
inp.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true }));
|
||
return 'FILLED';
|
||
}
|
||
}
|
||
return 'NO_TAG_INPUT';
|
||
}""", tag_value)
|
||
logger.info(f" 标签输入结果: {tag_filled}")
|
||
|
||
if tag_filled != 'FILLED':
|
||
try:
|
||
tag_input = self.page.locator('.p-dialog input').last
|
||
tag_input.fill(tag_value)
|
||
tag_input.press("Enter")
|
||
except:
|
||
logger.warning("⚠️ 标签输入兜底也失败")
|
||
time.sleep(0.5)
|
||
|
||
def save_image(self, desktop_name):
|
||
"""保存镜像 (完整弹窗交互)"""
|
||
self.wait_for_status(desktop_name, self.STATUS["RUNNING"])
|
||
logger.info(f"🎯 尝试保存桌面 {desktop_name} 为镜像")
|
||
|
||
self._click_row_action(desktop_name, "保存镜像")
|
||
time.sleep(1.5)
|
||
|
||
logger.info("📝 步骤1: 输入镜像名称")
|
||
self.smart_fill("请输入镜像名称", f"AutoImage_{desktop_name}")
|
||
time.sleep(0.5)
|
||
|
||
logger.info("📝 步骤2: 选择任务类型")
|
||
self._select_dropdown("任务类型", "其他")
|
||
|
||
logger.info("📝 步骤3: 输入镜像标签")
|
||
self._fill_tag_input("镜像标签", "auto-test")
|
||
|
||
logger.info("📝 步骤4: 提交保存")
|
||
self.smart_click("确定", role="button", timeout=5000)
|
||
time.sleep(3)
|
||
|
||
def _fill_tag_input(self, label_text, tag_value):
|
||
"""填写标签输入"""
|
||
logger.info(f"🏷️ 标签输入 [{label_text}]: {tag_value}")
|
||
# 找到标签区域的 "+" 按钮或输入框
|
||
found = self.page.evaluate("""(label) => {
|
||
const dialog = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]');
|
||
if (!dialog) return 'NO_DIALOG';
|
||
const labels = Array.from(dialog.querySelectorAll('label, span, div'));
|
||
const lbl = labels.find(el => el.innerText.trim().includes(label) && el.offsetWidth > 0);
|
||
if (!lbl) return 'NO_LABEL';
|
||
let container = lbl;
|
||
for (let i = 0; i < 6; i++) {
|
||
if (!container) break;
|
||
const addBtn = container.querySelector('button, .p-button, [role="button"]');
|
||
if (addBtn && addBtn.offsetWidth > 0) { addBtn.click(); return 'FOUND'; }
|
||
container = container.parentElement;
|
||
}
|
||
return 'NO_BTN';
|
||
}""", label_text)
|
||
logger.info(f" 标签输入框查找结果: {found}")
|
||
time.sleep(0.8)
|
||
|
||
# "+" 按钮点击后应该出现 input,填入标签值并回车
|
||
tag_filled = self.page.evaluate("""(tag) => {
|
||
const dialog = document.querySelector('.p-dialog') || document.querySelector('[role="dialog"]');
|
||
if (!dialog) return 'NO_DIALOG';
|
||
const inputs = Array.from(dialog.querySelectorAll('input[type="text"], input:not([type])'))
|
||
.filter(inp => !inp.readOnly && !inp.disabled && inp.offsetWidth > 0);
|
||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||
const inp = inputs[i];
|
||
if (inp.value === '' || inp.placeholder?.includes('标签') || inp.placeholder?.includes('tag')) {
|
||
const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
||
nativeSet.call(inp, tag);
|
||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||
inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
||
inp.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true }));
|
||
return 'FILLED';
|
||
}
|
||
}
|
||
return 'NO_TAG_INPUT';
|
||
}""", tag_value)
|
||
logger.info(f" 标签输入结果: {tag_filled}")
|
||
|
||
if tag_filled != 'FILLED':
|
||
try:
|
||
tag_input = self.page.locator('.p-dialog input').last
|
||
tag_input.fill(tag_value)
|
||
tag_input.press("Enter")
|
||
except:
|
||
logger.warning("⚠️ 标签输入兜底也失败")
|
||
time.sleep(0.5)
|
||
|
||
def save_image(self, desktop_name):
|
||
"""保存镜像 (完整弹窗交互)"""
|
||
self.wait_for_status(desktop_name, self.STATUS["RUNNING"])
|
||
logger.info(f"🎯 尝试保存桌面 {desktop_name} 为镜像")
|
||
|
||
self._click_row_action(desktop_name, "保存镜像")
|
||
time.sleep(1.5)
|
||
|
||
logger.info("📝 步骤1: 输入镜像名称")
|
||
self.smart_fill("请输入镜像名称", f"AutoImage_{desktop_name}")
|
||
time.sleep(0.5)
|
||
|
||
logger.info("📝 步骤2: 选择任务类型")
|
||
self._select_dropdown("任务类型", "其他")
|
||
|
||
logger.info("📝 步骤3: 输入镜像标签")
|
||
self._fill_tag_input("镜像标签", "auto-test")
|
||
|
||
logger.info("📝 步骤4: 提交保存")
|
||
self.smart_click("确定", role="button", timeout=5000)
|
||
time.sleep(3) |