run_loop 2.1.2 → 2.1.3

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.
@@ -294,7 +294,11 @@ module RunLoop
294
294
  end
295
295
  else
296
296
  possible = XCODE_5_SDKS.map do |sdk|
297
- File.expand_path("~/Library/Application Support/iPhone Simulator/#{sdk}")
297
+ File.join(RunLoop::Environment.user_home_directory,
298
+ "Library",
299
+ "Application Support",
300
+ "iPhone Simulator",
301
+ sdk)
298
302
  end
299
303
 
300
304
  dirs = (possible + existing).uniq
@@ -646,10 +650,11 @@ module RunLoop
646
650
  # The absolute path to the iPhone Simulator Application Support directory.
647
651
  # @return [String] absolute path
648
652
  def sim_app_support_dir
653
+ home_dir = RunLoop::Environment.user_home_directory
649
654
  if xcode_version_gte_6?
650
- File.expand_path('~/Library/Developer/CoreSimulator/Devices')
655
+ File.join(home_dir, "Library", "Developer", "CoreSimulator", "Devices")
651
656
  else
652
- File.expand_path('~/Library/Application Support/iPhone Simulator')
657
+ File.join(home_dir, "Library", "Application Support", "iPhone Simulator")
653
658
  end
654
659
  end
655
660
 
@@ -1,5 +1,5 @@
1
1
  module RunLoop
2
- VERSION = "2.1.2"
2
+ VERSION = "2.1.3"
3
3
 
4
4
  # A model of a software release version that can be used to compare two versions.
5
5
  #
@@ -3,6 +3,9 @@ module RunLoop
3
3
  # @!visibility private
4
4
  class XCUITest
5
5
 
6
+ require "run_loop/shell"
7
+ include RunLoop::Shell
8
+
6
9
  class HTTPError < RuntimeError; end
7
10
 
8
11
  # @!visibility private
@@ -38,59 +41,64 @@ module RunLoop
38
41
  core_sim.install
39
42
  end
40
43
 
41
- xcuitest = RunLoop::XCUITest.new(bundle_id, device)
44
+ cbx_launcher = XCUITest.detect_cbx_launcher(options, device)
45
+
46
+ xcuitest = RunLoop::XCUITest.new(bundle_id, device, cbx_launcher)
42
47
  xcuitest.launch
43
48
  xcuitest
44
49
  end
45
50
 
46
51
  # @!visibility private
47
- def self.xcodebuild_log_file
48
- path = File.join(XCUITest.dot_dir, "xcodebuild.log")
49
- FileUtils.touch(path) if !File.exist?(path)
50
- path
52
+ #
53
+ # @param [RunLoop::Device] device the device under test
54
+ def self.default_cbx_launcher(device)
55
+ RunLoop::DeviceAgent::XCTestctl.new(device)
56
+ end
57
+
58
+ # @!visibility private
59
+ # @param [Hash] options the options passed by the user
60
+ # @param [RunLoop::Device] device the device under test
61
+ def self.detect_cbx_launcher(options, device)
62
+ value = options[:cbx_launcher]
63
+ if value
64
+ if value == :xcodebuild
65
+ RunLoop::DeviceAgent::Xcodebuild.new(device)
66
+ elsif value == :xctestctl
67
+ RunLoop::DeviceAgent::XCTestctl.new(device)
68
+ else
69
+ raise(ArgumentError,
70
+ "Expected :cbx_launcher => #{value} to be :xcodebuild or :xctestctl")
71
+ end
72
+ else
73
+ XCUITest.default_cbx_launcher(device)
74
+ end
51
75
  end
52
76
 
77
+ attr_reader :bundle_id, :device, :cbx_launcher
78
+
53
79
  # @!visibility private
54
80
  #
55
81
  # The app with `bundle_id` needs to be installed.
56
82
  #
57
83
  # @param [String] bundle_id The identifier of the app under test.
58
84
  # @param [RunLoop::Device] device The device device.
59
- def initialize(bundle_id, device)
85
+ def initialize(bundle_id, device, cbx_launcher)
60
86
  @bundle_id = bundle_id
61
87
  @device = device
88
+ @cbx_launcher = cbx_launcher
62
89
  end
63
90
 
91
+ # @!visibility private
64
92
  def to_s
65
- "#<XCUITest #{url} : #{bundle_id} : #{device}>"
93
+ "#<XCUITest #{url} : #{bundle_id} : #{device} : #{cbx_launcher}>"
66
94
  end
67
95
 
96
+ # @!visibility private
68
97
  def inspect
69
98
  to_s
70
99
  end
71
100
 
72
101
  # @!visibility private
73
- def bundle_id
74
- @bundle_id
75
- end
76
-
77
- # @!visibility private
78
- def device
79
- @device
80
- end
81
-
82
- # @!visibility private
83
- def workspace
84
- @workspace ||= lambda do
85
- path = RunLoop::Environment.send(:cbxws)
86
- if path
87
- path
88
- else
89
- raise "TODO: figure out how to distribute the CBX-Runner"
90
- end
91
- end.call
92
- end
93
-
94
102
  def launch
95
103
  start = Time.now
96
104
  launch_cbx_runner
@@ -123,6 +131,15 @@ module RunLoop
123
131
  launch_aut(bundle_id)
124
132
  end
125
133
 
134
+ # @!visibility private
135
+ def runtime
136
+ options = http_options
137
+ request = request("device")
138
+ client = client(options)
139
+ response = client.get(request)
140
+ expect_200_response(response)
141
+ end
142
+
126
143
  # @!visibility private
127
144
  def query(mark)
128
145
  options = http_options
@@ -134,23 +151,50 @@ module RunLoop
134
151
  end
135
152
 
136
153
  # @!visibility private
137
- def tap_mark(mark)
154
+ def query_for_coordinate(mark)
138
155
  body = query(mark)
139
- tap_query_result(body)
156
+ coordinate_from_query_result(body)
140
157
  end
141
158
 
142
159
  # @!visibility private
143
- def tap_coordinate(x, y)
144
- options = http_options
145
- parameters = {
146
- :gesture => "touch",
160
+ def tap_mark(mark)
161
+ coordinate = query_for_coordinate(mark)
162
+ tap_coordinate(coordinate[:x], coordinate[:y])
163
+ end
164
+
165
+ # @!visibility private
166
+ def tap_coordinate(x, y, options)
167
+ make_coordinate_gesture_request("touch", x, y)
168
+ end
169
+
170
+ # @!visibility private
171
+ def double_tap(x, y)
172
+ make_coordinate_gesture_request("double_tap", x, y)
173
+ end
174
+
175
+ # @!visibiity private
176
+ def coordinate_gesture_parameters(name, x, y, options={})
177
+ {
178
+ :gesture => name,
147
179
  :specifiers => {
148
- :coordinate => {x: x, y: y}
180
+ :coordinate => [x, y]
149
181
  },
150
- :options => {}
182
+ :options => options
151
183
  }
152
- request = request("gesture", parameters)
153
- client(options)
184
+ end
185
+
186
+ # @!visibility private
187
+ def coordinate_gesture_request(name, x, y, gesture_options={})
188
+ parameters = coordinate_gesture_parameters(name, x, y, gesture_options)
189
+ request("gesture", parameters)
190
+ end
191
+
192
+ # @!visibility private
193
+ def make_coordinate_gesture_request(name, x, y, options={})
194
+ gesture_options = options.fetch(:gesture_options, {})
195
+ http_options = options.fetch(:http_options, http_options())
196
+ request = coordinate_gesture_request(name, x, y, gesture_options)
197
+ client = client(http_options)
154
198
  response = client.post(request)
155
199
  expect_200_response(response)
156
200
  end
@@ -162,7 +206,7 @@ module RunLoop
162
206
  :orientation => orientation
163
207
  }
164
208
  request = request("rotate_home_button_to", parameters)
165
- client(http_options)
209
+ client = client(http_options)
166
210
  response = client.post(request)
167
211
  json = expect_200_response(response)
168
212
  sleep(sleep_for)
@@ -179,23 +223,46 @@ module RunLoop
179
223
  :options => options
180
224
  }
181
225
 
226
+ RunLoop.log_debug(%Q[
227
+ Sending request to perform '#{gesture}' with:
228
+
229
+ #{JSON.pretty_generate(parameters)}
230
+
231
+ ])
182
232
  request = request("gesture", parameters)
183
- client(http_options)
233
+ client = client(http_options)
184
234
  response = client.post(request)
185
235
  expect_200_response(response)
186
236
  end
187
237
 
188
238
  # @!visibility private
189
- def tap_query_result(hash)
190
- rect = hash["rect"]
239
+ def coordinate_from_query_result(hash)
240
+ matches = hash["result"]
241
+
242
+ if matches.nil? || matches.empty?
243
+ raise "Expected #{hash} to contain some results"
244
+ end
245
+
246
+ rect = matches.first["rect"]
191
247
  h = rect["height"]
192
248
  w = rect["width"]
193
249
  x = rect["x"]
194
250
  y = rect["y"]
195
251
 
196
- touchx = x + (h/2)
197
- touchy = y + (w/2)
198
- tap_coordinate(touchx, touchy)
252
+ touchx = x + (w/2.0)
253
+ touchy = y + (h/2.0)
254
+
255
+ new_rect = rect.dup
256
+ new_rect[:center_x] = touchx
257
+ new_rect[:center_y] = touchy
258
+
259
+ RunLoop.log_debug(%Q[Rect from query:
260
+
261
+ #{JSON.pretty_generate(new_rect)}
262
+
263
+ ])
264
+ {:x => touchx,
265
+ :y => touchy}
199
266
  end
200
267
 
201
268
  private
@@ -223,7 +290,7 @@ module RunLoop
223
290
  :undef => :replace, # Replace anything not defined in ASCII
224
291
  :replace => "" # Use a blank for those replacements
225
292
  }
226
- encoded = device_name.encode(Encoding.find("ASCII"), encoding_options)
293
+ encoded = device_name.encode(::Encoding.find("ASCII"), encoding_options)
227
294
  "http://#{encoded}.local:27753/"
228
295
  end
229
296
  end
@@ -296,14 +363,6 @@ module RunLoop
296
363
  response = client.post(request)
297
364
  body = response.body
298
365
  RunLoop.log_debug("CBX-Runner says, \"#{body}\"")
299
- 5.times do
300
- begin
301
- health
302
- sleep(1.0)
303
- rescue => _
304
- break
305
- end
306
- end
307
366
  body
308
367
  rescue => e
309
368
  RunLoop.log_debug("CBX-Runner shutdown error: #{e}")
@@ -323,65 +382,22 @@ module RunLoop
323
382
  body
324
383
  end
325
384
 
326
- # @!visibility private
327
- def xcodebuild
328
- env = {
329
- "COMMAND_LINE_BUILD" => "1"
330
- }
331
-
332
- args = [
333
- "xcrun",
334
- "xcodebuild",
335
- "-scheme", "CBXAppStub",
336
- "-workspace", workspace,
337
- "-config", "Debug",
338
- "-destination",
339
- "id=#{device.udid}",
340
- "clean",
341
- "test"
342
- ]
343
-
344
- log_file = XCUITest.xcodebuild_log_file
345
-
346
- options = {
347
- :out => log_file,
348
- :err => log_file
349
- }
350
-
351
- command = "#{env.map.each { |k, v| "#{k}=#{v}" }.join(" ")} #{args.join(" ")}"
352
- RunLoop.log_unix_cmd("#{command} >& #{log_file}")
353
-
354
- pid = Process.spawn(env, *args, options)
355
- Process.detach(pid)
356
- pid
357
- end
358
-
359
385
  # @!visibility private
360
386
  def launch_cbx_runner
361
- # Fail fast if CBXWS is not defined.
362
- # WIP - we will distribute the workspace somehow.
363
- workspace
364
-
365
387
  shutdown
366
388
 
367
- # Temp measure; we need to manage the xcodebuild pids.
368
- system("pkill xcodebuild")
369
-
370
- if device.simulator?
371
- # quits the simulator
372
- sim = CoreSimulator.new(device, "")
373
- sim.launch_simulator
374
- else
375
- # anything special about physical devices?
376
- end
389
+ options = {:log_cmd => true}
390
+ exec(["pkill", "xctestctl"], options)
391
+ exec(["pkill", "testmanagerd"], options)
392
+ exec(["pkill", "xcodebuild"], options)
377
393
 
378
394
  start = Time.now
379
- pid = xcodebuild
380
- RunLoop.log_debug("Waiting for CBX-Runner to build...")
395
+ RunLoop.log_debug("Waiting for CBX-Runner to launch...")
396
+ pid = cbx_launcher.launch
381
397
  health
398
+ RunLoop.log_debug("Took #{Time.now - start} launch and respond to /health")
382
399
 
383
- RunLoop.log_debug("Took #{Time.now - start} seconds to build and launch")
384
- pid.to_i
400
+ pid
385
401
  end
386
402
 
387
403
  # @!visibility private
@@ -423,26 +439,28 @@ Something went wrong.
423
439
  # @!visibility private
424
440
  def expect_200_response(response)
425
441
  body = response_body_to_hash(response)
426
- return body if response.status_code < 300
442
+ if response.status_code < 300 && !body["error"]
443
+ return body
444
+ end
427
445
 
428
- raise RunLoop::XCUITest::HTTPError,
429
- %Q[Expected status code < 200, found #{response.status_code}.
446
+ if response.status_code > 300
447
+ raise RunLoop::XCUITest::HTTPError,
448
+ %Q[Expected status code < 300, found #{response.status_code}.
430
449
 
431
450
  Server replied with:
432
451
 
433
452
  #{body}
453
+
434
454
  ]
435
- end
455
+ else
456
+ raise RunLoop::XCUITest::HTTPError,
457
+ %Q[Expected JSON response with no error, but found
436
458
 
437
- # @!visibility private
438
- def self.dot_dir
439
- path = File.join(RunLoop::DotDir.directory, "xcuitest")
459
+ #{body["error"]}
440
460
 
441
- if !File.exist?(path)
442
- FileUtils.mkdir_p(path)
443
- end
461
+ ]
444
462
 
445
- path
463
+ end
446
464
  end
447
465
 
448
466
  # @!visibility private
@@ -17,7 +17,7 @@ var Log = (function () {
17
17
 
18
18
  return {
19
19
  result: function (status, data) {
20
- log_json({"status": status, "value": data, "index":_actualIndex})
20
+ log_json({"status": status, "value": data, "index":_actualIndex});
21
21
  },
22
22
  output: function (msg) {
23
23
  log_json({"output": msg,"last_index":_actualIndex});
@@ -5,9 +5,9 @@ function findAlertTitle(alert) {
5
5
  var title = alert.name();
6
6
  var staticTexts;
7
7
 
8
- if (title == null) {
8
+ if (title === null) {
9
9
  staticTexts = alert.staticTexts();
10
- if (staticTexts != null && staticTexts.length > 0) {
10
+ if (staticTexts !== null && staticTexts.length > 0) {
11
11
 
12
12
  title = staticText[0].name();
13
13
  }
@@ -49,31 +49,85 @@ function englishLocalizations() {
49
49
 
50
50
  function danishLocalizations() {
51
51
  return [
52
- // Location
53
52
  ["Tillad", /bruge din lokalitet, når du bruger appen/],
54
53
  ["Tillad", /også når du ikke bruger appen/],
55
- ["OK", /vil bruge din aktuelle placering/]
54
+ ["OK", /vil bruge din aktuelle placering/],
55
+ ["OK", /vil bruge dine kontakter/],
56
+ ["OK", /vil bruge mikrofonen/],
57
+ ["OK", /vil bruge din kalender/],
58
+ ["OK", /vil bruge dine påmindelser/],
59
+ ["OK", /vil bruge dine fotos/],
60
+ ["OK", /ønsker adgang til Twitter-konti/],
61
+ ["OK", /vil bruge din fysiske aktivitet og din træningsaktivitet/],
62
+ ["OK", /vil bruge kameraet/],
63
+ ["OK", /vil gerne sende dig meddelelser/]
56
64
  ];
57
65
  }
58
66
 
59
- function spanishLocalizations() {
67
+ function euSpanishLocalizations() {
68
+ return [
69
+ ["Permitir", /acceder a tu ubicación mientras utilizas la aplicación/],
70
+ ["Permitir", /acceder a tu ubicación aunque no estés utilizando la aplicación/],
71
+ ["OK", /quiere acceder a tus contactos/],
72
+ ["OK", /quiere acceder a tu calendario/],
73
+ ["OK", /quiere acceder a tus recordatorios/],
74
+ ["OK", /quiere acceder a tus fotos/],
75
+ ["OK", /quiere obtener acceso a cuentas Twitter/],
76
+ ["OK", /quiere acceder al micrófono/],
77
+ ["OK", /desea acceder a tu actividad física y deportiva/],
78
+ ["OK", /quiere acceder a la cámara/],
79
+ ["OK", /quiere enviarte notificaciones/]
80
+ ];
81
+ }
82
+
83
+ function es419SpanishLocalizations() {
60
84
  return [
61
- // APNS
62
- ["OK", /enviarle notificaiones/]
85
+ ["Permitir", /acceda a tu ubicación mientras la app está en uso/],
86
+ ["Permitir", /acceda a tu ubicación incluso cuando la app no está en uso/],
87
+ ["OK", /quiere acceder a tu condición y actividad física/]
63
88
  ];
64
89
  }
65
90
 
91
+ function spanishLocalizations() {
92
+ return [].concat(
93
+ euSpanishLocalizations(),
94
+ es419SpanishLocalizations()
95
+ );
96
+ }
97
+
66
98
  function germanLocalizations() {
67
99
  return [
68
- // Location
69
- ["Ja", /Darf (?:.)+ Ihren aktuellen Ort verwenden/]
100
+ ["Ja", /Darf (?:.)+ Ihren aktuellen Ort verwenden/],
101
+ ["Erlauben", /auf Ihren Standort zugreifen, wenn Sie die App benutzen/],
102
+ ["Erlauben", /auch auf Ihren Standort zugreifen, wenn Sie die App nicht benutzen/],
103
+ ["Erlauben", /auf Ihren Standort zugreifen, selbst wenn Sie die App nicht benutzen/],
104
+ ["Ja", /auf Ihre Kontakte zugreifen/],
105
+ ["Ja", /auf Ihren Kalender zugreifen/],
106
+ ["Ja", /auf Ihre Erinnerungen zugreifen/],
107
+ ["Ja", /auf Ihre Fotos zugreifen/],
108
+ ["Erlauben", /möchte auf Twitter-Accounts zugreifen/],
109
+ ["Ja", /auf das Mikrofon zugreifen/],
110
+ ["Ja", /möchte auf Ihre Bewegungs- und Fitnessdaten zugreifen/],
111
+ ["Ja", /auf Ihre Kamera zugreifen/],
112
+ ["OK", /Ihnen Mitteilungen senden/]
70
113
  ];
71
114
  }
72
115
 
73
116
  function dutchLocalizations() {
74
117
  return [
75
- // APNS
76
- ["OK", /wil u berichten stuern/]
118
+ ["Sta toe", /tot uw locatie toestaan terwijl u de app gebruikt/],
119
+ ["Sta toe", /toegang tot uw locatie toestaan terwijl u de app gebruikt/],
120
+ ["Sta toe", /ook toegang tot uw locatie toestaan, zelfs als u de app niet gebruikt/],
121
+ ["Sta toe", /toegang tot uw locatie toestaan, zelfs als u de app niet gebruikt/],
122
+ ["OK", /wil toegang tot uw contacten/],
123
+ ["OK", /wil toegang tot uw agenda/],
124
+ ["OK", /wil toegang tot uw herinneringen/],
125
+ ["OK", /wil toegang tot uw foto's/],
126
+ ["OK", /wil toegang tot Twitter-accounts/],
127
+ ["OK", /wil toegang tot de microfoon/],
128
+ ["OK", /wil toegang tot uw bewegings- en fitnessactiviteit/],
129
+ ["OK", /wil toegang tot de camera/],
130
+ ["OK", /wil u berichten sturen/]
77
131
  ];
78
132
  }
79
133
 
@@ -89,13 +143,15 @@ function frenchLocalizations() {
89
143
  ["OK", /vous envoyer des notifications/],
90
144
  ["Autoriser", /à accéder à vos données de localisation lorsque vous utilisez l’app/],
91
145
  ["Autoriser", /à accéder à vos données de localisation même lorsque vous n’utilisez pas l’app/],
146
+ ["Autoriser", /à accéder aussi à vos données de localisation lorsque vous n’utilisez pas l’app/],
92
147
  ["OK", /souhaite accéder à vos contacts/],
93
148
  ["OK", /souhaite accéder à votre calendrier/],
94
149
  ["OK", /souhaite accéder à vos rappels/],
95
150
  ["OK", /souhaite accéder à vos mouvements et vos activités physiques/],
96
151
  ["OK", /souhaite accéder à vos photos/],
97
152
  ["OK", /souhaite accéder à l’appareil photo/],
98
- ["OK", /souhaite accéder aux comptes Twitter/]
153
+ ["OK", /souhaite accéder aux comptes Twitter/],
154
+ ["OK", /souhaite accéder au micro/]
99
155
  ];
100
156
  }
101
157
 
@@ -117,13 +173,42 @@ function isPrivacyAlert(alert) {
117
173
 
118
174
  var title = findAlertTitle(alert);
119
175
 
120
- // Comment this out if you are capturing regexes. See comment below.
121
- Log.output({"output":"alert: " + title}, true);
176
+ // Comment this out when capturing new regexes. See the comment below.
177
+ Log.output({"alert":title});
122
178
 
123
179
  // When debugging or trying to capture the regexes for a new
124
- // localization, uncomment these lines and comment out the line above.
125
- // var buttonNames = findAlertButtonNames(alert);
126
- // Log.output({"output":"alert: " + title + "," + buttonNames}, true);
180
+ // localization, uncomment the logging below.
181
+ //
182
+ // Notes:
183
+ // * Microphone alerts only appear on physical devices.
184
+ // * You have to click through the Health alert; it is completely blocking -
185
+ // tests will not proceed.
186
+ // * Generating bluetooth alerts is NYI.
187
+ // * In general, there will be a different alert on device for using location
188
+ // in background mode.
189
+ // * Alerts vary by iOS.
190
+ // * To reset notifications on devices:
191
+ // General > Settings > Reset > Reset Location + Privacy
192
+ // - These are the last table rows
193
+ // - APNS permissions are reset once a day.
194
+ // * On devices, set the device language in Settings.
195
+ //
196
+ // Use the Permission.app.
197
+ //
198
+ // * Alert text is printed at the end of every test.
199
+ // * Alert text is written to a file in Permissions/tmp.
200
+ //
201
+ // Examples:
202
+ //
203
+ // # Simulator running Mexican Spanish
204
+ // $ APP_LANG="es-MX" APP_LOCALE="es_MX" be cucumber -t @supported
205
+ //
206
+ // # Device running Dutch
207
+ // $ APP_LANG="nl" APP_LOCALE="nl" be cucumber -t @supported -p macmini
208
+
209
+ // This is very slow, so only do this if you are trying to capture regexes.
210
+ //var buttonNames = findAlertButtonNames(alert);
211
+ //Log.output({"alert":{"title":title, "buttons":buttonNames, "capture":"YES"}});
127
212
 
128
213
  var answer;
129
214
  var expression;