redsquirrel-safariwatir 0.3.5

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.
data/README.rdoc ADDED
@@ -0,0 +1,46 @@
1
+ = SafariWatir
2
+
3
+ * http://safariwatir.rubyforge.org
4
+ * http://rubyforge.org/mailman/listinfo/safariwatir-general
5
+ * http://twitter.com/SafariWatir
6
+
7
+ == DESCRIPTION:
8
+
9
+ We are putting Watir on Safari.
10
+ The original Watir (Web Application Testing in Ruby) project supports only IE on Windows.
11
+ This project aims at adding Watir support for Safari on the Mac.
12
+
13
+ == Requirements
14
+
15
+ Mac OS X running Safari. Some features require you to turn on "Enable access for assistive devices" in System Preferences > Universal Access.
16
+
17
+
18
+ == SYNOPSIS:
19
+
20
+ require 'rubygems'
21
+ require 'safariwatir'
22
+
23
+ browser = Watir::Safari.new
24
+ browser.goto("http://google.com")
25
+ browser.text_field(:name, "q").set("obtiva")
26
+ browser.button(:name, "btnI").click
27
+ puts "FAILURE" unless browser.contains_text("software")
28
+
29
+ == INSTALL:
30
+
31
+ [sudo] gem install safariwatir
32
+
33
+ or
34
+
35
+ git clone git://github.com/redsquirrel/safariwatir.git
36
+ cd safariwatir
37
+ rake install
38
+
39
+ == RUNNING SAFARIWATIR AGAINST WATIR'S CORE TESTS
40
+
41
+ # First, install the SafariWatir gem (see above)
42
+ svn checkout https://svn.openqa.org/svn/watir/trunk
43
+ cd trunk/watir
44
+ cp unittests/options.yml.example unittests/options.yml
45
+ # Edit unittests/options.yml and set browser: safari
46
+ ruby unittests/core_tests.rb
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('safariwatir', '0.3.5') do | config |
6
+ config.summary = %q{Automated testing tool for web applications.}
7
+ config.description = %q{WATIR stands for "Web Application Testing in Ruby". See WATIR project for more information. This is a Safari-version of the original IE-only WATIR.}
8
+ config.url = 'http://safariwatir.rubyforge.org/'
9
+ config.author = 'Dave Hoover'
10
+ config.email = 'dave@obtiva.com'
11
+ config.ignore_pattern = ['tmp/*', 'script/*']
12
+ config.development_dependencies = ["rb-appscript"]
13
+ end
@@ -0,0 +1,31 @@
1
+ # Why shouldn't this be in core Ruby?
2
+ class Class
3
+ def def_init(*attrs)
4
+ constructor = %|def initialize(|
5
+ constructor << attrs.map{|a| a.to_s }.join(",")
6
+ constructor << ")\n"
7
+ attrs.each do |attribute|
8
+ constructor << "@#{attribute} = #{attribute}\n"
9
+ end
10
+ constructor << "end"
11
+ class_eval(constructor)
12
+ end
13
+ end
14
+
15
+ class String
16
+ def quote_safe
17
+ gsub(/"/, '\"')
18
+ end
19
+ end
20
+
21
+ class Object
22
+ def blank?
23
+ if respond_to?(:strip)
24
+ strip.empty?
25
+ elsif respond_to?(:empty?)
26
+ empty?
27
+ else
28
+ !self
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,585 @@
1
+ require File.dirname(__FILE__) + '/core_ext'
2
+ require File.dirname(__FILE__) + '/../watir/exceptions'
3
+ require 'appscript'
4
+
5
+ module Watir # :nodoc:
6
+ ELEMENT_NOT_FOUND = "__safari_watir_element_unfound__"
7
+ FRAME_NOT_FOUND = "__safari_watir_frame_unfound__"
8
+ NO_RESPONSE = "__safari_watir_no_response__"
9
+ TABLE_CELL_NOT_FOUND = "__safari_watir_cell_unfound__"
10
+ EXTRA_ACTION_SUCCESS = "__safari_watir_extra_action__"
11
+
12
+ JS_LIBRARY = %|
13
+ function dispatchOnChange(element) {
14
+ var event = document.createEvent('HTMLEvents');
15
+ event.initEvent('change', true, true);
16
+ element.dispatchEvent(event);
17
+ }|
18
+
19
+ class JavaScripter # :nodoc:
20
+ def operate(locator, operation)
21
+ %|#{locator}
22
+ if (element) {
23
+ #{operation}
24
+ } else {
25
+ return '#{ELEMENT_NOT_FOUND}';
26
+ }|
27
+ end
28
+
29
+ def wrap(script)
30
+ # Needed because createEvent must be called on a document, and the JavaScripter sub-classes
31
+ # do some transformations to lower-case "document" before we get here at runtime.
32
+ script.gsub! "DOCUMENT", "document"
33
+
34
+ # Needed because I would like to blindly use return statements, but Safari 3 enforces
35
+ # the ECMAScript standard that return statements are only valid within functions.
36
+ %|#{JS_LIBRARY}
37
+ (function() {
38
+ #{script}
39
+ })()|
40
+ end
41
+
42
+ def find_cell(cell)
43
+ return %|getElementById('#{cell.what}')| if cell.how == :id
44
+ raise RuntimeError, "Unable to use #{cell.how} to find TableCell" unless cell.row
45
+
46
+ finder =
47
+ case cell.row.how
48
+ when :id:
49
+ %|getElementById('#{cell.row.what}')|
50
+ when :index:
51
+ case cell.row.table.how
52
+ when :id
53
+ %|getElementById('#{cell.row.table.what}').rows[#{cell.row.what-1}]|
54
+ when :index:
55
+ %|getElementsByTagName('TABLE')[#{cell.row.table.what-1}].rows[#{cell.row.what-1}]|
56
+ else
57
+ raise MissingWayOfFindingObjectException, "Table element does not support #{cell.row.table.how}"
58
+ end
59
+ else
60
+ raise MissingWayOfFindingObjectException, "TableRow element does not support #{cell.row.how}"
61
+ end
62
+
63
+ finder + %|.cells[#{cell.what-1}]|
64
+ end
65
+ end
66
+
67
+ class FrameJavaScripter < JavaScripter # :nodoc:
68
+ def initialize(frame)
69
+ @page_container = "parent.#{frame.name}"
70
+ end
71
+
72
+ def wrap(script)
73
+ # add in frame name when referencing parent or document
74
+ script.gsub! "parent", "parent.#{@page_container}"
75
+ script.gsub! "document", "#{@page_container}.document"
76
+ super(script)
77
+ end
78
+ end
79
+
80
+ class TableJavaScripter < JavaScripter # :nodoc:
81
+ def_init :cell
82
+
83
+ def wrap(script)
84
+ script.gsub! "document", "document." + find_cell(@cell)
85
+ super(script)
86
+ end
87
+ end
88
+
89
+ class AppleScripter # :nodoc:
90
+ include Watir::Exception
91
+
92
+ attr_reader :js
93
+ attr_accessor :typing_lag
94
+ private :js
95
+
96
+ TIMEOUT = 10
97
+
98
+ def initialize(scripter = JavaScripter.new, opts={})
99
+ @js = scripter
100
+ @appname = opts[:appname] || "Safari"
101
+ @app = Appscript.app(@appname)
102
+ @document = @app.documents[1]
103
+ end
104
+
105
+ def ensure_window_ready
106
+ @app.activate
107
+ @app.make(:new => :document) if @app.documents.get.size == 0
108
+ @document = @app.documents[1]
109
+ end
110
+
111
+ def url
112
+ @document.URL.get
113
+ end
114
+
115
+ def hide
116
+ # because applescript is stupid and annoying you have
117
+ # to get all the processes from System Events, grab
118
+ # the right one for this instance and then set visible
119
+ # of it to false.
120
+ se = Appscript.app("System Events")
121
+ safari = se.processes.get.select do |app|
122
+ app.name.get == @appname
123
+ end.first
124
+ safari.visible.set(false)
125
+ end
126
+
127
+ def close
128
+ @app.quit
129
+ end
130
+
131
+ def navigate_to(url, &extra_action)
132
+ page_load(extra_action) do
133
+ @document.URL.set(url)
134
+ end
135
+ end
136
+
137
+ def reload
138
+ execute(%|window.location.reload()|)
139
+ end
140
+
141
+ def get_text_for(element = @element)
142
+ execute(element.operate { %|return element.innerText| }, element)
143
+ end
144
+
145
+ def get_html_for(element = @element)
146
+ execute(element.operate { %|return element.innerHTML| }, element)
147
+ end
148
+
149
+ def operate_by_table_cell(element = @element)
150
+ %|var element = document;
151
+ if (element == undefined) {
152
+ return '#{TABLE_CELL_NOT_FOUND}';
153
+ }
154
+ #{yield}|
155
+ end
156
+
157
+ def get_value_for(element = @element)
158
+ execute(element.operate { %|return element.value;| }, element)
159
+ end
160
+
161
+ def document_text
162
+ execute(%|return document.getElementsByTagName('BODY').item(0).innerText;|)
163
+ end
164
+
165
+ def document_html
166
+ execute(%|return document.all[0].outerHTML;|)
167
+ end
168
+
169
+ def document_title
170
+ execute(%|return document.title;|)
171
+ end
172
+
173
+ def focus(element)
174
+ execute(element.operate { %|element.focus();| }, element)
175
+ end
176
+
177
+ def blur(element)
178
+ execute(element.operate { %|element.blur();| }, element)
179
+ end
180
+
181
+ def highlight(element, &block)
182
+ execute(element.operate do
183
+ %|element.originalColor = element.style.backgroundColor;
184
+ element.style.backgroundColor = 'yellow';|
185
+ end, element)
186
+
187
+ @element = element
188
+ instance_eval(&block)
189
+ @element = nil
190
+
191
+ execute_and_ignore(element.operate { %|element.style.backgroundColor = element.originalColor;| })
192
+ end
193
+
194
+ def element_exists?(element = @element, &block)
195
+ block ||= Proc.new {}
196
+ execute(element.operate(&block), element)
197
+ return true
198
+ rescue UnknownObjectException
199
+ return false
200
+ end
201
+
202
+ def select_option(element = @element)
203
+ execute(element.operate do
204
+ %|var selected = -1;
205
+ var previous_selection = 0;
206
+ for (var i = 0; i < element.options.length; i++) {
207
+ if (element.options[i].selected) {
208
+ previous_selection = i;
209
+ }
210
+ if (element.options[i].#{element.how} == '#{element.what}') {
211
+ element.options[i].selected = true;
212
+ selected = i;
213
+ }
214
+ }
215
+ if (selected == -1) {
216
+ return '#{ELEMENT_NOT_FOUND}';
217
+ } else if (previous_selection != selected) {
218
+ element.selectedIndex = selected;
219
+ dispatchOnChange(element.options[selected]);
220
+ }
221
+ |
222
+ end, element)
223
+ end
224
+
225
+ def option_exists?(element = @element)
226
+ element_exists?(element) { handle_option(element) }
227
+ end
228
+
229
+ def handle_option(select_list)
230
+ %|var option_found = false;
231
+ for (var i = 0; i < element.options.length; i++) {
232
+ if (element.options[i].#{select_list.how} == '#{select_list.what}') {
233
+ option_found = true;
234
+ }
235
+ }
236
+ if (!option_found) {
237
+ return '#{ELEMENT_NOT_FOUND}';
238
+ }|
239
+ end
240
+ private :handle_option
241
+
242
+ def option_selected?(element = @element)
243
+ element_exists?(element) { handle_selected(element) == "true" }
244
+ end
245
+
246
+ def handle_selected(select_list)
247
+ %|var selected = false;
248
+ for (var i = 0; i < element.options.length; i++) {
249
+ if (element.options[i].#{select_list.how} == '#{select_list.what}' && element.options[i].selected) {
250
+ selected = true;
251
+ }
252
+ }
253
+ return selected;|
254
+ end
255
+ private :handle_option
256
+
257
+ def clear_text_input(element = @element)
258
+ execute(element.operate { %|element.value = '';| }, element)
259
+ end
260
+
261
+ def append_text_input(value, element = @element)
262
+ sleep typing_lag
263
+ execute(element.operate do
264
+ %|element.value += '#{value}';
265
+ dispatchOnChange(element);
266
+ element.setSelectionRange(element.value.length, element.value.length);|
267
+ end, element)
268
+ end
269
+
270
+ def click_element(element = @element)
271
+ page_load do
272
+ # Not sure if these events should be either/or. But if you have an image with an onclick, it fires twice without the else clause.
273
+ execute(element.operate { %|
274
+ if (element.click) {
275
+ element.click();
276
+ } else {
277
+ if (element.onclick) {
278
+ var event = DOCUMENT.createEvent('HTMLEvents');
279
+ event.initEvent('click', true, true);
280
+ element.onclick(event);
281
+ } else {
282
+ var event = DOCUMENT.createEvent('MouseEvents');
283
+ event.initEvent('click', true, true);
284
+ element.dispatchEvent(event);
285
+ }
286
+
287
+ }| })
288
+ end
289
+ end
290
+
291
+ def click_link(element = @element)
292
+ click = %/
293
+ function baseTarget() {
294
+ var bases = document.getElementsByTagName('BASE');
295
+ if (bases.length > 0) {
296
+ return bases[0].target;
297
+ } else {
298
+ return;
299
+ }
300
+ }
301
+ function undefinedTarget(target) {
302
+ return target == undefined || target == '';
303
+ }
304
+ function topTarget(target) {
305
+ return undefinedTarget(target) || target == '_top';
306
+ }
307
+ function nextLocation(element) {
308
+ var target = element.target;
309
+ if (undefinedTarget(target) && baseTarget()) {
310
+ top[baseTarget()].location = element.href;
311
+ } else if (topTarget(target)) {
312
+ top.location = element.href;
313
+ } else {
314
+ top[target].location = element.href;
315
+ }
316
+ }
317
+ var click = DOCUMENT.createEvent('HTMLEvents');
318
+ click.initEvent('click', true, true);
319
+ if (element.onclick) {
320
+ if (false != element.onclick(click)) {
321
+ nextLocation(element);
322
+ }
323
+ } else {
324
+ nextLocation(element);
325
+ }/
326
+ page_load do
327
+ execute(js.operate(find_link(element), click))
328
+ end
329
+ end
330
+
331
+ def operate_on_link(element)
332
+ js.operate(find_link(element), yield)
333
+ end
334
+
335
+ def find_link(element)
336
+ case element.how
337
+ when :index:
338
+ %|var element = document.getElementsByTagName('A')[#{element.what-1}];|
339
+ else
340
+ %|var element = undefined;
341
+ for (var i = 0; i < document.links.length; i++) {
342
+ if (document.links[i].#{handle_match(element)}) {
343
+ element = document.links[i];
344
+ break;
345
+ }
346
+ }|
347
+ end
348
+ end
349
+ private :find_link
350
+
351
+ def handle_match(element, how = nil)
352
+ how = {:text => "text", :url => "href"}[element.how] unless how
353
+ case element.what
354
+ when Regexp:
355
+ %|#{how}.match(/#{element.what.source}/#{element.what.casefold? ? "i":nil})|
356
+ when String:
357
+ %|#{how} == '#{element.what}'|
358
+ else
359
+ raise RuntimeError, "Unable to locate #{element.name} with #{element.how}"
360
+ end
361
+ end
362
+ private :handle_match
363
+
364
+ # Contributed by Kyle Campos
365
+ def checkbox_is_checked?(element = @element)
366
+ execute(element.operate { %|return element.checked;| }, element)
367
+ end
368
+
369
+ def operate_by_input_value(element)
370
+ js.operate(%|
371
+ var elements = document.getElementsByTagName('INPUT');
372
+ var element = undefined;
373
+ for (var i = 0; i < elements.length; i++) {
374
+ if (elements[i].value == '#{element.what}') {
375
+ element = elements[i];
376
+ break;
377
+ }
378
+ }|, yield)
379
+ end
380
+
381
+ def operate_by_name(element)
382
+ js.operate(%|
383
+ var elements = document.getElementsByName('#{element.what}');
384
+ var element = undefined;
385
+ for (var i = 0; i < elements.length; i++) {
386
+ if (elements[i].tagName != 'META' && elements[i].tagName == '#{element.tag}') {
387
+ #{handle_form_element_name_match(element)}
388
+ }
389
+ }|, yield)
390
+ end
391
+
392
+ def operate_by_class(element)
393
+ js.operate(%|
394
+ var elements = document.getElementsByClassName('#{element.what}');
395
+ var element = elements[0];|, yield)
396
+ end
397
+
398
+ # Checkboxes/Radios have the same name, different values
399
+ def handle_form_element_name_match(element)
400
+ element_capture = %|element = elements[i];break;|
401
+ if element.by_value
402
+ %|if (elements[i].value == '#{element.by_value}') {
403
+ #{element_capture}
404
+ }|
405
+ else
406
+ element_capture
407
+ end
408
+ end
409
+ private :handle_form_element_name_match
410
+
411
+ def operate_by_id(element)
412
+ js.operate("var element = document.getElementById('#{element.what}');", yield)
413
+ end
414
+
415
+ def operate_by_index(element)
416
+ js.operate(%|var element = document.getElementsByTagName('#{element.tag}')[#{element.what-1}];|, yield)
417
+ end
418
+
419
+ def operate_by_src(element, &block)
420
+ operate_by(element, 'src', &block)
421
+ end
422
+
423
+ def operate_by_text(element, &block)
424
+ operate_by(element, 'innerText', &block)
425
+ end
426
+
427
+ def operate_by(element, attribute)
428
+ js.operate(%|var elements = document.getElementsByTagName('#{element.tag}');
429
+ var element = undefined;
430
+ for (var i = 0; i < elements.length; i++) {
431
+ if (elements[i].#{handle_match(element, attribute)}) {
432
+ element = elements[i];
433
+ break;
434
+ }
435
+ }|, yield)
436
+ end
437
+
438
+ def submit_form(element)
439
+ page_load do
440
+ execute(element.operate { %|element.submit();| })
441
+ end
442
+ end
443
+
444
+ def click_alert
445
+ execute_system_events(%|
446
+ tell window 1
447
+ if button named "OK" exists then
448
+ click button named "OK"
449
+ end if
450
+ end tell|)
451
+ end
452
+
453
+ def click_security_warning(label)
454
+ execute_system_events(%|
455
+ tell window 1
456
+ tell sheet 1
457
+ tell group 2
458
+ if button named "#{label}" exists then
459
+ click button named "#{label}"
460
+ return "#{EXTRA_ACTION_SUCCESS}"
461
+ end if
462
+ end tell
463
+ end tell
464
+ end tell|, true)
465
+ end
466
+
467
+ def for_table(element)
468
+ AppleScripter.new(TableJavaScripter.new(element))
469
+ end
470
+
471
+ def for_frame(element)
472
+ # verify the frame exists
473
+ execute(
474
+ %|if (parent.#{element.name} == undefined) {
475
+ return '#{FRAME_NOT_FOUND}';
476
+ }|, element)
477
+ AppleScripter.new(FrameJavaScripter.new(element))
478
+ end
479
+
480
+ def speak_value_of(element = @element)
481
+ speak(get_value_for(element))
482
+ end
483
+
484
+ def speak_text_of(element = @element)
485
+ speak(element.text)
486
+ end
487
+
488
+ def speak_options_for(element = @element)
489
+ values = execute(element.operate do
490
+ %|var values = '';
491
+ for (var i = 0; i < element.options.length; i++) {
492
+ if (element.options[i].selected == true) {
493
+ values += ' ' + element.options[i].text;
494
+ }
495
+ }
496
+ return values|
497
+ end, element)
498
+ speak(values)
499
+ end
500
+
501
+ def speak(string)
502
+ `osascript <<SCRIPT
503
+ say "#{string.quote_safe}"
504
+ SCRIPT`
505
+ nil
506
+ end
507
+
508
+
509
+ private
510
+
511
+ def execute(script, element = nil)
512
+ response = eval_js(script)
513
+ case response
514
+ when NO_RESPONSE:
515
+ nil
516
+ when ELEMENT_NOT_FOUND:
517
+ raise UnknownObjectException, "Unable to locate #{element.name} element with #{element.how} of #{element.what}"
518
+ when TABLE_CELL_NOT_FOUND:
519
+ raise UnknownCellException, "Unable to locate a table cell with #{element.how} of #{element.what}"
520
+ when FRAME_NOT_FOUND:
521
+ raise UnknownFrameException, "Unable to locate a frame with name #{element.name}"
522
+ else
523
+ response
524
+ end
525
+ end
526
+
527
+ def execute_and_ignore(script)
528
+ eval_js(script)
529
+ nil
530
+ end
531
+
532
+ # Must have "Enable access for assistive devices" checked in System Preferences > Universal Access
533
+ def execute_system_events(script, capture_result = false)
534
+ result = `osascript <<SCRIPT
535
+ tell application "System Events" to tell process "Safari"
536
+ #{script}
537
+ end tell
538
+ SCRIPT`
539
+
540
+ if capture_result && result
541
+ return result.chomp
542
+ end
543
+ end
544
+
545
+ def page_load(extra_action = nil)
546
+ yield
547
+ sleep 1
548
+
549
+ tries = 0
550
+ TIMEOUT.times do |tries|
551
+ if "complete" == eval_js("return DOCUMENT.readyState") && !@document.URL.get.blank?
552
+ sleep 0.4
553
+ handle_client_redirect
554
+ break
555
+ elsif extra_action
556
+ result = extra_action.call
557
+ break if result == EXTRA_ACTION_SUCCESS
558
+ else
559
+ sleep 1
560
+ end
561
+ end
562
+ raise "Unable to load page within #{TIMEOUT} seconds" if tries == TIMEOUT-1
563
+ end
564
+
565
+ def handle_client_redirect
566
+ no_redirect_flag = "proceed"
567
+ redirect = eval_js(
568
+ %|var elements = DOCUMENT.getElementsByTagName('META');
569
+ for (var i = 0; i < elements.length; i++) {
570
+ if ("refresh" == elements[i].httpEquiv && elements[i].content != undefined && elements[i].content.indexOf(";") != "-1") {
571
+ return elements[i].content;
572
+ }
573
+ }
574
+ return "#{no_redirect_flag}"|)
575
+ if redirect != no_redirect_flag
576
+ time_til_redirect = redirect.split(";").first.to_i
577
+ sleep time_til_redirect
578
+ end
579
+ end
580
+
581
+ def eval_js(script)
582
+ @app.do_JavaScript(js.wrap(script), :in => @document)
583
+ end
584
+ end # class AppleScripter
585
+ end