imseng-capybara-webkit 0.12.1

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.
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