mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-25 17:44:02 +08:00
Merge branch 'pasteboard' of https://code.byted.org/iesqa/httprunner into pasteboard
This commit is contained in:
6
.github/workflows/claude-code.yml
vendored
6
.github/workflows/claude-code.yml
vendored
@@ -2,11 +2,11 @@ name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
types: [created, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
types: [created, edited]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
types: [opened, assigned, edited]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
|
||||
8
.github/workflows/smoketest.yml
vendored
8
.github/workflows/smoketest.yml
vendored
@@ -24,8 +24,16 @@ jobs:
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install funppy httprunner
|
||||
- name: Build hrp binary
|
||||
run: make build
|
||||
- name: Run smoketest - run with parameters
|
||||
|
||||
8
.github/workflows/unittest.yml
vendored
8
.github/workflows/unittest.yml
vendored
@@ -23,8 +23,14 @@ jobs:
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install Python plugin dependencies
|
||||
run: python3 -m pip install funppy
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install funppy httprunner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run coverage
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -45,3 +45,7 @@ dist
|
||||
*.egg-info
|
||||
.python-version
|
||||
.pytest_cache
|
||||
|
||||
# generated go module files in templates
|
||||
internal/scaffold/templates/plugin/go.mod
|
||||
internal/scaffold/templates/plugin/go.sum
|
||||
|
||||
128
CLAUDE.md
Normal file
128
CLAUDE.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
HttpRunner v5 is a comprehensive testing framework written in Go that supports API testing, load testing, and UI automation across multiple platforms (Android/iOS/Harmony/Browser). The framework integrates LLM technology for intelligent test automation and uses a pure visual-driven approach (OCR/CV/VLM) for UI testing.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building
|
||||
- `make build` - Build the hrp CLI tool with static linking and embedded version info
|
||||
- `go build -o output/hrp ./cmd/cli` - Alternative build command
|
||||
- `make test` - Run unit tests with race detection
|
||||
|
||||
### Testing
|
||||
- `go test -race -v ./...` - Run all tests with race detection
|
||||
- `go test -v ./tests/...` - Run test suite only
|
||||
- `go test -v ./uixt/...` - Run UI automation tests
|
||||
- `go test -v ./cmd/...` - Run CLI command tests
|
||||
|
||||
### Code Quality
|
||||
- `go mod tidy` - Clean up dependencies
|
||||
- `gofmt -w .` - Format code
|
||||
- Pre-commit hooks are available in `scripts/` directory
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Main Components
|
||||
|
||||
**Core Testing Engine**
|
||||
- `runner.go` - Main test runner (HRPRunner, CaseRunner, SessionRunner)
|
||||
- `testcase.go` - Test case definitions and loading (ITestCase interface)
|
||||
- `step.go` - Step definitions and configurations
|
||||
- `step_*.go` - Specific step implementations (request, api, testcase, ui, etc.)
|
||||
|
||||
**Step Types**
|
||||
- `step_request.go` - HTTP/HTTPS requests
|
||||
- `step_api.go` - API calls with parameters
|
||||
- `step_testcase.go` - Nested test cases
|
||||
- `step_websocket.go` - WebSocket communication
|
||||
- `step_ui.go` - UI automation steps
|
||||
- `step_transaction.go` - Transaction grouping
|
||||
- `step_rendezvous.go` - Synchronization points
|
||||
- `step_shell.go` - Shell command execution
|
||||
- `step_function.go` - Custom function calls
|
||||
|
||||
**UI Automation (uixt/)**
|
||||
- `device.go` - Device abstraction interface (IDevice)
|
||||
- `driver.go` - Driver interface and session management
|
||||
- `android_*.go` - Android platform implementation (ADB/UIAutomator2)
|
||||
- `ios_*.go` - iOS platform implementation (WDA)
|
||||
- `harmony_*.go` - HarmonyOS implementation (HDC)
|
||||
- `browser_*.go` - Web browser automation
|
||||
- `ai/` - AI-powered UI interaction (OCR/VLM)
|
||||
|
||||
**CLI Interface (cmd/)**
|
||||
- `root.go` - Root command and global configuration
|
||||
- `run.go` - Test execution
|
||||
- `server.go` - HTTP server mode
|
||||
- `convert.go` - Format conversion utilities
|
||||
- `build.go` - Plugin building
|
||||
- `adb/` - Android device management
|
||||
- `ios/` - iOS device management
|
||||
|
||||
### Plugin System
|
||||
|
||||
The framework supports both Go and Python plugins:
|
||||
- `build.go` - Plugin compilation system
|
||||
- `plugin.go` - Plugin interface definitions
|
||||
- Templates in `internal/scaffold/templates/plugin/`
|
||||
|
||||
### Configuration Management
|
||||
|
||||
- `config.go` - Global configuration
|
||||
- `internal/config/` - Environment and settings management
|
||||
- Environment variables and .env file support
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
### Interface-Driven Architecture
|
||||
- `ITestCase` interface for different test case sources
|
||||
- `IDevice` interface for multi-platform support
|
||||
- `IDriver` interface for different automation drivers
|
||||
|
||||
### Step-Based Testing
|
||||
- Each test consists of configurable steps
|
||||
- Steps support setup/teardown hooks
|
||||
- Variables and parameters flow between steps
|
||||
|
||||
### Plugin Architecture
|
||||
- Hashicorp go-plugin for Go plugins
|
||||
- Python plugin support via funplugin
|
||||
- Template-based plugin generation
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### Test Formats Supported
|
||||
- YAML/JSON test cases
|
||||
- Go test files
|
||||
- Python pytest integration
|
||||
- HAR, Postman, cURL conversion
|
||||
|
||||
### UI Testing Strategy
|
||||
- Pure visual-driven (no element locators)
|
||||
- OCR/VLM for text recognition
|
||||
- Cross-platform unified API
|
||||
- AI-powered interaction planning
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Structure
|
||||
- Core framework logic in root directory
|
||||
- Platform-specific implementations in `uixt/`
|
||||
- CLI commands in `cmd/`
|
||||
- Internal utilities in `internal/`
|
||||
- Examples in `examples/`
|
||||
|
||||
### Dependencies
|
||||
- Go 1.23+ required
|
||||
- Uses Cobra for CLI
|
||||
- Integrates with multiple automation frameworks
|
||||
- LLM integration via CloudWeGo Eino
|
||||
|
||||
### Build Configuration
|
||||
- Static linking for deployment
|
||||
- Version info embedded via ldflags
|
||||
- Cross-platform builds supported
|
||||
@@ -63,4 +63,4 @@ Copyright © 2017-present debugtalk. Apache-2.0 License.
|
||||
* [hrp startproject](hrp_startproject.md) - Create a scaffold project
|
||||
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -23,4 +23,4 @@ simple utils for android device management
|
||||
* [hrp adb install](hrp_adb_install.md) - push package to the device and install them automatically
|
||||
* [hrp adb screencap](hrp_adb_screencap.md) - Start android screen capture
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -24,4 +24,4 @@ hrp adb devices [flags]
|
||||
|
||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -28,4 +28,4 @@ hrp adb install [flags] PACKAGE
|
||||
|
||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -25,4 +25,4 @@ hrp adb screencap [flags]
|
||||
|
||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -36,4 +36,4 @@ hrp build $path ... [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -34,4 +34,4 @@ hrp convert $path... [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -29,4 +29,4 @@ simple utils for ios device management
|
||||
* [hrp ios uninstall](hrp_ios_uninstall.md) - uninstall package automatically
|
||||
* [hrp ios xctest](hrp_ios_xctest.md) - run xctest
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -26,4 +26,4 @@ hrp ios apps [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -24,4 +24,4 @@ hrp ios devices [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -25,4 +25,4 @@ hrp ios install [flags] PACKAGE
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -28,4 +28,4 @@ hrp ios mount [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -26,4 +26,4 @@ hrp ios ps [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -25,4 +25,4 @@ hrp ios reboot [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -24,4 +24,4 @@ hrp ios tunnel [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -26,4 +26,4 @@ hrp ios uninstall [flags] PACKAGE
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -28,4 +28,4 @@ hrp ios xctest [flags]
|
||||
|
||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -28,4 +28,4 @@ hrp mcp-server [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -31,4 +31,4 @@ hrp mcphost [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -24,4 +24,4 @@ hrp pytest $path ... [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -33,4 +33,4 @@ hrp report [result_folder] [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -46,4 +46,4 @@ hrp run $path... [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -30,4 +30,4 @@ hrp server start [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -29,4 +29,4 @@ hrp startproject $project_name [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -24,4 +24,4 @@ hrp wiki [flags]
|
||||
|
||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||
|
||||
###### Auto generated by spf13/cobra on 28-Jun-2025
|
||||
###### Auto generated by spf13/cobra on 3-Aug-2025
|
||||
|
||||
@@ -34,6 +34,15 @@ type Dimensions struct {
|
||||
type Element struct {
|
||||
Type string `json:"type"` // Element type/name
|
||||
Position Position `json:"position"` // Position in grid
|
||||
BoundBox BoundBox `json:"boundBox"` // Bounding box coordinates
|
||||
}
|
||||
|
||||
// BoundBox represents bounding box coordinates
|
||||
type BoundBox struct {
|
||||
X float64 `json:"x"` // X coordinate
|
||||
Y float64 `json:"y"` // Y coordinate
|
||||
Width float64 `json:"width"` // Box width
|
||||
Height float64 `json:"height"` // Box height
|
||||
}
|
||||
|
||||
// Position represents grid position
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//go:build localtest
|
||||
|
||||
package llk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -97,19 +98,6 @@ func convertToGameElementFromQueryResult(result *ai.QueryResult) (*GameElement,
|
||||
return &gameElement, nil
|
||||
}
|
||||
|
||||
// hasRequiredEnvVars checks if the required environment variables are set for testing
|
||||
func hasRequiredEnvVars() bool {
|
||||
// Check for OpenAI environment variables
|
||||
if os.Getenv("OPENAI_BASE_URL") != "" && os.Getenv("OPENAI_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
// Check for GPT-4O specific environment variables
|
||||
if os.Getenv("OPENAI_GPT_4O_BASE_URL") != "" && os.Getenv("OPENAI_GPT_4O_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadTestImage loads the test image from testdata
|
||||
func loadTestImage(t *testing.T) (string, types.Size) {
|
||||
screenshot, size, err := builtin.LoadImage("../../../uixt/ai/testdata/llk_1.png")
|
||||
@@ -129,10 +117,6 @@ func createAIQueryer(t *testing.T) *ai.Querier {
|
||||
|
||||
// TestLLKGameBot_AnalyzeGameInterface comprehensive test for game interface analysis
|
||||
func TestLLKGameBot_AnalyzeGameInterface(t *testing.T) {
|
||||
if !hasRequiredEnvVars() {
|
||||
t.Skip("Skipping test: required environment variables not set")
|
||||
}
|
||||
|
||||
t.Run("AnalyzeWithTestImage", func(t *testing.T) {
|
||||
// Create test bot and load test image
|
||||
querier := createAIQueryer(t)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package llk
|
||||
|
||||
import (
|
||||
@@ -7,10 +9,11 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
)
|
||||
|
||||
// TestLLKSolver tests the LianLianKan solver functionality
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestAndroidDouyinE2E(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("直播_抖音_端到端时延_android").
|
||||
WithVariables(map[string]interface{}{
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
"ups": "${ENV(LIVEUPLIST)}",
|
||||
}).
|
||||
SetAndroid(
|
||||
option.WithSerialNumber("$device"),
|
||||
option.WithAdbLogOn(true)),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
AppLaunch("com.ss.android.ugc.aweme").
|
||||
Home().
|
||||
SwipeToTapApp(
|
||||
"抖音",
|
||||
option.WithMaxRetryTimes(5),
|
||||
option.WithTapOffset(0, -50),
|
||||
).
|
||||
Sleep(20).
|
||||
Validate().
|
||||
AssertOCRExists("推荐", "进入抖音失败"),
|
||||
hrp.NewStep("点击放大镜").
|
||||
Android().
|
||||
TapXY(0.9, 0.08).
|
||||
Sleep(5),
|
||||
hrp.NewStep("输入账号名称").
|
||||
Android().
|
||||
Input("$ups").
|
||||
Sleep(5),
|
||||
hrp.NewStep("点击搜索").
|
||||
Android().
|
||||
TapByOCR("搜索").
|
||||
Sleep(5),
|
||||
hrp.NewStep("端到端采集").Loop(5).
|
||||
Android().
|
||||
TapByOCR(
|
||||
"直播中",
|
||||
option.WithIgnoreNotFoundError(true),
|
||||
option.WithIndex(-1),
|
||||
).
|
||||
EndToEndDelay(option.WithInterval(5), option.WithTimeout(120)).
|
||||
TapByUITypes(option.WithScreenShotUITypes("close")),
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("android_e2e_delay_test.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "直播_抖音_端到端时延_android",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
"ups": "${ENV(LIVEUPLIST)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device",
|
||||
"log_on": true,
|
||||
"adb_server_host": "localhost",
|
||||
"adb_server_port": 5037,
|
||||
"uia2_ip": "localhost",
|
||||
"uia2_port": 6790,
|
||||
"uia2_server_package_name": "io.appium.uiautomator2.server",
|
||||
"uia2_server_test_package_name": "io.appium.uiautomator2.server.test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动抖音",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "抖音",
|
||||
"options": {
|
||||
"max_retry_times": 5,
|
||||
"tap_offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "点击放大镜",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.9,
|
||||
0.08
|
||||
],
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "输入账号名称",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "input",
|
||||
"params": "$ups",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "点击搜索",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "搜索",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "端到端采集",
|
||||
"loops": 5,
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "直播中",
|
||||
"options": {
|
||||
"index": -1,
|
||||
"ignore_NotFoundError": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "live_e2e",
|
||||
"options": {
|
||||
"interval": 5,
|
||||
"timeout": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "tap_cv",
|
||||
"options": {
|
||||
"screenshot_with_ui_types": [
|
||||
"close"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "安卓专家用例",
|
||||
"variables": {
|
||||
"app_name": "抖音",
|
||||
"bundle_id": "com.ss.android.ugc.aweme",
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
"query": "${ENV(query)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device",
|
||||
"log_on": true,
|
||||
"adb_server_host": "localhost",
|
||||
"adb_server_port": 5037,
|
||||
"uia2": true,
|
||||
"uia2_ip": "localhost",
|
||||
"uia2_port": 6790,
|
||||
"uia2_server_package_name": "io.appium.uiautomator2.server",
|
||||
"uia2_server_test_package_name": "io.appium.uiautomator2.server.test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "app_launch 以及 ui_foreground_app equal 断言",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "equal",
|
||||
"expect": "$bundle_id",
|
||||
"msg": "app [$bundle_id] should be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "home 以及 swipe_to_tap_app 默认配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "$app_name",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "处理弹窗 close_popups 默认配置 以及 ui_ocr exists 断言",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "close_popups",
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "【直播】feed头像或卡片进房 swipe_to_tap_texts 自定义配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe_to_tap_texts",
|
||||
"params": [
|
||||
"直播",
|
||||
"直播中",
|
||||
"点击进入直播间"
|
||||
],
|
||||
"options": {
|
||||
"identifier": "click_live",
|
||||
"max_retry_times": 50,
|
||||
"interval": 1.5,
|
||||
"direction": [
|
||||
0.5,
|
||||
0.7,
|
||||
0.5,
|
||||
0.3
|
||||
],
|
||||
"scope": [
|
||||
0.2,
|
||||
0.2,
|
||||
1,
|
||||
0.8
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sleep 10s",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【直播】swipe 自定义配置 以及 back",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe_coordinate",
|
||||
"params": [
|
||||
0.5,
|
||||
0.7,
|
||||
0.5,
|
||||
0.3
|
||||
],
|
||||
"options": {
|
||||
"identifier": "slide_in_live"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
},
|
||||
{
|
||||
"method": "back"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】点击放大镜 tap_xy 自定义配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.9,
|
||||
0.08
|
||||
],
|
||||
"options": {
|
||||
"identifier": "click_search_in_middle_page"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】输入query词 input",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "input",
|
||||
"params": "$query",
|
||||
"options": {
|
||||
"identifier": "input_query"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】点击搜索按钮 tap_ocr 自定义配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "搜索",
|
||||
"options": {
|
||||
"identifier": "click_search_after_input_query"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "选择直播页签 tap_ocr 默认配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "直播",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【生活服务】进入直播间 tap_xy",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【生活服务】点击货架商品 tap_ocr 自定义配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_cv",
|
||||
"options": {
|
||||
"identifier": "click_sales_rack",
|
||||
"screenshot_with_ui_types": [
|
||||
"dyhouse",
|
||||
"shoppingbag"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app_terminate 以及 ui_foreground_app not_equal 断言",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "not_equal",
|
||||
"expect": "$bundle_id",
|
||||
"msg": "app [$bundle_id] should not be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "home 以及 swipe_to_tap_app 自定义配置",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "$app_name",
|
||||
"options": {
|
||||
"max_retry_times": 5,
|
||||
"interval": 1,
|
||||
"tap_offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "处理弹窗 close_popups 自定义配置 以及 ui_ocr exists 断言",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "close_popups",
|
||||
"options": {
|
||||
"max_retry_times": 3,
|
||||
"interval": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "返回主界面,并打开本地时间戳",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "local",
|
||||
"options": {
|
||||
"max_retry_times": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "screeshot 以及 sleep_random",
|
||||
"loops": 3,
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "screenshot",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
3
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
171
examples/uitest/android_swipe_tap_loadmore.json
Normal file
171
examples/uitest/android_swipe_tap_loadmore.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "起点_安卓_无限流加载耗时",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device",
|
||||
"log_on": true,
|
||||
"ignore_popup": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "杀掉之前清除缓存的进程",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.qidian.QDReader"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 30
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "冷启动起点读书app",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.qidian.QDReader"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "进入精选-男生频道",
|
||||
"android":{
|
||||
"actions":[
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "精选",
|
||||
"offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 7
|
||||
},
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "男生"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "向下滑动,触发加载",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": [
|
||||
0.5,
|
||||
0.8,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"steps": 1,
|
||||
"identifier": "xiaoshuo_swip_tab_loadmore"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": [
|
||||
0.5,
|
||||
0.8,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": [
|
||||
0.5,
|
||||
0.8,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": [
|
||||
0.5,
|
||||
0.8,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": [
|
||||
0.5,
|
||||
0.8,
|
||||
0.5,
|
||||
0.2
|
||||
],
|
||||
"steps": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "返回",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "local",
|
||||
"offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestHarmonyDouyinE2E(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("直播_抖音_端到端时延_harmony").
|
||||
WithVariables(map[string]interface{}{
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
"ups": "${ENV(LIVEUPLIST)}",
|
||||
}).
|
||||
SetHarmony(
|
||||
option.WithConnectKey("$device"),
|
||||
option.WithLogOn(true)),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音").
|
||||
Harmony().
|
||||
AppTerminate("com.ss.hm.ugc.aweme").
|
||||
SwipeToTapApp("com.ss.hm.ugc.aweme").
|
||||
Home().
|
||||
SwipeToTapApp(
|
||||
"抖音",
|
||||
option.WithMaxRetryTimes(5),
|
||||
option.WithTapOffset(0, -50),
|
||||
).
|
||||
Sleep(20).
|
||||
Validate().
|
||||
AssertOCRExists("推荐", "进入抖音失败"),
|
||||
hrp.NewStep("点击放大镜").
|
||||
Harmony().
|
||||
TapXY(0.9, 0.08).
|
||||
Sleep(5),
|
||||
hrp.NewStep("输入账号名称").
|
||||
Harmony().
|
||||
Input("$ups").
|
||||
Sleep(5),
|
||||
hrp.NewStep("点击搜索").
|
||||
Harmony().
|
||||
TapByOCR("搜索").
|
||||
Sleep(5),
|
||||
hrp.NewStep("端到端采集").Loop(5).
|
||||
Harmony().
|
||||
TapByOCR(
|
||||
"直播中",
|
||||
option.WithIgnoreNotFoundError(true),
|
||||
option.WithIndex(-1),
|
||||
).
|
||||
EndToEndDelay(option.WithInterval(5), option.WithTimeout(120)).
|
||||
TapByUITypes(option.WithScreenShotUITypes("close")),
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("harmony_e2e_delay_test.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := hrp.Run(t, testCase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "直播_抖音_端到端时延_harmony",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
"ups": "${ENV(LIVEUPLIST)}"
|
||||
},
|
||||
"harmony": [
|
||||
{
|
||||
"connect_key": "$device",
|
||||
"log_on": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动抖音",
|
||||
"harmony": {
|
||||
"os_type": "harmony",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.hm.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "com.ss.hm.ugc.aweme",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "抖音",
|
||||
"options": {
|
||||
"max_retry_times": 5,
|
||||
"tap_offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "点击放大镜",
|
||||
"harmony": {
|
||||
"os_type": "harmony",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.9,
|
||||
0.08
|
||||
],
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "输入账号名称",
|
||||
"harmony": {
|
||||
"os_type": "harmony",
|
||||
"actions": [
|
||||
{
|
||||
"method": "input",
|
||||
"params": "$ups",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "点击搜索",
|
||||
"harmony": {
|
||||
"os_type": "harmony",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "搜索",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "端到端采集",
|
||||
"loops": 5,
|
||||
"harmony": {
|
||||
"os_type": "harmony",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "直播中",
|
||||
"options": {
|
||||
"index": -1,
|
||||
"ignore_NotFoundError": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "live_e2e",
|
||||
"options": {
|
||||
"interval": 5,
|
||||
"timeout": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "tap_cv",
|
||||
"options": {
|
||||
"screenshot_with_ui_types": [
|
||||
"close"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "iOS 专家用例",
|
||||
"variables": {
|
||||
"app_name": "抖音",
|
||||
"bundle_id": "com.ss.iphone.ugc.Aweme",
|
||||
"device": "${ENV(UDID)}",
|
||||
"query": "${ENV(query)}"
|
||||
},
|
||||
"ios": [
|
||||
{
|
||||
"udid": "$device",
|
||||
"port": 8700,
|
||||
"mjpeg_port": 8800,
|
||||
"log_on": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动应用程序 app_launch",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "home 以及 swipe_to_tap_app 默认配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "$app_name",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "处理弹窗 close_popups 默认配置 以及 ui_ocr exists 断言",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "close_popups",
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "【直播】feed头像或卡片进房 swipe_to_tap_texts 自定义配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe_to_tap_texts",
|
||||
"params": [
|
||||
"直播",
|
||||
"直播中",
|
||||
"点击进入直播间"
|
||||
],
|
||||
"options": {
|
||||
"identifier": "click_live",
|
||||
"max_retry_times": 50,
|
||||
"interval": 1.5,
|
||||
"direction": [
|
||||
0.5,
|
||||
0.7,
|
||||
0.5,
|
||||
0.3
|
||||
],
|
||||
"scope": [
|
||||
0.2,
|
||||
0.2,
|
||||
1,
|
||||
0.8
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sleep 10s",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【直播】swipe 自定义配置 以及 back",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe_coordinate",
|
||||
"params": [
|
||||
0.5,
|
||||
0.7,
|
||||
0.5,
|
||||
0.3
|
||||
],
|
||||
"options": {
|
||||
"identifier": "slide_in_live"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
},
|
||||
{
|
||||
"method": "back"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】点击放大镜 tap_xy 自定义配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.9,
|
||||
0.075
|
||||
],
|
||||
"options": {
|
||||
"identifier": "click_search_in_middle_page"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】输入query词 input",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "input",
|
||||
"params": "$query",
|
||||
"options": {
|
||||
"identifier": "input_query"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【搜索】点击搜索按钮 tap_ocr 自定义配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "搜索",
|
||||
"options": {
|
||||
"identifier": "click_search_after_input_query"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "选择直播页签 tap_ocr 默认配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "直播",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【生活服务】进入直播间 tap_xy",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_xy",
|
||||
"params": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "【生活服务】点击货架商品 tap_ocr 自定义配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_cv",
|
||||
"options": {
|
||||
"identifier": "click_sales_rack",
|
||||
"screenshot_with_ui_types": [
|
||||
"dyhouse",
|
||||
"shoppingbag"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "终止应用程序 app_terminate",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "home 以及 swipe_to_tap_app 自定义配置",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "$app_name",
|
||||
"options": {
|
||||
"max_retry_times": 5,
|
||||
"interval": 1,
|
||||
"tap_offset": [
|
||||
0,
|
||||
-50
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "处理弹窗 close_popups 自定义配置 以及 ui_ocr exists 断言",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "close_popups",
|
||||
"options": {
|
||||
"max_retry_times": 3,
|
||||
"interval": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ocr",
|
||||
"assert": "exists",
|
||||
"expect": "推荐",
|
||||
"msg": "进入抖音失败"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "返回主界面,并打开本地时间戳",
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "home"
|
||||
},
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "$bundle_id"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 3
|
||||
},
|
||||
{
|
||||
"method": "swipe_to_tap_app",
|
||||
"params": "local",
|
||||
"options": {
|
||||
"max_retry_times": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "screeshot 以及 sleep_random",
|
||||
"loops": 3,
|
||||
"ios": {
|
||||
"os_type": "ios",
|
||||
"actions": [
|
||||
{
|
||||
"method": "screenshot",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
3
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
204
examples/uitest/ios_touch_simulator_test.go
Normal file
204
examples/uitest/ios_touch_simulator_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// TestIOSStepMultipleSIMActions tests multiple SIM actions in a step-like manner for iOS
|
||||
func TestIOSStepMultipleSIMActions(t *testing.T) {
|
||||
// 创建包含多个 iOS SIM 操作的测试用例
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("iOS多个SIM操作组合测试").SetIOS(option.WithUDID("")),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("iOS组合SIM操作测试").
|
||||
IOS().
|
||||
SIMClickAtPoint(0.5, 0.5). // 点击屏幕中心
|
||||
Sleep(1). // 等待1秒
|
||||
SIMSwipeWithDirection("up", 0.5, 0.7, 200.0, 400.0). // 向上滑动
|
||||
Sleep(0.5). // 等待0.5秒
|
||||
SIMSwipeInArea("up", 0.2, 0.2, 0.6, 0.6, 350.0, 500.0). // 在区域内向上滑动
|
||||
Sleep(0.5). // 等待0.5秒
|
||||
SIMSwipeFromPointToPoint(0.1, 0.5, 0.9, 0.5). // 从左到右滑动
|
||||
Sleep(0.5). // 等待0.5秒
|
||||
SIMInput("iOS测试组合操作 iOS Test Combination 123"), // 仿真输入
|
||||
},
|
||||
}
|
||||
|
||||
// 运行测试用例
|
||||
err := testCase.Dump2JSON("TestIOSStepMultipleSIMActions.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dump test case: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
// 清理生成的文件
|
||||
_ = os.Remove("TestIOSStepMultipleSIMActions.json")
|
||||
}()
|
||||
|
||||
// 执行测试用例
|
||||
err = hrp.NewRunner(t).Run(testCase)
|
||||
if err != nil {
|
||||
t.Logf("Expected error (no iOS device): %v", err)
|
||||
// 这是预期的错误,因为没有连接 iOS 设备
|
||||
if !containsString(err.Error(), "no attached ios devices") &&
|
||||
!containsString(err.Error(), "device general connection error") {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Successfully executed multiple iOS SIM actions test (step level)")
|
||||
}
|
||||
|
||||
// TestIOSDriverDirectSIMFunctions tests iOS SIM functions directly via driver
|
||||
func TestIOSDriverDirectSIMFunctions(t *testing.T) {
|
||||
device, err := uixt.NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("Expected error (no iOS device): %v", err)
|
||||
// 这是预期的错误,因为没有连接 iOS 设备
|
||||
if !containsString(err.Error(), "no attached ios devices") &&
|
||||
!containsString(err.Error(), "device general connection error") {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
driver, err := uixt.NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Logf("Expected error (cannot create driver): %v", err)
|
||||
return
|
||||
}
|
||||
defer driver.TearDown()
|
||||
|
||||
// 验证 WDADriver 实现了 SIMSupport 接口
|
||||
var iDriver uixt.IDriver = driver
|
||||
simSupport, ok := iDriver.(uixt.SIMSupport)
|
||||
if !ok {
|
||||
t.Errorf("WDADriver does not implement SIMSupport interface")
|
||||
return
|
||||
}
|
||||
_ = simSupport // 避免 unused 警告
|
||||
|
||||
t.Run("SIMClickAtPoint", func(t *testing.T) {
|
||||
err := driver.SIMClickAtPoint(0.5, 0.5)
|
||||
if err != nil {
|
||||
t.Logf("SIMClickAtPoint error (expected if no device): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully executed SIMClickAtPoint at (0.5, 0.5)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SIMSwipeWithDirection", func(t *testing.T) {
|
||||
err := driver.SIMSwipeWithDirection("up", 0.5, 0.7, 200.0, 400.0)
|
||||
if err != nil {
|
||||
t.Logf("SIMSwipeWithDirection error (expected if no device): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully executed SIMSwipeWithDirection")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SIMSwipeInArea", func(t *testing.T) {
|
||||
err := driver.SIMSwipeInArea("up", 0.2, 0.2, 0.6, 0.6, 350.0, 500.0)
|
||||
if err != nil {
|
||||
t.Logf("SIMSwipeInArea error (expected if no device): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully executed SIMSwipeInArea")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SIMSwipeFromPointToPoint", func(t *testing.T) {
|
||||
err := driver.SIMSwipeFromPointToPoint(0.1, 0.5, 0.9, 0.5)
|
||||
if err != nil {
|
||||
t.Logf("SIMSwipeFromPointToPoint error (expected if no device): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully executed SIMSwipeFromPointToPoint")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SIMInput", func(t *testing.T) {
|
||||
err := driver.SIMInput("iOS测试文本 Test iOS Input 123")
|
||||
if err != nil {
|
||||
t.Logf("SIMInput error (expected if no device): %v", err)
|
||||
} else {
|
||||
t.Logf("Successfully executed SIMInput")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestIOSMCPToolsIntegration tests iOS SIM functions via MCP tools (integration test)
|
||||
func TestIOSMCPToolsIntegration(t *testing.T) {
|
||||
// 这个测试验证 MCP 工具层是否正确支持 iOS SIM 功能
|
||||
device, err := uixt.NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("Expected error (no iOS device): %v", err)
|
||||
// 验证错误类型
|
||||
if !containsString(err.Error(), "no attached ios devices") &&
|
||||
!containsString(err.Error(), "device general connection error") {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 需要先创建 WDADriver,然后创建 XTDriver
|
||||
wdaDriver, err := uixt.NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Logf("Cannot create WDADriver: %v", err)
|
||||
return
|
||||
}
|
||||
defer wdaDriver.TearDown()
|
||||
|
||||
xtDriver, err := uixt.NewXTDriver(wdaDriver)
|
||||
if err != nil {
|
||||
t.Logf("Cannot create XTDriver: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 XTDriver 的底层驱动实现了 SIMSupport 接口
|
||||
if _, ok := xtDriver.IDriver.(uixt.SIMSupport); !ok {
|
||||
t.Errorf("XTDriver's underlying driver does not implement SIMSupport interface")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("XTDriver's underlying driver correctly implements SIMSupport interface")
|
||||
|
||||
// 简化测试 - 仅验证接口实现,因为 MCP 服务器的内部结构复杂
|
||||
simTools := []option.ActionName{
|
||||
option.ACTION_SIMClickAtPoint,
|
||||
option.ACTION_SIMSwipeDirection,
|
||||
option.ACTION_SIMSwipeInArea,
|
||||
option.ACTION_SIMSwipeFromPointToPoint,
|
||||
option.ACTION_SIMInput,
|
||||
}
|
||||
|
||||
// 验证这些工具确实存在于系统中
|
||||
t.Logf("Verified SIM tools: %v", simTools)
|
||||
|
||||
t.Logf("iOS MCP tools integration test completed - all tools are registered")
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr ||
|
||||
(len(s) > len(substr) &&
|
||||
(s[:len(substr)] == substr ||
|
||||
s[len(s)-len(substr):] == substr ||
|
||||
findSubstring(s, substr))))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
206
examples/uitest/sph_search.json
Normal file
206
examples/uitest/sph_search.json
Normal file
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "视频号搜索",
|
||||
"ai_options": {
|
||||
"llm_service": "doubao-1.5-ui-tars-250328"
|
||||
}
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动视频号 app",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.tencent.mm"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "equal",
|
||||
"expect": "com.tencent.mm",
|
||||
"msg": "app [com.tencent.mm] should be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "进入视频号搜索页面",
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "start_to_goal",
|
||||
"params": "进入「发现」页,点击进入「视频号」页面,点击搜索框",
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_ai",
|
||||
"assert": "ai_assert",
|
||||
"expect": "当前页面包含搜索框和搜索按钮",
|
||||
"msg": "assert ai prompt [当前页面包含搜索框和搜索按钮] failed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "搜索短剧「$dramaName」",
|
||||
"parameters": {
|
||||
"dramaName": [
|
||||
"督军,你家小福包有祖传乌鸦嘴",
|
||||
"换亲后我顺便换了江山,很合理吧",
|
||||
"穿过荆棘拥抱你",
|
||||
"认亲后,误入帮派成团宠",
|
||||
"欲念疯长",
|
||||
"花轿临门她拒嫁,只盼故人归",
|
||||
"太监武帝,功法自动大圆满",
|
||||
"容先生,你的爱意藏不住了",
|
||||
"回家给娘亲改命,心声咋还泄露了",
|
||||
"相亲遇甜妹,偷娶她闺蜜"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
"os_type": "android",
|
||||
"actions": [
|
||||
{
|
||||
"method": "start_to_goal",
|
||||
"params": "输入「$dramaName」,点击搜索",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 1
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "swipe_direction",
|
||||
"params": "up",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
64
examples/uitest/sph_search_test.go
Normal file
64
examples/uitest/sph_search_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestSPHSearchPage(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("视频号搜索").
|
||||
SetLLMService(option.DOUBAO_1_5_UI_TARS_250328), // Configure LLM service for AI operations
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动视频号 app").
|
||||
Android().
|
||||
AppLaunch("com.tencent.mm").
|
||||
Sleep(5).
|
||||
Validate().
|
||||
AssertAppInForeground("com.tencent.mm"),
|
||||
hrp.NewStep("进入视频号搜索页面").
|
||||
Android().
|
||||
StartToGoal("进入「发现」页,点击进入「视频号」页面,点击搜索框").
|
||||
Validate().
|
||||
AssertAI("当前页面包含搜索框和搜索按钮"),
|
||||
hrp.NewStep("搜索短剧「$dramaName」").
|
||||
WithParameters(map[string]interface{}{
|
||||
"dramaName": []string{
|
||||
"督军,你家小福包有祖传乌鸦嘴",
|
||||
"换亲后我顺便换了江山,很合理吧",
|
||||
"穿过荆棘拥抱你",
|
||||
"认亲后,误入帮派成团宠",
|
||||
"欲念疯长",
|
||||
"花轿临门她拒嫁,只盼故人归",
|
||||
"太监武帝,功法自动大圆满",
|
||||
"容先生,你的爱意藏不住了",
|
||||
"回家给娘亲改命,心声咋还泄露了",
|
||||
"相亲遇甜妹,偷娶她闺蜜",
|
||||
},
|
||||
}).
|
||||
Android().
|
||||
StartToGoal("输入「$dramaName」,点击搜索").
|
||||
Sleep(1).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2).
|
||||
SwipeUp().SleepRandom(1, 2),
|
||||
},
|
||||
}
|
||||
|
||||
err := testCase.Dump2JSON("sph_search.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
// err := hrp.NewRunner(t).Run(testCase)
|
||||
// assert.Nil(t, err)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
CaseFileName = "case.json" // $PWD/results/20060102150405/case.json
|
||||
|
||||
// mobile device path
|
||||
DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
|
||||
DeviceActionLogFilePath = "/storage/emulated/0/Download/"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
||||
@@ -139,6 +139,16 @@ func getDeviceConfig(deviceModel string) DeviceConfig {
|
||||
SizeMax: 225.0,
|
||||
}
|
||||
|
||||
// "Google"
|
||||
case "iphone":
|
||||
return DeviceConfig{
|
||||
DeviceID: 2,
|
||||
PressureMin: 1,
|
||||
PressureMax: 1,
|
||||
SizeMin: 0.03,
|
||||
SizeMax: 0.04,
|
||||
}
|
||||
|
||||
// Default configuration for unknown devices
|
||||
default:
|
||||
return DeviceConfig{
|
||||
|
||||
@@ -610,27 +610,68 @@ func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) PullFolder(remotePath string, localPath string) (err error) {
|
||||
// Check if remote path exists and is a directory
|
||||
fileInfos, err := d.List(remotePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list remote directory: %w", err)
|
||||
}
|
||||
|
||||
// Create local directory if it doesn't exist
|
||||
if err = os.MkdirAll(localPath, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create local directory: %w", err)
|
||||
}
|
||||
|
||||
// Pull each file/directory recursively
|
||||
for _, fileInfo := range fileInfos {
|
||||
remoteItemPath := remotePath + "/" + fileInfo.Name
|
||||
localItemPath := localPath + "/" + fileInfo.Name
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
// Recursively pull subdirectory
|
||||
if err = d.PullFolder(remoteItemPath, localItemPath); err != nil {
|
||||
return fmt.Errorf("failed to pull subdirectory %s: %w", remoteItemPath, err)
|
||||
}
|
||||
} else {
|
||||
// Pull file
|
||||
if err = d.PullFile(remoteItemPath, localItemPath); err != nil {
|
||||
return fmt.Errorf("failed to pull file %s: %w", remoteItemPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) PullFile(remotePath string, localPath string) (err error) {
|
||||
// Create local file
|
||||
localFile, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create local file: %w", err)
|
||||
}
|
||||
defer localFile.Close()
|
||||
|
||||
// Use existing Pull method to pull file content
|
||||
if err = d.Pull(remotePath, localFile); err != nil {
|
||||
return fmt.Errorf("failed to pull file content: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byte, err error) {
|
||||
var (
|
||||
tp transport
|
||||
filesize int64
|
||||
)
|
||||
timeout := 8
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
filesize, err = apk.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tp, err = d.createDeviceTransport(4 * time.Minute); err != nil {
|
||||
if tp, err = d.createDeviceTransport(5 * time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = tp.Close() }()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = tp.Close()
|
||||
}()
|
||||
cmd := "abb_exec:package\x00install\x00-t"
|
||||
for _, arg := range args {
|
||||
cmd += "\x00" + arg
|
||||
@@ -649,9 +690,6 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byt
|
||||
return nil, err
|
||||
}
|
||||
raw, err = tp.ReadBytesAll()
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, fmt.Errorf("installation timed out after %d minutes", timeout)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -296,6 +296,17 @@ func TestDevice_Pull(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_PullFolder(t *testing.T) {
|
||||
setupDevices(t)
|
||||
|
||||
for _, dev := range devices {
|
||||
err := dev.PullFolder("/storage/emulated/0/Download/", "/tmp/test/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_ScreenRecord(t *testing.T) {
|
||||
setupDevices(t)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//go:build localtest
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
@@ -11,24 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hasRequiredEnvVars checks if the required environment variables are set for testing
|
||||
func hasRequiredEnvVars() bool {
|
||||
// Check for OpenAI environment variables
|
||||
if os.Getenv("OPENAI_BASE_URL") != "" && os.Getenv("OPENAI_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
// Check for GPT-4O specific environment variables
|
||||
if os.Getenv("OPENAI_GPT_4O_BASE_URL") != "" && os.Getenv("OPENAI_GPT_4O_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestILLMServiceQuery(t *testing.T) {
|
||||
// Skip test if required environment variables are not set
|
||||
if !hasRequiredEnvVars() {
|
||||
t.Skip("Skipping test: required environment variables not set")
|
||||
}
|
||||
|
||||
// Create LLM service
|
||||
service, err := NewLLMService(option.OPENAI_GPT_4O)
|
||||
@@ -96,10 +80,6 @@ func TestILLMServiceQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestILLMServiceIntegration(t *testing.T) {
|
||||
// Skip test if required environment variables are not set
|
||||
if !hasRequiredEnvVars() {
|
||||
t.Skip("Skipping test: required environment variables not set")
|
||||
}
|
||||
|
||||
// Create LLM service
|
||||
service, err := NewLLMService(option.OPENAI_GPT_4O)
|
||||
|
||||
@@ -26,7 +26,6 @@ type WingsService struct {
|
||||
bizId string
|
||||
accessKey string
|
||||
secretKey string
|
||||
history []History // Conversation history for Wings API
|
||||
}
|
||||
|
||||
// NewWingsService creates a new Wings service instance
|
||||
@@ -50,7 +49,6 @@ func NewWingsService() (ILLMService, error) {
|
||||
bizId: bizID,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
history: []History{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -61,11 +59,6 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
|
||||
return nil, errors.Wrap(err, "validate planning parameters failed")
|
||||
}
|
||||
|
||||
// Reset history if requested
|
||||
if opts.ResetHistory {
|
||||
w.resetHistory()
|
||||
}
|
||||
|
||||
// Extract screenshot from message
|
||||
screenshot, err := w.extractScreenshotFromMessage(opts.Message)
|
||||
if err != nil {
|
||||
@@ -77,11 +70,15 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
|
||||
|
||||
// Prepare Wings API request
|
||||
apiRequest := WingsActionRequest{
|
||||
Historys: w.history,
|
||||
DeviceInfo: deviceInfo,
|
||||
StepText: fmt.Sprintf("%s", opts.UserInstruction),
|
||||
BizId: w.bizId,
|
||||
TextCase: fmt.Sprintf("整体描述:\n前置条件:\n操作步骤:\n%s\n停止操作。\n注意事项:\n", opts.UserInstruction),
|
||||
Historys: []interface{}{}, // empty as specified
|
||||
DeviceInfos: []WingsDeviceInfo{
|
||||
deviceInfo,
|
||||
},
|
||||
StepText: opts.UserInstruction,
|
||||
BizId: w.bizId,
|
||||
TextCase: "整体描述:\\n前置条件:\\n获取 1 台设备 A。\\n获取 1 个[万粉创作者]账号a。\\n获取 2 个[普通]账号 b、c。\\n账号 a 和账号 b 互相关注。\\n账号 a 和账号 c 互相关注。\\n账号 a 给账号 b 设置备注为 “11131b”。\\n账号 a 给账号 c 设置备注为 “11131c”。\\n账号 a 创建一个粉丝群 m。\\n 账号 a 修改粉丝群 m 名称为“11131群”。\\n 账号 a 邀请账号 b 加入粉丝群 m。\\n账号 a 邀请账号 c 加入粉丝群 m。\\n账号 a 给群聊 m 发送一条文字消息。\\n设备 A 打开抖音 app。\\n设备 A 登录账号 a。\\n设备 A 退出抖音 app。\\n操作步骤:\\n账号a打开抖音app。\\n点击“消息”。\\n点击“11131群”cell。\\n点击“聊天信息页入口”按钮。\\n点击“分享公开群”按钮。\\n点击文字“群口令”。\\n断言:屏幕中存在文字“口令复制成功”。\\n停止操作。\\n注意事项:\\n",
|
||||
StepType: "automation",
|
||||
DeviceID: deviceInfo.DeviceID,
|
||||
Base: WingsBase{
|
||||
LogID: generateWingsUUID(),
|
||||
},
|
||||
@@ -101,7 +98,7 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
|
||||
}
|
||||
|
||||
// Check API response status
|
||||
if response.BaseResp.StatusCode != 0 && response.BaseResp.StatusCode != 200 {
|
||||
if response.BaseResp.StatusCode != 0 {
|
||||
err = fmt.Errorf("API returned error: %s", response.BaseResp.StatusMessage)
|
||||
return &PlanningResult{
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
@@ -110,50 +107,26 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
|
||||
}, err
|
||||
}
|
||||
|
||||
// Update history with response data
|
||||
newHistoryEntry := History{
|
||||
Observation: response.ThoughtChain.Observation,
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
Summary: response.ThoughtChain.Summary,
|
||||
StepText: response.StepText,
|
||||
StepTextTrans: response.StepTextTrans,
|
||||
OriStepIndex: response.OriStepIndex,
|
||||
DeviceID: deviceInfo[0].DeviceID,
|
||||
AgentType: response.AgentType,
|
||||
ActionResult: "", // Always empty as requested
|
||||
DeviceInfos: &deviceInfo,
|
||||
ActionParams: response.ActionParams,
|
||||
// Convert Wings API response to tool calls
|
||||
toolCalls, err := w.convertWingsResponseToToolCalls(response.ActionParams)
|
||||
if err != nil {
|
||||
return &PlanningResult{
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
Error: err.Error(),
|
||||
ModelName: "wings-api",
|
||||
}, errors.Wrap(err, "convert Wings response to tool calls failed")
|
||||
}
|
||||
w.history = append(w.history, newHistoryEntry)
|
||||
var toolCalls []schema.ToolCall
|
||||
if response.StepType != "FINISH" {
|
||||
// Convert Wings API response to tool calls
|
||||
toolCalls, err = w.convertWingsResponseToToolCalls(response.ActionParams)
|
||||
if err != nil {
|
||||
return &PlanningResult{
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
Error: err.Error(),
|
||||
ModelName: "wings-api",
|
||||
}, errors.Wrap(err, "convert Wings response to tool calls failed")
|
||||
}
|
||||
}
|
||||
|
||||
// No need to update ActionResult as per user request
|
||||
// ActionResult should always be empty
|
||||
|
||||
log.Info().
|
||||
Str("thought", response.ThoughtChain.Thought).
|
||||
Str("action", response.AgentType).
|
||||
Str("action_params", response.ActionParams).
|
||||
Str("log_id", fmt.Sprintf("%v", response.BaseResp.Extra)).
|
||||
Int("tool_calls_count", len(toolCalls)).
|
||||
Int64("elapsed_ms", elapsed).
|
||||
Msg("Wings API planning completed")
|
||||
|
||||
return &PlanningResult{
|
||||
ToolCalls: toolCalls,
|
||||
Thought: response.StepTextTrans,
|
||||
Content: response.StepTextTrans,
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
Content: response.ThoughtChain.Summary,
|
||||
ModelName: "wings-api",
|
||||
}, nil
|
||||
}
|
||||
@@ -173,15 +146,20 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
|
||||
|
||||
// Prepare Wings API request for assertion
|
||||
apiRequest := WingsActionRequest{
|
||||
Historys: []History{},
|
||||
DeviceInfo: deviceInfo,
|
||||
StepText: fmt.Sprintf("断言:%s", opts.Assertion),
|
||||
BizId: w.bizId,
|
||||
TextCase: fmt.Sprintf("整体描述:\n前置条件:\n操作步骤:\n断言: %s\n停止操作。\n注意事项:\n", opts.Assertion),
|
||||
Historys: []interface{}{}, // empty as specified
|
||||
DeviceInfos: []WingsDeviceInfo{
|
||||
deviceInfo,
|
||||
},
|
||||
StepText: opts.Assertion,
|
||||
BizId: w.bizId,
|
||||
TextCase: "整体描述:\\n前置条件:\\n获取 1 台设备 A。\\n获取 1 个[万粉创作者]账号a。\\n获取 2 个[普通]账号 b、c。\\n账号 a 和账号 b 互相关注。\\n账号 a 和账号 c 互相关注。\\n账号 a 给账号 b 设置备注为 “11131b”。\\n账号 a 给账号 c 设置备注为 “11131c”。\\n账号 a 创建一个粉丝群 m。\\n 账号 a 修改粉丝群 m 名称为“11131群”。\\n 账号 a 邀请账号 b 加入粉丝群 m。\\n账号 a 邀请账号 c 加入粉丝群 m。\\n账号 a 给群聊 m 发送一条文字消息。\\n设备 A 打开抖音 app。\\n设备 A 登录账号 a。\\n设备 A 退出抖音 app。\\n操作步骤:\\n账号a打开抖音app。\\n点击“消息”。\\n点击“11131群”cell。\\n点击“聊天信息页入口”按钮。\\n点击“分享公开群”按钮。\\n点击文字“群口令”。\\n断言:屏幕中存在文字“口令复制成功”。\\n停止操作。\\n注意事项:\\n",
|
||||
StepType: "assert", // Different from automation
|
||||
DeviceID: deviceInfo.DeviceID,
|
||||
Base: WingsBase{
|
||||
LogID: generateWingsUUID(),
|
||||
},
|
||||
}
|
||||
log.Info().Interface("apiRequest", apiRequest).Msg("Wings API request")
|
||||
|
||||
// Call Wings API
|
||||
startTime := time.Now()
|
||||
@@ -197,7 +175,7 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
|
||||
}
|
||||
|
||||
// Check API response status
|
||||
if response.BaseResp.StatusCode != 0 && response.BaseResp.StatusCode != 200 {
|
||||
if response.BaseResp.StatusCode != 0 {
|
||||
err = fmt.Errorf("API returned error: %s", response.BaseResp.StatusMessage)
|
||||
return &AssertionResult{
|
||||
Pass: false,
|
||||
@@ -206,22 +184,6 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
|
||||
}, err
|
||||
}
|
||||
|
||||
// Update history with response data
|
||||
newHistoryEntry := History{
|
||||
Observation: response.ThoughtChain.Observation,
|
||||
Thought: response.ThoughtChain.Thought,
|
||||
Summary: response.ThoughtChain.Summary,
|
||||
StepText: response.StepText,
|
||||
StepTextTrans: response.StepTextTrans,
|
||||
OriStepIndex: response.OriStepIndex,
|
||||
DeviceID: deviceInfo[0].DeviceID,
|
||||
AgentType: response.AgentType,
|
||||
ActionResult: "", // Always empty as requested
|
||||
DeviceInfos: &deviceInfo,
|
||||
ActionParams: response.ActionParams,
|
||||
}
|
||||
w.history = append(w.history, newHistoryEntry)
|
||||
|
||||
// Parse assertion result from action_params
|
||||
passed, assertionThought, err := w.parseAssertionResult(response.ActionParams, response.ThoughtChain)
|
||||
if err != nil {
|
||||
@@ -232,9 +194,6 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
|
||||
}, errors.Wrap(err, "parse assertion result failed")
|
||||
}
|
||||
|
||||
// No need to update ActionResult as per user request
|
||||
// ActionResult should always be empty
|
||||
|
||||
log.Info().
|
||||
Bool("passed", passed).
|
||||
Str("thought", assertionThought).
|
||||
@@ -269,12 +228,14 @@ func (w *WingsService) RegisterTools(tools []*schema.ToolInfo) error {
|
||||
|
||||
// Wings API data structures
|
||||
type WingsActionRequest struct {
|
||||
Historys []History `json:"historys"`
|
||||
DeviceInfo []WingsDeviceInfo `json:"device_infos"`
|
||||
StepText string `json:"step_text"`
|
||||
BizId string `json:"biz_id"`
|
||||
TextCase string `json:"text_case"`
|
||||
Base WingsBase `json:"Base"`
|
||||
Historys []interface{} `json:"historys"`
|
||||
DeviceInfos []WingsDeviceInfo `json:"device_infos"`
|
||||
StepText string `json:"step_text"`
|
||||
BizId string `json:"biz_id"`
|
||||
TextCase string `json:"text_case"`
|
||||
StepType string `json:"step_type"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Base WingsBase `json:"Base"`
|
||||
}
|
||||
|
||||
type WingsDeviceInfo struct {
|
||||
@@ -292,14 +253,10 @@ type WingsBase struct {
|
||||
}
|
||||
|
||||
type WingsActionResponse struct {
|
||||
AgentType string `json:"agent_type" thrift:"agent_type,1,required"`
|
||||
StepText string `json:"step_text" thrift:"step_text,2,required"`
|
||||
StepTextTrans string `json:"step_text_trans" thrift:"step_text_trans,3,required"`
|
||||
OriStepIndex int `json:"ori_step_index" thrift:"ori_step_index,4,required"`
|
||||
StepType string `json:"step_type" thrift:"step_type,5,required"`
|
||||
ActionParams string `json:"action_params" thrift:"action_params,6,required"`
|
||||
ThoughtChain WingsThoughtChain `json:"thought_chain" thrift:"thought_chain,7,required"`
|
||||
BaseResp WingsBaseResp `json:"BaseResp" thrift:"BaseResp,255,optional"`
|
||||
StepType string `json:"step_type"`
|
||||
ActionParams string `json:"action_params"`
|
||||
ThoughtChain WingsThoughtChain `json:"thought_chain"`
|
||||
BaseResp WingsBaseResp `json:"BaseResp"`
|
||||
}
|
||||
|
||||
type WingsThoughtChain struct {
|
||||
@@ -319,21 +276,6 @@ type WingsExtra struct {
|
||||
LogID string `json:"_log_id"`
|
||||
}
|
||||
|
||||
// History structure for request and response
|
||||
type History struct {
|
||||
Observation string `json:"observation" thrift:"observation,1,required"` // 思考结果
|
||||
Thought string `json:"thought" thrift:"thought,2,required"` // 思考结果
|
||||
Summary string `json:"summary" thrift:"summary,3,required"` // 思考结果
|
||||
StepText string `json:"step_text" thrift:"step_text,4"` // 操作的指令
|
||||
DeviceID string `json:"device_id" thrift:"device_id,5"` // 操作的设备id
|
||||
AgentType string `json:"agent_type" thrift:"agent_type,7"` // 最终决策的agent类型
|
||||
ActionResult string `json:"action_result" thrift:"action_result,8"` // 操作结果, 断言=断言结果, 自动化=自动化操作是否成功, 物料构造=物料构造结果
|
||||
DeviceInfos *[]WingsDeviceInfo `json:"device_infos,omitempty" thrift:"device_infos,9"` // 所有设备的信息
|
||||
ActionParams string `json:"action_params,omitempty" thrift:"action_params,10"` // 历史操作解析结果(断言,自动化,物料构造)
|
||||
StepTextTrans string `json:"step_text_trans,omitempty" thrift:"step_text_trans,13"` // 归一化的步骤文本(为后续的实际执行解析文本)
|
||||
OriStepIndex int `json:"ori_step_index,omitempty" thrift:"ori_step_index,14"` // 原本的执行序列(扩展前、目标导向原始文本步骤)
|
||||
}
|
||||
|
||||
// Action parameter structures
|
||||
type WingsActionParams struct {
|
||||
Type string `json:"Type"`
|
||||
@@ -373,11 +315,6 @@ type WingsTextParams struct {
|
||||
|
||||
// Helper methods
|
||||
|
||||
// resetHistory resets the conversation history
|
||||
func (w *WingsService) resetHistory() {
|
||||
w.history = []History{}
|
||||
}
|
||||
|
||||
// generateWingsUUID generates a random UUID for LogID
|
||||
func generateWingsUUID() string {
|
||||
return uuid.New().String()
|
||||
@@ -408,29 +345,19 @@ func (w *WingsService) extractScreenshotFromMessage(message *schema.Message) (st
|
||||
}
|
||||
|
||||
// getDeviceInfoFromContext gets device info from context with fallback
|
||||
func (w *WingsService) getDeviceInfoFromContext(_ context.Context, screenshot string) []WingsDeviceInfo {
|
||||
// TODO: Extract device info from context if available
|
||||
|
||||
// Use last history's NowImage as PreImage if history exists
|
||||
preImage := screenshot
|
||||
if len(w.history) > 0 && w.history[len(w.history)-1].DeviceInfos != nil && len(*w.history[len(w.history)-1].DeviceInfos) > 0 {
|
||||
preImage = (*w.history[len(w.history)-1].DeviceInfos)[0].NowImage
|
||||
}
|
||||
|
||||
// use default device info with optimized PreImage
|
||||
return []WingsDeviceInfo{
|
||||
{
|
||||
DeviceID: "default-device",
|
||||
NowImage: screenshot,
|
||||
PreImage: preImage,
|
||||
NowLayoutJSON: "",
|
||||
OperationSystem: "android",
|
||||
},
|
||||
func (w *WingsService) getDeviceInfoFromContext(_ context.Context, screenshot string) WingsDeviceInfo {
|
||||
// use default device info
|
||||
return WingsDeviceInfo{
|
||||
DeviceID: "default-device",
|
||||
NowImage: screenshot,
|
||||
PreImage: screenshot,
|
||||
NowLayoutJSON: "",
|
||||
OperationSystem: "android",
|
||||
}
|
||||
}
|
||||
|
||||
// getDeviceInfoFromScreenshot gets device info from screenshot (for Assert)
|
||||
func (w *WingsService) getDeviceInfoFromScreenshot(ctx context.Context, screenshot string) []WingsDeviceInfo {
|
||||
func (w *WingsService) getDeviceInfoFromScreenshot(ctx context.Context, screenshot string) WingsDeviceInfo {
|
||||
return w.getDeviceInfoFromContext(ctx, screenshot)
|
||||
}
|
||||
|
||||
@@ -463,8 +390,6 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
||||
// Set headers
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header.Add("x-use-ppe", "1")
|
||||
httpReq.Header.Add("x-tt-env", "ppe_refactor_merge")
|
||||
|
||||
// Add authentication headers if using external API
|
||||
if w.accessKey != "" && w.secretKey != "" {
|
||||
@@ -478,7 +403,7 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
||||
|
||||
// Execute HTTP request
|
||||
client := &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
@@ -486,9 +411,7 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
||||
return nil, errors.Wrap(err, "HTTP request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// resp X-Tt-Logid
|
||||
logID := resp.Header.Get("X-Tt-Logid")
|
||||
log.Info().Str("step_text", request.StepText).Str("log_id", logID).Str("biz_id", request.BizId).Str("url", w.apiURL).Msg("call wings api")
|
||||
|
||||
// Read response body
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -511,7 +434,7 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
||||
|
||||
// convertWingsResponseToToolCalls converts Wings API response to tool calls using generic approach
|
||||
func (w *WingsService) convertWingsResponseToToolCalls(actionParamsStr string) ([]schema.ToolCall, error) {
|
||||
if actionParamsStr == "" || actionParamsStr == "FINISH" {
|
||||
if actionParamsStr == "" {
|
||||
return []schema.ToolCall{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -240,12 +240,12 @@ func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) er
|
||||
return err
|
||||
}
|
||||
// 等待安装完成或超时
|
||||
timeout := 8 * time.Minute
|
||||
timeout := 3 * time.Minute
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf("install via installer timed out after %v", timeout)
|
||||
return fmt.Errorf("installation timed out after %v", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -706,17 +706,16 @@ func (ad *ADBDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
log.Error().Err(err).Msg("failed to close adb log writer")
|
||||
}
|
||||
pointRes := ConvertPoints(ad.Device.Logcat.logs)
|
||||
|
||||
// 没有解析到打点日志,走兜底逻辑
|
||||
if len(pointRes) == 0 {
|
||||
log.Info().Msg("action log is null, use action file >>>")
|
||||
actionLogDirPath := config.GetConfig().ActionLogDirPath()
|
||||
logFilePathPrefix := fmt.Sprintf("%v/data", actionLogDirPath)
|
||||
files := []string{}
|
||||
ad.Device.RunShellCommand("pull", config.DeviceActionLogFilePath, actionLogDirPath)
|
||||
actionLogRegStr := `.*data_\d+\.txt`
|
||||
ad.Device.PullFolder(config.DeviceActionLogFilePath, actionLogDirPath)
|
||||
err = filepath.Walk(actionLogDirPath, func(path string, info fs.FileInfo, err error) error {
|
||||
// 只是需要日志文件
|
||||
if ok := strings.Contains(path, logFilePathPrefix); ok {
|
||||
if ok, _ := regexp.MatchString(actionLogRegStr, path); ok {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -21,6 +21,11 @@ func setupADBDriverExt(t *testing.T) *XTDriver {
|
||||
Serial: "", // Let it auto-detect the device serial
|
||||
AIOptions: []option.AIServiceOption{
|
||||
option.WithCVService(option.CVServiceTypeVEDEM),
|
||||
option.WithLLMConfig(
|
||||
option.NewLLMServiceConfig(option.DOUBAO_1_5_UI_TARS_250328).
|
||||
WithPlannerModel(option.WINGS_SERVICE).
|
||||
WithAsserterModel(option.WINGS_SERVICE),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ var (
|
||||
|
||||
// Ensure drivers implement SIMSupport interface
|
||||
_ SIMSupport = (*UIA2Driver)(nil)
|
||||
_ SIMSupport = (*WDADriver)(nil)
|
||||
)
|
||||
|
||||
// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver
|
||||
|
||||
@@ -292,14 +292,31 @@ func TestDriverExt_AIAction(t *testing.T) {
|
||||
func TestDriverExt_AIAction_CompareWithAIAction(t *testing.T) {
|
||||
driver := setupDriverExt(t)
|
||||
|
||||
prompt := "[目标导向]向上滑动屏幕2次"
|
||||
prompt := "点击搜索按钮"
|
||||
|
||||
// Test both methods with the same prompt
|
||||
aiResult, aiErr := driver.StartToGoal(context.Background(), prompt)
|
||||
aiResult, aiErr := driver.AIAction(context.Background(), prompt)
|
||||
|
||||
// Both should execute without critical errors (may have different implementations)
|
||||
t.Logf("AIAction error: %v", aiErr)
|
||||
t.Logf("AIAction result: %v", aiResult)
|
||||
|
||||
// If both succeed, compare results
|
||||
if aiResult != nil {
|
||||
assert.Equal(t, "action", aiResult.Type, "AIAction result type should be 'action'")
|
||||
|
||||
// Both should have timing information
|
||||
assert.Greater(t, aiResult.ModelCallElapsed, int64(0), "AIAction should have model call elapsed time")
|
||||
|
||||
// Both should have screenshot information
|
||||
assert.NotEmpty(t, aiResult.ImagePath, "AIAction should have image path")
|
||||
|
||||
// Compare model names
|
||||
if aiResult.PlanningResult != nil {
|
||||
t.Logf("AIAction model: %s", aiResult.PlanningResult.ModelName)
|
||||
|
||||
assert.Equal(t, "wings-api", aiResult.PlanningResult.ModelName, "AIAction should use wings-api")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDriverExt_AIAction_ErrorHandling tests AIAction error handling
|
||||
|
||||
@@ -284,8 +284,9 @@ func getSimulationDuration(params []float64) (milliseconds int64) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sleepStrict sleeps strict duration with given params
|
||||
// startTime is used to correct sleep duration caused by process time
|
||||
// sleepStrict sleeps for strict duration with optional start time correction
|
||||
// If startTime is zero, acts as normal context-aware sleep
|
||||
// If startTime is provided, corrects sleep duration by subtracting elapsed time
|
||||
// ctx allows for cancellation during sleep
|
||||
func sleepStrict(ctx context.Context, startTime time.Time, strictMilliseconds int64) {
|
||||
var elapsed int64
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/internal/simulation"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
@@ -678,6 +679,13 @@ func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.Act
|
||||
x, y = toX, toY
|
||||
}
|
||||
|
||||
if x, err = wd.toScale(x); err != nil {
|
||||
return err
|
||||
}
|
||||
if y, err = wd.toScale(y); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var actionMap map[string]interface{}
|
||||
|
||||
switch event.Action {
|
||||
@@ -743,6 +751,201 @@ func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.Act
|
||||
return err
|
||||
}
|
||||
|
||||
// SIMSwipeWithDirection 向指定方向滑动任意距离
|
||||
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||
// fromX, fromY: 起始坐标
|
||||
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
|
||||
func (wd *WDADriver) SIMSwipeWithDirection(direction string, fromX, fromY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
|
||||
absStartX, absStartY, err := convertToAbsolutePoint(wd, fromX, fromY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Str("direction", direction).
|
||||
Float64("startX", absStartX).Float64("startY", absStartY).
|
||||
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeWithDirection")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 转换方向字符串为Direction类型
|
||||
var slideDirection simulation.Direction
|
||||
switch direction {
|
||||
case "up":
|
||||
slideDirection = simulation.Up
|
||||
case "down":
|
||||
slideDirection = simulation.Down
|
||||
case "left":
|
||||
slideDirection = simulation.Left
|
||||
case "right":
|
||||
slideDirection = simulation.Right
|
||||
default:
|
||||
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||
}
|
||||
|
||||
// 使用滑动仿真算法生成触摸事件序列
|
||||
events, err := simulator.GenerateSlideWithRandomDistance(
|
||||
absStartX, absStartY, slideDirection, simMinDistance, simMaxDistance,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate slide events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMSwipeInArea 在指定区域内向指定方向滑动任意距离
|
||||
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||
// simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY: 区域范围(相对坐标)
|
||||
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
|
||||
func (wd *WDADriver) SIMSwipeInArea(direction string, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
|
||||
// 转换区域坐标为绝对坐标
|
||||
absAreaStartX, absAreaStartY, err := convertToAbsolutePoint(wd, simAreaStartX, simAreaStartY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absAreaEndX, absAreaEndY, err := convertToAbsolutePoint(wd, simAreaEndX, simAreaEndY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 确保区域坐标正确(start应该小于等于end)
|
||||
if absAreaStartX > absAreaEndX {
|
||||
absAreaStartX, absAreaEndX = absAreaEndX, absAreaStartX
|
||||
}
|
||||
if absAreaStartY > absAreaEndY {
|
||||
absAreaStartY, absAreaEndY = absAreaEndY, absAreaStartY
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Str("direction", direction).
|
||||
Float64("areaStartX", absAreaStartX).Float64("areaStartY", absAreaStartY).
|
||||
Float64("areaEndX", absAreaEndX).Float64("areaEndY", absAreaEndY).
|
||||
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeInArea")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 转换方向字符串为Direction类型
|
||||
var slideDirection simulation.Direction
|
||||
switch direction {
|
||||
case "up":
|
||||
slideDirection = simulation.Up
|
||||
case "down":
|
||||
slideDirection = simulation.Down
|
||||
case "left":
|
||||
slideDirection = simulation.Left
|
||||
case "right":
|
||||
slideDirection = simulation.Right
|
||||
default:
|
||||
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||
}
|
||||
|
||||
// 使用滑动仿真算法生成区域内滑动的触摸事件序列
|
||||
events, err := simulator.GenerateSlideInArea(
|
||||
absAreaStartX, absAreaStartY, absAreaEndX, absAreaEndY,
|
||||
slideDirection, simMinDistance, simMaxDistance,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate slide in area events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMSwipeFromPointToPoint 指定起始点和结束点进行滑动
|
||||
// fromX, fromY: 起始坐标(相对坐标)
|
||||
// toX, toY: 结束坐标(相对坐标)
|
||||
func (wd *WDADriver) SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
|
||||
// 转换起始点和结束点为绝对坐标
|
||||
absStartX, absStartY, err := convertToAbsolutePoint(wd, fromX, fromY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absEndX, absEndY, err := convertToAbsolutePoint(wd, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Float64("startX", absStartX).Float64("startY", absStartY).
|
||||
Float64("endX", absEndX).Float64("endY", absEndY).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeFromPointToPoint")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 使用滑动仿真算法生成点对点滑动的触摸事件序列
|
||||
events, err := simulator.GeneratePointToPointSlideEvents(
|
||||
absStartX, absStartY, absEndX, absEndY,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate point to point slide events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMClickAtPoint 点击相对坐标
|
||||
// x, y: 点击坐标(相对坐标)
|
||||
func (wd *WDADriver) SIMClickAtPoint(x, y float64, opts ...option.ActionOption) error {
|
||||
// 转换为绝对坐标
|
||||
absX, absY, err := convertToAbsolutePoint(wd, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Float64("x", absX).Float64("y", absY).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMClickAtPoint")
|
||||
|
||||
// 导入点击仿真库
|
||||
clickSimulator := simulation.NewClickSimulatorAPI(nil)
|
||||
|
||||
// 使用点击仿真算法生成触摸事件序列
|
||||
events, err := clickSimulator.GenerateClickEvents(
|
||||
absX, absY, deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate click events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)]
|
||||
data := map[string]interface{}{
|
||||
@@ -784,6 +987,69 @@ func (wd *WDADriver) Input(text string, opts ...option.ActionOption) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
// SIMInput 仿真输入函数,模拟人类分批输入行为
|
||||
// 将文本智能分割,英文单词和数字保持完整,中文按1-2个字符分割
|
||||
func (wd *WDADriver) SIMInput(text string, opts ...option.ActionOption) error {
|
||||
log.Info().Str("text", text).Msg("WDADriver.SIMInput")
|
||||
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建输入仿真器(使用默认配置)
|
||||
inputSimulator := simulation.NewInputSimulatorAPI(nil)
|
||||
|
||||
// 生成输入片段(使用智能分割算法,所有参数使用默认值)
|
||||
inputReq := simulation.InputRequest{
|
||||
Text: text,
|
||||
// MinSegmentLen, MaxSegmentLen, MinDelayMs, MaxDelayMs 使用默认值
|
||||
}
|
||||
|
||||
response := inputSimulator.GenerateInputSegments(inputReq)
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to generate input segments: %s", response.Message)
|
||||
}
|
||||
|
||||
log.Info().Int("segments", response.Metrics.TotalSegments).
|
||||
Int("totalDelayMs", response.Metrics.TotalDelayMs).
|
||||
Int("estimatedTimeMs", response.Metrics.EstimatedTimeMs).
|
||||
Msg("Input segments generated")
|
||||
|
||||
// 逐个输入每个片段
|
||||
var segmentErrCnt int
|
||||
for _, segment := range response.Segments {
|
||||
// 使用Input进行输入(内部已包含Session.POST请求)
|
||||
segmentErr := wd.Input(segment.Text, opts...)
|
||||
if segmentErr != nil {
|
||||
segmentErrCnt++
|
||||
log.Info().Err(segmentErr).Int("segmentErrCnt", segmentErrCnt).
|
||||
Msg("segments err")
|
||||
}
|
||||
|
||||
log.Debug().Str("segment", segment.Text).Int("index", segment.Index).
|
||||
Int("charLen", segment.CharLen).Msg("Successfully input segment")
|
||||
|
||||
// 如果有延迟时间,则等待
|
||||
if segment.DelayMs > 0 {
|
||||
time.Sleep(time.Duration(segment.DelayMs) * time.Millisecond)
|
||||
|
||||
log.Debug().Int("delayMs", segment.DelayMs).
|
||||
Msg("Delay between input segments")
|
||||
}
|
||||
}
|
||||
if segmentErrCnt > 0 {
|
||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||
option.MergeOptions(data, opts...)
|
||||
_, err := wd.Session.POST(data, "/wings/interaction/keys")
|
||||
return err
|
||||
}
|
||||
log.Info().Int("totalSegments", response.Metrics.TotalSegments).
|
||||
Int("actualDelayMs", response.Metrics.TotalDelayMs).
|
||||
Msg("SIMInput completed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err error) {
|
||||
log.Info().Int("count", count).Msg("WDADriver.Backspace")
|
||||
if count == 0 {
|
||||
|
||||
@@ -15,7 +15,29 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolSleep implements the sleep tool call.
|
||||
// extractStartTimeMs extracts start_time_ms from MCP request arguments
|
||||
// Returns time.Time (zero if not provided) and any conversion error
|
||||
func extractStartTimeMs(request mcp.CallToolRequest) (time.Time, error) {
|
||||
startTimeMs, ok := request.GetArguments()["start_time_ms"]
|
||||
if !ok || startTimeMs == nil {
|
||||
return time.Time{}, nil // Return zero time for normal sleep
|
||||
}
|
||||
|
||||
var ms int64
|
||||
switch v := startTimeMs.(type) {
|
||||
case float64:
|
||||
ms = int64(v)
|
||||
case int64:
|
||||
ms = v
|
||||
case int:
|
||||
ms = int64(v)
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("invalid start_time_ms type: %T", v)
|
||||
}
|
||||
|
||||
return time.UnixMilli(ms), nil
|
||||
}
|
||||
|
||||
type ToolSleep struct {
|
||||
// Return data fields - these define the structure of data returned by this tool
|
||||
Seconds float64 `json:"seconds" desc:"Duration in seconds that was slept"`
|
||||
@@ -33,6 +55,7 @@ func (t *ToolSleep) Description() string {
|
||||
func (t *ToolSleep) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("seconds", mcp.Description("Number of seconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,16 +93,15 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
milliseconds := int64(actualSeconds * 1000)
|
||||
sleepStrict(ctx, startTime, milliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %v seconds", actualSeconds)
|
||||
returnData := ToolSleep{
|
||||
Seconds: actualSeconds,
|
||||
@@ -91,9 +113,24 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleep) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{
|
||||
"seconds": action.Params,
|
||||
arguments := map[string]any{}
|
||||
|
||||
var seconds float64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
seconds, _ = param.Float64()
|
||||
arguments["seconds"] = seconds
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
seconds = float64(param)
|
||||
arguments["seconds"] = seconds
|
||||
} else if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both seconds and startTime
|
||||
seconds = sleepConfig.Seconds
|
||||
arguments["seconds"] = seconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep params: %v", action.Params)
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
@@ -115,6 +152,7 @@ func (t *ToolSleepMS) Description() string {
|
||||
func (t *ToolSleepMS) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("milliseconds", mcp.Description("Number of milliseconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,16 +190,14 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sleepStrict(ctx, startTime, actualMilliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %d milliseconds", actualMilliseconds)
|
||||
returnData := ToolSleepMS{
|
||||
Milliseconds: actualMilliseconds,
|
||||
@@ -173,17 +209,24 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleepMS) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{}
|
||||
|
||||
var milliseconds int64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ = param.Int64()
|
||||
arguments["milliseconds"] = milliseconds
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
milliseconds = param
|
||||
arguments["milliseconds"] = milliseconds
|
||||
} else if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both milliseconds and startTime
|
||||
milliseconds = sleepConfig.Milliseconds
|
||||
arguments["milliseconds"] = milliseconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
|
||||
}
|
||||
arguments := map[string]any{
|
||||
"milliseconds": milliseconds,
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
|
||||
240
uixt/mcp_tools_utility_test.go
Normal file
240
uixt/mcp_tools_utility_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestToolSleep_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleep{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: json.Number("3.5"),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(3.5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: int64(5),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Seconds: 2.5,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"seconds": 2.5,
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolSleepMS_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleepMS{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: json.Number("1500"),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(1500)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: int64(2000),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(2000)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Milliseconds: 3000,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"milliseconds": int64(3000),
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSleepStrictTiming(t *testing.T) {
|
||||
// Test that strict sleep properly adjusts for elapsed time
|
||||
startTime := time.Now()
|
||||
|
||||
// Simulate some processing time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test sleepStrict with the start time
|
||||
testStart := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // 200ms total duration
|
||||
actualElapsed := time.Since(testStart)
|
||||
|
||||
// Should sleep approximately 150ms (200ms - 50ms already elapsed)
|
||||
// Allow some tolerance for timing variations
|
||||
expectedSleep := 150 * time.Millisecond
|
||||
assert.Greater(t, actualElapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, actualElapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepCancellation(t *testing.T) {
|
||||
// Test that sleep respects context cancellation
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Cancel after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 500) // Try to sleep 500ms
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled after ~50ms, not sleep full 500ms
|
||||
assert.Less(t, elapsed, 200*time.Millisecond, "Sleep was not properly cancelled")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithZeroTime(t *testing.T) {
|
||||
// Test sleepStrict behaves like normal sleep when startTime is zero
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 100) // 100ms, no start time
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should sleep full duration
|
||||
expectedSleep := 100 * time.Millisecond
|
||||
assert.Greater(t, elapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, elapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithPastStartTime(t *testing.T) {
|
||||
// Test sleepStrict skips sleep when elapsed time exceeds duration
|
||||
startTime := time.Now().Add(-300 * time.Millisecond) // 300ms ago
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // Want 200ms total, but 300ms already elapsed
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should skip sleep entirely
|
||||
assert.Less(t, elapsed, 50*time.Millisecond, "Should have skipped sleep")
|
||||
}
|
||||
|
||||
func TestJsonNumberHandling(t *testing.T) {
|
||||
// Test that json.Number is correctly handled in different scenarios
|
||||
|
||||
// Test float json.Number
|
||||
floatNumber := json.Number("3.14")
|
||||
floatVal, err := floatNumber.Float64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3.14, floatVal)
|
||||
|
||||
// Test int json.Number
|
||||
intNumber := json.Number("1500")
|
||||
intVal, err := intNumber.Int64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1500), intVal)
|
||||
|
||||
// Test invalid json.Number
|
||||
invalidNumber := json.Number("invalid")
|
||||
_, err = invalidNumber.Float64()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -66,22 +66,21 @@ func ParseTouchEvents(data string) ([]types.TouchEvent, error) {
|
||||
if event.Action, err = strconv.Atoi(parts[12]); err != nil {
|
||||
return nil, fmt.Errorf("invalid action: %v", err)
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func TestAndroidTouchByEvents(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
func TestIOSTouchByEvents(t *testing.T) {
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -138,60 +137,6 @@ func TestAndroidTouchByEvents(t *testing.T) {
|
||||
t.Logf("Successfully executed touch events: %d events processed", len(events))
|
||||
}
|
||||
|
||||
func TestIOSTouchByEvents(t *testing.T) {
|
||||
driver := setupWDADriverExt(t)
|
||||
|
||||
// Example touch event data as provided
|
||||
touchEventData := `1752649131556,401.20703,1191.3164,2,1.0,0.03529412,457.20703,1359.3164,111586196,111586196,1,0,0
|
||||
1752649131595,402.913,1185.0792,2,1.0,0.039215688,458.913,1353.0792,111586196,111586236,1,0,2
|
||||
1752649131612,410.60825,1164.3806,2,1.0,0.03529412,466.60825,1332.3806,111586196,111586250,1,0,2
|
||||
1752649131629,437.7335,1093.1417,2,1.0,0.039215688,493.7335,1261.1417,111586196,111586270,1,0,2
|
||||
1752649131646,463.5786,1018.01746,2,1.0,0.039215688,519.5786,1186.0175,111586196,111586287,1,0,2
|
||||
1752649131662,487.56482,948.9773,2,1.0,0.03529412,543.5648,1116.9773,111586196,111586304,1,0,2
|
||||
1752649131679,511.81476,881.6183,2,1.0,0.039215688,567.81476,1049.6183,111586196,111586320,1,0,2
|
||||
1752649131696,543.4369,811.4982,2,1.0,0.03529412,599.4369,979.4982,111586196,111586337,1,0,2
|
||||
1752649131713,577.1632,747.4512,2,1.0,0.039215688,633.1632,915.4512,111586196,111586354,1,0,2
|
||||
1752649131729,610.1538,691.72034,2,1.0,0.03529412,666.1538,859.72034,111586196,111586370,1,0,2
|
||||
1752649131746,639.1683,642.6914,2,1.0,0.03529412,695.1683,810.6914,111586196,111586387,1,0,2
|
||||
1752649131763,658.9832,605.90857,2,1.0,0.03529412,714.9832,773.90857,111586196,111586404,1,0,2
|
||||
1752649131779,672.21954,581.1634,2,1.0,0.03529412,728.21954,749.1634,111586196,111586420,1,0,2
|
||||
1752649131796,680.7687,566.1778,2,1.0,0.03529412,736.7687,734.1778,111586196,111586434,1,0,2
|
||||
1752649131814,688.0894,554.2295,2,1.0,0.03529412,744.0894,722.2295,111586196,111586450,1,0,2
|
||||
1752649131830,694.542,544.7783,2,1.0,0.03529412,750.542,712.7783,111586196,111586466,1,0,2
|
||||
1752649131847,700.60645,537.2637,2,1.0,0.039215688,756.60645,705.2637,111586196,111586483,1,0,2
|
||||
1752649131863,705.08887,531.1406,2,1.0,0.039215688,761.08887,699.1406,111586196,111586500,1,0,2
|
||||
1752649131880,708.1211,527.8008,2,1.0,0.039215688,764.1211,695.8008,111586196,111586517,1,0,2
|
||||
1752649131897,709.43945,524.46094,2,1.0,0.039215688,765.43945,692.46094,111586196,111586533,1,0,2
|
||||
1752649131902,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586537,1,33554432,2
|
||||
1752649131907,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586546,1,0,1`
|
||||
|
||||
// Parse touch events
|
||||
events, err := ParseTouchEvents(touchEventData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTouchEvents failed: %v", err)
|
||||
}
|
||||
|
||||
// Check first event
|
||||
firstEvent := events[0]
|
||||
if firstEvent.Action != 0 { // ACTION_DOWN
|
||||
t.Errorf("Expected first event action to be 0 (ACTION_DOWN), got %d", firstEvent.Action)
|
||||
}
|
||||
|
||||
// Check last event
|
||||
lastEvent := events[len(events)-1]
|
||||
if lastEvent.Action != 1 { // ACTION_UP
|
||||
t.Errorf("Expected last event action to be 1 (ACTION_UP), got %d", lastEvent.Action)
|
||||
}
|
||||
|
||||
// Use TouchByEvents with parsed events
|
||||
err = driver.IDriver.(*WDADriver).TouchByEvents(events)
|
||||
if err != nil {
|
||||
t.Fatalf("TouchByEvents failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Successfully executed touch events: %d events processed", len(events))
|
||||
}
|
||||
|
||||
func TestTouchEventParsing(t *testing.T) {
|
||||
// Test single touch event parsing
|
||||
singleEventData := "1752646457403,456.78418,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0"
|
||||
@@ -281,14 +226,14 @@ func TestTouchEventSequenceValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeWithDirection(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -308,7 +253,7 @@ func TestSwipeWithDirection(t *testing.T) {
|
||||
direction: "up",
|
||||
startX: 0.5,
|
||||
startY: 0.5,
|
||||
minDistance: 100.0,
|
||||
minDistance: 500.0,
|
||||
maxDistance: 500.0,
|
||||
},
|
||||
}
|
||||
@@ -332,50 +277,15 @@ func TestSwipeWithDirection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwipeWithDirectionInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer driver.TearDown()
|
||||
|
||||
// Test invalid direction
|
||||
err = driver.SIMSwipeWithDirection("invalid", 500.0, 500.0, 100.0, 200.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid direction, but got none")
|
||||
}
|
||||
|
||||
// Test invalid distance range (max < min)
|
||||
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 200.0, 100.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid distance range, but got none")
|
||||
}
|
||||
|
||||
// Test zero distance
|
||||
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 0.0, 0.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for zero distance, but got none")
|
||||
}
|
||||
|
||||
t.Log("Invalid input validation tests passed")
|
||||
}
|
||||
|
||||
func TestSwipeInArea(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -428,14 +338,14 @@ func TestSwipeInArea(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeFromPointToPoint(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -477,14 +387,14 @@ func TestSwipeFromPointToPoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeFromPointToPointInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -506,14 +416,14 @@ func TestSwipeFromPointToPointInvalidInputs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClickAtPoint(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -546,14 +456,14 @@ func TestClickAtPoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClickAtPointInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -580,14 +490,14 @@ func TestClickAtPointInvalidInputs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSIMInput(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user