redsquirrel-safariwatir 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
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