import time import os import random import string from framework.core.base_page import BasePage from framework.core.logger import get_logger logger = get_logger("ThreeDGenerationPage") class ThreeDGenerationPage(BasePage): """3D生成页面 POM - 包含生成、等待与保存逻辑""" MENU_TEXT = "3D生成" def __init__(self, page): self.page = page def navigate_to(self): """进入3D生成页面""" logger.info("正在切换到3D生成页面...") self.smart_click(self.MENU_TEXT) try: self.page.wait_for_selector(".p-button:has-text('保存'), .p-tabmenu", timeout=10000) except: pass time.sleep(2) def upload_image(self, file_path): """通过隐藏 input 上传图片""" logger.info(f"📤 准备上传 3D 生成素材: {file_path}") try: # 直接通过 input 元素设置文件 self.page.set_input_files("input[type='file']", file_path) except Exception as e: logger.warning(f"⚠️ 直接注入失败,尝试点击图标触发: {e}") with self.page.expect_file_chooser() as fc_info: self.page.click("button.p-button-icon-only:has(.pi-arrow-up)") fc_info.value.set_files(file_path) time.sleep(2) def start_generation(self, retries=3): """点击底部生成按钮 (图标按钮) - 增加 3 次重试机制 & 15s 业务异常监控""" logger.info(f"🚀 准备点击生成按钮 (最多重试 {retries} 次)...") target_btn = "button.p-button-icon-only:has(.pi-arrow-up)" for i in range(retries): try: # 1. 执行物理点击 self.page.wait_for_selector(target_btn, state="visible", timeout=5000) self.page.click(target_btn) logger.info(f"🖱️ 第 {i+1} 次点击动作已发出") # 2. 持续观测业务状态 (监控 15s) logger.info(f"👀 正在观测第 {i+1} 次生成任务的业务反馈...") is_failed = False for _ in range(15): # 使用显式文本探测,捕捉 Toast 或 页面文字报错 error_detected = self.page.locator("text=/网络错误|Generation failed|failed/i").first.is_visible() if error_detected: logger.warning(f"⚠️ 第 {i+1} 次运行检测到业务异常文字,准备重试") is_failed = True break time.sleep(1) if not is_failed: logger.info(f"✅ 第 {i+1} 次任务成功启动且未检出即时报错") return True # 业务失败且有次数则间隔后重试 if i < retries - 1: time.sleep(2) continue except Exception as e: # 捕捉定位或点击失败 logger.warning(f"⚠️ 第 {i+1} 次点击尝试失败 (物理层): {e}") if i < retries - 1: time.sleep(2) continue # 3. 最终失败处理 error_msg = f"❌ 3D 生成任务在重试 {retries} 次后仍无法启动,程序强行退出" logger.error(error_msg) raise Exception(error_msg) def wait_for_result_and_save(self, timeout=900): """等待右上角[保存]按钮变为可点击状态后,立刻点击""" logger.info(f"⏳ 等待 3D 生成结果 (最长 {timeout}s)...") start_time = time.time() last_log = start_time while time.time() - start_time < timeout: elapsed = int(time.time() - start_time) # 每 30s 打印一次进度,方便追踪 if time.time() - last_log >= 30: logger.info(f" ⏱️ 已等待 {elapsed}s,继续监听保存按鈕...") last_log = time.time() try: save_btns = self.page.locator("button:has-text('\u4fdd\u5b58')") count = save_btns.count() for i in range(count): btn = save_btns.nth(i) try: # 跳过弹窗内的按鈕(弹窗内的“确定”不是目标) in_dialog = btn.evaluate("el => !!el.closest('.p-dialog')") if in_dialog: continue if btn.is_visible() and btn.is_enabled(): logger.info(f"✨ 第 {elapsed}s 时检测到[保存]按鈕就绪,立刻点击") btn.click() return True except Exception: continue except Exception: pass # 按鈕还未出现,继续等待 time.sleep(3) logger.error(f"❌ 等待结果超时 ({timeout}s),保存按鈕始终未就绪") return False def handle_save_dialog(self): """处理保存模型弹窗及其路径选择流程""" # 1. 生成随机资产名称 random_name = "".join(random.choices(string.ascii_letters + string.digits, k=8)) asset_name = f"Auto3D_{random_name}" logger.info(f"📝 输入资产名称: {asset_name}") # 输入资产名称 name_input = self.page.locator("input[placeholder='请输入资产名称']") name_input.fill(asset_name) time.sleep(1) # 2. 点击保存路径触发弹窗 logger.info("📂 点击保存路径选择器...") path_input = self.page.locator("input[placeholder='请选择要保存的文件夹']") path_input.click() time.sleep(2) # 3. 路径选择弹窗内部操作 logger.info(f"📂 正在开启路径选择弹窗,进行多列导航...") # 修正核心:弹窗标题实为“选择文件” (根据截图中的渲染结果) file_picker_dialog = self.page.locator(".p-dialog:has-text('选择文件')") # 定位左侧 3d生成(勿删) 根目录项 folder_item = file_picker_dialog.locator("span:has-text('3d生成(勿删)')").first folder_item.click() time.sleep(2) # 留出中间列刷新的渲染窗口 # 定位刚才生成的随机文件夹名称 logger.info(f"🎯 正在定位生成的文件夹项: {asset_name}") try: # 延长等待时间 (20s) 提高环境波动兼容性 target_item = file_picker_dialog.locator(f"//span[contains(text(), '{asset_name}')]").last target_item.wait_for(state="attached", timeout=20000) target_item.scroll_into_view_if_needed() target_item.click() logger.info(f"✅ 文件夹已在 [选择文件] 弹窗中成功选中") except Exception as e: logger.warning(f"⚠️ 在 20s 内未能自动选中项 {asset_name},可能是刷新延迟: {e}") # 4. 点击路径选择弹窗的确认按钮 logger.info("⏳ 点击路径选择器[确定]按钮...") # 修正:同步标题匹配为“选择文件” path_dialog_confirm = file_picker_dialog.locator("button:has-text('确定')").last path_dialog_confirm.click() logger.info("✅ 路径跳转弹窗已确认并关闭") time.sleep(2) # 留出 UI 彻底销毁的时间 # 5. 校验路径是否回填 filled_val = path_input.get_attribute("value") logger.info(f"🔍 检查保存路径回填状态: {filled_val}") # 6. 点击保存模型弹窗最终确认按钮 (精准定位保存弹窗确定) logger.info("💾 点击保存模型弹窗最终确认按钮") save_dialog_confirm = self.page.locator(".p-dialog:has-text('保存模型')").locator("button:has-text('确定')").last save_dialog_confirm.wait_for(state="visible", timeout=5000) save_dialog_confirm.click() logger.info("🎉 3D 生成场景全流程保存确认成功") time.sleep(2) return asset_name