rutl 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|