testautoi 0.9.135 → 0.9.142

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
1
  pkg
2
2
  Markdown.pl
3
3
  .idea
4
- Gemfile.lock
4
+ Gemfile.lock
5
+ staticlib
6
+ libcalabash
data/Rakefile CHANGED
@@ -1,2 +1,62 @@
1
1
  require 'bundler'
2
+ require 'fileutils'
3
+
2
4
  Bundler::GemHelper.install_tasks
5
+
6
+
7
+ task :build_server do
8
+
9
+ FRAMEWORK='calabash.framework'
10
+ ZIP_FILE="#{FRAMEWORK}.zip"
11
+
12
+ def build_server
13
+ return if ENV['SKIP_SERVER']
14
+ framework_zip = nil
15
+ dir = ENV['CALABASH_SERVER_PATH'] || File.join('..', '..', 'calabash-ios-server')
16
+ unless File.exist?(dir)
17
+ raise <<EOF
18
+ Unable to find calabash server checked out at #{dir}.
19
+ Please checkout as #{dir} or set CALABASH_SERVER_PATH to point
20
+ to Calabash server (branch 0.9.x).
21
+ EOF
22
+ end
23
+
24
+ FileUtils.cd(dir) do
25
+ puts 'Building Server'
26
+ cmd = 'xcodebuild build -project calabash.xcodeproj -target Framework -configuration Debug -sdk iphonesimulator6.1'
27
+ puts cmd
28
+ puts `#{cmd}`
29
+
30
+ unless File.exist?(FRAMEWORK)
31
+ raise 'Unable to build framework'
32
+ end
33
+
34
+ puts "Zipping down framework"
35
+
36
+
37
+ zip_cmd = "zip -q -r #{ZIP_FILE} #{FRAMEWORK}"
38
+ puts zip_cmd
39
+ puts `#{zip_cmd}`
40
+ framework_zip = File.expand_path(ZIP_FILE)
41
+ unless File.exist?(framework_zip)
42
+ raise 'Unable to zip down framework...'
43
+ end
44
+ end
45
+
46
+
47
+
48
+ FileUtils.mkdir_p('staticlib')
49
+ output_path = File.join('staticlib', ZIP_FILE)
50
+ FileUtils.mv(framework_zip,output_path, :force => true)
51
+ puts "Server built to path #{output_path}"
52
+
53
+ end
54
+
55
+ build_server
56
+
57
+ end
58
+
59
+ task :build => [:build_server]
60
+ task :install => [:build_server]
61
+ task :release => [:build_server]
62
+
data/bin/calabash-ios CHANGED
@@ -15,6 +15,7 @@ require File.join(File.dirname(__FILE__),"calabash-ios-build")
15
15
  @features_dir = File.join(FileUtils.pwd, "features")
16
16
  @source_dir = File.join(File.dirname(__FILE__), '..', 'features-skeleton')
17
17
  @script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
18
+ @framework_dir = File.join(File.dirname(__FILE__), '..', 'staticlib')
18
19
 
19
20
  if (ARGV.length == 0)
20
21
  print_usage
@@ -33,6 +34,9 @@ elsif cmd == 'console'
33
34
  elsif cmd == 'build'
34
35
  build
35
36
  exit 0
37
+ elsif cmd == 'update'
38
+ update(ARGV)
39
+ exit 0
36
40
  elsif cmd == 'run'
37
41
  run
38
42
  exit 0
@@ -1,6 +1,7 @@
1
1
  require 'tempfile'
2
2
  require 'json'
3
3
 
4
+ UPDATE_TARGETS = ['hooks']
4
5
 
5
6
  def msg(title, &block)
6
7
  puts "\n" + "-"*10 + title + "-"*10
@@ -21,8 +22,10 @@ def print_usage
21
22
  starts an interactive console to interact with your app via Calabash
22
23
  setup [<path>]
23
24
  setup your XCode project for calabash-ios (EXPERIMENTAL)
25
+ update [target]
26
+ updates one of the following targets: hooks
24
27
  download
25
- downloads latest compatible version of calabash.framework
28
+ install latest compatible version of calabash.framework
26
29
  check [{<path to .ipa>|<path to .app>}]
27
30
  check whether an app or ipa is linked with calabash.framework (EXPERIMENTAL)
28
31
  sim locale <lang> [<region>]
@@ -94,47 +94,9 @@ def download_calabash(project_path)
94
94
  ##Download calabash.framework
95
95
  if not File.directory?(File.join(project_path, file))
96
96
  msg("Info") do
97
- zip_file = "calabash.framework-#{ENV['FRAMEWORK_VERSION']||Calabash::Cucumber::FRAMEWORK_VERSION}.zip"
98
- puts "Did not find calabash.framework. I'll download it...'"
99
- puts "http://cloud.github.com/downloads/calabash/calabash-ios/#{zip_file}"
100
- require 'uri'
101
-
102
- uri = URI.parse "http://cloud.github.com/downloads/calabash/calabash-ios/#{zip_file}"
103
- success = false
104
- if has_proxy?
105
- proxy_url = proxy
106
- connection = Net::HTTP::Proxy(proxy_url[0], proxy_url[1])
107
- else
108
- connection = Net::HTTP
109
- end
110
- begin
111
- connection.start(uri.host, uri.port) do |http|
112
- request = Net::HTTP::Get.new uri.request_uri
113
-
114
- http.request request do |response|
115
- if response.code == '200'
116
- open zip_file, 'wb' do |io|
117
- response.read_body do |chunk|
118
- print "."
119
- io.write chunk
120
- end
121
- end
122
- success = true
123
- else
124
- puts "Got bad response code #{response.code}."
125
- puts "Aborting..."
126
- end
127
- end
128
- end
129
- rescue SocketError => e
130
- msg("Error") do
131
- puts "Exception: #{e}"
132
- puts "Unable to download Calabash. Please check connection."
133
- end
134
- exit 1
135
- end
136
- if success
137
- puts "\nDownload done: #{file}. Unzipping..."
97
+ zip_file = File.join(@framework_dir,"calabash.framework.zip")
98
+
99
+ if File.exist?(zip_file)
138
100
  if not system("unzip -C -K -o -q -d #{project_path} #{zip_file} -x __MACOSX/* calabash.framework/.DS_Store")
139
101
  msg("Error") do
140
102
  puts "Unable to unzip file: #{zip_file}"
@@ -142,8 +104,8 @@ def download_calabash(project_path)
142
104
  end
143
105
  exit 1
144
106
  end
145
- FileUtils.rm(zip_file)
146
107
  else
108
+ puts "Inconsistent gem state: Cannot find framework: #{zip_file}"
147
109
  exit 0
148
110
  end
149
111
  end
@@ -328,4 +290,50 @@ def validate_app(app)
328
290
  end
329
291
  end
330
292
 
293
+ end
294
+
295
+
296
+ def update(args)
297
+ if args.length > 0
298
+ target = args[0]
299
+ unless UPDATE_TARGETS.include?(target)
300
+ msg("Error") do
301
+ puts "Invalid target #{target}. Must be one of: #{UPDATE_TARGETS.join(' ')}"
302
+ end
303
+ exit 1
304
+ end
305
+
306
+
307
+
308
+ target_file = "features/support/launch.rb"
309
+ msg("Question") do
310
+ puts "I'm about to update the #{target_file} file."
311
+ puts "Please hit return to confirm that's what you want."
312
+ end
313
+ exit 2 unless STDIN.gets.chomp == ''
314
+
315
+
316
+ unless File.exist?(target_file)
317
+ msg("Error") do
318
+ puts "Unable to find file #{target_file}"
319
+ puts "Please change directory so that #{target_file} exists."
320
+ end
321
+ exit 1
322
+ end
323
+ new_launch_script = File.join(@script_dir,"launch.rb")
324
+
325
+ FileUtils.cp(new_launch_script, target_file, :verbose => true)
326
+
327
+ msg("Info") do
328
+ puts "File copied.\n"
329
+ puts "Launch on device using environment variable DEVICE_TARGET=device."
330
+ end
331
+ else
332
+ msg("Error") do
333
+ puts "update must take one of the following targets: #{UPDATE_TARGETS.join(' ')}"
334
+ end
335
+ exit 1
336
+
337
+ end
338
+
331
339
  end
data/bin/testautoi CHANGED
@@ -8,6 +8,8 @@ require 'uri'
8
8
 
9
9
  require File.join(File.dirname(__FILE__),"calabash-ios-sim")
10
10
 
11
+ @script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
12
+
11
13
  @settings_file = File.join(FileUtils.pwd, ".testautoi_settings")
12
14
 
13
15
  def print_usage
@@ -76,7 +78,7 @@ def print_usage
76
78
  Stop recording
77
79
  version
78
80
  prints the gem version
79
-
81
+
80
82
  EOF
81
83
  end
82
84
 
@@ -113,6 +115,8 @@ def run(option)
113
115
  env["APP_BUNDLE_PATH"] = File.join(FileUtils.pwd, app_bundle)
114
116
  if device_udid == ''
115
117
  # Use a Simulator
118
+ #Calabash::Cucumber::SimulatorHelper.relaunch(env["APP_BUNDLE_PATH"], @settings["sim_version"], @settings["sim_device"])
119
+ #calabash_sim_location(['on', app_bundle_id])
116
120
  if option == 'console'
117
121
  Calabash::Cucumber::SimulatorHelper.relaunch(env["APP_BUNDLE_PATH"], @settings["sim_version"], @settings["sim_device"])
118
122
  else
@@ -122,9 +126,11 @@ def run(option)
122
126
  else
123
127
  env["NO_LAUNCH"] = "1"
124
128
  get_device_ip
125
- if ENV["NO_INSTALL"] != "1"
129
+ if ENV["CLEAN_INSTALL"] == "1"
126
130
  uninstall_app(app_bundle_id)
127
131
  install_app(File.join(FileUtils.pwd, app_bundle))
132
+ elsif ENV["NO_INSTALL"] != "1"
133
+ install_app(File.join(FileUtils.pwd, app_bundle))
128
134
  end
129
135
  launch_app(app_url)
130
136
  sleep(5)
@@ -426,6 +432,14 @@ def uninstall_app(app_bundle_id)
426
432
  raise "Failed to install the app" if result == false
427
433
  end
428
434
 
435
+ def tracetemplate
436
+ "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate"
437
+ end
438
+
439
+ def simulator_path
440
+ "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\\ Simulator.app/Contents/MacOS/iPhone\\ Simulator"
441
+ end
442
+
429
443
  def launch_app(app_url)
430
444
  udid = @settings["device_udid"].to_s
431
445
  udids = Device.detect
@@ -435,13 +449,12 @@ def launch_app(app_url)
435
449
  udid = udids.first
436
450
  end
437
451
  raise "The device #{udid} is not found." if not udids.include?(udid)
438
- tracetempl = "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate"
439
452
  launcher = "./AppLaunch.app"
440
453
  script = 'LaunchApp.js'
441
454
  text = File.read(File.join(File.dirname(__FILE__), script))
442
455
  File.open(File.join(FileUtils.pwd, script), "w") {|file| file.puts text.gsub(/\[%app%\]/, app_url)}
443
456
  script = File.join(FileUtils.pwd, script)
444
- cmd = `instruments -w #{udid} -t #{tracetempl} #{launcher_path} -e UIASCRIPT #{script}`
457
+ cmd = `instruments -w #{udid} -t #{tracetemplate} #{launcher_path} -e UIASCRIPT #{script}`
445
458
  end
446
459
 
447
460
  def get_device_ip
@@ -454,20 +467,19 @@ def get_device_ip
454
467
  udid = udids.first
455
468
  end
456
469
  raise "The device #{udid} is not found." if not udids.include?(udid)
457
- tracetempl = "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate"
458
470
  launcher = "./AppLaunch.app"
459
471
  script = 'GetIPAddress.js'
460
472
  text = File.read(File.join(File.dirname(__FILE__), script))
461
473
  File.open(File.join(FileUtils.pwd, script), "w") {|file| file.puts text}
462
474
  script = File.join(FileUtils.pwd, script)
463
475
  File.open(File.join(FileUtils.pwd, "Device.cfg"), "w") {|file| file.puts "IPAddress="}
464
- cmd = `instruments -w #{udid} -t #{tracetempl} #{launcher_path} -e UIASCRIPT #{script}`
476
+ cmd = `instruments -w #{udid} -t #{tracetemplate} #{launcher_path} -e UIASCRIPT #{script}`
465
477
  puts File.read(File.join(FileUtils.pwd, "Device.cfg"))
466
478
  end
467
479
 
468
480
  def start_sim
469
481
  quit_sim
470
- pid = spawn("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\\ Simulator.app/Contents/MacOS/iPhone\\ Simulator")
482
+ pid = spawn(simulator_path)
471
483
  end
472
484
 
473
485
  def reset_sim
@@ -11,9 +11,9 @@ Gem::Specification.new do |s|
11
11
  s.homepage = "http://calaba.sh"
12
12
  s.summary = %q{Client for calabash-ios-server for automated functional testing on iOS}
13
13
  s.description = %q{calabash-cucumber drives tests for native iOS apps. You must link your app with calabash-ios-server framework to execute tests.}
14
- s.files = `git ls-files`.split("\n")
14
+ s.files = `git ls-files`.split("\n").concat(["staticlib/calabash.framework.zip"])
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
- s.executables = ["calabash-ios", "testautoi"]
16
+ s.executables = "calabash-ios"
17
17
  s.require_paths = ["lib"]
18
18
 
19
19
  s.add_dependency( "cucumber" )
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.add_dependency( "location-one", "~>0.0.9")
25
25
  s.add_dependency( "httpclient","2.3.2")
26
26
  s.add_dependency( "bundler", "~> 1.1")
27
- s.add_dependency( "run_loop", "0.0.5" )
27
+ s.add_dependency( "run_loop", "0.0.7" )
28
28
  s.add_dependency( "awesome_print")
29
29
 
30
30
  end
@@ -8,7 +8,10 @@ Usage: calabash-ios <command-name> [parameters]
8
8
  sim locale [lang] [regional]?
9
9
  sim reset
10
10
  sim acc
11
- sim device [iPad_Retina, iPhone, iPhone_Retina, iPhone_Retina_4inch]
11
+ sim device {iPad,iPad_Retina,iPhone,iPhone_Retina,iPhone_Retina_4inch}
12
+ sim location {on|off} <bundleid>
13
+
14
+
12
15
 
13
16
  Commands:
14
17
  gen creates a skeleton features dir. This is usually used once when
@@ -16,8 +19,8 @@ Usage: calabash-ios <command-name> [parameters]
16
19
  the right step definitions and environment to run with cucumber.
17
20
  console
18
21
  starts an interactive console to interact with your app via Calabash.
19
- Copies the irb_ios5.sh and irb_ios4.sh scripts and the .irbrc file
20
- then launches irb_ios5.sh.
22
+ Supports setting environment var CALABASH_IRBRC for custom .irbrc file.
23
+
21
24
  setup [path]? (EXPERIMENTAL) Automates setting up your iOS Xcode project
22
25
  with calabash-ios-server. It is your responsibility to ensure
23
26
  that your production build does not link with calabash.framework.
@@ -41,18 +44,24 @@ Usage: calabash-ios <command-name> [parameters]
41
44
  If something goes wrong. Close Xcode and copy project.pbxproj.bak
42
45
  to project.pbxproj inside your .xcodeproj folder.
43
46
 
47
+ update [target]
48
+ updates one of the following targets: hooks
49
+
44
50
  download [opt_path]?
45
- downloads latest compatible version of calabash.framework.
51
+ copies current compatible version of calabash.framework to your project.
46
52
  It should be run from a directory containing an Xcode project,
47
53
  or optionally opt_path should be supplied and pointing to a
48
54
  directory containing an Xcode project.
49
- Download will download the latest version that matches the
55
+ Will copy in the latest version that matches the
50
56
  currently installed calabash-cucumber gem.
51
57
  To update Calabash for your project run
52
58
 
53
- gem update calabash-cucumber
59
+ gem install calabash-cucumber
54
60
  calabash-ios download
55
61
 
62
+ Then clean and rebuild to your project.
63
+ Check the current server version on http://localhost:37265/version
64
+
56
65
  check (EXPERIMENTAL) [.app or .ipa]?
57
66
  check whether an app or ipa is linked with calabash.framework
58
67
  if called without parameter [.app or .ipa] then pwd should be
@@ -66,8 +75,11 @@ Usage: calabash-ios <command-name> [parameters]
66
75
  for the optional regional parameter, for example,
67
76
  da_DK, en_US.
68
77
 
78
+ sim location {on|off} <bundleid>
79
+ set allow location on/off for current project or bundleid
80
+
69
81
  sim reset (EXPERIMENTAL) Will select "Reset Content and Settings"
70
82
  in the iOS Simulators using AppleScript.
71
83
 
72
84
  sim device [device] Will set the default iOS Simulator device.
73
- [device] can be one of iPad, iPhone, iPhone_Retina.
85
+ [device] can be one of iPad iPad_Retina iPhone iPhone_Retina iPhone_Retina_4inch
@@ -0,0 +1,250 @@
1
+ # Cross-platform Acceptance Testing Best Practices
2
+
3
+ Test automation is programming - hence, well-established practices of programming apply to test automation. Ruby is object-oriented, and most Calabash tests should also follow good object-oriented design (e.g., principles of abstraction, separation of concerns, modularity, reuse...).
4
+
5
+ A well-established pattern in test engineering is the *Page-Object Pattern* (POP). While originating in web testing, the ideas of POP apply equally well to native mobile. In this short article, we'll illustrate how to use the page object pattern to better architect test code and obtain better cross-platform code reuse.
6
+
7
+ We've created a sample project: [X-Platform-Example](https://github.com/calabash/x-platform-example) using the open source WordPress app. If you want to follow along and try things out, go to the project page [https://github.com/calabash/x-platform-example](https://github.com/calabash/x-platform-example) and follow install and run instructions. You can also choose to just read about the principles in this article and try to implement them in you own app.
8
+
9
+ LessPainful also provides both on-site and public training courses where we teach the use of Calabash as well as cross-platform and automated testing best practices. If you're interested in a two-day, hands-on course, please contact us at [contact@lesspainful.com](mailto:contact@lesspainful.com).
10
+
11
+ # Page Objects
12
+
13
+ A *page object* is an object that represents a single screen (page) in your application. For mobile, "screen object" would possibly be a better word, but Page Object is an established term that we'll stick with.
14
+
15
+ A page object should abstract a single screen in your application. It should expose methods to query the state and data of the screen as well as methods to take actions on the screen.
16
+
17
+ As a trivial example, a "login screen" consisting of username and password text fields and a "Login" button could expose a method `login(user,pass)` method that would abstract the details of entering username, password, touching the "Login" button, as well as 'transitioning' to another page (after login). A screen with a list of talks for a conference could expose a `talks()` method to return the visible talks and perhaps a `details(talk)` method to navigate to details for a particular talk.
18
+
19
+ The most obvious benefit of this is abstraction and reuse. If you have several steps needing to navigate to details, the code for `details(talk)` is reused. Also, callers of this method need not worry about the details (e.g. query and touch) or navigating to this screen.
20
+
21
+ # Cross-platform Core Idea
22
+
23
+
24
+ Let's go into more detail with this last example. Consider the following sketch of a class (don't do it exactly like this - read on a bit):
25
+
26
+ ```ruby
27
+
28
+ class TalksScreen
29
+ def talks
30
+ # query all talks...
31
+ end
32
+
33
+ def details(talk)
34
+ #touch talk…
35
+ end
36
+ end
37
+ ```
38
+
39
+
40
+ Suppose you're building the same app for iPhone and Android phones. Most likely the interface of the `TalksScreen` class makes complete sense on both platforms. This means that the calling code, which is usually in a step definition, is independent of platform - hence it can be reused across platforms.
41
+
42
+ Working this way gets you complete reuse of Cucumber features as well as step definitions: the details of interacting with the screen is pushed to page object implementations.
43
+
44
+ The idea is that you provide an implementation of page objects for each platform you need to support (e.g. iPhone, iPad, Android phone, Android tablet, mobile web, desktop web,…).
45
+
46
+ # Cross-platform in practice
47
+
48
+ So… The idea and design looks good. The question now is how to implement this is practice. Here we describe the mechanics and below you'll find an example extracted from [X-Platform-Example](https://github.com/calabash/x-platform-example).
49
+
50
+ There are a couple of ingredients we need.
51
+
52
+ 1. For each screen you want to automate, decide on an interface for a page object class (e.g. like `TalksScreen` above).
53
+ 2. Use only custom steps, and in each step definition *only* use page objects and their methods (no direct calls to Calabash iOS or Calabash Android APIs).
54
+ 3. For each supported platform, define a class with implementations of the page-object methods.
55
+ 4. Create a Cucumber profile (`config/cucumber.yml`). Define a profile for each platform (e.g. `android` and `ios`), and ensure that the profile *only* loads the page object classes for the platform.
56
+ 5. Rejoice and profit!
57
+
58
+ Let's see what these steps look like in a concrete example on the [X-Platform-Example](https://github.com/calabash/x-platform-example).
59
+
60
+ # Example
61
+
62
+ ## Step 1 - Interface
63
+ For the wordpress app, let's focus on the Login/Add-WordPress Blog screen. This method has a single method: `login(user)` which takes a hash `{:email => 'username', :password => 'somepass'}` representing a user:
64
+
65
+ ```ruby
66
+
67
+ def login(user)
68
+ #…
69
+ end
70
+
71
+ def assert_invalid_login_message()
72
+ #…
73
+ end
74
+ ```
75
+
76
+ For this simple screen the interface consists of just these two methods.
77
+
78
+ ## Step 2 - Step definitions
79
+
80
+ We have a feature alà
81
+
82
+ Scenario: Invalid login to WordPress.com blog
83
+ Given I am about to login
84
+ When I enter invalid credentials
85
+ Then I am presented with an error message to correct credentials
86
+
87
+ Below are step definitions that *only use the page objects* and no Calabash methods like touch, query…
88
+
89
+ For the following, assume we have also a Page Object class `WelcomePage` with a method `wordpress_blog` that transitions to the `AddWordPressBlog` screen:
90
+
91
+ ```ruby
92
+
93
+ ## Invalid login ##
94
+ Given /^I am about to login$/ do
95
+
96
+ welcome = page(WelcomePage).await
97
+ @page = welcome.wordpress_blog
98
+
99
+ end
100
+
101
+ When /^I enter invalid credentials$/ do
102
+ @page = @page.login(USERS[:invalid])
103
+ end
104
+
105
+ Then /^I am presented with an error message to correct credentials$/ do
106
+ @page.assert_invalid_login_message
107
+ screenshot_embed
108
+ end
109
+ ```
110
+
111
+ The `page` method is a helper method in Calabash which initializes a page object from a class. The `await` method just returns the page object after waiting for the page to be loaded.
112
+
113
+ We store the page object in an instance variable in the cucumber world (`@page`) and use it in the subsequent steps.
114
+
115
+ Notice how the steps only use page-object methods. This feature, as well as the step definitions, can be 100% reused across platforms. Great!
116
+
117
+ ## Step 3 - Platform implementations
118
+
119
+ Now we need to give an implementation of the `WordPressComPage` on iOS and Android (and all other supported platforms). We put those implementations in separte directories `features/ios/pages` and `features/android/pages` and name them `word_press_com_page_ios.rb` and `word_press_com_android.rb`.
120
+
121
+ Here is the implementation for iOS. It uses an abstract Page Object Class defined in Calabash iOS (`Calabash::IBase`).
122
+
123
+ ```ruby
124
+ require 'calabash-cucumber/ibase'
125
+
126
+ class WordPressComPage < Calabash::IBase
127
+
128
+ def title
129
+ "Sign In"
130
+ end
131
+
132
+ def login(user)
133
+ touch("view marked:'Username'")
134
+ await_keyboard
135
+
136
+ keyboard_enter_text user[:email]
137
+
138
+ touch("view marked:'Password'")
139
+
140
+ keyboard_enter_text user[:password]
141
+ done
142
+
143
+ wait_for_elements_do_not_exist(["tableViewCell activityIndicatorView"],
144
+ :timeout => 120)
145
+
146
+
147
+ if element_exists(invalid_login_query)
148
+ self
149
+ else
150
+ page(MainPage).await
151
+ end
152
+ end
153
+
154
+ def assert_invalid_login_message
155
+ check_element_exists(trait)
156
+ check_element_exists(invalid_login_query)
157
+ end
158
+
159
+ def invalid_login_query
160
+ "label {text LIKE '*Sign in failed*'}"
161
+ end
162
+
163
+ end
164
+
165
+ ```
166
+
167
+ And for Android:
168
+
169
+ ```ruby
170
+ class WordPressComPage < Calabash::ABase
171
+
172
+ def trait
173
+ "* id:'username'"
174
+ end
175
+
176
+ def await(opts={})
177
+ wait_for_elements_exist([trait])
178
+ self
179
+ end
180
+
181
+
182
+ def login(user)
183
+ query("* id:'username'",{:setText => user[:email]})
184
+ query("* id:'password'",{:setText => user[:password]})
185
+
186
+ performAction('scroll_down')
187
+
188
+ touch(login_button_query)
189
+
190
+ sleep(1)#Chance to show Dialog
191
+
192
+ wait_for(:timeout => 60, :timeout_message => "Timed out logging in") do
193
+ current_dialogs = query("DialogTitle",:text)
194
+
195
+ empty_dialog = current_dialogs.empty?
196
+ error_dialog = current_dialogs.include?("Error")
197
+ no_network_dialog = current_dialogs.include?("No network available")
198
+
199
+ empty_dialog or error_dialog or no_network_dialog
200
+ end
201
+
202
+ main_page = page(MainPage)
203
+
204
+ if main_page.current_page?
205
+ main_page.await
206
+ else
207
+ self
208
+ end
209
+ end
210
+
211
+ def invalid_login_query
212
+ login_button_query
213
+ end
214
+
215
+ def login_button_query
216
+ "android.widget.Button marked:'Log In'"
217
+ end
218
+
219
+ def assert_invalid_login_message
220
+ check_element_exists(invalid_login_query)
221
+ end
222
+
223
+
224
+ end
225
+ ```
226
+
227
+ ## Step 4 - Conditional loading
228
+
229
+ The final missing part is conditionally loading page-object implementations based on which platform we're running. This is done using Cucumber *profiles*. We create a file `config/cucumber.yml`
230
+
231
+ ---
232
+ android: RESET_BETWEEN_SCENARIOS=1 PLATFORM=android -r features/support -r features/android/support -r features/android/helpers -r features/step_definitions -r features/android/pages/
233
+
234
+ ios: APP_BUNDLE_PATH=ios-source/3.3.1/build/Applications/WordPress.app RESET_BETWEEN_SCENARIOS=1 PLATFORM=ios -r features/support -r features/ios/support -r features/ios/helpers -r features/step_definitions -r features/ios/pages
235
+
236
+ We're using Cucumbers `-r` option to only load a subset of Ruby files. We can then execute the tests as specified [here](https://github.com/calabash/x-platform-example).
237
+
238
+ iOS:
239
+
240
+ cucumber -p ios features/login.feature
241
+
242
+ Android:
243
+
244
+ calabash-android run path_to.apk -p android features/login.feature
245
+
246
+ ## Conclusion
247
+
248
+ We've described how to improve the architecture of your test code base: using page objects you get better abstraction, reuse and cross-platform comes more easily. We've created an open source sample project that you can use for inspiration: [X-Platform-Example](https://github.com/calabash/x-platform-example).
249
+
250
+ Comments and improvements welcome!
@@ -325,7 +325,12 @@ module Calabash
325
325
  device = options["DEVICE"] || ENV["DEVICE"] || "iphone"
326
326
 
327
327
  unless os
328
- major = Calabash::Cucumber::SimulatorHelper.ios_major_version
328
+ if @calabash_launcher && @calabash_launcher.active?
329
+ major = @calabash_launcher.ios_major_version
330
+ else
331
+ major = Calabash::Cucumber::SimulatorHelper.ios_major_version
332
+ end
333
+
329
334
  unless major
330
335
  raise <<EOF
331
336
  Unable to determine iOS major version
@@ -471,24 +476,28 @@ EOF
471
476
  end
472
477
 
473
478
 
474
- def start_app_in_background(path=nil, sdk = nil, version = 'iphone', args = nil)
475
-
476
- if path.nil?
477
- path = ENV['APP_BUNDLE_PATH'] || (defined?(APP_BUNDLE_PATH) && APP_BUNDLE_PATH)
478
- end
479
- app_bundle_path = Calabash::Cucumber::SimulatorHelper.app_bundle_or_raise(path)
479
+ ## args :app for device bundle id, for sim path to app
480
+ ##
481
+ def start_test_server_in_background(args={})
482
+ target = args[:device_target] || :simulator
483
+ stop_test_server
484
+ @calabash_launcher = Calabash::Cucumber::Launcher.new(target)
485
+ @calabash_launcher.relaunch(args)
480
486
 
481
- @ios_device = RunLoop.run(:app => app_bundle_path)
482
487
  end
483
488
 
484
- def send_uia_command(opts ={})
485
- RunLoop.send_command(opts[:device] ||@ios_device, opts[:command])
489
+ def stop_test_server
490
+ if @calabash_launcher
491
+ @calabash_launcher.stop
492
+ end
486
493
  end
487
494
 
488
- def stop_background_app(stop_spec = nil)
489
-
490
- @ios_device = RunLoop.stop(stop_spec || @ios_device)
491
-
495
+ def send_uia_command(opts ={})
496
+ run_loop = opts[:run_loop] || (@calabash_launcher && @calabash_launcher.active? && @calabash_launcher.run_loop)
497
+ command = opts[:command]
498
+ raise ArgumentError, 'please supply :run_loop or instance var @calabash_launcher' unless run_loop
499
+ raise ArgumentError, 'please supply :command' unless command
500
+ RunLoop.send_command(run_loop, opts[:command])
492
501
  end
493
502
 
494
503
 
@@ -4,42 +4,95 @@ require 'calabash-cucumber/operations'
4
4
  class Calabash::IBase
5
5
  include Calabash::Cucumber::Operations
6
6
 
7
- def initialize(world)
8
- @world = world
9
- end
10
-
11
- def embed(*args)
12
- @world.send(:embed,*args)
13
- end
7
+ attr_accessor :world, :transition_duration
14
8
 
15
- def puts(*args)
16
- @world.send(:puts, *args)
9
+ def initialize(world, transition_duration=0.5)
10
+ self.world = world
11
+ self.transition_duration = transition_duration
17
12
  end
18
13
 
19
14
  def trait
15
+ raise "You should define a trait method or a title method" unless respond_to?(:title)
20
16
  "navigationItemView marked:'#{self.title}'"
21
17
  end
22
18
 
23
- def page(clz,*args)
24
- clz.new(@world,*args)
19
+ def current_page?
20
+ element_exists(trait)
25
21
  end
26
22
 
27
- def step(s)
23
+ def page(clz, *args)
24
+ clz.new(world, *args)
25
+ end
28
26
 
27
+ def await(wait_opts={})
28
+ wait_for_elements_exist([trait], wait_opts)
29
+ unless wait_opts.has_key?(:await_animation) && !wait_opts[:await_animation]
30
+ sleep(transition_duration)
31
+ end
32
+ self
29
33
  end
30
34
 
31
- def steps(ss)
35
+ ##
36
+ # Performs a transition from receiver page to another by performing a +:tap+ gesture
37
+ # or a user specified +:action+.
38
+ # Caller must supply a hash of options +transition_options+ to describe the transition.
39
+ # Transition options may have the following keys
40
+ #
41
+ # +:tap+: A uiquery used to perform a tap gesture to begin transition
42
+ # +:action+: A proc to use begin transition (either :tap or :action must be supplied)
43
+ # +:page+: A page object or page object class to transition to (target page). If a class is provided this
44
+ # is instantiated using the +page+ method of self. If no +:page+ is supplied, +self+ is used.
45
+ # +:await+: If specified and truthy will await the +:page+ after performing gesture (usually to wait
46
+ # for animation to finish)
47
+ # +:tap_options+: If +:tap+ is provided used to pass as options to touch
48
+ # +:wait_options+: When awaiting target page, pass these options to the +await+ method
49
+ #
50
+ # Returns the transition target page
51
+ #
52
+ # Note it is assumed that the target page is a Calabash::IBase (or acts accordingly)
53
+ def transition(transition_options={})
54
+ uiquery = transition_options[:tap]
55
+ action = transition_options[:action]
56
+ page_arg = transition_options[:page]
57
+ should_await = transition_options.has_key?(:await) ? transition_options[:await] : true
32
58
 
33
- end
59
+ if action.nil? && uiquery.nil?
60
+ raise "Called transition without providing a gesture (:tap or :action) #{transition_options}"
61
+ end
34
62
 
35
- def await(opts={})
36
- wait_for_elements_exist([trait], opts)
37
- self
63
+ if uiquery
64
+ tap_options = transition_options[:tap_options] || {}
65
+ touch(uiquery, tap_options)
66
+ else
67
+ action.call()
68
+ end
69
+
70
+ page_obj = page_arg.is_a?(Class) ? page(page_arg) : page_arg
71
+ page_obj ||= self
72
+
73
+ if should_await
74
+ wait_opts = transition_options[:wait_options] || {}
75
+ if page_obj == self
76
+ unless wait_opts.has_key?(:await_animation) && !wait_opts[:await_animation]
77
+ sleep(transition_duration)
78
+ end
79
+ else
80
+ page_obj.await(wait_opts)
81
+ end
82
+ end
83
+
84
+ page_obj
38
85
  end
39
86
 
40
- def await_screenshot(wait_opts={},screenshot_opts={})
87
+ def await_screenshot(wait_opts={}, screenshot_opts={})
41
88
  await(wait_opts)
42
89
  screenshot_embed(screenshot_opts)
43
90
  end
44
91
 
92
+
93
+ protected
94
+ def method_missing(name, *args, &block)
95
+ world.send(name, *args, &block)
96
+ end
97
+
45
98
  end
@@ -19,6 +19,10 @@ module Calabash
19
19
 
20
20
  DEFAULT_SIM_RETRY = 2
21
21
 
22
+ # Load environment variable for showing full console output
23
+ # If not env var set then we use true; i.e. output to console in full
24
+ FULL_CONSOLE_OUTPUT = ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == 'false' ? false : true
25
+
22
26
  def self.relaunch(path, sdk = nil, version = 'iphone', args = nil)
23
27
 
24
28
  app_bundle_path = app_bundle_or_raise(path)
@@ -176,12 +180,18 @@ module Calabash
176
180
  timeout = (ENV['CONNECT_TIMEOUT'] || DEFAULT_SIM_WAIT).to_i
177
181
  retry_count = 0
178
182
  connected = false
179
- puts "Waiting at most #{timeout} seconds for simulator (CONNECT_TIMEOUT)"
180
- puts "Retrying at most #{max_retry_count} times (MAX_CONNECT_RETRY)"
183
+
184
+ if FULL_CONSOLE_OUTPUT
185
+ puts "Waiting at most #{timeout} seconds for simulator (CONNECT_TIMEOUT)"
186
+ puts "Retrying at most #{max_retry_count} times (MAX_CONNECT_RETRY)"
187
+ end
188
+
181
189
  until connected do
182
190
  raise "MAX_RETRIES" if retry_count == max_retry_count
183
191
  retry_count += 1
184
- puts "(#{retry_count}.) Start Simulator #{sdk}, #{version}, for #{app_bundle_path}"
192
+ if FULL_CONSOLE_OUTPUT
193
+ puts "(#{retry_count}.) Start Simulator #{sdk}, #{version}, for #{app_bundle_path}"
194
+ end
185
195
  begin
186
196
  Timeout::timeout(timeout, TimeoutErr) do
187
197
  simulator = launch(app_bundle_path, sdk, version, args)
@@ -196,7 +206,9 @@ module Calabash
196
206
  if connected
197
207
  server_version = get_version
198
208
  if server_version
199
- p server_version
209
+ if FULL_CONSOLE_OUTPUT
210
+ p server_version
211
+ end
200
212
  unless version_check(server_version)
201
213
  msgs = ["You're running an older version of Calabash server with a newer client",
202
214
  "Client:#{Calabash::Cucumber::VERSION}",
@@ -241,7 +253,9 @@ module Calabash
241
253
 
242
254
  def self.ping_app
243
255
  url = URI.parse(ENV['DEVICE_ENDPOINT']|| "http://localhost:37265/")
244
- puts "Ping #{url}..."
256
+ if FULL_CONSOLE_OUTPUT
257
+ puts "Ping #{url}..."
258
+ end
245
259
  http = Net::HTTP.new(url.host, url.port)
246
260
  res = http.start do |sess|
247
261
  sess.request Net::HTTP::Get.new url.path
@@ -260,7 +274,9 @@ module Calabash
260
274
  endpoint += "/" unless endpoint.end_with? "/"
261
275
  url = URI.parse("#{endpoint}version")
262
276
 
263
- puts "Fetch version #{url}..."
277
+ if FULL_CONSOLE_OUTPUT
278
+ puts "Fetch version #{url}..."
279
+ end
264
280
  begin
265
281
  body = Net::HTTP.get_response(url).body
266
282
  res = JSON.parse(body)
@@ -0,0 +1,155 @@
1
+ require 'calabash-cucumber/launch/simulator_helper'
2
+ require 'sim_launcher'
3
+ require 'run_loop'
4
+
5
+
6
+ class Calabash::Cucumber::Launcher
7
+ attr_accessor :run_loop
8
+ attr_accessor :device_target
9
+ attr_accessor :ios_version
10
+
11
+ def initialize(device_target=:simulator)
12
+ self.device_target = device_target
13
+ end
14
+
15
+ class CalabashLauncherTimeoutErr < Timeout::Error
16
+ end
17
+
18
+ def calabash_no_stop?
19
+ ENV['NO_LAUNCH']=="1" or ENV['NO_STOP']=="1"
20
+ end
21
+
22
+ def device_target?
23
+ ENV['DEVICE_TARGET'] == 'device'
24
+ end
25
+
26
+ def simulator_target?
27
+ ENV['DEVICE_TARGET'] == 'simulator'
28
+ end
29
+
30
+ def active?
31
+ (simulator_target? || device_target?) && (not run_loop.nil?)
32
+ end
33
+
34
+ def ios_major_version
35
+ v = ios_version
36
+ (v && v.split('.')[0])
37
+ end
38
+
39
+
40
+ def reset_app_jail(sdk, app_path)
41
+ return if device_target?
42
+
43
+ app = File.basename(app_path)
44
+ bundle = `find "#{ENV['HOME']}/Library/Application Support/iPhone Simulator/#{sdk}/Applications/" -type d -depth 2 -name "#{app}" | head -n 1`
45
+ return if bundle.empty? # Assuming we're already clean
46
+
47
+ sandbox = File.dirname(bundle)
48
+ ['Library', 'Documents', 'tmp'].each do |dir|
49
+ FileUtils.rm_rf(File.join(sandbox, dir))
50
+ end
51
+ end
52
+
53
+ def relaunch(args={})
54
+ RunLoop.stop(run_loop) if run_loop
55
+
56
+ if device_target?
57
+ default_args = {:app => ENV['BUNDLE_ID']}
58
+ self.run_loop = RunLoop.run(default_args.merge(args))
59
+ else
60
+
61
+ sdk = ENV['SDK_VERSION'] || SimLauncher::SdkDetector.new().latest_sdk_version
62
+ path = Calabash::Cucumber::SimulatorHelper.app_bundle_or_raise(app_path)
63
+ if ENV['RESET_BETWEEN_SCENARIOS']=="1"
64
+ reset_app_jail(sdk, path)
65
+ end
66
+
67
+ if simulator_target?
68
+ device = (ENV['DEVICE'] || 'iphone').to_sym
69
+ default_args = {:app => path, :device => device}
70
+ self.run_loop = RunLoop.run(default_args.merge(args))
71
+ else
72
+ ## sim launcher
73
+ Calabash::Cucumber::SimulatorHelper.relaunch(path, sdk, ENV['DEVICE'] || 'iphone', args)
74
+ end
75
+
76
+ end
77
+ ensure_connectivity
78
+ end
79
+
80
+
81
+ def ensure_connectivity
82
+ begin
83
+ max_retry_count = (ENV['MAX_CONNECT_RETRY'] || 10).to_i
84
+ timeout = (ENV['CONNECT_TIMEOUT'] || 30).to_i
85
+ retry_count = 0
86
+ connected = false
87
+ puts "Waiting for App to be ready"
88
+ until connected do
89
+ raise "MAX_RETRIES" if retry_count == max_retry_count
90
+ retry_count += 1
91
+ begin
92
+ Timeout::timeout(timeout, CalabashLauncherTimeoutErr) do
93
+ until connected
94
+ begin
95
+ connected = (ping_app == '200')
96
+ break if connected
97
+ rescue Exception => e
98
+ #p e
99
+ #retry
100
+ ensure
101
+ sleep 1 unless connected
102
+ end
103
+ end
104
+ end
105
+ rescue CalabashLauncherTimeoutErr => e
106
+ puts "Timed out...Retry.."
107
+ end
108
+ end
109
+ rescue e
110
+ p e
111
+ msg = "Unable to make connection to Calabash Server at #{ENV['DEVICE_ENDPOINT']|| "http://localhost:37265/"}\n"
112
+ msg << "Make sure you don't have a firewall blocking traffic to #{ENV['DEVICE_ENDPOINT']|| "http://localhost:37265/"}.\n"
113
+ raise msg
114
+ end
115
+ end
116
+
117
+ def ping_app
118
+ url = URI.parse(ENV['DEVICE_ENDPOINT']|| "http://localhost:37265/")
119
+
120
+ http = Net::HTTP.new(url.host, url.port)
121
+ res = http.start do |sess|
122
+ sess.request Net::HTTP::Get.new "version"
123
+ end
124
+ status = res.code
125
+ begin
126
+ http.finish if http and http.started?
127
+ rescue Exception => e
128
+
129
+ end
130
+
131
+ if status=='200'
132
+ version_body = JSON.parse(res.body)
133
+ if version_body['iOS_version']
134
+ self.ios_version = version_body['iOS_version']
135
+ end
136
+ end
137
+
138
+ status
139
+ end
140
+
141
+ def stop
142
+ RunLoop.stop(run_loop)
143
+ end
144
+
145
+ def app_path
146
+ ENV['APP_BUNDLE_PATH'] || (defined?(APP_BUNDLE_PATH) && APP_BUNDLE_PATH)
147
+ end
148
+
149
+ def calabash_notify(world)
150
+ if world.respond_to?(:on_launch)
151
+ world.on_launch
152
+ end
153
+ end
154
+ end
155
+
@@ -3,6 +3,7 @@ require 'calabash-cucumber/tests_helpers'
3
3
  require 'calabash-cucumber/keyboard_helpers'
4
4
  require 'calabash-cucumber/wait_helpers'
5
5
  require 'calabash-cucumber/location'
6
+ require 'calabash-cucumber/launcher'
6
7
  require 'net/http'
7
8
  require 'test/unit/assertions'
8
9
  require 'json'
@@ -99,7 +100,6 @@ module Calabash
99
100
  end
100
101
 
101
102
 
102
-
103
103
  #not officially supported yet
104
104
  #def change_slider_value_to(q, value)
105
105
  # target = value.to_f
@@ -1,6 +1,6 @@
1
1
  module Calabash
2
2
  module Cucumber
3
- VERSION = "0.9.135"
4
- FRAMEWORK_VERSION = "0.9.126"
3
+ VERSION = '0.9.142'
4
+ FRAMEWORK_VERSION = '0.9.141'
5
5
  end
6
6
  end
data/scripts/launch.rb ADDED
@@ -0,0 +1,49 @@
1
+ ########################################
2
+ # #
3
+ # Important Note #
4
+ # #
5
+ # When running calabash-ios tests at #
6
+ # www.lesspainful.com #
7
+ # this file will be overwritten by #
8
+ # a file which automates #
9
+ # app launch on devices. #
10
+ # #
11
+ # Don't rely on this file being #
12
+ # present when running at #
13
+ # www.lesspainful.com. #
14
+ # #
15
+ # Only put stuff here to automate #
16
+ # iOS Simulator. #
17
+ # #
18
+ # You can put your app bundle path #
19
+ # for automating simulator app start: #
20
+ # Uncomment APP_BUNDLE_PATH =.. #
21
+ # #
22
+ ########################################
23
+
24
+ require 'calabash-cucumber/launcher'
25
+
26
+ # Uncomment and replace ?? appropriately
27
+ # This should point to your Simulator build
28
+ # which includes calabash framework
29
+ # this is usually the Calabash build configuration
30
+ # of your production target.
31
+ #APP_BUNDLE_PATH = "~/Library/Developer/Xcode/DerivedData/??/Build/Products/Calabash-iphonesimulator/??.app"
32
+ #
33
+
34
+
35
+ Before do |scenario|
36
+ @calabash_launcher = Calabash::Cucumber::Launcher.new
37
+ @calabash_launcher.relaunch
38
+ @calabash_launcher.calabash_notify(self)
39
+ end
40
+
41
+ After do |scenario|
42
+ unless @calabash_launcher.calabash_no_stop?
43
+ if @calabash_launcher.active?
44
+ @calabash_launcher.stop
45
+ else
46
+ Calabash::Cucumber::SimulatorHelper.stop
47
+ end
48
+ end
49
+ end
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testautoi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.135
4
+ version: 0.9.142
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-03 00:00:00.000000000 Z
12
+ date: 2013-04-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cucumber
@@ -146,7 +146,7 @@ dependencies:
146
146
  requirements:
147
147
  - - '='
148
148
  - !ruby/object:Gem::Version
149
- version: 0.0.5
149
+ version: 0.0.7
150
150
  type: :runtime
151
151
  prerelease: false
152
152
  version_requirements: !ruby/object:Gem::Requirement
@@ -154,7 +154,7 @@ dependencies:
154
154
  requirements:
155
155
  - - '='
156
156
  - !ruby/object:Gem::Version
157
- version: 0.0.5
157
+ version: 0.0.7
158
158
  - !ruby/object:Gem::Dependency
159
159
  name: awesome_print
160
160
  requirement: !ruby/object:Gem::Requirement
@@ -215,6 +215,7 @@ files:
215
215
  - bin/testautoi
216
216
  - calabash-cucumber.gemspec
217
217
  - doc/calabash-ios-help.txt
218
+ - doc/x-platform-testing.md
218
219
  - epl-v10.html
219
220
  - features-skeleton/my_first.feature
220
221
  - features-skeleton/step_definitions/calabash_steps.rb
@@ -230,6 +231,7 @@ files:
230
231
  - lib/calabash-cucumber/ibase.rb
231
232
  - lib/calabash-cucumber/keyboard_helpers.rb
232
233
  - lib/calabash-cucumber/launch/simulator_helper.rb
234
+ - lib/calabash-cucumber/launcher.rb
233
235
  - lib/calabash-cucumber/location.rb
234
236
  - lib/calabash-cucumber/operations.rb
235
237
  - lib/calabash-cucumber/resources/cell_swipe_ios4_ipad.base64
@@ -317,7 +319,9 @@ files:
317
319
  - scripts/data/.GlobalPreferences.plist
318
320
  - scripts/data/clients.plist
319
321
  - scripts/data/com.apple.Accessibility.plist
322
+ - scripts/launch.rb
320
323
  - scripts/reset_simulator.scpt
324
+ - staticlib/calabash.framework.zip
321
325
  homepage:
322
326
  licenses: []
323
327
  post_install_message: