Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // ext. libs | |
28 | 1 | var async = require('async'); |
29 | 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2; |
30 | ||
31 | // int. libs | |
32 | 1 | var Driver = require('./dalek/driver'); |
33 | 1 | var Reporter = require('./dalek/reporter'); |
34 | 1 | var Timer = require('./dalek/timer'); |
35 | 1 | var Config = require('./dalek/config'); |
36 | 1 | var Host = require('./dalek/host'); |
37 | ||
38 | /** | |
39 | * Default options | |
40 | * @type {Object} | |
41 | */ | |
42 | ||
43 | 1 | var defaults = { |
44 | reporter: ['console'], | |
45 | driver: ['native'], | |
46 | browser: ['phantomjs'], | |
47 | viewport: {width: 1280, height: 1024}, | |
48 | logLevel: 3 | |
49 | }; | |
50 | ||
51 | /** | |
52 | * Setup all the options needed to configure dalek | |
53 | * | |
54 | * @param {options} opts Configuration options | |
55 | * @constructor | |
56 | */ | |
57 | ||
58 | 1 | var Dalek = function (opts) { |
59 | // setup instance | |
60 | 7 | this._initialize(); |
61 | ||
62 | // register exception handler | |
63 | 7 | this._registerExceptionHandler(); |
64 | ||
65 | // normalize options | |
66 | 7 | this.options = this.normalizeOptions(opts); |
67 | ||
68 | // getting advanced options | |
69 | 7 | if (opts && opts.advanced) { |
70 | 2 | this.advancedOptions = opts.advanced; |
71 | } | |
72 | ||
73 | // initiate config | |
74 | 7 | this.config = new Config(defaults, this.options, this.advancedOptions); |
75 | ||
76 | // override tests if provided on the commandline | |
77 | 7 | if (this.options.tests) { |
78 | 6 | this.config.config.tests = this.options.tests; |
79 | } | |
80 | ||
81 | // prepare and load reporter(s) | |
82 | 7 | this._setupReporters(); |
83 | ||
84 | // count all passed & failed assertions | |
85 | 7 | this.reporterEvents.on('report:assertion', this._onReportAssertion.bind(this)); |
86 | ||
87 | // init the timer instance | |
88 | 7 | this.timer = new Timer(); |
89 | ||
90 | // prepare driver event emitter instance | |
91 | 7 | this._setupDriverEmitter(); |
92 | ||
93 | // check for file option, throw error if none is given | |
94 | // only bypasses if dalek runs in the remote mode | |
95 | 7 | if (!Array.isArray(this.config.get('tests')) && !this.options.remote) { |
96 | 1 | this.reporterEvents.emit('error', 'No test files given!'); |
97 | 1 | this.driverEmitter.emit('killAll'); |
98 | 1 | process.exit(127); |
99 | } | |
100 | ||
101 | // init the driver instance | |
102 | 7 | this._initDriver(); |
103 | ||
104 | // check if dalek runs as a remote browser launcher | |
105 | 7 | if (this.options.remote) { |
106 | 0 | var host = new Host({reporterEvents: this.reporterEvents, config: this.config}); |
107 | 0 | host.run({ |
108 | port: !isNaN(parseFloat(this.options.remote)) && isFinite(this.options.remote) ? this.options.remote : false | |
109 | }); | |
110 | } | |
111 | }; | |
112 | ||
113 | /** | |
114 | * Daleks base module | |
115 | * Used to configure all the things | |
116 | * and to start off the tests | |
117 | * | |
118 | * @module DalekJS | |
119 | * @class Dalek | |
120 | */ | |
121 | ||
122 | 1 | Dalek.prototype = { |
123 | ||
124 | /** | |
125 | * Runs the configured testsuites | |
126 | * | |
127 | * @method run | |
128 | * @chainable | |
129 | */ | |
130 | ||
131 | run: function () { | |
132 | // early return; in case of remote | |
133 | 0 | if (this.options.remote) { |
134 | 0 | return this; |
135 | } | |
136 | ||
137 | // start the timer to measure the execution time | |
138 | 0 | this.timer.start(); |
139 | ||
140 | // emit the runner started event | |
141 | 0 | this.reporterEvents.emit('report:runner:started'); |
142 | ||
143 | // execute all given drivers sequentially | |
144 | 0 | var drivers = this.driver.getDrivers(); |
145 | 0 | async.series(drivers, this.testsuitesFinished.bind(this)); |
146 | 0 | return this; |
147 | }, | |
148 | ||
149 | /** | |
150 | * Reports the all testsuites executed event | |
151 | * | |
152 | * @method testsuitesFinished | |
153 | * @chainable | |
154 | */ | |
155 | ||
156 | testsuitesFinished: function () { | |
157 | 1 | this.driverEmitter.emit('tests:complete'); |
158 | 1 | setTimeout(this.reportRunFinished.bind(this), 0); |
159 | 1 | return this; |
160 | }, | |
161 | ||
162 | /** | |
163 | * Reports the all testsuites executed event | |
164 | * | |
165 | * @method reportRunFinished | |
166 | * @chainable | |
167 | */ | |
168 | ||
169 | reportRunFinished: function () { | |
170 | 2 | this.reporterEvents.emit('report:runner:finished', { |
171 | elapsedTime: this.timer.stop().getElapsedTimeFormatted(), | |
172 | assertions: this.assertionsFailed + this.assertionsPassed, | |
173 | assertionsFailed: this.assertionsFailed, | |
174 | assertionsPassed: this.assertionsPassed, | |
175 | status: this.runnerStatus | |
176 | }); | |
177 | ||
178 | //we want to exit process with code 1 to single that test did not pass | |
179 | 2 | if(this.runnerStatus !== true) { |
180 | 0 | var processExitCaptured = false; |
181 | ||
182 | 0 | process.on('exit', function() { |
183 | 0 | if(processExitCaptured === false) { |
184 | 0 | processExitCaptured = true; |
185 | 0 | process.exit(1); |
186 | } | |
187 | }); | |
188 | } | |
189 | ||
190 | 2 | return this; |
191 | }, | |
192 | ||
193 | /** | |
194 | * Normalizes options | |
195 | * | |
196 | * @method normalizeOptions | |
197 | * @param {object} options Raw options | |
198 | * @return {object} Normalized options | |
199 | */ | |
200 | ||
201 | normalizeOptions: function (options) { | |
202 | 7 | Object.keys(options).forEach(function (key) { |
203 | 51 | if ({reporter: 1, driver: 1}[key]) { |
204 | 14 | options[key] = options[key].map(function (input) { return input.trim(); }); |
205 | } | |
206 | }); | |
207 | ||
208 | 7 | return options; |
209 | }, | |
210 | ||
211 | /** | |
212 | * Sets up system env properties | |
213 | * | |
214 | * @method _initialize | |
215 | * @chainable | |
216 | * @private | |
217 | */ | |
218 | ||
219 | _initialize: function () { | |
220 | // prepare error data | |
221 | 7 | this.warnings = []; |
222 | 7 | this.errors = []; |
223 | ||
224 | // prepare state data for the complete test run | |
225 | 7 | this.runnerStatus = true; |
226 | 7 | this.assertionsFailed = 0; |
227 | 7 | this.assertionsPassed = 0; |
228 | ||
229 | 7 | return this; |
230 | }, | |
231 | ||
232 | /** | |
233 | * Sets up all the reporters | |
234 | * | |
235 | * @method _setupReporters | |
236 | * @chainable | |
237 | * @private | |
238 | */ | |
239 | ||
240 | _setupReporters: function () { | |
241 | 7 | this.reporters = []; |
242 | 7 | this.reporterEvents = new EventEmitter2(); |
243 | 7 | this.reporterEvents.setMaxListeners(Infinity); |
244 | 7 | this.options.reporter = this.config.verifyReporters(this.config.get('reporter'), Reporter); |
245 | 7 | this.options.reporter.forEach(this._addReporter, this); |
246 | 7 | return this; |
247 | }, | |
248 | ||
249 | /** | |
250 | * Adds a reporter | |
251 | * | |
252 | * @method _addReporter | |
253 | * @param {string} reporter Name of the reporter to add | |
254 | * @chainable | |
255 | * @private | |
256 | */ | |
257 | ||
258 | _addReporter: function (reporter) { | |
259 | 7 | this.reporters.push(Reporter.loadReporter(reporter, {events: this.reporterEvents, config: this.config, logLevel: this.config.get('logLevel')})); |
260 | 7 | return this; |
261 | }, | |
262 | ||
263 | /** | |
264 | * Updates the assertion state | |
265 | * | |
266 | * @method _onReportAssertion | |
267 | * @param {object} assertion Informations aout the runned assertions | |
268 | * @chainable | |
269 | * @private | |
270 | */ | |
271 | ||
272 | _onReportAssertion: function (assertion) { | |
273 | 2 | if (assertion.success) { |
274 | 1 | this.assertionsPassed++; |
275 | } else { | |
276 | 1 | this.runnerStatus = false; |
277 | 1 | this.assertionsFailed++; |
278 | } | |
279 | 2 | return this; |
280 | }, | |
281 | ||
282 | /** | |
283 | * Initizializes the driver instances | |
284 | * | |
285 | * @method _initDriver | |
286 | * @chainable | |
287 | * @private | |
288 | */ | |
289 | ||
290 | _initDriver: function () { | |
291 | 7 | this.driver = new Driver({ |
292 | config: this.config, | |
293 | driverEmitter: this.driverEmitter, | |
294 | reporterEvents: this.reporterEvents | |
295 | }); | |
296 | ||
297 | 7 | this.options.driver = this.config.verifyDrivers(this.config.get('driver'), this.driver); |
298 | 7 | return this; |
299 | }, | |
300 | ||
301 | /** | |
302 | * Sets up the event dispatcher for driver events | |
303 | * | |
304 | * @method _setupDriverEmitter | |
305 | * @chainable | |
306 | * @private | |
307 | */ | |
308 | ||
309 | _setupDriverEmitter: function () { | |
310 | 7 | var driverEmitter = new EventEmitter2(); |
311 | 7 | driverEmitter.setMaxListeners(Infinity); |
312 | 7 | this.driverEmitter = driverEmitter; |
313 | 7 | return this; |
314 | }, | |
315 | ||
316 | /** | |
317 | * Make sure to shutdown dalek & its spawned | |
318 | * components, webservers gracefully if a | |
319 | * runtime error pops up | |
320 | * | |
321 | * @method _registerExceptionHandler | |
322 | * @private | |
323 | * @chainable | |
324 | */ | |
325 | ||
326 | _registerExceptionHandler: function () { | |
327 | 7 | process.setMaxListeners(Infinity); |
328 | 7 | process.on('uncaughtException', this._shutdown.bind(this)); |
329 | 7 | return this; |
330 | }, | |
331 | ||
332 | /** | |
333 | * Shutdown on uncaught exception | |
334 | * | |
335 | * @method _shutdown | |
336 | * @param {object} exception Runtime exception | |
337 | * @private | |
338 | */ | |
339 | ||
340 | _shutdown: function (exception) { | |
341 | // ios emulator hack, needs to go in the future | |
342 | 0 | if (exception.message && exception.message.search('This socket has been ended by the other party') !== -1) { |
343 | 0 | return false; |
344 | } | |
345 | ||
346 | 0 | this.driverEmitter.emit('killAll'); |
347 | 0 | this.reporterEvents.emit('error', exception); |
348 | } | |
349 | ||
350 | }; | |
351 | ||
352 | // export dalek as a module | |
353 | 1 | module.exports = Dalek; |
354 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | */ | |
23 | ||
24 | 1 | 'use strict'; |
25 | ||
26 | // ext. libs | |
27 | 1 | var Q = require('q'); |
28 | 1 | var uuid = require('./uuid'); |
29 | 1 | var cheerio = require('cheerio'); |
30 | ||
31 | // int. global | |
32 | 1 | var reporter = null; |
33 | ||
34 | /** | |
35 | * Actions are a way to control your browsers, e.g. simulate user interactions | |
36 | * like clicking elements, open urls, filling out input fields, etc. | |
37 | * | |
38 | * @class Actions | |
39 | * @constructor | |
40 | * @part Actions | |
41 | * @api | |
42 | */ | |
43 | ||
44 | 1 | var Actions = function () { |
45 | 5 | this.uuids = {}; |
46 | }; | |
47 | ||
48 | /** | |
49 | * It can be really cumbersome to repeat selectors all over when performing | |
50 | * multiple actions or assertions on the same element(s). | |
51 | * When you use the query method (or its alias $), you're able to specify a | |
52 | * selector once instead of repeating it all over the place. | |
53 | * | |
54 | * So, instead of writing this: | |
55 | * | |
56 | * ```javascript | |
57 | * test.open('http://doctorwhotv.co.uk/') | |
58 | * .assert.text('#nav').is('Navigation') | |
59 | * .assert.visible('#nav') | |
60 | * .assert.attr('#nav', 'data-nav', 'true') | |
61 | * .click('#nav') | |
62 | * .done(); | |
63 | * ``` | |
64 | * | |
65 | * you can write this: | |
66 | * | |
67 | * ```javascript | |
68 | * test.open('http://doctorwhotv.co.uk/') | |
69 | * .query('#nav') | |
70 | * .assert.text().is('Navigation') | |
71 | * .assert.visible() | |
72 | * .assert.attr('data-nav', 'true') | |
73 | * .click() | |
74 | * .end() | |
75 | * .done(); | |
76 | * ``` | |
77 | * | |
78 | * Always make sure to terminate it with the [end](assertions.html#meth-end) method! | |
79 | * | |
80 | * @api | |
81 | * @method query | |
82 | * @param {string} selector Selector of the element to query | |
83 | * @chainable | |
84 | */ | |
85 | ||
86 | 1 | Actions.prototype.query = function (selector) { |
87 | 0 | var that = !this.test ? this : this.test; |
88 | 0 | that.lastChain.push('querying'); |
89 | 0 | that.selector = selector; |
90 | 0 | that.querying = true; |
91 | 0 | return this.test ? this : that; |
92 | }; | |
93 | ||
94 | /** | |
95 | * Alias of query | |
96 | * | |
97 | * @api | |
98 | * @method $ | |
99 | * @param {string} selector Selector of the element to query | |
100 | * @chainable | |
101 | */ | |
102 | ||
103 | 1 | Actions.prototype.$ = Actions.prototype.query; |
104 | ||
105 | /** | |
106 | * Triggers a mouse event on the first element found matching the provided selector. | |
107 | * Supported events are mouseup, mousedown, click, mousemove, mouseover and mouseout. | |
108 | * TODO: IMPLEMENT | |
109 | * | |
110 | * @method mouseEvent | |
111 | * @param {string} type | |
112 | * @param {string} selector | |
113 | * @chainable | |
114 | */ | |
115 | ||
116 | 1 | Actions.prototype.mouseEvent = function (type, selector) { |
117 | 0 | var hash = uuid(); |
118 | 0 | var cb = this._generateCallbackAssertion('mouseEvent', 'mouseEvent', type, selector, hash); |
119 | 0 | this._addToActionQueue([type, selector, hash], 'mouseEvent', cb); |
120 | 0 | return this; |
121 | }; | |
122 | ||
123 | /** | |
124 | * Sets HTTP_AUTH_USER and HTTP_AUTH_PW values for HTTP based authentication systems. | |
125 | * | |
126 | * If your site is behind a HTTP basic auth, you're able to set the username and the password | |
127 | * | |
128 | * ```javascript | |
129 | * test.setHttpAuth('OSWIN', 'rycbrar') | |
130 | * .open('http://admin.therift.com'); | |
131 | * ``` | |
132 | * | |
133 | * Most of the time, you`re not storing your passwords within files that will be checked | |
134 | * in your vcs, for this scenario, you have two options: | |
135 | * | |
136 | * The first option is, to use daleks cli capabilities to generate config variables | |
137 | * from the command line, like this | |
138 | * | |
139 | * ```batch | |
140 | * $ dalek --vars USER=OSWIN,PASS=rycbrar | |
141 | * ``` | |
142 | * | |
143 | * ```javascript | |
144 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) | |
145 | * .open('http://admin.therift.com'); | |
146 | * ``` | |
147 | * | |
148 | * The second option is, to use env variables to generate config variables | |
149 | * from the command line, like this | |
150 | * | |
151 | * ```batch | |
152 | * $ SET USER=OSWIN | |
153 | * $ SET PASS=rycbrar | |
154 | * $ dalek | |
155 | * ``` | |
156 | * | |
157 | * ```javascript | |
158 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) | |
159 | * .open('http://admin.therift.com'); | |
160 | * ``` | |
161 | * | |
162 | * If both, dalek variables & env variables are set, the dalek variables win. | |
163 | * For more information about this, I recommend to check out the [configuration docs](/docs/config.html) | |
164 | * | |
165 | * TODO: IMPLEMENT | |
166 | * | |
167 | * @method setHttpAuth | |
168 | * @param {string} username | |
169 | * @param {string} password | |
170 | * @return {Actions} | |
171 | */ | |
172 | ||
173 | 1 | Actions.prototype.setHttpAuth = function (username, password) { |
174 | 0 | var hash = uuid(); |
175 | 0 | var cb = this._generateCallbackAssertion('setHttpAuth', 'setHttpAuth', username, password, hash); |
176 | 0 | this._addToActionQueue([username, password, hash], 'setHttpAuth', cb); |
177 | 0 | return this; |
178 | }; | |
179 | ||
180 | /** | |
181 | * Switches to an iFrame context | |
182 | * | |
183 | * Sometimes you encounter situations, where you need to drive/access an iFrame sitting in your page. | |
184 | * You can access such frames with this method, but be aware of the fact, that the complete test context | |
185 | * than switches to the iframe context, every action and assertion will be executed within the iFrame context. | |
186 | * Btw.: The domain of the IFrame can be whatever you want, this method has no same origin policy restrictions. | |
187 | * | |
188 | * If you wan't to get back to the parents context, you have to use the [toParent](#meth-toParent) method. | |
189 | * | |
190 | * ```html | |
191 | * <div> | |
192 | * <iframe id="login" src="/login.html"/> | |
193 | * </div> | |
194 | * ``` | |
195 | * | |
196 | * ```javascript | |
197 | * test.open('http://adomain.withiframe.com') | |
198 | * .assert.title().is('Title of a page that embeds an iframe') | |
199 | * .toFrame('#login') | |
200 | * .assert.title().is('Title of a page that can be embedded as an iframe') | |
201 | * .toParent() | |
202 | * .done(); | |
203 | * ``` | |
204 | * | |
205 | * > NOTE: Buggy in Firefox | |
206 | * | |
207 | * @api | |
208 | * @method toFrame | |
209 | * @param {string} selector Selector of the frame to switch to | |
210 | * @chainable | |
211 | */ | |
212 | ||
213 | 1 | Actions.prototype.toFrame = function (selector) { |
214 | 0 | var hash = uuid(); |
215 | ||
216 | 0 | if (this.querying === true) { |
217 | 0 | selector = this.selector; |
218 | } | |
219 | ||
220 | 0 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', selector, hash); |
221 | 0 | this._addToActionQueue([selector, hash], 'toFrame', cb); |
222 | 0 | return this; |
223 | }; | |
224 | ||
225 | /** | |
226 | * Switches back to the parent page context when the test context has been | |
227 | * switched to an iFrame context | |
228 | * | |
229 | * ```html | |
230 | * <div> | |
231 | * <iframe id="login" src="/login.html"/> | |
232 | * </div> | |
233 | * ``` | |
234 | * | |
235 | * ```javascript | |
236 | * test.open('http://adomain.withiframe.com') | |
237 | * .assert.title().is('Title of a page that embeds an iframe') | |
238 | * .toFrame('#login') | |
239 | * .assert.title().is('Title of a page that can be embedded as an iframe') | |
240 | * .toParent() | |
241 | * .assert.title().is('Title of a page that embeds an iframe') | |
242 | * .done(); | |
243 | * ``` | |
244 | * | |
245 | * > NOTE: Buggy in Firefox | |
246 | * | |
247 | * @api | |
248 | * @method toParent | |
249 | * @chainable | |
250 | */ | |
251 | ||
252 | 1 | Actions.prototype.toParent = function () { |
253 | 0 | var hash = uuid(); |
254 | 0 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', null, hash); |
255 | 0 | this._addToActionQueue([null, hash], 'toFrame', cb); |
256 | 0 | return this; |
257 | }; | |
258 | ||
259 | /** | |
260 | * Switches to a different window context | |
261 | * | |
262 | * Sometimes you encounter situations, where you need to access a different window, like popup windows. | |
263 | * You can access such windows with this method, but be aware of the fact, that the complete test context | |
264 | * than switches to the window context, every action and assertion will be executed within the chosen window context. | |
265 | * Btw.: The domain of the window can be whatever you want, this method has no same origin policy restrictions. | |
266 | * | |
267 | * If you want to get back to the parents context, you have to use the [toParentWindow](#meth-toParentWindow) method. | |
268 | * | |
269 | * ```html | |
270 | * <div> | |
271 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> | |
272 | * </div> | |
273 | * ``` | |
274 | * | |
275 | * ```javascript | |
276 | * test.open('http://adomain.com') | |
277 | * .assert.title().is('Title of a page that can open a popup window') | |
278 | * .toWindow('goog') | |
279 | * .assert.title().is('Google') | |
280 | * .toParentWindow() | |
281 | * .done(); | |
282 | * ``` | |
283 | * | |
284 | * > NOTE: Buggy in Firefox | |
285 | * | |
286 | * @api | |
287 | * @method toWindow | |
288 | * @param {string} name Name of the window to switch to | |
289 | * @chainable | |
290 | */ | |
291 | ||
292 | 1 | Actions.prototype.toWindow = function (name) { |
293 | 0 | var hash = uuid(); |
294 | 0 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', name, hash); |
295 | 0 | this._addToActionQueue([name, hash], 'toWindow', cb); |
296 | 0 | return this; |
297 | }; | |
298 | ||
299 | /** | |
300 | * Switches back to the parent window context when the test context has been | |
301 | * switched to a different window context | |
302 | * | |
303 | * ```html | |
304 | * <div> | |
305 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> | |
306 | * </div> | |
307 | * ``` | |
308 | * | |
309 | * ```javascript | |
310 | * test.open('http://adomain.com') | |
311 | * .assert.title().is('Title of a page that can open a popup window') | |
312 | * .toWindow('goog') | |
313 | * .assert.title().is('Google') | |
314 | * .toParentWindow() | |
315 | * .assert.title().is('Title of a page that can open a popup window') | |
316 | * .done(); | |
317 | * ``` | |
318 | * | |
319 | * > NOTE: Buggy in Firefox | |
320 | * | |
321 | * @api | |
322 | * @method toParentWindow | |
323 | * @chainable | |
324 | */ | |
325 | ||
326 | 1 | Actions.prototype.toParentWindow = function () { |
327 | 0 | var hash = uuid(); |
328 | 0 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', null, hash); |
329 | 0 | this._addToActionQueue([null, hash], 'toWindow', cb); |
330 | 0 | return this; |
331 | }; | |
332 | ||
333 | /** | |
334 | * Wait until a resource that matches the given testFx is loaded to process a next step. | |
335 | * | |
336 | * TODO: IMPLEMENT | |
337 | * | |
338 | * @method waitForResource | |
339 | * @param {string} ressource URL of the ressource that should be waited for | |
340 | * @param {number} timeout Timeout in miliseconds | |
341 | * @chainable | |
342 | */ | |
343 | ||
344 | 1 | Actions.prototype.waitForResource = function (ressource, timeout) { |
345 | 0 | var hash = uuid(); |
346 | 0 | var cb = this._generateCallbackAssertion('waitForResource', 'waitForResource', ressource, timeout, hash); |
347 | 0 | this._addToActionQueue([ressource, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForResource', cb); |
348 | 0 | return this; |
349 | }; | |
350 | ||
351 | /** | |
352 | * Waits until the passed text is present in the page contents before processing the immediate next step. | |
353 | * | |
354 | * TODO: IMPLEMENT | |
355 | * | |
356 | * @method waitForText | |
357 | * @param {string} text Text to be waited for | |
358 | * @param {number} timeout Timeout in miliseconds | |
359 | * @chainable | |
360 | */ | |
361 | ||
362 | 1 | Actions.prototype.waitForText = function (text, timeout) { |
363 | 0 | var hash = uuid(); |
364 | 0 | var cb = this._generateCallbackAssertion('waitForText', 'waitForText', text, timeout, hash); |
365 | 0 | this._addToActionQueue([text, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForText', cb); |
366 | 0 | return this; |
367 | }; | |
368 | ||
369 | /** | |
370 | * Waits until an element matching the provided selector expression is visible in the remote DOM to process a next step. | |
371 | * | |
372 | * TODO: IMPLEMENT | |
373 | * | |
374 | * @method waitUntilVisible | |
375 | * @param {string} selector Selector of the element that should be waited to become invisible | |
376 | * @param {number} timeout Timeout in miliseconds | |
377 | * @chainable | |
378 | */ | |
379 | ||
380 | 1 | Actions.prototype.waitUntilVisible = function (selector, timeout) { |
381 | 0 | var hash = uuid(); |
382 | ||
383 | 0 | if (this.querying === true) { |
384 | 0 | timeout = selector; |
385 | 0 | selector = this.selector; |
386 | } | |
387 | ||
388 | 0 | var cb = this._generateCallbackAssertion('waitUntilVisible', 'waitUntilVisible', selector, timeout, hash); |
389 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitUntilVisible', cb); |
390 | 0 | return this; |
391 | }; | |
392 | ||
393 | /** | |
394 | * Waits until an element matching the provided selector expression is no longer visible in remote DOM to process a next step. | |
395 | * | |
396 | * TODO: IMPLEMENT | |
397 | * | |
398 | * @method waitWhileVisible | |
399 | * @param {string} selector Selector of the element that should be waited to become visible | |
400 | * @param {number} timeout Timeout in miliseconds | |
401 | * @chainable | |
402 | */ | |
403 | ||
404 | 1 | Actions.prototype.waitWhileVisible = function (selector, timeout) { |
405 | 0 | var hash = uuid(); |
406 | ||
407 | 0 | if (this.querying === true) { |
408 | 0 | timeout = selector; |
409 | 0 | selector = this.selector; |
410 | } | |
411 | ||
412 | 0 | var cb = this._generateCallbackAssertion('waitWhileVisible', 'waitWhileVisible', selector, timeout, hash); |
413 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitWhileVisible', cb); |
414 | 0 | return this; |
415 | }; | |
416 | ||
417 | /** | |
418 | * Take a screenshot of the current page or css element. | |
419 | * | |
420 | * The pathname argument takes some placeholders that will be replaced | |
421 | * Placeholder: | |
422 | * | |
423 | * - `:browser` - The browser name (e.g. 'Chrome', 'Safari', 'Firefox', etc.) | |
424 | * - `:version` - The browser version (e.g. '10_0', '23_11_5', etc.) | |
425 | * - `:os` - The operating system (e.g. `OSX`, `Windows`, `Linux`) | |
426 | * - `:osVersion` - The operating system version (e.g `XP`, `7`, `10_8`, etc.) | |
427 | * - `:viewport` - The current viewport in pixels (e.g. `w1024_h768`) | |
428 | * - `:timestamp` - UNIX like timestapm (e.g. `637657345`) | |
429 | * - `:date` - Current date in format MM_DD_YYYY (e.g. `12_24_2013`) | |
430 | * - `:datetime` - Current datetime in format MM_DD_YYYY_HH_mm_ss (e.g. `12_24_2013_14_55_23`) | |
431 | * | |
432 | * ```javascript | |
433 | * // creates 'my/folder/my_file.png' | |
434 | * test.screenshot('my/folder/my_file.png'); | |
435 | * // creates 'my/page/in/safari/homepage.png' | |
436 | * test.screenshot('my/page/in/:browser/homepage.png'); | |
437 | * // creates 'my/page/in/safari_6_0_1/homepage.png' | |
438 | * test.screenshot('my/page/in/:browser_:version/homepage.png'); | |
439 | * // creates 'my/page/in/safari_6_0_1/on/osx/homepage.png' | |
440 | * test.screenshot('my/page/in/:browser_:version/on/:os/homepage.png'); | |
441 | * // creates 'my/page/in/safari_6_0_1/on/osx_10_8/homepage.png' | |
442 | * test.screenshot('my/page/in/:browser_:version/on/:os_:osVersion/homepage.png'); | |
443 | * // creates 'my/page/at/w1024_h768/homepage.png' | |
444 | * test.screenshot('my/page/at/:viewport/homepage.png'); | |
445 | * // creates 'my/page/at/637657345/homepage.png' | |
446 | * test.screenshot('my/page/in_time/:timestamp/homepage.png'); | |
447 | * // creates 'my/page/at/12_24_2013/homepage.png' | |
448 | * test.screenshot('my/page/in_time/:date/homepage.png'); | |
449 | * // creates 'my/page/at/12_24_2013_14_55_23/homepage.png' | |
450 | * test.screenshot('my/page/in_time/:datetime/homepage.png'); | |
451 | * ``` | |
452 | * | |
453 | * @api | |
454 | * @method screenshot | |
455 | * @param {string} pathname Name of the folder and file the screenshot should be saved to | |
456 | * @param {string} css selector of element should be screeshoted | |
457 | * @return chainable | |
458 | */ | |
459 | ||
460 | 1 | Actions.prototype.screenshot = function (pathname, selector) { |
461 | 3 | var hash = uuid(); |
462 | ||
463 | 3 | if (this.querying === true) { |
464 | 0 | selector = this.selector; |
465 | } | |
466 | ||
467 | 3 | var opts = { |
468 | realpath : undefined, | |
469 | selector : selector | |
470 | }; | |
471 | ||
472 | 3 | this.screenshotParams = opts; |
473 | ||
474 | 3 | var screenshotcb = this._generatePlainCallback('screenshot', hash, opts, 'realpath', typeof selector === 'undefined'); |
475 | 3 | this._addToActionQueue(['', pathname, hash], 'screenshot', screenshotcb.bind(this)); |
476 | ||
477 | 3 | if (selector) { |
478 | 1 | var imagecutcb = this._generateCallbackAssertion('imagecut', 'screenshot element', opts, hash); |
479 | 1 | this._addToActionQueue([opts, hash], 'imagecut', imagecutcb); |
480 | } | |
481 | ||
482 | 3 | this.reporter.emit('report:screenshot', { |
483 | 'pathname' : pathname, | |
484 | 'uuid' : hash | |
485 | }); | |
486 | ||
487 | 3 | return this; |
488 | }; | |
489 | ||
490 | /** | |
491 | * Generates a callback that will be fired when the action has been completed. | |
492 | * The callback will then store value into opts variable. | |
493 | * | |
494 | * @method _generateCallbackAssertion | |
495 | * @param {string} type Type of the action (normalle the actions name) | |
496 | * @param {string} hash Unique id of the action | |
497 | * @param {string} opts Variable where will be stored result of execution of the action | |
498 | * @param {string} key Name of the property where will be stored result of execution of the action | |
499 | * @return {function} The generated callback function | |
500 | * @private | |
501 | */ | |
502 | 1 | Actions.prototype._generatePlainCallback = function (type, hash, opts, property, last) { |
503 | 3 | var cb = function (data) { |
504 | 0 | if (data.hash === hash && data.key === type && !this.uuids[data.uuid]) { |
505 | 0 | if (typeof opts === 'object' && typeof property === 'string') { |
506 | 0 | opts[property] = data.value; |
507 | } | |
508 | 0 | if (data.key === 'screenshot') { |
509 | 0 | this.reporter.emit('report:action', { |
510 | value: data.value, | |
511 | type: type, | |
512 | uuid: data.uuid | |
513 | }); | |
514 | } | |
515 | ||
516 | 0 | if (last) { |
517 | 0 | this.uuids[data.uuid] = true; |
518 | } | |
519 | } | |
520 | }; | |
521 | 3 | return cb; |
522 | }; | |
523 | ||
524 | /** | |
525 | * Pause steps suite execution for a given amount of time, and optionally execute a step on done. | |
526 | * | |
527 | * This makes sense, if you have a ticker for example, tht scrolls like every ten seconds | |
528 | * & you want to assure that the visible content changes every ten seconds | |
529 | * | |
530 | * ```javascript | |
531 | * test.open('http://myticker.org') | |
532 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') | |
533 | * .wait(10000) | |
534 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') | |
535 | * .wait(10000) | |
536 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') | |
537 | * .done(); | |
538 | * ``` | |
539 | * If no timeout argument is given, a default timeout of 5 seconds will be used | |
540 | * | |
541 | * ```javascript | |
542 | * test.open('http://myticker.org') | |
543 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') | |
544 | * .wait() | |
545 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') | |
546 | * .wait() | |
547 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') | |
548 | * .done(); | |
549 | * ``` | |
550 | * | |
551 | * @api | |
552 | * @method wait | |
553 | * @param {number} timeout in milliseconds | |
554 | * @chainable | |
555 | */ | |
556 | ||
557 | 1 | Actions.prototype.wait = function (timeout) { |
558 | 0 | var hash = uuid(); |
559 | 0 | var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash); |
560 | 0 | this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb); |
561 | 0 | return this; |
562 | }; | |
563 | ||
564 | /** | |
565 | * Reloads current page location. | |
566 | * | |
567 | * This is basically the same as hitting F5/refresh in your browser | |
568 | * | |
569 | * ```javascript | |
570 | * test.open('http://google.com') | |
571 | * .reload() | |
572 | * .done(); | |
573 | * ``` | |
574 | * | |
575 | * @api | |
576 | * @method reload | |
577 | * @chainable | |
578 | */ | |
579 | ||
580 | 1 | Actions.prototype.reload = function () { |
581 | 0 | var hash = uuid(); |
582 | 0 | var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash); |
583 | 0 | this._addToActionQueue([hash], 'refresh', cb); |
584 | 0 | return this; |
585 | }; | |
586 | ||
587 | /** | |
588 | * Moves a step forward in browser's history. | |
589 | * | |
590 | * This is basically the same as hitting the forward button in your browser | |
591 | * | |
592 | * ```javascript | |
593 | * test.open('http://google.com') | |
594 | * .open('https://github.com') | |
595 | * .assert.url.is('https://github.com/', 'We are at GitHub') | |
596 | * .back() | |
597 | * .assert.url.is('http://google.com', 'We are at Google!') | |
598 | * .forward() | |
599 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW') | |
600 | * .done(); | |
601 | * ``` | |
602 | * | |
603 | * @api | |
604 | * @method forward | |
605 | * @chainable | |
606 | */ | |
607 | ||
608 | 1 | Actions.prototype.forward = function () { |
609 | 0 | var hash = uuid(); |
610 | 0 | var cb = this._generateCallbackAssertion('forward', 'forward', '', hash); |
611 | 0 | this._addToActionQueue([hash], 'forward', cb); |
612 | 0 | return this; |
613 | }; | |
614 | ||
615 | /** | |
616 | * Moves back a step in browser's history. | |
617 | * | |
618 | * This is basically the same as hitting the back button in your browser | |
619 | * | |
620 | * ```javascript | |
621 | * test.open('http://google.com') | |
622 | * .open('https://github.com') | |
623 | * .assert.url.is('https://github.com/', 'We are at GitHub') | |
624 | * .back() | |
625 | * .assert.url.is('http://google.com', 'We are at Google!') | |
626 | * .forward() | |
627 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW'); | |
628 | * .done(); | |
629 | * ``` | |
630 | * | |
631 | * @api | |
632 | * @method back | |
633 | * @chainable | |
634 | */ | |
635 | ||
636 | 1 | Actions.prototype.back = function () { |
637 | 0 | var hash = uuid(); |
638 | 0 | var cb = this._generateCallbackAssertion('back', 'back', '', hash); |
639 | 0 | this._addToActionQueue([hash], 'back', cb); |
640 | 0 | return this; |
641 | }; | |
642 | ||
643 | /** | |
644 | * Performs a click on the element matching the provided selector expression. | |
645 | * | |
646 | * If we take Daleks homepage (the one you're probably visiting right now), | |
647 | * the HTML looks something like this (it does not really, but hey, lets assume this for a second) | |
648 | * | |
649 | * ```html | |
650 | * <nav> | |
651 | * <ul> | |
652 | * <li><a id="homeapge" href="/index.html">DalekJS</a></li> | |
653 | * <li><a id="docs" href="/docs.html">Documentation</a></li> | |
654 | * <li><a id="faq" href="/faq.html">F.A.Q</a></li> | |
655 | * </ul> | |
656 | * </nav> | |
657 | * ``` | |
658 | * | |
659 | * ```javascript | |
660 | * test.open('http://dalekjs.com') | |
661 | * .click('#faq') | |
662 | * .assert.title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.') | |
663 | * .done(); | |
664 | * ``` | |
665 | * | |
666 | * By default, this performs a left click. | |
667 | * In the future it might become the ability to also execute a "right button" click. | |
668 | * | |
669 | * > Note: Does not work correctly in Firefox when used on `<select>` & `<option>` elements | |
670 | * | |
671 | * @api | |
672 | * @method click | |
673 | * @param {string} selector Selector of the element to be clicked | |
674 | * @chainable | |
675 | */ | |
676 | ||
677 | 1 | Actions.prototype.click = function (selector) { |
678 | 0 | var hash = uuid(); |
679 | ||
680 | 0 | if (this.querying === true) { |
681 | 0 | selector = this.selector; |
682 | } | |
683 | ||
684 | 0 | var cb = this._generateCallbackAssertion('click', 'click', selector, hash); |
685 | 0 | this._addToActionQueue([selector, hash], 'click', cb); |
686 | 0 | return this; |
687 | }; | |
688 | ||
689 | /** | |
690 | * Submits a form. | |
691 | * | |
692 | * ```html | |
693 | * <form id="skaaro" action="skaaro.php" method="GET"> | |
694 | * <input type="hidden" name="intheshadows" value="itis"/> | |
695 | * <input type="text" name="truth" id="truth" value=""/> | |
696 | * </form> | |
697 | * ``` | |
698 | * | |
699 | * ```javascript | |
700 | * test.open('http://home.dalek.com') | |
701 | * .type('#truth', 'out there is') | |
702 | * .submit('#skaaro') | |
703 | * .done(); | |
704 | * ``` | |
705 | * | |
706 | * > Note: Does not work in Firefox yet | |
707 | * | |
708 | * @api | |
709 | * @method submit | |
710 | * @param {string} selector Selector of the form to be submitted | |
711 | * @chainable | |
712 | */ | |
713 | ||
714 | 1 | Actions.prototype.submit = function (selector) { |
715 | 0 | var hash = uuid(); |
716 | ||
717 | 0 | if (this.querying === true) { |
718 | 0 | selector = this.selector; |
719 | } | |
720 | ||
721 | 0 | var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash); |
722 | 0 | this._addToActionQueue([selector, hash], 'submit', cb); |
723 | 0 | return this; |
724 | }; | |
725 | ||
726 | /** | |
727 | * Performs an HTTP request for opening a given location. | |
728 | * You can forge GET, POST, PUT, DELETE and HEAD requests. | |
729 | * | |
730 | * Basically the same as typing a location into your browsers URL bar and | |
731 | * hitting return. | |
732 | * | |
733 | * ```javascript | |
734 | * test.open('http://dalekjs.com') | |
735 | * .assert.url().is('http://dalekjs.com', 'DalekJS I\'m in you') | |
736 | * .done(); | |
737 | * ``` | |
738 | * | |
739 | * @api | |
740 | * @method open | |
741 | * @param {string} location URL of the page to open | |
742 | * @chainable | |
743 | */ | |
744 | ||
745 | 1 | Actions.prototype.open = function (location) { |
746 | //see if we should prepend the location with the configured base url is available and needed | |
747 | 0 | if(location.substr(0, 1) === '/' && this.driver.config.config.baseUrl) { |
748 | 0 | location = this.driver.config.config.baseUrl + location; |
749 | } | |
750 | ||
751 | 0 | var hash = uuid(); |
752 | 0 | var cb = this._generateCallbackAssertion('open', 'open', location, hash); |
753 | 0 | this._addToActionQueue([location, hash], 'open', cb); |
754 | 0 | return this; |
755 | }; | |
756 | ||
757 | /** | |
758 | * Types a text into an input field or text area. | |
759 | * And yes, it really types, character for character, like you would | |
760 | * do when using your keyboard. | |
761 | * | |
762 | * | |
763 | * ```html | |
764 | * <form id="skaaro" action="skaaro.php" method="GET"> | |
765 | * <input type="hidden" name="intheshadows" value="itis"/> | |
766 | * <input type="text" name="truth" id="truth" value=""/> | |
767 | * </form> | |
768 | * ``` | |
769 | * | |
770 | * ```javascript | |
771 | * test.open('http://home.dalek.com') | |
772 | * .type('#truth', 'out there is') | |
773 | * .assert.val('#truth', 'out there is', 'Text has been set') | |
774 | * .done(); | |
775 | * ``` | |
776 | * | |
777 | * You can also send special keys using unicode. | |
778 | * | |
779 | * * ```javascript | |
780 | * test.open('http://home.dalek.com') | |
781 | * .type('#truth', 'out \uE008there\uE008 is') | |
782 | * .assert.val('#truth', 'out THERE is', 'Text has been set') | |
783 | * .done(); | |
784 | * ``` | |
785 | * You can go [here](https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value) to read up on special keys and unicodes for them (note that a code of U+EXXX is actually written in code as \uEXXX). | |
786 | * | |
787 | * > Note: Does not work correctly in Firefox with special keys | |
788 | * | |
789 | * @api | |
790 | * @method type | |
791 | * @param {string} selector Selector of the form field to be filled | |
792 | * @param {string} keystrokes Text to be applied to the element | |
793 | * @chainable | |
794 | */ | |
795 | ||
796 | 1 | Actions.prototype.type = function (selector, keystrokes) { |
797 | 0 | var hash = uuid(); |
798 | ||
799 | 0 | if (this.querying === true) { |
800 | 0 | keystrokes = selector; |
801 | 0 | selector = this.selector; |
802 | } | |
803 | ||
804 | 0 | var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash); |
805 | 0 | this._addToActionQueue([selector, keystrokes], 'type', cb); |
806 | 0 | return this; |
807 | }; | |
808 | ||
809 | /** | |
810 | * This acts just like .type() with a key difference. | |
811 | * This action can be used on non-input elements (useful for test site wide keyboard shortcuts and the like). | |
812 | * So assumeing we have a keyboard shortcut that display an alert box, we could test that with something like this: | |
813 | * | |
814 | * ```javascript | |
815 | * test.open('http://home.dalek.com') | |
816 | * .sendKeys('body', '\uE00C') | |
817 | * .assert.dialogText('press the escape key give this alert text') | |
818 | * .done(); | |
819 | * ``` | |
820 | * | |
821 | * | |
822 | * > Note: Does not work correctly in Firefox with special keys | |
823 | * | |
824 | * @api | |
825 | * @method sendKeys | |
826 | * @param {string} selector Selector of the form field to be filled | |
827 | * @param {string} keystrokes Text to be applied to the element | |
828 | * @chainable | |
829 | */ | |
830 | ||
831 | 1 | Actions.prototype.sendKeys = function (selector, keystrokes) { |
832 | 0 | var hash = uuid(); |
833 | ||
834 | 0 | if (this.querying === true) { |
835 | 0 | keystrokes = selector; |
836 | 0 | selector = this.selector; |
837 | } | |
838 | ||
839 | 0 | var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash); |
840 | 0 | this._addToActionQueue([selector, keystrokes], 'sendKeys', cb); |
841 | 0 | return this; |
842 | }; | |
843 | ||
844 | /** | |
845 | * Types a text into the text input field of a prompt dialog. | |
846 | * Like you would do when using your keyboard. | |
847 | * | |
848 | * ```html | |
849 | * <div> | |
850 | * <a id="aquestion" onclick="this.innerText = window.prompt('Your favourite companion:')">????</a> | |
851 | * </div> | |
852 | * ``` | |
853 | * | |
854 | * ```javascript | |
855 | * test.open('http://adomain.com') | |
856 | * .click('#aquestion') | |
857 | * .answer('Rose') | |
858 | * .assert.text('#aquestion').is('Rose', 'Awesome she was!') | |
859 | * .done(); | |
860 | * ``` | |
861 | * | |
862 | * | |
863 | * > Note: Does not work in Firefox & PhantomJS | |
864 | * | |
865 | * @api | |
866 | * @method answer | |
867 | * @param {string} keystrokes Text to be applied to the element | |
868 | * @return chainable | |
869 | */ | |
870 | ||
871 | 1 | Actions.prototype.answer = function (keystrokes) { |
872 | 0 | var hash = uuid(); |
873 | 0 | var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash); |
874 | 0 | this._addToActionQueue([keystrokes, hash], 'promptText', cb); |
875 | 0 | return this; |
876 | }; | |
877 | ||
878 | /** | |
879 | * Executes a JavaScript function within the browser context | |
880 | * | |
881 | * ```javascript | |
882 | * test.open('http://adomain.com') | |
883 | * .execute(function () { | |
884 | * window.myFramework.addRow('foo'); | |
885 | * window.myFramework.addRow('bar'); | |
886 | * }) | |
887 | * .done(); | |
888 | * ``` | |
889 | * | |
890 | * You can also apply arguments to the function | |
891 | * | |
892 | * ```javascript | |
893 | * test.open('http://adomain.com') | |
894 | * .execute(function (paramFoo, aBar) { | |
895 | * window.myFramework.addRow(paramFoo); | |
896 | * window.myFramework.addRow(aBar); | |
897 | * }, 'foo', 'bar') | |
898 | * .done(); | |
899 | * ``` | |
900 | * | |
901 | * > Note: Buggy in Firefox | |
902 | * | |
903 | * @api | |
904 | * @method execute | |
905 | * @param {function} script JavaScript function that should be executed | |
906 | * @return chainable | |
907 | */ | |
908 | ||
909 | 1 | Actions.prototype.execute = function (script) { |
910 | 0 | var hash = uuid(); |
911 | 0 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
912 | 0 | var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash); |
913 | 0 | this._addToActionQueue([script, args, hash], 'execute', cb); |
914 | 0 | return this; |
915 | }; | |
916 | ||
917 | /** | |
918 | * Waits until a function returns true to process any next step. | |
919 | * | |
920 | * You can also set a callback on timeout using the onTimeout argument, | |
921 | * and set the timeout using the timeout one, in milliseconds. The default timeout is set to 5000ms. | |
922 | * | |
923 | * ```javascript | |
924 | * test.open('http://adomain.com') | |
925 | * .waitFor(function () { | |
926 | * return window.myCheck === true; | |
927 | * }) | |
928 | * .done(); | |
929 | * ``` | |
930 | * | |
931 | * You can also apply arguments to the function, as well as a timeout | |
932 | * | |
933 | * ```javascript | |
934 | * test.open('http://adomain.com') | |
935 | * .waitFor(function (aCheck) { | |
936 | * return window.myThing === aCheck; | |
937 | * }, ['arg1', 'arg2'], 10000) | |
938 | * .done(); | |
939 | * ``` | |
940 | * | |
941 | * > Note: Buggy in Firefox | |
942 | * | |
943 | * @method waitFor | |
944 | * @param {function} fn Async function that resolves an promise when ready | |
945 | * @param {array} args Additional arguments | |
946 | * @param {number} timeout Timeout in miliseconds | |
947 | * @chainable | |
948 | * @api | |
949 | */ | |
950 | ||
951 | 1 | Actions.prototype.waitFor = function (script, args, timeout) { |
952 | 0 | var hash = uuid(); |
953 | 0 | timeout = timeout || 5000; |
954 | 0 | args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
955 | 0 | var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash); |
956 | 0 | this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb); |
957 | 0 | return this; |
958 | }; | |
959 | ||
960 | /** | |
961 | * Accepts an alert/prompt/confirm dialog. This is basically the same actions as when | |
962 | * you are clicking okay or hitting return in one of that dialogs. | |
963 | * | |
964 | * ```html | |
965 | * <div> | |
966 | * <a id="attentione" onclick="window.alert('Alonsy!')">ALERT!ALERT!</a> | |
967 | * </div> | |
968 | * ``` | |
969 | * | |
970 | * ```javascript | |
971 | * test.open('http://adomain.com') | |
972 | * // alert appears | |
973 | * .click('#attentione') | |
974 | * // alert is gone | |
975 | * .accept() | |
976 | * .done(); | |
977 | * ``` | |
978 | * | |
979 | * > Note: Does not work in Firefox & PhantomJS | |
980 | * | |
981 | * @api | |
982 | * @method accept | |
983 | * @return chainable | |
984 | */ | |
985 | ||
986 | 1 | Actions.prototype.accept = function () { |
987 | 0 | var hash = uuid(); |
988 | 0 | var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash); |
989 | 0 | this._addToActionQueue([hash], 'acceptAlert', cb); |
990 | 0 | return this; |
991 | }; | |
992 | ||
993 | /** | |
994 | * Dismisses an prompt/confirm dialog. This is basically the same actions as when | |
995 | * you are clicking cancel in one of that dialogs. | |
996 | * | |
997 | * ```html | |
998 | * <div> | |
999 | * <a id="nonono" onclick="(this.innerText = window.confirm('No classic doctors in the 50th?') ? 'Buh!' : ':(') ">What!</a> | |
1000 | * </div> | |
1001 | * ``` | |
1002 | * | |
1003 | * ```javascript | |
1004 | * test.open('http://adomain.com') | |
1005 | * // prompt appears | |
1006 | * .click('#nonono') | |
1007 | * // prompt is gone | |
1008 | * .dismiss() | |
1009 | * .assert.text('#nonono').is(':(', 'So sad') | |
1010 | * .done(); | |
1011 | * ``` | |
1012 | * | |
1013 | * > Note: Does not work in Firefox & PhantomJS | |
1014 | * | |
1015 | * @api | |
1016 | * @method dismiss | |
1017 | * @return chainable | |
1018 | */ | |
1019 | ||
1020 | 1 | Actions.prototype.dismiss = function () { |
1021 | 0 | var hash = uuid(); |
1022 | 0 | var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash); |
1023 | 0 | this._addToActionQueue([hash], 'dismissAlert', cb); |
1024 | 0 | return this; |
1025 | }; | |
1026 | ||
1027 | /** | |
1028 | * Resizes the browser window to a set of given dimensions (in px). | |
1029 | * The default configuration of dalek opening pages is a width of 1280px | |
1030 | * and a height of 1024px. You can specify your own default in the configuration. | |
1031 | * | |
1032 | * ```html | |
1033 | * <div> | |
1034 | * <span id="magicspan">The span in the fireplace</span> | |
1035 | * </div> | |
1036 | * ``` | |
1037 | * | |
1038 | * ```css | |
1039 | * #magicspan { | |
1040 | * display: inline; | |
1041 | * } | |
1042 | * | |
1043 | * // @media all and (max-width: 500px) and (min-width: 300px) | |
1044 | * #magicspan { | |
1045 | * display: none; | |
1046 | * } | |
1047 | * ``` | |
1048 | * | |
1049 | * ```javascript | |
1050 | * test.open('http://adomain.com') | |
1051 | * .assert.visible('#magicspan', 'Big screen, visible span') | |
1052 | * .resize({width: 400, height: 500}) | |
1053 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') | |
1054 | * .done(); | |
1055 | * ``` | |
1056 | * | |
1057 | * | |
1058 | * > Note: Does not work in Firefox | |
1059 | * | |
1060 | * @api | |
1061 | * @method resize | |
1062 | * @param {object} dimensions Width and height as properties to apply | |
1063 | * @chainable | |
1064 | */ | |
1065 | ||
1066 | 1 | Actions.prototype.resize = function (dimensions) { |
1067 | 0 | var hash = uuid(); |
1068 | 0 | var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash); |
1069 | 0 | this._addToActionQueue([dimensions, hash], 'resize', cb); |
1070 | 0 | return this; |
1071 | }; | |
1072 | ||
1073 | /** | |
1074 | * Maximizes the browser window. | |
1075 | * | |
1076 | * ```html | |
1077 | * <div> | |
1078 | * <span id="magicspan">The span in the fireplace</span> | |
1079 | * </div> | |
1080 | * ``` | |
1081 | * | |
1082 | * ```css | |
1083 | * #magicspan { | |
1084 | * display: inline; | |
1085 | * } | |
1086 | * | |
1087 | * @media all and (max-width: 500px) and (min-width: 300px) { | |
1088 | * #magicspan { | |
1089 | * display: none; | |
1090 | * } | |
1091 | * } | |
1092 | * ``` | |
1093 | * | |
1094 | * ```javascript | |
1095 | * test.open('http://adomain.com') | |
1096 | * .resize({width: 400, height: 500}) | |
1097 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') | |
1098 | * .maximize() | |
1099 | * .assert.visible('#magicspan', 'Big screen, visible span') | |
1100 | * .done(); | |
1101 | * ``` | |
1102 | * | |
1103 | * > Note: Does not work in Firefox and PhantomJS | |
1104 | * | |
1105 | * @api | |
1106 | * @method maximize | |
1107 | * @chainable | |
1108 | */ | |
1109 | ||
1110 | 1 | Actions.prototype.maximize = function () { |
1111 | 0 | var hash = uuid(); |
1112 | 0 | var cb = this._generateCallbackAssertion('maximize', 'maximize', hash); |
1113 | 0 | this._addToActionQueue([hash], 'maximize', cb); |
1114 | 0 | return this; |
1115 | }; | |
1116 | ||
1117 | /** | |
1118 | * Sets a cookie. | |
1119 | * More configuration options will be implemented in the future, | |
1120 | * by now, you can only set a cookie with a specific name and contents. | |
1121 | * This will be a domain wide set cookie. | |
1122 | * | |
1123 | * ```javascript | |
1124 | * test.open('http://adomain.com') | |
1125 | * .setCookie('my_cookie_name', 'my=content') | |
1126 | * .done(); | |
1127 | * ``` | |
1128 | * | |
1129 | * @api | |
1130 | * @method setCookie | |
1131 | * @chainable | |
1132 | */ | |
1133 | ||
1134 | 1 | Actions.prototype.setCookie = function (name, contents) { |
1135 | 0 | var hash = uuid(); |
1136 | 0 | var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash); |
1137 | 0 | this._addToActionQueue([name, contents, hash], 'setCookie', cb); |
1138 | 0 | return this; |
1139 | }; | |
1140 | ||
1141 | /** | |
1142 | * Waits until an element matching the provided | |
1143 | * selector expression exists in remote DOM to process any next step. | |
1144 | * | |
1145 | * Lets assume we have a ticker that loads its contents via AJAX, | |
1146 | * and appends new elements, when the call has been successfully answered: | |
1147 | * | |
1148 | * ```javascript | |
1149 | * test.open('http://myticker.org') | |
1150 | * .assert.text('.ticker-element:first-child', 'First!', 'First ticker element is visible') | |
1151 | * // now we load the next ticker element, defsult timeout is 5 seconds | |
1152 | * .waitForElement('.ticker-element:nth-child(2)') | |
1153 | * .assert.text('.ticker-element:nth-child(2)', 'Me snd. one', 'Snd. ticker element is visible') | |
1154 | * // Lets assume that this AJAX call can take longer, so we raise the default timeout to 10 seconds | |
1155 | * .waitForElement('.ticker-element:last-child', 10000) | |
1156 | * .assert.text('.ticker-element:last-child', 'Me, third one!', 'Third ticker element is visible') | |
1157 | * .done(); | |
1158 | * ``` | |
1159 | * | |
1160 | * Note that the function exits succesfully when the first element is found, matching the given selector | |
1161 | * | |
1162 | * @api | |
1163 | * @method waitForElement | |
1164 | * @param {string} selector Selector that matches the element to wait for | |
1165 | * @param {number} timeout Timeout in milliseconds | |
1166 | * @chainable | |
1167 | */ | |
1168 | ||
1169 | 1 | Actions.prototype.waitForElement = function (selector, timeout) { |
1170 | 0 | var hash = uuid(); |
1171 | ||
1172 | 0 | if (this.querying === true) { |
1173 | 0 | timeout = selector; |
1174 | 0 | selector = this.selector; |
1175 | } | |
1176 | ||
1177 | 0 | var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash); |
1178 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb); |
1179 | 0 | return this; |
1180 | }; | |
1181 | ||
1182 | /** | |
1183 | * Fills the fields of a form with given values. | |
1184 | * | |
1185 | * ```html | |
1186 | * <input type="text" value="not really a value" id="ijustwannahaveavalue"/> | |
1187 | * ``` | |
1188 | * | |
1189 | * ```javascript | |
1190 | * test.open('http://dalekjs.com') | |
1191 | * .setValue('#ijustwannahaveavalue', 'a value') | |
1192 | * .assert.val('#ijustwannahaveavalue', 'a value', 'Value is changed'); | |
1193 | * ``` | |
1194 | * | |
1195 | * @api | |
1196 | * @method setValue | |
1197 | * @param {string} selector | |
1198 | * @param {string} value | |
1199 | * @return {Actions} | |
1200 | */ | |
1201 | ||
1202 | 1 | Actions.prototype.setValue = function (selector, value) { |
1203 | 0 | var hash = uuid(); |
1204 | ||
1205 | 0 | if (this.querying === true) { |
1206 | 0 | value = selector; |
1207 | 0 | selector = this.selector; |
1208 | } | |
1209 | ||
1210 | 0 | var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash); |
1211 | 0 | this._addToActionQueue([selector, value, hash], 'setValue', cb); |
1212 | 0 | return this; |
1213 | }; | |
1214 | ||
1215 | // LOG (May should live in its own module) | |
1216 | // --------------------------------------- | |
1217 | ||
1218 | 1 | Actions.prototype.logger = {}; |
1219 | ||
1220 | /** | |
1221 | * Logs a part of the remote dom | |
1222 | * | |
1223 | * ```html | |
1224 | * <body> | |
1225 | * <div id="smth"> | |
1226 | * <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> | |
1227 | * </div> | |
1228 | * </body> | |
1229 | * ``` | |
1230 | * | |
1231 | * ```javascript | |
1232 | * test.open('http://dalekjs.com/guineapig') | |
1233 | * .log.dom('#smth') | |
1234 | * .done(); | |
1235 | * ``` | |
1236 | * | |
1237 | * Will output this: | |
1238 | * | |
1239 | * ```html | |
1240 | * DOM: #smth <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> | |
1241 | * ``` | |
1242 | ||
1243 | * | |
1244 | * @api | |
1245 | * @method log.dom | |
1246 | * @param {string} selector CSS selector | |
1247 | * @chainable | |
1248 | */ | |
1249 | ||
1250 | 1 | Actions.prototype.logger.dom = function (selector) { |
1251 | 0 | var hash = uuid(); |
1252 | ||
1253 | 0 | var cb = function logDomCb (data) { |
1254 | 0 | if (data && data.key === 'source' && !this.uuids[data.uuid]) { |
1255 | 0 | this.uuids[data.uuid] = true; |
1256 | 0 | var $ = cheerio.load(data.value); |
1257 | 0 | var result = selector ? $(selector).html() : $.html(); |
1258 | 0 | selector = selector ? selector : ' '; |
1259 | 0 | result = !result ? ' Not found' : result; |
1260 | 0 | this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result); |
1261 | } | |
1262 | }.bind(this); | |
1263 | ||
1264 | 0 | this._addToActionQueue([hash], 'source', cb); |
1265 | 0 | return this; |
1266 | }; | |
1267 | ||
1268 | /** | |
1269 | * Logs a user defined message | |
1270 | * | |
1271 | * ```javascript | |
1272 | * test.open('http://dalekjs.com/guineapig') | |
1273 | * .execute(function () { | |
1274 | * this.data('aKey', 'aValue'); | |
1275 | * }) | |
1276 | * .log.message(function () { | |
1277 | * return test.data('aKey'); // outputs MESSAGE: 'aValue' | |
1278 | * }) | |
1279 | * .done(); | |
1280 | * ``` | |
1281 | * | |
1282 | * 'Normal' messages can be logged too: | |
1283 | * | |
1284 | * ```javascript | |
1285 | * test.open('http://dalekjs.com/guineapig') | |
1286 | * .log.message('FooBar') // outputs MESSAGE: FooBar | |
1287 | * .done(); | |
1288 | * ``` | |
1289 | * | |
1290 | * @api | |
1291 | * @method log.message | |
1292 | * @param {function|string} message | |
1293 | * @chainable | |
1294 | */ | |
1295 | ||
1296 | 1 | Actions.prototype.logger.message = function (message) { |
1297 | 0 | var hash = uuid(); |
1298 | ||
1299 | 0 | var cb = function logMessageCb (data) { |
1300 | 0 | if (data && data.key === 'noop' && !this.uuids[data.hash]) { |
1301 | 0 | this.uuids[data.hash] = true; |
1302 | 0 | var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value; |
1303 | 0 | this.reporter.emit('report:log:user', 'MESSAGE: ' + result); |
1304 | } | |
1305 | }.bind(this); | |
1306 | ||
1307 | 0 | this._addToActionQueue([message, hash], 'noop', cb); |
1308 | 0 | return this; |
1309 | }; | |
1310 | ||
1311 | /** | |
1312 | * Generates a callback that will be fired when the action has been completed. | |
1313 | * The callback itself will then validate the answer and will also emit an event | |
1314 | * that the action has been successfully executed. | |
1315 | * | |
1316 | * @method _generateCallbackAssertion | |
1317 | * @param {string} key Unique key of the action | |
1318 | * @param {string} type Type of the action (normalle the actions name) | |
1319 | * @return {function} The generated callback function | |
1320 | * @private | |
1321 | */ | |
1322 | ||
1323 | 1 | Actions.prototype._generateCallbackAssertion = function (key, type) { |
1324 | 1 | var cb = function (data) { |
1325 | 0 | if (data && data.key === key && !this.uuids[data.uuid]) { |
1326 | 0 | if (!data || (data.value && data.value === null)) { |
1327 | 0 | data.value = ''; |
1328 | } | |
1329 | ||
1330 | 0 | if (key === 'execute') { |
1331 | 0 | Object.keys(data.value.dalek).forEach(function (key) { |
1332 | 0 | this.contextVars[key] = data.value.dalek[key]; |
1333 | }.bind(this)); | |
1334 | ||
1335 | 0 | data.value.test.forEach(function (test) { |
1336 | 0 | this.reporter.emit('report:assertion', { |
1337 | success: test.ok, | |
1338 | expected: true, | |
1339 | value: test.ok, | |
1340 | message: test.message, | |
1341 | type: 'OK' | |
1342 | }); | |
1343 | ||
1344 | 0 | this.incrementExpectations(); |
1345 | ||
1346 | 0 | if (!test.ok) { |
1347 | 0 | this.incrementFailedAssertions(); |
1348 | } | |
1349 | }.bind(this)); | |
1350 | ||
1351 | 0 | data.value = ''; |
1352 | } | |
1353 | ||
1354 | 0 | this.uuids[data.uuid] = true; |
1355 | 0 | reporter.emit('report:action', { |
1356 | value: data.value, | |
1357 | type: type, | |
1358 | uuid: data.uuid | |
1359 | }); | |
1360 | } | |
1361 | }.bind(this); | |
1362 | 1 | return cb; |
1363 | }; | |
1364 | ||
1365 | /** | |
1366 | * Adds a method to the queue of actions/assertions to execute | |
1367 | * | |
1368 | * @method _addToActionQueue | |
1369 | * @param {object} opts Options of the action to invoke | |
1370 | * @param {string} driverMethod Name of the method to call on the driver | |
1371 | * @param {function} A callback function that will be executed when the action has been executed | |
1372 | * @private | |
1373 | * @chainable | |
1374 | */ | |
1375 | ||
1376 | 1 | Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) { |
1377 | 4 | if (driverMethod !== 'screenshot' && driverMethod !== 'imagecut') { |
1378 | 0 | this.screenshotParams = undefined; |
1379 | } | |
1380 | ||
1381 | 4 | this.actionPromiseQueue.push(function () { |
1382 | 0 | var deferred = Q.defer(); |
1383 | // add a generic identifier as the last argument to any action method call | |
1384 | 0 | opts.push(uuid()); |
1385 | // check the method on the driver object && the callback function | |
1386 | 0 | if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') { |
1387 | // call the method on the driver object | |
1388 | 0 | this.driver[driverMethod].apply(this.driver, opts); |
1389 | 0 | deferred.resolve(); |
1390 | } else { | |
1391 | 0 | deferred.reject(); |
1392 | } | |
1393 | ||
1394 | // listen to driver message events & apply the callback argument | |
1395 | 0 | this.driver.events.on('driver:message', cb); |
1396 | 0 | return deferred.promise; |
1397 | }.bind(this)); | |
1398 | 4 | return this; |
1399 | }; | |
1400 | ||
1401 | 1 | Actions.prototype._button = function(button) { |
1402 | 0 | var buttons = {LEFT: 0, MIDDLE: 1, RIGHT: 2}; |
1403 | ||
1404 | 0 | if (button === undefined) { |
1405 | 0 | button = 0; |
1406 | 0 | } else if (typeof button !== 'number') { |
1407 | 0 | button = buttons[button.toUpperCase()] || 0; |
1408 | } | |
1409 | ||
1410 | 0 | return button; |
1411 | }; | |
1412 | ||
1413 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/click | |
1414 | 1 | Actions.prototype.buttonClick = function (button) { |
1415 | 0 | var hash = uuid(); |
1416 | 0 | button = this._button(button); |
1417 | ||
1418 | 0 | var cb = this._generateCallbackAssertion('buttonClick', 'buttonClick'); |
1419 | 0 | this._addToActionQueue([button, hash], 'buttonClick', cb); |
1420 | ||
1421 | 0 | return this; |
1422 | }; | |
1423 | ||
1424 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/moveto | |
1425 | 1 | Actions.prototype.moveTo = function (selector, x, y) { |
1426 | 0 | var hash = uuid(); |
1427 | ||
1428 | 0 | if (this.querying === true) { |
1429 | 0 | selector = this.selector; |
1430 | } | |
1431 | ||
1432 | 0 | if (x === undefined) { |
1433 | 0 | x = null; |
1434 | } | |
1435 | ||
1436 | 0 | if (y === undefined) { |
1437 | 0 | y = null; |
1438 | } | |
1439 | ||
1440 | // move to coordinate | |
1441 | 0 | var cb = this._generateCallbackAssertion('moveto', 'moveto'); |
1442 | 0 | this._addToActionQueue([selector, x, y, hash], 'moveto', cb); |
1443 | ||
1444 | 0 | return this; |
1445 | }; | |
1446 | ||
1447 | /** | |
1448 | * Close the active window and automatically selects the parent window. | |
1449 | * | |
1450 | * ```javascript | |
1451 | * this.test.toWindow('test'); | |
1452 | * this.test.close(); | |
1453 | * | |
1454 | * //you can now write your code as if the original parent window was selected because .close() | |
1455 | * //selects that automatically for you so you don't have to call .toParentWindow() everytime | |
1456 | * ``` | |
1457 | * | |
1458 | * @api | |
1459 | * @method close | |
1460 | * @chainable | |
1461 | */ | |
1462 | 1 | Actions.prototype.close = function () { |
1463 | 0 | var hash = uuid(); |
1464 | 0 | var cb = this._generateCallbackAssertion('close', 'close', hash); |
1465 | 0 | this._addToActionQueue([hash], 'close', cb); |
1466 | ||
1467 | //since the current window is now closed, make sense to automatically select the parent window since you would have to do this anyway | |
1468 | 0 | this.toParentWindow(); |
1469 | ||
1470 | 0 | return this; |
1471 | }; | |
1472 | ||
1473 | /** | |
1474 | * @module DalekJS | |
1475 | */ | |
1476 | ||
1477 | 1 | module.exports = function (opts) { |
1478 | 1 | reporter = opts.reporter; |
1479 | 1 | return Actions; |
1480 | }; | |
1481 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | */ | |
23 | ||
24 | 1 | 'use strict'; |
25 | ||
26 | // ext. libs | |
27 | 1 | var Q = require('q'); |
28 | 1 | var uuid = require('./uuid'); |
29 | 1 | var chai = require('chai'); |
30 | ||
31 | // Module variable | |
32 | 1 | var Assertions; |
33 | ||
34 | /** | |
35 | * @module Assertions | |
36 | * @namespace Dalek.Internal | |
37 | */ | |
38 | ||
39 | 1 | module.exports = function () { |
40 | 1 | return Assertions; |
41 | }; | |
42 | ||
43 | /** | |
44 | * Assertions check if the assumptions you made about a website are correct. | |
45 | * For example they might check if the title of a page or the content text of | |
46 | * an element is as expected, or if your mobile website version only displays | |
47 | * a certain amount of elements. | |
48 | * | |
49 | * @class Assertions | |
50 | * @constructor | |
51 | * @part Assertions | |
52 | * @api | |
53 | */ | |
54 | ||
55 | 1 | Assertions = function (opts) { |
56 | 0 | this.test = opts.test; |
57 | 0 | this.proceeded = []; |
58 | 0 | this.chaining = false; |
59 | }; | |
60 | ||
61 | /** | |
62 | * It can be really cumbersome to always write assert, assert & assert | |
63 | * all over the place when you're doing multiple assertions. | |
64 | * To avoid this, open an assertion context in your test which allows | |
65 | * you to write (n) assertions without having to write 'assert' before each. | |
66 | * | |
67 | * So, instead of writing this: | |
68 | * | |
69 | * ```javascript | |
70 | * test.open('http://doctorwhotv.co.uk/') | |
71 | * .assert.text('#nav').is('Navigation') | |
72 | * .assert.visible('#nav') | |
73 | * .assert.attr('#nav', 'data-nav', 'true') | |
74 | * .done(); | |
75 | * ``` | |
76 | * | |
77 | * you can write this: | |
78 | * | |
79 | * ```javascript | |
80 | * test.open('http://doctorwhotv.co.uk/') | |
81 | * .assert.chain() | |
82 | * .text('#nav').is('Navigation') | |
83 | * .visible('#nav') | |
84 | * .attr('#nav', 'data-nav', 'true') | |
85 | * .end() | |
86 | * .done(); | |
87 | * ``` | |
88 | * | |
89 | * to make it even more concise, you can combine this with the [query](actions.html#meth-query) method: | |
90 | * | |
91 | * ```javascript | |
92 | * test.open('http://doctorwhotv.co.uk/') | |
93 | * .assert.chain() | |
94 | * .query('#nav') | |
95 | * .text().is('Navigation') | |
96 | * .visible() | |
97 | * .attr('data-nav', 'true') | |
98 | * .end() | |
99 | * .end() | |
100 | * .done(); | |
101 | * ``` | |
102 | * | |
103 | * Always make sure to terminate it with the [end](#meth-end) method! | |
104 | * | |
105 | * @api | |
106 | * @method chain | |
107 | * @chainable | |
108 | */ | |
109 | ||
110 | 1 | Assertions.prototype.chain = function () { |
111 | 0 | this.test.lastChain.push('chaining'); |
112 | 0 | this.chaining = true; |
113 | 0 | return this; |
114 | }; | |
115 | ||
116 | /** | |
117 | * Terminates an assertion chain or a query | |
118 | * | |
119 | * ```javascript | |
120 | * test.open('http://doctorwhotv.co.uk/') | |
121 | * .assert.chain() | |
122 | * .query('#nav') | |
123 | * .text().is('Navigation') | |
124 | * .visible() | |
125 | * .attr('data-nav', 'true') | |
126 | * .end() | |
127 | * .end() | |
128 | * .done(); | |
129 | * ``` | |
130 | * | |
131 | * @api | |
132 | * @method end | |
133 | * @chainable | |
134 | */ | |
135 | ||
136 | 1 | Assertions.prototype.end = function () { |
137 | 0 | var lastAction = this.test.lastChain.pop(); |
138 | 0 | if (lastAction === 'chaining') { |
139 | 0 | this.chaining = false; |
140 | } | |
141 | ||
142 | 0 | if (lastAction === 'querying') { |
143 | 0 | this.test.querying = false; |
144 | } | |
145 | 0 | return this.test; |
146 | }; | |
147 | ||
148 | /** | |
149 | * Asserts that a given resource does exist in the environment. | |
150 | * | |
151 | * @method resourceExists | |
152 | * @param {string} url URL of the resource to check | |
153 | * @param {string} message Message for the test reporter | |
154 | * @chainable | |
155 | */ | |
156 | ||
157 | 1 | Assertions.prototype.resourceExists = function (url, message) { |
158 | 0 | var hash = uuid(); |
159 | 0 | var cb = this._generateCallbackAssertion('resourceExists', 'resourceExists', this._testTruthy, hash, {url: url, message: message}).bind(this.test); |
160 | 0 | this._addToActionQueue([url, hash], 'resourceExists', cb); |
161 | 0 | return this.chaining ? this : this.test; |
162 | }; | |
163 | ||
164 | /** | |
165 | * Asserts that a given element appears n times on the page. | |
166 | * | |
167 | * Given this portion of HTML, you would like to assure that all of these elements | |
168 | * are ending up in your rendered markup on your page. | |
169 | * | |
170 | * ```html | |
171 | * <section id="blog-overview"> | |
172 | * <article class="teaser"></article> | |
173 | * <article class="teaser"></article> | |
174 | * <article class="teaser"></article> | |
175 | * <article class="teaser"></article> | |
176 | * </section> | |
177 | * ``` | |
178 | * | |
179 | * The simple solution is to check if all these elements are present | |
180 | * | |
181 | * ```javascript | |
182 | * test.assert.numberOfElements('#blog-overview .teaser', 4, '4 blog teasers are present') | |
183 | * ``` | |
184 | * The alternate syntax for this is: | |
185 | * | |
186 | * ```javascript | |
187 | * test.assert.numberOfElements('#blog-overview .teaser') | |
188 | * .is(4, '4 blog teasers are present') | |
189 | * ``` | |
190 | * | |
191 | * If you are not sure how many elements will exactly end up in your markup, | |
192 | * you could use the between assertion handler | |
193 | * | |
194 | * ```javascript | |
195 | * test.assert.numberOfElements('#blog-overview .teaser') | |
196 | * .is.between([2, 6], 'Between 2 and 6 blog teasers are present') | |
197 | * ``` | |
198 | * | |
199 | * If you dealing with the situation that you have a minimum of elements, | |
200 | * you expect, you can use this helper: | |
201 | * | |
202 | * ```javascript | |
203 | * test.assert.numberOfElements('#blog-overview .teaser') | |
204 | * .is.gt(2, 'At least 3 blog teasers are present') | |
205 | * ``` | |
206 | * If you want to know if its 'greater than equal', use this one | |
207 | * | |
208 | * ```javascript | |
209 | * test.assert.numberOfElements('#blog-overview .teaser') | |
210 | * .is.gte(2, 'At least 2 blog teasers are present') | |
211 | * ``` | |
212 | * as well as their 'lower than' and 'lower than equal' equivalents. | |
213 | * | |
214 | * ```javascript | |
215 | * test.assert.numberOfElements('#blog-overview .teaser') | |
216 | * .is.lt(5, 'Less than 5 blog teasers are present') | |
217 | * ``` | |
218 | * | |
219 | * ```javascript | |
220 | * test.assert.numberOfElements('#blog-overview .teaser') | |
221 | * .is.lte(5, 'Less than, or 5 blog teasers are present') | |
222 | * ``` | |
223 | * And if you just want to know, if a certain amount of teasers isnʼt present, | |
224 | * you can still use the not() assertion helper | |
225 | * | |
226 | * ```javascript | |
227 | * test.assert.numberOfElements('#blog-overview .teaser') | |
228 | * .is.not(5, 'There are more or less than 5 teasers present') | |
229 | * ``` | |
230 | * | |
231 | * @api | |
232 | * @method numberOfElements | |
233 | * @param {string} selector Selector that matches the elements to test | |
234 | * @param {string} expected Expected test result | |
235 | * @param {string} message Message for the test reporter | |
236 | * @chainable | |
237 | */ | |
238 | ||
239 | 1 | Assertions.prototype.numberOfElements = function (selector, expected, message) { |
240 | 0 | var hash = uuid(); |
241 | ||
242 | 0 | if (this.test.querying === true) { |
243 | 0 | message = expected; |
244 | 0 | expected = selector; |
245 | 0 | selector = this.test.selector; |
246 | } | |
247 | ||
248 | 0 | var cb = this._generateCallbackAssertion('numberOfElements', 'numberOfElements', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
249 | 0 | this._addToActionQueue([selector, expected, hash], 'getNumberOfElements', cb); |
250 | 0 | return this.chaining ? this : this.test; |
251 | }; | |
252 | ||
253 | /** | |
254 | * | |
255 | * Asserts that a given element is visible n times in the current viewport. | |
256 | * | |
257 | * | |
258 | * Given this portion of HTML, you would like to assure that all of these elements | |
259 | * are ending up in your rendered markup on your page. | |
260 | * | |
261 | * ```html | |
262 | * <section id="blog-overview"> | |
263 | * <article class="teaser"></article> | |
264 | * <article class="teaser"></article> | |
265 | * <article class="teaser"></article> | |
266 | * <article class="teaser"></article> | |
267 | * </section> | |
268 | * ``` | |
269 | * | |
270 | * The simple solution is to check if all these elements are visible | |
271 | * | |
272 | * ```javascript | |
273 | * test.assert.numberOfVisibleElements('#blog-overview .teaser', 4, '4 blog teasers are visible') | |
274 | * ``` | |
275 | * The alternate syntax for this is: | |
276 | * | |
277 | * ```javascript | |
278 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
279 | * .is(4, '4 blog teasers are visible') | |
280 | * ``` | |
281 | * | |
282 | * If you are not sure how many elements will exactly be shown in the current viewport, | |
283 | * you could use the between assertion handler | |
284 | * | |
285 | * ```javascript | |
286 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
287 | * .is.between(2, 6, 'Between 2 and 6 blog teasers are visible') | |
288 | * ``` | |
289 | * | |
290 | * If you dealing with the situation that you have a minimum of elements, | |
291 | * you expect, use this helper: | |
292 | * | |
293 | * ```javascript | |
294 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
295 | * .is.gt(2, 'At least 3 blog teasers are visible') | |
296 | * ``` | |
297 | * If you want to know if its 'greater than equal', you can use this one | |
298 | * | |
299 | * ```javascript | |
300 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
301 | * .is.gte(2, 'At least 2 blog teasers are visible') | |
302 | * ``` | |
303 | * as well as their 'lower than' and 'lower than equal' equivalents. | |
304 | * | |
305 | * ```javascript | |
306 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
307 | * .is.lt(5, 'Less than 5 blog teasers are visible') | |
308 | * ``` | |
309 | * | |
310 | * ```javascript | |
311 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
312 | * .is.lte(5, 'Less than, or 5 blog teasers are visible') | |
313 | * ``` | |
314 | * And if you just want to know, if a certain amount of teasers isnʼt visible, | |
315 | * you can still use the ':not(): assertion helper | |
316 | * | |
317 | * ```javascript | |
318 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
319 | * .is.not(5, 'There are more or less than 5 teasers visible') | |
320 | * ``` | |
321 | * | |
322 | * > NOTE: Buggy on all browsers | |
323 | * | |
324 | * @api | |
325 | * @method numberOfVisibleElements | |
326 | * @param {string} selector Selector that matches the elements to test | |
327 | * @param {string} expected Expected test result | |
328 | * @param {string} message Message for the test reporter | |
329 | * @chainable | |
330 | */ | |
331 | ||
332 | 1 | Assertions.prototype.numberOfVisibleElements = function (selector, expected, message) { |
333 | 0 | var hash = uuid(); |
334 | ||
335 | 0 | if (this.test.querying === true) { |
336 | 0 | message = expected; |
337 | 0 | expected = selector; |
338 | 0 | selector = this.test.selector; |
339 | } | |
340 | ||
341 | 0 | var cb = this._generateCallbackAssertion('numberOfVisibleElements', 'numberOfVisibleElements', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
342 | 0 | this._addToActionQueue([selector, expected, hash], 'getNumberOfVisibleElements', cb); |
343 | 0 | return this.chaining ? this : this.test; |
344 | }; | |
345 | ||
346 | /** | |
347 | * Asserts that a given form field has the provided value. | |
348 | * | |
349 | * Given this portion of HTML, we would like to get the information which option element | |
350 | * is currently selected. | |
351 | * | |
352 | * ```html | |
353 | * <form name="fav-doctor" id="fav-doctor"> | |
354 | * <select id="the-doctors"> | |
355 | * <option value="9">Eccleston</option> | |
356 | * <option selected value="10">Tennant</option> | |
357 | * <option value="11">Smith</option> | |
358 | * </select> | |
359 | * </form> | |
360 | * ``` | |
361 | * | |
362 | * ```javascript | |
363 | * test | |
364 | * .assert.val('#the-doctors', 10, 'David is the favourite') | |
365 | * // lets change the favourite by selection the last option | |
366 | * .click('#the-doctors option:last') | |
367 | * .assert.val('#the-doctors', 11, 'Matt is now my favourite, bow ties are cool') | |
368 | * ``` | |
369 | * | |
370 | * This assertion is capable of getting the values from every form element | |
371 | * that holds a value attribute | |
372 | * | |
373 | * Getting texts out of normal input fields is pretty straight forward | |
374 | * | |
375 | * ```html | |
376 | * <label for="fav-enemy">Tell my your favourity Who enemy:</label> | |
377 | * <input id="fav-enemy" name="fav-enemy" type="text" value="Daleks" /> | |
378 | * ``` | |
379 | * | |
380 | * ```javascript | |
381 | * test | |
382 | * .assert.val('#fav-enemy', 'Daleks', 'Daleks are so cute') | |
383 | * // lets change the favourite by typing smth. new | |
384 | * .type('#fav-enemy', 'Cyberman') | |
385 | * .assert.val('#fav-enemy', 'Cyberman', 'Cyberman are so cyber') | |
386 | * ``` | |
387 | * | |
388 | * @api | |
389 | * @method val | |
390 | * @param {string} selector Selector that matches the elements to test | |
391 | * @param {string} expected Expected test result | |
392 | * @param {string} message Message for the test reporter | |
393 | * @chainable | |
394 | */ | |
395 | ||
396 | 1 | Assertions.prototype.val = function (selector, expected, message) { |
397 | 0 | var hash = uuid(); |
398 | ||
399 | 0 | if (this.test.querying === true) { |
400 | 0 | message = expected; |
401 | 0 | expected = selector; |
402 | 0 | selector = this.test.selector; |
403 | } | |
404 | ||
405 | 0 | var cb = this._generateCallbackAssertion('val', 'val', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
406 | 0 | this._addToActionQueue([selector, expected, hash], 'val', cb); |
407 | 0 | return this.chaining ? this : this.test; |
408 | }; | |
409 | ||
410 | /** | |
411 | * Checks the computed style. | |
412 | * Note: When selecting values like em or percent, they will be converted to px. So it's currently not possible | |
413 | * to check em, %, ... values, only px. | |
414 | * | |
415 | * ```html | |
416 | * <div id="superColoredElement">Rose</div> | |
417 | * ``` | |
418 | * | |
419 | * ```css | |
420 | * #superColoredElement { | |
421 | * background-color: rgba(255, 0, 0, 1); | |
422 | * color: rgba(0, 128, 0, 1); | |
423 | * } | |
424 | * ``` | |
425 | * | |
426 | * ```javascript | |
427 | * test | |
428 | * .open('http://unicorns.rainbows.io') | |
429 | * .assert.css('#superColoredElement', 'background-color', 'rgba(255, 0, 0, 1)') | |
430 | * .assert.css('#superColoredElement', 'color', 'rgba(0, 128, 0, 1)') | |
431 | * .done(); | |
432 | * ``` | |
433 | * | |
434 | * Can also check if a computed style is greater or lower than the expected value. | |
435 | * TODO: We might extract the part that determines the comparison operator to reuse it in other test. We might also | |
436 | * add >= and <=. | |
437 | * | |
438 | * ```html | |
439 | * <div id="fancyPlacedElement">Tulip</div> | |
440 | * ``` | |
441 | * | |
442 | * ```css | |
443 | * #fancyPlacedElement { | |
444 | * top: 100px; | |
445 | * } | |
446 | * ``` | |
447 | * | |
448 | * ```javascript | |
449 | * test | |
450 | * .open('http://unicorns.rainbows.io') | |
451 | * .assert.css('#fancyPlacedElement', 'top', '>50px') // 100 is greater than 50, success | |
452 | * .assert.css('#fancyPlacedElement', 'top', '<150px') // 100 is lower than 150, success | |
453 | * .assert.css('#fancyPlacedElement', 'top', '>150px') // 100 is lower than 150, fail | |
454 | * .assert.css('#fancyPlacedElement', 'top', '<50px') // 100 is greater than 50, fail | |
455 | * .done(); | |
456 | * ``` | |
457 | * | |
458 | * @api | |
459 | * @method css | |
460 | * @param {string} selector Selector that matches the elements to test | |
461 | * @param {string} property CSS property to check | |
462 | * @param {string} expected Expected test result | |
463 | * @param {string} message Message for the test reporter | |
464 | * @chainable | |
465 | */ | |
466 | ||
467 | 1 | Assertions.prototype.css = function (selector, property, expected, message) { |
468 | 0 | var hash = uuid(); |
469 | 0 | var assertionMethod; |
470 | 0 | var comparisonOperator; |
471 | ||
472 | 0 | if (this.test.querying === true) { |
473 | 0 | message = expected; |
474 | 0 | expected = property; |
475 | 0 | property = selector; |
476 | 0 | selector = this.test.selector; |
477 | } | |
478 | ||
479 | 0 | switch (expected.substr(0, 1)) { |
480 | case '>': | |
481 | 0 | assertionMethod = this._testLowerThan; |
482 | 0 | expected = expected.substr(1); |
483 | 0 | comparisonOperator = '>'; |
484 | 0 | break; |
485 | case '<': | |
486 | 0 | assertionMethod = this._testGreaterThan; |
487 | 0 | expected = expected.substr(1); |
488 | 0 | comparisonOperator = '<'; |
489 | 0 | break; |
490 | default: | |
491 | 0 | assertionMethod = this._testShallowEquals; |
492 | 0 | comparisonOperator = ''; |
493 | 0 | break; |
494 | } | |
495 | ||
496 | 0 | var cb = this._generateCallbackAssertion('css', 'css', assertionMethod, hash, {comparisonOperator: comparisonOperator, expected: expected, selector: selector, porperty: property, message: message, parseFloatOnValues: true}).bind(this.test); |
497 | 0 | this._addToActionQueue([selector, property, expected, hash], 'css', cb); |
498 | 0 | return this.chaining ? this : this.test; |
499 | }; | |
500 | ||
501 | /** | |
502 | * Checks the actual width of an element. | |
503 | * | |
504 | * ```html | |
505 | * <div id="fixed-dimensions" style="width: 100px"></div> | |
506 | * ``` | |
507 | * | |
508 | * ```javascript | |
509 | * test | |
510 | * .open('http://localhost:5000/index.html') | |
511 | * // all true, all pixel | |
512 | * .assert.width('#fixed-dimensions', 100) | |
513 | * .assert.width('#fixed-dimensions').is(100) | |
514 | * .assert.width('#fixed-dimensions').is.not(100) | |
515 | * .assert.width('#fixed-dimensions').is.gt(90) | |
516 | * .assert.width('#fixed-dimensions').is.gte(97) | |
517 | * .assert.width('#fixed-dimensions').is.lt(120) | |
518 | * .assert.width('#fixed-dimensions').is.lte(110) | |
519 | * .assert.width('#fixed-dimensions').is.between([90, 110]) | |
520 | * .done(); | |
521 | * ``` | |
522 | * | |
523 | * @api | |
524 | * @method width | |
525 | * @param {string} selector Selector that matches the elements to test | |
526 | * @param {string} expected Expected test result | |
527 | * @param {string} message Message for the test reporter | |
528 | * @chainable | |
529 | */ | |
530 | ||
531 | 1 | Assertions.prototype.width = function (selector, expected, message) { |
532 | 0 | var hash = uuid(); |
533 | ||
534 | 0 | if (this.test.querying === true) { |
535 | 0 | message = expected; |
536 | 0 | expected = selector; |
537 | 0 | selector = this.test.selector; |
538 | } | |
539 | ||
540 | 0 | var cb = this._generateCallbackAssertion('width', 'width', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
541 | 0 | this._addToActionQueue([selector, expected, hash], 'width', cb); |
542 | 0 | return this.chaining ? this : this.test; |
543 | }; | |
544 | ||
545 | /** | |
546 | * Checks the actual height of an element. | |
547 | * | |
548 | * ```html | |
549 | * <div id="fixed-dimensions" style="height: 100px"></div> | |
550 | * ``` | |
551 | * | |
552 | * ```javascript | |
553 | * test | |
554 | * .open('http://localhost:5000/index.html') | |
555 | * // all true, all pixel | |
556 | * .assert.height('#fixed-dimensions', 100) | |
557 | * .assert.height('#fixed-dimensions').is(100) | |
558 | * .assert.height('#fixed-dimensions').is.not(100) | |
559 | * .assert.height('#fixed-dimensions').is.gt(90) | |
560 | * .assert.height('#fixed-dimensions').is.gte(97) | |
561 | * .assert.height('#fixed-dimensions').is.lt(120) | |
562 | * .assert.height('#fixed-dimensions').is.lte(110) | |
563 | * .assert.height('#fixed-dimensions').is.between([90, 110]) | |
564 | * .done(); | |
565 | * ``` | |
566 | * | |
567 | * @api | |
568 | * @method height | |
569 | * @param {string} selector Selector that matches the elements to test | |
570 | * @param {string} expected Expected test result | |
571 | * @param {string} message Message for the test reporter | |
572 | * @chainable | |
573 | */ | |
574 | ||
575 | 1 | Assertions.prototype.height = function (selector, expected, message) { |
576 | 0 | var hash = uuid(); |
577 | ||
578 | 0 | if (this.test.querying === true) { |
579 | 0 | message = expected; |
580 | 0 | expected = selector; |
581 | 0 | selector = this.test.selector; |
582 | } | |
583 | ||
584 | 0 | var cb = this._generateCallbackAssertion('height', 'height', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
585 | 0 | this._addToActionQueue([selector, expected, hash], 'height', cb); |
586 | 0 | return this.chaining ? this : this.test; |
587 | }; | |
588 | ||
589 | /** | |
590 | * Determine if an <option> element, or an <input> element of type checkbox or radio is currently selected. | |
591 | * | |
592 | * ```html | |
593 | * <input type="checkbox" id="unchecked_checkbox" name="unchecked_checkbox"/> | |
594 | * <input type="checkbox" id="checked_checkbox" name="checked_checkbox" checked="checked"/> | |
595 | * <select id="select_elm" name="select_elm"> | |
596 | * <option value="9">Eccleston</option> | |
597 | * <option selected value="10">Tennant</option> | |
598 | * <option value="11">Smith</option> | |
599 | * </select> | |
600 | * ``` | |
601 | * | |
602 | * Checking radio and checkboxes: | |
603 | * | |
604 | * ```javascript | |
605 | * test | |
606 | * .open('http://selectables.org') | |
607 | * .assert.selected('#checked_checkbox') | |
608 | * .done(); | |
609 | * ``` | |
610 | * | |
611 | * Checking option elements: | |
612 | * | |
613 | * ```javascript | |
614 | * test | |
615 | * .open('http://selectables.org') | |
616 | * .assert.selected('#select_elm option:nth-child(2)') | |
617 | * .done(); | |
618 | * ``` | |
619 | * | |
620 | * @api | |
621 | * @method selected | |
622 | * @param {string} selector Selector that matches the elements to test | |
623 | * @param {string} message Message for the test reporter | |
624 | * @chainable | |
625 | */ | |
626 | ||
627 | 1 | Assertions.prototype.selected = function (selector, message) { |
628 | 0 | var hash = uuid(); |
629 | ||
630 | 0 | if (this.test.querying === true) { |
631 | 0 | message = selector; |
632 | 0 | selector = this.test.selector; |
633 | } | |
634 | ||
635 | 0 | var cb = this._generateCallbackAssertion('selected', 'selected', this._testShallowEquals, hash, {expected: true, selector: selector, message: message}).bind(this.test); |
636 | 0 | this._addToActionQueue([selector, true, hash], 'selected', cb); |
637 | 0 | return this.chaining ? this : this.test; |
638 | }; | |
639 | ||
640 | /** | |
641 | * Determine if an <option> element, or an <input> element of type | |
642 | * checkbox or radio is currently not selected. | |
643 | * | |
644 | * ```html | |
645 | * <input type="checkbox" id="unchecked_checkbox" name="unchecked_checkbox"/> | |
646 | * <input type="checkbox" id="checked_checkbox" name="checked_checkbox" checked="checked"/> | |
647 | * <select id="select_elm" name="select_elm"> | |
648 | * <option value="9">Eccleston</option> | |
649 | * <option selected value="10">Tennant</option> | |
650 | * <option value="11">Smith</option> | |
651 | * </select> | |
652 | * ``` | |
653 | * | |
654 | * Checking radio and checkboxes: | |
655 | * | |
656 | * ```javascript | |
657 | * test | |
658 | * .open('http://selectables.org') | |
659 | * .assert.notSelected('#unchecked_checkbox') | |
660 | * .done(); | |
661 | * ``` | |
662 | * | |
663 | * Checking option elements: | |
664 | * | |
665 | * ```javascript | |
666 | * test | |
667 | * .open('http://selectables.org') | |
668 | * .assert.notSelected('#select_elm option:last-child') | |
669 | * .done(); | |
670 | * ``` | |
671 | * | |
672 | * @api | |
673 | * @method notSelected | |
674 | * @param {string} selector Selector that matches the elements to test | |
675 | * @param {string} message Message for the test reporter | |
676 | * @chainable | |
677 | */ | |
678 | ||
679 | 1 | Assertions.prototype.notSelected = function (selector, message) { |
680 | 0 | var hash = uuid(); |
681 | ||
682 | 0 | if (this.test.querying === true) { |
683 | 0 | message = selector; |
684 | 0 | selector = this.test.selector; |
685 | } | |
686 | ||
687 | 0 | var cb = this._generateCallbackAssertion('selected', 'selected', this._testShallowEquals, hash, {expected: false, selector: selector, message: message}).bind(this.test); |
688 | 0 | this._addToActionQueue([selector, false, hash], 'selected', cb); |
689 | 0 | return this.chaining ? this : this.test; |
690 | }; | |
691 | ||
692 | /** | |
693 | * Determine if an element is currently enabled. | |
694 | * | |
695 | * ```html | |
696 | * <input id="onmars" type="text" size="50" name="onmars" placeholder="State your name, rank and intention!"></input> | |
697 | * ``` | |
698 | * | |
699 | * ```javascript | |
700 | * test | |
701 | * .open('http://doctor.thedoctor.com/doctor') | |
702 | * .assert.enabled('#onmars', 'Is enabled!') | |
703 | * .done(); | |
704 | * ``` | |
705 | * | |
706 | * @api | |
707 | * @method enabled | |
708 | * @param {string} selector Selector that matches the elements to test | |
709 | * @param {string} message Message for the test reporter | |
710 | * @chainable | |
711 | */ | |
712 | ||
713 | 1 | Assertions.prototype.enabled = function (selector, message) { |
714 | 0 | var hash = uuid(); |
715 | ||
716 | 0 | if (this.test.querying === true) { |
717 | 0 | message = selector; |
718 | 0 | selector = this.test.selector; |
719 | } | |
720 | ||
721 | 0 | var cb = this._generateCallbackAssertion('enabled', 'enabled', this._testShallowEquals, hash, {expected: true, selector: selector, message: message}).bind(this.test); |
722 | 0 | this._addToActionQueue([selector, true, hash], 'enabled', cb); |
723 | 0 | return this.chaining ? this : this.test; |
724 | }; | |
725 | ||
726 | /** | |
727 | * Determine if an element is currently disabled. | |
728 | * | |
729 | * ```html | |
730 | * <input disabled id="onearth" type="text" size="50" name="onearth" placeholder="State your name, rank and intention!"></input> | |
731 | * ``` | |
732 | * | |
733 | * ```javascript | |
734 | * test | |
735 | * .open('http://doctor.thedoctor.com/doctor') | |
736 | * .assert.disabled('#onearth', 'Is disabled!') | |
737 | * .done(); | |
738 | * ``` | |
739 | * | |
740 | * @api | |
741 | * @method disabled | |
742 | * @param {string} selector Selector that matches the elements to test | |
743 | * @param {string} message Message for the test reporter | |
744 | * @chainable | |
745 | */ | |
746 | ||
747 | 1 | Assertions.prototype.disabled = function (selector, message) { |
748 | 0 | var hash = uuid(); |
749 | ||
750 | 0 | if (this.test.querying === true) { |
751 | 0 | message = selector; |
752 | 0 | selector = this.test.selector; |
753 | } | |
754 | ||
755 | 0 | var cb = this._generateCallbackAssertion('enabled', 'enabled', this._testShallowEquals, hash, {expected: false, selector: selector, message: message}).bind(this.test); |
756 | 0 | this._addToActionQueue([selector, false, hash], 'enabled', cb); |
757 | 0 | return this.chaining ? this : this.test; |
758 | }; | |
759 | ||
760 | /** | |
761 | * Checks the contents of a cookie. | |
762 | * | |
763 | * ```javascript | |
764 | * test | |
765 | * .open('http://cookiejar.io/not_your_mothers_javascript.html') | |
766 | * .setCookie('atestcookie', 'foobar=baz') | |
767 | * .assert.cookie('atestcookie', 'foobar=baz') | |
768 | * .done(); | |
769 | * ``` | |
770 | * | |
771 | * @api | |
772 | * @method cookie | |
773 | * @param {string} name Name of the cookie | |
774 | * @param {string} expect Expected testresult | |
775 | * @param {string} message Message for the test reporter | |
776 | * @chainable | |
777 | */ | |
778 | ||
779 | 1 | Assertions.prototype.cookie = function (name, expected, message) { |
780 | 0 | var hash = uuid(); |
781 | 0 | var cb = this._generateCallbackAssertion('cookie', 'cookie', this._testShallowEquals, hash, {expected: expected, name: name, message: message}).bind(this.test); |
782 | 0 | this._addToActionQueue([name, expected, hash], 'cookie', cb); |
783 | 0 | return this.chaining ? this : this.test; |
784 | }; | |
785 | ||
786 | /** | |
787 | * Asserts that current HTTP status code is the same as the one passed as argument. | |
788 | * TODO: Needs some work to be implement (maybe JavaScript, Webdriver ha no method for this) | |
789 | * | |
790 | * @method httpStatus | |
791 | * @param {integer} status HTTP status code | |
792 | * @param {string} message Message for the test reporter | |
793 | * @chainable | |
794 | */ | |
795 | ||
796 | 1 | Assertions.prototype.httpStatus = function (status, message) { |
797 | 0 | var hash = uuid(); |
798 | 0 | var cb = this._generateCallbackAssertion('httpStatus', 'httpStatus', this._testShallowEquals, hash, {expected: status, message: message}).bind(this.test); |
799 | 0 | this._addToActionQueue([status, hash], 'httpStatus', cb); |
800 | 0 | return this.chaining ? this : this.test; |
801 | }; | |
802 | ||
803 | /** | |
804 | * Asserts that an element matching the provided selector expression exists in remote DOM environment. | |
805 | * | |
806 | * ```html | |
807 | * <body> | |
808 | * <p id="so-lonely">Last of the timelords</p> | |
809 | * </body> | |
810 | * ``` | |
811 | * | |
812 | * ```javascript | |
813 | * test | |
814 | * .open('http://doctor.thedoctor.com/doctor') | |
815 | * .assert.exists('#so-lonely', 'The loneliest element in the universe exists') | |
816 | * .done() | |
817 | * ``` | |
818 | * | |
819 | * @api | |
820 | * @method exists | |
821 | * @param {string} selector Selector that matches the elements to test | |
822 | * @param {string} message Message for the test reporter | |
823 | * @chainable | |
824 | */ | |
825 | ||
826 | 1 | Assertions.prototype.exists = function (selector, message) { |
827 | 0 | var hash = uuid(); |
828 | ||
829 | 0 | if (this.test.querying === true) { |
830 | 0 | message = selector; |
831 | 0 | selector = this.test.selector; |
832 | } | |
833 | ||
834 | 0 | var cb = this._generateCallbackAssertion('exists', 'exists', this._testTruthy, hash, {selector: selector, message: message}).bind(this.test); |
835 | 0 | this._addToActionQueue([selector, hash], 'exists', cb); |
836 | 0 | return this.chaining ? this : this.test; |
837 | }; | |
838 | ||
839 | /** | |
840 | * Asserts that an element matching the provided selector expression doesnʼt | |
841 | * exists within the remote DOM environment. | |
842 | * | |
843 | * ```html | |
844 | * <body> | |
845 | * <p id="so-lonely">Last of the time lords</p> | |
846 | * </body> | |
847 | * ``` | |
848 | * | |
849 | * ```javascript | |
850 | * test | |
851 | * .open('http://doctor.thedoctor.com/doctor') | |
852 | * .assert.doesntExist('#the-master', 'The master element has not been seen') | |
853 | * .done(); | |
854 | * ``` | |
855 | * | |
856 | * @api | |
857 | * @method doesntExist | |
858 | * @param {string} selector Selector that matches the elements to test | |
859 | * @param {string} message Message for the test reporter | |
860 | * @chainable | |
861 | */ | |
862 | ||
863 | 1 | Assertions.prototype.doesntExist = function (selector, message) { |
864 | 0 | var hash = uuid(); |
865 | ||
866 | 0 | if (this.test.querying === true) { |
867 | 0 | message = selector; |
868 | 0 | selector = this.test.selector; |
869 | } | |
870 | ||
871 | 0 | var cb = this._generateCallbackAssertion('exists', '!exists', this._testFalsy, hash, {selector: selector, message: message}).bind(this.test); |
872 | 0 | this._addToActionQueue([selector, hash], 'exists', cb); |
873 | 0 | return this.chaining ? this : this.test; |
874 | }; | |
875 | ||
876 | /** | |
877 | * Asserts that the element matching the provided selector expression is not visible. | |
878 | * | |
879 | * ```html | |
880 | * <body> | |
881 | * <h1 style="display: none">Me? So hidden …</h1> | |
882 | * <h2>Me? So in viewport...</h2> | |
883 | * </body> | |
884 | * ``` | |
885 | * | |
886 | * ```javascript | |
887 | * test | |
888 | * .open('http://allyourviewportsbelongto.us') | |
889 | * .assert.notVisible('h1', 'Element is not visible') | |
890 | * .done(); | |
891 | * ``` | |
892 | * | |
893 | * | |
894 | * > NOTE: Buggy on all browsers | |
895 | * | |
896 | * @api | |
897 | * @method notVisible | |
898 | * @param {string} selector Selector that matches the elements to test | |
899 | * @param {string} message Message for the test reporter | |
900 | * @chainable | |
901 | */ | |
902 | ||
903 | 1 | Assertions.prototype.notVisible = function (selector, message) { |
904 | 0 | var hash = uuid(); |
905 | ||
906 | 0 | if (this.test.querying === true) { |
907 | 0 | message = selector; |
908 | 0 | selector = this.test.selector; |
909 | } | |
910 | ||
911 | 0 | var cb = this._generateCallbackAssertion('visible', '!visible', this._testFalsy, hash, {selector: selector, message: message}).bind(this.test); |
912 | 0 | this._addToActionQueue([selector, hash], 'visible', cb); |
913 | 0 | return this.chaining ? this : this.test; |
914 | }; | |
915 | ||
916 | /** | |
917 | * Asserts that the element matching the provided selector expression is visible. | |
918 | * | |
919 | * ```html | |
920 | * <body> | |
921 | * <h1>Me? So in viewport …</h1> | |
922 | * </body> | |
923 | * ``` | |
924 | * | |
925 | * ```javascript | |
926 | * test | |
927 | * .open('http://allyourviewportsbelongto.us') | |
928 | * .assert.visible('h1', 'Element is visible') | |
929 | * .done(); | |
930 | * ``` | |
931 | * | |
932 | * > NOTE: Buggy on all browsers | |
933 | * | |
934 | * @api | |
935 | * @method visible | |
936 | * @param {string} selector Selector that matches the elements to test | |
937 | * @param {string} message Message for the test reporter | |
938 | * @chainable | |
939 | */ | |
940 | ||
941 | 1 | Assertions.prototype.visible = function (selector, message) { |
942 | 0 | var hash = uuid(); |
943 | ||
944 | 0 | if (this.test.querying === true) { |
945 | 0 | message = selector; |
946 | 0 | selector = this.test.selector; |
947 | } | |
948 | ||
949 | 0 | var cb = this._generateCallbackAssertion('visible', 'visible', this._testTruthy, hash, {selector: selector, message: message}).bind(this.test); |
950 | 0 | this._addToActionQueue([selector, hash], 'visible', cb); |
951 | 0 | return this.chaining ? this : this.test; |
952 | }; | |
953 | ||
954 | /** | |
955 | * Asserts that given text does not exist in the provided selector. | |
956 | * | |
957 | * ```html | |
958 | * <body> | |
959 | * <h1>This is a CasperJS sandbox</h1> | |
960 | * </body> | |
961 | * ``` | |
962 | * | |
963 | * ```javascript | |
964 | * test | |
965 | * .open('http://dalekjs.com/guineapig/') | |
966 | * .assert.doesntHaveText('h1', 'This page is a Dalek sandbox', 'It´s a sandbox!') | |
967 | * .done(); | |
968 | * ``` | |
969 | * | |
970 | * > NOTE: You cant match for a substring with contain() here. | |
971 | * | |
972 | * @api | |
973 | * @method doesntHaveText | |
974 | * @param {string} selector Selector that matches the elements to test | |
975 | * @param {string} expected Expected test result | |
976 | * @param {string} message Message for the test reporter | |
977 | * @chainable | |
978 | */ | |
979 | ||
980 | 1 | Assertions.prototype.doesntHaveText = function (selector, expected, message) { |
981 | 0 | var hash = uuid(); |
982 | 0 | if (this.test.querying === true) { |
983 | 0 | message = expected; |
984 | 0 | expected = selector; |
985 | 0 | selector = this.test.selector; |
986 | } | |
987 | ||
988 | 0 | var cb = this._generateCallbackAssertion('text', '!text', this._testShallowUnequals, hash, {selector: selector, expected: expected, message: message}).bind(this.test); |
989 | 0 | this._addToActionQueue([selector, expected, hash], 'text', cb); |
990 | 0 | return this.chaining ? this : this.test; |
991 | }; | |
992 | ||
993 | /** | |
994 | * Asserts that given text does not exist in the current alert/prompt/confirm dialog. | |
995 | * | |
996 | * ```html | |
997 | * <a href="#" id="alert_confirm" onclick="confirm('Confirm me!')">I make confirm</a> | |
998 | * ``` | |
999 | * | |
1000 | * ```javascript | |
1001 | * test | |
1002 | * .open('http://skaaro.com/index.html') | |
1003 | * .click('#alert_confirm') | |
1004 | * .assert.dialogDoesntHaveText('I am an alert') | |
1005 | * .accept() | |
1006 | * .done(); | |
1007 | * ``` | |
1008 | * | |
1009 | * > NOTE: You cant match for a substring with contain() here. | |
1010 | * > NOTE: Does not work in Firefox & PhantomJS | |
1011 | * | |
1012 | * @api | |
1013 | * @method dialogDoesntHaveText | |
1014 | * @param {string} expected Expected test result | |
1015 | * @param {string} message Message for the test reporter | |
1016 | * @chainable | |
1017 | */ | |
1018 | ||
1019 | 1 | Assertions.prototype.dialogDoesntHaveText = function (expected, message) { |
1020 | 0 | var hash = uuid(); |
1021 | 0 | var cb = this._generateCallbackAssertion('alertText', '!alertText', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
1022 | 0 | this._addToActionQueue([expected, hash], 'alertText', cb); |
1023 | 0 | return this.chaining ? this : this.test; |
1024 | }; | |
1025 | ||
1026 | /** | |
1027 | * Asserts that given text does exist in the provided selector. | |
1028 | * | |
1029 | * ```html | |
1030 | * <body> | |
1031 | * <h1>This is a Dalek sandbox</h1> | |
1032 | * </body> | |
1033 | * ``` | |
1034 | * | |
1035 | * ```javascript | |
1036 | * test | |
1037 | * .open('http://dalekjs.com/guineapig/') | |
1038 | * .assert.text('h1', 'This page is a Dalek sandbox', 'Exterminate!') | |
1039 | * .done(); | |
1040 | * ``` | |
1041 | * | |
1042 | * of course, text works also with the assertion helpers is() and not() | |
1043 | * | |
1044 | * ```javascript | |
1045 | * test | |
1046 | * .open('http://dalekjs.com/guineapig/') | |
1047 | * .assert.text('h1').is('This page is a Dalek sandbox', 'Exterminate!') | |
1048 | * .done(); | |
1049 | * ``` | |
1050 | * | |
1051 | * ```javascript | |
1052 | * test | |
1053 | * .open('http://dalekjs.com/guineapig/') | |
1054 | * .assert.text('h1').is.not('This page is a CasperJS sandbox', 'Exterminate!') | |
1055 | * .done(); | |
1056 | * ``` | |
1057 | * | |
1058 | * and you can also check for the occurrence of a substring with to.contain() (but don't try to chain it with not() as this is not possible) | |
1059 | * | |
1060 | * ```javascript | |
1061 | * test | |
1062 | * .open('http://dalekjs.com/guineapig/') | |
1063 | * .assert.text('h1').to.contain('CasperJS') | |
1064 | * .done(); | |
1065 | * ``` | |
1066 | * | |
1067 | * @api | |
1068 | * @method text | |
1069 | * @param {string} selector Selector that matches the elements to test | |
1070 | * @param {string} expected Expected test result | |
1071 | * @param {string} message Message for the test reporter | |
1072 | * @chainable | |
1073 | */ | |
1074 | ||
1075 | 1 | Assertions.prototype.text = function (selector, expected, message) { |
1076 | 0 | var hash = uuid(); |
1077 | 0 | if (this.test.querying === true) { |
1078 | 0 | message = expected; |
1079 | 0 | expected = selector; |
1080 | 0 | selector = this.test.selector; |
1081 | } | |
1082 | ||
1083 | 0 | var cb = this._generateCallbackAssertion('text', 'text', this._testShallowEquals, hash, {selector: selector, expected: expected, message: message}).bind(this.test); |
1084 | 0 | this._addToActionQueue([selector, expected, hash], 'text', cb); |
1085 | 0 | return (this.chaining || this.test.querying) ? this : this.test; |
1086 | }; | |
1087 | ||
1088 | /** | |
1089 | * Asserts that given alertText does exist in the provided alert/confirm or prompt dialog. | |
1090 | * | |
1091 | * ```html | |
1092 | * <a href="#" id="alert" onclick="alert('I am an alert')">I make alerts!</a> | |
1093 | * ``` | |
1094 | * | |
1095 | * ```javascript | |
1096 | * test | |
1097 | * .open('http://skaaro.com/index.html') | |
1098 | * .click('#alert_confirm') | |
1099 | * .assert.dialogText('I am an alert') | |
1100 | * .accept() | |
1101 | * .done(); | |
1102 | * ``` | |
1103 | * | |
1104 | * of course, text works also with the assertion helpers is() and not() | |
1105 | * | |
1106 | * ```javascript | |
1107 | * test | |
1108 | * .open('http://dalekjs.com/guineapig/') | |
1109 | * .assert.dialogText().is('I am an alert', 'Exterminate!') | |
1110 | * .done(); | |
1111 | * ``` | |
1112 | * | |
1113 | * ```javascript | |
1114 | * test | |
1115 | * .open('http://dalekjs.com/guineapig/') | |
1116 | * .assert.dialogText().is.not('I am an prompt', 'Exterminate!') | |
1117 | * .done(); | |
1118 | * ``` | |
1119 | * | |
1120 | * | |
1121 | * > NOTE: Does not work in Firefox & PhantomJS | |
1122 | * | |
1123 | * @api | |
1124 | * @method dialogText | |
1125 | * @param {string} expected Expected testresult | |
1126 | * @param {string} message Message for the test reporter | |
1127 | * @chainable | |
1128 | */ | |
1129 | ||
1130 | 1 | Assertions.prototype.dialogText = function (expected, message) { |
1131 | 0 | var hash = uuid(); |
1132 | 0 | var cb = this._generateCallbackAssertion('alertText', 'alertText', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
1133 | 0 | this._addToActionQueue([expected, hash], 'alertText', cb); |
1134 | 0 | return (this.chaining || this.test.querying) ? this : this.test; |
1135 | }; | |
1136 | ||
1137 | /** | |
1138 | * Asserts that the page title is as expected. | |
1139 | * | |
1140 | * ```javascript | |
1141 | * test.open('http://doctorwhotv.co.uk/') | |
1142 | * .assert.title('Doctor Who TV', 'Not your Daleks TV') | |
1143 | * .done(); | |
1144 | * ``` | |
1145 | * | |
1146 | * Yep, using assertion helpers is also possible: | |
1147 | * | |
1148 | * ```javascript | |
1149 | * test.open('http://doctorwhotv.co.uk/') | |
1150 | * .assert.title().is('Doctor Who TV', 'Not your Daleks TV') | |
1151 | * .done(); | |
1152 | * ``` | |
1153 | * | |
1154 | * the not() helper is available too: | |
1155 | * | |
1156 | * ```javascript | |
1157 | * test.open('http://doctorwhotv.co.uk/') | |
1158 | * .assert.title().is.not('Dalek Emperor TV', 'Not your Daleks TV') | |
1159 | * .done(); | |
1160 | * ``` | |
1161 | * | |
1162 | * and you can also match for a substring with to.contain() (but don't try to chain it with not() as this is not possible): | |
1163 | * | |
1164 | * ```javascript | |
1165 | * test.open('http://doctorwhotv.co.uk/') | |
1166 | * .assert.title().to.contain('Emperor') | |
1167 | * .done(); | |
1168 | * ``` | |
1169 | * | |
1170 | * @api | |
1171 | * @method title | |
1172 | * @param {string} expected Expected test result | |
1173 | * @param {string} message Message for the test reporter | |
1174 | * @chainable | |
1175 | */ | |
1176 | ||
1177 | 1 | Assertions.prototype.title = function (expected, message) { |
1178 | 0 | var hash = uuid(); |
1179 | 0 | var cb = this._generateCallbackAssertion('title', 'title', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
1180 | 0 | this._addToActionQueue([expected, hash], 'title', cb); |
1181 | 0 | return this.chaining ? this : this.test; |
1182 | }; | |
1183 | ||
1184 | /** | |
1185 | * Asserts that given title does not match the given expectations. | |
1186 | * | |
1187 | * ```javascript | |
1188 | * test.open('http://doctorwhotv.co.uk/') | |
1189 | * .assert.doesntHaveTitle('Dalek Emperor TV', 'Not your Daleks TV') | |
1190 | * .done(); | |
1191 | * ``` | |
1192 | * | |
1193 | * @api | |
1194 | * @method doesntHaveTitle | |
1195 | * @param {string} expected Expected test result | |
1196 | * @param {string} message Message for the test reporter | |
1197 | * @chainable | |
1198 | */ | |
1199 | ||
1200 | 1 | Assertions.prototype.doesntHaveTitle = function (expected, message) { |
1201 | 0 | var hash = uuid(); |
1202 | 0 | var cb = this._generateCallbackAssertion('title', '!title', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
1203 | 0 | this._addToActionQueue([expected, hash], 'title', cb); |
1204 | 0 | return this.chaining ? this : this.test; |
1205 | }; | |
1206 | ||
1207 | /** | |
1208 | * Asserts that screenshot is equal to already stored image. | |
1209 | * Should follow only after screenshot action | |
1210 | * | |
1211 | * ```javascript | |
1212 | * test.open('http://google.com') | |
1213 | * .wait(500) | |
1214 | * .screenshot('test/screenshots/google.png','#lga') | |
1215 | * .assert.screenshotIsEqualTo('test/screenshots/google_etalon.png', true, 'Google Doodles') | |
1216 | * .done(); | |
1217 | * ``` | |
1218 | * | |
1219 | * @api | |
1220 | * @method doesntHaveTitle | |
1221 | * @param {string} expected Path to expected png image | |
1222 | * @param {boolean} do make diff image | |
1223 | * @param {string} message Message for the test reporter | |
1224 | * @chainable | |
1225 | */ | |
1226 | 1 | Assertions.prototype.screenshotIsEqualTo = function (expected, makediff, message) { |
1227 | 0 | if (this.test.screenshotParams) { |
1228 | 0 | var hash = uuid(); |
1229 | ||
1230 | 0 | if (typeof makediff === 'string') { |
1231 | 0 | message = makediff; |
1232 | 0 | makediff = true; |
1233 | 0 | } else if (typeof makediff === 'undefined') { |
1234 | 0 | makediff = true; |
1235 | } | |
1236 | ||
1237 | 0 | var cb = this._generateCallbackAssertion('imagecompare', 'compare with etalon', this._testImagecompare, hash, { expected: expected, message: message, comparisonOperator: 'Screenshot is equal to ' }).bind(this.test); |
1238 | 0 | this._addToActionQueue([this.test.screenshotParams, expected, makediff, hash], 'imagecompare', cb); |
1239 | } else { | |
1240 | 0 | this.test.reporter.emit('error', 'Assert screenshotIsEqualTo can follow only after screenshot action!'); |
1241 | } | |
1242 | ||
1243 | 0 | return this.chaining ? this : this.test; |
1244 | }; | |
1245 | ||
1246 | /** | |
1247 | * Asserts that the page’s url is as expected. | |
1248 | * | |
1249 | * ```javascript | |
1250 | * test.open('http://doctorwhotv.co.uk/') | |
1251 | * .assert.url('http://doctorwhotv.co.uk/', 'URL is as expected') | |
1252 | * .done(); | |
1253 | * ``` | |
1254 | * | |
1255 | * You can also check if the protocol changed, | |
1256 | * nice to see when you open GitHub with 'http' instead of 'https' | |
1257 | * | |
1258 | * ```javascript | |
1259 | * test.open('http://github.com') | |
1260 | * .assert.url('https://github.com/', 'Changed prototcols') | |
1261 | * .done(); | |
1262 | * ``` | |
1263 | * | |
1264 | * Yep, using assertion helpers is also possible: | |
1265 | * | |
1266 | * ```javascript | |
1267 | * test.open('http://github.com') | |
1268 | * .assert.url().is('http://doctorwhotv.co.uk/', 'URL is as expected') | |
1269 | * .done(); | |
1270 | * ``` | |
1271 | * | |
1272 | * the not() helper is available too: | |
1273 | * | |
1274 | * ```javascript | |
1275 | * test.open('http://doctorwhotv.co.uk/') | |
1276 | * .assert.url().is.not('http://doctorwhotv.co.uk/', 'URL is as expected') | |
1277 | * .done(); | |
1278 | * ``` | |
1279 | * | |
1280 | * and you can also match for a substring with to.contain() (but don't try to chain it with not() as this is not possible): | |
1281 | * | |
1282 | * ```javascript | |
1283 | * test.open('http://doctorwhotv.co.uk/') | |
1284 | * .assert.url().to.contain('doctor') | |
1285 | * .done(); | |
1286 | * ``` | |
1287 | * | |
1288 | * @api | |
1289 | * @method url | |
1290 | * @param {string} expected Expected test result | |
1291 | * @param {string} message Message for the test reporter | |
1292 | * @chainable | |
1293 | */ | |
1294 | ||
1295 | 1 | Assertions.prototype.url = function (expected, message) { |
1296 | 0 | var hash = uuid(); |
1297 | 0 | var cb = this._generateCallbackAssertion('url', 'url', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
1298 | 0 | this._addToActionQueue([expected, hash], 'url', cb); |
1299 | 0 | return this.chaining ? this : this.test; |
1300 | }; | |
1301 | ||
1302 | /** | |
1303 | * Asserts that the pages URL does not match the expectation. | |
1304 | * | |
1305 | * ```javascript | |
1306 | * test.open('http://doctorwhotv.co.uk/') | |
1307 | * .assert.doesntHaveUrl('http://doctorwhotv.co.uk/', 'URL is not expected') | |
1308 | * .done(); | |
1309 | * ``` | |
1310 | * | |
1311 | * Oh, you might also match for a substring with to.contain(): | |
1312 | * | |
1313 | * * ```javascript | |
1314 | * test.open('http://doctorwhotv.co.uk/') | |
1315 | * .assert.doesntHaveUrl().to.contain('doctor') | |
1316 | * .done(); | |
1317 | * ``` | |
1318 | * | |
1319 | * @api | |
1320 | * @method doesntHaveUrl | |
1321 | * @param {string} expected Expected test result | |
1322 | * @param {string} message Message for the test reporter | |
1323 | * @chainable | |
1324 | */ | |
1325 | ||
1326 | 1 | Assertions.prototype.doesntHaveUrl = function (expected, message) { |
1327 | 0 | var hash = uuid(); |
1328 | 0 | var cb = this._generateCallbackAssertion('url', '!url', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
1329 | 0 | this._addToActionQueue([expected, hash], 'url', cb); |
1330 | 0 | return this.chaining ? this : this.test; |
1331 | }; | |
1332 | ||
1333 | /** | |
1334 | * Asserts that an elements attribute is as expected. | |
1335 | * | |
1336 | * ```html | |
1337 | * <form> | |
1338 | * <button class="jumpButton" type="submit">Fire</button> | |
1339 | * </form> | |
1340 | * ``` | |
1341 | * | |
1342 | * ```javascript | |
1343 | * test | |
1344 | * .open('http://dalekjs.com/guineapig/') | |
1345 | * .assert.attr('.jumpButton', 'type', 'submit') | |
1346 | * .done(); | |
1347 | * ``` | |
1348 | * | |
1349 | * ```html | |
1350 | * <div class="wellImUpperUpperClassHighSociety" id="dataDiv" data-spot="cat"></div> | |
1351 | * ``` | |
1352 | * | |
1353 | * ```javascript | |
1354 | * test | |
1355 | * .open('http://dalekjs.com/guineapig/') | |
1356 | * .assert.attr('#dataDiv').is('data-spot', 'cat', 'We found Dataʼs cat!') | |
1357 | * .done(); | |
1358 | * ``` | |
1359 | * | |
1360 | * ```javascript | |
1361 | * test | |
1362 | * .open('http://dalekjs.com/guineapig/') | |
1363 | * .assert.attr('#dataDiv').is.not('data-spot', 'doc', 'Spot is not a dog!') | |
1364 | * .done(); | |
1365 | * ``` | |
1366 | * | |
1367 | * You can also use attr() for checking if a class is existent | |
1368 | * | |
1369 | * ```javascript | |
1370 | * test | |
1371 | * .open('http://dalekjs.com/guineapig/') | |
1372 | * .assert.attr('#dataDiv', 'class', 'wellImUpperUpperClassHighSociety') | |
1373 | * .done(); | |
1374 | * ``` | |
1375 | * | |
1376 | * and you can also match a substring (e. g. a single class if more classes are on that elem) with to.contain(): | |
1377 | * | |
1378 | * ```javascript | |
1379 | * test | |
1380 | * .open('http://dalekjs.com/guineapig/') | |
1381 | * .assert.attr('#dataDiv', 'class').to.contain('upperUpperClass') | |
1382 | * .done(); | |
1383 | * ``` | |
1384 | * | |
1385 | * @api | |
1386 | * @method attr | |
1387 | * @param {string} selector Selector that matches the elements to test | |
1388 | * @param {string} attribute The attribute to test | |
1389 | * @param {string} expected Expected test result | |
1390 | * @param {string} message Message for the test reporter | |
1391 | * @chainable | |
1392 | */ | |
1393 | ||
1394 | 1 | Assertions.prototype.attr = function (selector, attribute, expected, message) { |
1395 | 0 | var hash = uuid(); |
1396 | ||
1397 | 0 | if (this.test.querying === true) { |
1398 | 0 | message = expected; |
1399 | 0 | expected = attribute; |
1400 | 0 | attribute = selector; |
1401 | 0 | selector = this.test.selector; |
1402 | } | |
1403 | ||
1404 | 0 | var cb = this._generateCallbackAssertion('attribute', 'attribute', this._testShallowEquals, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
1405 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
1406 | 0 | return this.chaining ? this : this.test; |
1407 | }; | |
1408 | ||
1409 | ||
1410 | /** | |
1411 | * Asserts that an element contains class. | |
1412 | * | |
1413 | * ```html | |
1414 | * <form> | |
1415 | * <button class="plain-btn action submit-product" type="submit">Fire</button> | |
1416 | * </form> | |
1417 | * ``` | |
1418 | * | |
1419 | * ```javascript | |
1420 | * test | |
1421 | * .open('http://dalekjs.com/index.html') | |
1422 | * .assert.hasClass('.grid__item.one-whole.info-box', 'info-box', 'div has info-box class') | |
1423 | * .done(); | |
1424 | * ``` | |
1425 | * | |
1426 | * @api | |
1427 | * @method attr | |
1428 | * @param {string} selector Selector that matches the elements to test | |
1429 | * @param {string} expected Expected test result | |
1430 | * @param {string} message Message for the test reporter | |
1431 | * @chainable | |
1432 | */ | |
1433 | ||
1434 | 1 | Assertions.prototype.hasClass = function (selector, expected, message) { |
1435 | 0 | var hash = uuid(); |
1436 | 0 | var attribute = 'class'; |
1437 | ||
1438 | 0 | if (this.test.querying === true) { |
1439 | 0 | message = expected; |
1440 | 0 | expected = selector; |
1441 | 0 | selector = this.test.selector; |
1442 | } | |
1443 | ||
1444 | 0 | var cb = this._generateCallbackAssertion('attribute', 'hasClass', this._containsItem, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
1445 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
1446 | 0 | return this.chaining ? this : this.test; |
1447 | }; | |
1448 | ||
1449 | ||
1450 | /** | |
1451 | * Asserts that element doesn't contain class. | |
1452 | * | |
1453 | * ```html | |
1454 | * <form> | |
1455 | * <button class="plain-btn action submit-product" type="submit">Fire</button> | |
1456 | * </form> | |
1457 | * ``` | |
1458 | * | |
1459 | * ```javascript | |
1460 | * test | |
1461 | * .open('http://dalekjs.com/index.html') | |
1462 | * .assert.doesntHaveClass('.grid__item.one-whole.info-box', 'info', 'div doesn\'t have info class') | |
1463 | * .done(); | |
1464 | * ``` | |
1465 | * | |
1466 | * @api | |
1467 | * @method attr | |
1468 | * @param {string} selector Selector that matches the elements to test | |
1469 | * @param {string} expected Expected test result | |
1470 | * @param {string} message Message for the test reporter | |
1471 | * @chainable | |
1472 | */ | |
1473 | ||
1474 | 1 | Assertions.prototype.doesntHaveClass = function (selector, expected, message) { |
1475 | 0 | var hash = uuid(); |
1476 | 0 | var attribute = 'class'; |
1477 | ||
1478 | 0 | if (this.test.querying === true) { |
1479 | 0 | message = expected; |
1480 | 0 | expected = selector; |
1481 | 0 | selector = this.test.selector; |
1482 | } | |
1483 | ||
1484 | 0 | var cb = this._generateCallbackAssertion('attribute', '!hasClass', this._doesntContainItem, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
1485 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
1486 | 0 | return this.chaining ? this : this.test; |
1487 | }; | |
1488 | ||
1489 | /** | |
1490 | Assert result of the execution of JavaScript function within the browser context. | |
1491 | ||
1492 | test | |
1493 | .open('http://dalekjs.com/index.html') | |
1494 | .assert.evaluate(function () { | |
1495 | return document.getElementsByClassName('grid').length; | |
1496 | }).is(2, 'Count of grid on page is equal 2') | |
1497 | .done(); | |
1498 | ||
1499 | * > Note: Buggy in Firefox | |
1500 | * | |
1501 | * @api | |
1502 | * @method execute | |
1503 | * @param {function} script JavaScript function that should be executed | |
1504 | * @return chainable | |
1505 | */ | |
1506 | ||
1507 | 1 | Assertions.prototype.evaluate = function (script) { |
1508 | 0 | var hash = uuid(); |
1509 | 0 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
1510 | ||
1511 | 0 | var cb = this._generateCallbackAssertion('evaluate', 'evaluate', this._testTruthy, hash, {script: script, args: args, has:hash}).bind(this.test); |
1512 | 0 | this._addToActionQueue([script, args, hash], 'evaluate', cb); |
1513 | 0 | return this.chaining ? this : this.test; |
1514 | }; | |
1515 | ||
1516 | ||
1517 | // TEST HELPER | |
1518 | // ----------- | |
1519 | ||
1520 | /** | |
1521 | * Is helper | |
1522 | * | |
1523 | * @method is | |
1524 | * @param {mixed} expected Value to check | |
1525 | * @param {string} message Test message | |
1526 | * @chainable | |
1527 | */ | |
1528 | ||
1529 | 1 | Assertions.prototype.is = function (expected, message) { |
1530 | 0 | return this.generateTestHelper('is', '_testShallowEquals', false)(expected, message); |
1531 | }; | |
1532 | ||
1533 | /** | |
1534 | * Not helper | |
1535 | * | |
1536 | * @method not | |
1537 | * @param {mixed} expected Value to check | |
1538 | * @param {string} message Test message | |
1539 | * @chainable | |
1540 | */ | |
1541 | ||
1542 | 1 | Assertions.prototype.not = function (expected, message) { |
1543 | 0 | return this.generateTestHelper('not', '_testShallowEquals', true)(expected, message); |
1544 | }; | |
1545 | ||
1546 | /** | |
1547 | * Between helper | |
1548 | * | |
1549 | * @method between | |
1550 | * @param {mixed} expected Value to check | |
1551 | * @param {string} message Test message | |
1552 | * @chainable | |
1553 | */ | |
1554 | ||
1555 | 1 | Assertions.prototype.between = function (expected, message) { |
1556 | 0 | return this.generateTestHelper('between', '_testBetween', false)(expected, message); |
1557 | }; | |
1558 | ||
1559 | /** | |
1560 | * Gt helper | |
1561 | * | |
1562 | * @method gt | |
1563 | * @param {mixed} expected Value to check | |
1564 | * @param {string} message Test message | |
1565 | * @chainable | |
1566 | */ | |
1567 | ||
1568 | 1 | Assertions.prototype.gt = function (expected, message) { |
1569 | 0 | return this.generateTestHelper('gt', '_testGreaterThan', false)(expected, message); |
1570 | }; | |
1571 | ||
1572 | /** | |
1573 | * Gte helper | |
1574 | * | |
1575 | * @method gte | |
1576 | * @param {mixed} expected Value to check | |
1577 | * @param {string} message Test message | |
1578 | * @chainable | |
1579 | */ | |
1580 | ||
1581 | 1 | Assertions.prototype.gte = function (expected, message) { |
1582 | 0 | return this.generateTestHelper('gte', '_testGreaterThanEqual', false)(expected, message); |
1583 | }; | |
1584 | ||
1585 | /** | |
1586 | * Lt helper | |
1587 | * | |
1588 | * @method lt | |
1589 | * @param {mixed} expected Value to check | |
1590 | * @param {string} message Test message | |
1591 | * @chainable | |
1592 | */ | |
1593 | ||
1594 | 1 | Assertions.prototype.lt = function (expected, message) { |
1595 | 0 | return this.generateTestHelper('lt', '_testLowerThan', false)(expected, message); |
1596 | }; | |
1597 | ||
1598 | /** | |
1599 | * Lte helper | |
1600 | * | |
1601 | * @method lte | |
1602 | * @param {mixed} expected Value to check | |
1603 | * @param {string} message Test message | |
1604 | * @chainable | |
1605 | */ | |
1606 | ||
1607 | 1 | Assertions.prototype.lte = function (expected, message) { |
1608 | 0 | return this.generateTestHelper('lte', '_testLowerThanEqual', false)(expected, message); |
1609 | }; | |
1610 | ||
1611 | /** | |
1612 | * Assert if a given value contians a second given value | |
1613 | * | |
1614 | * @method _containsItem | |
1615 | * @param {mixed} a Value to test | |
1616 | * @param {mixed} b Value to test | |
1617 | * @return {bool} false if values doesn't contain, true if contains | |
1618 | * @private | |
1619 | */ | |
1620 | ||
1621 | 1 | Assertions.prototype._containsItem = function (a, b) { |
1622 | 0 | a = a || ''; |
1623 | 0 | var index = a.split(' ').indexOf(b); |
1624 | ||
1625 | 0 | try { |
1626 | 0 | chai.expect(index).to.be.above(-1); |
1627 | } catch (e) { | |
1628 | 0 | return false; |
1629 | } | |
1630 | ||
1631 | 0 | return true; |
1632 | }; | |
1633 | ||
1634 | /** | |
1635 | * Assert if a given value doesn't contian a second given value | |
1636 | * | |
1637 | * @method _containsItem | |
1638 | * @param {mixed} a Value to test | |
1639 | * @param {mixed} b Value to test | |
1640 | * @return {bool} false if values contains, true if don't contain | |
1641 | * @private | |
1642 | */ | |
1643 | 1 | Assertions.prototype._doesntContainItem = function (a, b) { |
1644 | 0 | a = a || ''; |
1645 | 0 | var index = a.split(' ').indexOf(b); |
1646 | 0 | try { |
1647 | 0 | chai.assert.equal(index, -1); |
1648 | } catch (e) { | |
1649 | 0 | return false; |
1650 | } | |
1651 | ||
1652 | 0 | return true; |
1653 | }; | |
1654 | ||
1655 | /** | |
1656 | * Contain helper | |
1657 | * | |
1658 | * @method contain | |
1659 | * @param {mixed} expected Value to check | |
1660 | * @param {string} message Test message | |
1661 | * @chainable | |
1662 | */ | |
1663 | ||
1664 | 1 | Assertions.prototype.contain = function (expected, message) { |
1665 | 0 | return this.generateTestHelper('contain', '_contain', false)(expected, message); |
1666 | }; | |
1667 | ||
1668 | /** | |
1669 | * Not contain helper | |
1670 | * | |
1671 | * @method notContain | |
1672 | * @param {mixed} expected Value to check | |
1673 | * @param {string} message Test message | |
1674 | * @chainable | |
1675 | */ | |
1676 | ||
1677 | 1 | Assertions.prototype.notContain = function (expected, message) { |
1678 | 0 | return this.generateTestHelper('notContain', '_notContain', false)(expected, message); |
1679 | }; | |
1680 | ||
1681 | /** | |
1682 | * Match helper | |
1683 | * | |
1684 | * @method match | |
1685 | * @param {string} expected Regex to match on | |
1686 | * @param {string} message Test message | |
1687 | * @chainable | |
1688 | */ | |
1689 | ||
1690 | 1 | Assertions.prototype.match = function (expected, message) { |
1691 | 0 | return this.generateTestHelper('match', '_match', false)(expected, message); |
1692 | }; | |
1693 | ||
1694 | /** | |
1695 | * Equals case insensitive helper | |
1696 | * | |
1697 | * @method equalsCaseInsensitive | |
1698 | * @param {mixed} expected Value to check | |
1699 | * @param {string} message Test message | |
1700 | * @chainable | |
1701 | */ | |
1702 | ||
1703 | 1 | Assertions.prototype.equalsCaseInsensitive = function (expected, message) { |
1704 | 0 | return this.generateTestHelper('equalsCaseInsensitive', '_caseInsensitiveMatch', false)(expected, message); |
1705 | }; | |
1706 | ||
1707 | // HELPER METHODS | |
1708 | // -------------- | |
1709 | ||
1710 | /** | |
1711 | * Generates a callback that will be fired when the action has been completed. | |
1712 | * The callback itself will then validate the answer and will also emit an event | |
1713 | * that the action has been successfully executed. | |
1714 | * | |
1715 | * @method _generateCallbackAssertion | |
1716 | * @param {string} key Unique key of the action | |
1717 | * @param {string} type Type of the action (usually the actions name) | |
1718 | * @param {string} test test method to be used | |
1719 | * @param {string} hash the uuid | |
1720 | * @param {object} opts the options object | |
1721 | * @return {function} The generated callback function | |
1722 | * @private | |
1723 | */ | |
1724 | ||
1725 | 1 | Assertions.prototype._generateCallbackAssertion = function (key, type, test, hash, opts) { |
1726 | 0 | var cb = function (data) { |
1727 | 0 | if (data && data.key === key && data.hash === hash) { |
1728 | ||
1729 | 0 | this._lastGeneratedAction = {key: key, type: type, test: test, hash: hash, opts: opts, data: data}; |
1730 | ||
1731 | 0 | var chainingKeys = ['title', 'width', 'height', 'url', 'text' , 'attribute', 'numberOfElements', 'numberOfVisibleElements', 'evaluate']; |
1732 | 0 | if (!opts.expected && chainingKeys.indexOf(key) > -1) { |
1733 | 0 | return false; |
1734 | } | |
1735 | ||
1736 | 0 | if (typeof opts.expected === 'function') { |
1737 | 0 | opts.expected = opts.expected(); |
1738 | } | |
1739 | ||
1740 | 0 | var testResult = test(data.value, opts.expected, opts.parseFloatOnValues); |
1741 | 0 | this.reporter.emit('report:assertion', { |
1742 | success: testResult, | |
1743 | expected: opts.comparisonOperator ? opts.comparisonOperator + opts.expected : opts.expected, | |
1744 | value: data.value, | |
1745 | message: opts.message, | |
1746 | type: type, | |
1747 | selector: data.selector | |
1748 | }); | |
1749 | ||
1750 | 0 | this.incrementExpectations(); |
1751 | 0 | if (!testResult) { |
1752 | 0 | this.incrementFailedAssertions(); |
1753 | } | |
1754 | } | |
1755 | }; | |
1756 | 0 | return cb; |
1757 | }; | |
1758 | ||
1759 | /** | |
1760 | * Adds a method to the queue of actions/assertions to execute | |
1761 | * | |
1762 | * @method _addToActionQueue | |
1763 | * @param {object} opts Options of the action to invoke | |
1764 | * @param {string} driverMethod Name of the method to call on the driver | |
1765 | * @param {function} A callback function that will be executed when the action has been executed | |
1766 | * @private | |
1767 | * @chainable | |
1768 | */ | |
1769 | ||
1770 | 1 | Assertions.prototype._addToActionQueue = function (opts, driverMethod, cb) { |
1771 | 0 | this._lastGeneratedShit = {opts: opts, driverMethod: driverMethod}; |
1772 | 0 | this.test.actionPromiseQueue.push(function () { |
1773 | 0 | var deferredAction = Q.defer(); |
1774 | 0 | this.test.driver[driverMethod].apply(this.test.driver, opts); |
1775 | 0 | deferredAction.resolve(); |
1776 | 0 | this.test.driver.events.on('driver:message', cb); |
1777 | 0 | return deferredAction.promise; |
1778 | }.bind(this)); | |
1779 | 0 | return this; |
1780 | }; | |
1781 | ||
1782 | /** | |
1783 | * Generates a function that can be used | |
1784 | * | |
1785 | * @method generateTestHelper | |
1786 | * @param name | |
1787 | * @param assertionFn | |
1788 | * @param negate | |
1789 | * @return | |
1790 | * @private | |
1791 | */ | |
1792 | ||
1793 | 1 | Assertions.prototype.generateTestHelper = function (name, assertionFn, negate) { |
1794 | 0 | return function (expected, message) { |
1795 | 0 | var gen = this._lastGeneratedShit; |
1796 | ||
1797 | 0 | this.test.actionPromiseQueue.push(function () { |
1798 | 0 | var deferredAction = Q.defer(); |
1799 | 0 | deferredAction.resolve(); |
1800 | 0 | this.test.driver.events.on('driver:message', function () { |
1801 | ||
1802 | 0 | if (gen.opts && gen.opts[(gen.opts.length - 1)] && this.test._lastGeneratedAction && this.test._lastGeneratedAction.hash) { |
1803 | 0 | if (gen.opts[(gen.opts.length - 1)] === this.test._lastGeneratedAction.hash && !this.proceeded[this.test._lastGeneratedAction.hash + name]) { |
1804 | 0 | var testResult = this[assertionFn](expected, this.test._lastGeneratedAction.data.value); |
1805 | ||
1806 | 0 | if (negate) { |
1807 | 0 | testResult = !testResult; |
1808 | } | |
1809 | ||
1810 | 0 | this.proceeded[this.test._lastGeneratedAction.hash + name] = true; |
1811 | ||
1812 | 0 | this.test.reporter.emit('report:assertion', { |
1813 | success: testResult, | |
1814 | expected: expected, | |
1815 | value: this.test._lastGeneratedAction.data.value, | |
1816 | message: message, | |
1817 | type: this.test._lastGeneratedAction.type | |
1818 | }); | |
1819 | ||
1820 | 0 | this.test.incrementExpectations(); |
1821 | ||
1822 | 0 | if (!testResult) { |
1823 | 0 | this.test.incrementFailedAssertions(); |
1824 | } | |
1825 | } | |
1826 | } | |
1827 | }.bind(this)); | |
1828 | 0 | return deferredAction.promise; |
1829 | }.bind(this)); | |
1830 | ||
1831 | 0 | return this.chaining ? this : this.test; |
1832 | }.bind(this); | |
1833 | }; | |
1834 | ||
1835 | // ASSERT METHODS | |
1836 | // -------------- | |
1837 | ||
1838 | /** | |
1839 | * Assert if a given value shallow equals a second given value | |
1840 | * | |
1841 | * @method _testShallowEquals | |
1842 | * @param {mixed} a Value to test | |
1843 | * @param {mixed} b Value to test | |
1844 | * @return {bool} false if values donʼt match, true if they match | |
1845 | * @private | |
1846 | */ | |
1847 | ||
1848 | 1 | Assertions.prototype._testShallowEquals = function (a, b) { |
1849 | 0 | try { |
1850 | 0 | chai.assert.equal(a, b); |
1851 | } catch (e) { | |
1852 | 0 | return false; |
1853 | } | |
1854 | ||
1855 | 0 | return true; |
1856 | }; | |
1857 | ||
1858 | /** | |
1859 | * Assert if a given value shallow does not equal a second given value | |
1860 | * | |
1861 | * @method _testShallowUnequals | |
1862 | * @param {mixed} a Value to test | |
1863 | * @param {mixed} b Value to test | |
1864 | * @return {bool} true if values donʼt match, false if they match | |
1865 | * @private | |
1866 | */ | |
1867 | ||
1868 | 1 | Assertions.prototype._testShallowUnequals = function (a, b) { |
1869 | 0 | try { |
1870 | 0 | chai.assert.notEqual(a, b); |
1871 | } catch (e) { | |
1872 | 0 | return false; |
1873 | } | |
1874 | ||
1875 | 0 | return true; |
1876 | }; | |
1877 | ||
1878 | /** | |
1879 | * Assert if a given value matches a range | |
1880 | * | |
1881 | * @method _testBetween | |
1882 | * @param {array} a Range to test | |
1883 | * @param {bool} b Value to compare | |
1884 | * @return {bool} test result | |
1885 | * @private | |
1886 | */ | |
1887 | ||
1888 | 1 | Assertions.prototype._testBetween = function (a, b) { |
1889 | 0 | try { |
1890 | 0 | chai.expect(b).to.be.within(a[0], a[1]); |
1891 | } catch (e) { | |
1892 | 0 | return false; |
1893 | } | |
1894 | ||
1895 | 0 | return true; |
1896 | }; | |
1897 | ||
1898 | /** | |
1899 | * Assert if a given value is greater than the value to compare | |
1900 | * | |
1901 | * @method _testGreaterThan | |
1902 | * @param {bool} a Value to test | |
1903 | * @param {bool} b Value to compare | |
1904 | * @param {parseFloatOnValues} b Whether to apply parseFloat to both values prior comparision | |
1905 | * @return {bool} test result | |
1906 | * @private | |
1907 | */ | |
1908 | ||
1909 | 1 | Assertions.prototype._testGreaterThan = function (a, b, parseFloatOnValues) { |
1910 | 0 | if (parseFloatOnValues) { |
1911 | 0 | a = parseFloat(a); |
1912 | 0 | b = parseFloat(b); |
1913 | } | |
1914 | ||
1915 | 0 | try { |
1916 | 0 | chai.expect(b).to.be.above(a); |
1917 | } catch (e) { | |
1918 | 0 | return false; |
1919 | } | |
1920 | ||
1921 | 0 | return true; |
1922 | }; | |
1923 | ||
1924 | /** | |
1925 | * Assert if a given value is greater or equal than the value to compare | |
1926 | * | |
1927 | * @method _testGreaterThanEqual | |
1928 | * @param {bool} a Value to test | |
1929 | * @param {bool} b Value to compare | |
1930 | * @return {bool} test result | |
1931 | * @private | |
1932 | */ | |
1933 | ||
1934 | 1 | Assertions.prototype._testGreaterThanEqual = function (a, b) { |
1935 | 0 | return this._testGreaterThan(a - 1, b); |
1936 | }; | |
1937 | ||
1938 | /** | |
1939 | * Assert if a given value is lower than the value to compare | |
1940 | * | |
1941 | * @method _testLowerThan | |
1942 | * @param {bool} a Value to test | |
1943 | * @param {bool} b Value to compare | |
1944 | * @param {parseFloatOnValues} b Whether to apply parseFloatOnValues to both values prior comparision | |
1945 | * @return {bool} test result | |
1946 | * @private | |
1947 | */ | |
1948 | ||
1949 | 1 | Assertions.prototype._testLowerThan = function (a, b, parseFloatOnValues) { |
1950 | 0 | if (parseFloatOnValues) { |
1951 | 0 | a = parseFloat(a); |
1952 | 0 | b = parseFloat(b); |
1953 | } | |
1954 | ||
1955 | 0 | try { |
1956 | 0 | chai.expect(b).to.be.below(a); |
1957 | } catch (e) { | |
1958 | 0 | return false; |
1959 | } | |
1960 | ||
1961 | 0 | return true; |
1962 | }; | |
1963 | ||
1964 | /** | |
1965 | * Asserts that 2 string match regardless of case | |
1966 | * | |
1967 | * @method _caseInsensitiveMatch | |
1968 | * @param {string} a Value to test | |
1969 | * @param {string} b Value to compare | |
1970 | * @return {bool} testresult | |
1971 | * @private | |
1972 | */ | |
1973 | ||
1974 | 1 | Assertions.prototype._caseInsensitiveMatch = function (a, b) { |
1975 | 0 | try { |
1976 | 0 | if(a.toLowerCase && b.toLowerCase) { |
1977 | 0 | chai.expect(b.toLowerCase()).to.eql(a.toLowerCase()); |
1978 | } else { | |
1979 | 0 | return false; |
1980 | } | |
1981 | } catch (e) { | |
1982 | 0 | return false; |
1983 | } | |
1984 | ||
1985 | 0 | return true; |
1986 | }; | |
1987 | ||
1988 | /** | |
1989 | * Assert if a given value contain another value | |
1990 | * | |
1991 | * @method _contain | |
1992 | * @param {bool} a Value to test | |
1993 | * @param {bool} b Value to compare | |
1994 | * @return {bool} test result | |
1995 | * @private | |
1996 | */ | |
1997 | ||
1998 | 1 | Assertions.prototype._contain = function (a, b) { |
1999 | 0 | try { |
2000 | 0 | chai.expect(b).to.include(a); |
2001 | } catch (e) { | |
2002 | 0 | return false; |
2003 | } | |
2004 | ||
2005 | 0 | return true; |
2006 | }; | |
2007 | ||
2008 | /** | |
2009 | * Assert if a given value doesn't contain another value | |
2010 | * | |
2011 | * @method _notContain | |
2012 | * @param {bool} a Value to test | |
2013 | * @param {bool} b Value to compare | |
2014 | * @return {bool} test result | |
2015 | * @private | |
2016 | */ | |
2017 | 1 | Assertions.prototype._notContain = function (a, b) { |
2018 | 0 | try { |
2019 | 0 | chai.expect(b).to.not.include(a); |
2020 | } catch (e) { | |
2021 | 0 | return false; |
2022 | } | |
2023 | ||
2024 | 0 | return true; |
2025 | }; | |
2026 | ||
2027 | /** | |
2028 | * Assert if a given value is lower or equal than the value to compare | |
2029 | * | |
2030 | * @method _testLowerThanEqual | |
2031 | * @param {bool} a Value to test | |
2032 | * @param {bool} b Value to compare | |
2033 | * @return {bool} test result | |
2034 | * @private | |
2035 | */ | |
2036 | ||
2037 | 1 | Assertions.prototype._testLowerThanEqual = function (a, b) { |
2038 | 0 | return this._testLowerThan(a + 1, b); |
2039 | }; | |
2040 | ||
2041 | /** | |
2042 | * Assert if a given value is boolean 'true' | |
2043 | * | |
2044 | * @method _testTruthy | |
2045 | * @param {bool} a Value to test | |
2046 | * @return {bool} false if value is false, true if value is true | |
2047 | * @private | |
2048 | */ | |
2049 | ||
2050 | 1 | Assertions.prototype._testTruthy = function (a) { |
2051 | 0 | return a === 'true' || a === true; |
2052 | }; | |
2053 | ||
2054 | /** | |
2055 | * Assert if a given value is boolean 'false' | |
2056 | * | |
2057 | * @method _testFalsy | |
2058 | * @param {bool} a Value to test | |
2059 | * @return {bool} true if value is false, false if value is true | |
2060 | * @private | |
2061 | */ | |
2062 | ||
2063 | 1 | Assertions.prototype._testFalsy = function (a) { |
2064 | 0 | return a === 'false' || a === false; |
2065 | }; | |
2066 | ||
2067 | /** | |
2068 | * Assert a given value matches a RegEx | |
2069 | * | |
2070 | * @method _contain | |
2071 | * @param {mixed} a Value to test | |
2072 | * @param {string} b Value to compare | |
2073 | * @return {bool} test result | |
2074 | * @private | |
2075 | */ | |
2076 | ||
2077 | 1 | Assertions.prototype._match = function (a, b) { |
2078 | 0 | try { |
2079 | 0 | chai.expect(b).to.match(a); |
2080 | } catch (e) { | |
2081 | 0 | return false; |
2082 | } | |
2083 | ||
2084 | 0 | return true; |
2085 | }; | |
2086 | ||
2087 | ||
2088 | /** | |
2089 | * Assert if a image compare result is equal | |
2090 | * | |
2091 | * @method _testImagecompare | |
2092 | * @param {string} a Value to test if it equal to 'equal' | |
2093 | * @return {bool} true if value is 'equal', false if value has some other values | |
2094 | * @private | |
2095 | */ | |
2096 | ||
2097 | 1 | Assertions.prototype._testImagecompare = function (a) { |
2098 | 0 | return a === 'equal'; |
2099 | }; | |
2100 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // ext. libs | |
28 | 1 | var path = require('path'); |
29 | 1 | var fs = require('fs'); |
30 | 1 | var _ = require('lodash'); |
31 | 1 | var yaml = require('js-yaml'); |
32 | 1 | var JSON5 = require('json5'); |
33 | 1 | var glob = require('glob'); |
34 | 1 | require('coffee-script/register'); |
35 | ||
36 | /** | |
37 | * Configures the config instance | |
38 | * | |
39 | * @param {object} defaults Default parameter options | |
40 | * @param {object} opts Command line options | |
41 | * @constructor | |
42 | */ | |
43 | ||
44 | 1 | var Config = function (defaults, opts, advOpts) { |
45 | 20 | this.customFilename = null; |
46 | 20 | this.defaultFilename = 'Dalekfile'; |
47 | 20 | this.supportedExtensions = ['yml', 'json5', 'json', 'js', 'coffee']; |
48 | 20 | this.advancedOptions = advOpts; |
49 | 20 | this.config = this.load(defaults, opts.config, opts); |
50 | }; | |
51 | ||
52 | /** | |
53 | * Parses config data & loads config files for [DalekJS](//github.com/dalekjs/dalek) tests. | |
54 | * | |
55 | * This module is a driver plugin for [DalekJS](//github.com/dalekjs/dalek). | |
56 | * It connects Daleks testsuite with the remote testing environment of [Sauce Labs](https://saucelabs.com). | |
57 | * | |
58 | * The driver can be installed with the following command: | |
59 | * | |
60 | * ```bash | |
61 | * $ npm install dalek-driver-sauce --save-dev | |
62 | * ``` | |
63 | * | |
64 | * You can use the driver by adding a config option to the your [Dalekfile](/docs/config.html) | |
65 | * | |
66 | * ```javascript | |
67 | * "driver": ["sauce"] | |
68 | * ``` | |
69 | * | |
70 | * Or you can tell Dalek that it should run your tests via sauces service via the command line: | |
71 | * | |
72 | * ```bash | |
73 | * $ dalek mytest.js -d sauce | |
74 | * ``` | |
75 | * | |
76 | * In order to run your tests within the Sauce Labs infrastructure, you must add your sauce username & key | |
77 | * to your dalek configuration. Those two parameters must be set in order to get this driver up & running. | |
78 | * You can specifiy them within your [Dalekfile](/docs/config.html) like so: | |
79 | * | |
80 | * ```javascript | |
81 | * "driver.sauce": { | |
82 | * "user": "dalekjs", | |
83 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689" | |
84 | * } | |
85 | * ``` | |
86 | * | |
87 | * It is also possible to specify a set of other extra saucy parameters like `name` & `tags`: | |
88 | * | |
89 | * ```javascript | |
90 | * "driver.sauce": { | |
91 | * "user": "dalekjs", | |
92 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689", | |
93 | * "name": "Guineapig", | |
94 | * "tags": ["dalek", "testproject"] | |
95 | * } | |
96 | * ``` | |
97 | * | |
98 | * If you would like to have a more control over the browser/OS combinations that are available, you are able | |
99 | * to configure you custom combinations: | |
100 | * | |
101 | * ```javascript | |
102 | * "browsers": [{ | |
103 | * "chrome": { | |
104 | * "platform": "OS X 10.6", | |
105 | * "actAs": "chrome", | |
106 | * "version": 27 | |
107 | * }, | |
108 | * "chromeWin": { | |
109 | * "platform": "Windows 7", | |
110 | * "actAs": "chrome", | |
111 | * "version": 27 | |
112 | * }, | |
113 | * "chromeLinux": { | |
114 | * "platform": "Linux", | |
115 | * "actAs": "chrome", | |
116 | * "version": 26 | |
117 | * } | |
118 | * ``` | |
119 | * | |
120 | * You can then call your custom browsers like so: | |
121 | * | |
122 | * ```bash | |
123 | * $ dalek mytest.js -d sauce -b chrome,chromeWin,chromeLinux | |
124 | * ``` | |
125 | * | |
126 | * or you can define them in your Dalekfile: | |
127 | * | |
128 | * ```javascript | |
129 | * "browser": ["chrome", "chromeWin", "chromeLinux"] | |
130 | * ``` | |
131 | * | |
132 | * A list of all available browser/OS combinations, can be found [here](https://saucelabs.com/docs/platforms). | |
133 | * | |
134 | * @module DalekJS | |
135 | * @class Config | |
136 | * @namespace Dalek | |
137 | * @part Config | |
138 | * @api | |
139 | */ | |
140 | ||
141 | 1 | Config.prototype = { |
142 | ||
143 | /** | |
144 | * Checks if a config file is available | |
145 | * | |
146 | * @method checkAvailabilityOfConfigFile | |
147 | * @param {String} pathname | |
148 | * @return {String} config File path | |
149 | */ | |
150 | ||
151 | checkAvailabilityOfConfigFile: function (pathname) { | |
152 | // check if a pathname is given, | |
153 | // then check if the file is available | |
154 | 21 | if (pathname && fs.existsSync(pathname)) { |
155 | 1 | return fs.realpathSync(pathname); |
156 | } | |
157 | ||
158 | // check if any of the default configuration files is available | |
159 | 20 | return this.supportedExtensions.reduce(this._checkFile.bind(this)); |
160 | }, | |
161 | ||
162 | /** | |
163 | * Iterator function that checks the existance of a given file | |
164 | * | |
165 | * @method _checkFile | |
166 | * @param {String} previousValue Last iterations result | |
167 | * @param {String} ext File extension to check | |
168 | * @param {integer} idx Iteration index | |
169 | * @param {object} data File data | |
170 | * @return {String} config File path | |
171 | * @private | |
172 | */ | |
173 | ||
174 | _checkFile: function (previousValue, ext, idx, data) { | |
175 | 83 | if (previousValue.length > 6) { |
176 | 1 | return previousValue; |
177 | } | |
178 | ||
179 | 82 | var fileToCheck = this.defaultFilename + '.' + previousValue; |
180 | 82 | if (fs.existsSync(fileToCheck)) { |
181 | 1 | return fs.realpathSync(fileToCheck); |
182 | } | |
183 | ||
184 | 81 | return this._checkDefaultFile(ext, data); |
185 | }, | |
186 | ||
187 | /** | |
188 | * Iterator function that checks the existance of a the default file | |
189 | * | |
190 | * @method _checkDefaultFile | |
191 | * @param {String} ext File extension to check | |
192 | * @param {object} data File data | |
193 | * @return {String} config File path | |
194 | * @private | |
195 | */ | |
196 | ||
197 | _checkDefaultFile: function (ext, data) { | |
198 | 81 | if (ext === data[data.length - 1]) { |
199 | 21 | var fileToCheck = this.defaultFilename + '.' + ext; |
200 | 21 | if (fs.existsSync(fileToCheck)) { |
201 | 1 | return fs.realpathSync(fileToCheck); |
202 | } | |
203 | } | |
204 | ||
205 | 80 | return ext; |
206 | }, | |
207 | ||
208 | /** | |
209 | * Loads a file & merges the results with the | |
210 | * commandline options & the default config | |
211 | * | |
212 | * @method load | |
213 | * @param {object} defaults Default config | |
214 | * @param {String} pathname Filename of the config file to load | |
215 | * @param {object} opts Command line options | |
216 | * @return {object} config Merged config data | |
217 | */ | |
218 | ||
219 | load: function (defaults, pathname, opts) { | |
220 | 20 | var file = this.checkAvailabilityOfConfigFile(pathname); |
221 | 20 | var data = {}; |
222 | ||
223 | 20 | if (!this.advancedOptions || this.advancedOptions.dalekfile !== false) { |
224 | 20 | data = this.loadFile(file); |
225 | } | |
226 | ||
227 | // remove the tests property if the array length is 0 | |
228 | 20 | if (opts.tests.length === 0) { |
229 | 14 | delete opts.tests; |
230 | } | |
231 | ||
232 | 20 | if (data.tests && _.isArray(data.tests) && data.tests.length > 0) { |
233 | 0 | var tests = []; |
234 | ||
235 | //get all the files that match | |
236 | 0 | _.forEach(data.tests, function(search) { |
237 | 0 | tests = tests.concat(glob.sync(search)); |
238 | }); | |
239 | ||
240 | //remove duplicate files | |
241 | 0 | tests = tests.filter(function(elem, pos, self) { |
242 | 0 | return self.indexOf(elem) === pos; |
243 | }); | |
244 | ||
245 | 0 | data.tests = tests; |
246 | } | |
247 | ||
248 | 20 | return _.merge(defaults, data, opts, (this.advancedOptions || {})); |
249 | }, | |
250 | ||
251 | /** | |
252 | * Loads a config file & parses it based on the file extension | |
253 | * | |
254 | * @method loadFile | |
255 | * @param {String} pathname Filename of the config file to load | |
256 | * @return {object} data Config data | |
257 | */ | |
258 | ||
259 | loadFile: function (pathname) { | |
260 | 20 | var ext = path.extname(pathname).replace('.', ''); |
261 | 20 | return this['read' + ext] ? this['read' + ext](pathname) : {}; |
262 | }, | |
263 | ||
264 | /** | |
265 | * Fetches & returns a config item | |
266 | * | |
267 | * @method get | |
268 | * @param {String} item Key of the item to load | |
269 | * @return {mixed|null} data Requested config data | |
270 | */ | |
271 | ||
272 | get: function (item) { | |
273 | 50 | return this.config[item] || null; |
274 | }, | |
275 | ||
276 | /** | |
277 | * Loads a json config file | |
278 | * | |
279 | * @method readjson | |
280 | * @return {object} data Parsed config data | |
281 | */ | |
282 | ||
283 | readjson: function (pathname) { | |
284 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json'), 'utf8'); |
285 | 1 | return JSON.parse(contents); |
286 | }, | |
287 | ||
288 | /** | |
289 | * Loads a json5 config file | |
290 | * | |
291 | * @method readJson5 | |
292 | * @return {object} data Parsed config data | |
293 | */ | |
294 | ||
295 | readjson5: function (pathname) { | |
296 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json5'), 'utf8'); |
297 | 1 | return JSON5.parse(contents); |
298 | }, | |
299 | ||
300 | /** | |
301 | * Loads a yaml config file | |
302 | * | |
303 | * @method readyaml | |
304 | * @return {object} data Parsed config data | |
305 | */ | |
306 | ||
307 | readyml: function (pathname) { | |
308 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.yml'), 'utf8'); |
309 | 1 | return yaml.load(contents); |
310 | }, | |
311 | ||
312 | /** | |
313 | * Loads a javascript config file | |
314 | * | |
315 | * @method readjs | |
316 | * @return {object} data Parsed config data | |
317 | */ | |
318 | ||
319 | readjs: function (pathname) { | |
320 | 1 | return require((pathname || this.defaultFilename)); |
321 | }, | |
322 | ||
323 | /** | |
324 | * Loads a coffescript config file | |
325 | * | |
326 | * @method readcoffee | |
327 | * @return {object} data Parsed config data | |
328 | */ | |
329 | ||
330 | readcoffee: function (pathname) { | |
331 | 1 | return require((pathname || this.defaultFilename)); |
332 | }, | |
333 | ||
334 | /** | |
335 | * Verifies if a reporter is given, exists & is valid | |
336 | * | |
337 | * @method verifyReporters | |
338 | * @return {array} data List of verified reporters | |
339 | */ | |
340 | ||
341 | verifyReporters: function (reporters, reporter) { | |
342 | 8 | return _.compact(this._verify(reporters, 'isReporter', reporter)); |
343 | }, | |
344 | ||
345 | /** | |
346 | * Verifies if a driver is given, exists & is valid | |
347 | * | |
348 | * @method verifyDrivers | |
349 | * @return {array} data List of verified drivers | |
350 | */ | |
351 | ||
352 | verifyDrivers: function (drivers, driver) { | |
353 | 8 | return _.compact(this._verify(drivers, 'isDriver', driver)); |
354 | }, | |
355 | ||
356 | /** | |
357 | * Verifies if a driver is given, exists & is valid | |
358 | * | |
359 | * @method _verify | |
360 | * @param {array} check Data that should be mapped | |
361 | * @param {string} fn Name of the function that should be invoked on the veryify object | |
362 | * @param {object} instance Object instance where the verify function should be invoked | |
363 | * @return {array} data List of verified items | |
364 | * @private | |
365 | */ | |
366 | ||
367 | _verify: function (check, fn, instance) { | |
368 | 16 | return check.map(this._verifyIterator.bind(this, fn, instance)); |
369 | }, | |
370 | ||
371 | /** | |
372 | * Verifies if a driver is given, exists & is valid | |
373 | * | |
374 | * @method _verifyIterator | |
375 | * @param {string} fn Name of the function that should be invoked on the veryify object | |
376 | * @param {object} instance Object instance where the verify function should be invoked | |
377 | * @param {string} elm Name of the element that should be checked | |
378 | * @return {string|null} element name of the verified element or false if checked failed | |
379 | * @priavte | |
380 | */ | |
381 | ||
382 | _verifyIterator: function (fn, instance, elm) { | |
383 | 16 | return instance[fn](elm) ? elm : false; |
384 | } | |
385 | }; | |
386 | ||
387 | // export the module | |
388 | 1 | module.exports = Config; |
389 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // ext. libs | |
28 | 1 | var async = require('async'); |
29 | ||
30 | // int. libs | |
31 | 1 | var Suite = require('./suite'); |
32 | ||
33 | /** | |
34 | * Configures the driver instance | |
35 | * | |
36 | * @constructor | |
37 | */ | |
38 | ||
39 | 1 | var Driver = function (options) { |
40 | // add configuration data to the driver instance | |
41 | 7 | this.config = options.config; |
42 | 7 | this.browser = this.config.get('browser'); |
43 | 7 | this.files = this.config.get('tests'); |
44 | 7 | this.drivers = this.config.get('driver'); |
45 | ||
46 | // flag if we use the canary driver builds | |
47 | 7 | this.driverIsCanary = false; |
48 | ||
49 | // link driver events | |
50 | 7 | this.driverEmitter = options.driverEmitter; |
51 | 7 | this.reporterEvents = options.reporterEvents; |
52 | }; | |
53 | ||
54 | /** | |
55 | * Generates & starts drivers & browsers | |
56 | * the tests will be run in | |
57 | * | |
58 | * @module DalekJS | |
59 | * @class Driver | |
60 | * @namespace Dalek | |
61 | * @part Driver | |
62 | * @api | |
63 | */ | |
64 | ||
65 | 1 | Driver.prototype = { |
66 | ||
67 | /** | |
68 | * Checks if the requested driver is available | |
69 | * | |
70 | * @method isDriver | |
71 | * @param {string} driver Name of the requested driver | |
72 | * @return {bool} isDriver Driver is availavle | |
73 | */ | |
74 | ||
75 | isDriver: function (driver) { | |
76 | 7 | try { |
77 | 7 | require.resolve('dalek-driver-' + driver); |
78 | } catch (e) { | |
79 | 0 | try { |
80 | 0 | require.resolve('dalek-driver-' + driver + '-canary'); |
81 | } catch (e) { | |
82 | 0 | return false; |
83 | } | |
84 | 0 | this.driverIsCanary = true; |
85 | 0 | return true; |
86 | } | |
87 | 7 | return true; |
88 | }, | |
89 | ||
90 | /** | |
91 | * Loads the requested driver | |
92 | * Emits an event to the reporter | |
93 | * | |
94 | * @method loadDriver | |
95 | * @param {string} driver Name of the requested driver | |
96 | * @return {object} driverModule Instance of the driver module | |
97 | */ | |
98 | ||
99 | loadDriver: function (driver) { | |
100 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-internal-driver: Loading driver: "' + driver + '"'); |
101 | 0 | return require('dalek-driver-' + driver + (this.driverIsCanary ? '-canary' : '')); |
102 | }, | |
103 | ||
104 | /** | |
105 | * Returns a list with browser driver instances | |
106 | * | |
107 | * @method getDrivers | |
108 | * @return {array} verifiedDrivers | |
109 | */ | |
110 | ||
111 | getDrivers: function () { | |
112 | 0 | return this.drivers.map(this.getVerifiedBrowser, this)[0]; |
113 | }, | |
114 | ||
115 | /** | |
116 | * Returns a list with browser driver instances | |
117 | * | |
118 | * @method getVerifiedBrowser | |
119 | * @param {string} driver Name of the requested driver | |
120 | * @return {array} verifiedDrivers Array of dribver 'run' functions | |
121 | */ | |
122 | ||
123 | getVerifiedBrowser: function (driver) { | |
124 | 0 | return this.browser.map(this.getVerifiedDriver.bind(this, this.loadDriver(driver), driver)); |
125 | }, | |
126 | ||
127 | /** | |
128 | * Returns a scoped version of the driver run function | |
129 | * | |
130 | * @method getVerifiedDriver | |
131 | * @param {object} driverModule Instance of the used driver | |
132 | * @param {string} driver Name of ther used driver | |
133 | * @param {string} browser Name of the used browser | |
134 | * @return {function} run Function that kicks off execution of a testsuite chain in a browser | |
135 | */ | |
136 | ||
137 | getVerifiedDriver: function (driverModule, driver, browser) { | |
138 | 0 | return this.run.bind(this, driver, driverModule, browser); |
139 | }, | |
140 | ||
141 | /** | |
142 | * Loads a browser driver | |
143 | * | |
144 | * @method loadBrowserConfiguration | |
145 | * @param {string} browser Name of the requested browser driver | |
146 | * @param {object} browsers Configuration options for the requested browser | |
147 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
148 | */ | |
149 | ||
150 | loadBrowserConfiguration: function (browser, browsers, driver) { | |
151 | 0 | var browserConfiguration; |
152 | ||
153 | 0 | if (driver.dummyBrowser && driver.dummyBrowser()) { |
154 | 0 | return driver.getBrowser(driver); |
155 | } | |
156 | ||
157 | 0 | try { |
158 | 0 | browserConfiguration = this.getDefaultBrowserConfiguration(browser, browsers); |
159 | } catch (e) { | |
160 | 0 | browserConfiguration = this.getUserBrowserConfiguration(browser, browsers); |
161 | } | |
162 | ||
163 | 0 | return browserConfiguration; |
164 | }, | |
165 | ||
166 | /** | |
167 | * Loads the default browser driver | |
168 | * | |
169 | * @method getDefaultBrowserConfiguration | |
170 | * @param {string} browser Name of the requested browser driver | |
171 | * @param {object} browsers Configuration options for the requested browser | |
172 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
173 | */ | |
174 | ||
175 | getDefaultBrowserConfiguration: function (browser, browsers) { | |
176 | 0 | var browserConfiguration = {configuration: null, module: null}; |
177 | ||
178 | // set browser configuration | |
179 | 0 | if (browsers[browser]) { |
180 | 0 | browserConfiguration.configuration = browsers[browser]; |
181 | } | |
182 | ||
183 | // try to load `normal` browser modules first, | |
184 | // if that doesnt work, try canary builds | |
185 | 0 | try { |
186 | // check if the browser is a remote instance | |
187 | // else, try to load the local browser | |
188 | 0 | if (browserConfiguration.configuration && browserConfiguration.configuration.type === 'remote') { |
189 | 0 | browserConfiguration.module = require('./remote'); |
190 | } else { | |
191 | 0 | browserConfiguration.module = require('dalek-browser-' + browser); |
192 | } | |
193 | } catch (e) { | |
194 | 0 | browserConfiguration.module = require('dalek-browser-' + browser + '-canary'); |
195 | } | |
196 | ||
197 | 0 | return browserConfiguration; |
198 | }, | |
199 | ||
200 | /** | |
201 | * Loads a user configured browser driver | |
202 | * | |
203 | * @method getUserBrowserConfiguration | |
204 | * @param {string} browser Name of the requested browser driver | |
205 | * @param {object} browsers Configuration options for the requested browser | |
206 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
207 | */ | |
208 | ||
209 | getUserBrowserConfiguration: function (browser, browsers) { | |
210 | 0 | var browserConfiguration = {configuration: null, module: null}; |
211 | ||
212 | 0 | if (browsers && browsers[browser] && browsers[browser].actAs) { |
213 | 0 | browserConfiguration.module = require('dalek-browser-' + browsers[browser].actAs); |
214 | 0 | browserConfiguration.configuration = browsers[browser]; |
215 | } | |
216 | ||
217 | 0 | if (!browserConfiguration.module && browser.search(':') !== -1) { |
218 | 0 | var args = browser.split(':'); |
219 | 0 | var extractedBrowser = args[0].trim(); |
220 | 0 | var browserType = args[1].trim().toLowerCase(); |
221 | 0 | browserConfiguration.module = require('dalek-browser-' + extractedBrowser); |
222 | ||
223 | 0 | if (browserConfiguration.module && browserConfiguration.module.browserTypes && browserConfiguration.module.browserTypes[browserType]) { |
224 | 0 | var binary = (process.platform === 'win32' ? browserConfiguration.module.browserTypes[browserType].win32 : browserConfiguration.module.browserTypes[browserType].darwin); |
225 | 0 | browserConfiguration.configuration = { |
226 | binary: binary, | |
227 | type: browserType | |
228 | }; | |
229 | } | |
230 | } | |
231 | ||
232 | 0 | return browserConfiguration; |
233 | }, | |
234 | ||
235 | /** | |
236 | * Couple driver & session status events for the reporter | |
237 | * | |
238 | * @method coupleReporterEvents | |
239 | * @param {string} driverName Name of the requested driver | |
240 | * @param {string} browser Name of the requested browser | |
241 | * @chainable | |
242 | */ | |
243 | ||
244 | coupleReporterEvents: function (driverName, browser) { | |
245 | 0 | this.driverEmitter.on('driver:sessionStatus:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:session')); |
246 | 0 | this.driverEmitter.on('driver:status:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:status')); |
247 | 0 | return this; |
248 | }, | |
249 | ||
250 | /** | |
251 | * Returns a list of testsuite runner functions | |
252 | * | |
253 | * @method getTestsuiteInstances | |
254 | * @param {object} driverInstance Instance of the requested driver | |
255 | * @return {array} testsuiteRunners List of testsuites that should be run | |
256 | */ | |
257 | ||
258 | getTestsuiteInstances: function (driverInstance) { | |
259 | 0 | return this.files.map(this.createTestsuiteInstance.bind(this, driverInstance)); |
260 | }, | |
261 | ||
262 | /** | |
263 | * Creates a testsuite runner function | |
264 | * | |
265 | * @method createTestsuiteInstance | |
266 | * @param {object} driverInstance Instance of the requested driver | |
267 | * @param {string} file Filename of the testsuite | |
268 | * @return {function} testsuiteRunner Runner function from the testsuite | |
269 | */ | |
270 | ||
271 | createTestsuiteInstance: function (driverInstance, file) { | |
272 | 0 | var suite = new Suite({numberOfSuites: this.files.length, file: file, driver: driverInstance, driverEmitter: this.driverEmitter, reporterEmitter: this.reporterEvents}); |
273 | 0 | return suite.run.bind(suite); |
274 | }, | |
275 | ||
276 | /** | |
277 | * Generates a testsuite instance, emits the | |
278 | * browser running event & starts a new async() sesries execution | |
279 | * Will be called when the driver is ready | |
280 | * | |
281 | * @method _onDriverReady | |
282 | * @param {string} browser Name of the requested browser | |
283 | * @param {string} driverName Name of the requested driver | |
284 | * @param {function} callback Asyncs next() callback function | |
285 | * @param {object} driverInstance Instance of the requested driver | |
286 | * @chainable | |
287 | * @private | |
288 | */ | |
289 | ||
290 | _onDriverReady: function (browser, driverName, callback, driverInstance) { | |
291 | // generate testsuite instance from test files | |
292 | 0 | var testsuites = this.getTestsuiteInstances(driverInstance); |
293 | 0 | this.reporterEvents.emit('report:run:browser', driverInstance.webdriverClient.opts.longName); |
294 | 0 | async.series(testsuites, this._onTestsuiteComplete.bind(this, callback, driverName, browser)); |
295 | 0 | return this; |
296 | }, | |
297 | ||
298 | /** | |
299 | * Emits a 'tests complete' event & calls async's next() callback | |
300 | * | |
301 | * @method _onTestsuiteComplete | |
302 | * @param {function} callback Async's next() callback function | |
303 | * @param {string} driverName Name of the requested driver | |
304 | * @param {string} browser Name of the requested browser | |
305 | * @chainable | |
306 | * @private | |
307 | */ | |
308 | ||
309 | _onTestsuiteComplete: function (callback, driverName, browser) { | |
310 | 0 | this.driverEmitter.emit('tests:complete:' + driverName + ':' + browser); |
311 | 0 | callback(); |
312 | 0 | return this; |
313 | }, | |
314 | ||
315 | /** | |
316 | * Driver runner function. | |
317 | * Registers event handlers for this run, | |
318 | * loads browser & driver configuration & instances, | |
319 | * emits the 'driver ready' event for the browser/driver combination | |
320 | * | |
321 | * @method run | |
322 | * @param {string} driverName Name of the requested driver | |
323 | * @param {object} driverModule Instance of the used driver module | |
324 | * @param {string} browser Name of the requested browser | |
325 | * @param {function} callback Asyncs next() callback function | |
326 | * @chainable | |
327 | */ | |
328 | ||
329 | run: function (driverName, driverModule, browser, callback) { | |
330 | // load browser configuration | |
331 | 0 | var browsersRaw = this.config.get('browsers'); |
332 | 0 | var browsers = []; |
333 | ||
334 | // Check if we have a valid browser conf, then get the data out | |
335 | 0 | if (browsersRaw !== null) { |
336 | 0 | browsers = browsersRaw[0]; |
337 | } | |
338 | ||
339 | // init the browser configuration | |
340 | 0 | var browserConfiguration = this.loadBrowserConfiguration(browser, browsers, driverModule); |
341 | ||
342 | // check if we need to inject the browser alias into the browser module | |
343 | 0 | if (browserConfiguration.module.setBrowser) { |
344 | 0 | browserConfiguration.module.setBrowser(browser); |
345 | } | |
346 | ||
347 | // init the driver instance | |
348 | 0 | var driverInstance = driverModule.create({events: this.driverEmitter, reporter: this.reporterEvents, browser: browser, config: this.config, browserMo: browserConfiguration.module, browserConf: browserConfiguration.configuration}); |
349 | // couple driver & session status events for the reporter | |
350 | 0 | this.coupleReporterEvents(driverName, browser); |
351 | ||
352 | // register shutdown handler | |
353 | 0 | if (driverInstance.webdriverClient.opts && driverInstance.webdriverClient.opts.kill) { |
354 | 0 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.opts.kill.bind(driverInstance.webdriverClient.opts)); |
355 | } | |
356 | ||
357 | 0 | if (driverInstance.webdriverClient.quit) { |
358 | 0 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.quit.bind(driverInstance.webdriverClient)); |
359 | } | |
360 | ||
361 | // dispatch some (web)driver events to the reporter | |
362 | 0 | this.driverEmitter.on('driver:webdriver:response', function (res) { |
363 | 0 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.statusCode + ' ' + res.method + ' ' + res.path); |
364 | 0 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.data); |
365 | }.bind(this)); | |
366 | ||
367 | // run the tests in the browser, when the driver is ready | |
368 | // emit the tests:complete event, when all tests have been run | |
369 | 0 | this.driverEmitter.on('driver:ready:' + driverName + ':' + browser, this._onDriverReady.bind(this, browser, driverName, callback, driverInstance)); |
370 | 0 | return this; |
371 | } | |
372 | }; | |
373 | ||
374 | // export driver module | |
375 | 1 | module.exports = Driver; |
376 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // ext. libs | |
28 | 1 | var http = require('http'); |
29 | 1 | var os = require('os'); |
30 | 1 | var Q = require('q'); |
31 | ||
32 | /** | |
33 | * Sets the configuration options for the | |
34 | * dalek remote browser executor | |
35 | * | |
36 | * @param {options} opts Configuration options | |
37 | * @constructor | |
38 | */ | |
39 | ||
40 | 1 | var Host = function (opts) { |
41 | 0 | this.reporterEvents = opts.reporterEvents; |
42 | 0 | this.config = opts.config; |
43 | }; | |
44 | ||
45 | /** | |
46 | * Remote Dalek host proxy | |
47 | * | |
48 | * @module Dalek | |
49 | * @class Host | |
50 | * @part Remote | |
51 | * @api | |
52 | */ | |
53 | ||
54 | 1 | Host.prototype = { |
55 | ||
56 | /** | |
57 | * Default port that the Dalek remote server is linking against | |
58 | * | |
59 | * @property defaultPort | |
60 | * @type {integer} | |
61 | * @default 9020 | |
62 | */ | |
63 | ||
64 | defaultPort: 9020, | |
65 | ||
66 | /** | |
67 | * Instance of the local browser | |
68 | * | |
69 | * @property bro | |
70 | * @type {object} | |
71 | * @default null | |
72 | */ | |
73 | ||
74 | bro: null, | |
75 | ||
76 | /** | |
77 | * Instance of the reporter event emitter | |
78 | * | |
79 | * @property reporterEvents | |
80 | * @type {EventEmitter2} | |
81 | * @default null | |
82 | */ | |
83 | ||
84 | reporterEvents: null, | |
85 | ||
86 | /** | |
87 | * Instance of the dalek config | |
88 | * | |
89 | * @property config | |
90 | * @type {Dalek.Config} | |
91 | * @default null | |
92 | */ | |
93 | ||
94 | config: null, | |
95 | ||
96 | /** | |
97 | * Local configuration | |
98 | * | |
99 | * @property configuration | |
100 | * @type {object} | |
101 | * @default {} | |
102 | */ | |
103 | ||
104 | configuration: {}, | |
105 | ||
106 | /** | |
107 | * Host address of the called webdriver server | |
108 | * | |
109 | * @property remoteHost | |
110 | * @type {string} | |
111 | * @default null | |
112 | */ | |
113 | ||
114 | remoteHost: null, | |
115 | ||
116 | /** | |
117 | * Path of the webdriver server endpoint | |
118 | * | |
119 | * @property remotePath | |
120 | * @type {string} | |
121 | * @default null | |
122 | */ | |
123 | ||
124 | remotePath: null, | |
125 | ||
126 | /** | |
127 | * Port of the called webdriver server | |
128 | * | |
129 | * @property remotePort | |
130 | * @type {string} | |
131 | * @default null | |
132 | */ | |
133 | ||
134 | remotePort: null, | |
135 | ||
136 | /** | |
137 | * Secret that got emitted by the remote instance | |
138 | * | |
139 | * @property remoteSecret | |
140 | * @type {string} | |
141 | * @default null | |
142 | */ | |
143 | ||
144 | remoteSecret: null, | |
145 | ||
146 | /** | |
147 | * Identifier of the remote client | |
148 | * | |
149 | * @property remoteId | |
150 | * @type {string} | |
151 | * @default null | |
152 | */ | |
153 | ||
154 | remoteId: null, | |
155 | ||
156 | /** | |
157 | * Secret that is stored in the local instance | |
158 | * | |
159 | * @property secret | |
160 | * @type {string} | |
161 | * @default null | |
162 | */ | |
163 | ||
164 | secret: null, | |
165 | ||
166 | /** | |
167 | * Incoming message that needs to be proxied | |
168 | * to the local webdriver server | |
169 | * | |
170 | * @property proxyRequest | |
171 | * @type {http.IncomingMessage} | |
172 | * @default null | |
173 | */ | |
174 | ||
175 | proxyRequest: null, | |
176 | ||
177 | /** | |
178 | * Starts the remote proxy server, | |
179 | * prepares the config | |
180 | * | |
181 | * @method run | |
182 | * @param {object} opts Configuration options | |
183 | * @chainable | |
184 | */ | |
185 | ||
186 | run: function (opts) { | |
187 | // apply configuration | |
188 | 0 | this.configuration = this.config.get('host') || {}; |
189 | 0 | this.configuration.host = this.configuration.host ? !this.configuration.port : 'localhost'; |
190 | 0 | this.secret = this.configuration.secret ? this.configuration.secret : this.secret; |
191 | 0 | if (!this.configuration.port || opts.port) { |
192 | 0 | this.configuration.port = opts.port ? opts.port : this.defaultPort; |
193 | } | |
194 | ||
195 | // start the proxy server// emit the instance ready event | |
196 | 0 | this.server = http.createServer(this._createServer.bind(this)).listen(this.configuration.port, this.reporterEvents.emit.bind(this.reporterEvents, 'report:remote:ready', {ip: this._getLocalIp(), port: this.configuration.port})); |
197 | 0 | return this; |
198 | }, | |
199 | ||
200 | /** | |
201 | * Shutdown the proxy server | |
202 | * | |
203 | * @method kill | |
204 | * @return {object} Promise | |
205 | */ | |
206 | ||
207 | kill: function () { | |
208 | 0 | var deferred = Q.defer(); |
209 | 0 | this.server.close(deferred.resolve); |
210 | 0 | return deferred.promise; |
211 | }, | |
212 | ||
213 | /** | |
214 | * Launches the local browser | |
215 | * | |
216 | * @method _launcher | |
217 | * @param {object} request Request from the dalek remote caller | |
218 | * @param {object} response Response to the dalek remote caller | |
219 | * @private | |
220 | * @chainable | |
221 | */ | |
222 | ||
223 | _launcher: function (request, response) { | |
224 | // extract the browser id from the request url | |
225 | 0 | var browser = this._extractBrowser(request.url); |
226 | ||
227 | // load local browser module | |
228 | 0 | this.bro = this._loadBrowserModule(browser, response); |
229 | ||
230 | // launch the local browser | |
231 | 0 | if (this.bro) { |
232 | 0 | this.bro |
233 | .launch({}, this.reporterEvents, this.config) | |
234 | .then(this._onBrowserLaunch.bind(this, browser, response)); | |
235 | } | |
236 | ||
237 | 0 | return this; |
238 | }, | |
239 | ||
240 | /** | |
241 | * Shuts the local browser down, | |
242 | * end the otherwise hanging request | |
243 | * | |
244 | * @method _launcher | |
245 | * @param {object} response Response to the dalek remote caller | |
246 | * @private | |
247 | * @chainable | |
248 | */ | |
249 | ||
250 | _killer: function (response) { | |
251 | 0 | if (this.bro) { |
252 | 0 | this.bro.kill(); |
253 | } | |
254 | 0 | response.setHeader('Connection', 'close'); |
255 | 0 | response.end(); |
256 | 0 | this.reporterEvents.emit('report:remote:closed', {id: this.remoteId, browser: this.bro.longName}); |
257 | 0 | return this; |
258 | }, | |
259 | ||
260 | /** | |
261 | * Requires the local browser module & returns it | |
262 | * | |
263 | * @method _loadBrowserModule | |
264 | * @param {string} browser Name of the browser to load | |
265 | * @param {object} response Response to the dalek remote caller | |
266 | * @return {object} The local browser module | |
267 | * @private | |
268 | */ | |
269 | ||
270 | _loadBrowserModule: function (browser, response) { | |
271 | 0 | var bro = null; |
272 | 0 | try { |
273 | 0 | bro = require('dalek-browser-' + browser); |
274 | } catch (e) { | |
275 | 0 | try { |
276 | 0 | bro = require('dalek-browser-' + browser + '-canary'); |
277 | } catch (e) { | |
278 | 0 | response.setHeader('Connection', 'close'); |
279 | 0 | response.end(JSON.stringify({error: 'The requested browser "' + browser + '" could not be loaded'})); |
280 | } | |
281 | } | |
282 | ||
283 | 0 | return bro; |
284 | }, | |
285 | ||
286 | /** | |
287 | * Stores network data from the local browser instance, | |
288 | * sends browser specific data to the client | |
289 | * | |
290 | * @method _onBrowserLaunch | |
291 | * @param {string} browser Name of the browser to load | |
292 | * @param {object} response Response to the dalek remote caller | |
293 | * @chainable | |
294 | * @private | |
295 | */ | |
296 | ||
297 | _onBrowserLaunch: function (browser, response) { | |
298 | 0 | this.remoteHost = this.bro.getHost(); |
299 | 0 | this.remotePort = this.bro.getPort(); |
300 | 0 | this.remotePath = this.bro.path.replace('/', ''); |
301 | 0 | this.reporterEvents.emit('report:remote:established', {id: this.remoteId, browser: this.bro.longName}); |
302 | 0 | response.setHeader('Connection', 'close'); |
303 | 0 | response.end(JSON.stringify({browser: browser, caps: this.bro.desiredCapabilities, defaults: this.bro.driverDefaults, name: this.bro.longName})); |
304 | 0 | return this; |
305 | }, | |
306 | ||
307 | /** | |
308 | * Dispatches all incoming requests, | |
309 | * possible endpoints local webdriver server, | |
310 | * browser launcher, browser shutdown handler | |
311 | * | |
312 | * @method _createServer | |
313 | * @param {object} request Request from the dalek remote caller | |
314 | * @param {object} response Response to the dalek remote caller | |
315 | * @private | |
316 | * @chainable | |
317 | */ | |
318 | ||
319 | _createServer: function (request, response) { | |
320 | // delegate calls based on url | |
321 | 0 | if (request.url.search('/dalek/launch/') !== -1) { |
322 | ||
323 | // store the remotes ip address | |
324 | 0 | this.remoteId = request.connection.remoteAddress; |
325 | ||
326 | // store the remote secret | |
327 | 0 | if (request.headers['secret-token']) { |
328 | 0 | this.remoteSecret = request.headers['secret-token']; |
329 | } | |
330 | ||
331 | // check if the secrets match, then launch browser | |
332 | // else emit an error | |
333 | 0 | if (this.secret === this.remoteSecret) { |
334 | 0 | this._launcher(request, response); |
335 | } else { | |
336 | 0 | response.setHeader('Connection', 'close'); |
337 | 0 | response.end(JSON.stringify({error: 'Secrets do not match'})); |
338 | } | |
339 | ||
340 | 0 | } else if (request.url.search('/dalek/kill') !== -1) { |
341 | 0 | this._killer(response); |
342 | } else { | |
343 | 0 | this.proxyRequest = http.request(this._generateProxyRequestOptions(request.headers, request.method, request.url), this._onProxyRequest.bind(this, response, request)); |
344 | 0 | request.on('data', this._onRequestDataChunk.bind(this)); |
345 | 0 | request.on('end', this.proxyRequest.end.bind(this.proxyRequest)); |
346 | } | |
347 | ||
348 | 0 | return this; |
349 | }, | |
350 | ||
351 | /** | |
352 | * Proxies data from the local webdriver server to the client | |
353 | * | |
354 | * @method _onRequestDataChunk | |
355 | * @param {buffer} chunk Chunk of the incoming request data | |
356 | * @private | |
357 | * @chainable | |
358 | */ | |
359 | ||
360 | _onRequestDataChunk: function (chunk) { | |
361 | 0 | this.proxyRequest.write(chunk, 'binary'); |
362 | 0 | return this; |
363 | }, | |
364 | ||
365 | /** | |
366 | * Proxies remote data to the webdriver server | |
367 | * | |
368 | * @method _onProxyRequest | |
369 | * @param {object} request Request from the dalek remote caller | |
370 | * @param {object} response Response to the dalek remote caller | |
371 | * @param {object} res Response to the local webdriver server | |
372 | * @private | |
373 | * @chainable | |
374 | */ | |
375 | ||
376 | _onProxyRequest: function (response, request, res) { | |
377 | 0 | var chunks = []; |
378 | ||
379 | // deny access if the remote ids (onitial request, webdriver request) do not match | |
380 | 0 | if (this.remoteId !== request.connection.remoteAddress) { |
381 | 0 | response.setHeader('Connection', 'close'); |
382 | 0 | response.end(); |
383 | 0 | return this; |
384 | } | |
385 | ||
386 | 0 | res.on('data', function (chunk) { |
387 | 0 | chunks.push(chunk+''); |
388 | }).on('end', this._onProxyRequestEnd.bind(this, res, response, request, chunks)); | |
389 | 0 | return this; |
390 | }, | |
391 | ||
392 | /** | |
393 | * Handles data exchange between the client and the | |
394 | * local webdriver server | |
395 | * | |
396 | * @method _onProxyRequest | |
397 | * @param {object} request Request from the dalek remote caller | |
398 | * @param {object} response Response to the dalek remote caller | |
399 | * @param {object} res Response to the local webdriver server | |
400 | * @param {array} chunks Array of received data pieces that should be forwarded to the local webdriver server | |
401 | * @private | |
402 | * @chainable | |
403 | */ | |
404 | ||
405 | _onProxyRequestEnd: function (res, response, request, chunks) { | |
406 | 0 | var buf = ''; |
407 | ||
408 | // proxy headers for the session request | |
409 | 0 | if (request.url === '/session') { |
410 | 0 | response.setHeader('Connection', 'close'); |
411 | 0 | Object.keys(res.headers).forEach(function (key) { |
412 | 0 | response.setHeader(key, res.headers[key]); |
413 | }); | |
414 | } | |
415 | ||
416 | 0 | if (chunks.length) { |
417 | 0 | buf = chunks.join(''); |
418 | } | |
419 | ||
420 | 0 | response.write(buf); |
421 | 0 | response.end(); |
422 | 0 | return this; |
423 | }, | |
424 | ||
425 | /** | |
426 | * Extracts the browser that should be launched | |
427 | * from the launch url request | |
428 | * | |
429 | * @method _extractBrowser | |
430 | * @param {string} url Url to parse | |
431 | * @return {string} Extracted browser | |
432 | * @private | |
433 | */ | |
434 | ||
435 | _extractBrowser: function (url) { | |
436 | 0 | return url.replace('/dalek/launch/', ''); |
437 | }, | |
438 | ||
439 | /** | |
440 | * Generates the request options from the incoming | |
441 | * request that should then be forwared to the local | |
442 | * webdriver server | |
443 | * | |
444 | * @method _generateProxyRequestOptions | |
445 | * @param {object} header Header meta data | |
446 | * @param {string} method HTTP method | |
447 | * @param {string} url Webriver server endpoint url | |
448 | * @return {object} Request options | |
449 | * @private | |
450 | */ | |
451 | ||
452 | _generateProxyRequestOptions: function (headers, method, url) { | |
453 | 0 | var options = { |
454 | host: this.remoteHost, | |
455 | port: this.remotePort, | |
456 | path: this.remotePath + url, | |
457 | method: method, | |
458 | headers: { | |
459 | 'Content-Type': headers['content-type'], | |
460 | 'Content-Length': headers['content-length'] | |
461 | } | |
462 | }; | |
463 | ||
464 | // check if the path is valid, | |
465 | // else prepend a `root` slash | |
466 | 0 | if (options.path.charAt(0) !== '/') { |
467 | 0 | options.path = '/' + options.path; |
468 | } | |
469 | ||
470 | 0 | return options; |
471 | }, | |
472 | ||
473 | /** | |
474 | * Gets the local ip address | |
475 | * (should be the IPv4 address where the runner is accessible from) | |
476 | * | |
477 | * @method _getLocalIp | |
478 | * @return {string} Local IP address | |
479 | * @private | |
480 | */ | |
481 | ||
482 | _getLocalIp: function () { | |
483 | 0 | var ifaces = os.networkInterfaces(); |
484 | 0 | var address = [null]; |
485 | 0 | for (var dev in ifaces) { |
486 | 0 | var alias = [0]; |
487 | 0 | ifaces[dev].forEach(this._grepIp.bind(this, alias, address)); |
488 | } | |
489 | ||
490 | 0 | return address[0]; |
491 | }, | |
492 | ||
493 | /** | |
494 | * Tries to find the local IP address | |
495 | * | |
496 | * @method _grepIp | |
497 | * @param | |
498 | * @param | |
499 | * @param | |
500 | * @chainable | |
501 | * @private | |
502 | */ | |
503 | ||
504 | _grepIp: function (alias, address, details) { | |
505 | 0 | if (details.family === 'IPv4') { |
506 | 0 | if (details.address !== '127.0.0.1') { |
507 | 0 | address[0] = details.address; |
508 | } | |
509 | 0 | ++alias[0]; |
510 | } | |
511 | ||
512 | 0 | return this; |
513 | } | |
514 | ||
515 | }; | |
516 | ||
517 | 1 | module.exports = Host; |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | /** | |
28 | * Checks & loads reporter modules | |
29 | * | |
30 | * @module DalekJS | |
31 | * @class Reporter | |
32 | * @namespace Dalek | |
33 | * @part Reporter | |
34 | * @api | |
35 | */ | |
36 | ||
37 | 1 | var Reporter = { |
38 | ||
39 | /** | |
40 | * Reporters from the canary channel | |
41 | * | |
42 | * @param isCanary | |
43 | */ | |
44 | ||
45 | isCanary: {}, | |
46 | ||
47 | /** | |
48 | * Checks if the requested reporter exists | |
49 | * | |
50 | * @method isReporter | |
51 | * @param {string} reporter Name of the reporter | |
52 | * @return {bool} isReporter Reporter exists | |
53 | */ | |
54 | ||
55 | isReporter: function (reporter) { | |
56 | 7 | try { |
57 | 7 | require.resolve('dalek-reporter-' + reporter); |
58 | } catch (e) { | |
59 | 0 | try { |
60 | 0 | require.resolve('dalek-reporter-' + reporter + '-canary'); |
61 | } catch (e) { | |
62 | 0 | return false; |
63 | } | |
64 | ||
65 | 0 | this.isCanary[reporter] = true; |
66 | 0 | return true; |
67 | } | |
68 | 7 | return true; |
69 | }, | |
70 | ||
71 | /** | |
72 | * Loads a requested reporter | |
73 | * | |
74 | * @method loadReporter | |
75 | * @param {string} reporter Name of the reporter | |
76 | * @param {object} options Options to pass to the reporter | |
77 | * @return {object} reporterInstance Reporter instance | |
78 | */ | |
79 | ||
80 | loadReporter: function (reporter, options) { | |
81 | 7 | return require('dalek-reporter-' + reporter + (this.isCanary[reporter] ? '-canary' : ''))(options); |
82 | } | |
83 | }; | |
84 | ||
85 | // export the module | |
86 | 1 | module.exports = Reporter; |
87 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // ext. libs | |
28 | 1 | var _ = require('lodash'); |
29 | 1 | var fs = require('fs'); |
30 | 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2; |
31 | ||
32 | // int. libs | |
33 | 1 | var unit = require('./unit'); |
34 | ||
35 | /** | |
36 | * @constructor | |
37 | * @param {object} options | |
38 | */ | |
39 | ||
40 | 1 | var Suite = function (options) { |
41 | 0 | this.emitter = new EventEmitter2(); |
42 | 0 | this.emitter.setMaxListeners(Infinity); |
43 | 0 | this.initialize(options); |
44 | 0 | this.suite = this.loadTestsuite(options.file); |
45 | }; | |
46 | ||
47 | /** | |
48 | * Suite (Testsuite) | |
49 | * | |
50 | * @module DalekJS | |
51 | * @class Suite | |
52 | * @namespace Dalek | |
53 | * @part Testsuite | |
54 | * @api | |
55 | */ | |
56 | ||
57 | 1 | Suite.prototype = { |
58 | ||
59 | /** | |
60 | * Assigns the initial options | |
61 | * driverEmitter -> the drivers event dispatcher | |
62 | * reporterEmitter -> the reporters event dispatcher | |
63 | * driver -> the driver instance (e.g. native webdriver, selenium, etc.) | |
64 | * name -> the suites filename (default suite name) | |
65 | * | |
66 | * @method initialize | |
67 | * @param {object} options | |
68 | * @chainable | |
69 | */ | |
70 | ||
71 | initialize: function (options) { | |
72 | 0 | this.driverEmitter = options.driverEmitter; |
73 | 0 | this.reporterEmitter = options.reporterEmitter; |
74 | 0 | this.driver = options.driver; |
75 | 0 | this.name = options.file; |
76 | 0 | this.numberOfSuites = options.numberOfSuites; |
77 | 0 | this.error = null; |
78 | 0 | return this; |
79 | }, | |
80 | ||
81 | /** | |
82 | * Loads the testsuite that should be executed | |
83 | * | |
84 | * @method loadTestsuite | |
85 | * @param {string} testfile | |
86 | * @return {object} testsuite | |
87 | */ | |
88 | ||
89 | loadTestsuite: function (testfile) { | |
90 | 0 | var suite = {}; |
91 | ||
92 | // if the tests were passed in *as* a list of tests, just use them | |
93 | 0 | if (testfile && typeof testfile === 'object') { |
94 | 0 | var allAreTestFunctions = true; |
95 | 0 | for (var testname in testfile) { |
96 | 0 | if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; } |
97 | } | |
98 | 0 | if (allAreTestFunctions) { |
99 | 0 | return testfile; |
100 | } | |
101 | } | |
102 | ||
103 | // catch any errors, like falsy requires & stuff | |
104 | 0 | try { |
105 | ||
106 | 0 | if (fs.existsSync(process.cwd() + '/' + testfile)) { |
107 | 0 | suite = require(process.cwd() + '/' + testfile.replace('.js', '')); |
108 | } else { | |
109 | 0 | this.error = 'Suite "' + testfile + '" does not exist. Skipping!'; |
110 | 0 | return suite; |
111 | } | |
112 | } catch (e) { | |
113 | 0 | this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e; |
114 | 0 | return suite; |
115 | } | |
116 | ||
117 | 0 | suite._uid = _.uniqueId('Suite'); |
118 | 0 | return suite; |
119 | }, | |
120 | ||
121 | /** | |
122 | * Checks if all tests from the testsuite are executed. | |
123 | * Runs the next test if not. | |
124 | * Triggers `asyncs` callback if the suite is finished. | |
125 | * Decrements the `testsToBeExecuted` counter | |
126 | * | |
127 | * @method testFinished | |
128 | * @param {function} callback | |
129 | * @param {array} tests | |
130 | * @param {object} test | |
131 | * @param {string} event | |
132 | * @chainable | |
133 | */ | |
134 | ||
135 | testFinished: function (callback, tests) { | |
136 | 0 | var complete = function() { |
137 | // check if there are still tests that should be executed in this suite, | |
138 | // if so, run them | |
139 | 0 | if (this.decrementTestsToBeExecuted() > 1) { |
140 | 0 | this.executeNextTest(tests); |
141 | 0 | return this; |
142 | } | |
143 | ||
144 | // run a function after the testsuite, if given | |
145 | 0 | if (this.options.teardown) { |
146 | 0 | this.options.teardown(); |
147 | } | |
148 | ||
149 | // emit the suite finished event | |
150 | 0 | this.reporterEmitter.emit('report:testsuite:finished', this.name); |
151 | ||
152 | // move on to the next suite | |
153 | 0 | callback(); |
154 | 0 | return this; |
155 | }.bind(this); | |
156 | ||
157 | // run a function after the test, if given | |
158 | ||
159 | 0 | if (typeof this.options.afterEach === 'function') { |
160 | // If there is an argument, assume async. | |
161 | 0 | if (this.options.afterEach.length === 1) { |
162 | 0 | this.options.afterEach(function() { |
163 | 0 | return complete(); |
164 | }.bind(this)); | |
165 | } else { | |
166 | // no argument, assume sync. | |
167 | 0 | this.options.afterEach(); |
168 | 0 | return complete(); |
169 | } | |
170 | } else { | |
171 | 0 | return complete(); |
172 | } | |
173 | ||
174 | }, | |
175 | ||
176 | /** | |
177 | * Decrements number of tests that should be executed in this suite | |
178 | * | |
179 | * @method decrementTestsToBeExecuted | |
180 | * @return {integer} numberOfTestsToBeExecuted | |
181 | */ | |
182 | ||
183 | decrementTestsToBeExecuted: function () { | |
184 | 0 | return (this.testsToBeExecuted--) -1; |
185 | }, | |
186 | ||
187 | /** | |
188 | * Returns the name of the testsuite | |
189 | * If the suite has no name, it will return the testsuites filename | |
190 | * | |
191 | * @method getName | |
192 | * @return {string} name | |
193 | */ | |
194 | ||
195 | getName: function () { | |
196 | 0 | if (this.suite.name && _.isString(this.suite.name)) { |
197 | 0 | var name = this.suite.name; |
198 | 0 | delete this.suite.name; |
199 | 0 | return name; |
200 | } | |
201 | ||
202 | 0 | return this.name; |
203 | }, | |
204 | ||
205 | /** | |
206 | * Returns the options of the testsuite | |
207 | * If the suite has no options, it will return an empty object | |
208 | * | |
209 | * @method getOptions | |
210 | * @return {object} options Suite options | |
211 | */ | |
212 | ||
213 | getOptions: function () { | |
214 | 0 | if (this.suite.options && _.isObject(this.suite.options)) { |
215 | 0 | var options = this.suite.options; |
216 | 0 | delete this.suite.options; |
217 | 0 | return options; |
218 | } | |
219 | ||
220 | 0 | return {}; |
221 | }, | |
222 | ||
223 | /** | |
224 | * Returns all names (aka. object keys) the tests that should be executed | |
225 | * | |
226 | * @method getTests | |
227 | * @return {array} test | |
228 | */ | |
229 | ||
230 | getTests: function () { | |
231 | 0 | return Object.keys(this.suite); |
232 | }, | |
233 | ||
234 | /** | |
235 | * Returns the number of tests to be executed | |
236 | * | |
237 | * @method getNumberOfTests | |
238 | * @param {array} tests | |
239 | * @return {integer} numberOfTests | |
240 | */ | |
241 | ||
242 | getNumberOfTests: function (tests) { | |
243 | 0 | return tests.length; |
244 | }, | |
245 | ||
246 | /** | |
247 | * Returns the next test, that should be executed | |
248 | * | |
249 | * @method getNextTest | |
250 | * @return {string} testName | |
251 | */ | |
252 | ||
253 | getNextTest: function (tests) { | |
254 | 0 | return tests.shift(); |
255 | }, | |
256 | ||
257 | /** | |
258 | * Executes the next test in the sequence | |
259 | * | |
260 | * @method executeNextTest | |
261 | * @param {array} tests | |
262 | * @return {mixed} testValue | |
263 | */ | |
264 | ||
265 | executeNextTest: function (tests, callback) { | |
266 | 0 | var cb = callback || function() {}; |
267 | // grab the next test in the queue | |
268 | 0 | var testName = this.getNextTest(tests); |
269 | // get the next test function | |
270 | 0 | var testFunction = this.getTest(testName); |
271 | // generate an instance of the test | |
272 | 0 | var test = this.getTestInstance(testName); |
273 | // run a setup function before the test, if given | |
274 | 0 | if (typeof this.options.beforeEach !== 'function') { |
275 | 0 | cb(null, null); |
276 | 0 | testFunction.apply(test,[test]); |
277 | 0 | return this; |
278 | } | |
279 | 0 | if (this.options.beforeEach.length === 1) { |
280 | // if function takes an argument, assume async | |
281 | 0 | this.options.beforeEach(function() { |
282 | // start it | |
283 | 0 | testFunction.apply(test,[test]); |
284 | 0 | cb(null, null); |
285 | }); | |
286 | } else { | |
287 | // otherwise, assume sync | |
288 | 0 | this.options.beforeEach(); |
289 | 0 | testFunction.apply(test,[test]); |
290 | 0 | cb(null, null); |
291 | } | |
292 | 0 | return this; |
293 | }, | |
294 | ||
295 | /** | |
296 | * Generates a new test instance | |
297 | * | |
298 | * @method getTestInstance | |
299 | * @param {string} name | |
300 | * @return {Dalek.Test} test | |
301 | */ | |
302 | ||
303 | getTestInstance: function (name) { | |
304 | 0 | return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name}); |
305 | }, | |
306 | ||
307 | /** | |
308 | * Returns a test function by its name | |
309 | * | |
310 | * @method getTest | |
311 | * @param {string} name | |
312 | * @return {function} test | |
313 | */ | |
314 | ||
315 | getTest: function (name) { | |
316 | 0 | return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist; |
317 | }, | |
318 | ||
319 | /** | |
320 | * Will be executed if a test is started, that does not exist | |
321 | * | |
322 | * @method testDoesNotExist | |
323 | * @param {object} options | |
324 | */ | |
325 | ||
326 | testDoesNotExist: function (options) { | |
327 | 0 | if (options.name) { |
328 | 0 | this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.'); |
329 | } | |
330 | 0 | return this; |
331 | }, | |
332 | ||
333 | /** | |
334 | * Runs any tests from this testsuite in sequence | |
335 | * | |
336 | * @method run | |
337 | * @param {function} callback | |
338 | * @chainable | |
339 | */ | |
340 | ||
341 | run: function (callback) { | |
342 | 0 | var tests = []; |
343 | ||
344 | // check if the suite is | |
345 | 0 | if (this.error) { |
346 | 0 | this.reporterEmitter.emit('report:testsuite:started', null); |
347 | // emit a warning notice | |
348 | 0 | this.reporterEmitter.emit('warning', this.error); |
349 | // emit the suite finished event | |
350 | 0 | this.reporterEmitter.emit('report:testsuite:finished', null); |
351 | // move on to the next suite | |
352 | 0 | callback(); |
353 | } | |
354 | ||
355 | // extract suite name | |
356 | 0 | this.name = this.getName(); |
357 | // extract suite options | |
358 | 0 | this.options = this.getOptions(); |
359 | ||
360 | // extract tests | |
361 | 0 | tests = this.getTests(); |
362 | 0 | this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests); |
363 | ||
364 | // run a function before the testsuite has been launched, if given | |
365 | 0 | if (this.options.setup) { |
366 | 0 | this.options.setup(); |
367 | } | |
368 | ||
369 | // kickstart the test execution | |
370 | 0 | this.executeNextTest(tests, function() { |
371 | // emit the suite started event | |
372 | 0 | this.reporterEmitter.emit('report:testsuite:started', this.name); |
373 | // listen to the test:finished event & then start the next test | |
374 | // if there are no tests in this suite left, | |
375 | // run the async callback & mark this suite as finished | |
376 | 0 | this.emitter.onAny(this.testFinished.bind(this, callback, tests)); |
377 | }.bind(this)); | |
378 | ||
379 | 0 | return this; |
380 | } | |
381 | }; | |
382 | ||
383 | // export the testuite instance | |
384 | 1 | module.exports = Suite; |
385 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | /** | |
28 | * Initializes the timers default values | |
29 | * | |
30 | * @constructor | |
31 | * @class Timer | |
32 | */ | |
33 | ||
34 | 1 | var Timer = function () { |
35 | 9 | this.timerData = [0, 0]; |
36 | 9 | this.interval = [0, 0]; |
37 | }; | |
38 | ||
39 | /** | |
40 | * Timing module to measure test run times | |
41 | * | |
42 | * @module DalekJS | |
43 | * @class Timer | |
44 | * @namespace Dalek | |
45 | * @part Timer | |
46 | * @api | |
47 | */ | |
48 | ||
49 | 1 | Timer.prototype = { |
50 | ||
51 | /** | |
52 | * Starts the timer | |
53 | * | |
54 | * @method start | |
55 | * @chainable | |
56 | */ | |
57 | ||
58 | start: function () { | |
59 | 1 | this.timerData = process.hrtime(); |
60 | 1 | return this; |
61 | }, | |
62 | ||
63 | /** | |
64 | * Stops the timer | |
65 | * | |
66 | * @method stop | |
67 | * @chainable | |
68 | */ | |
69 | ||
70 | stop: function () { | |
71 | 2 | this.interval = process.hrtime(this.timerData); |
72 | 2 | return this; |
73 | }, | |
74 | ||
75 | /** | |
76 | * Returns the elapsed time in ms | |
77 | * | |
78 | * @method getElapsedTime | |
79 | * @return {float} | |
80 | */ | |
81 | ||
82 | getElapsedTime: function () { | |
83 | 2 | return (this.interval[0]*1e3 + this.interval[1]/1e6) / 1000; |
84 | }, | |
85 | ||
86 | /** | |
87 | * Returns an object with test run time information | |
88 | * containing hours, minutes & seconds | |
89 | * | |
90 | * @method getElapsedTimeFormatted | |
91 | * @return {Object} | |
92 | */ | |
93 | ||
94 | getElapsedTimeFormatted: function () { | |
95 | 2 | var hours, minutes, seconds; |
96 | 2 | var elapsedTimeInSeconds = this.getElapsedTime(); |
97 | ||
98 | // assign elapsed time (in seconds) to the seconds output | |
99 | 2 | seconds = elapsedTimeInSeconds; |
100 | ||
101 | // check if the elapsed time in seconds is more than a minute | |
102 | // and convert the raw seconds to minutes & seconds | |
103 | 2 | if (elapsedTimeInSeconds > 60) { |
104 | 2 | minutes = Math.floor(elapsedTimeInSeconds / 60); |
105 | 2 | seconds = elapsedTimeInSeconds - minutes * 60; |
106 | } | |
107 | ||
108 | // check if the elapsed time in minutes is more than an hour | |
109 | // and convert the raw seconds to hours, minutes & seconds | |
110 | 2 | if (minutes > 60) { |
111 | 2 | hours = Math.floor(elapsedTimeInSeconds / 3600); |
112 | 2 | minutes = elapsedTimeInSeconds - hours * 60; |
113 | 2 | seconds = elapsedTimeInSeconds - minutes * 3600; |
114 | } | |
115 | ||
116 | 2 | return {hours: hours, minutes: minutes, seconds: seconds}; |
117 | } | |
118 | }; | |
119 | ||
120 | // export the timer module | |
121 | 1 | module.exports = Timer; |
122 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | // Ext. libs | |
28 | 1 | var _ = require('lodash'); |
29 | 1 | var Q = require('q'); |
30 | ||
31 | // Int. libs | |
32 | 1 | var actions = require('./actions'); |
33 | 1 | var assertions = require('./assertions'); |
34 | ||
35 | // Default timeout for calling done | |
36 | 1 | var DONE_TIMEOUT = 10000; |
37 | ||
38 | /** | |
39 | * Prepares the test instance values | |
40 | * | |
41 | * @param {object} opts Options like the tests name, etc. | |
42 | * @constructor | |
43 | */ | |
44 | ||
45 | 1 | var Unit = function (opts) { |
46 | // prepare meta queue data | |
47 | 0 | this.actionPromiseQueue = []; |
48 | ||
49 | // prepare assertion data | |
50 | 0 | this.expectation = null; |
51 | 0 | this.runnedExpactations = 0; |
52 | 0 | this.failedAssertions = 0; |
53 | ||
54 | // prepare test specific data | |
55 | 0 | this.name = opts.name; |
56 | 0 | this.lastChain = []; |
57 | 0 | this.uuids = {}; |
58 | 0 | this.contextVars = {}; |
59 | ||
60 | 0 | if (this.name) { |
61 | 0 | this.timeoutForDone = setTimeout(function () { |
62 | 0 | this.done(); |
63 | 0 | this.reporter.emit('warning', 'done() not called before timeout!'); |
64 | }.bind(this), DONE_TIMEOUT); | |
65 | } | |
66 | }; | |
67 | ||
68 | /** | |
69 | * Generates an test instance | |
70 | * | |
71 | * @module DalekJS | |
72 | * @class Unit | |
73 | * @namespace Dalek | |
74 | * @part test | |
75 | * @api | |
76 | */ | |
77 | ||
78 | 1 | Unit.prototype = { |
79 | ||
80 | /** | |
81 | * Specify how many assertions are expected to run within a test. | |
82 | * Very useful for ensuring that all your callbacks and assertions are run. | |
83 | * | |
84 | * @method expect | |
85 | * @param {Integer} expecatation Number of assertions that should be run | |
86 | * @chainable | |
87 | */ | |
88 | ||
89 | expect: function (expectation) { | |
90 | 0 | this.expectation = parseInt(expectation, 10); |
91 | 0 | return this; |
92 | }, | |
93 | ||
94 | /** | |
95 | * Global data store (works between the node & browser envs) | |
96 | * | |
97 | * @method data | |
98 | * @param {string|number} key Key to store or fetch data | |
99 | * @param {mixed} value *optional* Data that should be stored | |
100 | * @return {mixed} Data that has been stored | |
101 | * @chainable | |
102 | */ | |
103 | ||
104 | data: function (key, value) { | |
105 | 0 | if (value) { |
106 | 0 | this.contextVars[key] = value; |
107 | 0 | return this; |
108 | } | |
109 | ||
110 | 0 | return this.contextVars[key]; |
111 | }, | |
112 | ||
113 | /** | |
114 | * Increment the number of executed assertions | |
115 | * | |
116 | * @method incrementExpectations | |
117 | * @chainable | |
118 | */ | |
119 | ||
120 | incrementExpectations: function () { | |
121 | 0 | this.runnedExpactations++; |
122 | 0 | return this; |
123 | }, | |
124 | ||
125 | /** | |
126 | * Increment the number of failed assertions | |
127 | * | |
128 | * @method incrementFailedAssertions | |
129 | * @chainable | |
130 | */ | |
131 | ||
132 | incrementFailedAssertions: function () { | |
133 | 0 | this.failedAssertions++; |
134 | 0 | return this; |
135 | }, | |
136 | ||
137 | /** | |
138 | * Checks if the runned tests fullfill the set expectations | |
139 | * or if no expectations were raised | |
140 | * | |
141 | * @method checkExpectations | |
142 | * @return {bool} checkedExpectations Expectations match | |
143 | */ | |
144 | ||
145 | checkExpectations: function () { | |
146 | 0 | return (this.expectation === null || !this.expectation || (this.runnedExpactations === this.expectation)); |
147 | }, | |
148 | ||
149 | /** | |
150 | * Checks if all runned assertions passed | |
151 | * | |
152 | * @method checkAssertions | |
153 | * @return {bool} assertionFailed Any expectation failed | |
154 | */ | |
155 | ||
156 | checkAssertions: function () { | |
157 | 0 | return this.failedAssertions === 0; |
158 | }, | |
159 | ||
160 | /** | |
161 | * Sets up all the bindings needed for a test to run | |
162 | * | |
163 | * @method done | |
164 | * @return {object} result A promise | |
165 | * @private | |
166 | */ | |
167 | ||
168 | done: function () { | |
169 | 0 | var result = Q.resolve(); |
170 | // clear the done error timeout | |
171 | 0 | clearTimeout(this.timeoutForDone); |
172 | // remove all previously attached event listeners to clear the message queue | |
173 | 0 | this.driver.events.removeAllListeners('driver:message'); |
174 | // resolve the deferred when the test is finished | |
175 | 0 | Unit.testStarted.fin(this._testFinished.bind(this, result)); |
176 | 0 | return result; |
177 | }, | |
178 | ||
179 | /** | |
180 | * Allow to use custom functions in order to embrace code reuse across | |
181 | * multiple files (for example for use in Page Objects). | |
182 | * | |
183 | * @method andThen | |
184 | * @param {function} a function, where 'this' references the test | |
185 | * @chainable | |
186 | */ | |
187 | andThen: function(func) { | |
188 | 0 | return func.call(this); |
189 | }, | |
190 | ||
191 | /** | |
192 | * Adds a node style function (with node err callback) style to the test. | |
193 | * | |
194 | * @method node | |
195 | * @param {function} a node function that is executed in the context of the test. | |
196 | * @chainable | |
197 | */ | |
198 | node: function(nodeFunction) { | |
199 | 0 | var deferred = Q.defer(), |
200 | that = this; | |
201 | 0 | nodeFunction.call(this, function(err) { |
202 | 0 | if (typeof err !== 'undefined') { |
203 | 0 | that.reporter.emit('error', err); |
204 | 0 | that.incrementFailedAssertions(); |
205 | 0 | deferred.reject(); |
206 | } else { | |
207 | 0 | deferred.resolve(); |
208 | } | |
209 | }); | |
210 | 0 | this.promise(deferred); |
211 | 0 | return this; |
212 | }, | |
213 | ||
214 | /** | |
215 | * Adds a promise to the chain of tests. | |
216 | * | |
217 | * @method promise | |
218 | * @param {promise} a q promise | |
219 | * @chainable | |
220 | */ | |
221 | promise: function(deferred) { | |
222 | 0 | this.actionPromiseQueue.push(deferred); |
223 | 0 | return this; |
224 | }, | |
225 | ||
226 | /** | |
227 | * Emits the test finished events & resolves all promises | |
228 | * when its done | |
229 | * | |
230 | * @method _testFinished | |
231 | * @param {object} result Promised result var | |
232 | * @return {object} result Promised result var | |
233 | * @private | |
234 | */ | |
235 | ||
236 | _testFinished: function (result) { | |
237 | // add a last deferred function on the end of the action queue, | |
238 | // to tell that this test is finished | |
239 | 0 | this.actionPromiseQueue.push(this._testFin.bind(this)); |
240 | ||
241 | // initialize all of the event receiver functions, | |
242 | // that later take the driver result | |
243 | 0 | this.actionPromiseQueue.forEach(function (f) { |
244 | 0 | result = result.then(f).fail(function () { |
245 | 0 | console.error(arguments); |
246 | 0 | process.exit(0); |
247 | }); | |
248 | }.bind(this)); | |
249 | ||
250 | // run the driver when all actions are stored in the queue | |
251 | 0 | Q.allSettled(this.actionPromiseQueue) |
252 | .then(this.driver.run.bind(this.driver)); | |
253 | ||
254 | 0 | return result; |
255 | }, | |
256 | ||
257 | /** | |
258 | * Emits the test started event | |
259 | * | |
260 | * @method _reportTestStarted | |
261 | * @param {string} name Name of the test | |
262 | * @chainable | |
263 | * @private | |
264 | */ | |
265 | ||
266 | _reportTestStarted: function (name) { | |
267 | 0 | this.reporter.emit('report:test:started', {name: name}); |
268 | 0 | return this; |
269 | }, | |
270 | ||
271 | /** | |
272 | * Checks if the test run is complete & emits/resolves | |
273 | * all the needed events/promises when the run is complete | |
274 | * | |
275 | * @method _onDriverMessage | |
276 | * @param {object} data Data that is returned by the driver:message event | |
277 | * @chainable | |
278 | * @private | |
279 | */ | |
280 | ||
281 | _onDriverMessage: function (data) { | |
282 | // check if the test run is complete | |
283 | 0 | if (data && data.key === 'run.complete') { |
284 | // emit the test finish events & resolve the deferred | |
285 | 0 | this._emitConcreteTestFinished(); |
286 | 0 | this._emitAssertionStatus(); |
287 | 0 | this._emitTestFinished(); |
288 | 0 | this.deferred.resolve(); |
289 | } | |
290 | ||
291 | 0 | return this; |
292 | }, | |
293 | ||
294 | /** | |
295 | * Emits an event, that the current test run has been finished | |
296 | * | |
297 | * @method _emitConcreteTestFinished | |
298 | * @chainable | |
299 | * @private | |
300 | */ | |
301 | ||
302 | _emitConcreteTestFinished: function () { | |
303 | 0 | this.events.emit('test:' + this._uid + ':finished', 'test:finished', this); |
304 | 0 | return this; |
305 | }, | |
306 | ||
307 | /** | |
308 | * Emits an event that describes the current state of all assertions | |
309 | * | |
310 | * @method _emitAssertionStatus | |
311 | * @chainable | |
312 | * @private | |
313 | */ | |
314 | ||
315 | _emitAssertionStatus: function () { | |
316 | 0 | this.reporter.emit('report:assertion:status', { |
317 | expected: (this.expectation ? this.expectation : this.runnedExpactations), | |
318 | run: this.runnedExpactations, | |
319 | status: this._testStatus() | |
320 | }); | |
321 | 0 | return this; |
322 | }, | |
323 | ||
324 | /** | |
325 | * Get the overall test status (assertions & expectation) | |
326 | * | |
327 | * @method _testStatus | |
328 | * @return {bool} status The test status | |
329 | * @chainable | |
330 | * @private | |
331 | */ | |
332 | ||
333 | _testStatus: function () { | |
334 | 0 | return this.checkExpectations() && this.checkAssertions(); |
335 | }, | |
336 | ||
337 | /** | |
338 | * Emits an event that describes the current state of all assertions. | |
339 | * The event should be fired when a test is finished | |
340 | * | |
341 | * @method _emitTestFinished | |
342 | * @chainable | |
343 | * @private | |
344 | */ | |
345 | ||
346 | _emitTestFinished: function () { | |
347 | 0 | this.reporter.emit('report:test:finished', { |
348 | name: this.name, | |
349 | id: this._uid, | |
350 | passedAssertions: this.runnedExpactations - this.failedAssertions, | |
351 | failedAssertions: this.failedAssertions, | |
352 | runnedExpactations: this.runnedExpactations, | |
353 | status: this._testStatus(), | |
354 | nl: true | |
355 | }); | |
356 | ||
357 | 0 | return this; |
358 | }, | |
359 | ||
360 | /** | |
361 | * Kicks off the test & binds all promises/events | |
362 | * | |
363 | * @method _testFin | |
364 | * @return {object} promise A promise | |
365 | * @private | |
366 | */ | |
367 | ||
368 | _testFin: function () { | |
369 | 0 | this.deferred = Q.defer(); |
370 | ||
371 | 0 | if (_.isFunction(this.driver.end)) { |
372 | 0 | this.driver.end(); |
373 | } | |
374 | ||
375 | // emit report startet event | |
376 | 0 | this._reportTestStarted(this.name); |
377 | ||
378 | // listen to all the messages from the driver | |
379 | 0 | this.driver.events.on('driver:message', this._onDriverMessage.bind(this)); |
380 | 0 | return this.deferred.promise; |
381 | }, | |
382 | ||
383 | /** | |
384 | * Copies assertion methods | |
385 | * | |
386 | * @method _inheritAssertions | |
387 | * @param {Test} test Instacne of test | |
388 | * @chainable | |
389 | * @private | |
390 | */ | |
391 | ||
392 | _inheritAssertions: function (test) { | |
393 | 0 | ['is'].forEach(function (method) { |
394 | 0 | test[method] = test.assert[method].bind(test.assert); |
395 | }); | |
396 | 0 | return test; |
397 | }, | |
398 | ||
399 | /** | |
400 | * Copies assertion helper methods | |
401 | * | |
402 | * @method _inheritAssertions | |
403 | * @param {Test} test Instacne of test | |
404 | * @chainable | |
405 | * @private | |
406 | */ | |
407 | ||
408 | _inheritAssertionHelpers: function (test) { | |
409 | 0 | ['not', 'between', 'gt', 'gte', 'lt', 'lte', 'equalsCaseInsensitive'].forEach(function (method) { |
410 | 0 | test.is[method] = test.assert[method].bind(test.assert); |
411 | 0 | test.assert.is[method] = test.assert[method].bind(test.assert); |
412 | }); | |
413 | 0 | ['contain', 'match'].forEach(function (method) { |
414 | 0 | test.to = test.to || {}; |
415 | 0 | test.assert.to = test.assert.to || {}; |
416 | ||
417 | 0 | test.to[method] = test.assert[method].bind(test.assert); |
418 | 0 | test.assert.to[method] = test.assert[method].bind(test.assert); |
419 | }); | |
420 | 0 | ['notContain'].forEach(function (method) { |
421 | 0 | var apiName = method.substr(3, 1).toLowerCase() + method.substr(4); |
422 | 0 | test.to.not = test.to.not || {}; |
423 | 0 | test.assert.to.not = test.assert.to.not || {}; |
424 | ||
425 | 0 | test.to.not[apiName] = test.assert[method].bind(test.assert); |
426 | 0 | test.assert.to.not[apiName] = test.assert[method].bind(test.assert); |
427 | }); | |
428 | 0 | return test; |
429 | }, | |
430 | ||
431 | /** | |
432 | * Set up the instance | |
433 | * | |
434 | * @method _inheritAssertions | |
435 | * @param {Test} test Instacne of test | |
436 | * @param {object} opts Options | |
437 | * @chainable | |
438 | * @private | |
439 | */ | |
440 | ||
441 | _initialize: function (test, opts) { | |
442 | 0 | test._uid = _.uniqueId('test'); |
443 | 0 | test.events = opts.events; |
444 | 0 | test.driver = opts.driver; |
445 | 0 | test.reporter = opts.reporter; |
446 | 0 | return test; |
447 | } | |
448 | ||
449 | }; | |
450 | ||
451 | /** | |
452 | * Alias for 'andThen'; use if it is the first function called in the test case. | |
453 | * | |
454 | * @method start | |
455 | * @chainable | |
456 | */ | |
457 | 1 | Unit.prototype.start = Unit.prototype.andThen; |
458 | ||
459 | // export a function that generates a new test instance | |
460 | 1 | module.exports = function (opts) { |
461 | // mixin assertions, actions & getters | |
462 | 0 | Unit.prototype = _.extend(Unit.prototype, actions({reporter: opts.reporter}).prototype); |
463 | 0 | var unit = new Unit(opts); |
464 | 0 | unit.assert = new (assertions())({test: unit}); |
465 | 0 | unit.assert.done = unit.done.bind(this); |
466 | 0 | unit.assert.query = unit.query.bind(unit.assert); |
467 | 0 | unit.assert.$ = unit.query.bind(unit.assert); |
468 | 0 | unit.end = unit.assert.end.bind(unit.assert); |
469 | ||
470 | // copy log methods | |
471 | 0 | unit.log = {}; |
472 | 0 | unit.log.dom = unit.logger.dom.bind(unit); |
473 | 0 | unit.log.message = unit.logger.message.bind(unit); |
474 | ||
475 | // copy assertions methods | |
476 | 0 | unit = unit._inheritAssertions(unit); |
477 | ||
478 | // copy assertion helper methods | |
479 | 0 | unit = unit._inheritAssertionHelpers(unit); |
480 | ||
481 | // initialize the instance | |
482 | 0 | unit = unit._initialize(unit, opts); |
483 | ||
484 | // TODO: Promise driver start | |
485 | // so that we can reexecute them and clean the env between tests | |
486 | 0 | Unit.testStarted = unit.driver.start(Q); |
487 | 0 | return unit; |
488 | }; | |
489 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * | |
3 | * Copyright (c) 2013 Sebastian Golasch | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
6 | * copy of this software and associated documentation files (the "Software"), | |
7 | * to deal in the Software without restriction, including without limitation | |
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
9 | * and/or sell copies of the Software, and to permit persons to whom the | |
10 | * Software is furnished to do so, subject to the following conditions: | |
11 | * | |
12 | * The above copyright notice and this permission notice shall be included | |
13 | * in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
21 | * DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | */ | |
24 | ||
25 | 1 | 'use strict'; |
26 | ||
27 | 1 | var counter = 0; |
28 | ||
29 | 1 | module.exports = function() { |
30 | 3 | return 'uuid-' + (++counter); |
31 | }; | |
32 |