xcmonkey 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a6aa190c1c4cfa28f97b20524c907da488aad2d78f2c1dda8f827899f6ee53f
4
- data.tar.gz: 534245a695ed0f030df459986c326fb356b3593baec3bc6750eb1cc622d32390
3
+ metadata.gz: be6683e9fefe9666fb294e9939f5ebd83c71915307002dc6425413dfd1d9ae8b
4
+ data.tar.gz: d4782c2269b36dd950b7567c88d554c7b000cc81452f79e6659d4061135c1ebe
5
5
  SHA512:
6
- metadata.gz: 41415c8c576a3b6887ee87d44f4c4e571f1e7abda3a96f23abff52cebe77438be70ee76ab18f83a92b78cca2da429cf0c12305b2d3a8ca59b0b2e7e26405ec23
7
- data.tar.gz: 93d86451b2b7b8d2a755744ea47be375d042763ea550ddbdef5faff530bbf775e46ab24ede0f6d9dfe424f02ef4c2a923ccd3f342d1872cd6f4d82dcefae7cef
6
+ metadata.gz: d027d46289c6ea5bfba7359ff5c0b8172ea7d12d5d9cd1019a428b7b74416e6562930db5ce14508198937caed55ac2b5f1c68f1ce34c82fbe6ecbf3a92ea7100
7
+ data.tar.gz: 6d336b581f1bb93db017414b7080e038557200909e8a87b3a23c37fce4dfdd62b9fc5186c45272e662002f073c00a800f172488495b9bf84cd91d0a9089e275c
data/.github/FUNDING.yml CHANGED
@@ -1,3 +1,2 @@
1
1
  custom: [ 'https://revolut.me/alteral', 'https://paypal.me/aapesotskiy' ]
2
- github: alteral
3
2
  ko_fi: alteral
@@ -13,7 +13,7 @@ jobs:
13
13
  chat:
14
14
  name: Automated Code Review
15
15
  runs-on: macos-12
16
- timeout-minutes: 15
16
+ timeout-minutes: 30
17
17
  env:
18
18
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19
19
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
data/README.md CHANGED
@@ -9,8 +9,14 @@
9
9
  <a href="/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg?style=flat" /></a>
10
10
  </p>
11
11
 
12
+ ## Description
13
+
12
14
  *xcmonkey* is a tool for doing randomised UI testing of iOS apps.
13
15
 
16
+ It is inspired by and has similar goals to Android [monkey](https://developer.android.com/studio/test/monkey).
17
+
18
+ *xcmonkey* uses [idb](https://fbidb.io) as a driver that's why it's quite smart and can do a lot of things, such as taps, swipes and presses. Because *xcmonkey* has access to the screen hierarchy, it can either do things blindly (like tapping on random points) or precisely (like tapping on the existing elements).
19
+
14
20
  ## Requirements
15
21
 
16
22
  ```bash
@@ -24,22 +30,28 @@ pip3.6 install fb-idb
24
30
  gem install xcmonkey
25
31
  ```
26
32
 
33
+ If you prefer to use [bundler](https://bundler.io/), add the following line to your `Gemfile`:
34
+
35
+ ```ruby
36
+ gem 'xcmonkey'
37
+ ```
38
+
27
39
  ## Usage
28
40
 
29
- ### Test
41
+ ### To run a stress test
30
42
 
31
43
  ```bash
32
- $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.mobilesafari"
44
+ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
33
45
  12:44:19.343: 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
34
46
 
35
- 12:44:22.550: App info: com.apple.mobilesafari | MobileSafari | system | x86_64, arm64 | Running | Not Debuggable | pid=43398
47
+ 12:44:22.550: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=74636
36
48
 
37
49
  12:44:23.203: Tap: {
38
50
  "x": 53,
39
51
  "y": 749
40
52
  }
41
53
 
42
- 12:44:23.511: Swipe: {
54
+ 12:44:23.511: Swipe (0.5s): {
43
55
  "x": 196,
44
56
  "y": 426
45
57
  } => {
@@ -47,39 +59,37 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
47
59
  "y": 447
48
60
  }
49
61
 
50
- 12:44:24.355: Tap: {
62
+ 12:44:24.355: Press (1.2s): {
51
63
  "x": 143,
52
64
  "y": 323
53
65
  }
54
66
  ```
55
67
 
56
- ### Describe point
68
+ ### To describe the required point
57
69
 
58
70
  ```bash
59
- $ xcmonkey describe -x 125 -y 760 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
60
- 12:41:03.840: 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
+ $ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
72
+ 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
61
73
 
62
- 12:41:05.342: x:125 y:760 point info: {
63
- "AXFrame": "{{120, 759}, {64, 64}}",
64
- "AXUniqueId": "Safari",
74
+ 20:05:21.713: x:20 y:625 point info: {
75
+ "AXFrame": "{{19, 624.33333333333337}, {86, 130.66666666666663}}",
76
+ "AXUniqueId": "ShortcutsRowCell",
65
77
  "frame": {
66
- "y": 759,
67
- "x": 120,
68
- "width": 64,
69
- "height": 64
78
+ "y": 624.3333333333334,
79
+ "x": 19,
80
+ "width": 86,
81
+ "height": 130.66666666666663
70
82
  },
71
83
  "role_description": "button",
72
- "AXLabel": "Safari",
84
+ "AXLabel": "Home",
73
85
  "content_required": false,
74
86
  "type": "Button",
75
87
  "title": null,
76
- "help": "Double tap to open",
88
+ "help": null,
77
89
  "custom_actions": [
78
- "Edit mode",
79
- "Today",
80
- "App Library"
90
+
81
91
  ],
82
- "AXValue": "",
92
+ "AXValue": "Add",
83
93
  "enabled": true,
84
94
  "role": "AXButton",
85
95
  "subrole": null
Binary file
@@ -18,11 +18,23 @@ class Driver
18
18
  when :precise_tap
19
19
  tap(coordinates: el1_coordinates)
20
20
  when :blind_tap
21
- x = (el1_coordinates[:x] - el2_coordinates[:x]).abs
22
- y = (el1_coordinates[:y] - el2_coordinates[:y]).abs
23
- tap(coordinates: { x: x, y: y })
24
- when :swipe
25
- swipe(start_coordinates: el1_coordinates, end_coordinates: el2_coordinates)
21
+ tap(coordinates: random_coordinates)
22
+ when :precise_press
23
+ press(coordinates: el1_coordinates, duration: press_duration)
24
+ when :blind_press
25
+ press(coordinates: random_coordinates, duration: press_duration)
26
+ when :precise_swipe
27
+ swipe(
28
+ start_coordinates: el1_coordinates,
29
+ end_coordinates: el2_coordinates,
30
+ duration: swipe_duration
31
+ )
32
+ when :blind_swipe
33
+ swipe(
34
+ start_coordinates: random_coordinates,
35
+ end_coordinates: random_coordinates,
36
+ duration: swipe_duration
37
+ )
26
38
  else
27
39
  next
28
40
  end
@@ -31,9 +43,9 @@ class Driver
31
43
  end
32
44
  end
33
45
 
34
- def open_home_screen(return_tracker: false)
46
+ def open_home_screen(with_tracker: false)
35
47
  `idb ui button --udid #{udid} HOME`
36
- detect_home_unique_element if return_tracker
48
+ detect_home_unique_element if with_tracker
37
49
  end
38
50
 
39
51
  def describe_ui
@@ -57,7 +69,7 @@ class Driver
57
69
 
58
70
  def boot_simulator
59
71
  `idb boot #{udid}`
60
- ensure_simulator_was_booted
72
+ Logger.error("Failed to boot #{udid}") if device_info['state'] != 'Booted'
61
73
  end
62
74
 
63
75
  def shutdown_simulator
@@ -84,11 +96,6 @@ class Driver
84
96
  boot_simulator if device.include?('simulator')
85
97
  end
86
98
 
87
- def ensure_simulator_was_booted
88
- sim = list_booted_simulators.detect { |target| target.include?(udid) }
89
- Logger.error("Failed to boot #{udid}") if sim.nil?
90
- end
91
-
92
99
  def list_apps
93
100
  `idb list-apps --udid #{udid}`
94
101
  end
@@ -98,20 +105,58 @@ class Driver
98
105
  `idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
99
106
  end
100
107
 
101
- def swipe(start_coordinates:, end_coordinates:)
102
- Logger.info('Swipe:', payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
108
+ def press(coordinates:, duration:)
109
+ Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
110
+ `idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
111
+ end
112
+
113
+ def swipe(start_coordinates:, end_coordinates:, duration:)
114
+ Logger.info(
115
+ "Swipe (#{duration}s):",
116
+ payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
117
+ )
103
118
  coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
104
- `idb ui swipe --udid #{udid} --duration 0.5 #{coordinates}`
119
+ `idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
105
120
  end
106
121
 
107
122
  def central_coordinates(element)
108
123
  frame = element['frame']
124
+ x = (frame['x'] + (frame['width'] / 2)).abs.to_i
125
+ y = (frame['y'] + (frame['height'] / 2)).abs.to_i
126
+ {
127
+ x: x > screen_size[:width].to_i ? rand(0..screen_size[:width].to_i) : x,
128
+ y: y > screen_size[:height].to_i ? rand(0..screen_size[:height].to_i) : y
129
+ }
130
+ end
131
+
132
+ def random_coordinates
133
+ {
134
+ x: rand(0..screen_size[:width].to_i),
135
+ y: rand(0..screen_size[:height].to_i)
136
+ }
137
+ end
138
+
139
+ def device_info
140
+ @device_info ||= JSON.parse(`idb describe --udid #{udid} --json`)
141
+ @device_info
142
+ end
143
+
144
+ def screen_size
145
+ screen_dimensions = device_info['screen_dimensions']
109
146
  {
110
- x: (frame['x'] + (frame['width'] / 2)).to_i,
111
- y: (frame['y'] + (frame['height'] / 2)).to_i
147
+ width: screen_dimensions['width_points'],
148
+ height: screen_dimensions['height_points']
112
149
  }
113
150
  end
114
151
 
152
+ def swipe_duration
153
+ rand(0.1..0.7).ceil(1)
154
+ end
155
+
156
+ def press_duration
157
+ rand(0.5..1.5).ceil(1)
158
+ end
159
+
115
160
  private
116
161
 
117
162
  def ensure_driver_installed
@@ -1,4 +1,4 @@
1
1
  module Xcmonkey
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  GEM_NAME = 'xcmonkey'
4
4
  end
data/lib/xcmonkey.rb CHANGED
@@ -21,13 +21,16 @@ module Xcmonkey
21
21
  driver.ensure_device_exists
22
22
  driver.ensure_app_installed
23
23
  driver.terminate_app
24
- driver.open_home_screen(return_tracker: true)
24
+ driver.open_home_screen(with_tracker: true)
25
25
  driver.launch_app
26
26
  driver.monkey_test(gestures)
27
27
  end
28
28
 
29
29
  def gestures
30
- [:precise_tap, :blind_tap, :swipe]
30
+ taps = [:precise_tap, :blind_tap] * 10
31
+ swipes = [:precise_swipe, :blind_swipe] * 5
32
+ presses = [:precise_press, :blind_press]
33
+ taps + swipes + presses
31
34
  end
32
35
 
33
36
  def ensure_required_params(params)
data/spec/driver_spec.rb CHANGED
@@ -6,8 +6,7 @@ describe Driver do
6
6
  it 'verifies that sumulator was booted' do
7
7
  error_message = "Failed to boot #{udid}"
8
8
  expect(Logger).not_to receive(:error).with(error_message, payload: nil)
9
- expect(driver).to receive(:ensure_simulator_was_booted)
10
- driver.boot_simulator
9
+ expect { driver.boot_simulator }.not_to raise_error
11
10
  end
12
11
 
13
12
  it 'verifies that there are booted simulators' do
@@ -24,10 +23,16 @@ describe Driver do
24
23
 
25
24
  it 'verifies that home screen can be opened' do
26
25
  driver.boot_simulator
27
- home_tracker = driver.open_home_screen(return_tracker: true)
26
+ home_tracker = driver.open_home_screen(with_tracker: true)
28
27
  expect(home_tracker).not_to be_empty
29
28
  end
30
29
 
30
+ it 'verifies that home screen can be opened without tracker' do
31
+ driver.boot_simulator
32
+ home_tracker = driver.open_home_screen(with_tracker: false)
33
+ expect(home_tracker).to be_nil
34
+ end
35
+
31
36
  it 'verifies that list of targets can be showed' do
32
37
  list_targets = driver.list_targets
33
38
  expect(list_targets).not_to be_empty
@@ -85,6 +90,38 @@ describe Driver do
85
90
  expect(actual_coordinates).to eq(expected_coordinates)
86
91
  end
87
92
 
93
+ it 'verifies that device info can be for booted simulator' do
94
+ driver.boot_simulator
95
+ expect(driver.device_info).not_to be_empty
96
+ end
97
+
98
+ it 'verifies that device info can be for not booted simulator' do
99
+ driver.shutdown_simulator
100
+ expect(driver.device_info).not_to be_empty
101
+ end
102
+
103
+ it 'verifies that screen size can be found' do
104
+ driver.boot_simulator
105
+ screen_size = driver.screen_size
106
+ expect(screen_size[:width]).to be > 0
107
+ expect(screen_size[:height]).to be > 0
108
+ end
109
+
110
+ it 'verifies that random coordinates can be found' do
111
+ driver.boot_simulator
112
+ coordinates = driver.random_coordinates
113
+ expect(coordinates[:x]).to be > 0
114
+ expect(coordinates[:y]).to be > 0
115
+ end
116
+
117
+ it 'verifies swipe duration' do
118
+ expect(driver.swipe_duration).to be_between(0.1, 0.7)
119
+ end
120
+
121
+ it 'verifies press duration' do
122
+ expect(driver.press_duration).to be_between(0.5, 1.5)
123
+ end
124
+
88
125
  it 'verifies that app can be launched' do
89
126
  expect(Logger).not_to receive(:error)
90
127
  expect(Logger).to receive(:info)
@@ -93,10 +130,35 @@ describe Driver do
93
130
  expect { driver.launch_app }.not_to raise_error
94
131
  end
95
132
 
133
+ it 'verifies tap' do
134
+ driver.boot_simulator
135
+ coordinates = { x: 1, y: 1 }
136
+ expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
137
+ driver.tap(coordinates: coordinates)
138
+ end
139
+
140
+ it 'verifies press' do
141
+ driver.boot_simulator
142
+ duration = 0.5
143
+ coordinates = { x: 1, y: 1 }
144
+ expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
145
+ driver.press(coordinates: coordinates, duration: duration)
146
+ end
147
+
148
+ it 'verifies swipe' do
149
+ driver.boot_simulator
150
+ duration = 0.5
151
+ start_coordinates = { x: 1, y: 1 }
152
+ end_coordinates = { x: 2, y: 2 }
153
+ expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
154
+ driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
155
+ end
156
+
96
157
  it 'verifies that simulator was not booted' do
97
158
  driver.shutdown_simulator
98
159
  error_message = "Failed to boot #{udid}"
160
+ allow(driver).to receive(:device_info).and_return({ 'state' => 'Unknown' })
99
161
  expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
100
- expect { driver.ensure_simulator_was_booted }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
162
+ expect { driver.boot_simulator }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
101
163
  end
102
164
  end
data/spec/logger_spec.rb CHANGED
@@ -33,4 +33,24 @@ describe Logger do
33
33
  expect { described_class.error(message, payload: payload) }
34
34
  .to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
35
35
  end
36
+
37
+ it 'verifies custom log without payload' do
38
+ color = :blue
39
+ time = Time.now
40
+ allow(Time).to receive(:now).and_return(time)
41
+ expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
42
+ expect do
43
+ described_class.log(message, color: color, payload: nil)
44
+ end.to output("#{expected_output}\n\n").to_stdout
45
+ end
46
+
47
+ it 'verifies custom log with payload' do
48
+ color = :blue
49
+ time = Time.now
50
+ allow(Time).to receive(:now).and_return(time)
51
+ expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
52
+ expect do
53
+ described_class.log(message, color: color, payload: payload)
54
+ end.to output("#{expected_output} #{payload.colorize(:light_green)}\n\n").to_stdout
55
+ end
36
56
  end
@@ -5,7 +5,10 @@ describe Xcmonkey do
5
5
 
6
6
  it 'verifies gestures' do
7
7
  gestures = described_class.new(params).gestures
8
- expect(gestures) =~ [:swipe, :precise_tap, :blind_tap]
8
+ taps = [:precise_tap, :blind_tap] * 10
9
+ swipes = [:precise_swipe, :blind_swipe] * 5
10
+ presses = [:precise_press, :blind_press]
11
+ expect(gestures) =~ presses + taps + swipes
9
12
  end
10
13
 
11
14
  it 'verifies required params' do
@@ -42,5 +45,14 @@ describe Xcmonkey do
42
45
  expect(Logger).to receive(:error).with(duration_error_msg)
43
46
  described_class.new(params)
44
47
  end
48
+
49
+ it 'verifies version' do
50
+ current_version = Gem::Version.new(Xcmonkey::VERSION)
51
+ expect(current_version).to be > Gem::Version.new('0.1.0')
52
+ end
53
+
54
+ it 'verifies gem name' do
55
+ expect(Xcmonkey::GEM_NAME).to eq('xcmonkey')
56
+ end
45
57
  end
46
58
  end
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alteral
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-28 00:00:00.000000000 Z
11
+ date: 2022-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler