require 'httpclient' module Calabash module Cucumber module Core DATA_PATH = File.expand_path(File.dirname(__FILE__)) 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 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_all, *args) end def query_all(uiquery, *args) map(uiquery, :query_all, *args) end def touch(uiquery, options={}) options[:query] = uiquery views_touched = playback("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 swipe(dir, options={}) dir = dir.to_sym @current_rotation = @current_rotation || :down if @current_rotation == :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_rotation == :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_rotation == :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 def cell_swipe(options={}) 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 pinch(in_out, options={}) file = "pinch_in" if in_out.to_sym==:out file = "pinch_out" end playback(file, options) end #Current position of home button def current_rotation @current_rotation end def rotate(dir) @current_rotation = @current_rotation || :down rotate_cmd = nil case dir when :left then if @current_rotation == :down rotate_cmd = "left_home_down" @current_rotation = :right elsif @current_rotation == :right rotate_cmd = "left_home_right" @current_rotation = :up elsif @current_rotation == :left rotate_cmd = "left_home_left" @current_rotation = :down elsif @current_rotation == :up rotate_cmd = "left_home_up" @current_rotation = :left end when :right then if @current_rotation == :down rotate_cmd = "right_home_down" @current_rotation = :left elsif @current_rotation == :left rotate_cmd = "right_home_left" @current_rotation = :up elsif @current_rotation == :right rotate_cmd = "right_home_right" @current_rotation = :down elsif @current_rotation == :up rotate_cmd = "right_home_up" @current_rotation = :right end end if rotate_cmd.nil? screenshot_and_raise "Does not support rotating #{dir} when home button is pointing #{@current_rotation}" end playback("rotate_#{rotate_cmd}") end def background(secs) set_user_pref("__calabash_action", {:action => :background, :duration => secs}) end def prepare_dialog_action(opts={:dialog => nil, :answer => "Ok"}) if opts[:dialog].nil? || opts[:dialog].length < 1 raise ":dialog must be specified as a non-empty string (used as regexp to match dialog text)" end txt = opts[:answer] || 'Ok' set_user_pref("__calabash_action", {:action => :dialog, :text => opts[:dialog], :answer => txt}) 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 load_playback_data(recording, options={}) os = options["OS"] || ENV["OS"] || "ios5" device = options["DEVICE"] || ENV["DEVICE"] || "iphone" rec_dir = ENV['PLAYBACK_DIR'] || "#{Dir.pwd}/playback" if !recording.end_with? ".base64" recording = "#{recording}_#{os}_#{device}.base64" end data = nil if (File.exists?(recording)) data = File.read(recording) elsif (File.exists?("features/#{recording}")) data = File.read("features/#{recording}") elsif (File.exists?("#{rec_dir}/#{recording}")) data = File.read("#{rec_dir}/#{recording}") elsif (File.exists?("#{DATA_PATH}/resources/#{recording}")) data = File.read("#{DATA_PATH}/resources/#{recording}") else screenshot_and_raise "Playback not found: #{recording} (searched for #{recording} in #{Dir.pwd}, #{rec_dir}, #{DATA_PATH}/resources" end data end def playback(recording, options={}) data = load_playback_data(recording) post_data = %Q|{"events":"#{data}"| post_data<< %Q|,"query":"#{escape_quotes(options[:query])}"| if options[:query] post_data<< %Q|,"offset":#{options[:offset].to_json}| if options[:offset] post_data<< %Q|,"reverse":#{options[:reverse]}| if options[:reverse] post_data<< %Q|,"prototype":"#{options[:prototype]}"| if options[:prototype] post_data << "}" res = http({:method => :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'] || 'ios5' file_name = "#{file_name}_#{os}_#{device}.base64" system("/usr/bin/plutil -convert binary1 -o _recording_binary.plist _recording.plist") system("openssl base64 -in _recording_binary.plist -out #{file_name}") system("rm _recording.plist _recording_binary.plist") file_name 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 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 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 CAL_HTTP_RETRY_COUNT=3 def make_http_request(options) body = nil 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 HTTPClient::TimeoutError => e if count < CAL_HTTP_RETRY_COUNT-1 sleep(0.5) @http.reset_all @http=nil STDOUT.write "Retrying.. #{e.class}: (#{e})\n" STDOUT.flush else puts "Failing... #{e.class}" raise e end rescue Exception => e case e when Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT if count < CAL_HTTP_RETRY_COUNT-1 sleep(0.5) @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 http end end end end