xcmonkey 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/issue_template/bug_report.md +26 -0
- data/.github/issue_template/feature_request.md +26 -0
- data/.github/pull_request_template.md +5 -0
- data/.gitignore +3 -0
- data/README.md +34 -9
- data/bin/xcmonkey +24 -5
- data/lib/xcmonkey/describer.rb +1 -2
- data/lib/xcmonkey/driver.rb +67 -5
- data/lib/xcmonkey/repeater.rb +39 -0
- data/lib/xcmonkey/version.rb +1 -1
- data/lib/xcmonkey.rb +6 -9
- data/spec/driver_spec.rb +61 -4
- data/spec/repeater_spec.rb +52 -0
- data/spec/xcmonkey_spec.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be5b38b4ec7977038ba4f739a4516a376bc79652447a0cc52c10465ea85657d2
|
|
4
|
+
data.tar.gz: 97d3eefd65f8a438bfb2ed2919bdaa6bf42e0324235d10a7f7df5b0ede316d88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94b8daa5da4df0d18b1644bf9d110978c070ba032e661d4bf1079f7fad90a2ed6df553bc8685a7267d7f819159ac615474c88c4ebdd6e00627b74339cc2f9e0e
|
|
7
|
+
data.tar.gz: 66fe245043c8f4887a3849c7e60bc6b8ee81cecfb56781f409e09a6651511d009acd2a2284e3534d67ef59abffb5432b3d85ff9495a951a4fc26dbd44bff38fb
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Create a report to help us improve
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What did you do?
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## What did you expect to happen?
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## What happened instead?
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Environment
|
|
20
|
+
|
|
21
|
+
- `xcmonkey` version:
|
|
22
|
+
- `idb` version:
|
|
23
|
+
- `xcode` version:
|
|
24
|
+
- `macOS` version:
|
|
25
|
+
|
|
26
|
+
## Additional context
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature Request
|
|
3
|
+
about: Got any ideas about new features? Let us know!
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What are you trying to achieve?
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## If possible, how can you achieve this currently?
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## What would be the better way?
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Environment
|
|
20
|
+
|
|
21
|
+
- `xcmonkey` version:
|
|
22
|
+
- `idb` version:
|
|
23
|
+
- `xcode` version:
|
|
24
|
+
- `macOS` version:
|
|
25
|
+
|
|
26
|
+
## Additional context
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -11,13 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
## Description
|
|
13
13
|
|
|
14
|
-
*xcmonkey* is a tool for doing
|
|
14
|
+
*xcmonkey* is a tool for doing stress testing of iOS apps. It's inspired by and has similar goals to [*monkey*](https://developer.android.com/studio/test/monkey) on Android.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Under the hood, *xcmonkey* uses [iOS Development Bridge](https://fbidb.io/) as a driver, that's why it's pretty smart and can do a lot of things, such as taps, swipes and presses. All that comes «pseudo-random» because it has access to the screen hierarchy, and so can either do actions blindly (like tapping on random points) or precisely (like tapping on the existing elements).
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Requirements
|
|
18
|
+
## Prerequisites
|
|
21
19
|
|
|
22
20
|
```bash
|
|
23
21
|
brew install facebook/fb/idb-companion
|
|
@@ -30,7 +28,7 @@ pip3.6 install fb-idb
|
|
|
30
28
|
gem install xcmonkey
|
|
31
29
|
```
|
|
32
30
|
|
|
33
|
-
If you prefer to use [bundler](https://bundler.io/), add the following line to your `Gemfile`:
|
|
31
|
+
If you prefer to use [*bundler*](https://bundler.io/), add the following line to your `Gemfile`:
|
|
34
32
|
|
|
35
33
|
```ruby
|
|
36
34
|
gem 'xcmonkey'
|
|
@@ -65,6 +63,33 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
|
|
|
65
63
|
}
|
|
66
64
|
```
|
|
67
65
|
|
|
66
|
+
### To repeat the stress test from generated session
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
$ xcmonkey repeat --session-path "./xcmonkey-session.json"
|
|
70
|
+
12:48:13.333: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
|
|
71
|
+
|
|
72
|
+
12:48:16.542: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=73416
|
|
73
|
+
|
|
74
|
+
12:48:20.195: Tap: {
|
|
75
|
+
"x": 53,
|
|
76
|
+
"y": 749
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
12:48:20.404: Swipe (0.5s): {
|
|
80
|
+
"x": 196,
|
|
81
|
+
"y": 426
|
|
82
|
+
} => {
|
|
83
|
+
"x": 143,
|
|
84
|
+
"y": 447
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
12:48:21.155: Press (1.2s): {
|
|
88
|
+
"x": 143,
|
|
89
|
+
"y": 323
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
68
93
|
### To describe the required point
|
|
69
94
|
|
|
70
95
|
```bash
|
|
@@ -72,13 +97,13 @@ $ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
|
|
|
72
97
|
20:05:20.212: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
|
|
73
98
|
|
|
74
99
|
20:05:21.713: x:20 y:625 point info: {
|
|
75
|
-
"AXFrame": "{{19, 624.
|
|
100
|
+
"AXFrame": "{{19, 624.3}, {86, 130.6}}",
|
|
76
101
|
"AXUniqueId": "ShortcutsRowCell",
|
|
77
102
|
"frame": {
|
|
78
|
-
"y": 624.
|
|
103
|
+
"y": 624.3,
|
|
79
104
|
"x": 19,
|
|
80
105
|
"width": 86,
|
|
81
|
-
"height": 130.
|
|
106
|
+
"height": 130.6
|
|
82
107
|
},
|
|
83
108
|
"role_description": "button",
|
|
84
109
|
"AXLabel": "Home",
|
data/bin/xcmonkey
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'commander/import'
|
|
4
4
|
require_relative '../lib/xcmonkey'
|
|
5
5
|
require_relative '../lib/xcmonkey/describer'
|
|
6
|
+
require_relative '../lib/xcmonkey/repeater'
|
|
6
7
|
require_relative '../lib/xcmonkey/logger'
|
|
7
8
|
require_relative '../lib/xcmonkey/driver'
|
|
8
9
|
require_relative '../lib/xcmonkey/version'
|
|
@@ -16,25 +17,43 @@ module Xcmonkey
|
|
|
16
17
|
c.description = 'Runs monkey test'
|
|
17
18
|
c.option('-u', '--udid STRING', String, 'Set device UDID')
|
|
18
19
|
c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
|
|
19
|
-
c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds')
|
|
20
|
-
c.
|
|
21
|
-
|
|
20
|
+
c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds. Defaults to `60`')
|
|
21
|
+
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
|
|
22
|
+
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
|
|
23
|
+
c.action do |_, options|
|
|
24
|
+
options.default(
|
|
25
|
+
duration: 60,
|
|
26
|
+
session_path: Dir.pwd,
|
|
27
|
+
enable_simulator_keyboard: true
|
|
28
|
+
)
|
|
22
29
|
params = {
|
|
23
30
|
udid: options.udid,
|
|
24
31
|
bundle_id: options.bundle_id,
|
|
25
|
-
duration: options.duration
|
|
32
|
+
duration: options.duration,
|
|
33
|
+
session_path: options.session_path,
|
|
34
|
+
enable_simulator_keyboard: options.enable_simulator_keyboard
|
|
26
35
|
}
|
|
27
36
|
Xcmonkey.new(params).run
|
|
28
37
|
end
|
|
29
38
|
end
|
|
30
39
|
|
|
40
|
+
command :repeat do |c|
|
|
41
|
+
c.syntax = 'xcmonkey repeat [options]'
|
|
42
|
+
c.description = 'Repeats given session'
|
|
43
|
+
c.option('-s', '--session-path STRING', String, 'Path to monkey testing session')
|
|
44
|
+
c.action do |_, options|
|
|
45
|
+
params = { session_path: options.session_path }
|
|
46
|
+
Repeater.new(params).run
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
31
50
|
command :describe do |c|
|
|
32
51
|
c.syntax = 'xcmonkey describe [options]'
|
|
33
52
|
c.description = 'Describes given point'
|
|
34
53
|
c.option('-u', '--udid STRING', String, 'Set device UDID')
|
|
35
54
|
c.option('-x', '--x STRING', 'Point `x` coordinate')
|
|
36
55
|
c.option('-y', '--y STRING', 'Point `y` coordinate')
|
|
37
|
-
c.action do |
|
|
56
|
+
c.action do |_, options|
|
|
38
57
|
params = {
|
|
39
58
|
udid: options.udid,
|
|
40
59
|
x: options.x,
|
data/lib/xcmonkey/describer.rb
CHANGED
data/lib/xcmonkey/driver.rb
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
class Driver
|
|
2
|
-
attr_accessor :udid, :bundle_id, :
|
|
2
|
+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :session_duration, :session_path, :session_actions
|
|
3
3
|
|
|
4
4
|
def initialize(params)
|
|
5
5
|
self.udid = params[:udid]
|
|
6
6
|
self.bundle_id = params[:bundle_id]
|
|
7
|
-
self.
|
|
7
|
+
self.session_duration = params[:duration]
|
|
8
|
+
self.session_path = params[:session_path]
|
|
9
|
+
self.enable_simulator_keyboard = params[:enable_simulator_keyboard]
|
|
10
|
+
self.session_actions = params[:session_actions]
|
|
11
|
+
@session = { params: params, actions: [] }
|
|
8
12
|
ensure_driver_installed
|
|
9
13
|
end
|
|
10
14
|
|
|
15
|
+
def monkey_test_precondition
|
|
16
|
+
ensure_device_exists
|
|
17
|
+
ensure_app_installed
|
|
18
|
+
terminate_app
|
|
19
|
+
open_home_screen(with_tracker: true)
|
|
20
|
+
launch_app
|
|
21
|
+
end
|
|
22
|
+
|
|
11
23
|
def monkey_test(gestures)
|
|
24
|
+
monkey_test_precondition
|
|
12
25
|
app_elements = describe_ui.shuffle
|
|
13
26
|
current_time = Time.now
|
|
14
|
-
while Time.now < current_time +
|
|
27
|
+
while Time.now < current_time + session_duration
|
|
15
28
|
el1_coordinates = central_coordinates(app_elements.first)
|
|
16
29
|
el2_coordinates = central_coordinates(app_elements.last)
|
|
17
30
|
case gestures.sample
|
|
@@ -39,7 +52,30 @@ class Driver
|
|
|
39
52
|
next
|
|
40
53
|
end
|
|
41
54
|
app_elements = describe_ui.shuffle
|
|
42
|
-
|
|
55
|
+
next unless app_elements.include?(@home_tracker)
|
|
56
|
+
|
|
57
|
+
save_session
|
|
58
|
+
Logger.error('App lost')
|
|
59
|
+
end
|
|
60
|
+
save_session
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def repeat_monkey_test
|
|
64
|
+
monkey_test_precondition
|
|
65
|
+
session_actions.each do |action|
|
|
66
|
+
case action['type']
|
|
67
|
+
when 'tap'
|
|
68
|
+
tap(coordinates: { x: action['x'], y: action['y'] })
|
|
69
|
+
when 'press'
|
|
70
|
+
press(coordinates: { x: action['x'], y: action['y'] }, duration: action['duration'])
|
|
71
|
+
when 'swipe'
|
|
72
|
+
swipe(
|
|
73
|
+
start_coordinates: { x: action['x'], y: action['y'] },
|
|
74
|
+
end_coordinates: { x: action['endX'], y: action['endY'] },
|
|
75
|
+
duration: action['duration']
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
Logger.error('App lost') if describe_ui.shuffle.include?(@home_tracker)
|
|
43
79
|
end
|
|
44
80
|
end
|
|
45
81
|
|
|
@@ -76,6 +112,12 @@ class Driver
|
|
|
76
112
|
`idb shutdown #{udid}`
|
|
77
113
|
end
|
|
78
114
|
|
|
115
|
+
def configure_simulator_keyboard
|
|
116
|
+
shutdown_simulator
|
|
117
|
+
keyboard_status = enable_simulator_keyboard ? 0 : 1
|
|
118
|
+
`defaults write com.apple.iphonesimulator ConnectHardwareKeyboard #{keyboard_status}`
|
|
119
|
+
end
|
|
120
|
+
|
|
79
121
|
def list_targets
|
|
80
122
|
@list_targets ||= `idb list-targets`.split("\n")
|
|
81
123
|
@list_targets
|
|
@@ -92,8 +134,12 @@ class Driver
|
|
|
92
134
|
def ensure_device_exists
|
|
93
135
|
device = list_targets.detect { |target| target.include?(udid) }
|
|
94
136
|
Logger.error("Can't find device #{udid}") if device.nil?
|
|
137
|
+
|
|
95
138
|
Logger.info('Device info:', payload: device)
|
|
96
|
-
|
|
139
|
+
if device.include?('simulator')
|
|
140
|
+
configure_simulator_keyboard
|
|
141
|
+
boot_simulator
|
|
142
|
+
end
|
|
97
143
|
end
|
|
98
144
|
|
|
99
145
|
def list_apps
|
|
@@ -102,11 +148,13 @@ class Driver
|
|
|
102
148
|
|
|
103
149
|
def tap(coordinates:)
|
|
104
150
|
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
151
|
+
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
|
|
105
152
|
`idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
|
|
106
153
|
end
|
|
107
154
|
|
|
108
155
|
def press(coordinates:, duration:)
|
|
109
156
|
Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
157
|
+
@session[:actions] << { type: :press, x: coordinates[:x], y: coordinates[:y], duration: duration } unless session_actions
|
|
110
158
|
`idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
|
|
111
159
|
end
|
|
112
160
|
|
|
@@ -115,6 +163,16 @@ class Driver
|
|
|
115
163
|
"Swipe (#{duration}s):",
|
|
116
164
|
payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
|
|
117
165
|
)
|
|
166
|
+
unless session_actions
|
|
167
|
+
@session[:actions] << {
|
|
168
|
+
type: :swipe,
|
|
169
|
+
x: start_coordinates[:x],
|
|
170
|
+
y: start_coordinates[:y],
|
|
171
|
+
endX: end_coordinates[:x],
|
|
172
|
+
endY: end_coordinates[:y],
|
|
173
|
+
duration: duration
|
|
174
|
+
}
|
|
175
|
+
end
|
|
118
176
|
coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
|
|
119
177
|
`idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
|
|
120
178
|
end
|
|
@@ -157,6 +215,10 @@ class Driver
|
|
|
157
215
|
rand(0.5..1.5).ceil(1)
|
|
158
216
|
end
|
|
159
217
|
|
|
218
|
+
def save_session
|
|
219
|
+
File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
|
|
220
|
+
end
|
|
221
|
+
|
|
160
222
|
private
|
|
161
223
|
|
|
162
224
|
def ensure_driver_installed
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class Repeater
|
|
2
|
+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
|
|
3
|
+
|
|
4
|
+
def initialize(params)
|
|
5
|
+
validate_session(params[:session_path])
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def run
|
|
9
|
+
params = {
|
|
10
|
+
udid: udid,
|
|
11
|
+
bundle_id: bundle_id,
|
|
12
|
+
enable_simulator_keyboard: enable_simulator_keyboard,
|
|
13
|
+
session_actions: actions
|
|
14
|
+
}
|
|
15
|
+
Driver.new(params).repeat_monkey_test
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_session(session_path)
|
|
19
|
+
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)
|
|
20
|
+
|
|
21
|
+
session = JSON.parse(File.read(session_path))
|
|
22
|
+
|
|
23
|
+
if session['params'].nil?
|
|
24
|
+
Logger.error('Provided session is not valid: `params` should not be `nil`')
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self.actions = session['actions']
|
|
29
|
+
Logger.error('Provided session is not valid: `actions` should not be `nil` or `empty`') if actions.nil? || actions.empty?
|
|
30
|
+
|
|
31
|
+
self.udid = session['params']['udid']
|
|
32
|
+
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?
|
|
33
|
+
|
|
34
|
+
self.bundle_id = session['params']['bundle_id']
|
|
35
|
+
Logger.error('Provided session is not valid: `bundle_id` should not be `nil`') if bundle_id.nil?
|
|
36
|
+
|
|
37
|
+
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/xcmonkey/version.rb
CHANGED
data/lib/xcmonkey.rb
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'colorize'
|
|
3
3
|
require_relative 'xcmonkey/describer'
|
|
4
|
+
require_relative 'xcmonkey/repeater'
|
|
4
5
|
require_relative 'xcmonkey/version'
|
|
5
6
|
require_relative 'xcmonkey/logger'
|
|
6
7
|
require_relative 'xcmonkey/driver'
|
|
7
8
|
|
|
8
9
|
module Xcmonkey
|
|
9
10
|
class Xcmonkey
|
|
10
|
-
attr_accessor :
|
|
11
|
+
attr_accessor :driver
|
|
11
12
|
|
|
12
13
|
def initialize(params)
|
|
13
14
|
ensure_required_params(params)
|
|
14
|
-
self.udid = params[:udid]
|
|
15
|
-
self.bundle_id = params[:bundle_id]
|
|
16
|
-
self.duration = params[:duration]
|
|
17
15
|
self.driver = Driver.new(params)
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def run
|
|
21
|
-
driver.ensure_device_exists
|
|
22
|
-
driver.ensure_app_installed
|
|
23
|
-
driver.terminate_app
|
|
24
|
-
driver.open_home_screen(with_tracker: true)
|
|
25
|
-
driver.launch_app
|
|
26
19
|
driver.monkey_test(gestures)
|
|
27
20
|
end
|
|
28
21
|
|
|
@@ -35,7 +28,11 @@ module Xcmonkey
|
|
|
35
28
|
|
|
36
29
|
def ensure_required_params(params)
|
|
37
30
|
Logger.error('UDID should be provided') if params[:udid].nil?
|
|
31
|
+
|
|
38
32
|
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
|
|
33
|
+
|
|
34
|
+
Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
|
|
35
|
+
|
|
39
36
|
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
|
|
40
37
|
Logger.error('Duration must be Integer and not less than 1 second')
|
|
41
38
|
end
|
data/spec/driver_spec.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
describe Driver do
|
|
2
2
|
let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
|
|
3
3
|
let(:bundle_id) { 'com.apple.Maps' }
|
|
4
|
-
let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id) }
|
|
4
|
+
let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id, session_path: Dir.pwd) }
|
|
5
|
+
let(:driver_with_session) { described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ type: 'tap', x: 0, y: 0 }]) }
|
|
5
6
|
|
|
6
7
|
it 'verifies that sumulator was booted' do
|
|
7
8
|
error_message = "Failed to boot #{udid}"
|
|
@@ -122,6 +123,26 @@ describe Driver do
|
|
|
122
123
|
expect(driver.press_duration).to be_between(0.5, 1.5)
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
it 'verifies that simulator keyboard can be enabled' do
|
|
127
|
+
allow(driver).to receive(:is_simulator_keyboard_enabled?).and_return(false)
|
|
128
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, enable_simulator_keyboard: true)
|
|
129
|
+
expect(driver).to receive(:shutdown_simulator)
|
|
130
|
+
driver.configure_simulator_keyboard
|
|
131
|
+
keyboard_state = `defaults read com.apple.iphonesimulator`.split("\n").grep(/ConnectHardwareKeyboard/)
|
|
132
|
+
expect(keyboard_state).not_to be_empty
|
|
133
|
+
expect(keyboard_state.first).to include('0')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'verifies that simulator keyboard can be disabled' do
|
|
137
|
+
allow(driver).to receive(:is_simulator_keyboard_enabled?).and_return(true)
|
|
138
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, enable_simulator_keyboard: false)
|
|
139
|
+
expect(driver).to receive(:shutdown_simulator)
|
|
140
|
+
driver.configure_simulator_keyboard
|
|
141
|
+
keyboard_state = `defaults read com.apple.iphonesimulator`.split("\n").grep(/ConnectHardwareKeyboard/)
|
|
142
|
+
expect(keyboard_state).not_to be_empty
|
|
143
|
+
expect(keyboard_state.first).to include('1')
|
|
144
|
+
end
|
|
145
|
+
|
|
125
146
|
it 'verifies that app can be launched' do
|
|
126
147
|
expect(Logger).not_to receive(:error)
|
|
127
148
|
expect(Logger).to receive(:info)
|
|
@@ -130,28 +151,64 @@ describe Driver do
|
|
|
130
151
|
expect { driver.launch_app }.not_to raise_error
|
|
131
152
|
end
|
|
132
153
|
|
|
133
|
-
it 'verifies tap' do
|
|
154
|
+
it 'verifies tap in new session' do
|
|
134
155
|
driver.boot_simulator
|
|
135
156
|
coordinates = { x: 1, y: 1 }
|
|
136
157
|
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
137
158
|
driver.tap(coordinates: coordinates)
|
|
159
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
138
160
|
end
|
|
139
161
|
|
|
140
|
-
it 'verifies
|
|
162
|
+
it 'verifies tap in old session' do
|
|
163
|
+
driver_with_session.boot_simulator
|
|
164
|
+
coordinates = { x: 1, y: 1 }
|
|
165
|
+
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
166
|
+
driver_with_session.tap(coordinates: coordinates)
|
|
167
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'verifies press in new session' do
|
|
141
171
|
driver.boot_simulator
|
|
142
172
|
duration = 0.5
|
|
143
173
|
coordinates = { x: 1, y: 1 }
|
|
144
174
|
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
145
175
|
driver.press(coordinates: coordinates, duration: duration)
|
|
176
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
146
177
|
end
|
|
147
178
|
|
|
148
|
-
it 'verifies
|
|
179
|
+
it 'verifies press in old session' do
|
|
180
|
+
driver_with_session.boot_simulator
|
|
181
|
+
duration = 0.5
|
|
182
|
+
coordinates = { x: 1, y: 1 }
|
|
183
|
+
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
184
|
+
driver_with_session.press(coordinates: coordinates, duration: duration)
|
|
185
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'verifies swipe in new session' do
|
|
149
189
|
driver.boot_simulator
|
|
150
190
|
duration = 0.5
|
|
151
191
|
start_coordinates = { x: 1, y: 1 }
|
|
152
192
|
end_coordinates = { x: 2, y: 2 }
|
|
153
193
|
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
|
154
194
|
driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
|
195
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'verifies swipe in old session' do
|
|
199
|
+
driver_with_session.boot_simulator
|
|
200
|
+
duration = 0.5
|
|
201
|
+
start_coordinates = { x: 1, y: 1 }
|
|
202
|
+
end_coordinates = { x: 2, y: 2 }
|
|
203
|
+
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
|
204
|
+
driver_with_session.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
|
205
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'verifies that session can be saved' do
|
|
209
|
+
expect(File).to receive(:write)
|
|
210
|
+
driver.instance_variable_set(:@session, { params: {}, actions: [] })
|
|
211
|
+
driver.save_session
|
|
155
212
|
end
|
|
156
213
|
|
|
157
214
|
it 'verifies that simulator was not booted' do
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
describe Repeater do
|
|
2
|
+
let(:session_path) { 'test/path/session.json' }
|
|
3
|
+
let(:session_file_content_full) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
4
|
+
let(:session_file_content_without_params) { '{ "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
5
|
+
let(:session_file_content_with_empty_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [] }' }
|
|
6
|
+
let(:session_file_content_without_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true} }' }
|
|
7
|
+
let(:session_file_content_without_bundle_id) { '{ "params": {"udid": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
8
|
+
let(:session_file_content_without_udid) { '{ "params": {"bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
9
|
+
|
|
10
|
+
# TESTME
|
|
11
|
+
it 'verifies that session cannot be validated without params' do
|
|
12
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
13
|
+
allow(File).to receive(:read).and_return(session_file_content_without_params)
|
|
14
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `params` should not be `nil`')
|
|
15
|
+
described_class.new(session_path: session_path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'verifies that session cannot be validated without actions' do
|
|
19
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
20
|
+
allow(File).to receive(:read).and_return(session_file_content_without_actions)
|
|
21
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
|
|
22
|
+
described_class.new(session_path: session_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'verifies that session cannot be validated with empty actions' do
|
|
26
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
27
|
+
allow(File).to receive(:read).and_return(session_file_content_with_empty_actions)
|
|
28
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
|
|
29
|
+
described_class.new(session_path: session_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'verifies that session cannot be validated without bundle id' do
|
|
33
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
34
|
+
allow(File).to receive(:read).and_return(session_file_content_without_bundle_id)
|
|
35
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `bundle_id` should not be `nil`')
|
|
36
|
+
described_class.new(session_path: session_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'verifies that session cannot be validated without udid' do
|
|
40
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
41
|
+
allow(File).to receive(:read).and_return(session_file_content_without_udid)
|
|
42
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `udid` should not be `nil`')
|
|
43
|
+
described_class.new(session_path: session_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'verifies that session validation can pass' do
|
|
47
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
48
|
+
allow(File).to receive(:read).and_return(session_file_content_full)
|
|
49
|
+
expect(Logger).not_to receive(:error)
|
|
50
|
+
described_class.new(session_path: session_path)
|
|
51
|
+
end
|
|
52
|
+
end
|
data/spec/xcmonkey_spec.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
describe Xcmonkey do
|
|
2
2
|
describe Xcmonkey::Xcmonkey do
|
|
3
|
-
let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10 } }
|
|
3
|
+
let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10, session_path: Dir.pwd } }
|
|
4
4
|
let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
|
|
5
5
|
|
|
6
6
|
it 'verifies gestures' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: xcmonkey
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alteral
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-01-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -217,6 +217,8 @@ files:
|
|
|
217
217
|
- ".fasterer.yml"
|
|
218
218
|
- ".github/FUNDING.yml"
|
|
219
219
|
- ".github/dependabot.yml"
|
|
220
|
+
- ".github/issue_template/bug_report.md"
|
|
221
|
+
- ".github/issue_template/feature_request.md"
|
|
220
222
|
- ".github/pull_request_template.md"
|
|
221
223
|
- ".github/workflows/test.yml"
|
|
222
224
|
- ".gitignore"
|
|
@@ -236,12 +238,14 @@ files:
|
|
|
236
238
|
- lib/xcmonkey/describer.rb
|
|
237
239
|
- lib/xcmonkey/driver.rb
|
|
238
240
|
- lib/xcmonkey/logger.rb
|
|
241
|
+
- lib/xcmonkey/repeater.rb
|
|
239
242
|
- lib/xcmonkey/version.rb
|
|
240
243
|
- requirements.txt
|
|
241
244
|
- sonar-project.properties
|
|
242
245
|
- spec/describer_spec.rb
|
|
243
246
|
- spec/driver_spec.rb
|
|
244
247
|
- spec/logger_spec.rb
|
|
248
|
+
- spec/repeater_spec.rb
|
|
245
249
|
- spec/spec_helper.rb
|
|
246
250
|
- spec/xcmonkey_spec.rb
|
|
247
251
|
- xcmonkey.gemspec
|