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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03966911ea3475762f121b4735aaee43a66af675c6b5baa255e329810c5fd890'
4
- data.tar.gz: 4fbc7ec877d27b7daddae7f02c5b35396d9b6a5f59ffee90a5e4a29896627e1c
3
+ metadata.gz: 67625dcf7a3007342c0ea59bedc973fb833ff74ffd41eec945b0c2dd6f1022ef
4
+ data.tar.gz: 22f30517ae2fc0f2c33263db4443285a04f12d212174a7519f0bc58b07006283
5
5
  SHA512:
6
- metadata.gz: 1cfe7eecf5fb7ab352d10408412835b827b7cb276959efeac9085a538d008bd8a829ad468b158135353b85c4a9277a6af9cab5eae200932667c10ce425c6e2e5
7
- data.tar.gz: ab287a5457fd4b3fe81ed869b3316f3165c1f97fbd92c3af04c42a935366488911f5e0b51d335c6c89621817ada69d766eec157a00f8db0317d1f7271475d095
6
+ metadata.gz: 55d5a134ee68df90af1621f591852cba3a4fa362c3bd37fee884b913826ea2847c9784a786d1dc5478f4b5390a82acd2138263cc304afd2a91158ccee32423ae
7
+ data.tar.gz: d0600c7d541a0a226e635d94421d899dff5998a386decd0b71efad990e9ef4a8f36f8f68708991af6cceb5926c85aaee47fd98fd9d8191d412833407d1b41dca
@@ -7,7 +7,8 @@ jobs:
7
7
  build:
8
8
  docker:
9
9
  # specify the version you desire here
10
- - image: circleci/ruby:2.4.1-node-browsers
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
-
@@ -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'
@@ -1,23 +1,29 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2018-06-04 00:16:13 -0700 using RuboCop version 0.56.0.
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/base_element.rb'
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: 3
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'
@@ -5,3 +5,4 @@ language: ruby
5
5
  rvm:
6
6
  - 2.3.1
7
7
  before_install: gem install bundler -v 1.13.6
8
+ script: bundle exec rspec
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 error pages/partials.
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.
@@ -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) && expected.ancestors.include?(BasePage)
9
+ actual.is_a?(expected) && page?(expected)
7
10
  end
8
11
  end
@@ -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
@@ -1,65 +1,70 @@
1
- require 'rutl/utilities'
2
- require 'rutl/base_page'
1
+ require 'utilities'
2
+ require 'rutl/page'
3
3
 
4
- #
5
- # Currently called Browser, this top-level class controls a browser and
6
- # a fake browser. It will soon call into apps, at which point I need to
7
- # rethink this naming convention.
8
- #
9
- class Browser
10
- include Utilities
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
- attr_reader :interface
13
+ attr_reader :interface
13
14
 
14
- def initialize(type:, rutl_pages: RUTL::PAGES || ENV['RUTL_PAGES'])
15
- if rutl_pages.nil? || rutl_pages.empty?
16
- raise "Set RUTL::PAGES or ENV['RUTL_PAGES'] or pass dir as rutl_pages:"
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
- # Ugly. Requires files for page objects. Returns array of class names to load.
32
- def require_pages(dir: 'spec/pages')
33
- names = []
34
- Dir["#{dir}/*"].each do |file|
35
- require "rutl/../../#{file}"
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
- def load_pages(*)
45
- pages = []
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
- def method_missing(method, *args, &block)
55
- if args.empty?
56
- @interface.send(method)
57
- else
58
- @interface.send(method, *args, &block)
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
- def respond_to_missing?(*args)
63
- @interface.respond_to?(*args)
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
@@ -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 'rutl/utilities'
2
- require 'rutl/screencam'
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
- attr_accessor :driver
11
- attr_reader :camera
12
- attr_accessor :pages
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
- def initialize
15
- raise 'Child interface class must set @driver.' if @driver.nil?
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
- def goto(page)
23
- raise 'expect Page class' unless page.ancestors.include?(BasePage)
24
- find_page(page).go_to_here
25
- @camera.screenshot
26
- end
15
+ # RUTL::Camera
16
+ attr_accessor :camera
27
17
 
28
- def current_page
29
- raise 'define in child classes'
30
- end
18
+ # Array of all RUTL::Page classes
19
+ attr_accessor :pages
31
20
 
32
- def method_missing(method, *args, &block)
33
- if args.empty?
34
- current_page.send(method)
35
- else
36
- current_page.send(method, *args, &block)
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
- # TODO: Is this needed? I not only find the page but also make sure the
41
- # urls match. Even though that's what finding pages means?
42
- def find_state(target_states)
43
- target_states.each do |state|
44
- next unless state.url == current_page.url
45
- page = find_page(state)
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
- def find_page(page)
52
- @pages.each do |p|
53
- return p if page?(page) && p.class == page
54
- return p if String == page.class && page == p.url
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
- def wait_for_transition(target_states)
60
- #
61
- # TODO: Should also see if there are other things to wait for.
62
- # I don't think this is doing page load time.
63
- #
64
- await -> { find_state target_states }
65
- end
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
- def respond_to_missing?(*args)
68
- # This can't be right. Figure it out later.
69
- current_page.respond_to?(*args)
70
- end
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
- def quit
73
- @driver.quit
74
- # Maybe I'm reusing pages from @pages?
75
- # @pages = []
86
+ def quit
87
+ @driver.quit
88
+ end
76
89
  end
77
90
  end