mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-25 17:44:02 +08:00
feat: 支持基于touchEvent执行
This commit is contained in:
240
examples/uitest/android_touch_simulator_test.go
Normal file
240
examples/uitest/android_touch_simulator_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
|
||||
// ParseTouchEvents parses touch event data from comma-separated string format
|
||||
func ParseTouchEvents(data string) ([]types.TouchEvent, error) {
|
||||
lines := strings.Split(strings.TrimSpace(data), "\n")
|
||||
events := make([]types.TouchEvent, 0, len(lines))
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) != 13 {
|
||||
return nil, fmt.Errorf("invalid touch event data format: expected 13 fields, got %d", len(parts))
|
||||
}
|
||||
|
||||
event := types.TouchEvent{}
|
||||
var err error
|
||||
|
||||
// Parse each field
|
||||
if event.Timestamp, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid timestamp: %v", err)
|
||||
}
|
||||
if event.X, err = strconv.ParseFloat(parts[1], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid x coordinate: %v", err)
|
||||
}
|
||||
if event.Y, err = strconv.ParseFloat(parts[2], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid y coordinate: %v", err)
|
||||
}
|
||||
if event.DeviceID, err = strconv.Atoi(parts[3]); err != nil {
|
||||
return nil, fmt.Errorf("invalid device id: %v", err)
|
||||
}
|
||||
if event.Pressure, err = strconv.ParseFloat(parts[4], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid pressure: %v", err)
|
||||
}
|
||||
if event.Size, err = strconv.ParseFloat(parts[5], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid size: %v", err)
|
||||
}
|
||||
if event.RawX, err = strconv.ParseFloat(parts[6], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid raw x: %v", err)
|
||||
}
|
||||
if event.RawY, err = strconv.ParseFloat(parts[7], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid raw y: %v", err)
|
||||
}
|
||||
if event.DownTime, err = strconv.ParseInt(parts[8], 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid down time: %v", err)
|
||||
}
|
||||
if event.EventTime, err = strconv.ParseInt(parts[9], 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid event time: %v", err)
|
||||
}
|
||||
if event.ToolType, err = strconv.Atoi(parts[10]); err != nil {
|
||||
return nil, fmt.Errorf("invalid tool type: %v", err)
|
||||
}
|
||||
if event.Flag, err = strconv.Atoi(parts[11]); err != nil {
|
||||
return nil, fmt.Errorf("invalid flag: %v", err)
|
||||
}
|
||||
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 := uixt.NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := uixt.NewUIA2Driver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer driver.TearDown()
|
||||
|
||||
// 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.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"
|
||||
|
||||
events, err := ParseTouchEvents(singleEventData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTouchEvents failed: %v", err)
|
||||
}
|
||||
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("Expected 1 event, got %d", len(events))
|
||||
}
|
||||
|
||||
event := events[0]
|
||||
if event.Timestamp != 1752646457403 {
|
||||
t.Errorf("Expected timestamp 1752646457403, got %d", event.Timestamp)
|
||||
}
|
||||
if event.X != 456.78418 {
|
||||
t.Errorf("Expected X 456.78418, got %f", event.X)
|
||||
}
|
||||
if event.Y != 1574.0195 {
|
||||
t.Errorf("Expected Y 1574.0195, got %f", event.Y)
|
||||
}
|
||||
if event.Action != 0 {
|
||||
t.Errorf("Expected Action 0, got %d", event.Action)
|
||||
}
|
||||
if event.Pressure != 1.0 {
|
||||
t.Errorf("Expected Pressure 1.0, got %f", event.Pressure)
|
||||
}
|
||||
if event.Size != 0.016666668 {
|
||||
t.Errorf("Expected Size 0.016666668, got %f", event.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTouchEventParsingInvalidData(t *testing.T) {
|
||||
// Test with invalid data
|
||||
testCases := []struct {
|
||||
name string
|
||||
data string
|
||||
}{
|
||||
{
|
||||
name: "too few fields",
|
||||
data: "1752646457403,456.78418,1574.0195,7,1.0",
|
||||
},
|
||||
{
|
||||
name: "invalid timestamp",
|
||||
data: "invalid,456.78418,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0",
|
||||
},
|
||||
{
|
||||
name: "invalid x coordinate",
|
||||
data: "1752646457403,invalid,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := ParseTouchEvents(tc.data)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for invalid data, but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTouchEventSequenceValidation(t *testing.T) {
|
||||
// Test a complete touch sequence: DOWN -> MOVE -> MOVE -> UP
|
||||
sequenceData := `1752646457403,100.0,100.0,7,1.0,0.016666668,100.0,100.0,924451292,924451292,1,0,0
|
||||
1752646457420,120.0,120.0,7,1.0,0.022058824,120.0,120.0,924451292,924451335,1,0,2
|
||||
1752646457440,140.0,140.0,7,1.0,0.022058824,140.0,140.0,924451292,924451351,1,0,2
|
||||
1752646457460,160.0,160.0,7,1.0,0.012254903,160.0,160.0,924451292,924451619,1,0,1`
|
||||
|
||||
events, err := ParseTouchEvents(sequenceData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTouchEvents failed: %v", err)
|
||||
}
|
||||
|
||||
if len(events) != 4 {
|
||||
t.Fatalf("Expected 4 events, got %d", len(events))
|
||||
}
|
||||
|
||||
// Validate sequence: DOWN -> MOVE -> MOVE -> UP
|
||||
expectedActions := []int{0, 2, 2, 1} // ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_UP
|
||||
for i, event := range events {
|
||||
if event.Action != expectedActions[i] {
|
||||
t.Errorf("Event %d: expected action %d, got %d", i, expectedActions[i], event.Action)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate timestamps are in increasing order
|
||||
for i := 1; i < len(events); i++ {
|
||||
if events[i].Timestamp <= events[i-1].Timestamp {
|
||||
t.Errorf("Event %d: timestamp should be greater than previous event", i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -24,6 +24,7 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-openapi/spec v0.20.7
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/httprunner/funplugin v0.5.5
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
@@ -82,7 +83,6 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
|
||||
@@ -428,6 +428,112 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio
|
||||
return err
|
||||
}
|
||||
|
||||
// TouchByEvents performs a complex swipe using a sequence of touch events with pressure and size data
|
||||
func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.ActionOption) error {
|
||||
log.Info().Int("eventCount", len(events)).Msg("UIA2Driver.SwipeSimulator")
|
||||
|
||||
if len(events) == 0 {
|
||||
return fmt.Errorf("no touch events provided")
|
||||
}
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
|
||||
// Apply pre-handlers for the first and last events (start and end coordinates)
|
||||
firstEvent := events[0]
|
||||
lastEvent := events[len(events)-1]
|
||||
fromX, fromY, toX, toY, err := preHandler_Swipe(ud, option.ACTION_SwipeCoordinate, actionOptions,
|
||||
firstEvent.X, firstEvent.Y, lastEvent.X, lastEvent.Y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ud, option.ACTION_SwipeCoordinate, actionOptions)
|
||||
|
||||
var actions []interface{}
|
||||
var prevTimestamp int64
|
||||
|
||||
for i, event := range events {
|
||||
var duration float64
|
||||
if i > 0 {
|
||||
// Calculate duration from previous event in milliseconds
|
||||
duration = float64(event.Timestamp - prevTimestamp)
|
||||
}
|
||||
prevTimestamp = event.Timestamp
|
||||
|
||||
// Apply coordinate transformation if it's the first or last event
|
||||
x, y := event.X, event.Y
|
||||
if i == 0 {
|
||||
x, y = fromX, fromY
|
||||
} else if i == len(events)-1 {
|
||||
x, y = toX, toY
|
||||
}
|
||||
|
||||
var actionMap map[string]interface{}
|
||||
|
||||
switch event.Action {
|
||||
case 0: // ACTION_DOWN
|
||||
actionMap = map[string]interface{}{
|
||||
"type": "pointerDown",
|
||||
"duration": 0,
|
||||
"button": 0,
|
||||
"pressure": event.Pressure,
|
||||
"size": event.Size,
|
||||
}
|
||||
// Add initial move to position before down
|
||||
if i == 0 {
|
||||
moveAction := map[string]interface{}{
|
||||
"type": "pointerMove",
|
||||
"duration": 0,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"origin": "viewport",
|
||||
"pressure": event.Pressure,
|
||||
"size": event.Size,
|
||||
}
|
||||
actions = append(actions, moveAction)
|
||||
}
|
||||
case 1: // ACTION_UP
|
||||
actionMap = map[string]interface{}{
|
||||
"type": "pointerUp",
|
||||
"duration": 0,
|
||||
"button": 0,
|
||||
"pressure": event.Pressure,
|
||||
"size": event.Size,
|
||||
}
|
||||
case 2: // ACTION_MOVE
|
||||
actionMap = map[string]interface{}{
|
||||
"type": "pointerMove",
|
||||
"duration": duration,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"origin": "viewport",
|
||||
"pressure": event.Pressure,
|
||||
"size": event.Size,
|
||||
}
|
||||
default:
|
||||
log.Warn().Int("action", event.Action).Msg("Unknown action type, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
actions = append(actions, actionMap)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "pointer",
|
||||
"parameters": map[string]string{"pointerType": "touch"},
|
||||
"id": "touch",
|
||||
"actions": actions,
|
||||
},
|
||||
},
|
||||
}
|
||||
option.MergeOptions(data, opts...)
|
||||
|
||||
urlStr := fmt.Sprintf("/session/%s/actions/swipe", ud.Session.ID)
|
||||
_, err = ud.Session.POST(data, urlStr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
|
||||
log.Info().Str("contentType", string(contentType)).
|
||||
Str("content", content).Msg("UIA2Driver.SetPasteboard")
|
||||
|
||||
@@ -225,3 +225,20 @@ const (
|
||||
DirectionLeft Direction = "left"
|
||||
DirectionRight Direction = "right"
|
||||
)
|
||||
|
||||
// TouchEvent represents a single touch event with all its properties
|
||||
type TouchEvent struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
DeviceID int `json:"deviceId"`
|
||||
Pressure float64 `json:"pressure"`
|
||||
Size float64 `json:"size"`
|
||||
RawX float64 `json:"rawX"`
|
||||
RawY float64 `json:"rawY"`
|
||||
DownTime int64 `json:"downTime"`
|
||||
EventTime int64 `json:"eventTime"`
|
||||
ToolType int `json:"toolType"`
|
||||
Flag int `json:"flag"`
|
||||
Action int `json:"action"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user