新增UI测试步骤支持,优化execute_ui_test_case函数,更新相关数据结构和数据库表

This commit is contained in:
longpeng 2025-06-30 00:04:49 +08:00
parent b7263f1814
commit de3972577c
16 changed files with 149 additions and 45 deletions

2
.gitignore vendored
View File

@ -183,5 +183,5 @@ cython_debug/
*pb2.py*
*pb.go
*.sql
#*.sql
*.yaml

View File

@ -44,12 +44,6 @@ CREATE TABLE `composite_case_steps`
`parameters_json` JSON NULL COMMENT '步骤特定参数,例如:{"endpoint": "/users", "method": "GET"}',
`is_required` BOOLEAN DEFAULT TRUE COMMENT '是否必需步骤',
`step_description` TEXT NULL COMMENT '步骤描述',
`selector` VARCHAR(255) NULL COMMENT 'UI元素选择器',
`input_value` VARCHAR(255) NULL COMMENT '输入值',
`event_type` VARCHAR(50) NULL COMMENT '事件类型',
`offset_x` INT NULL COMMENT 'X轴偏移量',
`offset_y` INT NULL COMMENT 'Y轴偏移量',
`wait_time_seconds` INT NULL COMMENT '等待时间(秒)',
`success_next_step_order` INT NULL COMMENT '成功时跳转到的步骤顺序',
`failure_next_step_order` INT NULL COMMENT '失败时跳转到的步骤顺序',
`run_condition` JSON NULL COMMENT '执行条件,例如:{"previous_step_id": "step_xyz", "status": "success"}',
@ -66,23 +60,22 @@ ALTER TABLE composite_cases
ADD COLUMN deleted_at DATETIME NULL AFTER updated_at;
-- 为composite_case_steps表添加缺失的字段
ALTER TABLE composite_case_steps
ADD COLUMN step_description TEXT NULL AFTER step_name COMMENT '步骤描述';
ALTER TABLE composite_case_steps
ADD COLUMN selector VARCHAR(255) NULL AFTER is_required COMMENT 'UI元素选择器';
ADD COLUMN selector VARCHAR(255) NULL COMMENT 'UI元素选择器' AFTER is_required;
ALTER TABLE composite_case_steps
ADD COLUMN input_value VARCHAR(255) NULL AFTER selector COMMENT '输入值';
ADD COLUMN input_value VARCHAR(255) NULL COMMENT '输入值' AFTER selector;
ALTER TABLE composite_case_steps
ADD COLUMN event_type VARCHAR(50) NULL AFTER input_value COMMENT '事件类型';
ADD COLUMN event_type VARCHAR(50) NULL COMMENT '事件类型' AFTER input_value;
ALTER TABLE composite_case_steps
ADD COLUMN offset_x INT NULL AFTER event_type COMMENT 'X轴偏移量';
ADD COLUMN offset_x INT NULL COMMENT 'X轴偏移量' AFTER event_type;
ALTER TABLE composite_case_steps
ADD COLUMN offset_y INT NULL AFTER offset_x COMMENT 'Y轴偏移量';
ADD COLUMN offset_y INT NULL COMMENT 'Y轴偏移量' AFTER offset_x;
ALTER TABLE composite_case_steps
ADD COLUMN wait_time_seconds INT NULL AFTER offset_y COMMENT '等待时间(秒)';
ADD COLUMN wait_time_seconds INT NULL COMMENT '等待时间(秒)' AFTER offset_y;

4
go.sum
View File

@ -99,6 +99,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -287,6 +289,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -30,6 +30,12 @@ type CompositeCaseStep struct {
ActivityName string `json:"activity_name" gorm:"not null;size:255"`
ParametersJson string `json:"parameters_json" gorm:"type:json"`
IsRequired bool `json:"is_required" gorm:"default:true"`
Selector string `json:"selector" gorm:"size:255"`
InputValue string `json:"input_value" gorm:"size:255"`
EventType string `json:"event_type" gorm:"size:50"`
OffsetX int `json:"offset_x"`
OffsetY int `json:"offset_y"`
WaitTimeSeconds int `json:"wait_time_seconds"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@ -51,6 +57,12 @@ type CreateCompositeCaseStepRequest struct {
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
Selector string `json:"selector"`
InputValue string `json:"input_value"`
EventType string `json:"event_type"`
OffsetX int `json:"offset_x"`
OffsetY int `json:"offset_y"`
WaitTimeSeconds int `json:"wait_time_seconds"`
}
// UpdateCompositeCaseRequest 更新复合案例请求
@ -71,4 +83,10 @@ type UpdateCompositeCaseStepRequest struct {
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
Selector string `json:"selector"`
InputValue string `json:"input_value"`
EventType string `json:"event_type"`
OffsetX int `json:"offset_x"`
OffsetY int `json:"offset_y"`
WaitTimeSeconds int `json:"wait_time_seconds"`
}

View File

@ -26,14 +26,26 @@ message ApiTestRequest {
int32 expected_status_code = 6;
}
// UI测试步骤
message UiStep {
string name = 1; //
string selector = 2; // CSS or XPath selector for the element
string input_value = 3; // Value to input (for input events)
string event_type = 4; // "click", "input", "swipe", "wait"
int32 offset_x = 5; // Offset X for click/swipe
int32 offset_y = 6; // Offset Y for click/swipe
int32 wait_time_seconds = 7; // Time to wait after the event
}
// UI测试请求的参数
message UiTestRequest {
string test_case_id = 1; // UI测试用例ID
string url_path = 2; // base_url
string browser_type = 3; // "chromium", "firefox", "webkit"
bool headless = 4; //
map<string, string> user_data = 5; // Worker
// map<string, string> user_data = 5; // Worker
optional string browser_session_id = 6; //
repeated UiStep steps = 7; // UI测试步骤列表
}
//

View File

@ -66,6 +66,12 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
Selector: stepReq.Selector,
InputValue: stepReq.InputValue,
EventType: stepReq.EventType,
OffsetX: stepReq.OffsetX,
OffsetY: stepReq.OffsetY,
WaitTimeSeconds: stepReq.WaitTimeSeconds,
}
steps = append(steps, step)
}
@ -190,6 +196,12 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
Selector: stepReq.Selector,
InputValue: stepReq.InputValue,
EventType: stepReq.EventType,
OffsetX: stepReq.OffsetX,
OffsetY: stepReq.OffsetY,
WaitTimeSeconds: stepReq.WaitTimeSeconds,
}
steps = append(steps, step)
}

0
workers/__init__.py Normal file
View File

View File

View File

@ -12,7 +12,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gen'
from pb import common_test_pb2 as pb
from api_tests import execute_api_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 workers.python.utils import upload_file_to_s3, scalar_map_to_dict
class TestActivities:
@ -134,10 +134,12 @@ class TestActivities:
activity.heartbeat()
# 调用实际的UI测试逻辑执行浏览器自动化测试并返回本地文件路径
ui_test_success, log_output, screenshot_path, html_report_path = await execute_ui_test_case(
req.test_case_id, req.url_path, req.browser_type, req.headless, scalar_map_to_dict(req.user_data)
ui_test_success, log_output, screenshot_path, html_report_path, browser_session_id = await execute_ui_test_case(
req.test_case_id, req.url_path, req.browser_type, req.headless, list(req.steps), req.browser_session_id
)
result.browser_session_id = browser_session_id
# 填充基本测试结果
result.base_result.success = ui_test_success
result.base_result.log_output = log_output

View File

@ -9,7 +9,7 @@ from temporalio.worker import Worker
# 确保能导入 gen 模块
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gen')))
from activities import TestActivities # 导入定义的 Activity
from workers.python.activities import TestActivities # 导入定义的 Activity
async def main():
# 连接 Temporal Server

View File

View File

@ -5,3 +5,4 @@ grpcio-tools # 用于生成 protobuf 代码
requests # 用于API测试
pytest # 测试框架
playwright # UI自动化测试库或使用 selenium
pytest-asyncio # Pytest support for asyncio

View File

View File

@ -0,0 +1,52 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from workers.python.ui_tests import execute_ui_test_case
from workers.python.pb import common_test_pb2 as pb
@pytest.mark.asyncio
async def test_execute_ui_test_case_with_steps():
# 1. Setup Mocks
mock_page = AsyncMock()
mock_browser = AsyncMock()
mock_browser.new_page.return_value = mock_page
# `page.locator` is a sync method returning an object with async methods.
# So, we use MagicMock to return our AsyncMock element.
mock_element = AsyncMock()
mock_page.locator = MagicMock(return_value=mock_element)
# Patch the playwright instance and browser sessions
with patch('workers.python.ui_tests._playwright_instance', new_callable=AsyncMock) as mock_playwright:
mock_playwright.chromium.launch.return_value = mock_browser
with patch('workers.python.ui_tests._browser_sessions', new_callable=dict):
# 2. Prepare test data
steps = [
pb.UiStep(name="Click button", event_type="click", selector="#button1"),
pb.UiStep(name="Input text", event_type="input", selector="#input1", input_value="test"),
]
# 3. Call the function under test
success, log_output, _, _, _ = await execute_ui_test_case(
test_case_id="test1",
url_path="/",
browser_type="chromium",
headless=True,
steps=steps,
browser_session_id=None
)
# 4. Assertions
assert success is True, f"Test failed with logs: {log_output}"
# Assert page navigation
mock_page.goto.assert_awaited_once_with('https://playwright.dev/')
# Assert that the locator was called correctly
mock_page.locator.assert_any_call("#button1")
mock_page.locator.assert_any_call("#input1")
# Assert that the async methods on the element were awaited
mock_element.click.assert_awaited_once()
mock_element.fill.assert_awaited_once_with("test")

View File

@ -1,5 +1,5 @@
# UI 测试具体实现 (使用 Playwright)
from playwright.async_api import async_playwright, expect
from playwright.async_api import async_playwright
import os
import datetime
import uuid
@ -9,13 +9,13 @@ from typing import Optional
_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):
async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: str, headless: bool, steps: list, browser_session_id: Optional[str] = None):
"""
实际执行UI测试的函数
支持浏览器会话复用
"""
global _browser_sessions, _playwright_instance
base_url = "https://playwright.dev" # 假设 UI 测试的基地址
base_url = "" # 假设 UI 测试的基地址
full_url = f"{base_url}{url_path}"
log_output = []
@ -54,26 +54,36 @@ async def execute_ui_test_case(test_case_id: str, url_path: str, browser_type: s
page = await browser.new_page()
# 模拟登录(如果需要)
if user_data:
log_output.append(f"Attempting to log in with user: {user_data.get('user')}")
await page.goto(f"{base_url}/login")
await page.fill('input[name="username"]', user_data.get('user', ''))
await page.fill('input[name="password"]', user_data.get('pass', ''))
await page.click('button[type="submit"]')
await page.wait_for_url(full_url)
await page.goto(full_url)
log_output.append(f"Navigated to: {full_url}")
# 示例UI操作和断言
element = page.locator("text=Playwright enables reliable end-to-end testing for modern web apps.")
await expect(element).to_be_visible()
log_output.append("Found expected text on page.")
# 遍历并执行所有UI步骤
for step in steps:
log_output.append(f"Executing step: {step.name} ({step.event_type})")
element = page.locator(step.selector)
await page.click("text=Docs")
await page.wait_for_url("**/docs/intro")
log_output.append("Clicked 'Docs' link and navigated.")
if step.event_type == "click":
await element.click()
log_output.append(f"Clicked element with selector: {step.selector}")
elif step.event_type == "input":
await element.fill(step.input_value)
log_output.append(f"Input '{step.input_value}' into element with selector: {step.selector}")
elif step.event_type == "swipe":
# Playwright doesn't have a built-in swipe, so we simulate with mouse actions
box = await element.bounding_box()
if box:
start_x = box['x'] + box['width'] / 2
start_y = box['y'] + box['height'] / 2
await page.mouse.move(start_x, start_y)
await page.mouse.down()
await page.mouse.move(start_x + step.offset_x, start_y + step.offset_y)
await page.mouse.up()
log_output.append(f"Swiped on element with selector: {step.selector}")
elif step.event_type == "wait":
await page.wait_for_timeout(step.wait_time_seconds * 1000)
log_output.append(f"Waited for {step.wait_time_seconds} seconds.")
else:
log_output.append(f"Unsupported event type: {step.event_type}")
success = True
log_output.append("UI Test PASSED.")

View File

@ -86,7 +86,7 @@ func TestRunWorkflow(ctx workflow.Context, input *pb.TestRunInput) (*pb.TestRunO
UrlPath: "/dashboard", // 要测试的页面路径
BrowserType: "chromium", // 使用的浏览器类型
Headless: true, // 是否使用无头模式运行浏览器
UserData: map[string]string{"user": "test", "pass": "password"}, // 测试用的用户数据
//UserData: map[string]string{"user": "test", "pass": "password"}, // 测试用的用户数据
}
// 声明变量用于接收 UI 测试的结果