<commit_before>var _        = require('lodash');
var should   = require('should');
var helper   = require('../support/spec_helper');
var async    = require('async');
var ORM      = require('../../');

describe("Hook", function() {
	var db = null;
	var Person = null;

	var triggeredHooks = {};

	var checkHook = function (hook) {
		triggeredHooks[hook] = false;

		return function () {
			triggeredHooks[hook] = Date.now();
		};
	};

	var setup = function (hooks) {
		if (typeof hooks == "undefined") {
			hooks = {
				afterCreate      : checkHook("afterCreate"),
				beforeCreate     : checkHook("beforeCreate"),
				afterSave        : checkHook("afterSave"),
				beforeSave       : checkHook("beforeSave"),
				beforeValidation : checkHook("beforeValidation"),
				beforeRemove     : checkHook("beforeRemove"),
				afterRemove      : checkHook("afterRemove")
			};
		}

		return function (done) {
			Person = db.define("person", {
				name   : String
			}, {
				hooks  : hooks
			});

			return helper.dropSync(Person, done);
		};
	};

	before(function (done) {
		helper.connect(function (connection) {
			db = connection;

			return done();
		});
	});

	after(function () {
		return db.close();
	});

	// there are a lot of timeouts in this suite and Travis or other test runners can
	// have hickups that could force this suite to timeout to the default value (2 secs)
	this.timeout(30000);

	describe("beforeCreate", function () {
		before(setup());

		it("should trigger before creating instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.not.be.above(triggeredHooks.afterCreate);

				return done();
			});
		});

		describe("when setting properties", function () {
			before(setup({
				beforeCreate : function () {
					this.name = "Jane Doe";
				}
			}));

			it("should not be discarded", function (done) {
				Person.create([{ }], function (err, items) {
					should.equal(err, null);

					items.should.be.a("object");
					items.should.have.property("length", 1);
					items[0].name.should.equal("Jane Doe");

					return done();
				});
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeCreate = false;

			before(setup({
				beforeCreate : function (next) {
					setTimeout(function () {
						beforeCreate = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeCreate.should.be.true;

					return done();
				});

			});
		});
	});

	describe("afterCreate", function () {
		before(setup());

		it("should trigger after creating instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.afterCreate.should.not.be.below(triggeredHooks.beforeCreate);

				return done();
			});
		});
	});

	describe("beforeSave", function () {
		before(setup());

		it("should trigger before saving an instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterSave.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.beforeSave.should.not.be.above(triggeredHooks.afterSave);

				return done();
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeSave = false;

			before(setup({
				beforeSave : function (next) {
					setTimeout(function () {
						beforeSave = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeSave.should.be.true;

					return done();
				});

			});
		});
	});

	describe("afterSave", function () {
		before(setup());

		it("should trigger after saving an instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterSave.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.afterSave.should.not.be.below(triggeredHooks.beforeSave);

				return done();
			});
		});
	});

	describe("beforeValidation", function () {
		before(setup());

		it("should trigger before instance validation", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.beforeValidation.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.beforeValidation.should.not.be.above(triggeredHooks.beforeCreate);
				triggeredHooks.beforeValidation.should.not.be.above(triggeredHooks.beforeSave);

				return done();
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeValidation = false;

			before(setup({
				beforeValidation : function (next) {
					setTimeout(function () {
						beforeValidation = true;

						if (!this.name) return next("Name is missing");

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeValidation.should.be.true;

					return done();
				});
			});

			it("should trigger error if hook passes an error", function (done) {
				this.timeout(500);

				Person.create([{ name: "" }], function (err) {
					beforeValidation.should.be.true;

					err.should.equal("Name is missing");

					return done();
				});
			});
		});
	});

	describe("afterLoad", function () {
		var afterLoad = false;

		before(setup({
			afterLoad: function () {
				afterLoad = true;
			}
		}));

		it("should trigger when defining a model", function (done) {
			var John = new Person({ name: "John" });

			afterLoad.should.be.true;

			return done();
		});
	});

	describe("beforeRemove", function () {
		before(setup());

		it("should trigger before removing an instance", function (done) {
			Person.create([{ name: "John Doe" }], function (err, items) {
				items[0].remove(function () {
					triggeredHooks.afterRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.not.be.above(triggeredHooks.afterRemove);

					return done();
				});
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeRemove = false;

			before(setup({
				beforeRemove : function (next) {
					setTimeout(function () {
						beforeRemove = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function (err, items) {
					items[0].remove(function () {
						beforeRemove.should.be.true;

						return done();
					});
				});

			});
		});
	});

	describe("afterRemove", function () {
		before(setup());

		it("should trigger after removing an instance", function (done) {
			Person.create([{ name: "John Doe" }], function (err, items) {
				items[0].remove(function () {
					triggeredHooks.afterRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.be.a("number");
					triggeredHooks.afterRemove.should.not.be.below(triggeredHooks.beforeRemove);

					return done();
				});
			});
		});
	});
});
<commit_msg>Updates new hook tests to check error triggering<commit_after>var _        = require('lodash');
var should   = require('should');
var helper   = require('../support/spec_helper');
var async    = require('async');
var ORM      = require('../../');

describe("Hook", function() {
	var db = null;
	var Person = null;

	var triggeredHooks = {};

	var checkHook = function (hook) {
		triggeredHooks[hook] = false;

		return function () {
			triggeredHooks[hook] = Date.now();
		};
	};

	var setup = function (hooks) {
		if (typeof hooks == "undefined") {
			hooks = {
				afterCreate      : checkHook("afterCreate"),
				beforeCreate     : checkHook("beforeCreate"),
				afterSave        : checkHook("afterSave"),
				beforeSave       : checkHook("beforeSave"),
				beforeValidation : checkHook("beforeValidation"),
				beforeRemove     : checkHook("beforeRemove"),
				afterRemove      : checkHook("afterRemove")
			};
		}

		return function (done) {
			Person = db.define("person", {
				name   : String
			}, {
				hooks  : hooks
			});

			Person.settings.set("instance.returnAllErrors", false);

			return helper.dropSync(Person, done);
		};
	};

	before(function (done) {
		helper.connect(function (connection) {
			db = connection;

			return done();
		});
	});

	after(function () {
		return db.close();
	});

	// there are a lot of timeouts in this suite and Travis or other test runners can
	// have hickups that could force this suite to timeout to the default value (2 secs)
	this.timeout(30000);

	describe("beforeCreate", function () {
		before(setup());

		it("should trigger before creating instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.not.be.above(triggeredHooks.afterCreate);

				return done();
			});
		});

		describe("when setting properties", function () {
			before(setup({
				beforeCreate : function () {
					this.name = "Jane Doe";
				}
			}));

			it("should not be discarded", function (done) {
				Person.create([{ }], function (err, items) {
					should.equal(err, null);

					items.should.be.a("object");
					items.should.have.property("length", 1);
					items[0].name.should.equal("Jane Doe");

					return done();
				});
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeCreate = false;

			before(setup({
				beforeCreate : function (next) {
					setTimeout(function () {
						beforeCreate = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeCreate.should.be.true;

					return done();
				});
			});

			describe("if hook triggers error", function () {
				before(setup({
					beforeCreate : function (next) {
						setTimeout(function () {
							return next(new Error('beforeCreate-error'));
						}, 200);
					}
				}));

				it("should trigger error", function (done) {
					this.timeout(500);

					Person.create([{ name: "John Doe" }], function (err) {
						err.should.be.a("object");
						err.message.should.equal("beforeCreate-error");

						return done();
					});
				});
			});
		});
	});

	describe("afterCreate", function () {
		before(setup());

		it("should trigger after creating instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterCreate.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.afterCreate.should.not.be.below(triggeredHooks.beforeCreate);

				return done();
			});
		});
	});

	describe("beforeSave", function () {
		before(setup());

		it("should trigger before saving an instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterSave.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.beforeSave.should.not.be.above(triggeredHooks.afterSave);

				return done();
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeSave = false;

			before(setup({
				beforeSave : function (next) {
					setTimeout(function () {
						beforeSave = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeSave.should.be.true;

					return done();
				});

			});

			describe("if hook triggers error", function () {
				before(setup({
					beforeSave : function (next) {
						setTimeout(function () {
							return next(new Error('beforeSave-error'));
						}, 200);
					}
				}));

				it("should trigger error", function (done) {
					this.timeout(500);

					Person.create([{ name: "John Doe" }], function (err) {
						err.should.be.a("object");
						err.message.should.equal("beforeSave-error");

						return done();
					});
				});
			});
		});
	});

	describe("afterSave", function () {
		before(setup());

		it("should trigger after saving an instance", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.afterSave.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.afterSave.should.not.be.below(triggeredHooks.beforeSave);

				return done();
			});
		});
	});

	describe("beforeValidation", function () {
		before(setup());

		it("should trigger before instance validation", function (done) {
			Person.create([{ name: "John Doe" }], function () {
				triggeredHooks.beforeValidation.should.be.a("number");
				triggeredHooks.beforeCreate.should.be.a("number");
				triggeredHooks.beforeSave.should.be.a("number");
				triggeredHooks.beforeValidation.should.not.be.above(triggeredHooks.beforeCreate);
				triggeredHooks.beforeValidation.should.not.be.above(triggeredHooks.beforeSave);

				return done();
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeValidation = false;

			before(setup({
				beforeValidation : function (next) {
					setTimeout(function () {
						beforeValidation = true;

						if (!this.name) return next("Name is missing");

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function () {
					beforeValidation.should.be.true;

					return done();
				});
			});

			it("should trigger error if hook passes an error", function (done) {
				this.timeout(500);

				Person.create([{ name: "" }], function (err) {
					beforeValidation.should.be.true;

					err.should.equal("Name is missing");

					return done();
				});
			});
		});
	});

	describe("afterLoad", function () {
		var afterLoad = false;

		before(setup({
			afterLoad: function () {
				afterLoad = true;
			}
		}));

		it("should trigger when defining a model", function (done) {
			var John = new Person({ name: "John" });

			afterLoad.should.be.true;

			return done();
		});
	});

	describe("beforeRemove", function () {
		before(setup());

		it("should trigger before removing an instance", function (done) {
			Person.create([{ name: "John Doe" }], function (err, items) {
				items[0].remove(function () {
					triggeredHooks.afterRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.not.be.above(triggeredHooks.afterRemove);

					return done();
				});
			});
		});

		describe("if hook method has 1 argument", function () {
			var beforeRemove = false;

			before(setup({
				beforeRemove : function (next) {
					setTimeout(function () {
						beforeRemove = true;

						return next();
					}.bind(this), 200);
				}
			}));

			it("should wait for hook to finish", function (done) {
				this.timeout(500);

				Person.create([{ name: "John Doe" }], function (err, items) {
					items[0].remove(function () {
						beforeRemove.should.be.true;

						return done();
					});
				});

			});

			describe("if hook triggers error", function () {
				before(setup({
					beforeRemove : function (next) {
						setTimeout(function () {
							return next(new Error('beforeRemove-error'));
						}, 200);
					}
				}));

				it("should trigger error", function (done) {
					this.timeout(500);

					Person.create([{ name: "John Doe" }], function (err, items) {
						items[0].remove(function (err) {
							err.should.be.a("object");
							err.message.should.equal("beforeRemove-error");

							return done();
						});
					});
				});
			});
		});
	});

	describe("afterRemove", function () {
		before(setup());

		it("should trigger after removing an instance", function (done) {
			Person.create([{ name: "John Doe" }], function (err, items) {
				items[0].remove(function () {
					triggeredHooks.afterRemove.should.be.a("number");
					triggeredHooks.beforeRemove.should.be.a("number");
					triggeredHooks.afterRemove.should.not.be.below(triggeredHooks.beforeRemove);

					return done();
				});
			});
		});
	});
});
<|endoftext|>


<commit_before>if (true) {

  var log = require('domlog');
  var messages = {
    request: "\u21A0",
    response: "\u219E",
    input: "\u2190",
    output: "\u2192",
    exec: "exec",
    connect: "connect",
    save: "\u2907",
    load: "\u2906",
    remove: "\u2716",
    read: "\u2770",
    write: "\u2771",
  };

  module.exports = function (type, stream, item) {
    var message = messages[type];
    if (!stream) {
      return log(message || type, item);
    }
    if (!message) return stream;
    return { read: traceRead, abort: stream.abort };
    function traceRead(callback) {
      stream.read(function (err, item) {
        if (err) return callback(err);
        log(message, item);
        callback(null, item);
      });
    }
  }
}
else {
  module.exports = false;
}
<commit_msg>Trace to console too<commit_after>if (true) {

  var log = require('domlog');
  var messages = {
    request: "\u21A0",
    response: "\u219E",
    input: "\u2190",
    output: "\u2192",
    exec: "exec",
    connect: "connect",
    save: "\u2907",
    load: "\u2906",
    remove: "\u2716",
    read: "\u2770",
    write: "\u2771",
  };

  module.exports = function (type, stream, item) {
    var message = messages[type];
    if (!stream) {
      console.log(message || type, item);
      return log(message || type, item);
    }
    if (!message) return stream;
    return { read: traceRead, abort: stream.abort };
    function traceRead(callback) {
      stream.read(function (err, item) {
        if (err) return callback(err);
        log(message, item);
        callback(null, item);
      });
    }
  };
}
else {
  module.exports = false;
}
<|endoftext|>


<commit_before>var val = require('validator');
var fs = require('fs');
var path = require('path');

module.exports.string = function(string){
	return val.toString(string);
}
module.exports.bool = function(string){
	if (!val.isBoolean(string)){
		throw new Error();
	}
	return val.toBoolean(string);
}
module.exports.float = function(string){
	if (!val.isFloat(string)){
		throw new Error();
	}
	return val.toFloat(string);
}
module.exports.int = function(string){
	if (!val.isInt(string)){
		throw new Error();
	}
	return val.toInt(string);
}
module.exports.card = function(string){
	if (!val.isDate(string)){
		throw new Error();
	}
	return val.toDate(string);
}

module.exports.file = function(object){
	if (!object.hasOwnProperty('path')){
		throw new Error();
	}
	if (object.size == 0){
		throw new Error();
	}

	return object;
}

module.exports.ip = function(string){
	if (!val.isIP(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.mongo = function(string){
	if (!val.isMongoId(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.url = function(string){
	if (!val.isURL(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.base64 = function(string){
	if (!val.isBase64(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.card = function(string){
	if (!val.isCreditCard(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.email = function(string){
	if (!val.isEmail(string)){
		throw new Error();
	}
	return val.toString(string);
}<commit_msg>Updated types<commit_after>var val = require('validator');
var fs = require('fs');
var path = require('path');

module.exports.string = function(string){
	return val.toString(string);
}
module.exports.bool = function(string){
	if (!val.isBoolean(string)){
		throw new Error();
	}
	return val.toBoolean(string);
}
module.exports.float = function(string){
	if (!val.isFloat(string)){
		throw new Error();
	}
	return val.toFloat(string);
}
module.exports.int = function(string){
	if (!val.isInt(string)){
		throw new Error();
	}
	return val.toInt(string);
}
module.exports.date = function(string){
	if (!val.isDate(string)){
		throw new Error();
	}
	return val.toDate(string);
}

module.exports.file = function(object){
	if (!object.hasOwnProperty('path')){
		throw new Error();
	}
	if (object.size == 0){
		throw new Error();
	}

	return object;
}

module.exports.ip = function(string){
	if (!val.isIP(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.mongo = function(string){
	if (!val.isMongoId(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.url = function(string){
	if (!val.isURL(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.base64 = function(string){
	if (!val.isBase64(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.card = function(string){
	if (!val.isCreditCard(string)){
		throw new Error();
	}
	return val.toString(string);
}
module.exports.email = function(string){
	if (!val.isEmail(string)){
		throw new Error();
	}
	return val.toString(string);
}<|endoftext|>


<commit_before>const Q = require('../../lib/qbuilder')
const Lab = require('@hapi/lab')
const lab = exports.lab = Lab.script()
const { describe, before, after, it } = lab

describe('qbuilder', () => {
  /*
  it('', async () => {
    const ins_node = insert({
      into: 'users',
      values: { id: 'aaa', email: 'rr@voxgig.com' }
    })

    console.dir(ins_node), { depth: 8 }
    console.dir(toSql(ins_node), { depth: 4 })
  })
  */

  it('', async () => {
    const sel_node = select({
      columns: ['id', 'email'],
      from: 'users',
      where: exists(select({
        columns: '*',
        from: 'users',
        where: { id: 'aaaa' }
      })),
      limit: 1,
      offset: 5,
      order_by: { email: -1, id: 1 }
    })

    console.dir(sel_node, { depth: null })
    console.dir(toSql(sel_node), { depth: 4 })
  })
})

const Assert = require('assert')

class QNode {}

/*
type insert_t = {
  whatami$: "insert_t";
  into$: string;
  values$: object;
}
*/
function insert(args) {
  const { into, values } = args

  Assert.strictEqual(typeof into, 'string', 'into')
  Assert(values, 'values')

  return new InsertStm({
    into$: into,
    values$: values
  })
}

class InsertStm extends QNode {
  constructor(args) {
    super(args)

    this.into$ = args.into$
    this.values$ = args.values$
  }
}

class Op extends QNode {}

class UnaryOp extends Op {}

class ExistsOp extends UnaryOp {
  constructor(args) {
    super(args)

    Assert(args, 'args')
    Assert(args.expr$ instanceof SelectStm, 'expr$')

    this.expr$ = args.expr$
  }
}

class BinaryOp extends Op {}

class EqOp extends BinaryOp {
  constructor(args) {
    super(args)

    this.column$ = args.column$
    this.value$ = args.value$
  }
}

class AndOp extends BinaryOp {
  constructor(args) {
    super(args)

    this.lexpr$ = args.lexpr$
    this.rexpr$ = args.rexpr$
  }
}

/*
type expr_t = [
| select_t
| expr_null_t
| expr_not_t
| expr_eq_t
| expr_exists_t
]

type expr_null_t = {
  expr$: expr_t
}

type expr_not_t = {
  expr$: expr_t
}

type expr_in_t = {
  unsafe_args$: 'a array
}

type expr_eq_t = {
  column$: string;
  value$: 'a
}

type expr_exists_t = {
  whatami$: 'expr_exists_t';
  expr$: select_t
}
*/

function exists(expr) {
  return new ExistsOp({ expr$: expr })
}

function where(obj) {
  if (obj instanceof UnaryOp || obj instanceof BinaryOp) {
    return obj
  }

  if (obj instanceof QNode) {
    throw new Error(`The where-object is of the unsupported type`)
  }

  const kvs = Object.keys(obj).map(k => [k, obj[k]])

  const expr = kvs.reduce((acc, [colname, x]) => {
    const eq_expr = x instanceof QNode
      ? x
      : new EqOp({ column$: colname, value$: x })

    if (!acc) {
      return eq_expr
    }

    return new AndOp({ lexpr$: acc, rexpr$: eq_expr })
  }, null)

  return expr
}

/*
  type select_t = {
    from$: string;
    columns$: string array;
    offset$: int;
    limit$: int;
    order_by$: { [string]: [ -inf..-1 | 0..inf | 'desc' | 'asc' ] }
  }
*/
class SelectStm {
  constructor(args) {
    this.from$ = args.from$
    this.columns$ = args.column$ || '*'
    this.offset$ = args.offset$ || null
    this.limit$ = args.limit$ || null
    this.order_by$ = args.order_by$ || null
    this.where$ = args.where$ || null
  }
}

function select(args) {
  const {
    from,
    columns = '*',
    offset = null,
    limit = null,
    order_by = null,
    where: w
  } = args

  return new SelectStm({
    from$: from,
    columns$: columns,
    offset$: offset,
    limit$: limit,
    order_by$: order_by,
    where$: where(w)
  })
}

function sqlOfOrderBy(node) {
  let sql = ''
  let bindings = []

  let first_pair = true

  for (const order_col in node) {
    const order_val = node[order_col]

    let order

    if ('string' === typeof order_val) {
      if ('desc' === order_val.toLowerCase()) {
        order = 'desc'
      } else if ('asc' === order_val.toLowerCase()) {
        order = 'asc'
      } else {
        throw new Error(`Unknown order: ${order_val}`)
      }
    } else if ('number' === typeof order_val) {
      order = 0 <= order_val ? 'asc' : 'desc'
    } else {
      throw new Error('order must be a number or a string')
    }

    if (!first_pair) {
      sql += ', '
    }

    sql += '?? ' + order
    bindings.push(order_col)

    first_pair = false
  }

  return { sql, bindings }
}

function sqlOfExpr(node) {
  if (node instanceof SelectStm) {
    return sqlOfSelect(node)
  }

  if (node instanceof EqOp) {
    return {
      sql: '?? = ?',
      bindings: [node.column$, node.value$]
    }
  }

  if (node instanceof ExistsOp) {
    return sqlOfExistsExpr(node)
  }

  if (node instanceof AndOp) {
    const { lexpr$, rexpr$ } = node

    const lexpr_sql = sqlOfExpr(lexpr$)
    const rexpr_sql = sqlOfExpr(rexpr$)

    return {
      sql: lexpr_sql.sql + ' and ' + rexpr_sql.sql,
      bindings: [].concat(lexpr_sql.bindings, rexpr_sql.bindings)
    }
  }

  Assert.fail(`Unknown expression type: "${node && node.constructor}"`)
}

function sqlOfSelect(node) {
  Assert(node instanceof SelectStm, 'node')

  const {
    from$,
    columns$ = '*',
    offset$ = null,
    limit$ = null,
    order_by$ = null,
    where$ = null
  } = node

  let bindings = []
  let sql = ''

  sql += 'select '

  if ('*' === columns$) {
    sql += '*'
  } else {
    const col_placeholders = columns$.map(_ => '??')
    sql += col_placeholders.join(', ')

    bindings = bindings.concat(columns$)
  }

  sql += ' from ??'
  bindings.push(from$)

  if (null != where$) {
    const where = sqlOfExpr(where$)

    sql += ' where ' + where.sql
    bindings = bindings.concat(where.bindings)
  }

  if (null != order_by$) {
    const order_q = sqlOfOrderBy(order_by$)

    sql += ' order by ' + order_q.sql
    bindings = bindings.concat(order_q.bindings)
  }

  if (null != limit$) {
    sql += ' limit ?'
    bindings.push(limit$)
  }

  if (null != offset$) {
    sql += ' offset ?'
    bindings.push(offset$)
  }

  return { sql, bindings }
}

function sqlOfExistsExpr(node) {
  Assert(node instanceof ExistsOp, 'node')

  const { expr$ } = node
  const q = sqlOfSelect(expr$)

  return {
    sql: 'exists (' + q.sql + ')',
    bindings: q.bindings
  }
}

function sqlOfInsert(node) {
  Assert(node instanceof InsertStm, 'node')

  const { into$, values$ } = node

  const col_names = Object.keys(values$)
  const col_vals = Object.values(values$)

  let bindings = []
  let sql = ''

  sql += 'insert into ?? '
  bindings.push(into$)

  const col_placeholders = col_names.map(_ => '??')
  sql += '(' + col_placeholders.join(', ') + ') '
  bindings = bindings.concat(col_names)

  const val_placeholders = col_vals.map(_ => '?')
  sql += 'values (' + val_placeholders.join(', ') + ') '
  bindings = bindings.concat(col_vals)

  return { sql, bindings }
}

function toSql(node) {
  Assert(node, 'node')

  if (node instanceof InsertStm) {
    return sqlOfInsert(node)
  }

  if (node instanceof SelectStm) {
    return sqlOfSelect(node)
  }

  Assert.fail(`Unknown node type: ${node && node.constructor}`)
}

<commit_msg>Implement the IS NULL and IN operators<commit_after>const Q = require('../../lib/qbuilder')
const Lab = require('@hapi/lab')
const lab = exports.lab = Lab.script()
const { describe, before, after, it } = lab

describe('qbuilder', () => {
  /*
  it('', async () => {
    const ins_node = insert({
      into: 'users',
      values: { id: 'aaa', email: 'rr@voxgig.com' }
    })

    console.dir(ins_node), { depth: 8 }
    console.dir(toSql(ins_node), { depth: 4 })
  })
  */

  it('', async () => {
    const sel_node = select({
      columns: ['id', 'email'],
      from: 'users',
      where: exists(select({
        columns: '*',
        from: 'users',
        where: { id: 'aaaa', email: null, favorite_fruits: ['oranges', 'melon'] }
      })),
      limit: 1,
      offset: 5,
      order_by: { email: -1, id: 1 }
    })

    console.dir(sel_node, { depth: null })
    console.dir(toSql(sel_node), { depth: 4 })
  })
})

const Assert = require('assert')

class QNode {}

/*
type insert_t = {
  whatami$: "insert_t";
  into$: string;
  values$: object;
}
*/
function insert(args) {
  const { into, values } = args

  Assert.strictEqual(typeof into, 'string', 'into')
  Assert(values, 'values')

  return new InsertStm({
    into$: into,
    values$: values
  })
}

class InsertStm extends QNode {
  constructor(args) {
    super(args)

    this.into$ = args.into$
    this.values$ = args.values$
  }
}

class Op extends QNode {}

class UnaryOp extends Op {}

class ExistsOp extends UnaryOp {
  constructor(args) {
    super(args)

    Assert(args, 'args')
    Assert(args.expr$ instanceof SelectStm, 'expr$')

    this.expr$ = args.expr$
  }
}

/*
class NotOp extends UnaryOp {
}
*/

class InOp extends UnaryOp {
  constructor(args) {
    super(args)

    Assert(args, 'args')
    Assert(Array.isArray(args.values$), 'args.values$')

    this.column$ = args.column$
    this.values$ = args.values$
  }
}

class NullOp extends UnaryOp {
  constructor(args) {
    super(args)

    this.column$ = args.column$
  }
}

class BinaryOp extends Op {}

class EqOp extends BinaryOp {
  constructor(args) {
    super(args)

    this.column$ = args.column$
    this.value$ = args.value$
  }
}

class AndOp extends BinaryOp {
  constructor(args) {
    super(args)

    this.lexpr$ = args.lexpr$
    this.rexpr$ = args.rexpr$
  }
}

/*
type expr_t = [
| select_t
| expr_null_t
| expr_not_t
| expr_eq_t
| expr_exists_t
]

type expr_null_t = {
  expr$: expr_t
}

type expr_not_t = {
  expr$: expr_t
}

type expr_in_t = {
  unsafe_args$: 'a array
}

type expr_eq_t = {
  column$: string;
  value$: 'a
}

type expr_exists_t = {
  whatami$: 'expr_exists_t';
  expr$: select_t
}
*/

function exists(expr) {
  return new ExistsOp({ expr$: expr })
}

function where(obj) {
  if (obj instanceof UnaryOp || obj instanceof BinaryOp) {
    return obj
  }

  if (obj instanceof QNode) {
    throw new Error(`The where-object is of the unsupported type`)
  }

  const kvs = Object.keys(obj).map(k => [k, obj[k]])

  const expr = kvs.reduce((acc, [colname, x]) => {
    let subexpr

    if (x instanceof QNode) {
      subexpr = x
    } else {
      if (null == x) {
        subexpr = new NullOp({ column$: colname })
      } else if (Array.isArray(x)) {
        subexpr = new InOp({ column$: colname, values$: x })
      } else {
        subexpr = new EqOp({ column$: colname, value$: x })
      }
    }

    if (!acc) {
      return subexpr
    }

    return new AndOp({ lexpr$: acc, rexpr$: subexpr })
  }, null)

  return expr
}

/*
  type select_t = {
    from$: string;
    columns$: string array;
    offset$: int;
    limit$: int;
    order_by$: { [string]: [ -inf..-1 | 0..inf | 'desc' | 'asc' ] }
  }
*/
class SelectStm {
  constructor(args) {
    this.from$ = args.from$
    this.columns$ = args.column$ || '*'
    this.offset$ = args.offset$ || null
    this.limit$ = args.limit$ || null
    this.order_by$ = args.order_by$ || null
    this.where$ = args.where$ || null
  }
}

function select(args) {
  const {
    from,
    columns = '*',
    offset = null,
    limit = null,
    order_by = null,
    where: w
  } = args

  return new SelectStm({
    from$: from,
    columns$: columns,
    offset$: offset,
    limit$: limit,
    order_by$: order_by,
    where$: where(w)
  })
}

function sqlOfOrderBy(node) {
  let sql = ''
  let bindings = []

  let first_pair = true

  for (const order_col in node) {
    const order_val = node[order_col]

    let order

    if ('string' === typeof order_val) {
      if ('desc' === order_val.toLowerCase()) {
        order = 'desc'
      } else if ('asc' === order_val.toLowerCase()) {
        order = 'asc'
      } else {
        throw new Error(`Unknown order: ${order_val}`)
      }
    } else if ('number' === typeof order_val) {
      order = 0 <= order_val ? 'asc' : 'desc'
    } else {
      throw new Error('order must be a number or a string')
    }

    if (!first_pair) {
      sql += ', '
    }

    sql += '?? ' + order
    bindings.push(order_col)

    first_pair = false
  }

  return { sql, bindings }
}

function sqlOfExpr(node) {
  if (node instanceof SelectStm) {
    return sqlOfSelect(node)
  }

  if (node instanceof EqOp) {
    return {
      sql: '?? = ?',
      bindings: [node.column$, node.value$]
    }
  }

  if (node instanceof NullOp) {
    return {
      sql: '?? is null',
      bindings: [node.column$]
    }
  }

  if (node instanceof InOp) {
    const tuple = node.values$.map(_ => '?')

    return {
      sql: '?? in (' + tuple.join(', ') + ')',
      bindings: node.values$
    }
  }

  if (node instanceof ExistsOp) {
    return sqlOfExistsExpr(node)
  }

  if (node instanceof AndOp) {
    const { lexpr$, rexpr$ } = node

    const lexpr_sql = sqlOfExpr(lexpr$)
    const rexpr_sql = sqlOfExpr(rexpr$)

    return {
      sql: lexpr_sql.sql + ' and ' + rexpr_sql.sql,
      bindings: [].concat(lexpr_sql.bindings, rexpr_sql.bindings)
    }
  }

  Assert.fail(`Unknown expression type: "${node && node.constructor}"`)
}

function sqlOfSelect(node) {
  Assert(node instanceof SelectStm, 'node')

  const {
    from$,
    columns$ = '*',
    offset$ = null,
    limit$ = null,
    order_by$ = null,
    where$ = null
  } = node

  let bindings = []
  let sql = ''

  sql += 'select '

  if ('*' === columns$) {
    sql += '*'
  } else {
    const col_placeholders = columns$.map(_ => '??')
    sql += col_placeholders.join(', ')

    bindings = bindings.concat(columns$)
  }

  sql += ' from ??'
  bindings.push(from$)

  if (null != where$) {
    const where = sqlOfExpr(where$)

    sql += ' where ' + where.sql
    bindings = bindings.concat(where.bindings)
  }

  if (null != order_by$) {
    const order_q = sqlOfOrderBy(order_by$)

    sql += ' order by ' + order_q.sql
    bindings = bindings.concat(order_q.bindings)
  }

  if (null != limit$) {
    sql += ' limit ?'
    bindings.push(limit$)
  }

  if (null != offset$) {
    sql += ' offset ?'
    bindings.push(offset$)
  }

  return { sql, bindings }
}

function sqlOfExistsExpr(node) {
  Assert(node instanceof ExistsOp, 'node')

  const { expr$ } = node
  const q = sqlOfSelect(expr$)

  return {
    sql: 'exists (' + q.sql + ')',
    bindings: q.bindings
  }
}

function sqlOfInsert(node) {
  Assert(node instanceof InsertStm, 'node')

  const { into$, values$ } = node

  const col_names = Object.keys(values$)
  const col_vals = Object.values(values$)

  let bindings = []
  let sql = ''

  sql += 'insert into ?? '
  bindings.push(into$)

  const col_placeholders = col_names.map(_ => '??')
  sql += '(' + col_placeholders.join(', ') + ') '
  bindings = bindings.concat(col_names)

  const val_placeholders = col_vals.map(_ => '?')
  sql += 'values (' + val_placeholders.join(', ') + ') '
  bindings = bindings.concat(col_vals)

  return { sql, bindings }
}

function toSql(node) {
  Assert(node, 'node')

  if (node instanceof InsertStm) {
    return sqlOfInsert(node)
  }

  if (node instanceof SelectStm) {
    return sqlOfSelect(node)
  }

  Assert.fail(`Unknown node type: ${node && node.constructor}`)
}

<|endoftext|>


<commit_before>window.uber = function(str, callback) {
    cordova.exec(callback, function(err) {
        callback('Nothing to echo.');
    }, "Uber", "requestWithUber", [str]);
};<commit_msg>JS interface now properly displays an error message when something goes wrong under the hood, instead of 'nothing to echo'<commit_after>window.uber = function(str, callback) {
    cordova.exec(callback, function(err) {
        callback(err);
    }, "Uber", "requestWithUber", [str]);
};<|endoftext|>


<commit_before>import {
  stripLineComment,
  minifyRaw,
  minifyCooked,
  compressSymbols,
} from '../../src/minify'

describe('minify utils', () => {
  describe('stripLineComment', () => {
    it('splits a line by potential comment starts and joins until one is an actual comment', () => {
      expect(stripLineComment('abc def//ghi//jkl')).toBe('abc def')
    })

    it('ignores comment markers that are inside strings', () => {
      expect(stripLineComment('abc def"//"ghi\'//\'jkl//the end')).toBe(
        'abc def"//"ghi\'//\'jkl'
      )
      expect(stripLineComment('abc def"//"')).toBe('abc def"//"')
    })

    it('ignores comment markers that are inside parantheses', () => {
      expect(stripLineComment('bla (//) bla//the end')).toBe('bla (//) bla')
    })

    it('ignores even unescaped URLs', () => {
      expect(stripLineComment('https://test.com// comment//')).toBe(
        'https://test.com'
      )
    })
  })

  describe('minify(Raw|Cooked)', () => {
    it('Removes multi-line comments', () => {
      const input = 'this is a/* ignore me please */test'
      const expected = 'this is a test' // NOTE: They're replaced with newlines, and newlines are joined
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Joins all lines of code', () => {
      const input = 'this\nis\na/* ignore me \n please */\ntest'
      const expected = 'this is a test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Removes line comments filling an entire line', () => {
      const input = 'line one\n// remove this comment\nline two'
      const expected = 'line one line two'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Removes line comments at the end of lines of code', () => {
      const input = 'valid line with // a comment\nout comments'
      const expected = 'valid line with  out comments'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Preserves multi-line comments starting with /*!', () => {
      const input =
        'this is a /*! dont ignore me please */ test/* but you can ignore me */'
      const expected = 'this is a /*! dont ignore me please */ test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })
  })

  describe('minifyRaw', () => {
    it('works with raw escape codes', () => {
      const input = 'this\\nis\\na/* ignore me \\n please */\\ntest'
      const expected = 'this is a test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
    })
  })

  describe('minifyCooked', () => {
    it('works with raw escape codes', () => {
      const input = 'this\\nis\\na/* ignore me \\n please */\\ntest'
      const expected = 'this\\nis\\na \\ntest'
      const [actual] = minifyCooked(input)

      expect(actual).toBe(expected)
    })
  })

  describe('compressSymbols', () => {
    it('removes spaces around symbols', () => {
      const input = ';  :  {  }  ,  ;  '
      const expected = ';:{},;'

      expect(compressSymbols(input)).toBe(expected)
    })

    it('ignores symbols inside strings', () => {
      const input = ';   " : " \' : \' ;'
      const expected = ';" : " \' : \';'

      expect(compressSymbols(input)).toBe(expected)
    })
  })
})
<commit_msg>add unit test verifying the return signature behavior<commit_after>import {
  stripLineComment,
  minifyRaw,
  minifyCooked,
  compressSymbols,
} from '../../src/minify'

import { makePlaceholder } from '../../src/css/placeholderUtils'

describe('minify utils', () => {
  describe('stripLineComment', () => {
    it('splits a line by potential comment starts and joins until one is an actual comment', () => {
      expect(stripLineComment('abc def//ghi//jkl')).toBe('abc def')
    })

    it('ignores comment markers that are inside strings', () => {
      expect(stripLineComment('abc def"//"ghi\'//\'jkl//the end')).toBe(
        'abc def"//"ghi\'//\'jkl'
      )
      expect(stripLineComment('abc def"//"')).toBe('abc def"//"')
    })

    it('ignores comment markers that are inside parantheses', () => {
      expect(stripLineComment('bla (//) bla//the end')).toBe('bla (//) bla')
    })

    it('ignores even unescaped URLs', () => {
      expect(stripLineComment('https://test.com// comment//')).toBe(
        'https://test.com'
      )
    })
  })

  describe('minify(Raw|Cooked)', () => {
    it('Removes multi-line comments', () => {
      const input = 'this is a/* ignore me please */test'
      const expected = 'this is a test' // NOTE: They're replaced with newlines, and newlines are joined
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Joins all lines of code', () => {
      const input = 'this\nis\na/* ignore me \n please */\ntest'
      const expected = 'this is a test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Removes line comments filling an entire line', () => {
      const input = 'line one\n// remove this comment\nline two'
      const expected = 'line one line two'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Removes line comments at the end of lines of code', () => {
      const input = 'valid line with // a comment\nout comments'
      const expected = 'valid line with  out comments'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Preserves multi-line comments starting with /*!', () => {
      const input =
        'this is a /*! dont ignore me please */ test/* but you can ignore me */'
      const expected = 'this is a /*! dont ignore me please */ test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(actual).toBe(minifyCooked(input)[0])
    })

    it('Returns the indices of removed placeholders (expressions)', () => {
      const placeholder1 = makePlaceholder(0)
      const placeholder2 = makePlaceholder(1)
      const input = `this is some\ninput with ${placeholder1} and // ignored ${placeholder2}`
      const expected = `this is some input with ${placeholder1} and `
      const [actual, indices] = minifyRaw(input)

      expect(actual).toBe(expected)
      expect(indices).toEqual([1])
      expect(minifyCooked(input)).toEqual([actual, [1]])
    })
  })

  describe('minifyRaw', () => {
    it('works with raw escape codes', () => {
      const input = 'this\\nis\\na/* ignore me \\n please */\\ntest'
      const expected = 'this is a test'
      const [actual] = minifyRaw(input)

      expect(actual).toBe(expected)
    })
  })

  describe('minifyCooked', () => {
    it('works with raw escape codes', () => {
      const input = 'this\\nis\\na/* ignore me \\n please */\\ntest'
      const expected = 'this\\nis\\na \\ntest'
      const [actual] = minifyCooked(input)

      expect(actual).toBe(expected)
    })
  })

  describe('compressSymbols', () => {
    it('removes spaces around symbols', () => {
      const input = ';  :  {  }  ,  ;  '
      const expected = ';:{},;'

      expect(compressSymbols(input)).toBe(expected)
    })

    it('ignores symbols inside strings', () => {
      const input = ';   " : " \' : \' ;'
      const expected = ';" : " \' : \';'

      expect(compressSymbols(input)).toBe(expected)
    })
  })
})
<|endoftext|>


<commit_before>var promises = require('q');

var TestRight = require('../helpers/subject'),
	my = require('../helpers/driver').getDriverHolder(),
	expectedOutputs = require('../helpers/testWidget').expectedOutputs,
	WidgetTest;

/** Milliseconds the actions take to delay changing the output on the test page.
* Set in the test page (`test/resources/page.html`).
*/
var DELAYED_ACTIONS_DELAY = 500;

/** This test suite is redacted with [Mocha](http://visionmedia.github.com/mocha/) and [Should](https://github.com/visionmedia/should.js).
*/
describe('Feature', function() {
	var featureWithScenario;

	before(function() {
		WidgetTest = require('../helpers/testWidget').getWidget(my.driver);

		featureWithScenario = function featureWithScenario(scenario) {
			return new TestRight.Feature('Test feature', scenario, { TestWidget: WidgetTest });
		}
	});

	describe('functional scenarios with', function() {
		var failureReason = 'It’s a trap!';

		var failingFeatureTest = function() {
			return featureWithScenario([
				function() { throw failureReason }
			]).test();
		}

		function makeFailingPromiseWithSuffix(suffix) {
			return function() {
				var deferred = promises.defer();
				deferred.reject(failureReason + suffix);
				return deferred.promise;
			}
		}

		var failingPromise = makeFailingPromiseWithSuffix('');

		it('an empty feature should be accepted', function(done) {
			featureWithScenario([]).test().then(done, function() {
				console.error(arguments);
			}).end();
		});

		it('a failing function should be rejected', function(done) {
			failingFeatureTest().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function() {
					done();	// can't pass it directly, Mocha complains about param not being an error…
				}
			).end();
		});

		it('an error should be rejected and reasons passed', function(done) {
			failingFeatureTest().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.failures.should.have.length(0);
					reasons.errors.should.have.length(1);
					reasons.errors[0].should.match(new RegExp(failureReason));
					done();
				}
			).end();
		});

		it('a failing promise should be rejected and reasons passed', function(done) {
			featureWithScenario([
				failingPromise
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {	// second callback is the error callback, that's the one we're testing a call for
					reasons.errors.should.have.length(0);
					reasons.failures.should.have.length(1);
					reasons.failures[0].should.match(new RegExp(failureReason));
					done();
				}
			).end();
		});

		it('multiple failing promises should be rejected and reasons passed in correct order', function(done) {
			featureWithScenario([
				makeFailingPromiseWithSuffix(0),
				makeFailingPromiseWithSuffix(1),
				makeFailingPromiseWithSuffix(2)
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.failures.should.have.length(3);
					reasons.failures[0].should.match(new RegExp(failureReason + '0'));
					reasons.failures[1].should.match(new RegExp(failureReason + '1'));
					reasons.failures[2].should.match(new RegExp(failureReason + '2'));
					reasons.errors.should.have.length(0);
					done();
				}
			).end();
		});

		it('pure functions should be made into promises', function(done) {
			var called = false;

			featureWithScenario([ function() {
				called = true;
			} ]).test().then(function() {
				if (called)
					done();
				else
					done(new Error('Promise resolved without actually calling the scenario function'));
			}, function() {
				done(new Error('Feature evaluation failed, with ' + called ? '' : 'out'
								+ ' actually calling the scenario function (but that’s still an error)'));
			}).end();
		});

		it('arrays should be bound as arguments to previous functions', function(done) {
			var calledMarker = { called: false };	// an object rather than a simple flag, to ensure reference passing

			featureWithScenario([
				function(arg) {
					arg.called = true;
				},
				[ calledMarker ]	// if this test case works, the function above should set the `called` marker
			]).test().then(function() {
				if (calledMarker.called)
					done();
				else
					done(new Error('Promise resolved without actually calling the scenario function'));
			}, function() {
				done(new Error('Promise rejected with ' + calledMarker.called ? '' : 'out'
								+ ' actually calling the scenario function (but that’s still an error)'));
			}).end();
		});
	});

	describe('scenarios with widget states descriptions', function() {
		var expectedContents = {},
			wrongTexts    = {},
			firstKey;	// the first key of expected texts. Yes, it is used in a test.

		before(function() {
			Object.each(require('../helpers/testWidget').expectedContents, function(text, key) {	// we need to namespace all attributes to TestWidget
				expectedContents['TestWidget.' + key] = text;
				wrongTexts['TestWidget.' + key] = text + ' **modified**';

				if (! firstKey)
					firstKey = 'TestWidget.' + key;
			});
		});

		it('should be made into promises', function() {
			var result = featureWithScenario([]).buildAssertionPromise(expectedContents);	// weird construct, but that's just whitebox testing, necessarily made on an instance
			result.should.be.a('function');
			promises.isPromise(result()).should.be.ok;
		});

		it('should be parsed within a scenario', function() {
			var directCall = featureWithScenario([]).buildAssertionPromise(expectedContents);	// weird construct, but that's just whitebox testing, necessarily made on an instance
			var featureFromScenario = featureWithScenario([ expectedContents ]);

			featureFromScenario.should.have.property('steps').with.lengthOf(1);
			String(featureFromScenario.steps[0]).should.equal(String(directCall));
		});

		it('that are empty should pass', function(done) {
			featureWithScenario([
				{}
			]).test().then(done, function(err) {
				should.fail('Should have passed (reason: "' + err + ')');
				done();
			})
		});

		it('that fail should be rejected and reasons passed', function(done) {
			featureWithScenario([
				wrongTexts
			]).test().then(function() {
				done(new Error('Unmatched widget state description should not be resolved.'));
			}, function(reasons) {
				var firstReason = reasons.failures[0];
				if (firstReason.contains(firstKey)
					&& firstReason.contains(wrongTexts[firstKey])
					&& firstReason.contains(expectedContents[firstKey])) {
					done();
				} else {
					done(new Error('Unmatched widget state description was properly rejected, but the reason for rejection was not clear enough (got "' + firstReason + '").'));
				}
			}).end();
		});

		it('that are incorrectly written should throw an error upon creation', function() {
			(function() {
				featureWithScenario([
					{ toto: 'toto'}	// no widget matches this property path. We have to protect users against misspelled paths.
				]);
			}).should.throw();
		});

		/* We cannot decide in advance whether a given identifier will match in another page or not. The only thing we can check is whether we're trying to describe an unknown widget property.
		*/
		it('that are not accessible on the current page but properly written should not throw an error', function() {
			(function() {
				featureWithScenario([
					{ 'TestWidget.missing': 'missing'}
				]);
			}).should.not.throw();
		});
	});

	describe('state descriptions option', function() {
		describe('timeout', function() {
			it('should be allowed without any harm', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,
					{ timeout: 0 }
				]).test().then(done, done);
			});

			it('should do immediate evaluation if set to 0', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: 0,
						'TestWidget.output': expectedOutputs.immediateAction
					}
				]).test().then(function() {
					done(new Error('Matched while the expected result should have been set later than evaluation.'))
				}, function() {
					done();
				});
			});

			it('should do delayed evaluation if set to a proper positive value', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.delayedActionLink
					}
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				});
			});

			it('should not be longer than needed if set to a positive value', function(done) {
				this.timeout(DELAYED_ACTIONS_DELAY * 3);

				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.otherDelayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.otherDelayedActionLink
					}
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				});
			});

			it('should detect changes and fail earlier than maximum if there was a change', function(done) {
				this.timeout(DELAYED_ACTIONS_DELAY * 3);

				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.otherDelayedActionLink
					}
				]).test().then(function() {
					done(new Error('Matched while the expected result should have been set later than evaluation.'))
				}, function() {
					done();
				});
			});
		});
	});

	describe('matchers', function() {
		describe('existence matcher', function() {
			it('should pass on `true` in state descriptors on existing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.output': true }
				]).test().then(done, function(reasons) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should fail on `false` in state descriptors on existing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.output': false }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/was in the DOM/);
						done();
					}
				).end();
			});

			it('should fail on `true` in state descriptors on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': true }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/was not in the DOM/);
						done();
					}
				).end();
			});

			it('should pass on `false` in state descriptors on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': false }
				]).test().then(done, function(reasons) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});
		});

		describe('regexp matcher', function() {
			it('should pass on a regexp', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /This paragraph/ }
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should pass on a regexp in nested nodes', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /(to){2}/ }
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should fail on non-matching regexps', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /(tu){2}/ }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/did not match/);
						done();
					}
				).end();
			});

			it('should fail on on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': /toto/ }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/does not exist/);
						done();
					}
				).end();
			});
		});
	});

	describe('widget access', function() {
		it('of missing elements', function(done) {
			featureWithScenario([
				{ 'TestWidget.missing': 'toto' }
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.errors.should.have.length(0);
					reasons.failures.should.have.length(1);
					done();
				}
			).end();
		});
	});
});
<commit_msg>Minor feature test report improvement.<commit_after>var promises = require('q');

var TestRight = require('../helpers/subject'),
	my = require('../helpers/driver').getDriverHolder(),
	expectedOutputs = require('../helpers/testWidget').expectedOutputs,
	WidgetTest;

/** Milliseconds the actions take to delay changing the output on the test page.
* Set in the test page (`test/resources/page.html`).
*/
var DELAYED_ACTIONS_DELAY = 500;

/** This test suite is redacted with [Mocha](http://visionmedia.github.com/mocha/) and [Should](https://github.com/visionmedia/should.js).
*/
describe('Feature', function() {
	var featureWithScenario;

	before(function() {
		WidgetTest = require('../helpers/testWidget').getWidget(my.driver);

		featureWithScenario = function featureWithScenario(scenario) {
			return new TestRight.Feature('Test feature', scenario, { TestWidget: WidgetTest });
		}
	});

	describe('functional scenarios with', function() {
		var failureReason = 'It’s a trap!';

		var failingFeatureTest = function() {
			return featureWithScenario([
				function() { throw failureReason }
			]).test();
		}

		function makeFailingPromiseWithSuffix(suffix) {
			return function() {
				var deferred = promises.defer();
				deferred.reject(failureReason + suffix);
				return deferred.promise;
			}
		}

		var failingPromise = makeFailingPromiseWithSuffix('');

		it('an empty feature should be accepted', function(done) {
			featureWithScenario([]).test().then(done, function() {
				console.error(arguments);
			}).end();
		});

		it('a failing function should be rejected', function(done) {
			failingFeatureTest().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function() {
					done();	// can't pass it directly, Mocha complains about param not being an error…
				}
			).end();
		});

		it('an error should be rejected and reasons passed', function(done) {
			failingFeatureTest().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.failures.should.have.length(0);
					reasons.errors.should.have.length(1);
					reasons.errors[0].should.match(new RegExp(failureReason));
					done();
				}
			).end();
		});

		it('a failing promise should be rejected and reasons passed', function(done) {
			featureWithScenario([
				failingPromise
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {	// second callback is the error callback, that's the one we're testing a call for
					reasons.errors.should.have.length(0);
					reasons.failures.should.have.length(1);
					reasons.failures[0].should.match(new RegExp(failureReason));
					done();
				}
			).end();
		});

		it('multiple failing promises should be rejected and reasons passed in correct order', function(done) {
			featureWithScenario([
				makeFailingPromiseWithSuffix(0),
				makeFailingPromiseWithSuffix(1),
				makeFailingPromiseWithSuffix(2)
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.failures.should.have.length(3);
					reasons.failures[0].should.match(new RegExp(failureReason + '0'));
					reasons.failures[1].should.match(new RegExp(failureReason + '1'));
					reasons.failures[2].should.match(new RegExp(failureReason + '2'));
					reasons.errors.should.have.length(0);
					done();
				}
			).end();
		});

		it('pure functions should be made into promises', function(done) {
			var called = false;

			featureWithScenario([ function() {
				called = true;
			} ]).test().then(function() {
				if (called)
					done();
				else
					done(new Error('Promise resolved without actually calling the scenario function'));
			}, function() {
				done(new Error('Feature evaluation failed, with ' + called ? '' : 'out'
								+ ' actually calling the scenario function (but that’s still an error)'));
			}).end();
		});

		it('arrays should be bound as arguments to previous functions', function(done) {
			var calledMarker = { called: false };	// an object rather than a simple flag, to ensure reference passing

			featureWithScenario([
				function(arg) {
					arg.called = true;
				},
				[ calledMarker ]	// if this test case works, the function above should set the `called` marker
			]).test().then(function() {
				if (calledMarker.called)
					done();
				else
					done(new Error('Promise resolved without actually calling the scenario function'));
			}, function() {
				done(new Error('Promise rejected with ' + calledMarker.called ? '' : 'out'
								+ ' actually calling the scenario function (but that’s still an error)'));
			}).end();
		});
	});

	describe('scenarios with widget states descriptions', function() {
		var expectedContents = {},
			wrongTexts    = {},
			firstKey;	// the first key of expected texts. Yes, it is used in a test.

		before(function() {
			Object.each(require('../helpers/testWidget').expectedContents, function(text, key) {	// we need to namespace all attributes to TestWidget
				expectedContents['TestWidget.' + key] = text;
				wrongTexts['TestWidget.' + key] = text + ' **modified**';

				if (! firstKey)
					firstKey = 'TestWidget.' + key;
			});
		});

		it('should be made into promises', function() {
			var result = featureWithScenario([]).buildAssertionPromise(expectedContents);	// weird construct, but that's just whitebox testing, necessarily made on an instance
			result.should.be.a('function');
			promises.isPromise(result()).should.be.ok;
		});

		it('should be parsed within a scenario', function() {
			var directCall = featureWithScenario([]).buildAssertionPromise(expectedContents);	// weird construct, but that's just whitebox testing, necessarily made on an instance
			var featureFromScenario = featureWithScenario([ expectedContents ]);

			featureFromScenario.should.have.property('steps').with.lengthOf(1);
			String(featureFromScenario.steps[0]).should.equal(String(directCall));
		});

		it('that are empty should pass', function(done) {
			featureWithScenario([
				{}
			]).test().then(done, function(err) {
				should.fail('Should have passed (reason: "' + err + ')');
				done();
			})
		});

		it('that fail should be rejected and reasons passed', function(done) {
			featureWithScenario([
				wrongTexts
			]).test().then(function() {
				done(new Error('Unmatched widget state description should not be resolved.'));
			}, function(reasons) {
				var firstReason = reasons.failures[0];
				if (firstReason
					&& firstReason.contains(firstKey)
					&& firstReason.contains(wrongTexts[firstKey])
					&& firstReason.contains(expectedContents[firstKey])) {
					done();
				} else {
					done(new Error('Unmatched widget state description was properly rejected, but the reason for rejection was not clear enough (got "' + firstReason + '").'));
				}
			}).end();
		});

		it('that are incorrectly written should throw an error upon creation', function() {
			(function() {
				featureWithScenario([
					{ toto: 'toto'}	// no widget matches this property path. We have to protect users against misspelled paths.
				]);
			}).should.throw();
		});

		/* We cannot decide in advance whether a given identifier will match in another page or not. The only thing we can check is whether we're trying to describe an unknown widget property.
		*/
		it('that are not accessible on the current page but properly written should not throw an error', function() {
			(function() {
				featureWithScenario([
					{ 'TestWidget.missing': 'missing'}
				]);
			}).should.not.throw();
		});
	});

	describe('state descriptions option', function() {
		describe('timeout', function() {
			it('should be allowed without any harm', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,
					{ timeout: 0 }
				]).test().then(done, done);
			});

			it('should do immediate evaluation if set to 0', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: 0,
						'TestWidget.output': expectedOutputs.immediateAction
					}
				]).test().then(function() {
					done(new Error('Matched while the expected result should have been set later than evaluation.'))
				}, function() {
					done();
				});
			});

			it('should do delayed evaluation if set to a proper positive value', function(done) {
				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.delayedActionLink
					}
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				});
			});

			it('should not be longer than needed if set to a positive value', function(done) {
				this.timeout(DELAYED_ACTIONS_DELAY * 3);

				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.otherDelayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.otherDelayedActionLink
					}
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				});
			});

			it('should detect changes and fail earlier than maximum if there was a change', function(done) {
				this.timeout(DELAYED_ACTIONS_DELAY * 3);

				featureWithScenario([
					WidgetTest.immediateAction,	// make sure the content of the output is reset
					WidgetTest.delayedAction,
					{
						timeout: DELAYED_ACTIONS_DELAY * 2,
						'TestWidget.output': expectedOutputs.otherDelayedActionLink
					}
				]).test().then(function() {
					done(new Error('Matched while the expected result should have been set later than evaluation.'))
				}, function() {
					done();
				});
			});
		});
	});

	describe('matchers', function() {
		describe('existence matcher', function() {
			it('should pass on `true` in state descriptors on existing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.output': true }
				]).test().then(done, function(reasons) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should fail on `false` in state descriptors on existing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.output': false }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/was in the DOM/);
						done();
					}
				).end();
			});

			it('should fail on `true` in state descriptors on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': true }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/was not in the DOM/);
						done();
					}
				).end();
			});

			it('should pass on `false` in state descriptors on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': false }
				]).test().then(done, function(reasons) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});
		});

		describe('regexp matcher', function() {
			it('should pass on a regexp', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /This paragraph/ }
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should pass on a regexp in nested nodes', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /(to){2}/ }
				]).test().then(done, function(report) {
					var message = "No failure report. See code";

					if (report && report.failures && report.failures[0])
						message = report.failures[0];

					done(new Error(message));
				}).end();
			});

			it('should fail on non-matching regexps', function(done) {
				featureWithScenario([
					{ 'TestWidget.id': /(tu){2}/ }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/did not match/);
						done();
					}
				).end();
			});

			it('should fail on on missing elements', function(done) {
				featureWithScenario([
					{ 'TestWidget.missing': /toto/ }
				]).test().then(function() {
						done(new Error('Resolved instead of rejected!'));
					}, function(reasons) {
						reasons.errors.should.have.length(0);
						reasons.failures.should.have.length(1);
						reasons.failures[0].should.match(/does not exist/);
						done();
					}
				).end();
			});
		});
	});

	describe('widget access', function() {
		it('of missing elements', function(done) {
			featureWithScenario([
				{ 'TestWidget.missing': 'toto' }
			]).test().then(function() {
					done(new Error('Resolved instead of rejected!'));
				}, function(reasons) {
					reasons.errors.should.have.length(0);
					reasons.failures.should.have.length(1);
					done();
				}
			).end();
		});
	});
});
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Copyright 2002-2013, University of Colorado
 * RequireJS configuration file for Vibe testing.
 * @author John Blanco
 */

require.config( {
  deps: ["play-sound"],

  config: {
  },

  paths: {

    // plugins
    audio: '../../chipper/requirejs-plugins/audio',

    // PhET libs, uppercase names to identify them in require.js imports
    ASSERT: '../../assert/js',
    PHET_CORE: '../../phet-core/js',
    SHERPA: '../../sherpa',
    VIBE: '../../vibe/js'
  },

  urlArgs: new Date().getTime()  // cache buster to make browser refresh load all included scripts

} );
<commit_msg>Changing assertions to the preloaded assert/js/assert.js (which should be included in any HTML file that will include JS running our assertions). see https://github.com/phetsims/scenery/issues/300<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Copyright 2002-2013, University of Colorado
 * RequireJS configuration file for Vibe testing.
 * @author John Blanco
 */

require.config( {
  deps: ["play-sound"],

  config: {
  },

  paths: {

    // plugins
    audio: '../../chipper/requirejs-plugins/audio',

    // PhET libs, uppercase names to identify them in require.js imports
    PHET_CORE: '../../phet-core/js',
    SHERPA: '../../sherpa',
    VIBE: '../../vibe/js'
  },

  urlArgs: new Date().getTime()  // cache buster to make browser refresh load all included scripts

} );
<|endoftext|>


<commit_before>'use strict';
/*globals describe, it, beforeEach, afterEach*/

const chai = require('chai'),
    sinon = require('sinon'),
    async = require('async');
chai.should();
const expect = chai.expect;

// The thing we're testing
const likes = require('../../plugins/likes'),
    utils = require('../../lib/utils');
const dummyCfg = {
    mergeObjects: utils.mergeObjects
};

describe('likes plugin', () => {
    describe('exports', () => {
        it('should export prepare()', () => expect(likes.prepare).to.be.a('function'));
        it('should export start()', () => expect(likes.start).to.be.a('function'));
        it('should export stop()', () => expect(likes.stop).to.be.a('function'));
        it('should export binge()', () => expect(likes.binge).to.be.a('function'));
        it('should export handlePost()', () => expect(likes.handlePost).to.be.a('function'));
        it('should export messageHandler()', () => expect(likes.messageHandler).to.be.a('function'));
        it('should export defaultConfig()', () => expect(likes.defaultConfig).to.be.an('object'));
        it('should export internals()', () => expect(likes.internals).to.be.an('object'));
    });
    describe('prepare()', () => {
        it('should set browser', () => {
            const browser = {};
            likes.prepare({}, dummyCfg, {
                onTopic: () => 0
            }, browser);
            likes.internals.browser.should.equal(browser);
        });
        it('should use default config if config is not object', () => {
            likes.prepare(true, dummyCfg, {
                onTopic: () => 0
            }, null);
            likes.internals.config.should.not.equal(likes.defaultConfig);
            likes.internals.config.should.deep.equal(likes.defaultConfig);
        });
        it('should merge configs if config is object', () => {
            likes.prepare({
                a: 1
            }, dummyCfg, {
                onTopic: () => 0
            }, null);
            const expected = {
                a: 1,
                binge: false,
                bingeCap: 500,
                topics: [1000],
                delay: 15000,
                scatter: 5000
            };
            likes.internals.config.should.deep.equal(expected);
        });
        it('should register for messages from topics', () => {
            const spy = sinon.spy();
            likes.prepare({
                topics: [3, 5, 17]
            }, dummyCfg, {
                onTopic: spy
            }, null);
            spy.calledWith(3, likes.messageHandler).should.be.true;
            spy.calledWith(5, likes.messageHandler).should.be.true;
            spy.calledWith(17, likes.messageHandler).should.be.true;
        });

        it('should store events object in internals', () => {
            const events = {
                onTopic: () => 0
            };
            likes.prepare(true, dummyCfg, events, undefined);
            likes.internals.events.should.equal(events);
        });
    });
    describe('start()', () => {
        let sandbox;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.stub(global, 'setInterval');
        });
        afterEach(() => sandbox.restore());
        it('should set interval for binge if config binge selected', () => {
            likes.internals.config.binge = true;
            likes.start();
            setInterval.calledWith(likes.binge, 24 * 60 * 60 * 1000).should.be.true;
        });
        it('should not set interval for binge if config binge unselected', () => {
            likes.internals.config.binge = false;
            likes.start();
            setInterval.calledWith(likes.binge, 24 * 60 * 60 * 1000).should.be.false;
        });
        it('should save interval token for later if config binge selected', () => {
            const token = Math.random();
            setInterval.returns(token);
            likes.internals.config.binge = true;
            likes.start();
            likes.internals.bingeInterval.should.equal(token);
        });
        it('should save interval token for later if config binge unselected', () => {
            const token = Math.random();
            setInterval.returns(null);
            likes.internals.bingeInterval = token;
            likes.internals.config.binge = false;
            likes.start();
            likes.internals.bingeInterval.should.equal(token);
        });
    });
    describe('stop()', () => {
        let sandbox;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.stub(global, 'clearInterval');
        });
        afterEach(() => sandbox.restore());
        it('should remove interval if saved interval exists', () => {
            const token = Math.random();
            likes.internals.bingeInterval = token;
            likes.stop();
            clearInterval.calledWith(token).should.be.true;
        });
        it('should not remove interval if no saved interval exists', () => {
            likes.internals.bingeInterval = null;
            likes.stop();
            clearInterval.called.should.be.false;
        });
    });
    describe('messageHandler()', () => {
        let sandbox, events;
        beforeEach(() => {
            events = {
                emit: sinon.spy()
            };
            sandbox = sinon.sandbox.create();
            sandbox.spy(Math, 'random');
            sandbox.stub(global, 'setTimeout');
            likes.internals.events = events;
        });
        afterEach(() => sandbox.restore());
        it('should abort if message is not a create message', () => {
            likes.messageHandler({});
            setTimeout.called.should.be.false;
        });
        it('should randomize delay for likes', () => {
            likes.messageHandler({
                type: 'created'
            });
            const delay = Math.ceil(likes.internals.config.delay +
                Math.random.firstCall.returnValue * likes.internals.config.scatter);
            setTimeout.firstCall.args[1].should.equal(delay);
        });
        it('should activate postAction for the created post', () => {
            const id = Math.random(),
                spy = sinon.stub();
            spy.yields(null);
            setTimeout.callsArg(0);
            likes.internals.browser = {
                postAction: spy
            };
            likes.messageHandler({
                type: 'created'
            }, undefined, {
                id: id
            });
            spy.calledWith('like', id, '').should.be.true;
        });
        it('should log message when liking post', () => {
            const spy = sinon.stub();
            setTimeout.callsArg(0);
            likes.internals.browser = {
                postAction: spy
            };
            likes.messageHandler({
                type: 'created'
            }, undefined, {
                'topic_id': 314,
                'post_number': 159,
                username: 'quux'
            });
            events.emit.calledWith('logMessage', 'Liking Post /t/314/159 by @quux').should.be.true;
        });
    });
    describe('binge()', () => {
        let sandbox, browserSpy, events;
        beforeEach(() => {
            browserSpy = sinon.spy();
            sandbox = sinon.sandbox.create();
            sandbox.stub(utils, 'log');
            sandbox.stub(async, 'eachSeries');
            likes.internals.browser = {
                getPosts: browserSpy
            };

            events = {
                emit: sinon.spy()
            };
            likes.internals.events = events;
        });
        afterEach(() => sandbox.restore());
        it('should reset likeCount', () => {
            likes.internals.likeCount = null;
            likes.binge();
            likes.internals.likeCount.should.equal(0);
        });
        it('should log binge begin', () => {
            likes.binge();
            events.emit.calledWith('logMessage', 'Beginning Like Binge').should.be.true;
        });
        it('should call async.eachSeries with topic list', () => {
            likes.internals.config.topics = [1, 2, 3];
            likes.binge();
            async.eachSeries.firstCall.args[0].should.equal(likes.internals.config.topics);
        });
        it('async.eachSeries completed callback should log completion', () => {
            async.eachSeries.callsArgWith(2, 'this is the message');
            likes.binge();
            events.emit.calledWith('logMessage', 'Like Binge Completed: this is the message').should.be.true;
        });
        it('async.eachSeries eachCallback should call browser.getPosts', () => {
            const topicId = Math.random(),
                next = Math.random();
            async.eachSeries.callsArgWith(1, topicId, next);
            likes.binge();
            browserSpy.calledWith(topicId, likes.handlePost, next).should.be.true;
        });
    });
    describe('handlePost()', () => {
        let sandbox, browserSpy, spy;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.useFakeTimers();
            browserSpy = sinon.stub();
            spy = sinon.spy();
            likes.internals.browser = {
                postAction: browserSpy
            };
        });
        afterEach(() => sandbox.restore());
        it('should bypass post if already liked', () => {
            likes.handlePost({
                'actions_summary': [{
                    id: 2,
                    'can_act': false
                }]
            }, spy);
            spy.firstCall.args.should.have.length(0);
        });
        it('should bypass post if no liked action exists', () => {
            likes.handlePost({
                'actions_summary': [{
                    id: 3,
                    'can_act': false
                }]
            }, spy);
            spy.firstCall.args.should.have.length(0);
        });
        it('should like post if likeable', () => {
            const id = Math.random();
            likes.handlePost({
                id: id,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            browserSpy.calledWith('like', id, '').should.be.true;
        });
        it('should pass error onto callback on like failure', () => {
            browserSpy.yields('this is error');
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            spy.calledWith('this is error').should.be.true;
        });
        it('should increment like count on success', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 0;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            likes.internals.likeCount.should.equal(1);
        });
        it('should pass error onto callback on successfull like that exceeds bingeCap', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 10;
            likes.internals.config.bingeCap = 10;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            spy.calledWith('Like Binge Limit Reached').should.be.true;
        });
        it('should delay callback on success to prevent accidental server DOS', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 0;
            likes.internals.config.bingeCap = 10;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            sandbox.clock.tick(999);
            spy.called.should.be.false;
            sandbox.clock.tick(1);
            spy.called.should.be.true;
        });
    });
});
<commit_msg>Fixing broken likes tests.<commit_after>'use strict';
/*globals describe, it, beforeEach, afterEach*/

const chai = require('chai'),
    sinon = require('sinon'),
    async = require('async'),
    sinonChai = require('sinon-chai');
chai.should();
chai.use(sinonChai);
const expect = chai.expect;

const

// The thing we're testing
const likes = require('../../plugins/likes'),
    utils = require('../../lib/utils');
const dummyCfg = {
    mergeObjects: utils.mergeObjects
};

describe('likes plugin', () => {
    describe('exports', () => {
        it('should export prepare()', () => expect(likes.prepare).to.be.a('function'));
        it('should export start()', () => expect(likes.start).to.be.a('function'));
        it('should export stop()', () => expect(likes.stop).to.be.a('function'));
        it('should export binge()', () => expect(likes.binge).to.be.a('function'));
        it('should export handlePost()', () => expect(likes.handlePost).to.be.a('function'));
        it('should export messageHandler()', () => expect(likes.messageHandler).to.be.a('function'));
        it('should export defaultConfig()', () => expect(likes.defaultConfig).to.be.an('object'));
        it('should export internals()', () => expect(likes.internals).to.be.an('object'));
    });
    describe('prepare()', () => {
        it('should set browser', () => {
            const browser = {};
            likes.prepare({}, dummyCfg, {
                onTopic: () => 0
            }, browser);
            likes.internals.browser.should.equal(browser);
        });
        it('should use default config if config is not object', () => {
            likes.prepare(true, dummyCfg, {
                onTopic: () => 0
            }, null);
            likes.internals.config.should.not.equal(likes.defaultConfig);
            likes.internals.config.should.deep.equal(likes.defaultConfig);
        });
        it('should merge configs if config is object', () => {
            likes.prepare({
                a: 1
            }, dummyCfg, {
                onTopic: () => 0
            }, null);
            const expected = {
                a: 1,
                binge: false,
                bingeCap: 500,
                topics: [1000],
                delay: 15000,
                scatter: 5000
            };
            likes.internals.config.should.deep.equal(expected);
        });
        it('should register for messages from topics', () => {
            const spy = sinon.spy();
            likes.prepare({
                topics: [3, 5, 17]
            }, dummyCfg, {
                onTopic: spy
            }, null);
            spy.calledWith(3, likes.messageHandler).should.be.true;
            spy.calledWith(5, likes.messageHandler).should.be.true;
            spy.calledWith(17, likes.messageHandler).should.be.true;
        });

        it('should store events object in internals', () => {
            const events = {
                onTopic: () => 0
            };
            likes.prepare(true, dummyCfg, events, undefined);
            likes.internals.events.should.equal(events);
        });
    });
    describe('start()', () => {
        let sandbox;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.stub(global, 'setInterval');
        });
        afterEach(() => sandbox.restore());
        it('should set interval for binge if config binge selected', () => {
            likes.internals.config.binge = true;
            likes.start();
            setInterval.calledWith(likes.binge, 24 * 60 * 60 * 1000).should.be.true;
        });
        it('should not set interval for binge if config binge unselected', () => {
            likes.internals.config.binge = false;
            likes.start();
            setInterval.calledWith(likes.binge, 24 * 60 * 60 * 1000).should.be.false;
        });
        it('should save interval token for later if config binge selected', () => {
            const token = Math.random();
            setInterval.returns(token);
            likes.internals.config.binge = true;
            likes.start();
            likes.internals.bingeInterval.should.equal(token);
        });
        it('should save interval token for later if config binge unselected', () => {
            const token = Math.random();
            setInterval.returns(null);
            likes.internals.bingeInterval = token;
            likes.internals.config.binge = false;
            likes.start();
            likes.internals.bingeInterval.should.equal(token);
        });
    });
    describe('stop()', () => {
        let sandbox;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.stub(global, 'clearInterval');
        });
        afterEach(() => sandbox.restore());
        it('should remove interval if saved interval exists', () => {
            const token = Math.random();
            likes.internals.bingeInterval = token;
            likes.stop();
            clearInterval.calledWith(token).should.be.true;
        });
        it('should not remove interval if no saved interval exists', () => {
            likes.internals.bingeInterval = null;
            likes.stop();
            clearInterval.called.should.be.false;
        });
    });
    describe('messageHandler()', () => {
        let sandbox, events;
        beforeEach(() => {
            events = {
                emit: sinon.spy()
            };
            sandbox = sinon.sandbox.create();
            sandbox.spy(Math, 'random');
            sandbox.stub(global, 'setTimeout');
            likes.internals.events = events;
        });
        afterEach(() => sandbox.restore());
        it('should abort if message is not a create message', () => {
            likes.messageHandler({});
            setTimeout.called.should.be.false;
        });
        it('should randomize delay for likes', () => {
            likes.messageHandler({
                type: 'created'
            });
            const delay = Math.ceil(likes.internals.config.delay +
                Math.random.firstCall.returnValue * likes.internals.config.scatter);
            setTimeout.firstCall.args[1].should.equal(delay);
        });
        it('should activate postAction for the created post', () => {
            const id = Math.random(),
                spy = sinon.stub();
            spy.yields(null);
            setTimeout.callsArg(0);
            likes.internals.browser = {
                postAction: spy
            };
            likes.messageHandler({
                type: 'created'
            }, undefined, {
                id: id
            });
            spy.calledWith('like', id, '').should.be.true;
        });
        it('should log message when liking post', () => {
            const spy = sinon.stub();
            setTimeout.callsArg(0);
            likes.internals.browser = {
                postAction: spy
            };
            likes.messageHandler({
                type: 'created'
            }, undefined, {
                'topic_id': 314,
                'post_number': 159,
                username: 'quux'
            });
            events.emit.calledWith('logMessage', 'Liking Post /t/314/159 by @quux').should.be.true;
        });
    });
    describe('binge()', () => {
        let sandbox, browserSpy, events;
        beforeEach(() => {
            browserSpy = sinon.spy();
            sandbox = sinon.sandbox.create();
            sandbox.stub(utils, 'log');
            sandbox.stub(async, 'eachSeries');
            likes.internals.browser = {
                getPosts: browserSpy
            };

            events = {
                emit: sinon.spy()
            };
            likes.internals.events = events;
        });
        afterEach(() => sandbox.restore());
        it('should reset likeCount', () => {
            likes.internals.likeCount = null;
            likes.binge();
            likes.internals.likeCount.should.equal(0);
        });
        it('should log binge begin', () => {
            likes.binge();
            events.emit.calledWith('logMessage', 'Beginning Like Binge').should.be.true;
        });
        it('should call async.eachSeries with topic list', () => {
            likes.internals.config.topics = [1, 2, 3];
            likes.binge();
            async.eachSeries.firstCall.args[0].should.equal(likes.internals.config.topics);
        });
        it('async.eachSeries completed callback should log completion', () => {
            async.eachSeries.callsArgWith(2, 'this is the message');
            likes.binge();
            events.emit.calledWith('logMessage', 'Like Binge Completed: this is the message').should.be.true;
        });
        it('async.eachSeries eachCallback should call browser.getPosts', () => {
            const topicId = Math.random(),
                next = Math.random();
            async.eachSeries.callsArgWith(1, topicId, next);
            likes.binge();
            browserSpy.calledWith(topicId, likes.handlePost, next).should.be.true;
        });
    });
    describe('handlePost()', () => {
        let sandbox, browserSpy, spy;
        beforeEach(() => {
            sandbox = sinon.sandbox.create();
            sandbox.useFakeTimers();
            browserSpy = sinon.stub();
            spy = sinon.spy();
            likes.internals.browser = {
                postAction: browserSpy
            };
        });
        afterEach(() => sandbox.restore());
        it('should bypass post if already liked', () => {
            likes.handlePost({
                'actions_summary': [{
                    id: 2,
                    'can_act': false
                }]
            }, spy);
            spy.firstCall.args.should.have.length(0);
        });
        it('should bypass post if no liked action exists', () => {
            likes.handlePost({
                'actions_summary': [{
                    id: 3,
                    'can_act': false
                }]
            }, spy);
            spy.firstCall.args.should.have.length(0);
        });
        it('should like post if likeable', () => {
            const id = Math.random();
            likes.handlePost({
                id: id,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            browserSpy.calledWith('like', id, '').should.be.true;
        });
        it('should pass error onto callback on like failure', () => {
            browserSpy.yields('this is error');
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            spy.calledWith('this is error').should.be.true;
        });
        it('should increment like count on success', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 0;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            likes.internals.likeCount.should.equal(1);
        });
        it('should pass error onto callback on successfull like that exceeds bingeCap', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 10;
            likes.internals.config.bingeCap = 10;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            spy.calledWith('Like Binge Limit Reached').should.be.true;
        });
        it('should delay callback on success to prevent accidental server DOS', () => {
            browserSpy.yields(null);
            likes.internals.likeCount = 0;
            likes.internals.config.bingeCap = 10;
            likes.handlePost({
                id: 1,
                'actions_summary': [{
                    id: 2,
                    'can_act': true
                }]
            }, spy);
            sandbox.clock.tick(999);
            spy.called.should.be.false;
            sandbox.clock.tick(1);
            spy.called.should.be.true;
        });
    });
});
<|endoftext|>