capybara-ng 0.0.3
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +2 -0
- data/capybara-ng.gemspec +25 -0
- data/lib/angular/client_script.rb +669 -0
- data/lib/angular/driver.rb +354 -0
- data/lib/angular/dsl.rb +143 -0
- data/lib/angular/element_helper.rb +896 -0
- data/lib/angular/locator.rb +347 -0
- data/lib/angular/setup.rb +79 -0
- data/lib/angular/version.rb +3 -0
- data/lib/angular/waiter.rb +58 -0
- data/lib/angular.rb +11 -0
- data/lib/capybara-ng.rb +1 -0
- data/test/Gemfile +3 -0
- data/test/test.rb +6 -0
- metadata +108 -0
@@ -0,0 +1,354 @@
|
|
1
|
+
module Angular
|
2
|
+
class Driver
|
3
|
+
include Capybara::DSL
|
4
|
+
|
5
|
+
DEFER_LABEL = 'NG_DEFER_BOOTSTRAP!';
|
6
|
+
|
7
|
+
WEB_ELEMENT_FUNCTIONS = [
|
8
|
+
'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText',
|
9
|
+
'getSize', 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear',
|
10
|
+
'isDisplayed', 'getOuterHtml', 'getInnerHtml', 'getId'
|
11
|
+
]
|
12
|
+
|
13
|
+
DEFAULT_RESET_URL = 'data:text/html,<html></html>'
|
14
|
+
DEFAULT_GET_PAGE_TIMEOUT = 10000
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(opt)
|
18
|
+
@element = ElementHelper.new(self)
|
19
|
+
|
20
|
+
# All get methods will be resolved against this base URL. Relative URLs are =
|
21
|
+
# resolved the way anchor tags resolve.
|
22
|
+
#
|
23
|
+
# @type {string}
|
24
|
+
@baseUrl = opt[:base_url] || '';
|
25
|
+
|
26
|
+
#
|
27
|
+
# The css selector for an element on which to find Angular. This is usually
|
28
|
+
# 'body' but if your ng-app is on a subsection of the page it may be
|
29
|
+
# a subelement.
|
30
|
+
#
|
31
|
+
# @type {string}
|
32
|
+
@rootEl = opt[:root_element] || 'body';
|
33
|
+
|
34
|
+
#
|
35
|
+
# If true, Protractor will not attempt to synchronize with the page before
|
36
|
+
# performing actions. This can be harmful because Protractor will not wait
|
37
|
+
# until $timeouts and $http calls have been processed, which can cause
|
38
|
+
# tests to become flaky. This should be used only when necessary, such as
|
39
|
+
# when a page continuously polls an API using $timeout.
|
40
|
+
#
|
41
|
+
# @type {boolean}
|
42
|
+
@ignoreSynchronization = false;
|
43
|
+
|
44
|
+
#
|
45
|
+
# Timeout in milliseconds to wait for pages to load when calling `get`.
|
46
|
+
#
|
47
|
+
# @type {number}
|
48
|
+
@getPageTimeout = DEFAULT_GET_PAGE_TIMEOUT;
|
49
|
+
|
50
|
+
#
|
51
|
+
# An object that holds custom test parameters.
|
52
|
+
#
|
53
|
+
# @type {Object}
|
54
|
+
@params = {};
|
55
|
+
|
56
|
+
#
|
57
|
+
# The reset URL to use between page loads.
|
58
|
+
#
|
59
|
+
# @type {string}
|
60
|
+
@resetUrl = DEFAULT_RESET_URL;
|
61
|
+
|
62
|
+
#
|
63
|
+
# Information about mock modules that will be installed during every
|
64
|
+
# get().
|
65
|
+
#
|
66
|
+
# @type {Array<{name: string, script: function|string, args: Array.<string>}>}
|
67
|
+
@mockModules = [];
|
68
|
+
|
69
|
+
@addBaseMockModules_();
|
70
|
+
|
71
|
+
@functions = ClientScript.functions
|
72
|
+
end
|
73
|
+
|
74
|
+
# @see http://artsy.github.io/blog/2012/02/03/reliably-testing-asynchronous-ui-w-slash-rspec-and-capybara/
|
75
|
+
def wait_for_dom(timeout = Capybara.default_wait_time)
|
76
|
+
uuid = SecureRandom.uuid
|
77
|
+
page.find("body")
|
78
|
+
page.evaluate_script <<-EOS
|
79
|
+
_.defer(function() {
|
80
|
+
$('body').append("<div id='#{uuid}'></div>");
|
81
|
+
});
|
82
|
+
EOS
|
83
|
+
page.find("##{uuid}")
|
84
|
+
end
|
85
|
+
|
86
|
+
def setup
|
87
|
+
<<-FN
|
88
|
+
// These functions should delegate to the webdriver instance, but should
|
89
|
+
// wait for Angular to sync up before performing the action. This does not
|
90
|
+
// include functions which are overridden by protractor below.
|
91
|
+
var methodsToSync = ['getCurrentUrl', 'getPageSource', 'getTitle'];
|
92
|
+
|
93
|
+
// Mix all other driver functionality into Protractor.
|
94
|
+
for (var method in webdriverInstance) {
|
95
|
+
if(!this[method] && typeof webdriverInstance[method] == 'function') {
|
96
|
+
if (methodsToSync.indexOf(method) !== -1) {
|
97
|
+
mixin(this, webdriverInstance, method, this.waitForAngular.bind(this));
|
98
|
+
} else {
|
99
|
+
mixin(this, webdriverInstance, method);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
FN
|
104
|
+
end
|
105
|
+
|
106
|
+
def page
|
107
|
+
Capybara.current_session
|
108
|
+
end
|
109
|
+
|
110
|
+
def wait_for_angular
|
111
|
+
return if @ignoreSynchronization
|
112
|
+
result = page.evaluate_script(functions[:waitForAngular])
|
113
|
+
|
114
|
+
<<-FN
|
115
|
+
Protractor.prototype.waitForAngular = function() {
|
116
|
+
if (this.ignoreSynchronization) {
|
117
|
+
return webdriver.promise.fulfilled();
|
118
|
+
}
|
119
|
+
return this.driver.executeAsyncScript(
|
120
|
+
clientSideScripts.waitForAngular, this.rootEl).then(function(browserErr) {
|
121
|
+
if (browserErr) {
|
122
|
+
throw 'Error while waiting for Protractor to ' +
|
123
|
+
'sync with the page: ' + JSON.stringify(browserErr);
|
124
|
+
}
|
125
|
+
}).then(null, function(err) {
|
126
|
+
var timeout;
|
127
|
+
if (/asynchronous script timeout/.test(err.message)) {
|
128
|
+
// Timeout on Chrome
|
129
|
+
timeout = /-?[\d\.]*\ seconds/.exec(err.message);
|
130
|
+
} else if (/Timed out waiting for async script/.test(err.message)) {
|
131
|
+
// Timeout on Firefox
|
132
|
+
timeout = /-?[\d\.]*ms/.exec(err.message);
|
133
|
+
} else if (/Timed out waiting for an asynchronous script/.test(err.message)) {
|
134
|
+
// Timeout on Safari
|
135
|
+
timeout = /-?[\d\.]*\ ms/.exec(err.message);
|
136
|
+
}
|
137
|
+
if (timeout) {
|
138
|
+
throw 'Timed out waiting for Protractor to synchronize with ' +
|
139
|
+
'the page after ' + timeout + '. Please see ' +
|
140
|
+
'https://github.com/angular/protractor/blob/master/docs/faq.md';
|
141
|
+
} else {
|
142
|
+
throw err;
|
143
|
+
}
|
144
|
+
});
|
145
|
+
};
|
146
|
+
FN
|
147
|
+
end
|
148
|
+
|
149
|
+
def findElement
|
150
|
+
<<-FN
|
151
|
+
/**
|
152
|
+
* Waits for Angular to finish rendering before searching for elements.
|
153
|
+
* @see webdriver.WebDriver.findElement
|
154
|
+
* @return {!webdriver.WebElement}
|
155
|
+
*/
|
156
|
+
function(locator) {
|
157
|
+
return this.element(locator).getWebElement();
|
158
|
+
};
|
159
|
+
FN
|
160
|
+
end
|
161
|
+
|
162
|
+
def findElements
|
163
|
+
<<-FN
|
164
|
+
/**
|
165
|
+
* Waits for Angular to finish rendering before searching for elements.
|
166
|
+
* @see webdriver.WebDriver.findElements
|
167
|
+
* @return {!webdriver.promise.Promise} A promise that will be resolved to an
|
168
|
+
* array of the located {@link webdriver.WebElement}s.
|
169
|
+
*/
|
170
|
+
function(locator) {
|
171
|
+
return this.element.all(locator).getWebElements();
|
172
|
+
};
|
173
|
+
FN
|
174
|
+
end
|
175
|
+
|
176
|
+
def isElementPresent
|
177
|
+
<<-FN
|
178
|
+
/**
|
179
|
+
* Tests if an element is present on the page.
|
180
|
+
* @see webdriver.WebDriver.isElementPresent
|
181
|
+
* @return {!webdriver.promise.Promise} A promise that will resolve to whether
|
182
|
+
* the element is present on the page.
|
183
|
+
*/
|
184
|
+
function(locatorOrElement) {
|
185
|
+
var element = (locatorOrElement instanceof webdriver.promise.Promise) ?
|
186
|
+
locatorOrElement : this.element(locatorOrElement);
|
187
|
+
return element.isPresent();
|
188
|
+
};
|
189
|
+
FN
|
190
|
+
end
|
191
|
+
|
192
|
+
def get
|
193
|
+
<<-FN
|
194
|
+
/**
|
195
|
+
* See webdriver.WebDriver.get
|
196
|
+
*
|
197
|
+
* Navigate to the given destination and loads mock modules before
|
198
|
+
* Angular. Assumes that the page being loaded uses Angular.
|
199
|
+
* If you need to access a page which does not have Angular on load, use
|
200
|
+
* the wrapped webdriver directly.
|
201
|
+
*
|
202
|
+
* @param {string} destination Destination URL.
|
203
|
+
* @param {number=} opt_timeout Number of milliseconds to wait for Angular to
|
204
|
+
* start.
|
205
|
+
*/
|
206
|
+
function(destination, opt_timeout) {
|
207
|
+
var timeout = opt_timeout ? opt_timeout : this.getPageTimeout;
|
208
|
+
var self = this;
|
209
|
+
|
210
|
+
destination = this.baseUrl.indexOf('file://') === 0 ?
|
211
|
+
this.baseUrl + destination : url.resolve(this.baseUrl, destination);
|
212
|
+
|
213
|
+
if (this.ignoreSynchronization) {
|
214
|
+
return this.driver.get(destination);
|
215
|
+
}
|
216
|
+
|
217
|
+
this.driver.get(this.resetUrl);
|
218
|
+
this.driver.executeScript(
|
219
|
+
'window.name = "' + DEFER_LABEL + '" + window.name;' +
|
220
|
+
'window.location.replace("' + destination + '");');
|
221
|
+
|
222
|
+
// We need to make sure the new url has loaded before
|
223
|
+
// we try to execute any asynchronous scripts.
|
224
|
+
this.driver.wait(function() {
|
225
|
+
return self.driver.executeScript('return window.location.href;').
|
226
|
+
then(function(url) {
|
227
|
+
return url !== self.resetUrl;
|
228
|
+
}, function(err) {
|
229
|
+
if (err.code == 13) {
|
230
|
+
// Ignore the error, and continue trying. This is because IE
|
231
|
+
// driver sometimes (~1%) will throw an unknown error from this
|
232
|
+
// execution. See https://github.com/angular/protractor/issues/841
|
233
|
+
// This shouldn't mask errors because it will fail with the timeout
|
234
|
+
// anyway.
|
235
|
+
return false;
|
236
|
+
} else {
|
237
|
+
throw err;
|
238
|
+
}
|
239
|
+
});
|
240
|
+
}, timeout,
|
241
|
+
'Timed out waiting for page to load after ' + timeout + 'ms');
|
242
|
+
|
243
|
+
// Make sure the page is an Angular page.
|
244
|
+
self.driver.executeAsyncScript(clientSideScripts.testForAngular,
|
245
|
+
Math.floor(timeout / 1000)).
|
246
|
+
then(function(angularTestResult) {
|
247
|
+
var hasAngular = angularTestResult[0];
|
248
|
+
if (!hasAngular) {
|
249
|
+
var message = angularTestResult[1];
|
250
|
+
throw new Error('Angular could not be found on the page ' +
|
251
|
+
destination + ' : ' + message);
|
252
|
+
}
|
253
|
+
}, function(err) {
|
254
|
+
throw 'Error while running testForAngular: ' + err.message;
|
255
|
+
});
|
256
|
+
|
257
|
+
// At this point, Angular will pause for us until angular.resumeBootstrap
|
258
|
+
// is called.
|
259
|
+
var moduleNames = [];
|
260
|
+
for (var i = 0; i < this.mockModules_.length; ++i) {
|
261
|
+
var mockModule = this.mockModules_[i];
|
262
|
+
var name = mockModule.name;
|
263
|
+
moduleNames.push(name);
|
264
|
+
var executeScriptArgs = [mockModule.script].concat(mockModule.args);
|
265
|
+
this.driver.executeScript.apply(this, executeScriptArgs).
|
266
|
+
then(null, function(err) {
|
267
|
+
throw 'Error while running module script ' + name +
|
268
|
+
': ' + err.message;
|
269
|
+
});
|
270
|
+
}
|
271
|
+
|
272
|
+
return this.driver.executeScript(
|
273
|
+
'angular.resumeBootstrap(arguments[0]);',
|
274
|
+
moduleNames);
|
275
|
+
};
|
276
|
+
FN
|
277
|
+
end
|
278
|
+
|
279
|
+
def refresh
|
280
|
+
<<-FN
|
281
|
+
/**
|
282
|
+
* See webdriver.WebDriver.refresh
|
283
|
+
*
|
284
|
+
* Makes a full reload of the current page and loads mock modules before
|
285
|
+
* Angular. Assumes that the page being loaded uses Angular.
|
286
|
+
* If you need to access a page which does not have Angular on load, use
|
287
|
+
* the wrapped webdriver directly.
|
288
|
+
*
|
289
|
+
* @param {number=} opt_timeout Number of seconds to wait for Angular to start.
|
290
|
+
*/
|
291
|
+
= function(opt_timeout) {
|
292
|
+
var timeout = opt_timeout || 10;
|
293
|
+
var self = this;
|
294
|
+
|
295
|
+
if (self.ignoreSynchronization) {
|
296
|
+
return self.driver.navigate().refresh();
|
297
|
+
}
|
298
|
+
|
299
|
+
return self.driver.executeScript('return window.location.href').then(function(href) {
|
300
|
+
return self.get(href, timeout);
|
301
|
+
});
|
302
|
+
};
|
303
|
+
FN
|
304
|
+
end
|
305
|
+
|
306
|
+
def navigate
|
307
|
+
<<-FN
|
308
|
+
/**
|
309
|
+
* Mixin navigation methods back into the navigation object so that
|
310
|
+
* they are invoked as before, i.e. driver.navigate().refresh()
|
311
|
+
*/
|
312
|
+
function() {
|
313
|
+
var nav = this.driver.navigate();
|
314
|
+
mixin(nav, this, 'refresh');
|
315
|
+
return nav;
|
316
|
+
};
|
317
|
+
FN
|
318
|
+
end
|
319
|
+
|
320
|
+
def setLocation
|
321
|
+
<<-FN
|
322
|
+
/**
|
323
|
+
* Browse to another page using in-page navigation.
|
324
|
+
*
|
325
|
+
* @param {string} url In page URL using the same syntax as $location.url()
|
326
|
+
* @returns {!webdriver.promise.Promise} A promise that will resolve once
|
327
|
+
* page has been changed.
|
328
|
+
*/
|
329
|
+
function(url) {
|
330
|
+
this.waitForAngular();
|
331
|
+
return this.driver.executeScript(clientSideScripts.setLocation, this.rootEl, url)
|
332
|
+
.then(function(browserErr) {
|
333
|
+
if (browserErr) {
|
334
|
+
throw 'Error while navigating to \'' + url + '\' : ' +
|
335
|
+
JSON.stringify(browserErr);
|
336
|
+
}
|
337
|
+
});
|
338
|
+
};
|
339
|
+
FN
|
340
|
+
end
|
341
|
+
|
342
|
+
def getLocationAbsUrl
|
343
|
+
<<-FN
|
344
|
+
/**
|
345
|
+
* Returns the current absolute url from AngularJS.
|
346
|
+
*/
|
347
|
+
function() {
|
348
|
+
this.waitForAngular();
|
349
|
+
return this.driver.executeScript(clientSideScripts.getLocationAbsUrl, this.rootEl);
|
350
|
+
};
|
351
|
+
FN
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
data/lib/angular/dsl.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
module Angular
|
2
|
+
module DSL
|
3
|
+
def ng
|
4
|
+
@ng ||= Angular::Setup.new(Capybara.current_session)
|
5
|
+
end
|
6
|
+
|
7
|
+
def ng_install
|
8
|
+
ng.install
|
9
|
+
end
|
10
|
+
|
11
|
+
def ng_wait
|
12
|
+
ng.ng_wait
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# @return current location absolute url
|
17
|
+
#
|
18
|
+
def ng_location_abs(using = 'body')
|
19
|
+
ng.make_call :getLocationAbsUrl, [using], false
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return current location absolute url
|
23
|
+
#
|
24
|
+
def ng_location(using = 'body')
|
25
|
+
ng.make_call :getLocation, [using], false
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# @return current location
|
30
|
+
#
|
31
|
+
def ng_set_location(url, using = 'body')
|
32
|
+
ng.make_call :setLocation, [using, url], false
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @return eval result
|
37
|
+
#
|
38
|
+
def ng_eval(selector, expr)
|
39
|
+
ng.make_call :evaluate, [selector, expr], false
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Node for nth binding match
|
44
|
+
# @return nth node
|
45
|
+
#
|
46
|
+
def ng_binding(binding, exact = false, row = 0, using = nil , rootSelector = 'body')
|
47
|
+
ng_bindings(binding, exact, using , rootSelector)[row]
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# All nodes matching binding
|
52
|
+
#
|
53
|
+
# @return [node, ...]
|
54
|
+
#
|
55
|
+
def ng_bindings(binding, exact = false, using = nil , rootSelector = 'body')
|
56
|
+
ng.make_call :findBindings, [binding, exact, using, rootSelector], true
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Node for nth model match
|
61
|
+
#
|
62
|
+
# @return nth node
|
63
|
+
#
|
64
|
+
def ng_model(model, row = 0, using = nil , rootSelector = 'body')
|
65
|
+
ng_models(model, using, rootSelector)[row]
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# All nodes matching model
|
70
|
+
#
|
71
|
+
# @return [node, ...]
|
72
|
+
#
|
73
|
+
def ng_models(model, using = nil , rootSelector = 'body')
|
74
|
+
ng.make_call :findByModel, [model, using, rootSelector], true
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Node for nth option
|
79
|
+
#
|
80
|
+
# @return nth node
|
81
|
+
#
|
82
|
+
def ng_option(options, row = 0, using = nil , rootSelector = 'body')
|
83
|
+
ng_options(options, using, rootSelector)[row]
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# All option values matching option
|
88
|
+
# @return [node, ...]
|
89
|
+
#
|
90
|
+
def ng_options(options, using = nil , rootSelector = 'body')
|
91
|
+
ng.make_call :findByOptions, [options, using, rootSelector], true
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Node for nth repeater row
|
96
|
+
# @return nth node
|
97
|
+
#
|
98
|
+
def ng_repeater_row(repeater, row = 0, using = nil , rootSelector = 'body')
|
99
|
+
ng.make_call(:findRepeaterRows, [repeater, row, using, rootSelector], true).first
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# All nodes matching repeater
|
104
|
+
#
|
105
|
+
# @return [node, ...]
|
106
|
+
#
|
107
|
+
def ng_repeater_rows(repeater, using = nil , rootSelector = 'body')
|
108
|
+
ng.make_call :findAllRepeaterRows, [repeater, using, rootSelector], true
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Node for column binding value in row
|
113
|
+
#
|
114
|
+
# @return nth node
|
115
|
+
#
|
116
|
+
def ng_repeater_column(repeater, binding, row = 0, using = nil , rootSelector = 'body')
|
117
|
+
ng_repeater_columns(repeater, binding, using, rootSelector)[row]
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Node for column binding value in all rows
|
122
|
+
#
|
123
|
+
# @return [node, ...]
|
124
|
+
#
|
125
|
+
def ng_repeater_columns(repeater, binding, using = nil , rootSelector = 'body')
|
126
|
+
ng.make_call :findRepeaterColumn, [repeater, binding, using, rootSelector], true
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# @return nth node
|
131
|
+
#
|
132
|
+
def ng_repeater_element(repeater, index, binding, row = 0, using = nil, rootSelector = 'body')
|
133
|
+
ng_repeater_elements(repeater, index, binding, using, rootSelector)[row]
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# @return [node, ...]
|
138
|
+
#
|
139
|
+
def ng_repeater_elements(repeater, index, binding, using = nil, rootSelector = 'body')
|
140
|
+
ng.make_call :findRepeaterElement, [repeater, index, binding, using, rootSelector], true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|