rutl 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -2
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +10 -4
- data/.travis.yml +1 -0
- data/README.md +1 -2
- data/lib/rspec/rutl_matchers.rb +4 -1
- data/lib/rutl.rb +3 -3
- data/lib/rutl/browser.rb +56 -51
- data/lib/rutl/camera.rb +74 -0
- data/lib/rutl/interface/base_interface.rb +75 -62
- data/lib/rutl/interface/chrome_interface.rb +27 -25
- data/lib/rutl/interface/firefox_interface.rb +21 -19
- data/lib/rutl/interface/null_interface.rb +26 -24
- data/lib/rutl/null_driver/null_driver.rb +42 -0
- data/lib/rutl/null_driver/null_element.rb +56 -0
- data/lib/rutl/page.rb +119 -0
- data/lib/rutl/version.rb +1 -1
- data/lib/{rutl/utilities.rb → utilities.rb} +2 -6
- metadata +7 -16
- data/lib/rutl/base_page.rb +0 -111
- data/lib/rutl/driver/null_driver.rb +0 -38
- data/lib/rutl/driver/null_driver_page_element.rb +0 -51
- data/lib/rutl/interface/elements.rb +0 -5
- data/lib/rutl/interface/elements/base_element.rb +0 -24
- data/lib/rutl/interface/elements/button.rb +0 -10
- data/lib/rutl/interface/elements/checkbox.rb +0 -8
- data/lib/rutl/interface/elements/click_to_change_state_mixin.rb +0 -18
- data/lib/rutl/interface/elements/element_context.rb +0 -29
- data/lib/rutl/interface/elements/link.rb +0 -11
- data/lib/rutl/interface/elements/string_reader_writer_mixin.rb +0 -66
- data/lib/rutl/interface/elements/text.rb +0 -10
- data/lib/rutl/screencam.rb +0 -71
data/lib/rutl/base_page.rb
DELETED
@@ -1,111 +0,0 @@
|
|
1
|
-
require 'rutl/interface/elements'
|
2
|
-
require 'rutl/driver/null_driver'
|
3
|
-
|
4
|
-
#
|
5
|
-
# Base page class. It's used to call the magical method_messing
|
6
|
-
# stuff to parse the page object files into actual objects.
|
7
|
-
#
|
8
|
-
class BasePage
|
9
|
-
# BUGBUG: Kludgy. What do I really want to do here?
|
10
|
-
# Make it easy to define a page's default url and
|
11
|
-
# also matchers for page urls for pages with variable urls?
|
12
|
-
# rubocop:disable Style/TrivialAccessors
|
13
|
-
def self.url
|
14
|
-
@url
|
15
|
-
end
|
16
|
-
|
17
|
-
def url
|
18
|
-
self.class.url
|
19
|
-
end
|
20
|
-
# rubocop:enable Style/TrivialAccessors
|
21
|
-
|
22
|
-
@@loaded_pages = []
|
23
|
-
|
24
|
-
def initialize(interface)
|
25
|
-
@interface = interface
|
26
|
-
# Dirty trick because we're loading all of page classes from files and then
|
27
|
-
# initializing them, calling their layout methods to do magic.
|
28
|
-
# The base_page class knows what pages are loaded.
|
29
|
-
return if @@loaded_pages.include?(self.class)
|
30
|
-
layout
|
31
|
-
@@loaded_pages << self.class
|
32
|
-
end
|
33
|
-
|
34
|
-
def go_to_here
|
35
|
-
# Ovveride this in base page to have something more
|
36
|
-
# complicated than this.
|
37
|
-
@interface.driver.navigate.to(url)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Written by Browser and only used internally.
|
41
|
-
attr_writer :interface
|
42
|
-
|
43
|
-
def loaded?(driver)
|
44
|
-
url == driver.current_url
|
45
|
-
end
|
46
|
-
|
47
|
-
# Dynamically add a method, :<name> (or :<name>= if setter)
|
48
|
-
# to the current class where that method creates an instance
|
49
|
-
# of klass.
|
50
|
-
# context is an ElementContext
|
51
|
-
#
|
52
|
-
# As it is, this seems silly to break into pieces for Rubocop.
|
53
|
-
# rubocop:disable Metrics/MethodLength
|
54
|
-
def add_method(context:, klass:, name:, setter: false)
|
55
|
-
name = "#{name}_#{klass.downcase}"
|
56
|
-
constant = Module.const_get(klass.capitalize)
|
57
|
-
self.class.class_exec do
|
58
|
-
if setter
|
59
|
-
define_method("#{name}=") do |value|
|
60
|
-
constant.new(context, value)
|
61
|
-
end
|
62
|
-
else
|
63
|
-
define_method(name) do
|
64
|
-
constant.new(context)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
# rubocop:enable Metrics/MethodLength
|
70
|
-
|
71
|
-
# This creates a new element instance whenever it's called.
|
72
|
-
# Because of that we can't keep state in any element objects.
|
73
|
-
# That seems like a good thing, actually.
|
74
|
-
# Called by layout method on pages.
|
75
|
-
#
|
76
|
-
# Hard to make shorter.
|
77
|
-
# rubocop:disable Metrics/MethodLength
|
78
|
-
def method_missing(element, *args, &_block)
|
79
|
-
name, selectors, rest = args
|
80
|
-
context = ElementContext.new(destinations: rest,
|
81
|
-
interface: @interface,
|
82
|
-
selectors: selectors)
|
83
|
-
case element
|
84
|
-
when /button/, /checkbox/, /link/
|
85
|
-
add_method(name: name, context: context, klass: element)
|
86
|
-
when /text/
|
87
|
-
add_method(name: name, context: context, klass: element)
|
88
|
-
add_method(name: name, context: context, klass: element, setter: true)
|
89
|
-
else
|
90
|
-
# TODO: replace with a super call. This is useful for debugging for now.
|
91
|
-
raise "#{element} NOT FOUND WITH ARGS #{args}!!!"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
# rubocop:enable Metrics/MethodLength
|
95
|
-
|
96
|
-
def respond_to_missing?(*args)
|
97
|
-
# Is this right at all???
|
98
|
-
case args[0].to_s
|
99
|
-
when /button/, /checkbox/, /link/, /text/,
|
100
|
-
'driver', 'url', 'children', 'loaded?'
|
101
|
-
true
|
102
|
-
when 'ok_link'
|
103
|
-
raise 'OK LINK WAY DOWN HERE IN BASE PAGE!!!'
|
104
|
-
else
|
105
|
-
# I think it's good to raise but change the message.
|
106
|
-
raise 'Drew, you hit this most often when checking current page ' \
|
107
|
-
"rather than current page class:\n\n #{args}"
|
108
|
-
# I think I want to raise instead of returningn false.
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'rutl/driver/null_driver_page_element'
|
2
|
-
|
3
|
-
#
|
4
|
-
# This is at a peer level to the webdrivers but it's for a fake brwoser.
|
5
|
-
#
|
6
|
-
class NullDriver
|
7
|
-
attr_accessor :context
|
8
|
-
|
9
|
-
def initialize(context)
|
10
|
-
raise 'no context' unless context.is_a?(ElementContext)
|
11
|
-
@context = context
|
12
|
-
end
|
13
|
-
|
14
|
-
def find_element(type, location)
|
15
|
-
# Return a new one of these so that it can be clicked ar written
|
16
|
-
# to or whatever.
|
17
|
-
context = ElementContext.new(interface: @context.interface)
|
18
|
-
NullDriverPageElement.new(context, type, location)
|
19
|
-
end
|
20
|
-
|
21
|
-
# Cheap way to handle browser.navigate.to(url)
|
22
|
-
# TODO: Until I care about the url and then I should ????
|
23
|
-
def navigate
|
24
|
-
context = ElementContext.new(interface: @context.interface)
|
25
|
-
NullDriver.new(context)
|
26
|
-
end
|
27
|
-
|
28
|
-
def to(url)
|
29
|
-
result = @context.interface.find_page(url)
|
30
|
-
@context.interface.current_page = result
|
31
|
-
result.url
|
32
|
-
end
|
33
|
-
|
34
|
-
def quit
|
35
|
-
# Clean out the @@variables.
|
36
|
-
NullDriverPageElement.clear_variables
|
37
|
-
end
|
38
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'rutl/interface/elements/element_context'
|
2
|
-
|
3
|
-
#
|
4
|
-
# This fakes all page elements when used with the null driver.
|
5
|
-
# It's a dirty way to avoid modeling all of what a driver talks to.
|
6
|
-
#
|
7
|
-
class NullDriverPageElement
|
8
|
-
attr_accessor :context
|
9
|
-
|
10
|
-
def self.clear_variables
|
11
|
-
@@variables = {}
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize(context, _type, location)
|
15
|
-
@@variables ||= {}
|
16
|
-
@context = context
|
17
|
-
@location = location
|
18
|
-
end
|
19
|
-
|
20
|
-
# @@string is a class variable because this framework creates new instances
|
21
|
-
# of each element every time it accesses them. This is good behavior by
|
22
|
-
# default because pages could change underneath us.
|
23
|
-
# For text fields in the null browser, though, we want to preserve the values
|
24
|
-
# across calls, letting us write and then read.
|
25
|
-
def send_keys(string)
|
26
|
-
init = @@variables[@location] || ''
|
27
|
-
@@variables[@location] = init + string
|
28
|
-
end
|
29
|
-
|
30
|
-
def attribute(attr)
|
31
|
-
case attr.to_sym
|
32
|
-
when :value
|
33
|
-
@@variables[@location] || ''
|
34
|
-
else
|
35
|
-
raise ArgumentError, "Attribute unknown: #{attr}"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def clear
|
40
|
-
@@variables[@location] = ''
|
41
|
-
end
|
42
|
-
|
43
|
-
def this_css
|
44
|
-
self
|
45
|
-
end
|
46
|
-
|
47
|
-
def click
|
48
|
-
# nop
|
49
|
-
# Called by ClickToChangeStateMixin like Selenium driver.click
|
50
|
-
end
|
51
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Page elements. Base class.
|
3
|
-
#
|
4
|
-
class BaseElement
|
5
|
-
attr_accessor :context
|
6
|
-
|
7
|
-
def initialize(element_context)
|
8
|
-
raise element_context.to_s unless element_context.is_a? ElementContext
|
9
|
-
@context = element_context
|
10
|
-
# Not sure why, but I'm seeing Chrome fail becase the context interface
|
11
|
-
# passed in isn't the same as the browser's interface.
|
12
|
-
# This only happens with click test cases, before the click, and
|
13
|
-
# only if that case isn't run first.
|
14
|
-
# The context we're passed is also an instance from as ChromeInterface,
|
15
|
-
# but a different instance.
|
16
|
-
#
|
17
|
-
# Here's the kludge workaround line:
|
18
|
-
@context.interface = $browser.interface
|
19
|
-
end
|
20
|
-
|
21
|
-
def this_css
|
22
|
-
@context.find_element(:css)
|
23
|
-
end
|
24
|
-
end
|
@@ -1,10 +0,0 @@
|
|
1
|
-
require 'rutl/interface/elements/base_element'
|
2
|
-
require 'rutl/interface/elements/click_to_change_state_mixin'
|
3
|
-
|
4
|
-
#
|
5
|
-
# It's a button!
|
6
|
-
#
|
7
|
-
class Button < BaseElement
|
8
|
-
include ClickToChangeStateMixin
|
9
|
-
# def get, text - return button text; useful for text-changing buttons
|
10
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Mix this in for things that change state when clicked.
|
3
|
-
# The only things that wouldn't change state when clicked either
|
4
|
-
# shouldn't be clicked or are just annoying.
|
5
|
-
#
|
6
|
-
module ClickToChangeStateMixin
|
7
|
-
def click
|
8
|
-
# Screenshot before clicking. Is this really necessary?
|
9
|
-
@context.interface.camera.screenshot
|
10
|
-
this_css.click
|
11
|
-
# returns the page it found
|
12
|
-
result = @context.interface.wait_for_transition(@context.destinations)
|
13
|
-
# And after clicking and going to new state. This seems more needed
|
14
|
-
# because we want to see where we went.
|
15
|
-
@context.interface.camera.screenshot
|
16
|
-
result
|
17
|
-
end
|
18
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# The context passed around to all elements.
|
3
|
-
# What they need to know outside of themselves to function.
|
4
|
-
#
|
5
|
-
class ElementContext
|
6
|
-
attr_accessor :destinations
|
7
|
-
attr_accessor :interface
|
8
|
-
attr_accessor :selectors
|
9
|
-
|
10
|
-
def initialize(destinations: nil, interface: nil, selectors: [])
|
11
|
-
unless destinations.nil? || destinations.class == Array
|
12
|
-
# Should check each destination to make sure it's a Page or a _____, too.
|
13
|
-
raise 'destination must be an Array of destinations or nil.'
|
14
|
-
end
|
15
|
-
@destinations = destinations || []
|
16
|
-
unless interface.nil? || interface.class.ancestors.include?(BaseInterface)
|
17
|
-
raise "#{interface.class}: #{interface} must be a *Interface class."
|
18
|
-
end
|
19
|
-
@interface = interface
|
20
|
-
@selectors = selectors
|
21
|
-
end
|
22
|
-
|
23
|
-
def find_element(type)
|
24
|
-
# @interface.driver.find_element(type, @selectors[type])
|
25
|
-
# Should be this, but apparently @interface.driver is being overwritten
|
26
|
-
# (or not written to) and it doesn't work. Using $browser does. :-(
|
27
|
-
$browser.interface.driver.find_element(type, @selectors[type])
|
28
|
-
end
|
29
|
-
end
|
@@ -1,11 +0,0 @@
|
|
1
|
-
require 'rutl/interface/elements/base_element'
|
2
|
-
require 'rutl/interface/elements/click_to_change_state_mixin'
|
3
|
-
|
4
|
-
#
|
5
|
-
# Link, of course.
|
6
|
-
#
|
7
|
-
class Link < BaseElement
|
8
|
-
include ClickToChangeStateMixin
|
9
|
-
# text, url - get what they say
|
10
|
-
# should there be a 'get' - what would it get?
|
11
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Implement String stuff in a mixin.
|
3
|
-
# TODO: Not finished yet. Must be able to
|
4
|
-
#
|
5
|
-
module StringReaderWriterMixin
|
6
|
-
# Override BaseElement's normal initialize method.
|
7
|
-
def initialize(element_context, input_value = nil)
|
8
|
-
raise element_context.to_s unless element_context.is_a? ElementContext
|
9
|
-
@context = element_context
|
10
|
-
set input_value unless input_value.nil?
|
11
|
-
end
|
12
|
-
|
13
|
-
# I could cut set() and only foo_text= if I change this.
|
14
|
-
# The problem I'm running into is not having the driver in
|
15
|
-
# base element to do this_css calls. So I have to change the way
|
16
|
-
# drivers are passed into everything or initially have them everywhere,
|
17
|
-
# which means rewriting chosen drivers or changing page load.
|
18
|
-
# Ick.
|
19
|
-
def set(string)
|
20
|
-
clear
|
21
|
-
this_css.send_keys(string)
|
22
|
-
end
|
23
|
-
alias text= set
|
24
|
-
alias value= set
|
25
|
-
|
26
|
-
def get
|
27
|
-
this_css.attribute(:value)
|
28
|
-
end
|
29
|
-
alias text get
|
30
|
-
alias value get
|
31
|
-
alias to_s get
|
32
|
-
|
33
|
-
def clear
|
34
|
-
this_css.clear
|
35
|
-
get
|
36
|
-
end
|
37
|
-
|
38
|
-
def eql?(other)
|
39
|
-
other == get
|
40
|
-
end
|
41
|
-
alias == eql?
|
42
|
-
|
43
|
-
def send_keys(string)
|
44
|
-
this_css.send_keys(string)
|
45
|
-
get
|
46
|
-
end
|
47
|
-
|
48
|
-
def method_missing(method, *args, &block)
|
49
|
-
# RuboCop complains unless I fall back to super here
|
50
|
-
# even though that's pretty meaningless. Oh, well, it's harmless.
|
51
|
-
super unless get.respond_to?(method)
|
52
|
-
if args.empty?
|
53
|
-
get.send(method)
|
54
|
-
else
|
55
|
-
get.send(method, *args, &block)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def respond_to_missing?(method, flag)
|
60
|
-
get.respond_to?(method, flag)
|
61
|
-
end
|
62
|
-
|
63
|
-
#
|
64
|
-
# TODO: Fall through to String methods?
|
65
|
-
#
|
66
|
-
end
|
@@ -1,10 +0,0 @@
|
|
1
|
-
require 'rutl/interface/elements/base_element'
|
2
|
-
require 'rutl/interface/elements/string_reader_writer_mixin.rb'
|
3
|
-
|
4
|
-
#
|
5
|
-
# I'm using the text element for all text-like things. Passowrds, too.
|
6
|
-
# TODO: Also have a reader only class with StringReaderMixin for labels?
|
7
|
-
#
|
8
|
-
class Text < BaseElement
|
9
|
-
include StringReaderWriterMixin
|
10
|
-
end
|
data/lib/rutl/screencam.rb
DELETED
@@ -1,71 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
#
|
3
|
-
# class to take photos of the screen (and diff them?)
|
4
|
-
#
|
5
|
-
class ScreenCam
|
6
|
-
def guard
|
7
|
-
# When running headless, Selenium seems not to drop screenshots.
|
8
|
-
# So that makes this safe in places like Travis.
|
9
|
-
#
|
10
|
-
# We still need to guard against NullDriver or we'll to to screencap
|
11
|
-
# it when we're running head-fully.
|
12
|
-
#
|
13
|
-
# Will there be others?
|
14
|
-
@driver.is_a? NullDriver
|
15
|
-
end
|
16
|
-
|
17
|
-
def initialize(driver, base_name: '')
|
18
|
-
@counter = 0
|
19
|
-
@driver = driver
|
20
|
-
return if guard
|
21
|
-
@base_name = base_name
|
22
|
-
@dir = File.join(RUTL::SCREENSHOTS, base_name)
|
23
|
-
FileUtils.mkdir_p @dir
|
24
|
-
end
|
25
|
-
|
26
|
-
def shoot(path = nil)
|
27
|
-
return if guard
|
28
|
-
# Magic path is used for all auto-screenshots.
|
29
|
-
name = path || magic_path
|
30
|
-
|
31
|
-
FileUtils.mkdir_p @dir
|
32
|
-
file = File.join(@dir, pathify(name))
|
33
|
-
@driver.save_screenshot(file)
|
34
|
-
end
|
35
|
-
alias screenshot shoot
|
36
|
-
|
37
|
-
def clean_dir(dir)
|
38
|
-
FileUtils.rm_rf dir
|
39
|
-
FileUtils.mkdir_p dir
|
40
|
-
end
|
41
|
-
|
42
|
-
def counter
|
43
|
-
@counter += 1
|
44
|
-
# In the unlikely even that we have > 9 screenshots in a test case,
|
45
|
-
# format the counter to be two digits, zero padded.
|
46
|
-
format('%02d', @counter)
|
47
|
-
end
|
48
|
-
|
49
|
-
def magic_path
|
50
|
-
if defined? RSpec
|
51
|
-
RSpec.current_example.metadata[:full_description].to_s
|
52
|
-
else
|
53
|
-
# TODO: The behavior for non-RSpec users is ugly and broken.
|
54
|
-
# Each new test case will start taking numbered "auto-screenshot" pngs.
|
55
|
-
# And the next test case will overwrite them. Even if they didn't
|
56
|
-
# overwrite, I don't know how to correllate tests w/ scrrenshots. I'm
|
57
|
-
# leaving this broken for now.
|
58
|
-
# You can still tell it to take your own named screenshots whenever you
|
59
|
-
# like, of course.
|
60
|
-
'auto-screenshot'
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def pathify(path)
|
65
|
-
# Replace any of these with an underscore:
|
66
|
-
# space, octothorpe, slash, backslash, colon, period
|
67
|
-
name = path.gsub(%r{[ \#\/\\\:\.]}, '_')
|
68
|
-
# Also insert a counter and make sure we end with .png.
|
69
|
-
name.sub(/.png$/, '') + '_' + counter + '.png'
|
70
|
-
end
|
71
|
-
end
|