mirror of
https://github.com/mkschreder/juci.git
synced 2025-01-07 03:16:39 +08:00
add unit test for uci.js
This commit is contained in:
parent
284ef14400
commit
a7234b93bf
3
juci/src/js/runtests.sh
Executable file
3
juci/src/js/runtests.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
istanbul cover _mocha -- -u exports -R tap
|
134
juci/src/js/test/test-uci.js
Normal file
134
juci/src/js/test/test-uci.js
Normal file
@ -0,0 +1,134 @@
|
||||
/*global.JSON = require("JSON");
|
||||
global.async = require("async");
|
||||
global.$ = global.jQuery = require("jquery-deferred");
|
||||
global.$.ajax = require("najax");
|
||||
global.async = require("async");
|
||||
global.watch = require("watchjs").watch;
|
||||
global.expect = require("expect.js");
|
||||
*/
|
||||
global.assert = require("assert");
|
||||
global.async = require("async");
|
||||
global.$ = global.jQuery = require("jquery-deferred");
|
||||
global.UBUS = {
|
||||
uci: {
|
||||
get: function(params){
|
||||
console.log("UCI get");
|
||||
var def = $.Deferred();
|
||||
setTimeout(function(){
|
||||
def.resolve({
|
||||
values: {
|
||||
".name": "mysection",
|
||||
".type": "test",
|
||||
field: "foo"
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
return def.promise();
|
||||
},
|
||||
set: function(params){
|
||||
console.log("UCI add");
|
||||
var def = $.Deferred();
|
||||
setTimeout(function(){
|
||||
def.resolve({
|
||||
});
|
||||
}, 0);
|
||||
return def.promise();
|
||||
},
|
||||
add: function(params){
|
||||
console.log("UCI add");
|
||||
var def = $.Deferred();
|
||||
setTimeout(function(){
|
||||
def.resolve({
|
||||
section: params["name"] || "unknown"
|
||||
});
|
||||
}, 0);
|
||||
return def.promise();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var uci = require("../uci");
|
||||
|
||||
UCI.$registerConfig("test");
|
||||
UCI.test.$registerSectionType("test", {
|
||||
"field": { dvalue: "test", type: String }
|
||||
});
|
||||
|
||||
|
||||
describe("Basic test", function(){
|
||||
it("tests the basic function of loading a config", function(done){
|
||||
UCI.$sync("test").done(function(){
|
||||
done();
|
||||
}).fail(function(){
|
||||
done(new Error("unable to sync config"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Section operations", function(){
|
||||
it("add a new section", function(done){
|
||||
UCI.test.$create({
|
||||
".name": "mysection",
|
||||
".type": "test",
|
||||
"field": "value"
|
||||
}).done(function(){
|
||||
var s = UCI.test["@test"][0];
|
||||
assert.equal(s.field.value, "value");
|
||||
assert.equal(UCI.test.mysection.field.value, "value");
|
||||
done();
|
||||
}).fail(function(){
|
||||
done(new Error("unable to create section"));
|
||||
});
|
||||
});
|
||||
|
||||
it("add a new section with invalid type", function(done){
|
||||
try {
|
||||
UCI.test.$create({
|
||||
".name": "mysection",
|
||||
".type": "noexist"
|
||||
}).done(function(){
|
||||
done(new Error("Able to create invlid sections"));
|
||||
}).fail(function(){
|
||||
done(new Error("unable to create section"));
|
||||
});
|
||||
} catch(err){
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("saves a section", function(done){
|
||||
UCI.$save().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("modifies a field and then saves a section", function(done){
|
||||
UCI.test.mysection.field.value = "newstuff";
|
||||
UCI.$save().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Field operations", function(){
|
||||
it("try setting different values", function(done){
|
||||
UCI.test.$create({
|
||||
".name": "another",
|
||||
".type": "test",
|
||||
"field": "value"
|
||||
}).done(function(){
|
||||
var s = UCI.test["@test"][1];
|
||||
assert.equal(s.field.value, "value");
|
||||
s.field.value = "me";
|
||||
assert.equal(s.field.value, "me");
|
||||
assert.equal(s.field.uvalue, "me");
|
||||
assert.equal(s.field.dvalue, "test");
|
||||
assert.equal(s.field.ovalue, "value");
|
||||
done();
|
||||
}).fail(function(){
|
||||
done(new Error("unable to create section"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -318,6 +318,9 @@
|
||||
$commit: function(){
|
||||
this.$update(this.uvalue, false);
|
||||
},
|
||||
get dvalue(){
|
||||
return this.schema.dvalue;
|
||||
},
|
||||
get value(){
|
||||
if(this.schema.type == Boolean){
|
||||
var uvalue = (this.uvalue == undefined)?this.ovalue:this.uvalue;
|
||||
@ -767,7 +770,10 @@
|
||||
|
||||
if(!$rpc.uci) {
|
||||
// this will happen if there is no router connection!
|
||||
setTimeout(function(){ deferred.reject(); }, 0);
|
||||
setTimeout(function(){
|
||||
console.error("uci is not defined!");
|
||||
deferred.reject();
|
||||
}, 0);
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
//! Author: Martin K. Schröder <mkschreder.uk@gmail.com>
|
||||
#!javascript
|
||||
var assert = require("assert");
|
||||
var JSON = require("juci/json");
|
||||
//global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
|
||||
global.$ = global.jQuery = require("jquery-deferred");
|
||||
global.$.ajax = require("najax");
|
||||
var async = require("async");
|
||||
require("../htdocs/lib/js/jquery-jsonrpc");
|
||||
//global.request = require("../htdocs/lib/js/request").request;
|
||||
require("../htdocs/js/rpc");
|
||||
require("../htdocs/js/uci");
|
||||
var $rpc = global.UBUS;
|
||||
var $uci = global.UCI;
|
||||
|
||||
var username;
|
||||
var password;
|
||||
|
||||
for(var i = 0; i < process.argv.length; i++){
|
||||
switch(process.argv[i]){
|
||||
case "--pass": password = process.argv[++i]; break;
|
||||
case "--user": username = process.argv[++i]; break;
|
||||
};
|
||||
}
|
||||
|
||||
if(!username || !password){
|
||||
console.error("Please specify -u <rpcuser> and -p <rpcpassword> arguments!");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function init(){
|
||||
var deferred = $.Deferred();
|
||||
async.series([
|
||||
function(next){
|
||||
$rpc.$init({host: "http://192.168.1.4"}).done(function(){
|
||||
console.log("Initialized rpc: "+Object.keys($rpc));
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next){
|
||||
$rpc.$login({username: username, password: password}).done(function(){
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next){
|
||||
$uci.$init().done(function(){
|
||||
console.log("Initialized uci: "+Object.keys($uci));
|
||||
next();
|
||||
});
|
||||
}
|
||||
], function(){
|
||||
deferred.resolve();
|
||||
});
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
exports.test = function(){
|
||||
console.log("test");
|
||||
}
|
||||
|
||||
describe("test", function(){
|
||||
it("should pass", function(done){
|
||||
init().done(function(){
|
||||
assert.equal(0, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,208 +0,0 @@
|
||||
global.assert = require("assert");
|
||||
global.JSON = require("juci/json");
|
||||
global.async = require("async");
|
||||
global.$ = global.jQuery = require("jquery-deferred");
|
||||
global.$.ajax = require("najax");
|
||||
global.async = require("async");
|
||||
global.watch = require("watchjs").watch;
|
||||
global.expect = require("expect.js");
|
||||
require("../htdocs/lib/js/jquery-jsonrpc");
|
||||
require("../htdocs/js/localStorage");
|
||||
require("../htdocs/js/rpc");
|
||||
require("../htdocs/js/uci");
|
||||
var $rpc = global.$rpc = global.UBUS;
|
||||
var $uci = global.$uci = global.UCI;
|
||||
Object.prototype.assign = require("object-assign");
|
||||
|
||||
global.annotate = function annotate(fn) {
|
||||
var $inject,
|
||||
fnText,
|
||||
argDecl,
|
||||
last;
|
||||
|
||||
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
|
||||
var FN_ARG_SPLIT = /,/;
|
||||
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
|
||||
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
|
||||
|
||||
if (typeof fn == 'function') {
|
||||
if (!($inject = fn.$inject)) {
|
||||
$inject = [];
|
||||
fnText = fn.toString().replace(STRIP_COMMENTS, '');
|
||||
argDecl = fnText.match(FN_ARGS);
|
||||
argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg){
|
||||
arg.replace(FN_ARG, function(all, underscore, name){
|
||||
$inject.push(name);
|
||||
});
|
||||
});
|
||||
fn.$inject = $inject;
|
||||
}
|
||||
} else if (isArray(fn)) {
|
||||
last = fn.length - 1;
|
||||
assertArgFn(fn[last], 'fn')
|
||||
$inject = fn.slice(0, last);
|
||||
} else {
|
||||
assertArgFn(fn, 'fn', true);
|
||||
}
|
||||
return $inject;
|
||||
}
|
||||
|
||||
function JUCIMock(){
|
||||
var _controllers = {};
|
||||
var self = this;
|
||||
self.app = {
|
||||
directive: function(name, fn){
|
||||
console.log("JUCI.app.directive("+name+")");
|
||||
return self.app;
|
||||
},
|
||||
controller: function(name, fn){
|
||||
console.log("JUCI.app.controller("+name+")");
|
||||
_controllers[name] = fn;
|
||||
return self.app;
|
||||
},
|
||||
config: function(){}
|
||||
}
|
||||
self.redirect = function(url){
|
||||
console.log("JUCIMock redirect: "+url);
|
||||
}
|
||||
function LocalStorageMock(){
|
||||
var _items = {};
|
||||
this.getItem = function(name){
|
||||
return _items[name];
|
||||
}
|
||||
this.setItem = function(name, value){
|
||||
_items[name] = value;
|
||||
}
|
||||
}
|
||||
function MockWindow(){
|
||||
var _location = "";
|
||||
this.location = {
|
||||
set href(value){
|
||||
console.log("window.location.href="+value);
|
||||
_location = value;
|
||||
},
|
||||
get href(){
|
||||
return _location;
|
||||
}
|
||||
}
|
||||
}
|
||||
global.window = new MockWindow();
|
||||
function MockLanguages(){
|
||||
var _currentLanguage;
|
||||
this.getLanguages = function(){
|
||||
return ["se", "en"];
|
||||
},
|
||||
this.setLanguage = function(short_code){
|
||||
_currentLanguage = short_code;
|
||||
}
|
||||
}
|
||||
var mocks = {
|
||||
"$window": global.window,
|
||||
"$state": { },
|
||||
"$session": global.UBUS.$session,
|
||||
"$localStorage": new LocalStorageMock(),
|
||||
"$rpc": global.UBUS,
|
||||
"$uci": global.UCI,
|
||||
"gettext": function(str) { return str; },
|
||||
"$tr": function(str) { return str; },
|
||||
"$languages": new MockLanguages()
|
||||
};
|
||||
global.controller = function(name, scope){
|
||||
if(_controllers[name] !== undefined){
|
||||
var ctrl = _controllers[name];
|
||||
scope.$apply = function(){
|
||||
console.log("$scope.apply");
|
||||
}
|
||||
scope.$watch = function(name, fn){
|
||||
global.watch(this, name, function(){
|
||||
fn(this[name]);
|
||||
});
|
||||
}
|
||||
var args = annotate(ctrl).map(function(x){
|
||||
switch(x){
|
||||
case "$scope":
|
||||
return scope;
|
||||
default:
|
||||
if(x in mocks) return mocks[x];
|
||||
else if(x in global) return global[x];
|
||||
else{
|
||||
console.error("Missing mock for "+x);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
ctrl.apply(ctrl, args);
|
||||
}
|
||||
else throw new Error("Controller "+name+" was not found!");
|
||||
}
|
||||
}
|
||||
|
||||
global.JUCI = new JUCIMock();
|
||||
|
||||
var PARAMS = {
|
||||
host: "localhost",
|
||||
username: "",
|
||||
password: ""
|
||||
};
|
||||
|
||||
for(var i = 0; i < process.argv.length; i++){
|
||||
switch(process.argv[i]){
|
||||
case "--pass": PARAMS.password = process.argv[++i]; break;
|
||||
case "--user": PARAMS.username = process.argv[++i]; break;
|
||||
case "--host": PARAMS.host = process.argv[++i]; break;
|
||||
};
|
||||
}
|
||||
|
||||
if(!PARAMS.username || !PARAMS.password ){
|
||||
console.error("Please specify --user <rpcuser> and --pass <rpcpassword> arguments!");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
global.PARAMS = PARAMS;
|
||||
|
||||
function init(){
|
||||
var deferred = $.Deferred();
|
||||
async.series([
|
||||
function(next){
|
||||
console.log("Trying to connect to RPC host '"+PARAMS.host+"'...");
|
||||
|
||||
$rpc.$init({host: "http://"+PARAMS.host}).done(function(){
|
||||
//console.log("Initialized rpc: "+Object.keys($rpc));
|
||||
next();
|
||||
}).fail(function(){
|
||||
throw new Error("Could not connect to rpc host!");
|
||||
});
|
||||
},
|
||||
function(next){
|
||||
$rpc.$login({username: PARAMS.username, password: PARAMS.password}).done(function(){
|
||||
next();
|
||||
}).fail(function(){
|
||||
throw new Error("Could not login over RPC using specified username and password!");
|
||||
});
|
||||
},
|
||||
function(next){
|
||||
$uci.$init().done(function(){
|
||||
//console.log("Initialized uci: "+Object.keys($uci));
|
||||
next();
|
||||
});
|
||||
}
|
||||
], function(){
|
||||
deferred.resolve();
|
||||
});
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
before(function(done){
|
||||
init().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
exports.$uci = $uci;
|
||||
exports.$init = init;
|
||||
exports.$rpc = $rpc;
|
||||
exports.app = {
|
||||
config: function(func){
|
||||
//func();
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
//! Author: Martin K. Schröder <mkschreder.uk@gmail.com>
|
||||
#!javascript
|
||||
require("./lib-juci");
|
||||
|
||||
describe("JUCI", function(){
|
||||
it("should have questd installed on router", function(){
|
||||
expect($rpc.router).to.be.an(Object);
|
||||
expect($rpc.router.info).to.be.a(Function);
|
||||
});
|
||||
it("should have juci installed on the router", function(){
|
||||
// TODO: rename juci stuff to juci
|
||||
expect($rpc.juci).to.be.an(Object);
|
||||
expect($rpc.juci.ui.menu).to.be.a(Function);
|
||||
});
|
||||
});
|
@ -1,156 +0,0 @@
|
||||
//! Author: Martin K. Schröder <mkschreder.uk@gmail.com>
|
||||
#!javascript
|
||||
require("./lib-juci");
|
||||
|
||||
describe("UCI", function(){
|
||||
beforeEach(function(done){
|
||||
$uci.$sync().done(function(){
|
||||
expect($uci).to.have.property("juci");
|
||||
var to_delete = $uci.juci["@test"].map(function(x){
|
||||
return x;
|
||||
});
|
||||
// have to use a copy of the array because deletion modifies the array
|
||||
async.eachSeries(to_delete, function(section, next){
|
||||
section.$delete().done(function(){
|
||||
next();
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to delete section "+section[".name"]+" from config juci!");
|
||||
});
|
||||
}, function(){
|
||||
$uci.save().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it("should have schema definition for each data field found in each defined uci config", function(done){
|
||||
//$uci.sync().done(function(){
|
||||
// get all configs that have been successfully loaded
|
||||
var configs_map = {} ;
|
||||
$uci.$eachConfig(function(cf){
|
||||
configs_map[cf[".name"]] = cf;
|
||||
});
|
||||
// for each config, retreive the raw data using rpc and compare that all fields in rpc data are present in the uci data
|
||||
async.eachSeries(Object.keys(configs_map), function(k, next){
|
||||
console.log("Testing config "+k);
|
||||
var cf = configs_map[k];
|
||||
$rpc.uci.state({config: k}).done(function(result){
|
||||
Object.keys(result.values).filter(function(x){
|
||||
return x in cf;
|
||||
}).map(function(name){
|
||||
var values = result.values[name];
|
||||
var section = cf[name];
|
||||
console.log(".. testing section "+name+" of type "+section[".type"]);
|
||||
expect(section).to.be.ok();
|
||||
var type = section[".section_type"];
|
||||
expect(type).to.be.ok();
|
||||
Object.keys(values).filter(function(x){
|
||||
return x.indexOf(".") != 0; // filter out hidden values
|
||||
}).map(function(x){
|
||||
expect(Object.keys(type)).to.contain(x);
|
||||
});
|
||||
});
|
||||
next();
|
||||
}).fail(function(){
|
||||
throw new Error("Have not been able to retreive state for config "+k);
|
||||
});
|
||||
}, function(){
|
||||
done();
|
||||
});
|
||||
//});
|
||||
});
|
||||
// will create an anonymous 'test' section and then delete it, commiting the changes
|
||||
it("should be able to add and delete an anonymous section", function(done){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
$uci.juci.create({".type": "test"}).done(function(section){
|
||||
expect($uci.juci["@test"]).to.be.ok();
|
||||
expect($uci.juci["@test"].length).not.to.be(0);
|
||||
expect($uci.juci["@test"]).to.contain(section);
|
||||
section.str.value = "test";
|
||||
$uci.save().done(function(){
|
||||
$uci.$sync("juci").done(function(){
|
||||
expect($uci.juci["@test"]).to.contain(section);
|
||||
expect($uci.juci[section[".name"]]).to.be.ok();
|
||||
expect($uci.juci[section[".name"]].str.value).to.eql("test");
|
||||
section.$delete().done(function(){
|
||||
expect(section[".name"]).to.be.ok();
|
||||
expect($uci.juci[section[".name"]]).not.to.be.ok();
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
$uci.save().done(function(){
|
||||
done();
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to save uci config after deleting section!");
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Deleting section has failed!");
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to sync config juci. Check that acl list on the server allows access to juci config!");
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to save newly created section in juci config!");
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to create section 'test' in config juci");
|
||||
});
|
||||
});
|
||||
// will try to resync (old changes should be reverted also on the server)
|
||||
it("should remove previously created section if resync is done without saving", function(done){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
$uci.juci.create({".type": "test"}).done(function(section){
|
||||
expect($uci.juci["@test"]).to.contain(section);
|
||||
expect($uci.juci["@test"].length).to.be(1);
|
||||
$uci.$sync("juci").done(function(){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
expect($uci.juci["@test"]).not.to.contain(section);
|
||||
$uci.save().done(function(){ // this should not commit previulsy added section!
|
||||
$uci.$sync("juci").done(function(){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
expect($uci.juci["@test"]).not.to.contain(section);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to sync config juci. Check that acl list on the server allows access to juci config!");
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Was unable to create section 'test' in config juci");
|
||||
});
|
||||
});
|
||||
it("should be able to add a named section", function(done){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
$uci.juci.create({".type": "test", ".name": "testname"}).done(function(section){
|
||||
expect($uci.juci.testname).to.be.ok();
|
||||
$uci.save().done(function(){
|
||||
$uci.$sync("juci").done(function(){
|
||||
expect($uci.juci.testname).to.be.ok();
|
||||
expect($uci.juci["@test"]).to.contain(section);
|
||||
expect($uci.juci["@test"].length).to.be(1);
|
||||
section.$delete().done(function(){
|
||||
$uci.save().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Could not create named section in juci config!");
|
||||
});
|
||||
});
|
||||
it("should not be able to add a section that already exists", function(done){
|
||||
expect($uci.juci["@test"]).to.be.empty();
|
||||
$uci.juci.create({".type": "test", ".name": "testname"}).done(function(section){
|
||||
$uci.juci.create({".type": "test", ".name": "testname"}).done(function(section2){
|
||||
throw new Error("Creating a section that already exists returned success when it should fail!");
|
||||
}).fail(function(){
|
||||
section.$delete().done(function(){
|
||||
$uci.save().done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).fail(function(){
|
||||
throw new Error("Failed to create section testname when it was supposed to work fine!");
|
||||
});
|
||||
});
|
||||
});
|
22
package.json
22
package.json
@ -12,24 +12,26 @@
|
||||
"object-assign": "*",
|
||||
"jquery-deferred": "*",
|
||||
"watchjs": "*",
|
||||
"uglify-js": "*",
|
||||
"uglify-js": "*",
|
||||
"array.prototype.find": "*",
|
||||
"node-getopt": "*",
|
||||
"gettext-parser": "*",
|
||||
"underscore": "*",
|
||||
"ws": "*",
|
||||
"jquery-deferred": "*",
|
||||
"express-ws": "*"
|
||||
"node-getopt": "*",
|
||||
"gettext-parser": "*",
|
||||
"underscore": "*",
|
||||
"ws": "*",
|
||||
"express-ws": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.10.x",
|
||||
"npm": "1.2.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.0.1",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasmine-core": "~2.3.3",
|
||||
"karma-jasmine": "~0.3.5",
|
||||
"karma": "^1.3.0",
|
||||
"karma-firefox-launcher": "~0.1.6",
|
||||
"mocha": "~2.2.5",
|
||||
"karma-mocha": "~0.1.10"
|
||||
"karma-jasmine": "~0.3.5",
|
||||
"karma-mocha": "~0.1.10",
|
||||
"mocha": "~2.2.5"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user