caius-safariwatir 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,573 @@
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 close
116
+ @app.quit
117
+ end
118
+
119
+ def navigate_to(url, &extra_action)
120
+ page_load(extra_action) do
121
+ @document.URL.set(url)
122
+ end
123
+ end
124
+
125
+ def reload
126
+ execute(%|window.location.reload()|)
127
+ end
128
+
129
+ def get_text_for(element = @element)
130
+ execute(element.operate { %|return element.innerText| }, element)
131
+ end
132
+
133
+ def get_html_for(element = @element)
134
+ execute(element.operate { %|return element.innerHTML| }, element)
135
+ end
136
+
137
+ def operate_by_table_cell(element = @element)
138
+ %|var element = document;
139
+ if (element == undefined) {
140
+ return '#{TABLE_CELL_NOT_FOUND}';
141
+ }
142
+ #{yield}|
143
+ end
144
+
145
+ def get_value_for(element = @element)
146
+ execute(element.operate { %|return element.value;| }, element)
147
+ end
148
+
149
+ def document_text
150
+ execute(%|return document.getElementsByTagName('BODY').item(0).innerText;|)
151
+ end
152
+
153
+ def document_html
154
+ execute(%|return document.all[0].outerHTML;|)
155
+ end
156
+
157
+ def document_title
158
+ execute(%|return document.title;|)
159
+ end
160
+
161
+ def focus(element)
162
+ execute(element.operate { %|element.focus();| }, element)
163
+ end
164
+
165
+ def blur(element)
166
+ execute(element.operate { %|element.blur();| }, element)
167
+ end
168
+
169
+ def highlight(element, &block)
170
+ execute(element.operate do
171
+ %|element.originalColor = element.style.backgroundColor;
172
+ element.style.backgroundColor = 'yellow';|
173
+ end, element)
174
+
175
+ @element = element
176
+ instance_eval(&block)
177
+ @element = nil
178
+
179
+ execute_and_ignore(element.operate { %|element.style.backgroundColor = element.originalColor;| })
180
+ end
181
+
182
+ def element_exists?(element = @element, &block)
183
+ block ||= Proc.new {}
184
+ execute(element.operate(&block), element)
185
+ return true
186
+ rescue UnknownObjectException
187
+ return false
188
+ end
189
+
190
+ def select_option(element = @element)
191
+ execute(element.operate do
192
+ %|var selected = -1;
193
+ var previous_selection = 0;
194
+ for (var i = 0; i < element.options.length; i++) {
195
+ if (element.options[i].selected) {
196
+ previous_selection = i;
197
+ }
198
+ if (element.options[i].#{element.how} == '#{element.what}') {
199
+ element.options[i].selected = true;
200
+ selected = i;
201
+ }
202
+ }
203
+ if (selected == -1) {
204
+ return '#{ELEMENT_NOT_FOUND}';
205
+ } else if (previous_selection != selected) {
206
+ element.selectedIndex = selected;
207
+ dispatchOnChange(element.options[selected]);
208
+ }
209
+ |
210
+ end, element)
211
+ end
212
+
213
+ def option_exists?(element = @element)
214
+ element_exists?(element) { handle_option(element) }
215
+ end
216
+
217
+ def handle_option(select_list)
218
+ %|var option_found = false;
219
+ for (var i = 0; i < element.options.length; i++) {
220
+ if (element.options[i].#{select_list.how} == '#{select_list.what}') {
221
+ option_found = true;
222
+ }
223
+ }
224
+ if (!option_found) {
225
+ return '#{ELEMENT_NOT_FOUND}';
226
+ }|
227
+ end
228
+ private :handle_option
229
+
230
+ def option_selected?(element = @element)
231
+ element_exists?(element) { handle_selected(element) == "true" }
232
+ end
233
+
234
+ def handle_selected(select_list)
235
+ %|var selected = false;
236
+ for (var i = 0; i < element.options.length; i++) {
237
+ if (element.options[i].#{select_list.how} == '#{select_list.what}' && element.options[i].selected) {
238
+ selected = true;
239
+ }
240
+ }
241
+ return selected;|
242
+ end
243
+ private :handle_option
244
+
245
+ def clear_text_input(element = @element)
246
+ execute(element.operate { %|element.value = '';| }, element)
247
+ end
248
+
249
+ def append_text_input(value, element = @element)
250
+ sleep typing_lag
251
+ execute(element.operate do
252
+ %|element.value += '#{value}';
253
+ dispatchOnChange(element);
254
+ element.setSelectionRange(element.value.length, element.value.length);|
255
+ end, element)
256
+ end
257
+
258
+ def click_element(element = @element)
259
+ page_load do
260
+ # 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.
261
+ execute(element.operate { %|
262
+ if (element.click) {
263
+ element.click();
264
+ } else {
265
+ if (element.onclick) {
266
+ var event = DOCUMENT.createEvent('HTMLEvents');
267
+ event.initEvent('click', true, true);
268
+ element.onclick(event);
269
+ } else {
270
+ var event = DOCUMENT.createEvent('MouseEvents');
271
+ event.initEvent('click', true, true);
272
+ element.dispatchEvent(event);
273
+ }
274
+
275
+ }| })
276
+ end
277
+ end
278
+
279
+ def click_link(element = @element)
280
+ click = %/
281
+ function baseTarget() {
282
+ var bases = document.getElementsByTagName('BASE');
283
+ if (bases.length > 0) {
284
+ return bases[0].target;
285
+ } else {
286
+ return;
287
+ }
288
+ }
289
+ function undefinedTarget(target) {
290
+ return target == undefined || target == '';
291
+ }
292
+ function topTarget(target) {
293
+ return undefinedTarget(target) || target == '_top';
294
+ }
295
+ function nextLocation(element) {
296
+ var target = element.target;
297
+ if (undefinedTarget(target) && baseTarget()) {
298
+ top[baseTarget()].location = element.href;
299
+ } else if (topTarget(target)) {
300
+ top.location = element.href;
301
+ } else {
302
+ top[target].location = element.href;
303
+ }
304
+ }
305
+ var click = DOCUMENT.createEvent('HTMLEvents');
306
+ click.initEvent('click', true, true);
307
+ if (element.onclick) {
308
+ if (false != element.onclick(click)) {
309
+ nextLocation(element);
310
+ }
311
+ } else {
312
+ nextLocation(element);
313
+ }/
314
+ page_load do
315
+ execute(js.operate(find_link(element), click))
316
+ end
317
+ end
318
+
319
+ def operate_on_link(element)
320
+ js.operate(find_link(element), yield)
321
+ end
322
+
323
+ def find_link(element)
324
+ case element.how
325
+ when :index:
326
+ %|var element = document.getElementsByTagName('A')[#{element.what-1}];|
327
+ else
328
+ %|var element = undefined;
329
+ for (var i = 0; i < document.links.length; i++) {
330
+ if (document.links[i].#{handle_match(element)}) {
331
+ element = document.links[i];
332
+ break;
333
+ }
334
+ }|
335
+ end
336
+ end
337
+ private :find_link
338
+
339
+ def handle_match(element, how = nil)
340
+ how = {:text => "text", :url => "href"}[element.how] unless how
341
+ case element.what
342
+ when Regexp:
343
+ %|#{how}.match(/#{element.what.source}/#{element.what.casefold? ? "i":nil})|
344
+ when String:
345
+ %|#{how} == '#{element.what}'|
346
+ else
347
+ raise RuntimeError, "Unable to locate #{element.name} with #{element.how}"
348
+ end
349
+ end
350
+ private :handle_match
351
+
352
+ # Contributed by Kyle Campos
353
+ def checkbox_is_checked?(element = @element)
354
+ execute(element.operate { %|return element.checked;| }, element)
355
+ end
356
+
357
+ def operate_by_input_value(element)
358
+ js.operate(%|
359
+ var elements = document.getElementsByTagName('INPUT');
360
+ var element = undefined;
361
+ for (var i = 0; i < elements.length; i++) {
362
+ if (elements[i].value == '#{element.what}') {
363
+ element = elements[i];
364
+ break;
365
+ }
366
+ }|, yield)
367
+ end
368
+
369
+ def operate_by_name(element)
370
+ js.operate(%|
371
+ var elements = document.getElementsByName('#{element.what}');
372
+ var element = undefined;
373
+ for (var i = 0; i < elements.length; i++) {
374
+ if (elements[i].tagName != 'META' && elements[i].tagName == '#{element.tag}') {
375
+ #{handle_form_element_name_match(element)}
376
+ }
377
+ }|, yield)
378
+ end
379
+
380
+ def operate_by_class(element)
381
+ js.operate(%|
382
+ var elements = document.getElementsByClassName('#{element.what}');
383
+ var element = elements[0];|, yield)
384
+ end
385
+
386
+ # Checkboxes/Radios have the same name, different values
387
+ def handle_form_element_name_match(element)
388
+ element_capture = %|element = elements[i];break;|
389
+ if element.by_value
390
+ %|if (elements[i].value == '#{element.by_value}') {
391
+ #{element_capture}
392
+ }|
393
+ else
394
+ element_capture
395
+ end
396
+ end
397
+ private :handle_form_element_name_match
398
+
399
+ def operate_by_id(element)
400
+ js.operate("var element = document.getElementById('#{element.what}');", yield)
401
+ end
402
+
403
+ def operate_by_index(element)
404
+ js.operate(%|var element = document.getElementsByTagName('#{element.tag}')[#{element.what-1}];|, yield)
405
+ end
406
+
407
+ def operate_by_src(element, &block)
408
+ operate_by(element, 'src', &block)
409
+ end
410
+
411
+ def operate_by_text(element, &block)
412
+ operate_by(element, 'innerText', &block)
413
+ end
414
+
415
+ def operate_by(element, attribute)
416
+ js.operate(%|var elements = document.getElementsByTagName('#{element.tag}');
417
+ var element = undefined;
418
+ for (var i = 0; i < elements.length; i++) {
419
+ if (elements[i].#{handle_match(element, attribute)}) {
420
+ element = elements[i];
421
+ break;
422
+ }
423
+ }|, yield)
424
+ end
425
+
426
+ def submit_form(element)
427
+ page_load do
428
+ execute(element.operate { %|element.submit();| })
429
+ end
430
+ end
431
+
432
+ def click_alert
433
+ execute_system_events(%|
434
+ tell window 1
435
+ if button named "OK" exists then
436
+ click button named "OK"
437
+ end if
438
+ end tell|)
439
+ end
440
+
441
+ def click_security_warning(label)
442
+ execute_system_events(%|
443
+ tell window 1
444
+ tell sheet 1
445
+ tell group 2
446
+ if button named "#{label}" exists then
447
+ click button named "#{label}"
448
+ return "#{EXTRA_ACTION_SUCCESS}"
449
+ end if
450
+ end tell
451
+ end tell
452
+ end tell|, true)
453
+ end
454
+
455
+ def for_table(element)
456
+ AppleScripter.new(TableJavaScripter.new(element))
457
+ end
458
+
459
+ def for_frame(element)
460
+ # verify the frame exists
461
+ execute(
462
+ %|if (parent.#{element.name} == undefined) {
463
+ return '#{FRAME_NOT_FOUND}';
464
+ }|, element)
465
+ AppleScripter.new(FrameJavaScripter.new(element))
466
+ end
467
+
468
+ def speak_value_of(element = @element)
469
+ speak(get_value_for(element))
470
+ end
471
+
472
+ def speak_text_of(element = @element)
473
+ speak(element.text)
474
+ end
475
+
476
+ def speak_options_for(element = @element)
477
+ values = execute(element.operate do
478
+ %|var values = '';
479
+ for (var i = 0; i < element.options.length; i++) {
480
+ if (element.options[i].selected == true) {
481
+ values += ' ' + element.options[i].text;
482
+ }
483
+ }
484
+ return values|
485
+ end, element)
486
+ speak(values)
487
+ end
488
+
489
+ def speak(string)
490
+ `osascript <<SCRIPT
491
+ say "#{string.quote_safe}"
492
+ SCRIPT`
493
+ nil
494
+ end
495
+
496
+
497
+ private
498
+
499
+ def execute(script, element = nil)
500
+ response = eval_js(script)
501
+ case response
502
+ when NO_RESPONSE:
503
+ nil
504
+ when ELEMENT_NOT_FOUND:
505
+ raise UnknownObjectException, "Unable to locate #{element.name} element with #{element.how} of #{element.what}"
506
+ when TABLE_CELL_NOT_FOUND:
507
+ raise UnknownCellException, "Unable to locate a table cell with #{element.how} of #{element.what}"
508
+ when FRAME_NOT_FOUND:
509
+ raise UnknownFrameException, "Unable to locate a frame with name #{element.name}"
510
+ else
511
+ response
512
+ end
513
+ end
514
+
515
+ def execute_and_ignore(script)
516
+ eval_js(script)
517
+ nil
518
+ end
519
+
520
+ # Must have "Enable access for assistive devices" checked in System Preferences > Universal Access
521
+ def execute_system_events(script, capture_result = false)
522
+ result = `osascript <<SCRIPT
523
+ tell application "System Events" to tell process "Safari"
524
+ #{script}
525
+ end tell
526
+ SCRIPT`
527
+
528
+ if capture_result && result
529
+ return result.chomp
530
+ end
531
+ end
532
+
533
+ def page_load(extra_action = nil)
534
+ yield
535
+ sleep 1
536
+
537
+ tries = 0
538
+ TIMEOUT.times do |tries|
539
+ if "complete" == eval_js("return DOCUMENT.readyState") && !@document.URL.get.blank?
540
+ sleep 0.4
541
+ handle_client_redirect
542
+ break
543
+ elsif extra_action
544
+ result = extra_action.call
545
+ break if result == EXTRA_ACTION_SUCCESS
546
+ else
547
+ sleep 1
548
+ end
549
+ end
550
+ raise "Unable to load page within #{TIMEOUT} seconds" if tries == TIMEOUT-1
551
+ end
552
+
553
+ def handle_client_redirect
554
+ no_redirect_flag = "proceed"
555
+ redirect = eval_js(
556
+ %|var elements = DOCUMENT.getElementsByTagName('META');
557
+ for (var i = 0; i < elements.length; i++) {
558
+ if ("refresh" == elements[i].httpEquiv && elements[i].content != undefined && elements[i].content.indexOf(";") != "-1") {
559
+ return elements[i].content;
560
+ }
561
+ }
562
+ return "#{no_redirect_flag}"|)
563
+ if redirect != no_redirect_flag
564
+ time_til_redirect = redirect.split(";").first.to_i
565
+ sleep time_til_redirect
566
+ end
567
+ end
568
+
569
+ def eval_js(script)
570
+ @app.do_JavaScript(js.wrap(script), :in => @document)
571
+ end
572
+ end # class AppleScripter
573
+ end