spreewald 2.4.2 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -3
  3. data/README.md +87 -15
  4. data/Rakefile +10 -5
  5. data/examples/paths.rb +36 -0
  6. data/examples/selectors.rb +28 -0
  7. data/lib/spreewald/all_steps.rb +4 -1
  8. data/lib/spreewald/browser_tab_steps.rb +91 -0
  9. data/lib/spreewald/development_steps.rb +1 -1
  10. data/lib/spreewald/email_steps.rb +3 -1
  11. data/lib/spreewald/session_steps.rb +18 -0
  12. data/lib/spreewald/time_steps.rb +102 -0
  13. data/lib/spreewald/timecop_steps.rb +6 -76
  14. data/lib/spreewald/web_steps.rb +25 -25
  15. data/lib/spreewald_support/driver_info.rb +8 -0
  16. data/lib/spreewald_support/tolerance_for_selenium_sync_issues.rb +9 -3
  17. data/lib/spreewald_support/version.rb +1 -1
  18. data/lib/spreewald_support/web_steps_helpers.rb +17 -0
  19. data/support/step_definition.rb +1 -1
  20. data/support/step_definition_file.rb +2 -0
  21. data/tests/rails-3_capybara-1/Gemfile +2 -1
  22. data/tests/rails-3_capybara-1/Gemfile.lock +3 -1
  23. data/tests/rails-3_capybara-2/Gemfile +2 -1
  24. data/tests/rails-3_capybara-2/Gemfile.lock +3 -1
  25. data/tests/rails-4_capybara-3/Gemfile.lock +1 -1
  26. data/tests/rails-4_capybara-3/features/browser_tab_steps.feature +1 -0
  27. data/tests/rails-4_capybara-3/features/session_steps.feature +1 -0
  28. data/tests/rails-4_capybara-3/features/time_steps.feature +1 -0
  29. data/tests/rails-6_capybara-3/.ruby-version +1 -1
  30. data/tests/rails-6_capybara-3/Gemfile +1 -1
  31. data/tests/rails-6_capybara-3/Gemfile.lock +1 -1
  32. data/tests/rails-6_capybara-3/features/browser_tab_steps.feature +1 -0
  33. data/tests/rails-6_capybara-3/features/session_steps.feature +1 -0
  34. data/tests/rails-6_capybara-3/features/time_steps.feature +1 -0
  35. data/tests/shared/app/controllers/downloads_controller.rb +1 -1
  36. data/tests/shared/app/controllers/static_pages_controller.rb +9 -0
  37. data/tests/shared/app/views/forms/disabled_elements.html.haml +18 -0
  38. data/tests/shared/app/views/mailer/html_email_with_links.haml +2 -0
  39. data/tests/shared/app/views/mailer/text_email_with_links.text.erb +1 -0
  40. data/tests/shared/app/views/static_pages/session_1.haml +1 -0
  41. data/tests/shared/app/views/static_pages/session_2.haml +1 -0
  42. data/tests/shared/app/views/static_pages/session_3.haml +1 -0
  43. data/tests/shared/app/views/static_pages/tab_1.haml +3 -0
  44. data/tests/shared/app/views/static_pages/tab_2.haml +2 -0
  45. data/tests/shared/app/views/static_pages/tab_3.haml +1 -0
  46. data/tests/shared/app/views/static_pages/time.html.haml +2 -0
  47. data/tests/shared/config/routes.rb +9 -2
  48. data/tests/shared/features/shared/browser_tab_steps.feature +103 -0
  49. data/tests/shared/features/shared/session_steps.feature +29 -0
  50. data/tests/shared/features/shared/time_steps.feature +31 -0
  51. data/tests/shared/features/shared/web_steps.feature +8 -2
  52. metadata +37 -2
@@ -57,7 +57,7 @@ AfterStep('@slow-motion') do
57
57
  sleep 2
58
58
  end
59
59
 
60
- # Waits for keypress after each step
60
+ # Waits for a keypress after each step
61
61
  AfterStep('@single-step') do
62
62
  print "Single Stepping. Hit enter to continue"
63
63
  STDIN.getc
@@ -65,6 +65,8 @@ Then /^(an|no) e?mail should have been sent((?: |and|with|from "[^"]+"|bcc "[^"]
65
65
  end
66
66
  end.overridable
67
67
 
68
+ # Please note that this step will only follow HTTP and HTTPS links.
69
+ # Other links (such as mailto: or ftp:// links) are ignored.
68
70
  When /^I follow the (first|second|third)? ?link in the e?mail$/ do |index_in_words|
69
71
  mail = @mail || ActionMailer::Base.deliveries.last
70
72
  index = { nil => 0, 'first' => 0, 'second' => 1, 'third' => 2 }[index_in_words]
@@ -72,7 +74,7 @@ When /^I follow the (first|second|third)? ?link in the e?mail$/ do |index_in_wor
72
74
 
73
75
  paths = if mail.html_part
74
76
  dom = Nokogiri::HTML(mail.html_part.body.to_s)
75
- (dom / 'a[href]').map { |a| a['href'].match(url_pattern)[1] }
77
+ (dom / 'a[href]').map { |a| a['href'].match(url_pattern) }.compact.map { |match| match[1] }
76
78
  else
77
79
  mail_body = MailFinder.email_text_body(mail).to_s
78
80
  mail_body.scan(url_pattern).flatten(1)
@@ -0,0 +1,18 @@
1
+ # You can append `in the browser session "name"` to any other step to execute
2
+ # the step in a different browser session.
3
+ #
4
+ # You may need to update other steps to allow multiple sessions (e.g. your
5
+ # authentication steps have to support multiple logged in users).
6
+ # More details [here](https://makandracards.com/makandra/474480-how-to-make-a-cucumber-test-work-with-multiple-browser-sessions).
7
+ When /^(.*) in the browser session "([^"]+)"$/ do |nested_step, session_name|
8
+ Capybara.using_session(session_name) do
9
+ step(nested_step)
10
+ end
11
+ end.overridable(priority: 5)
12
+
13
+ # nodoc
14
+ When /^(.*) in the browser session "([^"]+)":$/ do |nested_step, session_name, table_or_string|
15
+ Capybara.using_session(session_name) do
16
+ step("#{nested_step}:", table_or_string)
17
+ end
18
+ end.overridable(priority: 5)
@@ -0,0 +1,102 @@
1
+ # coding: UTF-8
2
+
3
+
4
+ # Steps to travel through time
5
+ #
6
+ # This uses [Timecop](https://github.com/jtrupiano/timecop) or Active Support 4.1+ to stub Time.now / Time.current.
7
+ # The user is responsible for including one of the two gems.
8
+ #
9
+ # Please note that the two approaches branch. While ActiveSupport will freeze the time, Timecop will keep it running.
10
+ # FILE_COMMENT_END
11
+
12
+ major_minor_rails_version = defined?(ActiveSupport) ? [ActiveSupport::VERSION::MAJOR, ActiveSupport::VERSION::MINOR] : [0, 0]
13
+ is_at_least_rails_4_1 = (major_minor_rails_version <=> [4, 1]) != -1
14
+
15
+ if defined?(Timecop) || is_at_least_rails_4_1
16
+
17
+ module TimeHelpers
18
+
19
+ # When you have to make your rails app time zone aware you have to go 100%
20
+ # otherwise you are better off ignoring time zones at all.
21
+ # https://makandracards.com/makandra/8723-guide-to-localizing-a-rails-application
22
+
23
+ def use_timezones?
24
+ active_record_loaded = defined?(ActiveRecord::Base)
25
+ (!active_record_loaded || ActiveRecord::Base.default_timezone != :local) && Time.zone
26
+ end
27
+
28
+ def parse_time(str)
29
+ if use_timezones?
30
+ Time.zone.parse(str)
31
+ else
32
+ Time.parse(str)
33
+ end
34
+ end
35
+
36
+ def current_time
37
+ if use_timezones?
38
+ Time.current
39
+ else
40
+ Time.now
41
+ end
42
+ end
43
+
44
+ if defined?(Timecop)
45
+ # Emulate ActiveSupport time helper methods with Timecop - don't rename these methods
46
+ def travel(duration)
47
+ Timecop.travel(current_time + duration)
48
+ end
49
+
50
+ def travel_to(date_or_time)
51
+ Timecop.travel(date_or_time)
52
+ end
53
+
54
+ def travel_back
55
+ Timecop.return
56
+ end
57
+ else
58
+ require 'active_support/testing/time_helpers'
59
+ include ActiveSupport::Testing::TimeHelpers
60
+ end
61
+
62
+ end
63
+
64
+ World(TimeHelpers)
65
+
66
+ # Example:
67
+ #
68
+ # Given the date is 2012-02-10
69
+ # Given the time is 2012-02-10 13:40
70
+ When /^the (?:date|time) is "?(\d{4}-\d{2}-\d{2}(?: \d{1,2}:\d{2})?)"?$/ do |time|
71
+ travel_to parse_time(time)
72
+ end.overridable
73
+
74
+ # Example:
75
+ #
76
+ # Given the time is 13:40
77
+ When /^the time is "?(\d{1,2}:\d{2})"?$/ do |time_without_date|
78
+ travel_to parse_time(time_without_date) # date will be today
79
+ end.overridable
80
+
81
+ # Example:
82
+ #
83
+ # When it is 10 minutes later
84
+ # When it is a few hours earlier
85
+ When /^it is (\d+|an?|some|a few) (seconds?|minutes?|hours?|days?|weeks?|months?|years?) (later|earlier)$/ do |amount, unit, direction|
86
+ amount = case amount
87
+ when 'a', 'an'
88
+ 1
89
+ when 'some', 'a few'
90
+ 10
91
+ else
92
+ amount.to_i
93
+ end
94
+ amount = -amount if direction == 'earlier'
95
+ travel amount.send(unit)
96
+ end.overridable
97
+
98
+ After do
99
+ travel_back
100
+ end
101
+
102
+ end
@@ -1,79 +1,9 @@
1
- # coding: UTF-8
2
-
3
-
4
- # Steps to travel through time using [Timecop](https://github.com/jtrupiano/timecop).
5
- #
6
- # See [this article](https://makandracards.com/makandra/1222-useful-cucumber-steps-to-travel-through-time-with-timecop) for details.
1
+ # nodoc
7
2
  # FILE_COMMENT_END
8
3
 
4
+ warn <<-WARNING
5
+ Warning: The file spreewald/timecop_steps.rb is deprecated. It was moved to
6
+ spreewald/time_steps.rb. Please require the new file instead.
7
+ WARNING
9
8
 
10
- if defined?(Timecop)
11
-
12
- module TimecopHarness
13
-
14
- # When you have to make your rails app time zone aware you have to go 100%
15
- # otherwise you are better off ignoring time zones at all.
16
- # https://makandracards.com/makandra/8723-guide-to-localizing-a-rails-application
17
-
18
- def use_timezones?
19
- active_record_loaded = defined?(ActiveRecord::Base)
20
- (!active_record_loaded || ActiveRecord::Base.default_timezone != :local) && Time.zone
21
- end
22
-
23
- def parse_time(str)
24
- if use_timezones?
25
- Time.zone.parse(str)
26
- else
27
- Time.parse(str)
28
- end
29
- end
30
-
31
- def current_time
32
- if use_timezones?
33
- Time.current
34
- else
35
- Time.now
36
- end
37
- end
38
-
39
- end
40
-
41
- World(TimecopHarness)
42
-
43
- # Example:
44
- #
45
- # Given the date is 2012-02-10
46
- # Given the time is 2012-02-10 13:40
47
- When /^the (?:date|time) is "?(\d{4}-\d{2}-\d{2}(?: \d{1,2}:\d{2})?)"?$/ do |time|
48
- Timecop.travel(parse_time(time))
49
- end.overridable
50
-
51
- # Example:
52
- #
53
- # Given the time is 13:40
54
- When /^the time is "?(\d{1,2}:\d{2})"?$/ do |time_without_date|
55
- Timecop.travel(parse_time(time_without_date)) # date will be today
56
- end.overridable
57
-
58
- # Example:
59
- #
60
- # When it is 10 minutes later
61
- # When it is a few hours earlier
62
- When /^it is (\d+|a|some|a few) (seconds?|minutes?|hours?|days?|weeks?|months?|years?) (later|earlier)$/ do |amount, unit, direction|
63
- amount = case amount
64
- when 'a'
65
- 1
66
- when 'some', 'a few'
67
- 10
68
- else
69
- amount.to_i
70
- end
71
- amount = -amount if direction == 'earlier'
72
- Timecop.travel(current_time + amount.send(unit))
73
- end.overridable
74
-
75
- After do
76
- Timecop.return
77
- end
78
-
79
- end
9
+ require 'spreewald/time_steps'
@@ -35,7 +35,7 @@ require 'uri'
35
35
  require 'cgi'
36
36
 
37
37
 
38
- # You can append `within [selector]` to any other web step.
38
+ # You can append `within [selector]` to any other web step, even multiple times.
39
39
  # Be aware that within will only look at the first element that matches.
40
40
  # If this is a problem for you following links, you might want to have a look
41
41
  # at the 'When I follow "..." inside any "..."'-step.
@@ -463,7 +463,18 @@ end.overridable
463
463
  #
464
464
  # Attention: Doesn't work with Selenium, see https://github.com/jnicklas/capybara#gotchas
465
465
  Then /^I should get a download with filename "([^\"]*)"$/ do |filename|
466
- expect(page.response_headers['Content-Disposition']).to match /filename="#{Regexp.escape(filename)}"(;|$)/
466
+ content_disposition = page.response_headers['Content-Disposition']
467
+ expect(content_disposition).to be_present
468
+
469
+ fields = content_disposition.split(/;\s*/)
470
+
471
+ # Rails 6+ encodes filenames as defined in https://tools.ietf.org/html/rfc5987.
472
+ # If available, we prefer the UTF-8 filename.
473
+ download_filename = [/^filename\*=UTF-8''(.+)/, /^filename="(.+?)"/].each do |regexp|
474
+ matched_filename = fields.map { |field| field.scan(regexp).flatten.first }.compact.first
475
+ break CGI.unescape(matched_filename) if matched_filename
476
+ end
477
+ expect(download_filename).to eq(filename)
467
478
  end.overridable
468
479
 
469
480
  # Checks that a certain option is selected for a text field
@@ -595,7 +606,7 @@ end.overridable(priority: -5) # priority lower than within
595
606
  #
596
607
  # Then "Sponsor" should link to "http://makandra.com/"
597
608
  #
598
- # Don't forget the trailing slash. Otherwise you'll get the error
609
+ # Don't forget the trailing slash. Otherwise you'll get the error
599
610
  # expected: /http:\/\/makandra.com(\?[^\/]*)?$/
600
611
  # got: "http://makandra.com/" (using =~)
601
612
  Then /^"([^"]*)" should link to "([^"]*)"$/ do |link_label, target|
@@ -641,49 +652,38 @@ end.overridable
641
652
 
642
653
  When /^I confirm the browser dialog$/ do
643
654
  patiently do
644
- page.driver.browser.switch_to.alert.accept
655
+ browser.switch_to.alert.accept
645
656
  end
646
657
  end.overridable
647
658
 
648
659
  When /^I cancel the browser dialog$/ do
649
660
  patiently do
650
- page.driver.browser.switch_to.alert.dismiss
661
+ browser.switch_to.alert.dismiss
651
662
  end
652
663
  end.overridable
653
664
 
654
665
  When /^I enter "([^"]*)" into the browser dialog$/ do |text|
655
666
  patiently do
656
- alert = page.driver.browser.switch_to.alert
667
+ alert = browser.switch_to.alert
657
668
  alert.send_keys(text)
658
669
  alert.accept
659
670
  end
660
671
  end.overridable
661
672
 
662
- When /^I switch to the new tab$/ do
663
- if javascript_capable?
664
- page.driver.browser.switch_to.window(page.driver.browser.window_handles.last)
673
+ # Tests that an input, button, checkbox or radio button with the given label is disabled.
674
+ Then /^the "([^\"]*)" (field|button|checkbox|radio button) should( not)? be disabled$/ do |label, kind, negate|
675
+ element = if kind == 'button'
676
+ find_with_disabled(:button, label)
665
677
  else
666
- raise("This step works only with selenium")
678
+ find_with_disabled(:field, label)
667
679
  end
668
- end.overridable
669
680
 
670
- # Tests that an input, button or checkbox with the given label is disabled.
671
- Then /^the "([^\"]*)" (field|button|checkbox) should( not)? be disabled$/ do |label, kind, negate|
672
681
  if Spreewald::Comparison.compare_versions(Capybara::VERSION, :<, "2.1")
673
- if kind == 'field' || kind == 'checkbox'
674
- element = find_field(label)
675
- else
676
- element = find_button(label)
677
- end
678
682
  expect(element[:disabled]).send(negate ? :to : :not_to, eq(nil))
679
683
  else
680
- if kind == 'field' || kind == 'checkbox'
681
- element = find_field(label, disabled: !negate)
682
- else
683
- element = find_button(label, disabled: !negate)
684
- end
685
- expect(element).to be_present
684
+ expect(!!element.disabled?).to be !negate
686
685
  end
686
+
687
687
  end.overridable
688
688
 
689
689
  # Tests that a field with the given label is visible.
@@ -728,7 +728,7 @@ When /^I perform basic authentication as "([^\"]*)\/([^\"]*)" and go to (.*)$/ d
728
728
  visit("http://#{user}:#{password}@#{server.host}:#{server.port}#{path}")
729
729
  else
730
730
  authorizers = [
731
- (page.driver.browser if page.driver.respond_to?(:browser)),
731
+ (browser),
732
732
  (self),
733
733
  (page.driver)
734
734
  ].compact
@@ -17,6 +17,14 @@ module Spreewald
17
17
  Object.const_defined?('Capybara::Webkit') && Capybara.current_session.driver.is_a?(Capybara::Webkit::Driver)
18
18
  end
19
19
 
20
+ def browser
21
+ page.driver.browser if page.driver.respond_to?(:browser)
22
+ end
23
+
24
+ def require_selenium!
25
+ raise 'This step only works with Selenium' unless selenium_driver?
26
+ end
27
+
20
28
  end
21
29
  end
22
30
 
@@ -38,22 +38,28 @@ module ToleranceForSeleniumSyncIssues
38
38
  WAIT_PERIOD = 0.05
39
39
 
40
40
  def patiently(seconds, &block)
41
- started = Time.now
41
+ started = monotonic_time
42
42
  tries = 0
43
43
  begin
44
44
  tries += 1
45
45
  block.call
46
46
  rescue Exception => e
47
47
  raise e unless retryable_error?(e)
48
- raise e if (Time.now - started > seconds && tries >= 2)
48
+ raise e if (monotonic_time - started > seconds && tries >= 2)
49
49
  sleep(WAIT_PERIOD)
50
- raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Time.now == started
50
+ raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if monotonic_time == started
51
51
  retry
52
52
  end
53
53
  end
54
54
 
55
55
  private
56
56
 
57
+ def monotonic_time
58
+ # We use the system clock (i.e. seconds since boot) to calculate the time,
59
+ # because Time.now may be frozen
60
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
+ end
62
+
57
63
  def retryable_error?(e)
58
64
  RETRY_ERRORS.include?(e.class.name)
59
65
  end
@@ -1,3 +1,3 @@
1
1
  module Spreewald
2
- VERSION = '2.4.2'
2
+ VERSION = '2.8.0'
3
3
  end
@@ -1,3 +1,5 @@
1
+ require 'spreewald_support/comparison'
2
+
1
3
  module WebStepsHelpers
2
4
 
3
5
  def assert_visible(options)
@@ -8,6 +10,21 @@ module WebStepsHelpers
8
10
  visibility_test(options.merge(:expectation => :hidden))
9
11
  end
10
12
 
13
+ def find_with_disabled(*args, **options, &optional_filter_block)
14
+ if Spreewald::Comparison.compare_versions(Capybara::VERSION, :<, "2.0")
15
+ find(:xpath, XPath::HTML.send(args[0], args[1])) # Versions < 2.0 will find both enabled and disabled fields, nothing to do
16
+ elsif Spreewald::Comparison.compare_versions(Capybara::VERSION, :<, "2.6")
17
+ begin
18
+ find(*args, **options, disabled: false, &optional_filter_block)
19
+ rescue Capybara::ElementNotFound
20
+ find(*args, **options, disabled: true, &optional_filter_block)
21
+ end
22
+ else
23
+ options_with_disabled = { disabled: :all }.merge(options)
24
+ find(*args, **options_with_disabled, &optional_filter_block)
25
+ end
26
+ end
27
+
11
28
  private
12
29
 
13
30
  def visibility_test(options)
@@ -37,7 +37,7 @@ class StepDefinition
37
37
 
38
38
  def pretty_step
39
39
  if kind == 'AfterStep'
40
- step[/@\w+/]
40
+ step[/@[\w-]+/]
41
41
  elsif step =~ /^'(.*)'$/ # Surrounded by single quotes
42
42
  $1
43
43
  else
@@ -16,6 +16,8 @@ class StepDefinitionFile
16
16
  end
17
17
 
18
18
  def to_markdown
19
+ return '' if @comment =~ /\bnodoc\b/
20
+
19
21
  spaced_comment = "\n\n" + @comment if @comment
20
22
 
21
23
  <<-TEXT
@@ -16,4 +16,5 @@ group :test do
16
16
  gem 'selenium-webdriver'
17
17
  gem 'rspec-rails'
18
18
  gem 'spreewald', :path => '../..'
19
- end
19
+ gem 'timecop'
20
+ end