From 023b0a3c7f49e78dc20eb7a3d9a5378ca0c4a227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Thu, 31 Jul 2025 22:15:10 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81iOS=20=E6=89=8B?= =?UTF-8?q?=E5=8A=BF=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uixt/ios_driver_wda.go | 123 +++++++ uixt/touch_simulator_test.go | 618 +++++++++++++++++++++++++++++++++++ 2 files changed, 741 insertions(+) create mode 100644 uixt/touch_simulator_test.go diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 9042c736..283c5a92 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -620,6 +620,129 @@ func (wd *WDADriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Action return wd.Drag(fromX, fromY, toX, toY, opts...) } +// TouchByEvents performs a complex swipe using a sequence of touch events with pressure and size data +func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.ActionOption) error { + log.Info().Int("eventCount", len(events)).Msg("WDADriver.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] + + // Use rawX/rawY if available, otherwise fallback to X/Y for first event + startX, startY := firstEvent.RawX, firstEvent.RawY + if startX == 0 && startY == 0 { + startX, startY = firstEvent.X, firstEvent.Y + } + + // Use rawX/rawY if available, otherwise fallback to X/Y for last event + endX, endY := lastEvent.RawX, lastEvent.RawY + if endX == 0 && endY == 0 { + endX, endY = lastEvent.X, lastEvent.Y + } + + fromX, fromY, toX, toY, err := preHandler_Swipe(wd, option.ACTION_SwipeCoordinate, actionOptions, + startX, startY, endX, endY) + if err != nil { + return err + } + defer postHandler(wd, option.ACTION_SwipeCoordinate, actionOptions) + + var actions []interface{} + var prevEventTime int64 + + for i, event := range events { + var duration float64 + if i > 0 { + // Calculate duration from previous event using EventTime (milliseconds) + duration = float64(event.EventTime - prevEventTime) + } + prevEventTime = event.EventTime + + // Use rawX/rawY if available, otherwise fallback to X/Y + x, y := event.RawX, event.RawY + if x == 0 && y == 0 { + // Fallback to X/Y if rawX/rawY are not set + x, y = event.X, event.Y + } + + // Apply coordinate transformation if it's the first or last event + 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...) + + _, err = wd.Session.POST(data, "/wings/actions") + return err +} + func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content string) (err error) { // [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)] data := map[string]interface{}{ diff --git a/uixt/touch_simulator_test.go b/uixt/touch_simulator_test.go new file mode 100644 index 00000000..4cdcb453 --- /dev/null +++ b/uixt/touch_simulator_test.go @@ -0,0 +1,618 @@ +//go:build localtest + +package uixt + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "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 + + 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 := NewAndroidDevice( + option.WithSerialNumber(""), + ) + if err != nil { + t.Fatal(err) + } + + driver, err := 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 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" + + 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.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) + } + } + + t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events)) +} + +func TestSwipeWithDirection(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 cases for different directions and distance configurations + testCases := []struct { + name string + direction string + startX float64 + startY float64 + minDistance float64 + maxDistance float64 + }{ + { + name: "随机距离上滑", + direction: "up", + startX: 0.5, + startY: 0.5, + minDistance: 100.0, + maxDistance: 500.0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := driver.SIMSwipeWithDirection( + tc.direction, + tc.startX, + tc.startY, + tc.minDistance, + tc.maxDistance, + ) + if err != nil { + t.Errorf("SwipeWithDirection failed: %v", err) + } else { + t.Logf("Successfully executed swipe: direction=%s, start=(%.1f,%.1f), distance=%.1f-%.1f", + tc.direction, tc.startX, tc.startY, tc.minDistance, tc.maxDistance) + } + }) + } +} + +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(""), + ) + if err != nil { + t.Fatal(err) + } + + driver, err := NewUIA2Driver(device) + if err != nil { + t.Fatal(err) + } + defer driver.TearDown() + + // Test cases for different area configurations and directions + testCases := []struct { + name string + direction string + areaStartX float64 + areaStartY float64 + areaEndX float64 + areaEndY float64 + minDistance float64 + maxDistance float64 + }{ + { + name: "中心区域上滑_固定距离", + direction: "up", + areaStartX: 0.2, + areaStartY: 0.3, + areaEndX: 0.8, + areaEndY: 0.6, + minDistance: 500.0, + maxDistance: 700.0, // 固定距离 + }, + } + + for _, tc := range testCases { + for i := 0; i < 3; i++ { + t.Run(tc.name, func(t *testing.T) { + err := driver.SIMSwipeInArea( + tc.direction, + tc.areaStartX, + tc.areaStartY, + tc.areaEndX, + tc.areaEndY, + tc.minDistance, + tc.maxDistance, + ) + if err != nil { + t.Errorf("SwipeInArea failed: %v", err) + } else { + t.Logf("Successfully executed area swipe: direction=%s, area=(%.1f,%.1f)-(%.1f,%.1f), distance=%.1f-%.1f", + tc.direction, tc.areaStartX, tc.areaStartY, tc.areaEndX, tc.areaEndY, tc.minDistance, tc.maxDistance) + } + }) + } + } +} + +func TestSwipeFromPointToPoint(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 cases for different point-to-point swipes + testCases := []struct { + name string + startX float64 + startY float64 + endX float64 + endY float64 + }{ + { + name: "对角线滑动_左上到右下", + startX: 0.2, + startY: 0.3, + endX: 0.8, + endY: 0.5, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := driver.SIMSwipeFromPointToPoint( + tc.startX, + tc.startY, + tc.endX, + tc.endY, + ) + if err != nil { + t.Errorf("SwipeFromPointToPoint failed: %v", err) + } else { + t.Logf("Successfully executed point-to-point swipe: %s, from (%.1f,%.1f) to (%.1f,%.1f)", + tc.name, tc.startX, tc.startY, tc.endX, tc.endY) + } + }) + } +} + +func TestSwipeFromPointToPointInvalidInputs(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 same start and end point + err = driver.SIMSwipeFromPointToPoint(0.5, 0.5, 0.5, 0.5) + if err == nil { + t.Error("Expected error for same start and end point, but got none") + } + + // Test very close points (should result in distance too short) + err = driver.SIMSwipeFromPointToPoint(0.5, 0.5, 0.501, 0.501) + if err == nil { + t.Error("Expected error for very close points, but got none") + } + + t.Log("Point-to-point swipe invalid input validation tests passed") +} + +func TestClickAtPoint(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 cases for different click positions + testCases := []struct { + name string + x float64 + y float64 + }{ + { + name: "屏幕中心点击", + x: 0.5, + y: 0.5, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := driver.SIMClickAtPoint(tc.x, tc.y) + if err != nil { + t.Errorf("ClickAtPoint failed: %v", err) + } else { + t.Logf("Successfully executed click: %s at (%.1f, %.1f)", + tc.name, tc.x, tc.y) + } + }) + } +} + +func TestClickAtPointInvalidInputs(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 negative coordinates + err = driver.SIMClickAtPoint(-0.1, 0.5) + if err == nil { + t.Error("Expected error for negative x coordinate, but got none") + } + + err = driver.SIMClickAtPoint(0.5, -0.1) + if err == nil { + t.Error("Expected error for negative y coordinate, but got none") + } + + // Test coordinates out of range (though these should be handled by convertToAbsolutePoint) + err = driver.SIMClickAtPoint(1.5, 0.5) + if err != nil { + t.Logf("Out of range coordinates handled properly: %v", err) + } + + t.Log("Click invalid input validation tests passed") +} + +func TestSIMInput(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 cases for different text inputs + testCases := []struct { + name string + text string + }{ + { + name: "长文本", + text: "This is a very long text to test the performance of SIMInput function. 这是一个很长的文本用来测试SIMInput函数的性能。1234567890!@#$%^&*()英語の長い文字", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := driver.SIMInput(tc.text) + // err := driver.Input(tc.text) + if err != nil { + t.Errorf("SIMInput failed: %v", err) + } else { + t.Logf("Successfully executed SIMInput: %s with text '%s'", tc.name, tc.text) + } + }) + } +} From bf541785a1cb8290f5c2b5768ea7be137edfd103 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 2 Aug 2025 21:54:07 +0800 Subject: [PATCH 2/8] feat: add workflow claude code --- .github/workflows/claude-code.yml | 37 +++++++++++++++++++++++++++++++ internal/version/VERSION | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/claude-code.yml diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml new file mode 100644 index 00000000..5c1c9bd9 --- /dev/null +++ b/.github/workflows/claude-code.yml @@ -0,0 +1,37 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + env: + ANTHROPIC_BASE_URL: "${{ secrets.ANTHROPIC_BASE_URL }}" + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} \ No newline at end of file diff --git a/internal/version/VERSION b/internal/version/VERSION index 043ab204..a36de551 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250731 +v5.0.0-250802 From c61c5af33412996e4f2d825d505e0b7af7f13190 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 2 Aug 2025 22:09:40 +0800 Subject: [PATCH 3/8] fix: claude code actions --- .github/workflows/claude-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index 5c1c9bd9..5c8179fc 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -6,7 +6,7 @@ on: pull_request_review_comment: types: [created] issues: - types: [opened, assigned] + types: [opened, assigned, edited] pull_request_review: types: [submitted] From 84f038344a61d5f698881c620434cf4d6b3f0315 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 2 Aug 2025 22:13:32 +0800 Subject: [PATCH 4/8] test: debug workflow --- .github/workflows/claude-code.yml | 1 + .github/workflows/test.yml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index 5c8179fc..22afa6a6 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -9,6 +9,7 @@ on: types: [opened, assigned, edited] pull_request_review: types: [submitted] + workflow_dispatch: jobs: claude: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c36da14b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test Actions + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Echo test + run: echo "GitHub Actions is working!" \ No newline at end of file From e1867f99ef4aba24662c0e813ba0ae00b15a19f1 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 2 Aug 2025 22:18:15 +0800 Subject: [PATCH 5/8] test: debug workflow --- .github/workflows/claude-code.yml | 1 + .github/workflows/test.yml | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index 22afa6a6..73abfe62 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -14,6 +14,7 @@ on: jobs: claude: if: | + github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index c36da14b..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Test Actions - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Echo test - run: echo "GitHub Actions is working!" \ No newline at end of file From 38acfe3e3a815a78d9e5e5670f08a0bd25d21548 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 2 Aug 2025 22:29:54 +0800 Subject: [PATCH 6/8] fix: claude code actions --- .github/workflows/claude-code.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index 73abfe62..5c1c9bd9 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -6,15 +6,13 @@ on: pull_request_review_comment: types: [created] issues: - types: [opened, assigned, edited] + types: [opened, assigned] pull_request_review: types: [submitted] - workflow_dispatch: jobs: claude: if: | - github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || From 73db0cd124ea986e680b6782c9f8c71b81b75b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Tue, 5 Aug 2025 01:43:19 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E8=AE=BE=E7=BD=AE=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E9=97=B4120s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/version/VERSION | 2 +- uixt/driver_session.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index a36de551..1e99fc70 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250802 +v5.0.0-250805 diff --git a/uixt/driver_session.go b/uixt/driver_session.go index 35f5cc57..fddc04f7 100644 --- a/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -41,7 +41,7 @@ type DriverRequests struct { } func NewDriverSession() *DriverSession { - timeout := 30 * time.Second + timeout := 120 * time.Second session := &DriverSession{ ctx: context.Background(), ID: "", From 3c384775f6f6738afd31450908f0f2c1c1953079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Tue, 5 Aug 2025 20:55:58 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/code.go | 51 ++++++++++++++++++++++-------------------- uixt/driver_session.go | 14 ++++++++++-- uixt/ios_driver_wda.go | 16 ++++++++++++- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/code/code.go b/code/code.go index 54da9ae0..96ef4826 100644 --- a/code/code.go +++ b/code/code.go @@ -66,18 +66,19 @@ var ( // device related: [50, 70) var ( - DeviceConnectionError = errors.New("device general connection error") // 50 - DeviceHTTPDriverError = errors.New("device HTTP driver error") // 51 - DeviceUSBDriverError = errors.New("device USB driver error") // 52 - DeviceAppNotInstalled = errors.New("device app not installed") // 59 - DeviceGetInfoError = errors.New("device get info error") // 60 - DeviceConfigureError = errors.New("device configure error") // 61 - DeviceShellExecError = errors.New("device shell exec error") // 62 - DeviceOfflineError = errors.New("device offline") // 63 - DeviceInstallFailed = errors.New("device install app failed") // 64 - DeviceScreenShotError = errors.New("device screenshot error") // 65 - DeviceCaptureLogError = errors.New("device capture log error") // 66 - DeviceUIResponseSlow = errors.New("device UI response slow") // 67 + DeviceConnectionError = errors.New("device general connection error") // 50 + DeviceHTTPDriverError = errors.New("device HTTP driver error") // 51 + DeviceUSBDriverError = errors.New("device USB driver error") // 52 + DeviceUntrustedCertError = errors.New("device app certificate not trusted") // 53 + DeviceAppNotInstalled = errors.New("device app not installed") // 59 + DeviceGetInfoError = errors.New("device get info error") // 60 + DeviceConfigureError = errors.New("device configure error") // 61 + DeviceShellExecError = errors.New("device shell exec error") // 62 + DeviceOfflineError = errors.New("device offline") // 63 + DeviceInstallFailed = errors.New("device install app failed") // 64 + DeviceScreenShotError = errors.New("device screenshot error") // 65 + DeviceCaptureLogError = errors.New("device capture log error") // 66 + DeviceUIResponseSlow = errors.New("device UI response slow") // 67 ) // UI automation related: [70, 80) @@ -176,18 +177,20 @@ var errorsMap = map[error]int{ UploadFailed: 49, // device related - DeviceConnectionError: 50, - DeviceHTTPDriverError: 51, - DeviceUSBDriverError: 52, - DeviceAppNotInstalled: 59, - DeviceGetInfoError: 60, - DeviceConfigureError: 61, - DeviceShellExecError: 62, - DeviceOfflineError: 63, - DeviceInstallFailed: 64, - DeviceScreenShotError: 65, - DeviceCaptureLogError: 66, - DeviceUIResponseSlow: 67, + DeviceConnectionError: 50, + DeviceHTTPDriverError: 51, + DeviceUSBDriverError: 52, + DeviceUntrustedCertError: 53, + DeviceAppNotInstalled: 59, + DeviceGetInfoError: 60, + DeviceConfigureError: 61, + DeviceShellExecError: 62, + DeviceOfflineError: 63, + DeviceInstallFailed: 64, + DeviceScreenShotError: 65, + DeviceCaptureLogError: 66, + DeviceUIResponseSlow: 67, + DeviceUntrustedCertError: 68, // UI automation related MobileUIDriverAppNotInstalled: 68, diff --git a/uixt/driver_session.go b/uixt/driver_session.go index fddc04f7..10adbf60 100644 --- a/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -202,8 +202,13 @@ func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody [ synthesizeEventRetryAdded = true } - // Notice: use DeviceHTTPDriverError when request driver failed - lastError = errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + // Check if it's already a DeviceOfflineError + if errors.Is(err, code.DeviceOfflineError) { + lastError = err + } else { + // Use DeviceHTTPDriverError for other errors + lastError = errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } // If this was the last attempt, break if attempt == maxRetry { @@ -294,6 +299,11 @@ func (s *DriverSession) Request(method string, urlStr string, rawBody []byte, op driverResult.RequestTime = time.Now() var resp *http.Response if resp, err = s.client.Do(req); err != nil { + // Check for connection reset or EOF errors and classify as DeviceOfflineError + if strings.Contains(err.Error(), "read: connection reset by peer") || + strings.Contains(err.Error(), "EOF") { + return nil, errors.Wrap(code.DeviceOfflineError, err.Error()) + } return nil, err } defer func() { diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 283c5a92..0cb6d668 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -436,6 +436,13 @@ func (wd *WDADriver) AppLaunch(bundleId string) (err error) { // 超时两分钟 _, err = wd.Session.POST(data, "/wings/apps/launch", option.WithTimeout(120)) if err != nil { + // Check for untrusted certificate error + errMsg := err.Error() + if strings.Contains(errMsg, "has not been explicitly trusted by the user") || + strings.Contains(errMsg, "invalid code signature") || + strings.Contains(errMsg, "inadequate entitlements") { + return errors.Wrap(code.DeviceUntrustedCertError, "App certificate not trusted: "+bundleId) + } return errors.Wrap(err, "wda app launch failed") } return nil @@ -443,10 +450,17 @@ func (wd *WDADriver) AppLaunch(bundleId string) (err error) { func (wd *WDADriver) AppLaunchUnattached(bundleId string) (err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppLaunchUnattached") - // [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)] + // [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)]] data := map[string]interface{}{"bundleId": bundleId} _, err = wd.Session.POST(data, "/wda/apps/launchUnattached") if err != nil { + // Check for untrusted certificate error + errMsg := err.Error() + if strings.Contains(errMsg, "has not been explicitly trusted by the user") || + strings.Contains(errMsg, "invalid code signature") || + strings.Contains(errMsg, "inadequate entitlements") { + return errors.Wrap(code.DeviceUntrustedCertError, "App certificate not trusted: "+bundleId) + } return errors.Wrap(err, "wda app launchUnattached failed") } return nil