mpeychich-safariwatir 0.3.6

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.
@@ -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,587 @@
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 || Symbol:
357
+ %|#{how} == '#{element.what}'|
358
+ else
359
+ raise RuntimeError, "Unable to locate #{element.tag} 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 element_disabled?(element = @element)
370
+ execute(element.operate { %|return element.disabled;| }, element)
371
+ end
372
+
373
+ def operate_by_input_value(element)
374
+ js.operate(%|
375
+ var elements = document.getElementsByTagName('INPUT');
376
+ var element = undefined;
377
+ for (var i = 0; i < elements.length; i++) {
378
+ if (elements[i].value == '#{element.what}') {
379
+ element = elements[i];
380
+ break;
381
+ }
382
+ }|, yield)
383
+ end
384
+
385
+ def operate_by_name(element)
386
+ js.operate(%|
387
+ var elements = document.getElementsByName('#{element.what}');
388
+ var element = undefined;
389
+ for (var i = 0; i < elements.length; i++) {
390
+ if (elements[i].tagName != 'META' && elements[i].tagName == '#{element.tag}') {
391
+ #{handle_form_element_name_match(element)}
392
+ }
393
+ }|, yield)
394
+ end
395
+
396
+ def operate_by_class(element, &block)
397
+ operate_by(element, 'className', &block)
398
+ end
399
+
400
+ # Checkboxes/Radios have the same name, different values
401
+ def handle_form_element_name_match(element)
402
+ element_capture = %|element = elements[i];break;|
403
+ if element.by_value
404
+ %|if (elements[i].value == '#{element.by_value}') {
405
+ #{element_capture}
406
+ }|
407
+ else
408
+ element_capture
409
+ end
410
+ end
411
+ private :handle_form_element_name_match
412
+
413
+ def operate_by_id(element, &block)
414
+ operate_by(element, 'id', &block)
415
+ end
416
+
417
+ def operate_by_index(element)
418
+ js.operate(%|var element = document.getElementsByTagName('#{element.tag}')[#{element.what-1}];|, yield)
419
+ end
420
+
421
+ def operate_by_src(element, &block)
422
+ operate_by(element, 'src', &block)
423
+ end
424
+
425
+ def operate_by_text(element, &block)
426
+ operate_by(element, 'innerText', &block)
427
+ end
428
+
429
+ def operate_by(element, attribute)
430
+ js.operate(%|var elements = document.getElementsByTagName('#{element.tag}');
431
+ var element = undefined;
432
+ for (var i = 0; i < elements.length; i++) {
433
+ if (elements[i].#{handle_match(element, attribute)}) {
434
+ element = elements[i];
435
+ break;
436
+ }
437
+ }|, yield)
438
+ end
439
+
440
+ def submit_form(element)
441
+ page_load do
442
+ execute(element.operate { %|element.submit();| })
443
+ end
444
+ end
445
+
446
+ def click_alert
447
+ execute_system_events(%|
448
+ tell window 1
449
+ if button named "OK" exists then
450
+ click button named "OK"
451
+ end if
452
+ end tell|)
453
+ end
454
+
455
+ def click_security_warning(label)
456
+ execute_system_events(%|
457
+ tell window 1
458
+ tell sheet 1
459
+ tell group 2
460
+ if button named "#{label}" exists then
461
+ click button named "#{label}"
462
+ return "#{EXTRA_ACTION_SUCCESS}"
463
+ end if
464
+ end tell
465
+ end tell
466
+ end tell|, true)
467
+ end
468
+
469
+ def for_table(element)
470
+ AppleScripter.new(TableJavaScripter.new(element))
471
+ end
472
+
473
+ def for_frame(element)
474
+ # verify the frame exists
475
+ execute(
476
+ %|if (parent.#{element.name} == undefined) {
477
+ return '#{FRAME_NOT_FOUND}';
478
+ }|, element)
479
+ AppleScripter.new(FrameJavaScripter.new(element))
480
+ end
481
+
482
+ def speak_value_of(element = @element)
483
+ speak(get_value_for(element))
484
+ end
485
+
486
+ def speak_text_of(element = @element)
487
+ speak(element.text)
488
+ end
489
+
490
+ def speak_options_for(element = @element)
491
+ values = execute(element.operate do
492
+ %|var values = '';
493
+ for (var i = 0; i < element.options.length; i++) {
494
+ if (element.options[i].selected == true) {
495
+ values += ' ' + element.options[i].text;
496
+ }
497
+ }
498
+ return values|
499
+ end, element)
500
+ speak(values)
501
+ end
502
+
503
+ def speak(string)
504
+ `osascript <<SCRIPT
505
+ say "#{string.quote_safe}"
506
+ SCRIPT`
507
+ nil
508
+ end
509
+
510
+
511
+ private
512
+
513
+ def execute(script, element = nil)
514
+ response = eval_js(script)
515
+ case response
516
+ when NO_RESPONSE:
517
+ nil
518
+ when ELEMENT_NOT_FOUND:
519
+ raise UnknownObjectException, "Unable to locate #{element.tag} element with #{element.how} of #{element.what}"
520
+ when TABLE_CELL_NOT_FOUND:
521
+ raise UnknownCellException, "Unable to locate a table cell with #{element.how} of #{element.what}"
522
+ when FRAME_NOT_FOUND:
523
+ raise UnknownFrameException, "Unable to locate a frame with name #{element.tag}"
524
+ else
525
+ response
526
+ end
527
+ end
528
+
529
+ def execute_and_ignore(script)
530
+ eval_js(script)
531
+ nil
532
+ end
533
+
534
+ # Must have "Enable access for assistive devices" checked in System Preferences > Universal Access
535
+ def execute_system_events(script, capture_result = false)
536
+ result = `osascript <<SCRIPT
537
+ tell application "System Events" to tell process "Safari"
538
+ #{script}
539
+ end tell
540
+ SCRIPT`
541
+
542
+ if capture_result && result
543
+ return result.chomp
544
+ end
545
+ end
546
+
547
+ def page_load(extra_action = nil)
548
+ yield
549
+ sleep 1
550
+
551
+ tries = 0
552
+ TIMEOUT.times do |tries|
553
+ if "complete" == eval_js("return DOCUMENT.readyState") && !@document.URL.get.blank?
554
+ sleep 0.4
555
+ handle_client_redirect
556
+ break
557
+ elsif extra_action
558
+ result = extra_action.call
559
+ break if result == EXTRA_ACTION_SUCCESS
560
+ else
561
+ sleep 1
562
+ end
563
+ end
564
+ raise "Unable to load page within #{TIMEOUT} seconds" if tries == TIMEOUT-1
565
+ end
566
+
567
+ def handle_client_redirect
568
+ no_redirect_flag = "proceed"
569
+ redirect = eval_js(
570
+ %|var elements = DOCUMENT.getElementsByTagName('META');
571
+ for (var i = 0; i < elements.length; i++) {
572
+ if ("refresh" == elements[i].httpEquiv && elements[i].content != undefined && elements[i].content.indexOf(";") != "-1") {
573
+ return elements[i].content;
574
+ }
575
+ }
576
+ return "#{no_redirect_flag}"|)
577
+ if redirect != no_redirect_flag
578
+ time_til_redirect = redirect.split(";").first.to_i
579
+ sleep time_til_redirect
580
+ end
581
+ end
582
+
583
+ def eval_js(script)
584
+ @app.do_JavaScript(js.wrap(script), :in => @document)
585
+ end
586
+ end # class AppleScripter
587
+ end