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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67625dcf7a3007342c0ea59bedc973fb833ff74ffd41eec945b0c2dd6f1022ef
|
4
|
+
data.tar.gz: 22f30517ae2fc0f2c33263db4443285a04f12d212174a7519f0bc58b07006283
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55d5a134ee68df90af1621f591852cba3a4fa362c3bd37fee884b913826ea2847c9784a786d1dc5478f4b5390a82acd2138263cc304afd2a91158ccee32423ae
|
7
|
+
data.tar.gz: d0600c7d541a0a226e635d94421d899dff5998a386decd0b71efad990e9ef4a8f36f8f68708991af6cceb5926c85aaee47fd98fd9d8191d412833407d1b41dca
|
data/.circleci/config.yml
CHANGED
@@ -7,7 +7,8 @@ jobs:
|
|
7
7
|
build:
|
8
8
|
docker:
|
9
9
|
# specify the version you desire here
|
10
|
-
- image: circleci/ruby:2.
|
10
|
+
- image: circleci/ruby:2.5.1-stretch-node-browsers
|
11
|
+
#- image: circleci/ruby:2.4.1-node-browsers
|
11
12
|
|
12
13
|
# Specify service dependencies here if necessary
|
13
14
|
# CircleCI maintains a library of pre-built images
|
@@ -71,4 +72,3 @@ jobs:
|
|
71
72
|
- store_artifacts:
|
72
73
|
path: /tmp/test-results
|
73
74
|
destination: test-results
|
74
|
-
|
data/.rubocop.yml
CHANGED
@@ -42,3 +42,10 @@ Style/ClassVars:
|
|
42
42
|
Exclude:
|
43
43
|
- 'lib/rutl/driver/null_driver_page_element.rb'
|
44
44
|
- 'lib/rutl/base_page.rb'
|
45
|
+
|
46
|
+
# Rubocop flags lots of things as useless assignment when they're actually
|
47
|
+
# magic methods. Maybe this means I'm not handling respond_to_missing
|
48
|
+
# correctly. In fact, that seems likely.
|
49
|
+
Lint/UselessAssignment:
|
50
|
+
Exclude:
|
51
|
+
- 'spec/*_spec.rb'
|
data/.rubocop_todo.yml
CHANGED
@@ -1,23 +1,29 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2018-06-
|
3
|
+
# on 2018-06-07 12:55:41 -0700 using RuboCop version 0.56.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
|
+
# Offense count: 1
|
10
|
+
# Configuration parameters: CountComments.
|
11
|
+
Metrics/MethodLength:
|
12
|
+
Max: 11
|
13
|
+
|
9
14
|
# Offense count: 4
|
10
15
|
# Configuration parameters: AllowedVariables.
|
11
16
|
Style/GlobalVars:
|
12
17
|
Exclude:
|
13
18
|
- 'lib/rutl/browser.rb'
|
14
|
-
- 'lib/rutl/interface/elements/
|
19
|
+
- 'lib/rutl/interface/elements/element.rb'
|
15
20
|
- 'lib/rutl/interface/elements/element_context.rb'
|
16
21
|
- 'lib/rutl/interface/null_interface.rb'
|
17
22
|
|
18
|
-
# Offense count:
|
23
|
+
# Offense count: 4
|
19
24
|
Style/MethodMissingSuper:
|
20
25
|
Exclude:
|
21
|
-
- 'lib/rutl/base_page.rb'
|
22
26
|
- 'lib/rutl/browser.rb'
|
23
27
|
- 'lib/rutl/interface/base_interface.rb'
|
28
|
+
- 'lib/rutl/interface/elements/element.rb'
|
29
|
+
- 'lib/rutl/page.rb'
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -169,8 +169,7 @@ your tests screenshot anyway, just less magic.
|
|
169
169
|
|
170
170
|
## Roadmap
|
171
171
|
Coming up soon in almost no order:
|
172
|
-
* Handle
|
173
|
-
* Auto-screenshot on errors. Error destinations. Navigation errors. Unexpected exceptions?
|
172
|
+
* Handle other errors. Auto-screenshot on errors. Navigation errors. Unexpected exceptions?
|
174
173
|
* A test framework should have better tests.
|
175
174
|
* Diff screenshots. Make this smart so we don't have to be experts.
|
176
175
|
* Put more info in this readme.
|
data/lib/rspec/rutl_matchers.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
require 'utilities'
|
1
2
|
#
|
2
3
|
# Additional RSpec matchers specific to this framework go here.
|
3
4
|
#
|
5
|
+
|
6
|
+
# Is it the expected page?
|
4
7
|
RSpec::Matchers.define :be_page do |expected|
|
5
8
|
match do |actual|
|
6
|
-
actual.is_a?(expected) &&
|
9
|
+
actual.is_a?(expected) && page?(expected)
|
7
10
|
end
|
8
11
|
end
|
data/lib/rutl.rb
CHANGED
@@ -9,13 +9,13 @@ module RUTL
|
|
9
9
|
# Should define RUTL::PAGES directory for your code
|
10
10
|
# or set ENV['RUTL_PAGES']
|
11
11
|
# or Browser intialize will raise.
|
12
|
-
PAGES = nil
|
12
|
+
# PAGES = nil
|
13
13
|
|
14
14
|
# If this RUTL::SCREENSHOT_DIR or ENV['SCREENSHOT_DIR']
|
15
15
|
# or Browser initialize is set, we take screenshots.
|
16
|
-
SCREENSHOTS = nil
|
16
|
+
# SCREENSHOTS = nil
|
17
17
|
|
18
18
|
# This one is for diffing against.
|
19
19
|
# RUTL::KNOWN_GOOD_SCREENSHOTS
|
20
|
-
REFERENCE_SCREENSHOTS = nil
|
20
|
+
# REFERENCE_SCREENSHOTS = nil
|
21
21
|
end
|
data/lib/rutl/browser.rb
CHANGED
@@ -1,65 +1,70 @@
|
|
1
|
-
require '
|
2
|
-
require 'rutl/
|
1
|
+
require 'utilities'
|
2
|
+
require 'rutl/page'
|
3
3
|
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
4
|
+
module RUTL
|
5
|
+
#
|
6
|
+
# Currently called Browser, 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 Browser
|
11
|
+
include Utilities
|
11
12
|
|
12
|
-
|
13
|
+
attr_reader :interface
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def initialize(type:, rutl_pages: RUTL::PAGES || ENV['RUTL_PAGES'])
|
16
|
+
if rutl_pages.nil? || rutl_pages.empty?
|
17
|
+
raise "Set RUTL::PAGES or ENV['RUTL_PAGES'] or pass dir as rutl_pages:"
|
18
|
+
end
|
19
|
+
# This is kind of evil. Figure out how to ditch the $ variable.
|
20
|
+
$browser = self
|
21
|
+
@interface = nil # TODO: Why this line? Do I need to do this?
|
22
|
+
@interface = load_interface(type)
|
23
|
+
@interface.pages = load_pages(dir: rutl_pages)
|
17
24
|
end
|
18
|
-
# This is kind of evil. Figure out how to ditch the $ variable.
|
19
|
-
$browser = self
|
20
|
-
@interface = nil
|
21
|
-
@interface = load_interface(type)
|
22
|
-
@interface.pages = load_pages(dir: rutl_pages)
|
23
|
-
end
|
24
|
-
|
25
|
-
def load_interface(type)
|
26
|
-
require "rutl/interface/#{type}_interface"
|
27
|
-
klass = "#{type.to_s.capitalize}Interface"
|
28
|
-
Object.const_get(klass).new
|
29
|
-
end
|
30
25
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
File.open(file).each do |line|
|
37
|
-
bingo = line.match(/class (.*) < BasePage/)
|
38
|
-
names << bingo[1] if bingo && bingo[1]
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
if args.empty?
|
28
|
+
@interface.send(method)
|
29
|
+
else
|
30
|
+
@interface.send(method, *args, &block)
|
39
31
|
end
|
40
32
|
end
|
41
|
-
names
|
42
|
-
end
|
43
33
|
|
44
|
-
|
45
|
-
|
46
|
-
require_pages.each do |klass|
|
47
|
-
# Don't have @interface set yet.
|
48
|
-
# That would have been the param to new, :interface.
|
49
|
-
pages << Object.const_get(klass).new(@interface)
|
34
|
+
def respond_to_missing?(*args)
|
35
|
+
@interface.respond_to?(*args)
|
50
36
|
end
|
51
|
-
pages
|
52
|
-
end
|
53
37
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
38
|
+
private
|
39
|
+
|
40
|
+
def load_interface(type)
|
41
|
+
require "rutl/interface/#{type}_interface"
|
42
|
+
klass = "RUTL::#{type.to_s.capitalize}Interface"
|
43
|
+
Object.const_get(klass).new
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_pages(*)
|
47
|
+
pages = []
|
48
|
+
require_pages.each do |klass|
|
49
|
+
# Don't have @interface set yet.
|
50
|
+
# That would have been the param to new, :interface.
|
51
|
+
pages << Object.const_get(klass).new(@interface)
|
52
|
+
end
|
53
|
+
pages
|
59
54
|
end
|
60
|
-
end
|
61
55
|
|
62
|
-
|
63
|
-
|
56
|
+
# Ugly. Requires files for page objects.
|
57
|
+
# Returns array of class names to load.
|
58
|
+
def require_pages(dir: 'spec/pages')
|
59
|
+
names = []
|
60
|
+
Dir["#{dir}/*"].each do |file|
|
61
|
+
require "rutl/../../#{file}"
|
62
|
+
File.open(file).each do |line|
|
63
|
+
bingo = line.match(/class (.*) < RUTL::Page/)
|
64
|
+
names << bingo[1] if bingo && bingo[1]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
names
|
68
|
+
end
|
64
69
|
end
|
65
70
|
end
|
data/lib/rutl/camera.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module RUTL
|
4
|
+
#
|
5
|
+
# class to take photos of the screen (and diff them?)
|
6
|
+
#
|
7
|
+
class Camera
|
8
|
+
def guard
|
9
|
+
# When running headless, Selenium seems not to drop screenshots.
|
10
|
+
# So that makes this safe in places like Travis.
|
11
|
+
#
|
12
|
+
# We still need to guard against NullDriver or we'll to to screencap
|
13
|
+
# it when we're running head-fully.
|
14
|
+
#
|
15
|
+
# Will there be others?
|
16
|
+
@driver.is_a? RUTL::NullDriver
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(driver, base_name: '')
|
20
|
+
@counter = 0
|
21
|
+
@driver = driver
|
22
|
+
return if guard
|
23
|
+
@base_name = base_name.sub('RUTL::', '')
|
24
|
+
@dir = File.join(RUTL::SCREENSHOTS, @base_name)
|
25
|
+
FileUtils.mkdir_p @dir
|
26
|
+
end
|
27
|
+
|
28
|
+
def shoot(path = nil)
|
29
|
+
return if guard
|
30
|
+
# Magic path is used for all auto-screenshots.
|
31
|
+
name = path || magic_path
|
32
|
+
|
33
|
+
FileUtils.mkdir_p @dir
|
34
|
+
file = File.join(@dir, pathify(name))
|
35
|
+
@driver.save_screenshot(file)
|
36
|
+
end
|
37
|
+
alias screenshot shoot
|
38
|
+
|
39
|
+
def clean_dir(dir)
|
40
|
+
FileUtils.rm_rf dir
|
41
|
+
FileUtils.mkdir_p dir
|
42
|
+
end
|
43
|
+
|
44
|
+
def counter
|
45
|
+
@counter += 1
|
46
|
+
# In the unlikely even that we have > 9 screenshots in a test case,
|
47
|
+
# format the counter to be two digits, zero padded.
|
48
|
+
format('%02d', @counter)
|
49
|
+
end
|
50
|
+
|
51
|
+
def magic_path
|
52
|
+
if defined? RSpec
|
53
|
+
RSpec.current_example.metadata[:full_description].to_s
|
54
|
+
else
|
55
|
+
# TODO: The behavior for non-RSpec users is ugly and broken.
|
56
|
+
# Each new test case will start taking numbered "auto-screenshot" pngs.
|
57
|
+
# And the next test case will overwrite them. Even if they didn't
|
58
|
+
# overwrite, I don't know how to correllate tests w/ scrrenshots. I'm
|
59
|
+
# leaving this broken for now.
|
60
|
+
# You can still tell it to take your own named screenshots whenever you
|
61
|
+
# like, of course.
|
62
|
+
'auto-screenshot'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def pathify(path)
|
67
|
+
# Replace any of these with an underscore:
|
68
|
+
# space, octothorpe, slash, backslash, colon, period
|
69
|
+
name = path.gsub(%r{[ \#\/\\\:\.]}, '_')
|
70
|
+
# Also insert a counter and make sure we end with .png.
|
71
|
+
name.sub(/.png$/, '') + '_' + counter + '.png'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -1,77 +1,90 @@
|
|
1
|
-
require '
|
2
|
-
require 'rutl/
|
3
|
-
#
|
4
|
-
# I might need to consider renaming these.
|
5
|
-
# The *interface classes lie between Browser and the webdriver-level classes.
|
6
|
-
#
|
7
|
-
class BaseInterface
|
8
|
-
include Utilities
|
1
|
+
require 'utilities'
|
2
|
+
require 'rutl/camera'
|
9
3
|
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
module RUTL
|
5
|
+
#
|
6
|
+
# I might need to consider renaming these.
|
7
|
+
# The *interface classes lie between Browser and the webdriver-level classes.
|
8
|
+
#
|
9
|
+
class BaseInterface
|
10
|
+
include Utilities
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
# base_name avoids collisions when unning the same tests with
|
17
|
-
# different browsers.
|
18
|
-
name = self.class.to_s .sub('Interface', '')
|
19
|
-
@camera = ScreenCam.new(@driver, base_name: name)
|
20
|
-
end
|
12
|
+
# RUTL::Driver
|
13
|
+
attr_accessor :driver
|
21
14
|
|
22
|
-
|
23
|
-
|
24
|
-
find_page(page).go_to_here
|
25
|
-
@camera.screenshot
|
26
|
-
end
|
15
|
+
# RUTL::Camera
|
16
|
+
attr_accessor :camera
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
end
|
18
|
+
# Array of all RUTL::Page classes
|
19
|
+
attr_accessor :pages
|
31
20
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
21
|
+
def initialize
|
22
|
+
raise 'Child interface class must set @driver.' if @driver.nil?
|
23
|
+
# base_name avoids collisions when unning the same tests with
|
24
|
+
# different browsers.
|
25
|
+
name = self.class.to_s.sub('RUTL::Interface', '')
|
26
|
+
@camera = Camera.new(@driver, base_name: name)
|
37
27
|
end
|
38
|
-
end
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
return page if page.loaded?(@driver)
|
29
|
+
# Attempts to navigate to the page.
|
30
|
+
# Takes screenshot if successful.
|
31
|
+
def goto(page)
|
32
|
+
raise 'expect Page class' unless page?(page)
|
33
|
+
find_page(page).go_to_here
|
34
|
+
@camera.screenshot
|
47
35
|
end
|
48
|
-
false
|
49
|
-
end
|
50
36
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
37
|
+
# Should define in children; raises here.
|
38
|
+
# Should return the current page class.
|
39
|
+
def current_page
|
40
|
+
raise 'define in child classes'
|
55
41
|
end
|
56
|
-
raise "Page \"#{page}\" not found in pages #{@pages}"
|
57
|
-
end
|
58
42
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
43
|
+
def method_missing(method, *args, &block)
|
44
|
+
if args.empty?
|
45
|
+
current_page.send(method)
|
46
|
+
else
|
47
|
+
current_page.send(method, *args, &block)
|
48
|
+
end
|
49
|
+
end
|
66
50
|
|
67
|
-
|
68
|
-
#
|
69
|
-
|
70
|
-
|
51
|
+
# TODO: Is this needed? I not only find the page but also make sure the
|
52
|
+
# urls match. Even though that's what finding pages means?
|
53
|
+
def find_state(target_states)
|
54
|
+
target_states.each do |state|
|
55
|
+
next unless state.url == current_page.url
|
56
|
+
page = find_page(state)
|
57
|
+
return page if page.loaded?
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# Attempts to find page by class or url.
|
63
|
+
def find_page(page)
|
64
|
+
@pages.each do |p|
|
65
|
+
return p if page?(page) && p.class == page
|
66
|
+
return p if String == page.class && page == p.url
|
67
|
+
end
|
68
|
+
raise "Page \"#{page}\" not found in pages #{@pages}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Calls the polling utility mathod await() with a lambda trying to
|
72
|
+
# find the next state, probably a Page class.
|
73
|
+
def wait_for_transition(target_states)
|
74
|
+
#
|
75
|
+
# TODO: Should also see if there are other things to wait for.
|
76
|
+
# I don't think this is doing page load time.
|
77
|
+
#
|
78
|
+
await -> { find_state target_states }
|
79
|
+
end
|
80
|
+
|
81
|
+
def respond_to_missing?(*args)
|
82
|
+
# This can't be right. Figure it out later.
|
83
|
+
current_page.respond_to?(*args)
|
84
|
+
end
|
71
85
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
# @pages = []
|
86
|
+
def quit
|
87
|
+
@driver.quit
|
88
|
+
end
|
76
89
|
end
|
77
90
|
end
|