capybara-ng 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|