Merge pull request #681 from darkdarkdragon/develop-RLJS-564

fix for browser
This commit is contained in:
Chris Clark
2016-01-13 11:31:14 -08:00
19 changed files with 392 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ var rename = require('gulp-rename');
var webpack = require('webpack'); var webpack = require('webpack');
var bump = require('gulp-bump'); var bump = require('gulp-bump');
var argv = require('yargs').argv; var argv = require('yargs').argv;
var assert = require('assert');
var pkg = require('./package.json'); var pkg = require('./package.json');
@@ -21,10 +22,19 @@ function webpackConfig(extension, overrides) {
path: './build/', path: './build/',
filename: ['ripple-', extension].join(pkg.version) filename: ['ripple-', extension].join(pkg.version)
}, },
plugins: [
new webpack.NormalModuleReplacementPlugin(/^ws$/, './wswrapper'),
new webpack.NormalModuleReplacementPlugin(/^\.\/wallet$/, './wallet-web'),
new webpack.NormalModuleReplacementPlugin(/^.*setup-api$/,
'./setup-api-web')
],
module: { module: {
loaders: [{ loaders: [{
test: /jayson/,
loader: 'null'
}, {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/, exclude: [/node_modules/],
loader: 'babel-loader?optional=runtime' loader: 'babel-loader?optional=runtime'
}, { }, {
test: /\.json/, test: /\.json/,
@@ -35,6 +45,53 @@ function webpackConfig(extension, overrides) {
return _.assign({}, defaults, overrides); return _.assign({}, defaults, overrides);
} }
function webpackConfigForWebTest(testFileName, path) {
var match = testFileName.match(/\/?([^\/]*)-test.js$/);
if (!match) {
assert(false, 'wrong filename:' + testFileName);
}
var configOverrides = {
externals: [{
'ripple-api': 'ripple',
'net': 'null'
}],
entry: testFileName,
output: {
library: match[1].replace(/-/g, '_'),
path: './test-compiled-for-web/' + (path ? path : ''),
filename: match[1] + '-test.js'
}
};
return webpackConfig('.js', configOverrides);
}
gulp.task('build-for-web-tests', function(callback) {
var configOverrides = {
output: {
library: 'ripple',
path: './test-compiled-for-web/',
filename: 'ripple-for-web-tests.js'
}
};
var config = webpackConfig('-debug.js', configOverrides);
webpack(config, callback);
});
gulp.task('build-tests', function(callback) {
var times = 0;
function done() {
if (++times >= 5) {
callback();
}
}
webpack(webpackConfigForWebTest('./test/rangeset-test.js'), done);
webpack(webpackConfigForWebTest('./test/connection-test.js'), done);
webpack(webpackConfigForWebTest('./test/api-test.js'), done);
webpack(webpackConfigForWebTest('./test/broadcast-api-test.js'), done);
webpack(webpackConfigForWebTest('./test/integration/integration-test.js',
'integration/'), done);
});
gulp.task('build', function(callback) { gulp.task('build', function(callback) {
webpack(webpackConfig('.js'), callback); webpack(webpackConfig('.js'), callback);
}); });

View File

@@ -50,6 +50,9 @@
"json-loader": "^0.5.2", "json-loader": "^0.5.2",
"json-schema-to-markdown-table": "^0.4.0", "json-schema-to-markdown-table": "^0.4.0",
"mocha": "^2.1.0", "mocha": "^2.1.0",
"mocha-junit-reporter": "^1.9.1",
"mocha-phantomjs": "^4.0.1",
"null-loader": "^0.1.1",
"webpack": "^1.5.3", "webpack": "^1.5.3",
"yargs": "^1.3.1" "yargs": "^1.3.1"
}, },

View File

@@ -24,6 +24,7 @@ lint() {
unittest() { unittest() {
# test "src" # test "src"
mocha test --reporter mocha-junit-reporter --reporter-options mochaFile=$CIRCLE_TEST_REPORTS/test-results.xml
npm test --coverage npm test --coverage
npm run coveralls npm run coveralls
@@ -33,12 +34,26 @@ unittest() {
mkdir -p test-compiled/node_modules mkdir -p test-compiled/node_modules
ln -nfs ../../dist/npm test-compiled/node_modules/ripple-api ln -nfs ../../dist/npm test-compiled/node_modules/ripple-api
mocha --opts test-compiled/mocha.opts test-compiled mocha --opts test-compiled/mocha.opts test-compiled
# compile tests for browser testing
gulp build-tests build-for-web-tests
node --harmony test-compiled/mocked-server.js > /dev/null &
echo "Running tests in PhantomJS"
mocha-phantomjs test/localrunner.html
pkill -f mocked-server.js
rm -rf test-compiled rm -rf test-compiled
} }
integrationtest() { integrationtest() {
mocha test/integration/integration-test.js mocha test/integration/integration-test.js
mocha test/integration/http-integration-test.js mocha test/integration/http-integration-test.js
# run integration tests in PhantomJS
gulp build-tests build-for-web-tests
echo "Running integragtion tests in PhantomJS"
mocha-phantomjs test/localintegrationrunner.html
} }
doctest() { doctest() {

View File

@@ -11,6 +11,9 @@ class RippleAPIBroadcast extends RippleAPI {
_.assign({}, options, {server}) _.assign({}, options, {server})
)); ));
// exposed for testing
this._apis = apis;
this.getMethodNames().forEach(name => { this.getMethodNames().forEach(name => {
this[name] = function() { // eslint-disable-line no-loop-func this[name] = function() { // eslint-disable-line no-loop-func
return Promise.race(apis.map(api => api[name].apply(api, arguments))); return Promise.race(apis.map(api => api[name].apply(api, arguments)));

View File

@@ -148,10 +148,12 @@ class Connection extends EventEmitter {
cert: this._certificate cert: this._certificate
}, _.isUndefined); }, _.isUndefined);
const websocketOptions = _.assign({}, options, optionsOverrides); const websocketOptions = _.assign({}, options, optionsOverrides);
const websocket = new WebSocket(this._url, websocketOptions); const websocket = new WebSocket(this._url, null, websocketOptions);
// we will have a listener for each outstanding request, // we will have a listener for each outstanding request,
// so we have to raise the limit (the default is 10) // so we have to raise the limit (the default is 10)
websocket.setMaxListeners(Infinity); if (typeof websocket.setMaxListeners === 'function') {
websocket.setMaxListeners(Infinity);
}
return websocket; return websocket;
} }

View File

@@ -7,7 +7,9 @@ class RippleError extends Error {
this.name = this.constructor.name; this.name = this.constructor.name;
this.message = message; this.message = message;
this.data = data; this.data = data;
Error.captureStackTrace(this, this.constructor.name); if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor.name);
}
} }
toString() { toString() {

View File

@@ -35,12 +35,13 @@ function toRippledAmount(amount: Amount): RippledAmount {
}; };
} }
const FINDSNAKE = /([a-zA-Z]_[a-zA-Z])/g;
function convertKeysFromSnakeCaseToCamelCase(obj: any): any { function convertKeysFromSnakeCaseToCamelCase(obj: any): any {
if (typeof obj === 'object') { if (typeof obj === 'object') {
let newKey; let newKey;
return _.reduce(obj, (result, value, key) => { return _.reduce(obj, (result, value, key) => {
newKey = key; newKey = key;
// taking this out of function leads to error in PhantomJS
const FINDSNAKE = /([a-zA-Z]_[a-zA-Z])/g;
if (FINDSNAKE.test(key)) { if (FINDSNAKE.test(key)) {
newKey = key.replace(FINDSNAKE, r => r[0] + r[2].toUpperCase()); newKey = key.replace(FINDSNAKE, r => r[0] + r[2].toUpperCase());
} }

62
src/common/wswrapper.js Normal file
View File

@@ -0,0 +1,62 @@
'use strict';
const {EventEmitter} = require('events');
function unsused() {}
/**
* Provides `EventEmitter` interface for native browser `WebSocket`,
* same, as `ws` package provides.
*/
class WSWrapper extends EventEmitter {
constructor(url, protocols = null, websocketOptions = {}) {
super();
unsused(protocols);
unsused(websocketOptions);
this.setMaxListeners(Infinity);
this._ws = new WebSocket(url);
this._ws.onclose = () => {
this.emit('close');
};
this._ws.onopen = () => {
this.emit('open');
};
this._ws.onerror = (error) => {
if (this.listenerCount('error') > 0) {
this.emit('error', error);
}
};
this._ws.onmessage = (message) => {
this.emit('message', message.data);
};
}
close() {
if (this.readyState === 1) {
this._ws.close();
}
}
send(message) {
this._ws.send(message);
}
get readyState() {
return this._ws.readyState;
}
}
WSWrapper.CONNECTING = 0;
WSWrapper.OPEN = 1;
WSWrapper.CLOSING = 2;
WSWrapper.CLOSED = 3;
module.exports = WSWrapper;

View File

@@ -16,6 +16,8 @@ const ledgerClosed = require('./fixtures/rippled/ledger-close-newer');
const schemaValidator = RippleAPI._PRIVATE.schemaValidator; const schemaValidator = RippleAPI._PRIVATE.schemaValidator;
assert.options.strict = true; assert.options.strict = true;
const TIMEOUT = 10000; // how long before each test case times out
function unused() { function unused() {
} }
@@ -37,6 +39,7 @@ function checkResult(expected, schemaName, response) {
describe('RippleAPI', function() { describe('RippleAPI', function() {
this.timeout(TIMEOUT);
const instructions = {maxLedgerVersionOffset: 100}; const instructions = {maxLedgerVersionOffset: 100};
beforeEach(setupAPI.setup); beforeEach(setupAPI.setup);
afterEach(setupAPI.teardown); afterEach(setupAPI.teardown);
@@ -944,7 +947,11 @@ describe('RippleAPI', function() {
}); });
it('getServerInfo - error', function() { it('getServerInfo - error', function() {
this.mockRippled.returnErrorOnServerInfo = true; this.api.connection._send(JSON.stringify({
command: 'config',
data: {returnErrorOnServerInfo: true}
}));
return this.api.getServerInfo().then(() => { return this.api.getServerInfo().then(() => {
assert(false, 'Should throw NetworkError'); assert(false, 'Should throw NetworkError');
}).catch(error => { }).catch(error => {

View File

@@ -26,7 +26,9 @@ describe('RippleAPIBroadcast', function() {
it('base', function() { it('base', function() {
const expected = {request_server_info: 1}; const expected = {request_server_info: 1};
this.mocks.forEach(mock => mock.expect(_.assign({}, expected))); if (!process.browser) {
this.mocks.forEach(mock => mock.expect(_.assign({}, expected)));
}
assert(this.api.isConnected()); assert(this.api.isConnected());
return this.api.getServerInfo().then( return this.api.getServerInfo().then(
_.partial(checkResult, responses.getServerInfo, 'getServerInfo')); _.partial(checkResult, responses.getServerInfo, 'getServerInfo'));
@@ -39,13 +41,16 @@ describe('RippleAPIBroadcast', function() {
}); });
const ledgerNext = _.assign({}, ledgerClosed); const ledgerNext = _.assign({}, ledgerClosed);
ledgerNext.ledger_index++; ledgerNext.ledger_index++;
this.mocks.forEach(mock => mock.socket.send(JSON.stringify(ledgerNext)));
this.api._apis.forEach(api => api.connection._send(JSON.stringify({
command: 'echo',
data: ledgerNext
})));
setTimeout(() => { setTimeout(() => {
assert.strictEqual(gotLedger, 1); assert.strictEqual(gotLedger, 1);
done(); done();
}, 50); }, 250);
}); });
it('error propagation', function(done) { it('error propagation', function(done) {
@@ -54,8 +59,10 @@ describe('RippleAPIBroadcast', function() {
assert.strictEqual(info, 'info'); assert.strictEqual(info, 'info');
done(); done();
}); });
this.mocks[1].socket.send( this.api._apis[1].connection._send(JSON.stringify({
JSON.stringify({error: 'type', error_message: 'info'})); command: 'echo',
data: {error: 'type', error_message: 'info'}
}));
}); });
}); });

View File

@@ -10,6 +10,8 @@ const utils = RippleAPI._PRIVATE.ledgerUtils;
const ledgerClose = require('./fixtures/rippled/ledger-close.json'); const ledgerClose = require('./fixtures/rippled/ledger-close.json');
const TIMEOUT = 10000; // how long before each test case times out
function unused() { function unused() {
} }
@@ -27,6 +29,7 @@ function createServer() {
} }
describe('Connection', function() { describe('Connection', function() {
this.timeout(TIMEOUT);
beforeEach(setupAPI.setup); beforeEach(setupAPI.setup);
afterEach(setupAPI.teardown); afterEach(setupAPI.teardown);
@@ -47,6 +50,9 @@ describe('Connection', function() {
messages.push(message); messages.push(message);
} }
}; };
connection._ws = {
send: function() {}
};
connection._onMessage(message1); connection._onMessage(message1);
connection._send(message2); connection._send(message2);
@@ -54,6 +60,10 @@ describe('Connection', function() {
}); });
it('with proxy', function(done) { it('with proxy', function(done) {
if (process.browser) {
done();
return;
}
createServer().then((server) => { createServer().then((server) => {
const port = server.address().port; const port = server.address().port;
const expect = 'CONNECT localhost'; const expect = 'CONNECT localhost';
@@ -97,9 +107,10 @@ describe('Connection', function() {
}); });
it('DisconnectedError', function() { it('DisconnectedError', function() {
this.api.connection._send = function() { this.api.connection._send(JSON.stringify({
this._ws.close(); command: 'config',
}; data: {disconnectOnServerInfo: true}
}));
return this.api.getServerInfo().then(() => { return this.api.getServerInfo().then(() => {
assert(false, 'Should throw DisconnectedError'); assert(false, 'Should throw DisconnectedError');
}).catch(error => { }).catch(error => {

View File

@@ -12,7 +12,8 @@ const {isValidSecret} = require('../../src/common');
const {payTo, ledgerAccept} = require('./utils'); const {payTo, ledgerAccept} = require('./utils');
const TIMEOUT = 10000; // how long before each test case times out // how long before each test case times out
const TIMEOUT = process.browser ? 25000 : 10000;
const INTERVAL = 1000; // how long to wait between checks for validated ledger const INTERVAL = 1000; // how long to wait between checks for validated ledger
const serverUrl = 'ws://127.0.0.1:6006'; const serverUrl = 'ws://127.0.0.1:6006';

View File

@@ -0,0 +1,14 @@
'use strict';
function getAddress() {
return 'rQDhz2ZNXmhxzCYwxU6qAbdxsHA4HV45Y2';
}
function getSecret() {
return 'shK6YXzwYfnFVn3YZSaMh5zuAddKx';
}
module.exports = {
getAddress,
getSecret
};

View File

@@ -1,4 +1,5 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');

View File

@@ -0,0 +1,57 @@
<html>
<head>
<meta charset="utf-8">
<!-- encoding must be set for mocha's special characters to render properly -->
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="deb"></div>
<div id="mocha"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script>
(function() {
var phantomTest = /PhantomJS/;
if (phantomTest.test(navigator.userAgent)) {
// mocha-phantomjs-core has wrong shim for Function.bind, so we
// will replace it with correct one
// this bind polyfill copied from MDN documentation
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
// native functions don't have a prototype
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
})();
</script>
<script src="../test-compiled-for-web/ripple-for-web-tests.js"></script>
<script>
mocha.ui('bdd')
</script>
<script src="../test-compiled-for-web/integration/integration-test.js"></script>
<script>
mocha.run()
</script>
</body>
</html>

63
test/localrunner.html Normal file
View File

@@ -0,0 +1,63 @@
<html>
<head>
<meta charset="utf-8">
<!-- encoding must be set for mocha's special characters to render properly -->
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="deb"></div>
<div id="mocha"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script>
(function() {
var phantomTest = /PhantomJS/;
if (phantomTest.test(navigator.userAgent)) {
// mocha-phantomjs-core has wrong shim for Function.bind, so we
// will replace it with correct one
// this bind polyfill copied from MDN documentation
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
// native functions don't have a prototype
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
})();
</script>
<script src="../test-compiled-for-web/ripple-for-web-tests.js"></script>
<script>
mocha.ui('bdd')
</script>
<script src="../test-compiled-for-web/api-test.js"></script>
<script src="../test-compiled-for-web/broadcast-api-test.js"></script>
<script src="../test-compiled-for-web/connection-test.js"></script>
<script src="../test-compiled-for-web/rangeset-test.js"></script>
<script>
mocha.run()
</script>
</body>
</html>

View File

@@ -72,6 +72,7 @@ module.exports = function(port) {
mock.on('connection', function(conn) { mock.on('connection', function(conn) {
this.socket = conn; this.socket = conn;
conn.config = {};
conn.on('message', function(requestJSON) { conn.on('message', function(requestJSON) {
const request = JSON.parse(requestJSON); const request = JSON.parse(requestJSON);
mock.emit('request_' + request.command, request, conn); mock.emit('request_' + request.command, request, conn);
@@ -95,10 +96,22 @@ module.exports = function(port) {
mock.expectedRequests[this.event] -= 1; mock.expectedRequests[this.event] -= 1;
}); });
mock.on('request_config', function(request, conn) {
assert.strictEqual(request.command, 'config');
conn.config = _.assign(conn.config, request.data);
});
mock.on('request_echo', function(request, conn) {
assert.strictEqual(request.command, 'echo');
conn.send(JSON.stringify(request.data));
});
mock.on('request_server_info', function(request, conn) { mock.on('request_server_info', function(request, conn) {
assert.strictEqual(request.command, 'server_info'); assert.strictEqual(request.command, 'server_info');
if (mock.returnErrorOnServerInfo) { if (conn.config.returnErrorOnServerInfo) {
conn.send(createResponse(request, fixtures.server_info.error)); conn.send(createResponse(request, fixtures.server_info.error));
} else if (conn.config.disconnectOnServerInfo) {
conn.terminate();
} else { } else {
conn.send(createResponse(request, fixtures.server_info.normal)); conn.send(createResponse(request, fixtures.server_info.normal));
} }

15
test/mocked-server.js Normal file
View File

@@ -0,0 +1,15 @@
'use strict';
const port = 34371;
const createMockRippled = require('./mock-rippled');
function main() {
console.log('starting server on port ' + port);
createMockRippled(port);
console.log('starting server on port ' + String(port + 1));
createMockRippled(port + 1);
}
main();

41
test/setup-api-web.js Normal file
View File

@@ -0,0 +1,41 @@
/* eslint-disable max-nested-callbacks */
'use strict';
const {RippleAPI, RippleAPIBroadcast} = require('ripple-api');
const ledgerClosed = require('./fixtures/rippled/ledger-close');
const port = 34371;
function setup(port_ = port) {
return new Promise((resolve, reject) => {
this.api = new RippleAPI({server: 'ws://localhost:' + port_});
this.api.connect().then(() => {
this.api.once('ledger', () => resolve());
this.api.connection._ws.emit('message', JSON.stringify(ledgerClosed));
}).catch(reject);
});
}
function setupBroadcast() {
const servers = [port, port + 1].map(port_ => 'ws://localhost:' + port_);
this.api = new RippleAPIBroadcast(servers);
return new Promise((resolve, reject) => {
this.api.connect().then(() => {
this.api.once('ledger', () => resolve());
this.api._apis[0].connection._ws.emit('message',
JSON.stringify(ledgerClosed));
}).catch(reject);
});
}
function teardown() {
if (this.api.isConnected()) {
return this.api.disconnect();
}
}
module.exports = {
setup: setup,
teardown: teardown,
setupBroadcast: setupBroadcast
};