|
| 1 | +require 'rubygems' |
| 2 | +require 'ap' |
| 3 | +require 'selenium-webdriver' |
| 4 | +require 'nokogiri' |
| 5 | + |
| 6 | +# base |
| 7 | +require_relative 'driver' |
| 8 | +require_relative 'capabilities' |
| 9 | + |
| 10 | +# common |
| 11 | +require_relative 'common/helper' |
| 12 | +require_relative 'common/wait' |
| 13 | +require_relative 'common/patch' |
| 14 | +require_relative 'common/version' |
| 15 | +require_relative 'common/error' |
| 16 | +require_relative 'common/search_context' |
| 17 | +require_relative 'common/command' |
| 18 | +require_relative 'common/element/window' |
| 19 | + |
| 20 | +# ios |
| 21 | +require_relative 'ios/ios' |
| 22 | + |
| 23 | +# android |
| 24 | +require_relative 'android/android' |
| 25 | + |
| 26 | +# device methods |
| 27 | +require_relative 'device/device' |
| 28 | +require_relative 'device/touch_actions' |
| 29 | +require_relative 'device/multi_touch' |
| 30 | + |
| 31 | +module Appium |
| 32 | + # Load arbitrary text ([toml format](https://github.com/toml-lang/toml)) |
| 33 | + # The toml is parsed by https://github.com/fbernier/tomlrb . |
| 34 | + # |
| 35 | + # ``` |
| 36 | + # [caps] |
| 37 | + # app = "path/to/app" |
| 38 | + # |
| 39 | + # [appium_lib] |
| 40 | + # port = 8080 |
| 41 | + # ``` |
| 42 | + # |
| 43 | + # :app is expanded |
| 44 | + # :require is expanded |
| 45 | + # all keys are converted to symbols |
| 46 | + # |
| 47 | + # @param opts [Hash] file: '/path/to/appium.txt', verbose: true |
| 48 | + # @return [hash] the symbolized hash with updated :app and :require keys |
| 49 | + def self.load_settings(opts = {}) |
| 50 | + raise 'opts must be a hash' unless opts.is_a? Hash |
| 51 | + raise 'opts must not be empty' if opts.empty? |
| 52 | + |
| 53 | + toml = opts[:file] |
| 54 | + raise 'Must pass a capability file which has [caps] and [appium_lib]' unless toml |
| 55 | + verbose = opts.fetch :verbose, false |
| 56 | + |
| 57 | + Appium::Logger.info "appium settings path: #{toml}" if verbose |
| 58 | + |
| 59 | + toml_exists = File.exist? toml |
| 60 | + Appium::Logger.info "Exists? #{toml_exists}" if verbose |
| 61 | + |
| 62 | + raise "toml doesn't exist #{toml}" unless toml_exists |
| 63 | + require 'tomlrb' |
| 64 | + Appium::Logger.info "Loading #{toml}" if verbose |
| 65 | + |
| 66 | + data = Tomlrb.load_file(toml, symbolize_keys: true) |
| 67 | + if verbose |
| 68 | + Appium::Logger.ap_info data unless data.empty? |
| 69 | + end |
| 70 | + |
| 71 | + if data && data[:caps] && data[:caps][:app] && !data[:caps][:app].empty? |
| 72 | + data[:caps][:app] = Appium::Driver.absolute_app_path data |
| 73 | + end |
| 74 | + |
| 75 | + if data && data[:appium_lib] && data[:appium_lib][:require] |
| 76 | + parent_dir = File.dirname toml |
| 77 | + data[:appium_lib][:require] = expand_required_files(parent_dir, data[:appium_lib][:require]) |
| 78 | + end |
| 79 | + |
| 80 | + data |
| 81 | + end |
| 82 | + |
| 83 | + class << self |
| 84 | + # rubocop:disable Style/Alias |
| 85 | + alias_method :load_appium_txt, :load_settings |
| 86 | + end |
| 87 | + |
| 88 | + # @param [String] base_dir parent directory of loaded appium.txt (toml) |
| 89 | + # @param [String] file_paths |
| 90 | + # @return [Array] list of require files as an array, nil if require doesn't exist |
| 91 | + def self.expand_required_files(base_dir, file_paths) |
| 92 | + # ensure files are absolute |
| 93 | + Array(file_paths).map! do |f| |
| 94 | + file = File.exist?(f) ? f : File.join(base_dir, f) |
| 95 | + file = File.expand_path file |
| 96 | + |
| 97 | + File.exist?(file) ? file : nil |
| 98 | + end |
| 99 | + file_paths.compact! # remove nils |
| 100 | + |
| 101 | + files = [] |
| 102 | + |
| 103 | + # now expand dirs |
| 104 | + file_paths.each do |item| |
| 105 | + unless File.directory? item |
| 106 | + # save file |
| 107 | + files << item |
| 108 | + next # only look inside folders |
| 109 | + end |
| 110 | + Dir.glob(File.expand_path(File.join(item, '**', '*.rb'))) do |f| |
| 111 | + # do not add folders to the file list |
| 112 | + files << File.expand_path(f) unless File.directory? f |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + files |
| 117 | + end |
| 118 | + |
| 119 | + # convert all keys (including nested) to symbols |
| 120 | + # |
| 121 | + # based on deep_symbolize_keys & deep_transform_keys from rails |
| 122 | + # https://github.com/rails/docrails/blob/a3b1105ada3da64acfa3843b164b14b734456a50/activesupport/lib/active_support/core_ext/hash/keys.rb#L84 |
| 123 | + def self.symbolize_keys(hash) |
| 124 | + raise 'symbolize_keys requires a hash' unless hash.is_a? Hash |
| 125 | + result = {} |
| 126 | + hash.each do |key, value| |
| 127 | + key = key.to_sym rescue key # rubocop:disable Style/RescueModifier |
| 128 | + result[key] = value.is_a?(Hash) ? symbolize_keys(value) : value |
| 129 | + end |
| 130 | + result |
| 131 | + end |
| 132 | + |
| 133 | + # This method is intended to work with page objects that share |
| 134 | + # a common module. For example, Page::HomePage, Page::SignIn |
| 135 | + # those could be promoted on with Appium.promote_singleton_appium_methods Page |
| 136 | + # |
| 137 | + # If you are promoting on an individual class then you should use |
| 138 | + # Appium.promote_appium_methods instead. The singleton method is intended |
| 139 | + # only for the shared module use case. |
| 140 | + # |
| 141 | + # if modules is a module instead of an array, then the constants of |
| 142 | + # that module are promoted on. |
| 143 | + # otherwise, the array of modules will be used as the promotion target. |
| 144 | + def self.promote_singleton_appium_methods(modules, driver = $driver) |
| 145 | + raise 'Global $driver is nil' if driver.nil? |
| 146 | + |
| 147 | + target_modules = [] |
| 148 | + |
| 149 | + if modules.is_a? Module |
| 150 | + modules.constants.each do |sub_module| |
| 151 | + target_modules << modules.const_get(sub_module) |
| 152 | + end |
| 153 | + else |
| 154 | + raise 'modules must be a module or an array' unless modules.is_a? Array |
| 155 | + target_modules = modules |
| 156 | + end |
| 157 | + |
| 158 | + target_modules.each do |const| |
| 159 | + # noinspection RubyResolve |
| 160 | + # rubocop:disable Style/MultilineIfModifier |
| 161 | + driver.public_methods(false).each do |m| |
| 162 | + const.send(:define_singleton_method, m) do |*args, &block| |
| 163 | + begin |
| 164 | + super(*args, &block) # promote.rb |
| 165 | + rescue NoMethodError, ArgumentError |
| 166 | + driver.send m, *args, &block if driver.respond_to?(m) |
| 167 | + end |
| 168 | + # override unless there's an existing method with matching arity |
| 169 | + end unless const.respond_to?(m) && const.method(m).arity == driver.method(m).arity |
| 170 | + end |
| 171 | + # rubocop:enable Style/MultilineIfModifier |
| 172 | + end |
| 173 | + end |
| 174 | + |
| 175 | + ## |
| 176 | + # Promote appium methods to class instance methods |
| 177 | + # |
| 178 | + # @param class_array [Array<Class>] An array of classes |
| 179 | + # |
| 180 | + # To promote methods to all classes: |
| 181 | + # |
| 182 | + # ```ruby |
| 183 | + # Appium.promote_appium_methods Object |
| 184 | + # ``` |
| 185 | + # |
| 186 | + # It's better to promote on specific classes instead of Object |
| 187 | + # |
| 188 | + # ```ruby |
| 189 | + # # promote on rspec |
| 190 | + # Appium.promote_appium_methods RSpec::Core::ExampleGroup |
| 191 | + # ``` |
| 192 | + # |
| 193 | + # ```ruby |
| 194 | + # # promote on minispec |
| 195 | + # Appium.promote_appium_methods Minitest::Spec |
| 196 | + # ``` |
| 197 | + def self.promote_appium_methods(class_array, driver = $driver) |
| 198 | + raise 'Driver is nil' if driver.nil? |
| 199 | + # Wrap single class into an array |
| 200 | + class_array = [class_array] unless class_array.class == Array |
| 201 | + # Promote Appium driver methods to class instance methods. |
| 202 | + class_array.each do |klass| |
| 203 | + driver.public_methods(false).each do |m| |
| 204 | + klass.class_eval do |
| 205 | + define_method m do |*args, &block| |
| 206 | + begin |
| 207 | + # Prefer existing method. |
| 208 | + # super will invoke method missing on driver |
| 209 | + super(*args, &block) |
| 210 | + |
| 211 | + # minitest also defines a name method, |
| 212 | + # so rescue argument error |
| 213 | + # and call the name method on $driver |
| 214 | + rescue NoMethodError, ArgumentError |
| 215 | + driver.send m, *args, &block if driver.respond_to?(m) |
| 216 | + end |
| 217 | + end |
| 218 | + end |
| 219 | + end |
| 220 | + end |
| 221 | + nil # return nil |
| 222 | + end |
| 223 | + |
| 224 | + def self.selenium_webdriver_version_more?(version) |
| 225 | + require 'rubygems' |
| 226 | + Gem.loaded_specs['selenium-webdriver'].version >= Gem::Version.new(version) |
| 227 | + end |
| 228 | +end |
0 commit comments