imseng-capybara-webkit 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. data/.gitignore +21 -0
  2. data/.rspec +2 -0
  3. data/Appraisals +7 -0
  4. data/CONTRIBUTING.md +47 -0
  5. data/ChangeLog +70 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +19 -0
  8. data/NEWS.md +36 -0
  9. data/README.md +114 -0
  10. data/Rakefile +65 -0
  11. data/bin/Info.plist +22 -0
  12. data/capybara-webkit.gemspec +28 -0
  13. data/extconf.rb +2 -0
  14. data/gemfiles/1.0.gemfile +7 -0
  15. data/gemfiles/1.0.gemfile.lock +70 -0
  16. data/gemfiles/1.1.gemfile +7 -0
  17. data/gemfiles/1.1.gemfile.lock +70 -0
  18. data/lib/capybara/driver/webkit/browser.rb +164 -0
  19. data/lib/capybara/driver/webkit/connection.rb +120 -0
  20. data/lib/capybara/driver/webkit/cookie_jar.rb +55 -0
  21. data/lib/capybara/driver/webkit/node.rb +118 -0
  22. data/lib/capybara/driver/webkit/socket_debugger.rb +43 -0
  23. data/lib/capybara/driver/webkit/version.rb +7 -0
  24. data/lib/capybara/driver/webkit.rb +136 -0
  25. data/lib/capybara/webkit/matchers.rb +37 -0
  26. data/lib/capybara/webkit.rb +13 -0
  27. data/lib/capybara-webkit.rb +1 -0
  28. data/lib/capybara_webkit_builder.rb +68 -0
  29. data/spec/browser_spec.rb +173 -0
  30. data/spec/capybara_webkit_builder_spec.rb +37 -0
  31. data/spec/connection_spec.rb +54 -0
  32. data/spec/cookie_jar_spec.rb +48 -0
  33. data/spec/driver_rendering_spec.rb +80 -0
  34. data/spec/driver_resize_window_spec.rb +59 -0
  35. data/spec/driver_spec.rb +1552 -0
  36. data/spec/integration/driver_spec.rb +20 -0
  37. data/spec/integration/session_spec.rb +137 -0
  38. data/spec/self_signed_ssl_cert.rb +42 -0
  39. data/spec/spec_helper.rb +46 -0
  40. data/src/Body.h +12 -0
  41. data/src/ClearCookies.cpp +15 -0
  42. data/src/ClearCookies.h +11 -0
  43. data/src/Command.cpp +19 -0
  44. data/src/Command.h +31 -0
  45. data/src/CommandFactory.cpp +37 -0
  46. data/src/CommandFactory.h +16 -0
  47. data/src/CommandParser.cpp +76 -0
  48. data/src/CommandParser.h +33 -0
  49. data/src/Connection.cpp +71 -0
  50. data/src/Connection.h +37 -0
  51. data/src/ConsoleMessages.cpp +10 -0
  52. data/src/ConsoleMessages.h +12 -0
  53. data/src/CurrentUrl.cpp +68 -0
  54. data/src/CurrentUrl.h +16 -0
  55. data/src/Evaluate.cpp +84 -0
  56. data/src/Evaluate.h +22 -0
  57. data/src/Execute.cpp +16 -0
  58. data/src/Execute.h +12 -0
  59. data/src/Find.cpp +19 -0
  60. data/src/Find.h +13 -0
  61. data/src/FrameFocus.cpp +66 -0
  62. data/src/FrameFocus.h +28 -0
  63. data/src/GetCookies.cpp +20 -0
  64. data/src/GetCookies.h +14 -0
  65. data/src/Header.cpp +18 -0
  66. data/src/Header.h +11 -0
  67. data/src/Headers.cpp +10 -0
  68. data/src/Headers.h +12 -0
  69. data/src/IgnoreSslErrors.cpp +12 -0
  70. data/src/IgnoreSslErrors.h +12 -0
  71. data/src/JavascriptInvocation.cpp +14 -0
  72. data/src/JavascriptInvocation.h +19 -0
  73. data/src/NetworkAccessManager.cpp +29 -0
  74. data/src/NetworkAccessManager.h +19 -0
  75. data/src/NetworkCookieJar.cpp +101 -0
  76. data/src/NetworkCookieJar.h +15 -0
  77. data/src/Node.cpp +14 -0
  78. data/src/Node.h +13 -0
  79. data/src/NullCommand.cpp +10 -0
  80. data/src/NullCommand.h +11 -0
  81. data/src/PageLoadingCommand.cpp +46 -0
  82. data/src/PageLoadingCommand.h +40 -0
  83. data/src/Render.cpp +18 -0
  84. data/src/Render.h +12 -0
  85. data/src/RequestedUrl.cpp +12 -0
  86. data/src/RequestedUrl.h +12 -0
  87. data/src/Reset.cpp +29 -0
  88. data/src/Reset.h +15 -0
  89. data/src/ResizeWindow.cpp +16 -0
  90. data/src/ResizeWindow.h +12 -0
  91. data/src/Response.cpp +24 -0
  92. data/src/Response.h +15 -0
  93. data/src/Server.cpp +24 -0
  94. data/src/Server.h +21 -0
  95. data/src/SetCookie.cpp +16 -0
  96. data/src/SetCookie.h +11 -0
  97. data/src/SetProxy.cpp +22 -0
  98. data/src/SetProxy.h +11 -0
  99. data/src/Source.cpp +18 -0
  100. data/src/Source.h +19 -0
  101. data/src/Status.cpp +12 -0
  102. data/src/Status.h +12 -0
  103. data/src/UnsupportedContentHandler.cpp +32 -0
  104. data/src/UnsupportedContentHandler.h +18 -0
  105. data/src/Url.cpp +12 -0
  106. data/src/Url.h +12 -0
  107. data/src/Visit.cpp +12 -0
  108. data/src/Visit.h +12 -0
  109. data/src/WebPage.cpp +239 -0
  110. data/src/WebPage.h +58 -0
  111. data/src/body.cpp +10 -0
  112. data/src/capybara.js +315 -0
  113. data/src/find_command.h +29 -0
  114. data/src/main.cpp +33 -0
  115. data/src/webkit_server.pro +85 -0
  116. data/src/webkit_server.qrc +5 -0
  117. data/templates/Command.cpp +10 -0
  118. data/templates/Command.h +12 -0
  119. data/webkit_server.pro +4 -0
  120. metadata +298 -0
@@ -0,0 +1,1552 @@
1
+ require 'spec_helper'
2
+ require 'capybara/driver/webkit'
3
+
4
+ describe Capybara::Driver::Webkit do
5
+ subject { Capybara::Driver::Webkit.new(@app, :browser => $webkit_browser) }
6
+ before { subject.visit("/hello/world?success=true") }
7
+ after { subject.reset! }
8
+
9
+ context "iframe app" do
10
+ before(:all) do
11
+ @app = lambda do |env|
12
+ params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
13
+ if params["iframe"] == "true"
14
+ # We are in an iframe request.
15
+ p_id = "farewell"
16
+ msg = "goodbye"
17
+ iframe = nil
18
+ else
19
+ # We are not in an iframe request and need to make an iframe!
20
+ p_id = "greeting"
21
+ msg = "hello"
22
+ iframe = "<iframe id=\"f\" src=\"/?iframe=true\"></iframe>"
23
+ end
24
+ body = <<-HTML
25
+ <html>
26
+ <head>
27
+ <style type="text/css">
28
+ #display_none { display: none }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ #{iframe}
33
+ <script type="text/javascript">
34
+ document.write("<p id='#{p_id}'>#{msg}</p>");
35
+ </script>
36
+ </body>
37
+ </html>
38
+ HTML
39
+ [200,
40
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
41
+ [body]]
42
+ end
43
+ end
44
+
45
+ it "finds frames by index" do
46
+ subject.within_frame(0) do
47
+ subject.find("//*[contains(., 'goodbye')]").should_not be_empty
48
+ end
49
+ end
50
+
51
+ it "finds frames by id" do
52
+ subject.within_frame("f") do
53
+ subject.find("//*[contains(., 'goodbye')]").should_not be_empty
54
+ end
55
+ end
56
+
57
+ it "raises error for missing frame by index" do
58
+ expect { subject.within_frame(1) { } }.
59
+ to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
60
+ end
61
+
62
+ it "raise_error for missing frame by id" do
63
+ expect { subject.within_frame("foo") { } }.
64
+ to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
65
+ end
66
+
67
+ it "returns an attribute's value" do
68
+ subject.within_frame("f") do
69
+ subject.find("//p").first["id"].should == "farewell"
70
+ end
71
+ end
72
+
73
+ it "returns a node's text" do
74
+ subject.within_frame("f") do
75
+ subject.find("//p").first.text.should == "goodbye"
76
+ end
77
+ end
78
+
79
+ it "returns the current URL" do
80
+ subject.within_frame("f") do
81
+ port = subject.instance_variable_get("@rack_server").port
82
+ subject.current_url.should == "http://127.0.0.1:#{port}/?iframe=true"
83
+ end
84
+ end
85
+
86
+ it "returns the source code for the page" do
87
+ subject.within_frame("f") do
88
+ subject.source.should =~ %r{<html>.*farewell.*}m
89
+ end
90
+ end
91
+
92
+ it "evaluates Javascript" do
93
+ subject.within_frame("f") do
94
+ result = subject.evaluate_script(%<document.getElementById('farewell').innerText>)
95
+ result.should == "goodbye"
96
+ end
97
+ end
98
+
99
+ it "executes Javascript" do
100
+ subject.within_frame("f") do
101
+ subject.execute_script(%<document.getElementById('farewell').innerHTML = 'yo'>)
102
+ subject.find("//p[contains(., 'yo')]").should_not be_empty
103
+ end
104
+ end
105
+ end
106
+
107
+ context "redirect app" do
108
+ before(:all) do
109
+ @app = lambda do |env|
110
+ if env['PATH_INFO'] == '/target'
111
+ content_type = "<p>#{env['CONTENT_TYPE']}</p>"
112
+ [200, {"Content-Type" => "text/html", "Content-Length" => content_type.length.to_s}, [content_type]]
113
+ elsif env['PATH_INFO'] == '/form'
114
+ body = <<-HTML
115
+ <html>
116
+ <body>
117
+ <form action="/redirect" method="POST" enctype="multipart/form-data">
118
+ <input name="submit" type="submit" />
119
+ </form>
120
+ </body>
121
+ </html>
122
+ HTML
123
+ [200, {"Content-Type" => "text/html", "Content-Length" => body.length.to_s}, [body]]
124
+ else
125
+ [301, {"Location" => "/target"}, [""]]
126
+ end
127
+ end
128
+ end
129
+
130
+ it "should redirect without content type" do
131
+ subject.visit("/form")
132
+ subject.find("//input").first.click
133
+ subject.find("//p").first.text.should == ""
134
+ end
135
+
136
+ it "returns the current URL when changed by pushState after a redirect" do
137
+ subject.visit("/redirect-me")
138
+ port = subject.instance_variable_get("@rack_server").port
139
+ subject.execute_script("window.history.pushState({}, '', '/pushed-after-redirect')")
140
+ subject.current_url.should == "http://127.0.0.1:#{port}/pushed-after-redirect"
141
+ end
142
+
143
+ it "returns the current URL when changed by replaceState after a redirect" do
144
+ subject.visit("/redirect-me")
145
+ port = subject.instance_variable_get("@rack_server").port
146
+ subject.execute_script("window.history.replaceState({}, '', '/replaced-after-redirect')")
147
+ subject.current_url.should == "http://127.0.0.1:#{port}/replaced-after-redirect"
148
+ end
149
+ end
150
+
151
+ context "css app" do
152
+ before(:all) do
153
+ body = "css"
154
+ @app = lambda do |env|
155
+ [200, {"Content-Type" => "text/css", "Content-Length" => body.length.to_s}, [body]]
156
+ end
157
+ subject.visit("/")
158
+ end
159
+
160
+ it "renders unsupported content types gracefully" do
161
+ subject.body.should =~ /css/
162
+ end
163
+
164
+ it "sets the response headers with respect to the unsupported request" do
165
+ subject.response_headers["Content-Type"].should == "text/css"
166
+ end
167
+ end
168
+
169
+ context "hello app" do
170
+ before(:all) do
171
+ @app = lambda do |env|
172
+ body = <<-HTML
173
+ <html>
174
+ <head>
175
+ <style type="text/css">
176
+ #display_none { display: none }
177
+ </style>
178
+ </head>
179
+ <body>
180
+ <div class='normalize'>Spaces&nbsp;not&nbsp;normalized&nbsp;</div>
181
+ <div id="display_none">
182
+ <div id="invisible">Can't see me</div>
183
+ </div>
184
+ <input type="text" disabled="disabled"/>
185
+ <input id="checktest" type="checkbox" checked="checked"/>
186
+ <script type="text/javascript">
187
+ document.write("<p id='greeting'>he" + "llo</p>");
188
+ </script>
189
+ </body>
190
+ </html>
191
+ HTML
192
+ [200,
193
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
194
+ [body]]
195
+ end
196
+ end
197
+
198
+ it "handles anchor tags" do
199
+ subject.visit("#test")
200
+ subject.find("//*[contains(., 'hello')]").should_not be_empty
201
+ subject.visit("#test")
202
+ subject.find("//*[contains(., 'hello')]").should_not be_empty
203
+ end
204
+
205
+ it "finds content after loading a URL" do
206
+ subject.find("//*[contains(., 'hello')]").should_not be_empty
207
+ end
208
+
209
+ it "has an empty page after reseting" do
210
+ subject.reset!
211
+ subject.find("//*[contains(., 'hello')]").should be_empty
212
+ end
213
+
214
+ it "has a location of 'about:blank' after reseting" do
215
+ subject.reset!
216
+ subject.current_url.should == "about:blank"
217
+ end
218
+
219
+ it "raises an error for an invalid xpath query" do
220
+ expect { subject.find("totally invalid salad") }.
221
+ to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, /xpath/i)
222
+ end
223
+
224
+ it "returns an attribute's value" do
225
+ subject.find("//p").first["id"].should == "greeting"
226
+ end
227
+
228
+ it "parses xpath with quotes" do
229
+ subject.find('//*[contains(., "hello")]').should_not be_empty
230
+ end
231
+
232
+ it "returns a node's text" do
233
+ subject.find("//p").first.text.should == "hello"
234
+ end
235
+
236
+ it "normalizes a node's text" do
237
+ subject.find("//div[contains(@class, 'normalize')]").first.text.should == "Spaces not normalized"
238
+ end
239
+
240
+ it "returns the current URL" do
241
+ port = subject.instance_variable_get("@rack_server").port
242
+ subject.current_url.should == "http://127.0.0.1:#{port}/hello/world?success=true"
243
+ end
244
+
245
+ it "returns the current URL when changed by pushState" do
246
+ port = subject.instance_variable_get("@rack_server").port
247
+ subject.execute_script("window.history.pushState({}, '', '/pushed')")
248
+ subject.current_url.should == "http://127.0.0.1:#{port}/pushed"
249
+ end
250
+
251
+ it "returns the current URL when changed by replaceState" do
252
+ port = subject.instance_variable_get("@rack_server").port
253
+ subject.execute_script("window.history.replaceState({}, '', '/replaced')")
254
+ subject.current_url.should == "http://127.0.0.1:#{port}/replaced"
255
+ end
256
+
257
+ it "does not double-encode URLs" do
258
+ subject.visit("/hello/world?success=%25true")
259
+ subject.current_url.should =~ /success=\%25true/
260
+ end
261
+
262
+ it "visits a page with an anchor" do
263
+ subject.visit("/hello#display_none")
264
+ subject.current_url.should =~ /hello#display_none/
265
+ end
266
+
267
+ it "returns the source code for the page" do
268
+ subject.source.should =~ %r{<html>.*greeting.*}m
269
+ end
270
+
271
+ it "evaluates Javascript and returns a string" do
272
+ result = subject.evaluate_script(%<document.getElementById('greeting').innerText>)
273
+ result.should == "hello"
274
+ end
275
+
276
+ it "evaluates Javascript and returns an array" do
277
+ result = subject.evaluate_script(%<["hello", "world"]>)
278
+ result.should == %w(hello world)
279
+ end
280
+
281
+ it "evaluates Javascript and returns an int" do
282
+ result = subject.evaluate_script(%<123>)
283
+ result.should == 123
284
+ end
285
+
286
+ it "evaluates Javascript and returns a float" do
287
+ result = subject.evaluate_script(%<1.5>)
288
+ result.should == 1.5
289
+ end
290
+
291
+ it "evaluates Javascript and returns null" do
292
+ result = subject.evaluate_script(%<(function () {})()>)
293
+ result.should == nil
294
+ end
295
+
296
+ it "evaluates Javascript and returns an object" do
297
+ result = subject.evaluate_script(%<({ 'one' : 1 })>)
298
+ result.should == { 'one' => 1 }
299
+ end
300
+
301
+ it "evaluates Javascript and returns true" do
302
+ result = subject.evaluate_script(%<true>)
303
+ result.should === true
304
+ end
305
+
306
+ it "evaluates Javascript and returns false" do
307
+ result = subject.evaluate_script(%<false>)
308
+ result.should === false
309
+ end
310
+
311
+ it "evaluates Javascript and returns an escaped string" do
312
+ result = subject.evaluate_script(%<'"'>)
313
+ result.should === "\""
314
+ end
315
+
316
+ it "evaluates Javascript with multiple lines" do
317
+ result = subject.evaluate_script("[1,\n2]")
318
+ result.should == [1, 2]
319
+ end
320
+
321
+ it "executes Javascript" do
322
+ subject.execute_script(%<document.getElementById('greeting').innerHTML = 'yo'>)
323
+ subject.find("//p[contains(., 'yo')]").should_not be_empty
324
+ end
325
+
326
+ it "raises an error for failing Javascript" do
327
+ expect { subject.execute_script(%<invalid salad>) }.
328
+ to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
329
+ end
330
+
331
+ it "doesn't raise an error for Javascript that doesn't return anything" do
332
+ lambda { subject.execute_script(%<(function () { "returns nothing" })()>) }.
333
+ should_not raise_error
334
+ end
335
+
336
+ it "returns a node's tag name" do
337
+ subject.find("//p").first.tag_name.should == "p"
338
+ end
339
+
340
+ it "reads disabled property" do
341
+ subject.find("//input").first.should be_disabled
342
+ end
343
+
344
+ it "reads checked property" do
345
+ subject.find("//input[@id='checktest']").first.should be_checked
346
+ end
347
+
348
+ it "finds visible elements" do
349
+ subject.find("//p").first.should be_visible
350
+ subject.find("//*[@id='invisible']").first.should_not be_visible
351
+ end
352
+ end
353
+
354
+ context "console messages app" do
355
+
356
+ before(:all) do
357
+ @app = lambda do |env|
358
+ body = <<-HTML
359
+ <html>
360
+ <head>
361
+ </head>
362
+ <body>
363
+ <script type="text/javascript">
364
+ console.log("hello");
365
+ console.log("hello again");
366
+ oops
367
+ </script>
368
+ </body>
369
+ </html>
370
+ HTML
371
+ [200,
372
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
373
+ [body]]
374
+ end
375
+ end
376
+
377
+ it "collects messages logged to the console" do
378
+ subject.console_messages.first.should include :source, :message => "hello", :line_number => 6
379
+ subject.console_messages.length.should eq 3
380
+ end
381
+
382
+ it "logs errors to the console" do
383
+ subject.error_messages.length.should eq 1
384
+ end
385
+
386
+ end
387
+
388
+ context "form app" do
389
+ before(:all) do
390
+ @app = lambda do |env|
391
+ body = <<-HTML
392
+ <html><body>
393
+ <form action="/" method="GET">
394
+ <input type="text" name="foo" value="bar"/>
395
+ <input type="text" name="maxlength_foo" value="bar" maxlength="10"/>
396
+ <input type="text" id="disabled_input" disabled="disabled"/>
397
+ <input type="checkbox" name="checkedbox" value="1" checked="checked"/>
398
+ <input type="checkbox" name="uncheckedbox" value="2"/>
399
+ <select name="animal">
400
+ <option id="select-option-monkey">Monkey</option>
401
+ <option id="select-option-capybara" selected="selected">Capybara</option>
402
+ </select>
403
+ <select name="toppings" multiple="multiple">
404
+ <optgroup label="Mediocre Toppings">
405
+ <option selected="selected" id="topping-apple">Apple</option>
406
+ <option selected="selected" id="topping-banana">Banana</option>
407
+ </optgroup>
408
+ <optgroup label="Best Toppings">
409
+ <option selected="selected" id="topping-cherry">Cherry</option>
410
+ </optgroup>
411
+ </select>
412
+ <textarea id="only-textarea">what a wonderful area for text</textarea>
413
+ <input type="radio" id="only-radio" value="1"/>
414
+ <button type="reset">Reset Form</button>
415
+ </form>
416
+ </body></html>
417
+ HTML
418
+ [200,
419
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
420
+ [body]]
421
+ end
422
+ end
423
+
424
+ it "returns a textarea's value" do
425
+ subject.find("//textarea").first.value.should == "what a wonderful area for text"
426
+ end
427
+
428
+ it "returns a text input's value" do
429
+ subject.find("//input").first.value.should == "bar"
430
+ end
431
+
432
+ it "returns a select's value" do
433
+ subject.find("//select").first.value.should == "Capybara"
434
+ end
435
+
436
+ it "sets an input's value" do
437
+ input = subject.find("//input").first
438
+ input.set("newvalue")
439
+ input.value.should == "newvalue"
440
+ end
441
+
442
+ it "sets an input's value greater than the max length" do
443
+ input = subject.find("//input[@name='maxlength_foo']").first
444
+ input.set("allegories (poems)")
445
+ input.value.should == "allegories"
446
+ end
447
+
448
+ it "sets an input's value equal to the max length" do
449
+ input = subject.find("//input[@name='maxlength_foo']").first
450
+ input.set("allegories")
451
+ input.value.should == "allegories"
452
+ end
453
+
454
+ it "sets an input's value less than the max length" do
455
+ input = subject.find("//input[@name='maxlength_foo']").first
456
+ input.set("poems")
457
+ input.value.should == "poems"
458
+ end
459
+
460
+ it "sets an input's nil value" do
461
+ input = subject.find("//input").first
462
+ input.set(nil)
463
+ input.value.should == ""
464
+ end
465
+
466
+ it "sets a select's value" do
467
+ select = subject.find("//select").first
468
+ select.set("Monkey")
469
+ select.value.should == "Monkey"
470
+ end
471
+
472
+ it "sets a textarea's value" do
473
+ textarea = subject.find("//textarea").first
474
+ textarea.set("newvalue")
475
+ textarea.value.should == "newvalue"
476
+ end
477
+
478
+ let(:monkey_option) { subject.find("//option[@id='select-option-monkey']").first }
479
+ let(:capybara_option) { subject.find("//option[@id='select-option-capybara']").first }
480
+ let(:animal_select) { subject.find("//select[@name='animal']").first }
481
+ let(:apple_option) { subject.find("//option[@id='topping-apple']").first }
482
+ let(:banana_option) { subject.find("//option[@id='topping-banana']").first }
483
+ let(:cherry_option) { subject.find("//option[@id='topping-cherry']").first }
484
+ let(:toppings_select) { subject.find("//select[@name='toppings']").first }
485
+ let(:reset_button) { subject.find("//button[@type='reset']").first }
486
+
487
+ context "a select element's selection has been changed" do
488
+ before do
489
+ animal_select.value.should == "Capybara"
490
+ monkey_option.select_option
491
+ end
492
+
493
+ it "returns the new selection" do
494
+ animal_select.value.should == "Monkey"
495
+ end
496
+
497
+ it "does not modify the selected attribute of a new selection" do
498
+ monkey_option['selected'].should be_empty
499
+ end
500
+
501
+ it "returns the old value when a reset button is clicked" do
502
+ reset_button.click
503
+
504
+ animal_select.value.should == "Capybara"
505
+ end
506
+ end
507
+
508
+ context "a multi-select element's option has been unselected" do
509
+ before do
510
+ toppings_select.value.should include("Apple", "Banana", "Cherry")
511
+
512
+ apple_option.unselect_option
513
+ end
514
+
515
+ it "does not return the deselected option" do
516
+ toppings_select.value.should_not include("Apple")
517
+ end
518
+
519
+ it "returns the deselected option when a reset button is clicked" do
520
+ reset_button.click
521
+
522
+ toppings_select.value.should include("Apple", "Banana", "Cherry")
523
+ end
524
+ end
525
+
526
+ it "reselects an option in a multi-select" do
527
+ apple_option.unselect_option
528
+ banana_option.unselect_option
529
+ cherry_option.unselect_option
530
+
531
+ toppings_select.value.should == []
532
+
533
+ apple_option.select_option
534
+ banana_option.select_option
535
+ cherry_option.select_option
536
+
537
+ toppings_select.value.should include("Apple", "Banana", "Cherry")
538
+ end
539
+
540
+ let(:checked_box) { subject.find("//input[@name='checkedbox']").first }
541
+ let(:unchecked_box) { subject.find("//input[@name='uncheckedbox']").first }
542
+
543
+ it "knows a checked box is checked" do
544
+ checked_box['checked'].should be_true
545
+ end
546
+
547
+ it "knows a checked box is checked using checked?" do
548
+ checked_box.should be_checked
549
+ end
550
+
551
+ it "knows an unchecked box is unchecked" do
552
+ unchecked_box['checked'].should_not be_true
553
+ end
554
+
555
+ it "knows an unchecked box is unchecked using checked?" do
556
+ unchecked_box.should_not be_checked
557
+ end
558
+
559
+ it "checks an unchecked box" do
560
+ unchecked_box.set(true)
561
+ unchecked_box.should be_checked
562
+ end
563
+
564
+ it "unchecks a checked box" do
565
+ checked_box.set(false)
566
+ checked_box.should_not be_checked
567
+ end
568
+
569
+ it "leaves a checked box checked" do
570
+ checked_box.set(true)
571
+ checked_box.should be_checked
572
+ end
573
+
574
+ it "leaves an unchecked box unchecked" do
575
+ unchecked_box.set(false)
576
+ unchecked_box.should_not be_checked
577
+ end
578
+
579
+ let(:enabled_input) { subject.find("//input[@name='foo']").first }
580
+ let(:disabled_input) { subject.find("//input[@id='disabled_input']").first }
581
+
582
+ it "knows a disabled input is disabled" do
583
+ disabled_input['disabled'].should be_true
584
+ end
585
+
586
+ it "knows a not disabled input is not disabled" do
587
+ enabled_input['disabled'].should_not be_true
588
+ end
589
+ end
590
+
591
+ context "dom events" do
592
+ before(:all) do
593
+ @app = lambda do |env|
594
+ body = <<-HTML
595
+
596
+ <html><body>
597
+ <a href='#' class='watch'>Link</a>
598
+ <ul id="events"></ul>
599
+ <script type="text/javascript">
600
+ var events = document.getElementById("events");
601
+ var recordEvent = function (event) {
602
+ var element = document.createElement("li");
603
+ element.innerHTML = event.type;
604
+ events.appendChild(element);
605
+ };
606
+
607
+ var elements = document.getElementsByClassName("watch");
608
+ for (var i = 0; i < elements.length; i++) {
609
+ var element = elements[i];
610
+ element.addEventListener("mousedown", recordEvent);
611
+ element.addEventListener("mouseup", recordEvent);
612
+ element.addEventListener("click", recordEvent);
613
+ }
614
+ </script>
615
+ </body></html>
616
+ HTML
617
+ [200,
618
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
619
+ [body]]
620
+ end
621
+ end
622
+
623
+ it "triggers mouse events" do
624
+ subject.find("//a").first.click
625
+ subject.find("//li").map(&:text).should == %w(mousedown mouseup click)
626
+ end
627
+ end
628
+
629
+ context "form events app" do
630
+ before(:all) do
631
+ @app = lambda do |env|
632
+ body = <<-HTML
633
+ <html><body>
634
+ <form action="/" method="GET">
635
+ <input class="watch" type="email"/>
636
+ <input class="watch" type="number"/>
637
+ <input class="watch" type="password"/>
638
+ <input class="watch" type="search"/>
639
+ <input class="watch" type="tel"/>
640
+ <input class="watch" type="text"/>
641
+ <input class="watch" type="url"/>
642
+ <textarea class="watch"></textarea>
643
+ <input class="watch" type="checkbox"/>
644
+ <input class="watch" type="radio"/>
645
+ </form>
646
+ <ul id="events"></ul>
647
+ <script type="text/javascript">
648
+ var events = document.getElementById("events");
649
+ var recordEvent = function (event) {
650
+ var element = document.createElement("li");
651
+ element.innerHTML = event.type;
652
+ events.appendChild(element);
653
+ };
654
+
655
+ var elements = document.getElementsByClassName("watch");
656
+ for (var i = 0; i < elements.length; i++) {
657
+ var element = elements[i];
658
+ element.addEventListener("focus", recordEvent);
659
+ element.addEventListener("keydown", recordEvent);
660
+ element.addEventListener("keypress", recordEvent);
661
+ element.addEventListener("keyup", recordEvent);
662
+ element.addEventListener("input", recordEvent);
663
+ element.addEventListener("change", recordEvent);
664
+ element.addEventListener("blur", recordEvent);
665
+ element.addEventListener("mousedown", recordEvent);
666
+ element.addEventListener("mouseup", recordEvent);
667
+ element.addEventListener("click", recordEvent);
668
+ }
669
+ </script>
670
+ </body></html>
671
+ HTML
672
+ [200,
673
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
674
+ [body]]
675
+ end
676
+ end
677
+
678
+ let(:newtext) { 'newvalue' }
679
+
680
+ let(:keyevents) do
681
+ (%w{focus} +
682
+ newtext.length.times.collect { %w{keydown keypress keyup input} } +
683
+ %w{change blur}).flatten
684
+ end
685
+
686
+ %w(email number password search tel text url).each do | field_type |
687
+ it "triggers text input events on inputs of type #{field_type}" do
688
+ subject.find("//input[@type='#{field_type}']").first.set(newtext)
689
+ subject.find("//li").map(&:text).should == keyevents
690
+ end
691
+ end
692
+
693
+ it "triggers textarea input events" do
694
+ subject.find("//textarea").first.set(newtext)
695
+ subject.find("//li").map(&:text).should == keyevents
696
+ end
697
+
698
+ it "triggers radio input events" do
699
+ subject.find("//input[@type='radio']").first.set(true)
700
+ subject.find("//li").map(&:text).should == %w(mousedown mouseup change click)
701
+ end
702
+
703
+ it "triggers checkbox events" do
704
+ subject.find("//input[@type='checkbox']").first.set(true)
705
+ subject.find("//li").map(&:text).should == %w(mousedown mouseup change click)
706
+ end
707
+ end
708
+
709
+ context "mouse app" do
710
+ before(:all) do
711
+ @app =lambda do |env|
712
+ body = <<-HTML
713
+ <html><body>
714
+ <div id="change">Change me</div>
715
+ <div id="mouseup">Push me</div>
716
+ <div id="mousedown">Release me</div>
717
+ <form action="/" method="GET">
718
+ <select id="change_select" name="change_select">
719
+ <option value="1" id="option-1" selected="selected">one</option>
720
+ <option value="2" id="option-2">two</option>
721
+ </select>
722
+ </form>
723
+ <script type="text/javascript">
724
+ document.getElementById("change_select").
725
+ addEventListener("change", function () {
726
+ this.className = "triggered";
727
+ });
728
+ document.getElementById("change").
729
+ addEventListener("change", function () {
730
+ this.className = "triggered";
731
+ });
732
+ document.getElementById("mouseup").
733
+ addEventListener("mouseup", function () {
734
+ this.className = "triggered";
735
+ });
736
+ document.getElementById("mousedown").
737
+ addEventListener("mousedown", function () {
738
+ this.className = "triggered";
739
+ });
740
+ </script>
741
+ <a href="/next">Next</a>
742
+ </body></html>
743
+ HTML
744
+ [200,
745
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
746
+ [body]]
747
+ end
748
+ end
749
+
750
+ it "clicks an element" do
751
+ subject.find("//a").first.click
752
+ subject.current_url =~ %r{/next$}
753
+ end
754
+
755
+ it "fires a mouse event" do
756
+ subject.find("//*[@id='mouseup']").first.trigger("mouseup")
757
+ subject.find("//*[@class='triggered']").should_not be_empty
758
+ end
759
+
760
+ it "fires a non-mouse event" do
761
+ subject.find("//*[@id='change']").first.trigger("change")
762
+ subject.find("//*[@class='triggered']").should_not be_empty
763
+ end
764
+
765
+ it "fires a change on select" do
766
+ select = subject.find("//select").first
767
+ select.value.should == "1"
768
+ option = subject.find("//option[@id='option-2']").first
769
+ option.select_option
770
+ select.value.should == "2"
771
+ subject.find("//select[@class='triggered']").should_not be_empty
772
+ end
773
+
774
+ it "fires drag events" do
775
+ draggable = subject.find("//*[@id='mousedown']").first
776
+ container = subject.find("//*[@id='mouseup']").first
777
+
778
+ draggable.drag_to(container)
779
+
780
+ subject.find("//*[@class='triggered']").size.should == 1
781
+ end
782
+ end
783
+
784
+ context "nesting app" do
785
+ before(:all) do
786
+ @app = lambda do |env|
787
+ body = <<-HTML
788
+ <html><body>
789
+ <div id="parent">
790
+ <div class="find">Expected</div>
791
+ </div>
792
+ <div class="find">Unexpected</div>
793
+ </body></html>
794
+ HTML
795
+ [200,
796
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
797
+ [body]]
798
+ end
799
+ end
800
+
801
+ it "evaluates nested xpath expressions" do
802
+ parent = subject.find("//*[@id='parent']").first
803
+ parent.find("./*[@class='find']").map(&:text).should == %w(Expected)
804
+ end
805
+ end
806
+
807
+ context "slow app" do
808
+ before(:all) do
809
+ @result = ""
810
+ @app = lambda do |env|
811
+ if env["PATH_INFO"] == "/result"
812
+ sleep(0.5)
813
+ @result << "finished"
814
+ end
815
+ body = %{<html><body><a href="/result">Go</a></body></html>}
816
+ [200,
817
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
818
+ [body]]
819
+ end
820
+ end
821
+
822
+ it "waits for a request to load" do
823
+ subject.find("//a").first.click
824
+ @result.should == "finished"
825
+ end
826
+ end
827
+
828
+ context "error app" do
829
+ before(:all) do
830
+ @app = lambda do |env|
831
+ if env['PATH_INFO'] == "/error"
832
+ [404, {}, []]
833
+ else
834
+ body = <<-HTML
835
+ <html><body>
836
+ <form action="/error"><input type="submit"/></form>
837
+ </body></html>
838
+ HTML
839
+ [200,
840
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
841
+ [body]]
842
+ end
843
+ end
844
+ end
845
+
846
+ it "raises a webkit error for the requested url" do
847
+ expect {
848
+ subject.find("//input").first.click
849
+ wait_for_error_to_complete
850
+ subject.find("//body")
851
+ }.
852
+ to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, %r{/error})
853
+ end
854
+
855
+ def wait_for_error_to_complete
856
+ sleep(0.5)
857
+ end
858
+ end
859
+
860
+ context "slow error app" do
861
+ before(:all) do
862
+ @app = lambda do |env|
863
+ if env['PATH_INFO'] == "/error"
864
+ body = "error"
865
+ sleep(1)
866
+ [304, {}, []]
867
+ else
868
+ body = <<-HTML
869
+ <html><body>
870
+ <form action="/error"><input type="submit"/></form>
871
+ <p>hello</p>
872
+ </body></html>
873
+ HTML
874
+ [200,
875
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
876
+ [body]]
877
+ end
878
+ end
879
+ end
880
+
881
+ it "raises a webkit error and then continues" do
882
+ subject.find("//input").first.click
883
+ expect { subject.find("//p") }.to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
884
+ subject.visit("/")
885
+ subject.find("//p").first.text.should == "hello"
886
+ end
887
+ end
888
+
889
+ context "popup app" do
890
+ before(:all) do
891
+ @app = lambda do |env|
892
+ body = <<-HTML
893
+ <html><body>
894
+ <script type="text/javascript">
895
+ alert("alert");
896
+ confirm("confirm");
897
+ prompt("prompt");
898
+ </script>
899
+ <p>success</p>
900
+ </body></html>
901
+ HTML
902
+ sleep(0.5)
903
+ [200,
904
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
905
+ [body]]
906
+ end
907
+ end
908
+
909
+ it "doesn't crash from alerts" do
910
+ subject.find("//p").first.text.should == "success"
911
+ end
912
+ end
913
+
914
+ context "custom header" do
915
+ before(:all) do
916
+ @app = lambda do |env|
917
+ body = <<-HTML
918
+ <html><body>
919
+ <p id="user-agent">#{env['HTTP_USER_AGENT']}</p>
920
+ <p id="x-capybara-webkit-header">#{env['HTTP_X_CAPYBARA_WEBKIT_HEADER']}</p>
921
+ <p id="accept">#{env['HTTP_ACCEPT']}</p>
922
+ <a href="/">/</a>
923
+ </body></html>
924
+ HTML
925
+ [200,
926
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
927
+ [body]]
928
+ end
929
+ end
930
+
931
+ before do
932
+ subject.header('user-agent', 'capybara-webkit/custom-user-agent')
933
+ subject.header('x-capybara-webkit-header', 'x-capybara-webkit-header')
934
+ subject.header('accept', 'text/html')
935
+ subject.visit('/')
936
+ end
937
+
938
+ it "can set user_agent" do
939
+ subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent'
940
+ subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent'
941
+ end
942
+
943
+ it "keep user_agent in next page" do
944
+ subject.find("//a").first.click
945
+ subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent'
946
+ subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent'
947
+ end
948
+
949
+ it "can set custom header" do
950
+ subject.find('id("x-capybara-webkit-header")').first.text.should == 'x-capybara-webkit-header'
951
+ end
952
+
953
+ it "can set Accept header" do
954
+ subject.find('id("accept")').first.text.should == 'text/html'
955
+ end
956
+
957
+ it "can reset all custom header" do
958
+ subject.reset!
959
+ subject.visit('/')
960
+ subject.find('id("user-agent")').first.text.should_not == 'capybara-webkit/custom-user-agent'
961
+ subject.evaluate_script('navigator.userAgent').should_not == 'capybara-webkit/custom-user-agent'
962
+ subject.find('id("x-capybara-webkit-header")').first.text.should be_empty
963
+ subject.find('id("accept")').first.text.should_not == 'text/html'
964
+ end
965
+ end
966
+
967
+ context "no response app" do
968
+ before(:all) do
969
+ @app = lambda do |env|
970
+ body = <<-HTML
971
+ <html><body>
972
+ <form action="/error"><input type="submit"/></form>
973
+ </body></html>
974
+ HTML
975
+ [200,
976
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
977
+ [body]]
978
+ end
979
+ end
980
+
981
+ it "raises a webkit error for the requested url" do
982
+ make_the_server_go_away
983
+ expect {
984
+ subject.find("//body")
985
+ }.
986
+ to raise_error(Capybara::Driver::Webkit::WebkitNoResponseError, %r{response})
987
+ make_the_server_come_back
988
+ end
989
+
990
+ def make_the_server_come_back
991
+ subject.browser.instance_variable_get(:@connection).unstub!(:gets)
992
+ subject.browser.instance_variable_get(:@connection).unstub!(:puts)
993
+ subject.browser.instance_variable_get(:@connection).unstub!(:print)
994
+ end
995
+
996
+ def make_the_server_go_away
997
+ subject.browser.instance_variable_get(:@connection).stub!(:gets).and_return(nil)
998
+ subject.browser.instance_variable_get(:@connection).stub!(:puts)
999
+ subject.browser.instance_variable_get(:@connection).stub!(:print)
1000
+ end
1001
+ end
1002
+
1003
+ context "custom font app" do
1004
+ before(:all) do
1005
+ @app = lambda do |env|
1006
+ body = <<-HTML
1007
+ <html>
1008
+ <head>
1009
+ <style type="text/css">
1010
+ p { font-family: "Verdana"; }
1011
+ </style>
1012
+ </head>
1013
+ <body>
1014
+ <p id="text">Hello</p>
1015
+ </body>
1016
+ </html>
1017
+ HTML
1018
+ [200,
1019
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1020
+ [body]]
1021
+ end
1022
+ end
1023
+
1024
+ it "ignores custom fonts" do
1025
+ font_family = subject.evaluate_script(<<-SCRIPT)
1026
+ var element = document.getElementById("text");
1027
+ element.ownerDocument.defaultView.getComputedStyle(element, null).getPropertyValue("font-family");
1028
+ SCRIPT
1029
+ font_family.should == "Arial"
1030
+ end
1031
+ end
1032
+
1033
+ context "cookie-based app" do
1034
+ before(:all) do
1035
+ @cookie = 'cookie=abc; domain=127.0.0.1; path=/'
1036
+ @app = lambda do |env|
1037
+ request = ::Rack::Request.new(env)
1038
+
1039
+ body = <<-HTML
1040
+ <html><body>
1041
+ <p id="cookie">#{request.cookies["cookie"] || ""}</p>
1042
+ </body></html>
1043
+ HTML
1044
+ [200,
1045
+ { 'Content-Type' => 'text/html; charset=UTF-8',
1046
+ 'Content-Length' => body.length.to_s,
1047
+ 'Set-Cookie' => @cookie,
1048
+ },
1049
+ [body]]
1050
+ end
1051
+ end
1052
+
1053
+ def echoed_cookie
1054
+ subject.find('id("cookie")').first.text
1055
+ end
1056
+
1057
+ it "remembers the cookie on second visit" do
1058
+ echoed_cookie.should == ""
1059
+ subject.visit "/"
1060
+ echoed_cookie.should == "abc"
1061
+ end
1062
+
1063
+ it "uses a custom cookie" do
1064
+ subject.browser.set_cookie @cookie
1065
+ subject.visit "/"
1066
+ echoed_cookie.should == "abc"
1067
+ end
1068
+
1069
+ it "clears cookies" do
1070
+ subject.browser.clear_cookies
1071
+ subject.visit "/"
1072
+ echoed_cookie.should == ""
1073
+ end
1074
+
1075
+ it "allows enumeration of cookies" do
1076
+ cookies = subject.browser.get_cookies
1077
+
1078
+ cookies.size.should == 1
1079
+
1080
+ cookie = Hash[cookies[0].split(/\s*;\s*/).map { |x| x.split("=", 2) }]
1081
+ cookie["cookie"].should == "abc"
1082
+ cookie["domain"].should include "127.0.0.1"
1083
+ cookie["path"].should == "/"
1084
+ end
1085
+
1086
+ it "allows reading access to cookies using a nice syntax" do
1087
+ subject.cookies["cookie"].should == "abc"
1088
+ end
1089
+ end
1090
+
1091
+ context "with socket debugger" do
1092
+ let(:socket_debugger_class){ Capybara::Driver::Webkit::SocketDebugger }
1093
+ let(:browser_with_debugger){
1094
+ connection = Capybara::Driver::Webkit::Connection.new(:socket_class => socket_debugger_class)
1095
+ Capybara::Driver::Webkit::Browser.new(connection)
1096
+ }
1097
+ let(:driver_with_debugger){ Capybara::Driver::Webkit.new(@app, :browser => browser_with_debugger) }
1098
+
1099
+ before(:all) do
1100
+ @app = lambda do |env|
1101
+ body = <<-HTML
1102
+ <html><body>
1103
+ <div id="parent">
1104
+ <div class="find">Expected</div>
1105
+ </div>
1106
+ <div class="find">Unexpected</div>
1107
+ </body></html>
1108
+ HTML
1109
+ [200,
1110
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1111
+ [body]]
1112
+ end
1113
+ end
1114
+
1115
+ it "prints out sent content" do
1116
+ socket_debugger_class.any_instance.stub(:received){|content| content }
1117
+ sent_content = ['Find', 1, 17, "//*[@id='parent']"]
1118
+ socket_debugger_class.any_instance.should_receive(:sent).exactly(sent_content.size).times
1119
+ driver_with_debugger.find("//*[@id='parent']")
1120
+ end
1121
+
1122
+ it "prints out received content" do
1123
+ socket_debugger_class.any_instance.stub(:sent)
1124
+ socket_debugger_class.any_instance.should_receive(:received).at_least(:once).and_return("ok")
1125
+ driver_with_debugger.find("//*[@id='parent']")
1126
+ end
1127
+ end
1128
+
1129
+ context "remove node app" do
1130
+ before(:all) do
1131
+ @app = lambda do |env|
1132
+ body = <<-HTML
1133
+ <html>
1134
+ <div id="parent">
1135
+ <p id="removeMe">Hello</p>
1136
+ </div>
1137
+ </html>
1138
+ HTML
1139
+ [200,
1140
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1141
+ [body]]
1142
+ end
1143
+ end
1144
+
1145
+ before { set_automatic_reload false }
1146
+ after { set_automatic_reload true }
1147
+
1148
+ def set_automatic_reload(value)
1149
+ if Capybara.respond_to?(:automatic_reload)
1150
+ Capybara.automatic_reload = value
1151
+ end
1152
+ end
1153
+
1154
+ it "allows removed nodes when reloading is disabled" do
1155
+ node = subject.find("//p[@id='removeMe']").first
1156
+ subject.evaluate_script("document.getElementById('parent').innerHTML = 'Magic'")
1157
+ node.text.should == 'Hello'
1158
+ end
1159
+ end
1160
+
1161
+ context "app with a lot of HTML tags" do
1162
+ before(:all) do
1163
+ @app = lambda do |env|
1164
+ body = <<-HTML
1165
+ <html>
1166
+ <head>
1167
+ <title>My eBook</title>
1168
+ <meta class="charset" name="charset" value="utf-8" />
1169
+ <meta class="author" name="author" value="Firstname Lastname" />
1170
+ </head>
1171
+ <body>
1172
+ <div id="toc">
1173
+ <table>
1174
+ <thead id="head">
1175
+ <tr><td class="td1">Chapter</td><td>Page</td></tr>
1176
+ </thead>
1177
+ <tbody>
1178
+ <tr><td>Intro</td><td>1</td></tr>
1179
+ <tr><td>Chapter 1</td><td class="td2">1</td></tr>
1180
+ <tr><td>Chapter 2</td><td>1</td></tr>
1181
+ </tbody>
1182
+ </table>
1183
+ </div>
1184
+
1185
+ <h1 class="h1">My first book</h1>
1186
+ <p class="p1">Written by me</p>
1187
+ <div id="intro" class="intro">
1188
+ <p>Let's try out XPath</p>
1189
+ <p class="p2">in capybara-webkit</p>
1190
+ </div>
1191
+
1192
+ <h2 class="chapter1">Chapter 1</h2>
1193
+ <p>This paragraph is fascinating.</p>
1194
+ <p class="p3">But not as much as this one.</p>
1195
+
1196
+ <h2 class="chapter2">Chapter 2</h2>
1197
+ <p>Let's try if we can select this</p>
1198
+ </body>
1199
+ </html>
1200
+ HTML
1201
+ [200,
1202
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1203
+ [body]]
1204
+ end
1205
+ end
1206
+
1207
+ it "builds up node paths correctly" do
1208
+ cases = {
1209
+ "//*[contains(@class, 'author')]" => "/html/head/meta[2]",
1210
+ "//*[contains(@class, 'td1')]" => "/html/body/div[@id='toc']/table/thead[@id='head']/tr/td[1]",
1211
+ "//*[contains(@class, 'td2')]" => "/html/body/div[@id='toc']/table/tbody/tr[2]/td[2]",
1212
+ "//h1" => "/html/body/h1",
1213
+ "//*[contains(@class, 'chapter2')]" => "/html/body/h2[2]",
1214
+ "//*[contains(@class, 'p1')]" => "/html/body/p[1]",
1215
+ "//*[contains(@class, 'p2')]" => "/html/body/div[@id='intro']/p[2]",
1216
+ "//*[contains(@class, 'p3')]" => "/html/body/p[3]",
1217
+ }
1218
+
1219
+ cases.each do |xpath, path|
1220
+ nodes = subject.find(xpath)
1221
+ nodes.size.should == 1
1222
+ nodes[0].path.should == path
1223
+ end
1224
+ end
1225
+ end
1226
+
1227
+ context "css overflow app" do
1228
+ before(:all) do
1229
+ @app = lambda do |env|
1230
+ body = <<-HTML
1231
+ <html>
1232
+ <head>
1233
+ <style type="text/css">
1234
+ #overflow { overflow: hidden }
1235
+ </style>
1236
+ </head>
1237
+ <body>
1238
+ <div id="overflow">Overflow</div>
1239
+ </body>
1240
+ </html>
1241
+ HTML
1242
+ [200,
1243
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1244
+ [body]]
1245
+ end
1246
+ end
1247
+
1248
+ it "handles overflow hidden" do
1249
+ subject.find("//div[@id='overflow']").first.text.should == "Overflow"
1250
+ end
1251
+ end
1252
+
1253
+ context "javascript redirect app" do
1254
+ before(:all) do
1255
+ @app = lambda do |env|
1256
+ if env['PATH_INFO'] == '/redirect'
1257
+ body = <<-HTML
1258
+ <html>
1259
+ <script type="text/javascript">
1260
+ window.location = "/next";
1261
+ </script>
1262
+ </html>
1263
+ HTML
1264
+ else
1265
+ body = "<html><p>finished</p></html>"
1266
+ end
1267
+ [200,
1268
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1269
+ [body]]
1270
+ end
1271
+ end
1272
+
1273
+ it "loads a page without error" do
1274
+ 10.times do
1275
+ subject.visit("/redirect")
1276
+ subject.find("//p").first.text.should == "finished"
1277
+ end
1278
+ end
1279
+ end
1280
+
1281
+ context "localStorage works" do
1282
+ before(:all) do
1283
+ @app = lambda do |env|
1284
+ body = <<-HTML
1285
+ <html>
1286
+ <body>
1287
+ <span id='output'></span>
1288
+ <script type="text/javascript">
1289
+ if (typeof localStorage !== "undefined") {
1290
+ if (!localStorage.refreshCounter) {
1291
+ localStorage.refreshCounter = 0;
1292
+ }
1293
+ if (localStorage.refreshCounter++ > 0) {
1294
+ document.getElementById("output").innerHTML = "localStorage is enabled";
1295
+ }
1296
+ }
1297
+ </script>
1298
+ </body>
1299
+ </html>
1300
+ HTML
1301
+ [200,
1302
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1303
+ [body]]
1304
+ end
1305
+ end
1306
+
1307
+ it "displays the message on subsequent page loads" do
1308
+ subject.find("//span[contains(.,'localStorage is enabled')]").should be_empty
1309
+ subject.visit "/"
1310
+ subject.find("//span[contains(.,'localStorage is enabled')]").should_not be_empty
1311
+ end
1312
+ end
1313
+
1314
+ context "app with a lot of HTML tags" do
1315
+ before(:all) do
1316
+ @app = lambda do |env|
1317
+ body = <<-HTML
1318
+ <html>
1319
+ <head>
1320
+ <title>My eBook</title>
1321
+ <meta class="charset" name="charset" value="utf-8" />
1322
+ <meta class="author" name="author" value="Firstname Lastname" />
1323
+ </head>
1324
+ <body>
1325
+ <div id="toc">
1326
+ <table>
1327
+ <thead id="head">
1328
+ <tr><td class="td1">Chapter</td><td>Page</td></tr>
1329
+ </thead>
1330
+ <tbody>
1331
+ <tr><td>Intro</td><td>1</td></tr>
1332
+ <tr><td>Chapter 1</td><td class="td2">1</td></tr>
1333
+ <tr><td>Chapter 2</td><td>1</td></tr>
1334
+ </tbody>
1335
+ </table>
1336
+ </div>
1337
+
1338
+ <h1 class="h1">My first book</h1>
1339
+ <p class="p1">Written by me</p>
1340
+ <div id="intro" class="intro">
1341
+ <p>Let's try out XPath</p>
1342
+ <p class="p2">in capybara-webkit</p>
1343
+ </div>
1344
+
1345
+ <h2 class="chapter1">Chapter 1</h2>
1346
+ <p>This paragraph is fascinating.</p>
1347
+ <p class="p3">But not as much as this one.</p>
1348
+
1349
+ <h2 class="chapter2">Chapter 2</h2>
1350
+ <p>Let's try if we can select this</p>
1351
+ </body>
1352
+ </html>
1353
+ HTML
1354
+ [200,
1355
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1356
+ [body]]
1357
+ end
1358
+ end
1359
+
1360
+ it "builds up node paths correctly" do
1361
+ cases = {
1362
+ "//*[contains(@class, 'author')]" => "/html/head/meta[2]",
1363
+ "//*[contains(@class, 'td1')]" => "/html/body/div[@id='toc']/table/thead[@id='head']/tr/td[1]",
1364
+ "//*[contains(@class, 'td2')]" => "/html/body/div[@id='toc']/table/tbody/tr[2]/td[2]",
1365
+ "//h1" => "/html/body/h1",
1366
+ "//*[contains(@class, 'chapter2')]" => "/html/body/h2[2]",
1367
+ "//*[contains(@class, 'p1')]" => "/html/body/p[1]",
1368
+ "//*[contains(@class, 'p2')]" => "/html/body/div[@id='intro']/p[2]",
1369
+ "//*[contains(@class, 'p3')]" => "/html/body/p[3]",
1370
+ }
1371
+
1372
+ cases.each do |xpath, path|
1373
+ nodes = subject.find(xpath)
1374
+ nodes.size.should == 1
1375
+ nodes[0].path.should == path
1376
+ end
1377
+ end
1378
+ end
1379
+
1380
+ context "form app with server-side handler" do
1381
+ before(:all) do
1382
+ @app = lambda do |env|
1383
+ if env["REQUEST_METHOD"] == "POST"
1384
+ body = "<html><body><p>Congrats!</p></body></html>"
1385
+ else
1386
+ body = <<-HTML
1387
+ <html>
1388
+ <head><title>Form</title>
1389
+ <body>
1390
+ <form action="/" method="POST">
1391
+ <input type="hidden" name="abc" value="123" />
1392
+ <input type="submit" value="Submit" />
1393
+ </form>
1394
+ </body>
1395
+ </html>
1396
+ HTML
1397
+ end
1398
+ [200,
1399
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1400
+ [body]]
1401
+ end
1402
+ end
1403
+
1404
+ it "submits a form without clicking" do
1405
+ subject.find("//form")[0].submit
1406
+ subject.body.should include "Congrats"
1407
+ end
1408
+ end
1409
+
1410
+ def key_app_body(event)
1411
+ body = <<-HTML
1412
+ <html>
1413
+ <head><title>Form</title></head>
1414
+ <body>
1415
+ <div id="charcode_value"></div>
1416
+ <div id="keycode_value"></div>
1417
+ <div id="which_value"></div>
1418
+ <input type="text" id="charcode" name="charcode" on#{event}="setcharcode" />
1419
+ <script type="text/javascript">
1420
+ var element = document.getElementById("charcode")
1421
+ element.addEventListener("#{event}", setcharcode);
1422
+ function setcharcode(event) {
1423
+ var element = document.getElementById("charcode_value");
1424
+ element.innerHTML = event.charCode;
1425
+ element = document.getElementById("keycode_value");
1426
+ element.innerHTML = event.keyCode;
1427
+ element = document.getElementById("which_value");
1428
+ element.innerHTML = event.which;
1429
+ }
1430
+ </script>
1431
+ </body>
1432
+ </html>
1433
+ HTML
1434
+ body
1435
+ end
1436
+
1437
+ def charCode_for(character)
1438
+ subject.find("//input")[0].set(character)
1439
+ subject.find("//div[@id='charcode_value']")[0].text
1440
+ end
1441
+
1442
+ def keyCode_for(character)
1443
+ subject.find("//input")[0].set(character)
1444
+ subject.find("//div[@id='keycode_value']")[0].text
1445
+ end
1446
+
1447
+ def which_for(character)
1448
+ subject.find("//input")[0].set(character)
1449
+ subject.find("//div[@id='which_value']")[0].text
1450
+ end
1451
+
1452
+ context "keypress app" do
1453
+ before(:all) do
1454
+ @app = lambda do |env|
1455
+ body = key_app_body("keypress")
1456
+ [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]]
1457
+ end
1458
+ end
1459
+
1460
+ it "returns the charCode for the keypressed" do
1461
+ charCode_for("a").should == "97"
1462
+ charCode_for("A").should == "65"
1463
+ charCode_for("\r").should == "13"
1464
+ charCode_for(",").should == "44"
1465
+ charCode_for("<").should == "60"
1466
+ charCode_for("0").should == "48"
1467
+ end
1468
+
1469
+ it "returns the keyCode for the keypressed" do
1470
+ keyCode_for("a").should == "97"
1471
+ keyCode_for("A").should == "65"
1472
+ keyCode_for("\r").should == "13"
1473
+ keyCode_for(",").should == "44"
1474
+ keyCode_for("<").should == "60"
1475
+ keyCode_for("0").should == "48"
1476
+ end
1477
+
1478
+ it "returns the which for the keypressed" do
1479
+ which_for("a").should == "97"
1480
+ which_for("A").should == "65"
1481
+ which_for("\r").should == "13"
1482
+ which_for(",").should == "44"
1483
+ which_for("<").should == "60"
1484
+ which_for("0").should == "48"
1485
+ end
1486
+ end
1487
+
1488
+ shared_examples "a keyupdown app" do
1489
+ it "returns a 0 charCode for the event" do
1490
+ charCode_for("a").should == "0"
1491
+ charCode_for("A").should == "0"
1492
+ charCode_for("\r").should == "0"
1493
+ charCode_for(",").should == "0"
1494
+ charCode_for("<").should == "0"
1495
+ charCode_for("0").should == "0"
1496
+ end
1497
+
1498
+ it "returns the keyCode for the event" do
1499
+ keyCode_for("a").should == "65"
1500
+ keyCode_for("A").should == "65"
1501
+ keyCode_for("\r").should == "13"
1502
+ keyCode_for(",").should == "188"
1503
+ keyCode_for("<").should == "188"
1504
+ keyCode_for("0").should == "48"
1505
+ end
1506
+
1507
+ it "returns the which for the event" do
1508
+ which_for("a").should == "65"
1509
+ which_for("A").should == "65"
1510
+ which_for("\r").should == "13"
1511
+ which_for(",").should == "188"
1512
+ which_for("<").should == "188"
1513
+ which_for("0").should == "48"
1514
+ end
1515
+ end
1516
+
1517
+ context "keydown app" do
1518
+ before(:all) do
1519
+ @app = lambda do |env|
1520
+ body = key_app_body("keydown")
1521
+ [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]]
1522
+ end
1523
+ end
1524
+ it_behaves_like "a keyupdown app"
1525
+ end
1526
+
1527
+ context "keyup app" do
1528
+ before(:all) do
1529
+ @app = lambda do |env|
1530
+ body = key_app_body("keyup")
1531
+ [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]]
1532
+ end
1533
+ end
1534
+
1535
+ it_behaves_like "a keyupdown app"
1536
+ end
1537
+
1538
+ context "null byte app" do
1539
+ before(:all) do
1540
+ @app = lambda do |env|
1541
+ body = "Hello\0World"
1542
+ [200,
1543
+ { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
1544
+ [body]]
1545
+ end
1546
+ end
1547
+
1548
+ it "should include all the bytes in the source" do
1549
+ subject.source.should == "Hello\0World"
1550
+ end
1551
+ end
1552
+ end