1 | var http = require( "http" ),
|
2 | querystring = require( "querystring" ),
|
3 | util = require( "util" ),
|
4 | userAgent = getUA();
|
5 |
|
6 | function getUA() {
|
7 | var os = require( "os" ),
|
8 | version = require( "../package.json" ).version;
|
9 | return os.platform() + "/" + os.release() + " " +
|
10 | "node/" + process.versions.node + " " +
|
11 | "node-browserstack/" + version;
|
12 | }
|
13 |
|
14 | function extend( a, b ) {
|
15 | for ( var p in b ) {
|
16 | a[ p ] = b[ p ];
|
17 | }
|
18 |
|
19 | return a;
|
20 | }
|
21 |
|
22 | function Client( settings ) {
|
23 | if ( !settings.username ) {
|
24 | throw new Error( "Username is required." );
|
25 | }
|
26 | if ( !settings.password ) {
|
27 | throw new Error( "Password is required." );
|
28 | }
|
29 |
|
30 | extend( this, settings );
|
31 | this.authHeader = "Basic " +
|
32 | new Buffer( this.username + ":" + this.password ).toString( "base64" );
|
33 |
|
34 | this.server = extend({
|
35 | host: "api.browserstack.com",
|
36 | port: 80
|
37 | }, this.server || {});
|
38 | }
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | extend( Client.prototype, {
|
44 | getBrowsers: function( fn ) {
|
45 | this._getBrowsers( function( error, browsers ) {
|
46 | if ( !error ) {
|
47 | this.updateLatest( browsers );
|
48 | }
|
49 |
|
50 | fn( error, browsers );
|
51 | }.bind( this ) );
|
52 | },
|
53 |
|
54 | createWorker: function( options, fn ) {
|
55 | if ( options[ this.versionField ] === "latest" ) {
|
56 | return this.getLatest( options, function( error, version ) {
|
57 | if ( error ) {
|
58 | return fn( error );
|
59 | }
|
60 |
|
61 | options = extend( {}, options );
|
62 | options[ this.versionField ] = version;
|
63 | this.createWorker( options, fn );
|
64 | }.bind( this ) );
|
65 | }
|
66 |
|
67 | var data = querystring.stringify( options );
|
68 | this.request({
|
69 | path: this.path( "/worker" ),
|
70 | method: "POST"
|
71 | }, data, fn );
|
72 | },
|
73 |
|
74 | getWorker: function( id, fn ) {
|
75 | this.request({
|
76 | path: this.path( "/worker/" + id )
|
77 | }, fn );
|
78 | },
|
79 |
|
80 | terminateWorker: function( id, fn ) {
|
81 | this.request({
|
82 | path: this.path( "/worker/" + id ),
|
83 | method: "DELETE"
|
84 | }, fn );
|
85 | },
|
86 |
|
87 | getWorkers: function( fn ) {
|
88 | this.request({
|
89 | path: this.path( "/workers" )
|
90 | }, fn );
|
91 | },
|
92 |
|
93 | getLatest: function( browser, fn ) {
|
94 | var latest = this.latest,
|
95 | browserId = this.getBrowserId( browser );
|
96 |
|
97 | if ( typeof browser === "function" ) {
|
98 | fn = browser;
|
99 | browser = null;
|
100 | }
|
101 |
|
102 |
|
103 |
|
104 | if ( this.latestPending ) {
|
105 | return setTimeout(function() {
|
106 | this.getLatest( browser, fn );
|
107 | }.bind( this ), 50 );
|
108 | }
|
109 |
|
110 |
|
111 | if ( !latest || this.latestUpdate < (new Date() - 864e5) ) {
|
112 | this.latestPending = true;
|
113 | return this.getBrowsers(function( error ) {
|
114 | this.latestPending = false;
|
115 |
|
116 | if ( error ) {
|
117 | return fn( error );
|
118 | }
|
119 |
|
120 | this.getLatest( browser, fn );
|
121 | }.bind( this ) );
|
122 | }
|
123 |
|
124 | process.nextTick(function() {
|
125 | fn( null, browser ? latest[ browserId ] : extend( {}, latest ) );
|
126 | });
|
127 | },
|
128 |
|
129 | takeScreenshot: function( id, fn ) {
|
130 | this.request({
|
131 | path: this.path( "/worker/" + id + "/screenshot.json" )
|
132 | }, fn );
|
133 | }
|
134 | });
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | extend( Client.prototype, {
|
140 | latest: null,
|
141 | latestUpdate: 0,
|
142 | latestPending: false,
|
143 |
|
144 | path: function( path ) {
|
145 | return "/" + this.version + path;
|
146 | },
|
147 |
|
148 | request: function( options, data, fn ) {
|
149 | if ( typeof data === "function" ) {
|
150 | fn = data;
|
151 | data = null;
|
152 | }
|
153 | fn = fn || function() {};
|
154 |
|
155 | var req = http.request( extend({
|
156 | host: this.server.host,
|
157 | port: this.server.port,
|
158 | method: "GET",
|
159 | headers: {
|
160 | authorization: this.authHeader,
|
161 | "user-agent": userAgent,
|
162 | "content-length": typeof data === "string" ? data.length : 0
|
163 | }
|
164 | }, options ), function( res ) {
|
165 | var response = "";
|
166 | res.setEncoding( "utf8" );
|
167 | res.on( "data", function( chunk ) {
|
168 | response += chunk;
|
169 | });
|
170 | res.on( "end", function() {
|
171 | if ( res.statusCode !== 200 ) {
|
172 | var message;
|
173 | if ( res.headers[ "content-type" ].indexOf( "json" ) !== -1 ) {
|
174 | response = JSON.parse( response );
|
175 | message = response.message;
|
176 | message += " - " + response.errors.map(function( error ) {
|
177 | return "`" + error.field + "` " + error.code;
|
178 | }).join( ", " );
|
179 | } else {
|
180 | message = response;
|
181 | }
|
182 | if ( !message && res.statusCode === 403 ) {
|
183 | message = "Forbidden";
|
184 | }
|
185 | fn( new Error( message ) );
|
186 | } else {
|
187 | fn( null, JSON.parse( response ) );
|
188 | }
|
189 | });
|
190 | });
|
191 |
|
192 | if ( data ) {
|
193 | req.write( data );
|
194 | }
|
195 | req.end();
|
196 | },
|
197 |
|
198 | updateLatest: function( browsers ) {
|
199 | var latest = this.latest = {},
|
200 | getBrowserId = this.getBrowserId,
|
201 | versionField = this.versionField;
|
202 |
|
203 | this.latestUpdate = new Date();
|
204 | browsers.forEach(function( browser ) {
|
205 | var version = browser[ versionField ],
|
206 | browserId = getBrowserId( browser );
|
207 |
|
208 |
|
209 | if ( !version ) {
|
210 | return;
|
211 | }
|
212 |
|
213 |
|
214 | if ( /\s/.test( version ) ) {
|
215 | return;
|
216 | }
|
217 |
|
218 | if ( parseFloat( version ) >
|
219 | (parseFloat( latest[ browserId ] ) || 0) ) {
|
220 | latest[ browserId ] = version;
|
221 | }
|
222 | });
|
223 | }
|
224 | });
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 | Client.versions = {};
|
231 | Client.latestVersion = 0;
|
232 | Client.createVersion = function( version, prototype ) {
|
233 | function Version( settings ) {
|
234 | Client.call( this, settings );
|
235 | }
|
236 | util.inherits( Version, Client );
|
237 |
|
238 | Version.prototype.version = version;
|
239 | extend( Version.prototype, prototype );
|
240 |
|
241 | Client.versions[ version ] = Version;
|
242 | Client.latestVersion = Math.max( Client.latestVersion, version );
|
243 | };
|
244 |
|
245 | Client.createVersion( 1, {
|
246 | versionField: "version",
|
247 |
|
248 | _getBrowsers: function( fn ) {
|
249 | this.request({
|
250 | path: this.path( "/browsers" )
|
251 | }, fn );
|
252 | },
|
253 |
|
254 | getBrowserId: function( browser ) {
|
255 | return browser.browser;
|
256 | }
|
257 | });
|
258 |
|
259 | Client.createVersion( 2, {
|
260 | versionField: "version",
|
261 |
|
262 | _getBrowsers: function( fn ) {
|
263 | this.request({
|
264 | path: this.path( "/browsers" )
|
265 | }, function( error, osBrowsers ) {
|
266 | if ( error ) {
|
267 | return fn( error );
|
268 | }
|
269 |
|
270 | fn( null, [].concat.apply( [],
|
271 | Object.keys( osBrowsers ).map(function( os ) {
|
272 | return osBrowsers[ os ].map(function( browser ) {
|
273 | browser.os = os;
|
274 | return browser;
|
275 | });
|
276 | })
|
277 | ));
|
278 | });
|
279 | },
|
280 |
|
281 | getBrowserId: function( browser ) {
|
282 | return browser.os + ":" + (browser.browser || browser.device);
|
283 | }
|
284 | });
|
285 |
|
286 | Client.createVersion( 3, {
|
287 | versionField: "browser_version",
|
288 |
|
289 | _getBrowsers: function( fn ) {
|
290 | this.request({
|
291 | path: this.path( "/browsers?flat=true" )
|
292 | }, fn );
|
293 | },
|
294 |
|
295 | getBrowserId: function( browser ) {
|
296 | var id = browser.os + ":" + browser.os_version + ":" + browser.browser;
|
297 | if ( browser.device ) {
|
298 | id += ":" + browser.device;
|
299 | }
|
300 |
|
301 | return id;
|
302 | },
|
303 |
|
304 | getApiStatus: function( fn ) {
|
305 | this.request({
|
306 | path: this.path( "/status" )
|
307 | }, fn );
|
308 | }
|
309 | });
|
310 |
|
311 |
|
312 | module.exports = {
|
313 | createClient: function( settings ) {
|
314 | var Version = Client.versions[ settings.version || Client.latestVersion ];
|
315 | if ( !Version ) {
|
316 | throw new Error( "Invalid version" );
|
317 | }
|
318 |
|
319 | return new Version( settings );
|
320 | }
|
321 | };
|