rsel 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,3 +3,6 @@ Gemfile.lock
3
3
  .rvmrc
4
4
  selenium-rc.log
5
5
  pkg/*
6
+ *~
7
+ coverage/*
8
+ doc/*
data/.yardopts CHANGED
@@ -1,7 +1,17 @@
1
1
  --readme docs/index.md
2
2
  --markup markdown
3
3
  --no-highlight
4
- --exclude lib/rsel/selenium.rb
5
4
  lib/rsel/*.rb
6
5
  -
7
- docs/*.md
6
+ docs/index.md
7
+ docs/install.md
8
+ docs/usage.md
9
+ docs/fitnesse.md
10
+ docs/locators.md
11
+ docs/scoping.md
12
+ docs/examples.md
13
+ docs/custom.md
14
+ docs/scenarios.md
15
+ docs/development.md
16
+ docs/todo.md
17
+ docs/history.md
data/Rakefile CHANGED
@@ -50,6 +50,32 @@ RSpec::Core::RakeTask.new(:spec) do |t|
50
50
  t.rspec_opts = ['--color', '--format doc']
51
51
  end
52
52
 
53
+ namespace 'rcov' do
54
+ desc "Run support spec tests with coverage analysis"
55
+ RSpec::Core::RakeTask.new(:support) do |t|
56
+ t.pattern = 'spec/support_spec.rb'
57
+ t.rspec_opts = ['--color', '--format doc']
58
+ t.rcov = true
59
+ t.rcov_opts = [
60
+ '--exclude /.gem/,/gems/,spec',
61
+ '--include lib/**/*.rb',
62
+ ]
63
+ end
64
+
65
+ desc "Run support spec tests with coverage analysis"
66
+ RSpec::Core::RakeTask.new(:all) do |t|
67
+ t.pattern = 'spec/**/*.rb'
68
+ t.rspec_opts = ['--color', '--format doc']
69
+ t.rcov = true
70
+ t.rcov_opts = [
71
+ '--exclude /.gem/,/gems/,spec',
72
+ '--include lib/**/*.rb',
73
+ # Ensure the main .rb file gets included
74
+ '--include-file lib/rsel/selenium_test.rb',
75
+ ]
76
+ end
77
+ end
78
+
53
79
  namespace 'servers' do
54
80
  desc "Start the Selenium and testapp servers"
55
81
  task :start do
@@ -6,6 +6,29 @@ If you would like to contribute to development of Rsel, create a fork of the
6
6
  pull requests for any improvements you would like to share.
7
7
 
8
8
 
9
+ Prerequisites
10
+ -------------
11
+
12
+ Before developing and testing Rsel, you will need to install some dependencies.
13
+ Most of these will be handled by simply running:
14
+
15
+ $ bundle install
16
+
17
+ from the git clone of Rsel. The nokogiri gem is known to fail if certain XML
18
+ development headers are missing; if you encounter this, try:
19
+
20
+ $ sudo apt-get install libxml2-dev libxslt-dev
21
+
22
+ Then re-run `bundle install`. If this still fails, or if you're on a non-Debian OS,
23
+ consult the nokogiri
24
+ [installation instructions](http://nokogiri.org/tutorials/installing_nokogiri.html).
25
+
26
+ Aside from gem dependencies, you will need to have Java installed in order to
27
+ run the Selenium server provided with Rsel (in the `test/server` directory).
28
+ Startup of the server is handled automatically by the Rake tasks during
29
+ testing; see below.
30
+
31
+
9
32
  Testing
10
33
  -------
11
34
 
@@ -115,5 +115,32 @@ Tables
115
115
  | See title | Editing Eric |
116
116
  | Close browser |
117
117
 
118
+ Conditionals
119
+ -------------
120
+
121
+ !define do_bad_stuff {false}
122
+ | script | selenium test | http://localhost:8070 |
123
+ | Open browser |
124
+ | Maximize browser |
125
+ | If parameter | ${do_bad_stuff} |
126
+ | Click | Nonexistent link |
127
+ | Page loads in | 30 | seconds or less |
128
+ | If I see | About this site |
129
+ | Click | About this site |
130
+ | Page loads in | 30 | seconds or less |
131
+ | End If |
132
+ | Otherwise |
133
+ | If I see | About this site |
134
+ | Click | About this site |
135
+ | Page loads in | 30 | seconds or less |
136
+ | Otherwise |
137
+ | Click | Nonexistent link |
138
+ | Page loads in | 30 | seconds or less |
139
+ | End If |
140
+ | End If |
141
+ | See | This site is really cool. |
142
+ | Do not see | This is a Sinatra webapp for unit testing Rsel. |
143
+ | Close browser |
144
+
118
145
 
119
146
  Next: [Customization](custom.md)
@@ -4,10 +4,10 @@ FitNesse
4
4
  With FitNesse, some initial configuration steps are necessary. Assuming you
5
5
  have a FitNesse wiki-page hierarchy like this:
6
6
 
7
- - `FitNesseRoot`
8
- - `SeleniumTests`
9
- - `SetUp`
10
- - `LoginTest`
7
+ * `FitNesseRoot`
8
+ * `SeleniumTests`
9
+ * `SetUp`
10
+ * `LoginTest`
11
11
 
12
12
  Put this in your `SeleniumTests.SetUp` page:
13
13
 
@@ -1,6 +1,26 @@
1
1
  Rsel History
2
2
  ============
3
3
 
4
+ 0.1.1
5
+ -----
6
+
7
+ - Conditional expressions (if_is, if_i_see, if_parameter, otherwise, end_if)
8
+ - Temporal visibility (see|do_not_see)_within_seconds
9
+ - Generic field equality (generic_field_equals)
10
+ - Generic field fill-in (set_field, set_field_among)
11
+ - Hash-based field fill-in (set_fields, sef_fields_among)
12
+ - Hash-based field equality (fields_equal, field_equals_among, fields_equal_among)
13
+ - Allow visit to accept URLs containing markup
14
+ - Show error messages in FitNesse: | Show | errors |
15
+ - row_exists now ignores cell order
16
+
17
+
18
+ 0.1.0
19
+ -----
20
+
21
+ - begin_scenario and end_scenario return true
22
+
23
+
4
24
  0.0.9
5
25
  -----
6
26
 
@@ -54,7 +54,7 @@ module Rsel
54
54
  #
55
55
  def initialize(url, options={})
56
56
  # Strip HTML tags from URL
57
- @url = url.gsub(/<\/?[^>]*>/, '')
57
+ @url = strip_tags(url)
58
58
  @browser = Selenium::Client::Driver.new(
59
59
  :host => options[:host] || 'localhost',
60
60
  :port => options[:port] || 4444,
@@ -68,6 +68,9 @@ module Rsel
68
68
  @stop_on_failure = false
69
69
  end
70
70
  @found_failure = false
71
+ @conditional_stack = [ true ]
72
+ # A list of error messages:
73
+ @errors = []
71
74
  end
72
75
 
73
76
  attr_reader :url, :browser, :stop_on_failure, :found_failure
@@ -103,15 +106,35 @@ module Rsel
103
106
 
104
107
 
105
108
  # Stop the session and close the browser window.
109
+ # Show error messages in an exception if requested.
106
110
  #
107
111
  # @example
108
112
  # | Close browser |
113
+ # | Close browser | and show errors |
114
+ # | Close browser | without showing errors |
109
115
  #
110
- def close_browser
116
+ def close_browser(show_errors='')
111
117
  @browser.close_current_browser_session
118
+ # Show errors in an exception if requested.
119
+ if (!(/not|without/i === show_errors) && @errors.length > 0)
120
+ raise StopTestStepFailed, @errors.join("\n").gsub('<','&lt;')
121
+ end
112
122
  return true
113
123
  end
114
124
 
125
+ # Show any current error messages.
126
+ # Also clears the error message log.
127
+ #
128
+ # @example
129
+ # | Show | errors |
130
+ #
131
+ # @since 0.1.1
132
+ #
133
+ def errors
134
+ current_errors = @errors
135
+ @errors = []
136
+ return current_errors.join("\n")
137
+ end
115
138
 
116
139
  # Begin a new scenario, and forget about any previous failures.
117
140
  # This allows you to modularize your tests into standalone sections
@@ -125,6 +148,7 @@ module Rsel
125
148
  #
126
149
  def begin_scenario
127
150
  @found_failure = false
151
+ @errors = []
128
152
  return true
129
153
  end
130
154
 
@@ -153,9 +177,9 @@ module Rsel
153
177
  # | Visit | /software |
154
178
  #
155
179
  def visit(path_or_url)
156
- return false if aborted?
180
+ return skip_status if skip_step?
157
181
  fail_on_exception do
158
- @browser.open(path_or_url)
182
+ @browser.open(strip_tags(path_or_url))
159
183
  end
160
184
  end
161
185
 
@@ -166,7 +190,7 @@ module Rsel
166
190
  # | Click back |
167
191
  #
168
192
  def click_back
169
- return false if aborted?
193
+ return skip_status if skip_step?
170
194
  fail_on_exception do
171
195
  @browser.go_back
172
196
  end
@@ -179,7 +203,7 @@ module Rsel
179
203
  # | Refresh page |
180
204
  #
181
205
  def refresh_page
182
- return false if aborted?
206
+ return skip_status if skip_step?
183
207
  fail_on_exception do
184
208
  @browser.refresh
185
209
  end
@@ -206,7 +230,7 @@ module Rsel
206
230
  # | See | Welcome, Marcus |
207
231
  #
208
232
  def see(text)
209
- return false if aborted?
233
+ return skip_status if skip_step?
210
234
  pass_if @browser.text?(text)
211
235
  end
212
236
 
@@ -220,11 +244,59 @@ module Rsel
220
244
  # | Do not see | Take a hike |
221
245
  #
222
246
  def do_not_see(text)
223
- return false if aborted?
247
+ return skip_status if skip_step?
224
248
  pass_if !@browser.text?(text)
225
249
  end
226
250
 
227
251
 
252
+ # Temporally ensure text is present or absent
253
+
254
+ # Ensure that the given text appears on the current page, eventually.
255
+ #
256
+ # @param [String] text
257
+ # Plain text that should be appear on or visible on the current page
258
+ # @param [String] seconds
259
+ # Integer number of seconds to wait.
260
+ #
261
+ # @example
262
+ # | Click | ajax_login | button |
263
+ # | See | Welcome back, Marcus | within | 10 | seconds |
264
+ # | Note | The following uses the Selenium default timeout: |
265
+ # | See | How do you feel? | within seconds |
266
+ #
267
+ # @since 0.1.1
268
+ #
269
+ def see_within_seconds(text, seconds=-1)
270
+ return skip_status if skip_step?
271
+ seconds = @browser.default_timeout_in_seconds if seconds == -1
272
+ pass_if !(Integer(seconds)+1).times{ break if (@browser.text?(text) rescue false); sleep 1 }
273
+ # This would be better if it worked:
274
+ # pass_if @browser.wait_for(:text => text, :timeout_in_seconds => seconds);
275
+ end
276
+
277
+ # Ensure that the given text does not appear on the current page, eventually.
278
+ #
279
+ # @param [String] text
280
+ # Plain text that should disappear from or not be present on the current page
281
+ # @param [String] seconds
282
+ # Integer number of seconds to wait.
283
+ #
284
+ # @example
285
+ # | Click | close | button | !{within:popup_ad} |
286
+ # | Do not see | advertisement | within | 10 | seconds |
287
+ #
288
+ # @since 0.1.1
289
+ #
290
+ def do_not_see_within_seconds(text, seconds=-1)
291
+ return skip_status if skip_step?
292
+ seconds = @browser.default_timeout_in_seconds if seconds == -1
293
+ pass_if !(Integer(seconds)+1).times{ break if (!@browser.text?(text) rescue false); sleep 1 }
294
+ # This would be better if it worked:
295
+ # pass_if @browser.wait_for(:no_text => text, :timeout_in_seconds => seconds);
296
+ end
297
+
298
+
299
+
228
300
  # Ensure that the current page has the given title text.
229
301
  #
230
302
  # @param [String] title
@@ -234,8 +306,8 @@ module Rsel
234
306
  # | See title | Our Homepage |
235
307
  #
236
308
  def see_title(title)
237
- return false if aborted?
238
- pass_if @browser.get_title == title
309
+ return skip_status if skip_step?
310
+ pass_if @browser.get_title == title, "Page title is '#{@browser.get_title}', not '#{title}'"
239
311
  end
240
312
 
241
313
 
@@ -248,7 +320,7 @@ module Rsel
248
320
  # | Do not see title | Someone else's homepage |
249
321
  #
250
322
  def do_not_see_title(title)
251
- return false if aborted?
323
+ return skip_status if skip_step?
252
324
  pass_if !(@browser.get_title == title)
253
325
  end
254
326
 
@@ -267,7 +339,7 @@ module Rsel
267
339
  # @since 0.0.2
268
340
  #
269
341
  def link_exists(locator, scope={})
270
- return false if aborted?
342
+ return skip_status if skip_step?
271
343
  pass_if @browser.element?(loc(locator, 'link', scope))
272
344
  end
273
345
 
@@ -286,28 +358,30 @@ module Rsel
286
358
  # @since 0.0.2
287
359
  #
288
360
  def button_exists(locator, scope={})
289
- return false if aborted?
361
+ return skip_status if skip_step?
290
362
  pass_if @browser.element?(loc(locator, 'button', scope))
291
363
  end
292
364
 
293
365
 
294
366
  # Ensure that a table row with the given cell values exists.
367
+ # Order does not matter as of v0.1.1.
295
368
  #
296
369
  # @param [String] cells
297
- # Comma-separated cell values you expect to see
370
+ # Comma-separated cell values you expect to see. If you need to include a
371
+ # literal comma, use the {#escape_for_hash} syntax, \'.
298
372
  #
299
373
  # @example
300
374
  # | Row exists | First, Middle, Last, Email |
301
- # | Row | First, Middle, Last, Email | exists |
375
+ # | Row | First, Last, Middle, Email | exists |
302
376
  #
303
377
  # @since 0.0.3
304
378
  #
305
379
  def row_exists(cells)
306
- return false if aborted?
307
- row = XPath.descendant(:tr)[XPath::HTML.table_row(cells.split(/, */))]
308
- pass_if @browser.element?("xpath=#{row.to_s}")
380
+ return skip_status if skip_step?
381
+ pass_if @browser.element?("xpath=#{xpath_row_containing(cells.split(/, */).map{|s| escape_for_hash(s)})}")
309
382
  end
310
383
 
384
+ #
311
385
 
312
386
  # Type a value into the given field. Passes if the field exists and is
313
387
  # editable. Fails if the field is not found, or is not editable.
@@ -324,7 +398,7 @@ module Rsel
324
398
  # | Type | Dale | into | First name | field | !{within:contact} |
325
399
  #
326
400
  def type_into_field(text, locator, scope={})
327
- return false if aborted?
401
+ return skip_status if skip_step?
328
402
  field = loc(locator, 'field', scope)
329
403
  fail_on_exception do
330
404
  ensure_editable(field) && @browser.type(field, text)
@@ -346,7 +420,7 @@ module Rsel
346
420
  # | Fill in | First name | with | Eric |
347
421
  #
348
422
  def fill_in_with(locator, text, scope={})
349
- return false if aborted?
423
+ return skip_status if skip_step?
350
424
  type_into_field(text, locator, scope)
351
425
  end
352
426
 
@@ -363,13 +437,13 @@ module Rsel
363
437
  # | Field | First name | contains | Eric |
364
438
  #
365
439
  def field_contains(locator, text, scope={})
366
- return false if aborted?
440
+ return skip_status if skip_step?
367
441
  begin
368
442
  field = @browser.field(loc(locator, 'field', scope))
369
443
  rescue
370
- failure
444
+ failure "Can't identify field #{locator}"
371
445
  else
372
- pass_if field.include?(text)
446
+ pass_if field.include?(text), "Field contains '#{field}', not '#{text}'"
373
447
  end
374
448
  end
375
449
 
@@ -387,13 +461,13 @@ module Rsel
387
461
  # | Field | First name | equals; | Eric | !{within:contact} |
388
462
  #
389
463
  def field_equals(locator, text, scope={})
390
- return false if aborted?
464
+ return skip_status if skip_step?
391
465
  begin
392
466
  field = @browser.field(loc(locator, 'field', scope))
393
467
  rescue
394
- failure
468
+ failure "Can't identify field #{locator}"
395
469
  else
396
- pass_if field == text
470
+ pass_if field == text, "Field contains '#{field}', not '#{text}'"
397
471
  end
398
472
  end
399
473
 
@@ -409,7 +483,7 @@ module Rsel
409
483
  # | Click; | Logout | !{within:header} |
410
484
  #
411
485
  def click(locator, scope={})
412
- return false if aborted?
486
+ return skip_status if skip_step?
413
487
  fail_on_exception do
414
488
  @browser.click(loc(locator, 'link_or_button', scope))
415
489
  end
@@ -429,7 +503,7 @@ module Rsel
429
503
  # | Click | Edit | link | !{in_row:Eric} |
430
504
  #
431
505
  def click_link(locator, scope={})
432
- return false if aborted?
506
+ return skip_status if skip_step?
433
507
  fail_on_exception do
434
508
  @browser.click(loc(locator, 'link', scope))
435
509
  end
@@ -451,7 +525,7 @@ module Rsel
451
525
  # | Click | Search | button | !{within:customers} |
452
526
  #
453
527
  def click_button(locator, scope={})
454
- return false if aborted?
528
+ return skip_status if skip_step?
455
529
  button = loc(locator, 'button', scope)
456
530
  fail_on_exception do
457
531
  ensure_editable(button) && @browser.click(button)
@@ -474,7 +548,7 @@ module Rsel
474
548
  # | Enable | Send me spam | checkbox | !{within:opt_in} |
475
549
  #
476
550
  def enable_checkbox(locator, scope={})
477
- return false if aborted?
551
+ return skip_status if skip_step?
478
552
  cb = loc(locator, 'checkbox', scope)
479
553
  fail_on_exception do
480
554
  ensure_editable(cb) && checkbox_is_disabled(cb) && @browser.click(cb)
@@ -496,7 +570,7 @@ module Rsel
496
570
  # | Disable | Send me spam | checkbox | !{within:opt_in} |
497
571
  #
498
572
  def disable_checkbox(locator, scope={})
499
- return false if aborted?
573
+ return skip_status if skip_step?
500
574
  cb = loc(locator, 'checkbox', scope)
501
575
  fail_on_exception do
502
576
  ensure_editable(cb) && checkbox_is_enabled(cb) && @browser.click(cb)
@@ -516,12 +590,12 @@ module Rsel
516
590
  # | Checkbox | send me spam | is enabled | !{within:opt_in} |
517
591
  #
518
592
  def checkbox_is_enabled(locator, scope={})
519
- return false if aborted?
593
+ return skip_status if skip_step?
520
594
  xp = loc(locator, 'checkbox', scope)
521
595
  begin
522
596
  enabled = @browser.checked?(xp)
523
597
  rescue
524
- failure
598
+ failure "Can't identify checkbox #{locator}"
525
599
  else
526
600
  return enabled
527
601
  end
@@ -542,12 +616,12 @@ module Rsel
542
616
  # @since 0.0.4
543
617
  #
544
618
  def radio_is_enabled(locator, scope={})
545
- return false if aborted?
619
+ return skip_status if skip_step?
546
620
  xp = loc(locator, 'radio_button', scope)
547
621
  begin
548
622
  enabled = @browser.checked?(xp)
549
623
  rescue
550
- failure
624
+ failure "Can't identify radio #{locator}"
551
625
  else
552
626
  return enabled
553
627
  end
@@ -566,12 +640,12 @@ module Rsel
566
640
  # | Checkbox | send me spam | is disabled | !{within:opt_in} |
567
641
  #
568
642
  def checkbox_is_disabled(locator, scope={})
569
- return false if aborted?
643
+ return skip_status if skip_step?
570
644
  xp = loc(locator, 'checkbox', scope)
571
645
  begin
572
646
  enabled = @browser.checked?(xp)
573
647
  rescue
574
- failure
648
+ failure "Can't identify checkbox #{locator}"
575
649
  else
576
650
  return !enabled
577
651
  end
@@ -592,12 +666,12 @@ module Rsel
592
666
  # @since 0.0.4
593
667
  #
594
668
  def radio_is_disabled(locator, scope={})
595
- return false if aborted?
669
+ return skip_status if skip_step?
596
670
  xp = loc(locator, 'radio_button', scope)
597
671
  begin
598
672
  enabled = @browser.checked?(xp)
599
673
  rescue
600
- failure
674
+ failure "Can't identify radio #{locator}"
601
675
  else
602
676
  return !enabled
603
677
  end
@@ -617,7 +691,7 @@ module Rsel
617
691
  # | Select | female | radio | !{within:gender} |
618
692
  #
619
693
  def select_radio(locator, scope={})
620
- return false if aborted?
694
+ return skip_status if skip_step?
621
695
  radio = loc(locator, 'radio_button', scope)
622
696
  fail_on_exception do
623
697
  ensure_editable(radio) && @browser.click(radio)
@@ -639,7 +713,7 @@ module Rsel
639
713
  # | Select | Tall | from dropdown | Height |
640
714
  #
641
715
  def select_from_dropdown(option, locator, scope={})
642
- return false if aborted?
716
+ return skip_status if skip_step?
643
717
  dropdown = loc(locator, 'select', scope)
644
718
  fail_on_exception do
645
719
  ensure_editable(dropdown) && @browser.select(dropdown, option)
@@ -660,7 +734,7 @@ module Rsel
660
734
  # @since 0.0.2
661
735
  #
662
736
  def dropdown_includes(locator, option, scope={})
663
- return false if aborted?
737
+ return skip_status if skip_step?
664
738
  # TODO: Apply scope
665
739
  dropdown = XPath::HTML.select(locator)
666
740
  opt = dropdown[XPath::HTML.option(option)]
@@ -682,13 +756,13 @@ module Rsel
682
756
  # @since 0.0.2
683
757
  #
684
758
  def dropdown_equals(locator, option, scope={})
685
- return false if aborted?
759
+ return skip_status if skip_step?
686
760
  begin
687
761
  selected = @browser.get_selected_label(loc(locator, 'select', scope))
688
762
  rescue
689
- failure
763
+ failure "Can't identify dropdown #{locator}"
690
764
  else
691
- return selected == option
765
+ pass_if selected == option, "Dropdown equals '#{selected}', not '#{option}'"
692
766
  end
693
767
  end
694
768
 
@@ -702,7 +776,7 @@ module Rsel
702
776
  # | Pause | 5 | seconds |
703
777
  #
704
778
  def pause_seconds(seconds)
705
- return false if aborted?
779
+ return skip_status if skip_step?
706
780
  sleep seconds.to_i
707
781
  return true
708
782
  end
@@ -719,13 +793,350 @@ module Rsel
719
793
  # | Page loads in | 10 | seconds or less |
720
794
  #
721
795
  def page_loads_in_seconds_or_less(seconds)
722
- return false if aborted?
796
+ return skip_status if skip_step?
723
797
  fail_on_exception do
724
798
  @browser.wait_for_page_to_load(seconds)
725
799
  end
726
800
  end
727
801
 
728
802
 
803
+ # A generic way to fill in any field, of any type. (Just about.)
804
+ # Kind of nasty since it needs to use Javascript on the page.
805
+ #
806
+ # Types accepted:
807
+ #
808
+ # * a*
809
+ # * button*
810
+ # * input
811
+ # * type=button*
812
+ # * type=checkbox
813
+ # * type=image*
814
+ # * type=radio*
815
+ # * type=reset*
816
+ # * type=submit*
817
+ # * type=text
818
+ # * select
819
+ # * textarea
820
+ #
821
+ # \* Value is ignored: this control type is just clicked/selected.
822
+ #
823
+ # @param [String] locator
824
+ # Label, name, or id of the field control. Identification by
825
+ # non-Selenium methods may not work for some links and buttons.
826
+ # @param [String] value
827
+ # Value you want to set the field to. (Default: empty string.)
828
+ # Parsed by {#string_is_true?}
829
+ #
830
+ # @since 0.1.1
831
+ #
832
+ def set_field(locator, value='', scope={})
833
+ return skip_status if skip_step?
834
+ fail_on_exception do
835
+ # First, use Javascript to find out what the field is.
836
+ begin
837
+ loceval = loc(locator, 'field', scope)
838
+ rescue
839
+ loceval = loc(locator, 'link_or_button', scope)
840
+ end
841
+
842
+ case tagname(loceval)
843
+ when 'input.text', /^textarea\./
844
+ return type_into_field(value, loceval)
845
+ when 'input.radio'
846
+ return select_radio(loceval)
847
+ when 'input.checkbox'
848
+ if string_is_true?(value)
849
+ return enable_checkbox(loceval)
850
+ else
851
+ return disable_checkbox(loceval)
852
+ end
853
+ when /^select\./
854
+ return select_from_dropdown(value, loceval)
855
+ when /^(a|button)\./,'input.button','input.submit','input.image','input.reset'
856
+ return click(loceval)
857
+ else
858
+ #raise ArgumentError, "Unidentified field #{locator}."
859
+ return failure("Unidentified field #{locator}.")
860
+ end
861
+ end
862
+ end
863
+
864
+
865
+ # Set a value (with {#set_field}) in the named field, based on the given
866
+ # name/value pairs. Uses {#escape_for_hash} to allow certain characters in
867
+ # FitNesse.
868
+ #
869
+ # @param [String] field
870
+ # A Locator or a name listed in the ids hash below. If a name listed in
871
+ # the ids below, this field is case-insensitive.
872
+ # @param [String] value
873
+ # Plain text to go into a field
874
+ # @param ids
875
+ # A hash mapping common names to Locators. (Optional, but redundant
876
+ # without it) The hash keys are case-insensitive.
877
+ #
878
+ # @since 0.1.1
879
+ #
880
+ def set_field_among(field, value, ids={}, scope={})
881
+ return skip_status if skip_step?
882
+ # FitNesse passes in "" for an empty field. Fix it.
883
+ ids = {} if ids == ""
884
+
885
+ normalize_ids(ids)
886
+
887
+ if ids[field.downcase]
888
+ return set_field(escape_for_hash(ids[field.downcase]), value, scope)
889
+ else
890
+ return set_field(field, value, scope)
891
+ end
892
+ end
893
+
894
+ # Set values (with {#set_field}) in the named fields of a hash, based on the
895
+ # given name/value pairs. Uses {#escape_for_hash} to allow certain
896
+ # characters in FitNesse. Note: Order of entries is not guaranteed, and
897
+ # depends on the version of Ruby on your server!
898
+ #
899
+ # @param fields
900
+ # A key-value hash where the keys are Locators (case-sensitive) and the
901
+ # values are the string values you want in the fields.
902
+ #
903
+ # @since 0.1.1
904
+ #
905
+ def set_fields(fields={}, scope={})
906
+ return skip_status if skip_step?
907
+ # FitNesse passes in "" for an empty field. Fix it.
908
+ fields = {} if fields == ""
909
+ fields.each do |key, value|
910
+ key_esc = escape_for_hash(key.to_s)
911
+ value_esc = escape_for_hash(value.to_s)
912
+ unless set_field(key_esc, value_esc, scope)
913
+ return failure "Failed to set field #{key_esc} to #{value_esc}"
914
+ end
915
+ end
916
+ return true
917
+ end
918
+
919
+ # Set values (with {#set_field}) in the named fields, based on the given
920
+ # name/value pairs, and with mapping of names in the ids field. Uses
921
+ # {#escape_for_hash} to allow certain characters in FitNesse. Note: Order
922
+ # of entries is not guaranteed, and depends on the version of Ruby on your
923
+ # server!
924
+ #
925
+ # @param fields
926
+ # A key-value hash where the keys are keys of the ids hash
927
+ # (case-insensitive), or Locators (case-sensitive), and the values are
928
+ # the string values you want in the fields.
929
+ # @param ids
930
+ # A hash mapping common names to Locators. (Optional, but redundant
931
+ # without it) The hash keys are case-insensitive.
932
+ #
933
+ # @example
934
+ # Suppose you have a nasty form whose fields have nasty locators. Suppose
935
+ # further that you want to fill in this form, many times, filling in
936
+ # different fields different ways. Begin by creating a Scenario table:
937
+ #
938
+ # | scenario | Set nasty form fields | values |
939
+ # | Set | @values | fields among | !{Name:id=nasty_field_name_1,Email:id=nasty_field_name_2,E-mail:id=nasty_field_name_2,Send me spam:id=nasty_checkbox_name_1} |
940
+ #
941
+ # Using that you can now say something like:
942
+ #
943
+ # | Set nasty form fields | !{Name:Ken,email:ken@kensaddress.com,send me spam: no} |
944
+ #
945
+ # Or:
946
+ #
947
+ # | Set nasty form fields | !{Name:Ken,Send me Spam: no} |
948
+ #
949
+ # Or:
950
+ #
951
+ # | Set nasty form fields | !{name:Ken,e-mail:,SEND ME SPAM: yes} |
952
+ #
953
+ # @since 0.1.1
954
+ #
955
+ def set_fields_among(fields={}, ids={}, scope={})
956
+ return skip_status if skip_step?
957
+ # FitNesse passes in "" for an empty field. Fix it.
958
+ ids = {} if ids == ""
959
+ fields = {} if fields == ""
960
+
961
+ fields.each do |key, value|
962
+ key_esc = escape_for_hash(key.to_s)
963
+ value_esc = escape_for_hash(value.to_s)
964
+ unless set_field_among(key_esc, value_esc, ids, scope)
965
+ return failure("Failed to set #{key_esc} (#{ids[key_esc]}) to #{value_esc}")
966
+ end
967
+ end
968
+ return true
969
+ end
970
+
971
+ # A generic way to check any field, of any type. (Just about.) Kind of
972
+ # nasty since it needs to use Javascript on the page.
973
+ #
974
+ # Types accepted:
975
+ #
976
+ # * a*
977
+ # * button*
978
+ # * input
979
+ # * type=button*
980
+ # * type=checkbox
981
+ # * type=image*
982
+ # * type=radio*
983
+ # * type=reset*
984
+ # * type=submit*
985
+ # * type=text
986
+ # * select
987
+ # * textarea
988
+ #
989
+ # \* Value is ignored: this control type is just clicked/selected.
990
+ #
991
+ # @param [String] locator
992
+ # Label, name, or id of the field control. Identification by
993
+ # non-Selenium methods may not work for some links and buttons.
994
+ # @param [String] value
995
+ # Value you want to verify the field equal to. (Default: empty string.)
996
+ # Parsed by {#string_is_true?}
997
+ #
998
+ # @since 0.1.1
999
+ #
1000
+ def generic_field_equals(locator, value='', scope={})
1001
+ return skip_status if skip_step?
1002
+ fail_on_exception do
1003
+ # First, use Javascript to find out what the field is.
1004
+ begin
1005
+ loceval = loc(locator, 'field', scope)
1006
+ rescue
1007
+ loceval = loc(locator, 'link_or_button', scope)
1008
+ end
1009
+
1010
+ case tagname(loceval)
1011
+ when 'input.text', /^textarea\./
1012
+ return field_equals(loceval, value)
1013
+ when 'input.radio'
1014
+ if string_is_true?(value)
1015
+ return radio_is_enabled(loceval)
1016
+ else
1017
+ return radio_is_disabled(loceval)
1018
+ end
1019
+ when 'input.checkbox'
1020
+ if string_is_true?(value)
1021
+ return checkbox_is_enabled(loceval)
1022
+ else
1023
+ return checkbox_is_disabled(loceval)
1024
+ end
1025
+ when /^select\./
1026
+ return dropdown_equals(loceval, value)
1027
+ else
1028
+ #raise ArgumentError, "Unidentified field #{locator}."
1029
+ return failure("Unidentified field for comparison: #{locator}.")
1030
+ end
1031
+ end
1032
+ end
1033
+
1034
+
1035
+ def tagname(loceval)
1036
+ return @browser.get_eval(
1037
+ 'var loceval=this.browserbot.findElement("' +
1038
+ loceval + '");loceval.tagName+"."+loceval.type').downcase
1039
+ end
1040
+
1041
+ # Check a value (with {#set_field}) in the named field, based on the given
1042
+ # name/value pairs. Uses {#escape_for_hash} to allow certain characters in
1043
+ # FitNesse.
1044
+ #
1045
+ # @param [String] field
1046
+ # A Locator or a name listed in the ids hash below. If a name listed in
1047
+ # the ids below, this field is case-insensitive.
1048
+ # @param [String] value
1049
+ # Plain text to go into a field
1050
+ # @param ids
1051
+ # A hash mapping common names to Locators. (Optional, but redundant
1052
+ # without it) The hash keys are case-insensitive.
1053
+ #
1054
+ # @since 0.1.1
1055
+ #
1056
+ def field_equals_among(field, value, ids={}, scope={})
1057
+ return skip_status if skip_step?
1058
+ # FitNesse passes in "" for an empty field. Fix it.
1059
+ ids = {} if ids == ""
1060
+
1061
+ normalize_ids(ids)
1062
+
1063
+ if ids[field.downcase]
1064
+ return generic_field_equals(escape_for_hash(ids[field.downcase]), value, scope)
1065
+ else
1066
+ return generic_field_equals(field, value, scope)
1067
+ end
1068
+ end
1069
+
1070
+ # Check values (with {#set_field}) in the named fields of a hash, based on
1071
+ # the given name/value pairs. Uses {#escape_for_hash} to allow certain
1072
+ # characters in FitNesse. Note: Order of entries is not guaranteed, and
1073
+ # depends on the version of Ruby on your server!
1074
+ #
1075
+ # @param fields
1076
+ # A key-value hash where the keys are Locators (case-sensitive) and the
1077
+ # values are the string values you want in the fields.
1078
+ #
1079
+ # @since 0.1.1
1080
+ #
1081
+ def fields_equal(fields={}, scope={})
1082
+ return skip_status if skip_step?
1083
+ # FitNesse passes in "" for an empty field. Fix it.
1084
+ fields = {} if fields == ""
1085
+ fields.keys.each do |field|
1086
+ return failure unless generic_field_equals(escape_for_hash(field.to_s), escape_for_hash(fields[field]), scope)
1087
+ end
1088
+ return true
1089
+ end
1090
+
1091
+ # Check values (with {#set_field}) in the named fields, based on the given
1092
+ # name/value pairs, and with mapping of names in the ids field. Uses
1093
+ # {#escape_for_hash} to allow certain characters in FitNesse. Note: Order
1094
+ # of entries is not guaranteed, and depends on the version of Ruby on your
1095
+ # server!
1096
+ #
1097
+ # @param fields
1098
+ # A key-value hash where the keys are keys of the ids hash
1099
+ # (case-insensitive), or Locators (case-sensitive),
1100
+ # and the values are the string values you want in the fields.
1101
+ # @param ids
1102
+ # A hash mapping common names to Locators. (Optional, but redundant
1103
+ # without it) The hash keys are case-insensitive.
1104
+ #
1105
+ # @example
1106
+ # Suppose you have a nasty form whose fields have nasty locators.
1107
+ # Suppose further that you want to fill in this form, many times, filling
1108
+ # in different fields different ways.
1109
+ # Begin by creating a Scenario table:
1110
+ #
1111
+ # | scenario | Check nasty form fields | values |
1112
+ # | fields equal | @values | among | !{Name:id=nasty_field_name_1,Email:id=nasty_field_name_2,E-mail:id=nasty_field_name_2,Send me spam:id=nasty_checkbox_name_1} |
1113
+ #
1114
+ # Using that you can now say something like:
1115
+ #
1116
+ # | Check nasty form fields | !{Name:Ken,email:ken@kensaddress.com,send me spam: no} |
1117
+ #
1118
+ # Or:
1119
+ #
1120
+ # | Check nasty form fields | !{Name:Ken,Send me Spam: no} |
1121
+ #
1122
+ # Or:
1123
+ #
1124
+ # | Check nasty form fields | !{name:Ken,e-mail:,SEND ME SPAM: yes} |
1125
+ #
1126
+ # @since 0.1.1
1127
+ #
1128
+ def fields_equal_among(fields={}, ids={}, scope={})
1129
+ return skip_status if skip_step?
1130
+ # FitNesse passes in "" for an empty field. Fix it.
1131
+ ids = {} if ids == ""
1132
+ fields = {} if fields == ""
1133
+
1134
+ fields.keys.each do |field|
1135
+ return failure unless field_equals_among(escape_for_hash(field.to_s), escape_for_hash(fields[field]), ids, scope)
1136
+ end
1137
+ return true
1138
+ end
1139
+
729
1140
  # Invoke a missing method. If a method is called on a SeleniumTest
730
1141
  # instance, and that method is not explicitly defined, this method
731
1142
  # will check to see whether the underlying Selenium::Client::Driver
@@ -735,15 +1146,18 @@ module Rsel
735
1146
  # @since 0.0.6
736
1147
  #
737
1148
  def method_missing(method, *args, &block)
1149
+ return skip_status if skip_step?
738
1150
  if @browser.respond_to?(method)
739
1151
  begin
740
1152
  result = @browser.send(method, *args, &block)
741
1153
  rescue
742
- failure
1154
+ failure "Method #{method} error"
743
1155
  else
744
1156
  # The method call succeeded; did it return true or false?
745
- return result if [true, false].include? result
746
- # Not a Boolean return value--assume passing
1157
+ return failure if result == false
1158
+ # If a string, return that. We might Check or Show it.
1159
+ return result if result == true || (result.is_a? String)
1160
+ # Not a Boolean return value or string--assume passing
747
1161
  return true
748
1162
  end
749
1163
  else
@@ -767,6 +1181,144 @@ module Rsel
767
1181
  end
768
1182
  end
769
1183
 
1184
+ # Conditionals
1185
+
1186
+ # If I see the given text, do the steps until I see an otherwise or end_if.
1187
+ # Otherwise do not do those steps.
1188
+ #
1189
+ # @param [String] text
1190
+ # Plain text that should be visible on the current page
1191
+ #
1192
+ # @example
1193
+ # | If I see | pop-over ad |
1194
+ # | Click | Close | button |
1195
+ # | End if |
1196
+ #
1197
+ # @since 0.1.1
1198
+ #
1199
+ def if_i_see(text)
1200
+ return false if aborted?
1201
+ # If this if is inside a block that's not running, record that.
1202
+ if !@conditional_stack.last
1203
+ @conditional_stack.push nil
1204
+ return nil
1205
+ end
1206
+
1207
+ # Test the condition.
1208
+ @conditional_stack.push @browser.text?(text)
1209
+
1210
+ return true if @conditional_stack.last == true
1211
+ return nil if @conditional_stack.last == false
1212
+ return failure
1213
+ end
1214
+
1215
+ # If the given parameter is "yes" or "true", do the steps until I see an
1216
+ # otherwise or end_if. Otherwise do not do those steps.
1217
+ #
1218
+ # @param [String] text
1219
+ # A string. Parsed by {#string_is_true?}. True values cause the
1220
+ # following steps to run. Anything else does not.
1221
+ #
1222
+ # @example
1223
+ # | If parameter | ${spam_me} |
1224
+ # | Enable | Send me spam | checkbox |
1225
+ # | See | Email: | within | 10 | seconds |
1226
+ # | Type | ${spam_me_email} | into field | spammable_email |
1227
+ # | End if |
1228
+ #
1229
+ # @since 0.1.1
1230
+ #
1231
+ def if_parameter(text)
1232
+ return false if aborted?
1233
+ if !@conditional_stack.last
1234
+ @conditional_stack.push nil
1235
+ return nil
1236
+ end
1237
+
1238
+ # Test the condition.
1239
+ @conditional_stack.push string_is_true?(text)
1240
+
1241
+ return true if @conditional_stack.last == true
1242
+ return nil if @conditional_stack.last == false
1243
+ return failure
1244
+ end
1245
+
1246
+ # If the first parameter is the same as the second, do the steps until I see an
1247
+ # otherwise or end_if. Otherwise do not do those steps.
1248
+ #
1249
+ # @param [String] text
1250
+ # A string.
1251
+ #
1252
+ # @param [String] expected
1253
+ # Another string.
1254
+ # Uses `selenium_compare', so glob, regexp, etc. are accepted.
1255
+ #
1256
+ # @example
1257
+ # | $name= | Get value | id=response_field |
1258
+ # | If | $name | is | George |
1259
+ # | Type | Hi, George. | into | chat | field |
1260
+ # | Otherwise |
1261
+ # | Type | Go away! Bring me George! | into | chat | field |
1262
+ # | End if |
1263
+ #
1264
+ # @since 0.1.1
1265
+ #
1266
+ def if_is(text, expected)
1267
+ return false if aborted?
1268
+ if !@conditional_stack.last
1269
+ @conditional_stack.push nil
1270
+ return nil
1271
+ end
1272
+
1273
+ # Test the condition.
1274
+ @conditional_stack.push selenium_compare(text, expected)
1275
+
1276
+ return true if @conditional_stack.last == true
1277
+ return nil if @conditional_stack.last == false
1278
+ return failure
1279
+ end
1280
+
1281
+ # End an if block.
1282
+ #
1283
+ # @since 0.1.1
1284
+ #
1285
+ def end_if
1286
+ return false if aborted?
1287
+ # If there was no prior matching if, fail.
1288
+ return failure if @conditional_stack.length <= 1
1289
+
1290
+ last_status = @conditional_stack.pop
1291
+ # If this end_if is within an un-executed if block, don't execute it.
1292
+ return nil if last_status == nil
1293
+ return true
1294
+ end
1295
+
1296
+ # The else case to match any if.
1297
+ #
1298
+ # @example
1299
+ # | if parameter | ${login_by_phone} |
1300
+ # | type | ${login} | into field | phone_number |
1301
+ # | otherwise |
1302
+ # | type | ${login} | into field | login |
1303
+ # | end if |
1304
+ #
1305
+ # @since 0.1.1
1306
+ #
1307
+ def otherwise
1308
+ return false if aborted?
1309
+ # If there was no prior matching if, fail.
1310
+ return failure if @conditional_stack.length <= 1
1311
+
1312
+ # If this otherwise is within an un-executed if block, don't execute it.
1313
+ return nil if @conditional_stack.last == nil
1314
+
1315
+ last_stack = @conditional_stack.pop
1316
+ @conditional_stack.push !last_stack
1317
+ return true if @conditional_stack.last == true
1318
+ return nil if @conditional_stack.last == false
1319
+ return failure
1320
+ end
1321
+
770
1322
 
771
1323
  private
772
1324
 
@@ -784,7 +1336,7 @@ module Rsel
784
1336
  rescue => e
785
1337
  #puts e.message
786
1338
  #puts e.backtrace
787
- failure
1339
+ failure("#{e.message}")
788
1340
  else
789
1341
  return true
790
1342
  end
@@ -814,8 +1366,9 @@ module Rsel
814
1366
  #
815
1367
  # @since 0.0.6
816
1368
  #
817
- def failure
1369
+ def failure(reason='')
818
1370
  @found_failure = true
1371
+ @errors.push(reason) unless (reason == '')
819
1372
  return false
820
1373
  end
821
1374
 
@@ -824,15 +1377,36 @@ module Rsel
824
1377
  #
825
1378
  # @since 0.0.6
826
1379
  #
827
- def pass_if(condition)
1380
+ def pass_if(condition, errormsg='')
828
1381
  if condition
829
1382
  return true
830
1383
  else
831
- failure
1384
+ failure(errormsg)
832
1385
  end
833
1386
  end
834
1387
 
835
1388
 
1389
+ # Conditionals
1390
+
1391
+ # Should the current step be skipped, either because the test was aborted or
1392
+ # because we're in a conditional?
1393
+ #
1394
+ # @since 0.1.1
1395
+ #
1396
+ def skip_step?
1397
+ return aborted? || !@conditional_stack.last
1398
+ end
1399
+
1400
+ # Presuming the current step should be skipped, what status should I return?
1401
+ #
1402
+ # @since 0.1.1
1403
+ #
1404
+ def skip_status
1405
+ return false if aborted?
1406
+ return nil if !@conditional_stack.last
1407
+ end
1408
+
1409
+
836
1410
  # Return true if this test has been aborted.
837
1411
  #
838
1412
  # @since 0.0.9
@@ -844,5 +1418,3 @@ module Rsel
844
1418
  end
845
1419
  end
846
1420
 
847
-
848
-