新增关闭浏览器会话功能,支持浏览器会话复用,优化UI测试逻辑
This commit is contained in:
parent
eba56bc756
commit
bf263a667d
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整个测试任务的输出结果
|
// 整个测试任务的输出结果
|
||||||
|
@ -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}
|
||||||
|
@ -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()
|
||||||
|
@ -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,51 +22,61 @@ 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.")
|
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
log_output.append("UI Test PASSED.")
|
log_output.append("UI Test PASSED.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_output.append(f"UI Test FAILED (Exception): {e}")
|
log_output.append(f"UI Test FAILED (Exception): {e}")
|
||||||
@ -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}"
|
@ -72,11 +72,12 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
// 步骤3: 初始化执行状态和结果收集器
|
// 步骤3: 初始化执行状态和结果收集器
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
var (
|
var (
|
||||||
overallSuccess = true // 整体执行成功标志,任何一个步骤失败都会置为 false
|
overallSuccess = true // 整体执行成功标志,任何一个步骤失败都会置为 false
|
||||||
apiResults []*pb.ApiTestResult // 收集所有 API 测试结果
|
apiResults []*pb.ApiTestResult // 收集所有 API 测试结果
|
||||||
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, // 运行标识符
|
||||||
|
Loading…
Reference in New Issue
Block a user