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
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
|