capybara-webkit 0.1.0
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.
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +19 -0
- data/README.md +37 -0
- data/Rakefile +76 -0
- data/capybara-webkit.gemspec +15 -0
- data/extconf.rb +2 -0
- data/lib/capybara-webkit.rb +7 -0
- data/lib/capybara/driver/webkit.rb +84 -0
- data/lib/capybara/driver/webkit/browser.rb +86 -0
- data/lib/capybara/driver/webkit/node.rb +87 -0
- data/spec/driver_spec.rb +465 -0
- data/spec/integration/driver_spec.rb +21 -0
- data/spec/integration/session_spec.rb +12 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/socket_debugger.rb +42 -0
- data/src/Command.cpp +15 -0
- data/src/Command.h +28 -0
- data/src/Connection.cpp +130 -0
- data/src/Connection.h +36 -0
- data/src/Evaluate.cpp +84 -0
- data/src/Evaluate.h +22 -0
- data/src/Execute.cpp +17 -0
- data/src/Execute.h +12 -0
- data/src/Find.cpp +20 -0
- data/src/Find.h +13 -0
- data/src/JavascriptInvocation.cpp +14 -0
- data/src/JavascriptInvocation.h +19 -0
- data/src/Node.cpp +14 -0
- data/src/Node.h +13 -0
- data/src/Reset.cpp +16 -0
- data/src/Reset.h +12 -0
- data/src/Server.cpp +21 -0
- data/src/Server.h +20 -0
- data/src/Source.cpp +14 -0
- data/src/Source.h +12 -0
- data/src/Url.cpp +14 -0
- data/src/Url.h +12 -0
- data/src/Visit.cpp +20 -0
- data/src/Visit.h +16 -0
- data/src/WebPage.cpp +81 -0
- data/src/WebPage.h +29 -0
- data/src/capybara.js +98 -0
- data/src/find_command.h +13 -0
- data/src/main.cpp +20 -0
- data/src/webkit_server.pro +9 -0
- data/src/webkit_server.qrc +5 -0
- data/templates/Command.cpp +10 -0
- data/templates/Command.h +12 -0
- data/webkit_server.pro +4 -0
- metadata +140 -0
data/spec/driver_spec.rb
ADDED
@@ -0,0 +1,465 @@
|
|
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 "hello app" do
|
10
|
+
before(:all) do
|
11
|
+
@app = lambda do |env|
|
12
|
+
body = <<-HTML
|
13
|
+
<html>
|
14
|
+
<head>
|
15
|
+
<style type="text/css">
|
16
|
+
#display_none { display: none }
|
17
|
+
</style>
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div id="display_none">
|
21
|
+
<div id="invisible">Can't see me</div>
|
22
|
+
</div>
|
23
|
+
<script type="text/javascript">
|
24
|
+
document.write("<p id='greeting'>he" + "llo</p>");
|
25
|
+
</script>
|
26
|
+
</body>
|
27
|
+
</html>
|
28
|
+
HTML
|
29
|
+
[200,
|
30
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
31
|
+
[body]]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "finds content after loading a URL" do
|
36
|
+
subject.find("//*[contains(., 'hello')]").should_not be_empty
|
37
|
+
end
|
38
|
+
|
39
|
+
it "has an empty page after reseting" do
|
40
|
+
subject.reset!
|
41
|
+
subject.find("//*[contains(., 'hello')]").should be_empty
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises an error for an invalid xpath query" do
|
45
|
+
expect { subject.find("totally invalid salad") }.
|
46
|
+
to raise_error(Capybara::Driver::Webkit::WebkitError, /xpath/i)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns an attribute's value" do
|
50
|
+
subject.find("//p").first["id"].should == "greeting"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "parses xpath with quotes" do
|
54
|
+
subject.find('//*[contains(., "hello")]').should_not be_empty
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns a node's text" do
|
58
|
+
subject.find("//p").first.text.should == "hello"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns the current URL" do
|
62
|
+
port = subject.instance_variable_get("@rack_server").port
|
63
|
+
subject.current_url.should == "http://127.0.0.1:#{port}/hello/world?success=true"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns the source code for the page" do
|
67
|
+
subject.source.should =~ %r{<html>.*greeting.*}m
|
68
|
+
end
|
69
|
+
|
70
|
+
it "aliases body as source" do
|
71
|
+
subject.body.should == subject.source
|
72
|
+
end
|
73
|
+
|
74
|
+
it "evaluates Javascript and returns a string" do
|
75
|
+
result = subject.evaluate_script(%<document.getElementById('greeting').innerText>)
|
76
|
+
result.should == "hello"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "evaluates Javascript and returns an array" do
|
80
|
+
result = subject.evaluate_script(%<["hello", "world"]>)
|
81
|
+
result.should == %w(hello world)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "evaluates Javascript and returns an int" do
|
85
|
+
result = subject.evaluate_script(%<123>)
|
86
|
+
result.should == 123
|
87
|
+
end
|
88
|
+
|
89
|
+
it "evaluates Javascript and returns a float" do
|
90
|
+
result = subject.evaluate_script(%<1.5>)
|
91
|
+
result.should == 1.5
|
92
|
+
end
|
93
|
+
|
94
|
+
it "evaluates Javascript and returns null" do
|
95
|
+
result = subject.evaluate_script(%<(function () {})()>)
|
96
|
+
result.should == nil
|
97
|
+
end
|
98
|
+
|
99
|
+
it "evaluates Javascript and returns an object" do
|
100
|
+
result = subject.evaluate_script(%<({ 'one' : 1 })>)
|
101
|
+
result.should == { 'one' => 1 }
|
102
|
+
end
|
103
|
+
|
104
|
+
it "evaluates Javascript and returns true" do
|
105
|
+
result = subject.evaluate_script(%<true>)
|
106
|
+
result.should === true
|
107
|
+
end
|
108
|
+
|
109
|
+
it "evaluates Javascript and returns false" do
|
110
|
+
result = subject.evaluate_script(%<false>)
|
111
|
+
result.should === false
|
112
|
+
end
|
113
|
+
|
114
|
+
it "evaluates Javascript and returns an escaped string" do
|
115
|
+
result = subject.evaluate_script(%<'"'>)
|
116
|
+
result.should === "\""
|
117
|
+
end
|
118
|
+
|
119
|
+
it "evaluates Javascript with multiple lines" do
|
120
|
+
result = subject.evaluate_script("[1,\n2]")
|
121
|
+
result.should == [1, 2]
|
122
|
+
end
|
123
|
+
|
124
|
+
it "executes Javascript" do
|
125
|
+
subject.execute_script(%<document.getElementById('greeting').innerHTML = 'yo'>)
|
126
|
+
subject.find("//p[contains(., 'yo')]").should_not be_empty
|
127
|
+
end
|
128
|
+
|
129
|
+
it "raises an error for failing Javascript" do
|
130
|
+
expect { subject.execute_script(%<invalid salad>) }.
|
131
|
+
to raise_error(Capybara::Driver::Webkit::WebkitError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "returns a node's tag name" do
|
135
|
+
subject.find("//p").first.tag_name.should == "p"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "finds visible elements" do
|
139
|
+
subject.find("//p").first.should be_visible
|
140
|
+
subject.find("//*[@id='invisible']").first.should_not be_visible
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "form app" do
|
145
|
+
before(:all) do
|
146
|
+
@app = lambda do |env|
|
147
|
+
body = <<-HTML
|
148
|
+
<html><body>
|
149
|
+
<form action="/" method="GET">
|
150
|
+
<input type="text" name="foo" value="bar"/>
|
151
|
+
<input type="checkbox" name="checkedbox" value="1" checked="checked"/>
|
152
|
+
<input type="checkbox" name="uncheckedbox" value="2"/>
|
153
|
+
<select name="animal">
|
154
|
+
<option id="select-option-monkey">Monkey</option>
|
155
|
+
<option id="select-option-capybara" selected="selected">Capybara</option>
|
156
|
+
</select>
|
157
|
+
<select name="toppings" multiple="multiple">
|
158
|
+
<optgroup label="Mediocre Toppings">
|
159
|
+
<option selected="selected" id="topping-apple">Apple</option>
|
160
|
+
<option selected="selected" id="topping-banana">Banana</option>
|
161
|
+
</optgroup>
|
162
|
+
<optgroup label="Best Toppings">
|
163
|
+
<option selected="selected" id="topping-cherry">Cherry</option>
|
164
|
+
</optgroup>
|
165
|
+
</select>
|
166
|
+
<textarea id="only-textarea">what a wonderful area for text</textarea>
|
167
|
+
<input type="radio" id="only-radio" value="1"/>
|
168
|
+
</form>
|
169
|
+
</body></html>
|
170
|
+
HTML
|
171
|
+
[200,
|
172
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
173
|
+
[body]]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
it "returns a textarea's value" do
|
178
|
+
subject.find("//textarea").first.value.should == "what a wonderful area for text"
|
179
|
+
end
|
180
|
+
|
181
|
+
it "returns a text input's value" do
|
182
|
+
subject.find("//input").first.value.should == "bar"
|
183
|
+
end
|
184
|
+
|
185
|
+
it "returns a select's value" do
|
186
|
+
subject.find("//select").first.value.should == "Capybara"
|
187
|
+
end
|
188
|
+
|
189
|
+
it "sets an input's value" do
|
190
|
+
input = subject.find("//input").first
|
191
|
+
input.set("newvalue")
|
192
|
+
input.value.should == "newvalue"
|
193
|
+
end
|
194
|
+
|
195
|
+
it "sets a select's value" do
|
196
|
+
select = subject.find("//select").first
|
197
|
+
select.set("Monkey")
|
198
|
+
select.value.should == "Monkey"
|
199
|
+
end
|
200
|
+
|
201
|
+
it "sets a textarea's value" do
|
202
|
+
textarea = subject.find("//textarea").first
|
203
|
+
textarea.set("newvalue")
|
204
|
+
textarea.value.should == "newvalue"
|
205
|
+
end
|
206
|
+
|
207
|
+
let(:monkey_option) { subject.find("//option[@id='select-option-monkey']").first }
|
208
|
+
let(:capybara_option) { subject.find("//option[@id='select-option-capybara']").first }
|
209
|
+
let(:animal_select) { subject.find("//select[@name='animal']").first }
|
210
|
+
let(:apple_option) { subject.find("//option[@id='topping-apple']").first }
|
211
|
+
let(:banana_option) { subject.find("//option[@id='topping-banana']").first }
|
212
|
+
let(:cherry_option) { subject.find("//option[@id='topping-cherry']").first }
|
213
|
+
let(:toppings_select) { subject.find("//select[@name='toppings']").first }
|
214
|
+
|
215
|
+
it "selects an option" do
|
216
|
+
animal_select.value.should == "Capybara"
|
217
|
+
monkey_option.select_option
|
218
|
+
animal_select.value.should == "Monkey"
|
219
|
+
end
|
220
|
+
|
221
|
+
it "unselects an option in a multi-select" do
|
222
|
+
toppings_select.value.should include("Apple", "Banana", "Cherry")
|
223
|
+
|
224
|
+
apple_option.unselect_option
|
225
|
+
toppings_select.value.should_not include("Apple")
|
226
|
+
end
|
227
|
+
|
228
|
+
it "reselects an option in a multi-select" do
|
229
|
+
apple_option.unselect_option
|
230
|
+
banana_option.unselect_option
|
231
|
+
cherry_option.unselect_option
|
232
|
+
|
233
|
+
toppings_select.value.should == []
|
234
|
+
|
235
|
+
apple_option.select_option
|
236
|
+
banana_option.select_option
|
237
|
+
cherry_option.select_option
|
238
|
+
|
239
|
+
toppings_select.value.should include("Apple", "Banana", "Cherry")
|
240
|
+
end
|
241
|
+
|
242
|
+
let(:checked_box) { subject.find("//input[@name='checkedbox']").first }
|
243
|
+
let(:unchecked_box) { subject.find("//input[@name='uncheckedbox']").first }
|
244
|
+
|
245
|
+
it "knows a checked box is checked" do
|
246
|
+
checked_box['checked'].should be_true
|
247
|
+
end
|
248
|
+
|
249
|
+
it "knows an unchecked box is unchecked" do
|
250
|
+
unchecked_box['checked'].should_not be_true
|
251
|
+
end
|
252
|
+
|
253
|
+
it "checks an unchecked box" do
|
254
|
+
unchecked_box.set(true)
|
255
|
+
unchecked_box['checked'].should be_true
|
256
|
+
end
|
257
|
+
|
258
|
+
it "unchecks a checked box" do
|
259
|
+
checked_box.set(false)
|
260
|
+
checked_box['checked'].should_not be_true
|
261
|
+
end
|
262
|
+
|
263
|
+
it "leaves a checked box checked" do
|
264
|
+
checked_box.set(true)
|
265
|
+
checked_box['checked'].should be_true
|
266
|
+
end
|
267
|
+
|
268
|
+
it "leaves an unchecked box unchecked" do
|
269
|
+
unchecked_box.set(false)
|
270
|
+
unchecked_box['checked'].should_not be_true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context "form events app" do
|
275
|
+
before(:all) do
|
276
|
+
@app = lambda do |env|
|
277
|
+
body = <<-HTML
|
278
|
+
<html><body>
|
279
|
+
<form action="/" method="GET">
|
280
|
+
<input class="watch" type="text"/>
|
281
|
+
<input class="watch" type="password"/>
|
282
|
+
<textarea class="watch"></textarea>
|
283
|
+
<input class="watch" type="checkbox"/>
|
284
|
+
<input class="watch" type="radio"/>
|
285
|
+
</form>
|
286
|
+
<ul id="events"></ul>
|
287
|
+
<script type="text/javascript">
|
288
|
+
var events = document.getElementById("events");
|
289
|
+
var recordEvent = function (event) {
|
290
|
+
var element = document.createElement("li");
|
291
|
+
element.innerHTML = event.type;
|
292
|
+
events.appendChild(element);
|
293
|
+
};
|
294
|
+
|
295
|
+
var elements = document.getElementsByClassName("watch");
|
296
|
+
for (var i = 0; i < elements.length; i++) {
|
297
|
+
var element = elements[i];
|
298
|
+
element.addEventListener("focus", recordEvent);
|
299
|
+
element.addEventListener("keydown", recordEvent);
|
300
|
+
element.addEventListener("keyup", recordEvent);
|
301
|
+
element.addEventListener("change", recordEvent);
|
302
|
+
element.addEventListener("blur", recordEvent);
|
303
|
+
element.addEventListener("click", recordEvent);
|
304
|
+
}
|
305
|
+
</script>
|
306
|
+
</body></html>
|
307
|
+
HTML
|
308
|
+
[200,
|
309
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
310
|
+
[body]]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
it "triggers text input events" do
|
315
|
+
subject.find("//input[@type='text']").first.set("newvalue")
|
316
|
+
subject.find("//li").map(&:text).should == %w(focus keydown keyup change blur)
|
317
|
+
end
|
318
|
+
|
319
|
+
it "triggers textarea input events" do
|
320
|
+
subject.find("//textarea").first.set("newvalue")
|
321
|
+
subject.find("//li").map(&:text).should == %w(focus keydown keyup change blur)
|
322
|
+
end
|
323
|
+
|
324
|
+
it "triggers password input events" do
|
325
|
+
subject.find("//input[@type='password']").first.set("newvalue")
|
326
|
+
subject.find("//li").map(&:text).should == %w(focus keydown keyup change blur)
|
327
|
+
end
|
328
|
+
|
329
|
+
it "triggers radio input events" do
|
330
|
+
subject.find("//input[@type='radio']").first.set(true)
|
331
|
+
subject.find("//li").map(&:text).should == %w(click)
|
332
|
+
end
|
333
|
+
|
334
|
+
it "triggers checkbox events" do
|
335
|
+
subject.find("//input[@type='checkbox']").first.set(true)
|
336
|
+
subject.find("//li").map(&:text).should == %w(click)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
context "mouse app" do
|
341
|
+
before(:all) do
|
342
|
+
@app =lambda do |env|
|
343
|
+
body = <<-HTML
|
344
|
+
<html><body>
|
345
|
+
<div id="change">Change me</div>
|
346
|
+
<div id="mouseup">Push me</div>
|
347
|
+
<div id="mousedown">Release me</div>
|
348
|
+
<script type="text/javascript">
|
349
|
+
document.getElementById("change").
|
350
|
+
addEventListener("change", function () {
|
351
|
+
this.className = "triggered";
|
352
|
+
});
|
353
|
+
document.getElementById("mouseup").
|
354
|
+
addEventListener("mouseup", function () {
|
355
|
+
this.className = "triggered";
|
356
|
+
});
|
357
|
+
document.getElementById("mousedown").
|
358
|
+
addEventListener("mousedown", function () {
|
359
|
+
this.className = "triggered";
|
360
|
+
});
|
361
|
+
</script>
|
362
|
+
<a href="/next">Next</a>
|
363
|
+
</body></html>
|
364
|
+
HTML
|
365
|
+
[200,
|
366
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
367
|
+
[body]]
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
it "clicks an element" do
|
372
|
+
subject.find("//a").first.click
|
373
|
+
subject.current_url =~ %r{/next$}
|
374
|
+
end
|
375
|
+
|
376
|
+
it "fires a mouse event" do
|
377
|
+
subject.find("//*[@id='mouseup']").first.trigger("mouseup")
|
378
|
+
subject.find("//*[@class='triggered']").should_not be_empty
|
379
|
+
end
|
380
|
+
|
381
|
+
it "fires a non-mouse event" do
|
382
|
+
subject.find("//*[@id='change']").first.trigger("change")
|
383
|
+
subject.find("//*[@class='triggered']").should_not be_empty
|
384
|
+
end
|
385
|
+
|
386
|
+
it "fires drag events" do
|
387
|
+
draggable = subject.find("//*[@id='mousedown']").first
|
388
|
+
container = subject.find("//*[@id='mouseup']").first
|
389
|
+
|
390
|
+
draggable.drag_to(container)
|
391
|
+
|
392
|
+
subject.find("//*[@class='triggered']").size.should == 2
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
context "nesting app" do
|
397
|
+
before(:all) do
|
398
|
+
@app = lambda do |env|
|
399
|
+
body = <<-HTML
|
400
|
+
<html><body>
|
401
|
+
<div id="parent">
|
402
|
+
<div class="find">Expected</div>
|
403
|
+
</div>
|
404
|
+
<div class="find">Unexpected</div>
|
405
|
+
</body></html>
|
406
|
+
HTML
|
407
|
+
[200,
|
408
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
409
|
+
[body]]
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
it "evaluates nested xpath expressions" do
|
414
|
+
parent = subject.find("//*[@id='parent']").first
|
415
|
+
parent.find("./*[@class='find']").map(&:text).should == %w(Expected)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context "slow app" do
|
420
|
+
before(:all) do
|
421
|
+
@app = lambda do |env|
|
422
|
+
body = <<-HTML
|
423
|
+
<html><body>
|
424
|
+
<form action="/next"><input type="submit"/></form>
|
425
|
+
<p>#{env['PATH_INFO']}</p>
|
426
|
+
</body></html>
|
427
|
+
HTML
|
428
|
+
sleep(0.5)
|
429
|
+
[200,
|
430
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
431
|
+
[body]]
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
it "waits for a request to load" do
|
436
|
+
subject.find("//input").first.click
|
437
|
+
subject.find("//p").first.text.should == "/next"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
context "popup app" do
|
442
|
+
before(:all) do
|
443
|
+
@app = lambda do |env|
|
444
|
+
body = <<-HTML
|
445
|
+
<html><body>
|
446
|
+
<script type="text/javascript">
|
447
|
+
alert("alert");
|
448
|
+
confirm("confirm");
|
449
|
+
prompt("prompt");
|
450
|
+
</script>
|
451
|
+
<p>success</p>
|
452
|
+
</body></html>
|
453
|
+
HTML
|
454
|
+
sleep(0.5)
|
455
|
+
[200,
|
456
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
457
|
+
[body]]
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
it "doesn't crash from alerts" do
|
462
|
+
subject.find("//p").first.text.should == "success"
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|