新增关闭浏览器会话功能,支持浏览器会话复用,优化UI测试逻辑

This commit is contained in:
longpeng 2025-06-27 18:07:24 +08:00
parent eba56bc756
commit bf263a667d
5 changed files with 111 additions and 42 deletions

View File

@ -33,6 +33,12 @@ message UiTestRequest {
string browser_type = 3; // "chromium", "firefox", "webkit" string browser_type = 3; // "chromium", "firefox", "webkit"
bool headless = 4; // bool headless = 4; //
map<string, string> user_data = 5; // Worker map<string, string> user_data = 5; // Worker
optional string browser_session_id = 6; //
}
//
message CloseBrowserRequest {
string browser_session_id = 1; // ID
} }
// -------- -------- // -------- --------
@ -59,6 +65,7 @@ message UiTestResult {
BaseTestResult base_result = 1; BaseTestResult base_result = 1;
string screenshot_url = 2; // URL (worker上传后返回) string screenshot_url = 2; // URL (worker上传后返回)
string html_report_url = 3; // HTML报告URL string html_report_url = 3; // HTML报告URL
string browser_session_id = 4; // ID
} }
// //

View File

@ -11,7 +11,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gen'
# 导入protobuf生成的模块和其他依赖 # 导入protobuf生成的模块和其他依赖
from pb import common_test_pb2 as pb from pb import common_test_pb2 as pb
from api_tests import execute_api_test_case from api_tests import execute_api_test_case
from ui_tests import execute_ui_test_case from ui_tests import execute_ui_test_case, close_browser_session
from utils import upload_file_to_s3, scalar_map_to_dict from utils import upload_file_to_s3, scalar_map_to_dict
@ -178,3 +178,11 @@ class TestActivities:
# 计算并记录测试执行时长 # 计算并记录测试执行时长
result.base_result.duration_seconds = time.time() - start_time result.base_result.duration_seconds = time.time() - start_time
return result return result
@activity.defn(name="CloseBrowser")
async def close_browser(self, req: pb.CloseBrowserRequest) -> dict:
"""
关闭指定的浏览器会话
"""
success, msg = await close_browser_session(req.browser_session_id)
return {"success": success, "message": msg}

View File

@ -20,7 +20,7 @@ async def main():
worker = Worker( worker = Worker(
client, client,
task_queue="python-task-queue", # 保持与 Go Client 一致 task_queue="python-task-queue", # 保持与 Go Client 一致
activities=[activities.run_api_test,activities.run_ui_test] activities=[activities.run_api_test,activities.run_ui_test,activities.close_browser]
) )
print("Starting Python Temporal Worker...") print("Starting Python Temporal Worker...")
await worker.run() await worker.run()

View File

@ -2,12 +2,19 @@
from playwright.async_api import async_playwright, expect from playwright.async_api import async_playwright, expect
import os import os
import datetime import datetime
import uuid
from typing import Optional
async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: str, headless: bool, user_data: dict): # 全局浏览器会话管理
_browser_sessions = {}
_playwright_instance = None
async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: str, headless: bool, user_data: dict, browser_session_id: Optional[str] = None):
""" """
实际执行UI测试的函数 实际执行UI测试的函数
这里使用 Playwright你也可以替换成 Selenium 支持浏览器会话复用
""" """
global _browser_sessions, _playwright_instance
base_url = "https://playwright.dev" # 假设 UI 测试的基地址 base_url = "https://playwright.dev" # 假设 UI 测试的基地址
full_url = f"{base_url}{url_path}" full_url = f"{base_url}{url_path}"
@ -15,45 +22,55 @@ async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: s
success = False success = False
screenshot_path = None screenshot_path = None
html_report_path = None html_report_path = None
browser = None
page = None page = None
created_new_session = False
session_id = browser_session_id
log_output.append(f"Executing UI test: {test_case_id} - {full_url} with {browser_type}") log_output.append(f"Executing UI test: {test_case_id} - {full_url} with {browser_type}")
try: try:
async with async_playwright() as p: if _playwright_instance is None:
_playwright_instance = await async_playwright().start()
# 浏览器复用逻辑
if session_id and session_id in _browser_sessions:
browser = _browser_sessions[session_id]
log_output.append(f"Reusing browser session: {session_id}")
else:
# 新建浏览器
if browser_type == "chromium": if browser_type == "chromium":
browser = await p.chromium.launch(headless=headless) browser = await _playwright_instance.chromium.launch(headless=headless)
elif browser_type == "firefox": elif browser_type == "firefox":
browser = await p.firefox.launch(headless=headless) browser = await _playwright_instance.firefox.launch(headless=headless)
elif browser_type == "webkit": elif browser_type == "webkit":
browser = await p.webkit.launch(headless=headless) browser = await _playwright_instance.webkit.launch(headless=headless)
else: else:
raise ValueError(f"Unsupported browser type: {browser_type}") raise ValueError(f"Unsupported browser type: {browser_type}")
# 生成新的 session_id
session_id = str(uuid.uuid4())
_browser_sessions[session_id] = browser
created_new_session = True
log_output.append(f"Created new browser session: {session_id}")
page = await browser.new_page() page = await browser.new_page()
# 模拟登录(如果需要) # 模拟登录(如果需要)
if user_data: if user_data:
log_output.append(f"Attempting to log in with user: {user_data.get('user')}") log_output.append(f"Attempting to log in with user: {user_data.get('user')}")
# 假设有一个登录页面
await page.goto(f"{base_url}/login") await page.goto(f"{base_url}/login")
await page.fill('input[name="username"]', user_data.get('user', '')) await page.fill('input[name="username"]', user_data.get('user', ''))
await page.fill('input[name="password"]', user_data.get('pass', '')) await page.fill('input[name="password"]', user_data.get('pass', ''))
await page.click('button[type="submit"]') await page.click('button[type="submit"]')
await page.wait_for_url(full_url) # 等待跳转到目标页面 await page.wait_for_url(full_url)
await page.goto(full_url) await page.goto(full_url)
log_output.append(f"Navigated to: {full_url}") log_output.append(f"Navigated to: {full_url}")
# 示例UI操作和断言 # 示例UI操作和断言
# 查找一个元素并验证其文本
element = page.locator("text=Playwright enables reliable end-to-end testing for modern web apps.") element = page.locator("text=Playwright enables reliable end-to-end testing for modern web apps.")
await expect(element).to_be_visible() await expect(element).to_be_visible()
log_output.append("Found expected text on page.") log_output.append("Found expected text on page.")
# 点击一个链接
await page.click("text=Docs") await page.click("text=Docs")
await page.wait_for_url("**/docs/intro") await page.wait_for_url("**/docs/intro")
log_output.append("Clicked 'Docs' link and navigated.") log_output.append("Clicked 'Docs' link and navigated.")
@ -74,11 +91,27 @@ async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: s
log_output.append(f"Screenshot saved to: {screenshot_path}") log_output.append(f"Screenshot saved to: {screenshot_path}")
except Exception as e: except Exception as e:
log_output.append(f"Failed to take screenshot: {e}") log_output.append(f"Failed to take screenshot: {e}")
# 注意:只有不是复用时才自动关闭浏览器
if browser: if created_new_session and session_id in _browser_sessions:
try: try:
await browser.close() await _browser_sessions[session_id].close()
del _browser_sessions[session_id]
log_output.append(f"Closed browser session: {session_id}")
except Exception as e: except Exception as e:
log_output.append(f"Failed to close browser: {e}") log_output.append(f"Failed to close browser: {e}")
return success, "\n".join(log_output), screenshot_path, html_report_path return success, "\n".join(log_output), screenshot_path, html_report_path, session_id
async def close_browser_session(browser_session_id: str):
"""
显式关闭指定的浏览器会话
"""
global _browser_sessions
if browser_session_id in _browser_sessions:
try:
await _browser_sessions[browser_session_id].close()
del _browser_sessions[browser_session_id]
return True, f"Closed browser session: {browser_session_id}"
except Exception as e:
return False, f"Failed to close browser: {e}"
return False, f"No such browser session: {browser_session_id}"

View File

@ -77,6 +77,7 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
uiResults []*pb.UiTestResult // 收集所有 UI 测试结果 uiResults []*pb.UiTestResult // 收集所有 UI 测试结果
stepResults = make(map[string]bool) // 存储每个步骤的成功/失败状态,用于条件跳转判断 stepResults = make(map[string]bool) // 存储每个步骤的成功/失败状态,用于条件跳转判断
currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转 currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转
lastBrowserSessionID string // 用于UI测试的浏览器会话ID
) )
// 初始化全局变量,包含输入参数中的全局参数 // 初始化全局变量,包含输入参数中的全局参数
@ -161,6 +162,10 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
overallSuccess = false overallSuccess = false
break // 参数解析失败,跳出当前步骤 break // 参数解析失败,跳出当前步骤
} }
// 注入 browser_session_id
if lastBrowserSessionID != "" {
uiReq.BrowserSessionId = &lastBrowserSessionID
}
activityInput = uiReq activityInput = uiReq
activityResult = &pb.UiTestResult{} // 预创建结果容器 activityResult = &pb.UiTestResult{} // 预创建结果容器
@ -258,6 +263,10 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
Data: res, Data: res,
} }
parameterProcessor.AddActivityResult(step.StepId, activityResult) parameterProcessor.AddActivityResult(step.StepId, activityResult)
// 记录 browser_session_id 以便下一个 UI 步骤复用
if res.BrowserSessionId != "" {
lastBrowserSessionID = res.BrowserSessionId
}
// 可以在这里添加更多结果类型的处理 // 可以在这里添加更多结果类型的处理
} }
logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed) logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed)
@ -300,6 +309,18 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
"apiResultsCount", len(apiResults), "apiResultsCount", len(apiResults),
"uiResultsCount", len(uiResults)) "uiResultsCount", len(uiResults))
// 工作流结束时关闭浏览器会话
if lastBrowserSessionID != "" {
closeReq := &pb.CloseBrowserRequest{BrowserSessionId: lastBrowserSessionID}
closeCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
TaskQueue: "python-task-queue",
StartToCloseTimeout: 2 * time.Minute,
})
var closeResp interface{}
_ = workflow.ExecuteActivity(closeCtx, "CloseBrowser", closeReq).Get(closeCtx, &closeResp)
logger.Info("Closed browser session at workflow end", "browserSessionId", lastBrowserSessionID)
}
// 返回包含所有测试结果和执行状态的输出结构 // 返回包含所有测试结果和执行状态的输出结构
return &pb.TestRunOutput{ return &pb.TestRunOutput{
RunId: input.RunId, // 运行标识符 RunId: input.RunId, // 运行标识符