├── .ruby-version ├── .rspec ├── .travis.yml ├── lib ├── tap_watir │ ├── version.rb │ └── element.rb ├── appium │ └── driver.rb └── tap_watir.rb ├── CHANGES.md ├── Gemfile ├── .gitignore ├── Rakefile ├── spec ├── tap_watir_spec.rb ├── spec_helper.rb ├── element_spec.rb └── initialize │ └── tap_watir_spec.rb ├── LICENSE.txt ├── .rubocop.yml ├── README.md └── tap_watir.gemspec /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.7 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.7 5 | before_install: gem install bundler -v 1.16.2 6 | -------------------------------------------------------------------------------- /lib/tap_watir/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TapWatir 4 | VERSION = '0.1.0'.freeze 5 | end 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 0.1.0 (2018-06-02) 2 | 3 | * Initial release 4 | * Support running Watir tests locally on Android using Chrome Browser 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in tap_watir.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /.idea/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | Gemfile.lock -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new(:rubocop) do |t| 9 | t.options = ['--display-cop-names'] 10 | end 11 | 12 | task default: :spec 13 | -------------------------------------------------------------------------------- /lib/appium/driver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appium 4 | module Core 5 | # 6 | # The Appium Driver is defined here to allow a monkey patch 7 | # This can be removed with future Appium release 8 | # 9 | class Driver 10 | alias appium_set_app_path set_app_path 11 | 12 | def set_app_path 13 | appium_set_app_path unless @caps[:app] =~ URI::DEFAULT_PARSER.make_regexp 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/tap_watir_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe TapWatir::App do 6 | it 'delegates calls to driver' do 7 | expect($app.respond_to?(:session_capabilities)) 8 | expect($app.session_capabilities).to be_a Selenium::WebDriver::Remote::W3C::Capabilities 9 | end 10 | 11 | describe '#element' do 12 | it 'returns a TapWatir Element' do 13 | expect($app.element(id: 'android:id/statusBarBackground')).to be_a TapWatir::Element 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tap_watir/element.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TapWatir 4 | # 5 | # This is a element in the native app context 6 | # 7 | class Element 8 | def initialize(driver, selector) 9 | @driver = driver 10 | @selector = selector 11 | end 12 | 13 | def wd 14 | @element || locate 15 | end 16 | 17 | def exists? 18 | assert_exists 19 | true 20 | rescue Watir::Exception::UnknownObjectException 21 | false 22 | end 23 | 24 | private 25 | 26 | def locate 27 | @element = @driver.find_element(@selector.keys.first, @selector.values.first) 28 | rescue Selenium::WebDriver::Error::NoSuchElementError 29 | nil 30 | end 31 | 32 | def assert_exists 33 | locate unless @element 34 | return if @element 35 | raise Watir::Exception::UnknownObjectException 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'tap_watir' 5 | require 'rspec' 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | 18 | config.before(:suite) do 19 | opts = {url: 'http://localhost:4723/wd/hub', 20 | platformName: 'Android', 21 | platformVersion: '8.1', 22 | deviceName: 'Nexus', 23 | appWaitActivity: 'com.address.book.MainActivity', 24 | app: 'https://github.com/address-book/mobile_apps/blob/master/AddressBook.apk?raw=true'} 25 | 26 | $app = TapWatir::App.new(caps: opts) 27 | end 28 | 29 | config.after(:suite) do 30 | $app.close 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Titus Fortner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-07-30 15:07:38 -0700 using RuboCop version 0.58.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Configuration parameters: CountComments, ExcludedMethods. 10 | # ExcludedMethods: refine 11 | Metrics/BlockLength: 12 | Exclude: 13 | - 'spec/**/*' 14 | 15 | # Configuration parameters: AllowedVariables. 16 | Style/GlobalVars: 17 | Exclude: 18 | - 'spec/**/*' 19 | 20 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 21 | # URISchemes: http, https 22 | Metrics/LineLength: 23 | Max: 120 24 | 25 | # Cop supports --auto-correct. 26 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. 27 | # SupportedStyles: space, no_space, compact 28 | # SupportedStylesForEmptyBraces: space, no_space 29 | Layout/SpaceInsideHashLiteralBraces: 30 | EnforcedStyle: no_space 31 | -------------------------------------------------------------------------------- /lib/tap_watir.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'watir' 4 | require 'appium_lib_core' 5 | require 'appium/driver' 6 | require 'tap_watir/element' 7 | 8 | module TapWatir 9 | # 10 | # For driving a native application or or a native app context 11 | # 12 | class App 13 | attr_accessor :driver 14 | 15 | def initialize(opts) 16 | url = opts[:caps].delete(:url) 17 | @driver = Appium::Core.for(self, opts).start_driver(server_url: url) 18 | end 19 | 20 | def quit 21 | @driver.quit 22 | end 23 | alias close quit 24 | 25 | def element(selector) 26 | Element.new(driver, selector) 27 | end 28 | 29 | def method_missing(method_name, *arguments, &block) 30 | if driver.respond_to? method_name 31 | driver.send method_name, *arguments, &block 32 | else 33 | super 34 | end 35 | end 36 | 37 | def respond_to_missing?(method_name, include_private = false) 38 | driver.respond_to?(method_name) || super 39 | end 40 | end 41 | 42 | # 43 | # For driving a mobile browser or a webview context 44 | # 45 | class MobileBrowser < Watir::Browser 46 | def initialize(opts) 47 | @browser = super Selenium::WebDriver.for(:remote, opts) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TapWatir 2 | 3 | Watir for testing your Mobile Devices. Powered by Appium. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'tap_watir' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install tap_watir 20 | 21 | ## Usage 22 | 23 | For right now, this is how you access a Chrome browser locally: 24 | ``` 25 | appium_url = 'http://localhost:4723/wd/hub' 26 | caps = {platformName: 'Android', 27 | platformVersion: '8.1', 28 | deviceName: 'Nexus', 29 | browserName: 'Chrome'} 30 | 31 | browser = TapWatir::MobileBrowser.new(url: appium_url, 32 | desired_capabilities: caps) 33 | ``` 34 | 35 | ## Development 36 | 37 | To get the specs to run: 38 | * Install Android Studio 39 | * Create a Virtual Device named Nexus using Android 8.1 40 | * Install Appium Desktop v1.6.2 41 | * Download [Chromedriver 2.34](https://chromedriver.storage.googleapis.com/index.html?path=2.34/) and specify its location in Appium Desktop Advanced Tab 42 | * Start the Appium server. 43 | 44 | ## Contributing 45 | 46 | Bug reports and pull requests are welcome on GitHub at https://github.com/watir/tap_watir. 47 | 48 | ## License 49 | 50 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 51 | -------------------------------------------------------------------------------- /spec/element_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe TapWatir::Element do 6 | describe '#new' do 7 | it 'stores drivers as an instance vars' do 8 | element = $app.element(id: 'android:id/statusBarBackground') 9 | expect(element.instance_variable_get('@driver')).to eq $app.driver 10 | end 11 | 12 | it 'stores selector as an instance vars' do 13 | selector = {id: 'android:id/statusBarBackground'} 14 | element = $app.element(selector) 15 | expect(element.instance_variable_get('@selector')).to eq selector 16 | end 17 | 18 | it 'does not locate' do 19 | element = $app.element(id: 'android:id/statusBarBackground') 20 | expect(element.instance_variable_get('@element')).to be_nil 21 | end 22 | end 23 | 24 | describe '#wd' do 25 | it 'locates element if not fetched' do 26 | element = $app.element(id: 'android:id/statusBarBackground') 27 | expect(element.wd).to be_a Selenium::WebDriver::Element 28 | end 29 | end 30 | 31 | describe '#exist?' do 32 | it 'checks if the element exists' do 33 | expect($app.element(id: 'com.address.book:id/progressBar')).to exist 34 | end 35 | 36 | it 'checks if the element does not exists' do 37 | expect($app.element(id: 'NotAnElement')).not_to exist 38 | end 39 | 40 | it 'exists if previously located' do 41 | element = $app.element(id: 'android:id/statusBarBackground') 42 | element.wd 43 | expect(element).to exist 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /tap_watir.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'tap_watir/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'tap_watir' 9 | spec.version = TapWatir::VERSION 10 | spec.authors = ['Titus Fortner'] 11 | spec.email = ['titusfortner@gmail.com'] 12 | 13 | spec.summary = 'Watir for testing your Mobile Devices. Powered by Appium.' 14 | spec.description = 'Tap Watir facilitates the writing of automated tests for your mobile applications.' 15 | spec.homepage = 'http://watir.com' 16 | spec.license = 'MIT' 17 | 18 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 19 | # to allow pushing to a single host or delete this section to allow pushing to any host. 20 | if spec.respond_to?(:metadata) 21 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 22 | else 23 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 24 | 'public gem pushes.' 25 | end 26 | 27 | # Specify which files should be added to the gem when it is released. 28 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 29 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 30 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 31 | end 32 | 33 | spec.require_paths = ['lib'] 34 | spec.required_ruby_version = '>= 2.3.0' 35 | 36 | spec.add_development_dependency 'bundler', '~> 1.16' 37 | spec.add_development_dependency 'rake', '~> 10.0' 38 | spec.add_development_dependency 'rspec', '~> 3.0' 39 | spec.add_development_dependency 'rubocop', '~> 0.50' 40 | 41 | spec.add_dependency 'appium_lib_core', '~> 1.0' 42 | spec.add_dependency 'watir', '~> 6.0' 43 | end 44 | -------------------------------------------------------------------------------- /spec/initialize/tap_watir_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TapWatir do 4 | context 'with Android' do 5 | it 'opens Browser on Mobile Device' do 6 | opts = {url: 'http://localhost:4723/wd/hub', 7 | platformName: 'Android', 8 | platformVersion: '8.1', 9 | deviceName: 'Nexus', 10 | browserName: 'Chrome'} 11 | 12 | browser = TapWatir::MobileBrowser.new(url: opts[:url], 13 | desired_capabilities: opts) 14 | browser.goto 'http://watir.com/' 15 | expect(browser.url).to eq 'http://watir.com/' 16 | browser.close 17 | end 18 | 19 | it 'opens Native/Hybrid App Locally' do 20 | opts = {url: 'http://localhost:4723/wd/hub', 21 | platformName: 'Android', 22 | platformVersion: '8.1', 23 | deviceName: 'Nexus', 24 | appWaitActivity: 'com.address.book.MainActivity', 25 | app: 'https://github.com/address-book/mobile_apps/blob/master/AddressBook.apk?raw=true'} 26 | 27 | app = TapWatir::App.new(caps: opts) 28 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 29 | app.quit 30 | end 31 | 32 | it 'opens Native App EmuSim' do 33 | opts = {platformName: 'Android', 34 | platformVersion: '7.1', 35 | deviceName: 'Android GoogleAPI Emulator', 36 | app: 'https://github.com/address-book/mobile_apps/blob/master/AddressBook.apk?raw=true', 37 | name: 'Hybrid application test using emu/sim', 38 | build: 'TapWatir Tests Android', 39 | appWaitActivity: 'com.address.book.MainActivity', 40 | url: "http://#{ENV['SAUCE_USERNAME']}:#{ENV['SAUCE_ACCESS_KEY']}@ondemand.saucelabs.com:80/wd/hub", 41 | sauce_username: ENV['SAUCE_USERNAME'], 42 | sauce_access_key: ENV['SAUCE_ACCESS_KEY']} 43 | 44 | app = TapWatir::App.new(caps: opts) 45 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 46 | app.close 47 | end 48 | 49 | it 'opens Native App Real Device Cloud' do 50 | opts = {testobject_api_key: ENV['SAUCE_RDC_ANDROID'], 51 | platformName: 'Android', 52 | deviceName: 'LG Nexus 5X', 53 | url: 'https://us1.appium.testobject.com/wd/hub', 54 | appWaitActivity: 'com.address.book.MainActivity'} 55 | 56 | app = TapWatir::App.new(caps: opts) 57 | 58 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 59 | puts app.driver.window_size 60 | app.close 61 | end 62 | end 63 | 64 | context 'with iOS' do 65 | it 'opens Browser on Mobile Device' do 66 | opts = {url: 'http://localhost:4723/wd/hub', 67 | platformVersion: '11.4', 68 | deviceName: 'iPhone X', 69 | platformName: 'iOS', 70 | browserName: 'Safari'} 71 | 72 | browser = TapWatir::MobileBrowser.new(url: opts[:url], desired_capabilities: opts) 73 | browser.goto 'a.testaddressbook.com' 74 | expect(browser.title).to eq 'Address Book' 75 | browser.close 76 | end 77 | 78 | it 'opens Native/Hybrid App Locally' do 79 | opts = {url: 'http://localhost:4723/wd/hub', 80 | platformVersion: '11.4', 81 | platformName: 'iOS', 82 | deviceName: 'iPhone X', 83 | app: 'https://github.com/address-book/mobile_apps/blob/master/AddressBook.app.zip?raw=true'} 84 | 85 | app = TapWatir::App.new(caps: opts) 86 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 87 | app.quit 88 | end 89 | 90 | it 'opens Native App EmuSim' do 91 | opts = {platformName: 'iOS', 92 | platformVersion: '11.0', 93 | deviceName: 'iPhone X Simulator', 94 | app: 'https://github.com/address-book/mobile_apps/blob/master/AddressBook.app.zip?raw=true', 95 | buildName: 'Native and Hybrid test on iOS Sauce', 96 | name: 'TapWatir tests iOS', 97 | url: "http://#{ENV['SAUCE_USERNAME']}:#{ENV['SAUCE_ACCESS_KEY']}@ondemand.saucelabs.com:80/wd/hub", 98 | sauce_username: ENV['SAUCE_USERNAME'], 99 | sauce_access_key: ENV['SAUCE_ACCESS_KEY']} 100 | 101 | app = TapWatir::App.new(caps: opts) 102 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 103 | app.close 104 | end 105 | 106 | it 'opens Native App Real Device Cloud' do 107 | opts = {testobject_api_key: ENV['SAUCE_RDC_IOS'], 108 | platformName: 'iOS', 109 | platformVersion: '10.0', 110 | url: 'https://us1.appium.testobject.com/wd/hub'} 111 | 112 | app = TapWatir::App.new(caps: opts) 113 | 114 | expect(app.driver).to be_a(Appium::Core::Base::Driver) 115 | puts app.driver.window_size 116 | app.close 117 | end 118 | end 119 | end 120 | --------------------------------------------------------------------------------