require 'httpclient' require 'calabash-cucumber/launch/simulator_helper' require 'calabash-cucumber/uia' require 'calabash-cucumber/ios7_operations' module Calabash module Cucumber module Core include Calabash::Cucumber::UIA include Calabash::Cucumber::IOS7Operations DATA_PATH = File.expand_path(File.dirname(__FILE__)) CAL_HTTP_RETRY_COUNT=3 RETRYABLE_ERRORS = [HTTPClient::TimeoutError, HTTPClient::KeepAliveDisconnected, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT] def macro(txt) if self.respond_to? :step step(txt) else Then txt end end def query(uiquery, *args) map(uiquery, :query, *args) end def flash(uiquery, *args) map(uiquery, :flash, *args) end def server_version JSON.parse(http(:path => 'version')) end def client_version Calabash::Cucumber::VERSION end def perform(*args) if args.length == 1 #simple selector hash = args.first q = hash[:on] hash = hash.dup hash.delete(:on) args = [hash] elsif args.length == 2 q = args[1][:on] if args[0].is_a? Hash args = [args[0]] else args = args[0] end end map(q, :query, *args) end def query_all(uiquery, *args) unless ENV['CALABASH_NO_DEPRECATION'] == '1' puts "query_all is deprecated. Use the new all/visible feature." puts "see: https://github.com/calabash/calabash-ios/wiki/05-Query-syntax" end map("all #{uiquery}", :query, *args) end def touch(uiquery, options={}) if (uiquery.is_a?(Array)) raise "No elements to touch in array" if uiquery.empty? uiquery = uiquery.first end if (uiquery.is_a?(Hash)) options[:offset] = point_from(uiquery, options) return touch(nil, options) end options[:query] = uiquery views_touched = do_touch(options) unless uiquery.nil? screenshot_and_raise "could not find view to touch: '#{uiquery}', args: #{options}" if views_touched.empty? end views_touched end def do_touch(options) if ios7? touch_ios7(options) else playback("touch", options) end end def swipe(dir, options={}) dir = dir.to_sym if ios7? swipe_ios7(options.merge(:direction => dir)) else current_orientation = status_bar_orientation.to_sym if current_orientation == :left case dir when :left then dir = :down when :right then dir = :up when :up then dir = :left when :down then dir = :right else end end if current_orientation == :right case dir when :left then dir = :up when :right then dir = :down when :up then dir = :right when :down then dir = :left else end end if current_orientation == :up case dir when :left then dir = :right when :right then dir = :left when :up then dir = :down when :down then dir = :up else end end playback("swipe_#{dir}", options) end end def pan(from, to, options={}) if ios7? pan_ios7(from, to, options) else interpolate "pan", options.merge(:start => from, :end => to) end end def cell_swipe(options={}) if ios7? raise "cell_swipe not supported on iOS7, simply use swipe with a query that matches the cell" end playback("cell_swipe", options) end def scroll(uiquery, direction) views_touched=map(uiquery, :scroll, direction) screenshot_and_raise "could not find view to scroll: '#{uiquery}', args: #{direction}" if views_touched.empty? views_touched end def scroll_to_row(uiquery, number) views_touched=map(uiquery, :scrollToRow, number) if views_touched.empty? or views_touched.member? "" screenshot_and_raise "Unable to scroll: '#{uiquery}' to: #{number}" end views_touched end def scroll_to_cell(options={:query => "tableView", :row => 0, :section => 0, :scroll_position => :top, :animate => true}) uiquery = options[:query] || "tableView" row = options[:row] sec = options[:section] if row.nil? or sec.nil? raise "You must supply both :row and :section keys to scroll_to_cell" end args = [] if options.has_key?(:scroll_position) args << options[:scroll_position] else args << "top" end if options.has_key?(:animate) args << options[:animate] end views_touched=map(uiquery, :scrollToRow, row.to_i, sec.to_i, *args) if views_touched.empty? or views_touched.member? "" screenshot_and_raise "Unable to scroll: '#{uiquery}' to: #{options}" end views_touched end def scroll_to_row_with_mark(row_id, options={:query => 'tableView', :scroll_position => :middle, :animate => true}) uiquery = options[:query] || 'tableView' args = [] if options.has_key?(:scroll_position) args << options[:scroll_position] else args << 'middle' end if options.has_key?(:animate) args << options[:animate] end views_touched=map(uiquery, :scrollToRowWithMark, row_id, *args) if views_touched.empty? or views_touched.member? '' msg = options[:failed_message] || "Unable to scroll: '#{uiquery}' to: #{options}" screenshot_and_raise msg end views_touched end def pinch(in_out, options={}) in_out = in_out.to_sym if ios7? pinch_ios7(in_out.to_sym, options) else file = "pinch_in" if in_out==:out file = "pinch_out" end playback(file, options) end end def rotation_candidates ['rotate_left_home_down', 'rotate_left_home_left', 'rotate_left_home_right', 'rotate_left_home_up', 'rotate_right_home_down', 'rotate_right_home_left', 'rotate_right_home_right', 'rotate_right_home_up'] end # orientations refer to home button position # down ==> bottom # up ==> top # left ==> landscape with left home button AKA: _right_ landscape* # right ==> landscape with right home button AKA: _left_ landscape* # # * see apple documentation for clarification about where the home button # is in left and right landscape orientations def rotate_home_button_to(dir) dir_sym = dir.to_sym if dir_sym.eql?(:top) if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' warn "converting '#{dir}' to ':up' - please adjust your code" end dir_sym = :up end if dir_sym.eql?(:bottom) if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' warn "converting '#{dir}' to ':down' - please adjust your code" end dir_sym = :down end directions = [:down, :up, :left, :right] unless directions.include?(dir_sym) screenshot_and_raise "expected one of '#{directions}' as an arg to 'rotate_home_button_to but found '#{dir}'" end res = status_bar_orientation() if res.nil? screenshot_and_raise "expected 'status_bar_orientation' to return a non-nil value" else res = res.to_sym end return res if res.eql? dir_sym rotation_candidates.each { |candidate| if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' puts "try to rotate to '#{dir_sym}' using '#{candidate}'" end playback(candidate) # need a longer sleep for cloud testing sleep(0.4) res = status_bar_orientation if res.nil? screenshot_and_raise "expected 'status_bar_orientation' to return a non-nil value" else res = res.to_sym end return if res.eql? dir_sym } if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' warn "Could not rotate home button to '#{dir}'." warn 'Is rotation enabled for this controller?' warn "Will return 'down'" end :down end def device_orientation(force_down=false) res = map(nil, :orientation, :device).first if ['face up', 'face down'].include?(res) if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' if force_down puts "WARN found orientation '#{res}' - will rotate to force orientation to 'down'" end end return res if !force_down return rotate_home_button_to :down end return res if !res.eql?('unknown') return res if !force_down rotate_home_button_to(:down) end def status_bar_orientation map(nil, :orientation, :status_bar).first end def rotate(dir) dir = dir.to_sym current_orientation = status_bar_orientation().to_sym rotate_cmd = nil case dir when :left then if current_orientation == :down rotate_cmd = "left_home_down" elsif current_orientation == :right rotate_cmd = "left_home_right" elsif current_orientation == :left rotate_cmd = "left_home_left" elsif current_orientation == :up rotate_cmd = "left_home_up" end when :right then if current_orientation == :down rotate_cmd = "right_home_down" elsif current_orientation == :left rotate_cmd = "right_home_left" elsif current_orientation == :right rotate_cmd = "right_home_right" elsif current_orientation == :up rotate_cmd = "right_home_up" end end if rotate_cmd.nil? if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' puts "Could not rotate device in direction '#{dir}' with orientation '#{current_orientation} - will do nothing" end else playback("rotate_#{rotate_cmd}") end end def send_app_to_background(secs) uia_send_app_to_background(secs) end def move_wheel(opts={}) q = opts[:query] || "pickerView" wheel = opts[:wheel] || 0 dir = opts[:dir] || :down raise "Wheel index must be non negative" if wheel < 0 raise "Only up and down supported :dir (#{dir})" unless [:up, :down].include?(dir) if ENV['OS'] == "ios4" playback "wheel_#{dir}", :query => "#{q} pickerTable index:#{wheel}" else playback "wheel_#{dir}", :query => "#{q} pickerTableView index:#{wheel}" end end def picker(opts={:query => "pickerView", :action => :texts}) raise "Not implemented" unless opts[:action] == :texts q = opts[:query] check_element_exists(q) comps = query(q, :numberOfComponents).first row_counts = [] texts = [] comps.times do |i| row_counts[i] = query(q, :numberOfRowsInComponent => i).first texts[i] = [] end row_counts.each_with_index do |row_count, comp| row_count.times do |i| #view = query(q,[{:viewForRow => 0}, {:forComponent => 0}],:accessibilityLabel).first spec = [{:viewForRow => i}, {:forComponent => comp}] view = query(q, spec).first if view txt = query(q, spec, :accessibilityLabel).first else txt = query(q, :delegate, [{:pickerView => :view}, {:titleForRow => i}, {:forComponent => comp}]).first end texts[comp] << txt end end texts end def recording_name_for(recording_name, os, device) if !recording_name.end_with? ".base64" "#{recording_name}_#{os}_#{device}.base64" else recording_name end end def load_recording(recording, rec_dir) directories = playback_file_directories(rec_dir) directories.each { |dir| path = "#{dir}/#{recording}" if File.exists?(path) # useful for debugging recordings, but too verbose for release # suggest (yet) another variable CALABASH_DEBUG_PLAYBACK ? #if ENV['CALABASH_FULL_CONSOLE_OUTPUT'] == '1' # puts "found compatible playback: '#{path}'" #end return File.read(path) end } nil end def playback_file_directories (rec_dir) # rec_dir is either ENV['PLAYBACK_DIR'] or ./features/playback [File.expand_path(rec_dir), "#{Dir.pwd}", "#{Dir.pwd}/features", "#{Dir.pwd}/features/playback", "#{DATA_PATH}/resources/"].uniq end def load_playback_data(recording_name, options={}) os = options["OS"] || ENV["OS"] device = options["DEVICE"] || ENV["DEVICE"] || "iphone" unless os if @calabash_launcher && @calabash_launcher.active? major = @calabash_launcher.ios_major_version else major = Calabash::Cucumber::SimulatorHelper.ios_major_version end unless major raise < :post, :raw => true, :path => 'play'}, post_data) res = JSON.parse(res) if res['outcome'] != 'SUCCESS' screenshot_and_raise "playback failed because: #{res['reason']}\n#{res['details']}" end res['results'] end def interpolate(recording, options={}) data = load_playback_data(recording) post_data = %Q|{"events":"#{data}"| post_data<< %Q|,"start":"#{escape_quotes(options[:start])}"| if options[:start] post_data<< %Q|,"end":"#{escape_quotes(options[:end])}"| if options[:end] post_data<< %Q|,"offset_start":#{options[:offset_start].to_json}| if options[:offset_start] post_data<< %Q|,"offset_end":#{options[:offset_end].to_json}| if options[:offset_end] post_data << "}" res = http({:method => :post, :raw => true, :path => 'interpolate'}, post_data) res = JSON.parse(res) if res['outcome'] != 'SUCCESS' screenshot_and_raise "interpolate failed because: #{res['reason']}\n#{res['details']}" end res['results'] end def record_begin http({:method => :post, :path => 'record'}, {:action => :start}) end def record_end(file_name) res = http({:method => :post, :path => 'record'}, {:action => :stop}) File.open("_recording.plist", 'wb') do |f| f.write res end device = ENV['DEVICE'] || 'iphone' os = ENV['OS'] unless os if @calabash_launcher && @calabash_launcher.active? major = @calabash_launcher.ios_major_version else major = Calabash::Cucumber::SimulatorHelper.ios_major_version end unless major raise < '#{rec_dir}/#{file_name}'" end def point_from(query_result, options) offset_x = 0 offset_y = 0 if options[:offset] offset_x += options[:offset][:x] || 0 offset_y += options[:offset][:y] || 0 end x = offset_x y = offset_y rect = query_result["rect"] || query_result[:rect] if rect x += rect['center_x'] || rect[:center_x] || rect[:x] || 0 y += rect['center_y'] || rect[:center_y] || rect[:y] || 0 else x += query_result['center_x'] || query_result[:center_x] || query_result[:x] || 0 y += query_result['center_y'] || query_result[:center_y] || query_result[:y] || 0 end {:x => x, :y => y} end def backdoor(sel, arg) json = { :selector => sel, :arg => arg } res = http({:method => :post, :path => 'backdoor'}, json) res = JSON.parse(res) if res['outcome'] != 'SUCCESS' screenshot_and_raise "backdoor #{json} failed because: #{res['reason']}\n#{res['details']}" end res['result'] end def calabash_exit # Exiting the app shuts down the HTTP connection and generates ECONNREFUSED, # or HTTPClient::KeepAliveDisconnected # which needs to be suppressed. begin http({:method => :post, :path => 'exit', :retryable_errors => RETRYABLE_ERRORS - [Errno::ECONNREFUSED, HTTPClient::KeepAliveDisconnected]}) rescue Errno::ECONNREFUSED, HTTPClient::KeepAliveDisconnected [] end end def map(query, method_name, *method_args) operation_map = { :method_name => method_name, :arguments => method_args } res = http({:method => :post, :path => 'map'}, {:query => query, :operation => operation_map}) res = JSON.parse(res) if res['outcome'] != 'SUCCESS' screenshot_and_raise "map #{query}, #{method_name} failed because: #{res['reason']}\n#{res['details']}" end res['results'] end ## args :app for device bundle id, for sim path to app ## def start_test_server_in_background(args={}) stop_test_server @calabash_launcher = Calabash::Cucumber::Launcher.new() @calabash_launcher.relaunch(args) @calabash_launcher end def stop_test_server if @calabash_launcher @calabash_launcher.stop end end def default_device @calabash_launcher && @calabash_launcher.device end def http(options, data=nil) options[:uri] = url_for(options[:path]) options[:method] = options[:method] || :get if data if options[:raw] options[:body] = data else options[:body] = data.to_json end end res = make_http_request(options) res.force_encoding("UTF-8") if res.respond_to?(:force_encoding) res end def url_for(verb) url = URI.parse(ENV['DEVICE_ENDPOINT']|| "http://localhost:37265") path = url.path if path.end_with? "/" path = "#{path}#{verb}" else path = "#{path}/#{verb}" end url.path = path url end def make_http_request(options) body = nil retryable_errors = options[:retryable_errors] || RETRYABLE_ERRORS CAL_HTTP_RETRY_COUNT.times do |count| begin if not @http @http = init_request(options) end if options[:method] == :post body = @http.post(options[:uri], options[:body]).body else body = @http.get(options[:uri], options[:body]).body end break rescue Exception => e if retryable_errors.include?(e) || retryable_errors.any? { |c| e.is_a?(c) } if count < CAL_HTTP_RETRY_COUNT-1 if e.is_a?(HTTPClient::TimeoutError) sleep(3) else sleep(0.5) end @http.reset_all @http=nil STDOUT.write "Retrying.. #{e.class}: (#{e})\n" STDOUT.flush else puts "Failing... #{e.class}" raise e end else raise e end end end body end def init_request(url) http = HTTPClient.new http.connect_timeout = 15 http.send_timeout = 15 http.receive_timeout = 15 if ENV['DEBUG_HTTP'] and (ENV['DEBUG_HTTP'] != "0") http.debug_dev = $stdout end http end end end end