rutl 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +37 -10
  6. data/.travis.yml +1 -0
  7. data/README.md +75 -38
  8. data/appveyor.yml +48 -0
  9. data/bin/ie.reg +0 -0
  10. data/bin/window_data.rb +27 -0
  11. data/lib/rspec/default_method_object_to_app.rb +28 -0
  12. data/lib/rspec/rutl_matchers.rb +7 -5
  13. data/lib/rutl.rb +26 -6
  14. data/lib/rutl/appium/appium_extension.rb +27 -0
  15. data/lib/rutl/appium/appium_server.rb +36 -0
  16. data/lib/rutl/appium/windows_test_app_wrapper.rb +40 -0
  17. data/lib/rutl/application.rb +70 -0
  18. data/lib/rutl/camera.rb +20 -4
  19. data/lib/rutl/element/click_to_change_state_mixin.rb +1 -1
  20. data/lib/rutl/element/element.rb +9 -10
  21. data/lib/rutl/element/element_context.rb +5 -4
  22. data/lib/rutl/element/string_reader_writer_mixin.rb +11 -7
  23. data/lib/rutl/interface/base.rb +30 -28
  24. data/lib/rutl/interface/browser/browser.rb +22 -0
  25. data/lib/rutl/interface/{chrome.rb → browser/chrome.rb} +3 -10
  26. data/lib/rutl/interface/{firefox.rb → browser/firefox.rb} +3 -10
  27. data/lib/rutl/interface/browser/internet_explorer.rb +23 -0
  28. data/lib/rutl/interface/browser/null.rb +36 -0
  29. data/lib/rutl/interface/windows/hello.rb +36 -0
  30. data/lib/rutl/interface/windows/notepad.rb +26 -0
  31. data/lib/rutl/interface/windows/windows_app.rb +35 -0
  32. data/lib/rutl/null_driver/null_driver.rb +4 -4
  33. data/lib/rutl/null_driver/null_element.rb +4 -4
  34. data/lib/rutl/version.rb +1 -1
  35. data/lib/rutl/{page.rb → view.rb} +37 -28
  36. data/lib/utilities/check_view.rb +12 -0
  37. data/lib/utilities/string.rb +12 -0
  38. data/lib/utilities/waiter.rb +23 -0
  39. data/rutl.gemspec +13 -0
  40. metadata +94 -10
  41. data/lib/rspec/default_rspec_to_browser.rb +0 -22
  42. data/lib/rutl/browser.rb +0 -70
  43. data/lib/rutl/interface/null.rb +0 -35
  44. data/lib/utilities.rb +0 -41
@@ -1,11 +1,13 @@
1
- require 'utilities'
1
+ require 'utilities/check_view'
2
2
  #
3
3
  # Additional RSpec matchers specific to this framework go here.
4
4
  #
5
+ module RSpec
6
+ include CheckView
5
7
 
6
- # Is it the expected page?
7
- RSpec::Matchers.define :be_page do |expected|
8
- match do |actual|
9
- actual.is_a?(expected) && page?(expected)
8
+ Matchers.define :be_view do |expected|
9
+ match do |actual|
10
+ actual.is_a?(expected) && view?(expected)
11
+ end
10
12
  end
11
13
  end
data/lib/rutl.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'rutl/browser'
1
+ require 'rutl/application'
2
2
  require 'rutl/version'
3
3
  #
4
4
  # TODO: Rename to something better. RubyUI2API? RAPID for Ruby API DSL?
@@ -6,13 +6,33 @@ require 'rutl/version'
6
6
  # desktop UI testing, turning the UI into an API via its DSL.
7
7
  #
8
8
  module RUTL
9
- # Should define RUTL::PAGES directory for your code
10
- # or set ENV['RUTL_PAGES']
11
- # or Browser intialize will raise.
12
- # PAGES = nil
9
+ # maybe this doesn't exist so much anymore.
10
+ # Should there be one flat directory? Nested dirs organized?
11
+ # web pages aren't the same as app views (have urls)
12
+ # or do I make them look the same?
13
+ # Not have any methods for views that depend on url or have them explicitly
14
+ # call out url and non-url versions.
15
+ #
16
+ # Should define RUTL::VIEWS directory for your code
17
+ # or set ENV['RUTL_VIEWS']
18
+ # or Application intialize will raise.
19
+ # VIEWS = nil
20
+
21
+ # HUNGARIAN automatically appends _<element_type> to all view elements.
22
+ # So
23
+ # button :foo
24
+ # is referred to later in code as
25
+ # foo_button
26
+ # instead of just the flat name. So
27
+ # bar_link.click
28
+ # instead of
29
+ # bar.click
30
+ #
31
+ # And I like it so it goes on by default.
32
+ HUNGARIAN = true
13
33
 
14
34
  # If this RUTL::SCREENSHOT_DIR or ENV['SCREENSHOT_DIR']
15
- # or Browser initialize is set, we take screenshots.
35
+ # or Application initialize is set, we take screenshots.
16
36
  # SCREENSHOTS = nil
17
37
 
18
38
  # This one is for diffing against.
@@ -0,0 +1,27 @@
1
+ module Appium
2
+ ##
3
+ # Extend Appium because I keep digging through WinAppDriver junk to
4
+ # figure out what's broken.
5
+ #
6
+ class Driver
7
+ def start
8
+ start_driver
9
+ rescue RuntimeError => e
10
+ if e.cause.to_s =~ /Failed to open TCP connection/
11
+ puts 'Cannot reach Appium server. Is it running? On the right port?'
12
+ else
13
+ puts "Cannot diagnose:\n#{e}"
14
+ end
15
+ exit
16
+ rescue Selenium::WebDriver::Error::NoSuchWindowError => e
17
+ puts "\n\n" + e.class
18
+ puts "\n\n" + e.cause
19
+ puts "\n\n" + e.backtrace
20
+ puts "\n\n" + (e.methods - Class.methods)
21
+ end
22
+
23
+ def quit
24
+ driver_quit
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ require 'faraday'
2
+ require 'utilities/waiter'
3
+ #
4
+ # Class to wrap Appium in a Rubyish way.
5
+ #
6
+ class AppiumServer
7
+ include Waiter
8
+ attr_accessor :port, :server
9
+
10
+ def initialize(server: nil, port: nil)
11
+ @server = server || 'localhost'
12
+ @port = port || 4723
13
+ end
14
+
15
+ def quiet_cmd(in_string)
16
+ system in_string + ' 1>nul 2>&1'
17
+ end
18
+
19
+ def start
20
+ raise 'server already started' if started?
21
+ quiet_cmd('start "appium" cmd /c appium')
22
+ await -> { started? }
23
+ end
24
+
25
+ def started?
26
+ Faraday.get("http://#{@server}:#{@port}/wd/hub/status")
27
+ true
28
+ rescue Faraday::ConnectionFailed
29
+ false
30
+ end
31
+
32
+ def stop
33
+ raise 'server not started' unless started?
34
+ quiet_cmd('taskkill /f /fi "WINDOWTITLE eq appium" /t')
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ require 'win32/window'
2
+ require 'utilities/waiter'
3
+ #
4
+ # wrapper for simple test apps
5
+ #
6
+ class WindowsTestApp
7
+ include Waiter
8
+ attr_reader :window_handle_string
9
+
10
+ def initialize(name:, title:)
11
+ @name = name
12
+ @title = title
13
+ end
14
+
15
+ def find_window_by_title
16
+ result = Win32::Window.find(title: @title)
17
+ raise 'found more than one instance of app' if result.size > 1
18
+ result.empty? ? false : result.first
19
+ end
20
+
21
+ def wait_for_started
22
+ app_window = await -> { find_window_by_title }
23
+ @pid = app_window.pid
24
+ @window_handle_string = format('0x%08x', app_window.handle)
25
+ end
26
+
27
+ def start
28
+ quiet_cmd "start \"NO TITLE\" #{@name}"
29
+ wait_for_started
30
+ end
31
+
32
+ def stop
33
+ quiet_cmd "taskkill /f /pid #{@pid} /t"
34
+ end
35
+ alias kill stop
36
+
37
+ def quiet_cmd(in_string)
38
+ system in_string + ' 1>nul 2>&1'
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ require 'rutl/view'
2
+ require 'utilities/string'
3
+
4
+ module RUTL
5
+ #
6
+ # Application, this top-level class, controls a browser and
7
+ # a fake browser. It will soon call into apps, at which point I need to
8
+ # rethink this naming convention.
9
+ #
10
+ class Application
11
+ attr_reader :interface
12
+
13
+ def screenshot
14
+ @interface.camera.screenshot
15
+ end
16
+
17
+ def initialize(family:, type:, views: RUTL::VIEWS || ENV['RUTL_VIEWS'])
18
+ raise 'Must set views!' if views.nil? || views.empty?
19
+ # This is kind of evil. Figure out how to ditch the $ variable.
20
+ $application = self
21
+ @interface = load_interface(family: family, type: type)
22
+ @interface.views = load_views(directory: views)
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ if args.empty?
27
+ @interface.send(method)
28
+ else
29
+ @interface.send(method, *args, &block)
30
+ end
31
+ end
32
+
33
+ def respond_to_missing?(*args)
34
+ @interface.respond_to?(*args)
35
+ end
36
+
37
+ private
38
+
39
+ def load_interface(family:, type:)
40
+ require "rutl/interface/#{family}/#{type}"
41
+ klass = "RUTL::Interface::#{type.to_s.pascal_case}"
42
+ Object.const_get(klass).new
43
+ end
44
+
45
+ def load_views(directory:)
46
+ require_views(directory: directory).map do |klass|
47
+ Object.const_get(klass).new(@interface)
48
+ end
49
+ end
50
+
51
+ # Ugly. Requires files for view objects.
52
+ # Returns array of class names to load.
53
+ def require_views(directory:)
54
+ Dir["#{directory}/*"].map do |file|
55
+ result = find_class_name(file)
56
+ result if result
57
+ end
58
+ end
59
+
60
+ def find_class_name(file)
61
+ require "rutl/../../#{file}"
62
+ File.open(file).each do |line|
63
+ bingo = line.match(/class (.*) < RUTL::View/)
64
+ # One class per file.
65
+ return bingo[1] if bingo && bingo[1]
66
+ end
67
+ false
68
+ end
69
+ end
70
+ end
data/lib/rutl/camera.rb CHANGED
@@ -27,14 +27,30 @@ module RUTL
27
27
 
28
28
  def shoot(path = nil)
29
29
  return if guard
30
+ file = prepare_shot(path)
31
+ cheese_already(file)
32
+ end
33
+ alias screenshot shoot
34
+
35
+ def prepare_shot(path = nil)
36
+ FileUtils.mkdir_p @dir
30
37
  # Magic path is used for all auto-screenshots.
31
38
  name = path || magic_path
39
+ File.join(@dir, pathify(name))
40
+ end
32
41
 
33
- FileUtils.mkdir_p @dir
34
- file = File.join(@dir, pathify(name))
35
- @driver.save_screenshot(file)
42
+ def cheese_already(file)
43
+ if @driver.respond_to?(:save_screenshot)
44
+ @driver.save_screenshot(file)
45
+ elsif @driver.respond_to?(:screenshot)
46
+ @driver.screenshot(file)
47
+ else
48
+ raise 'unknown screenshot method!'
49
+ end
50
+ rescue Selenium::WebDriver::Error::NoSuchWindowError
51
+ puts 'app closed; no photos, please'
52
+ # leave a zero length file as a sign that we came down this path
36
53
  end
37
- alias screenshot shoot
38
54
 
39
55
  def clean_dir(dir)
40
56
  FileUtils.rm_rf dir
@@ -14,7 +14,7 @@ module RUTL
14
14
  # * Returns the state we transitioned to.
15
15
  def click
16
16
  @context.interface.camera.screenshot
17
- this_css.click
17
+ @context.find_element.click
18
18
  result = @context.interface.wait_for_transition(@context.destinations)
19
19
  @context.interface.camera.screenshot
20
20
  result
@@ -1,7 +1,7 @@
1
1
  module RUTL
2
2
  module Element
3
3
  #
4
- # Page element base class.
4
+ # View element base class.
5
5
  #
6
6
  class Element
7
7
  attr_accessor :context
@@ -10,19 +10,18 @@ module RUTL
10
10
  raise element_context.to_s unless element_context.is_a? ElementContext
11
11
  @context = element_context
12
12
  # Not sure why, but I'm seeing Chrome fail becase the context interface
13
- # passed in isn't the same as the browser's interface.
13
+ # passed in isn't the same as the application's interface.
14
14
  # This only happens with click test cases, before the click, and
15
15
  # only if that case isn't run first.
16
16
  # The context we're passed is also an instance from as
17
17
  # RUTL::Interface::Chrome, but a different instance.
18
18
  #
19
19
  # Here's the kludge workaround line:
20
- @context.interface = $browser.interface
20
+ @context.interface = $application.interface
21
21
  end
22
22
 
23
- # Returns the element at this css selector.
24
- def this_css
25
- @context.find_element(:css)
23
+ def find_element
24
+ @context.find_element
26
25
  end
27
26
 
28
27
  # Returns boolean, of course.
@@ -30,21 +29,21 @@ module RUTL
30
29
  # have a valid Element object for something that doesn't exist. Anymore.
31
30
  # Or yet.
32
31
  def exists?
33
- this_css
32
+ find_element
34
33
  rescue Selenium::WebDriver::Error::NoSuchElementError
35
34
  false
36
35
  end
37
36
 
38
37
  def method_missing(method, *args, &block)
39
38
  if args.empty?
40
- this_css.send(method)
39
+ find_element.send(method)
41
40
  else
42
- this_css.send(method, *args, &block)
41
+ find_element.send(method, *args, &block)
43
42
  end
44
43
  end
45
44
 
46
45
  def respond_to_missing?(*args)
47
- this_css.respond_to?(*args)
46
+ find_element.respond_to?(*args)
48
47
  end
49
48
  end
50
49
  end
@@ -17,7 +17,7 @@ module RUTL
17
17
  def initialize(destinations: nil, interface: nil, selectors: [])
18
18
  unless destinations.nil? || destinations.class == Array
19
19
  # Should check each destination to make sure it's a
20
- # Page or a _____, too.
20
+ # View or a _____, too.
21
21
  raise 'destination must be an Array of destinations or nil.'
22
22
  end
23
23
  @destinations = destinations || []
@@ -29,11 +29,12 @@ module RUTL
29
29
  @selectors = selectors
30
30
  end
31
31
 
32
- def find_element(type)
32
+ def find_element(type = nil)
33
+ type ||= @selectors.first.first
33
34
  # @interface.driver.find_element(type, @selectors[type])
34
35
  # Should be this, but apparently @interface.driver is being overwritten
35
- # (or not written to) and it doesn't work. Using $browser does. :-(
36
- $browser.interface.driver.find_element(type, @selectors[type])
36
+ # (or not written to) and it doesn't work. Using $application does. :-(
37
+ $application.interface.driver.find_element(type, @selectors[type])
37
38
  end
38
39
  end
39
40
  end
@@ -14,28 +14,32 @@ module RUTL
14
14
 
15
15
  # I could cut set() and only foo_text= if I change this.
16
16
  # The problem I'm running into is not having the driver in
17
- # base element to do this_css calls. So I have to change the way
17
+ # base element to do find_element calls. So I have to change the way
18
18
  # drivers are passed into everything or initially have them everywhere,
19
- # which means rewriting chosen drivers or changing page load.
19
+ # which means rewriting chosen drivers or changing view load.
20
20
  # Ick.
21
21
  def set(string)
22
22
  clear
23
- this_css.send_keys(string)
23
+ find_element.send_keys(string)
24
24
  end
25
25
  alias text= set
26
26
  alias value= set
27
27
 
28
28
  # Return the String held by this element.
29
29
  def get
30
- this_css.attribute(:value)
30
+ found = find_element
31
+ # This is a clumsy workaround for winappdriver, which gets textfields
32
+ # as #text even though everything else seems to use #attribute(:value).
33
+ # If both are false, this is undefined.
34
+ found.attribute(:value) || found.text
31
35
  end
32
36
  alias text get
33
37
  alias value get
34
38
  alias to_s get
35
39
 
36
- # Talk to the page and set the element's string to ''.
40
+ # Talk to the view and set the element's string to ''.
37
41
  def clear
38
- this_css.clear
42
+ find_element.clear
39
43
  get
40
44
  end
41
45
 
@@ -48,7 +52,7 @@ module RUTL
48
52
  # Sends these keystrokes without clearing the field.
49
53
  # Returns the whole string in the field, including this input.
50
54
  def send_keys(string)
51
- this_css.send_keys(string)
55
+ find_element.send_keys(string)
52
56
  get
53
57
  end
54
58
 
@@ -1,15 +1,17 @@
1
- require 'utilities'
2
1
  require 'rutl/camera'
2
+ require 'utilities/check_view'
3
+ require 'utilities/waiter'
3
4
 
4
5
  module RUTL
5
6
  module Interface
6
7
  #
7
8
  # I might need to consider renaming these.
8
- # The *interface classes lie between Browser
9
+ # The *interface classes lie between Application
9
10
  # and the webdriver-level classes.
10
11
  #
11
12
  class Base
12
- include Utilities
13
+ include CheckView
14
+ include Waiter
13
15
 
14
16
  # RUTL::Driver
15
17
  attr_accessor :driver
@@ -17,72 +19,72 @@ module RUTL
17
19
  # RUTL::Camera
18
20
  attr_accessor :camera
19
21
 
20
- # Array of all RUTL::Page classes
21
- attr_accessor :pages
22
+ # Array of all RUTL::View classes
23
+ attr_accessor :views
22
24
 
23
25
  def initialize
24
26
  raise 'Child interface class must set @driver.' if @driver.nil?
25
27
  # base_name avoids collisions when unning the same tests with
26
- # different browsers.
28
+ # different applications
27
29
  name = self.class.to_s.sub('RUTL::Interface', '')
28
30
  @camera = Camera.new(@driver, base_name: name)
29
31
  end
30
32
 
31
- # Attempts to navigate to the page.
33
+ # Attempts to navigate to the view.
32
34
  # Takes screenshot if successful.
33
- def goto(page)
34
- raise 'expect Page class' unless page?(page)
35
- find_page(page).go_to_here
35
+ def goto(view)
36
+ raise 'expect View class' unless view?(view)
37
+ find_view(view).go_to_here
36
38
  @camera.screenshot
37
39
  end
38
40
 
39
41
  # Should define in children; raises here.
40
- # Should return the current page class.
41
- def current_page
42
+ # Should return the current view class.
43
+ def current_view
42
44
  raise 'define in child classes'
43
45
  end
44
46
 
45
47
  def method_missing(method, *args, &block)
46
48
  if args.empty?
47
- current_page.send(method)
49
+ current_view.send(method)
48
50
  else
49
- current_page.send(method, *args, &block)
51
+ current_view.send(method, *args, &block)
50
52
  end
51
53
  end
52
54
 
53
- # TODO: Is this needed? I not only find the page but also make sure the
54
- # urls match. Even though that's what finding pages means?
55
+ # TODO: Is this needed? I not only find the view but also make sure the
56
+ # urls match. Even though that's what finding views means?
55
57
  def find_state(target_states)
56
58
  target_states.each do |state|
57
- next unless state.url == current_page.url
58
- page = find_page(state)
59
- return page if page.loaded?
59
+ next unless state.url == current_view.url
60
+ view = find_view(state)
61
+ return view if view.loaded?
60
62
  end
61
63
  false
62
64
  end
63
65
 
64
- # Attempts to find page by class or url.
65
- def find_page(page)
66
- @pages.each do |p|
67
- return p if page?(page) && p.class == page
68
- return p if String == page.class && page == p.url
66
+ # Attempts to find view by class or url.
67
+ def find_view(view)
68
+ @views.each do |p|
69
+ return p if view?(view) && p.class == view
70
+ return p if String == view.class && view == p.url
69
71
  end
70
- raise "Page \"#{page}\" not found in pages #{@pages}"
72
+ raise "View \"#{view}\" not found in views #{@views}"
71
73
  end
72
74
 
73
75
  # Calls the polling utility mathod await() with a lambda trying to
74
- # find the next state, probably a Page class.
76
+ # find the next state, probably a View class.
75
77
  def wait_for_transition(target_states)
76
78
  #
77
79
  # TODO: Should also see if there are other things to wait for.
78
- # I don't think this is doing page load time.
80
+ # I don't think this is doing view load time.
79
81
  #
80
82
  await -> { find_state target_states }
81
83
  end
82
84
 
83
85
  def respond_to_missing?(*args)
84
86
  # This can't be right. Figure it out later.
85
- current_page.respond_to?(*args)
87
+ current_view.respond_to?(*args)
86
88
  end
87
89
 
88
90
  def quit