<commit_before>define([
	'jquery',
	'underscore',
	'backbone',
	'config',
	'utils',
	'text!templates/header.html',
], function($, _, Backbone, config, utils, templateHtml) {
    "use strict";

	var HeaderView = Backbone.View.extend({

	    tagName: 'div',
		className: 'header-view',

	    events: {
			'click #aboutMap': 'aboutMapClicked',
			'click #aboutGeoSense' : 'aboutGeoSenseClicked',
			'click #shareLink' : 'shareLinkClicked',
			'click #postFacebook' : 'postFacebookClicked',
			'click #postTwitter' : 'postTwitterClicked',
			'click .map-tool.setup' : 'setupButtonClicked',
			'click .map-tool.data-library' : 'dataLibraryButtonClicked',
			'click .map-tool.data-import' : 'dataImportButtonClicked',
			'click .map-tool.timeline' : 'timelineButtonClicked',
			'click #mapView a' : 'mapViewToggleClicked',
			'click #viewBase .dropdown-menu a' : 'viewBaseToggleClicked',
			'click .view-style a' : 'viewStyleToggleClicked',
			'click #customizeViewOptions': 'customizeViewOptionsClicked',
			'click .search-form button': 'searchClicked'
	    },

	    initialize: function(options)
	    {
		    this.template = _.template(templateHtml);
			this.vent = options.vent;
			this.listenTo(this.model, 'sync', this.populateFromModel);
	    },

	    render: function()
	    {
			$(this.el).html(this.template());
			this.populateFromModel();

			this.$('.search-query').click(function() {
				$(this).select();
			});

			if (!app.isMapAdmin()) {
				this.$('.admin-tool').remove();
			}

	        return this;
	    },

		searchClicked: function(event)
		{
			var address = this.$('.search-query').val();
			if (address != '') {
				app.zoomToAddress(address);
			}
			return false;
		},

	    populateFromModel: function()
	    {
			var mapInfo = this.model.attributes;
			var title = this.$('.map-title');
			title.text(mapInfo.title);
			title.attr('href', app.genPublicURL());
			title.click(function() {
				console.log(app.getDefaultVisibleMapArea());
				app.mapView.setVisibleMapArea(app.getDefaultVisibleMapArea());
				return false;
			});

			if (mapInfo.linkURL) {
				this.$('#authorLink').show();
				this.$('#authorLink a').attr('href', mapInfo.linkURL);
				this.$('#authorLink .text').text(mapInfo.linkTitle || mapInfo.title);
			} else {
				this.$('#authorLink').hide();
			}
		},

		mapViewToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var mapViewName = link.href.split('#')[1];
			if (mapViewName != app.mapViewName) {
				app.navigate(app.genMapURI(mapViewName), {trigger: true});
			}
			evt.preventDefault();
		},

		viewBaseToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var viewBase = link.href.split('#')[1];
			app.setViewBase(viewBase);
			evt.preventDefault();
		},

		viewStyleToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var style = link.href.split('#')[1];
			app.setViewStyle(style);
			evt.preventDefault();
		},

		customizeViewOptionsClicked: function(evt)
		{
			app.showBaselayerEditor();
			evt.preventDefault();
		},

		timelineButtonClicked: function(evt)
		{
			evt.preventDefault();
		},

		setupButtonClicked: function(evt)
		{
			app.showSetupView();
			evt.preventDefault();
		},

		dataImportButtonClicked: function(evt)
		{
			app.toggleDataImport();
			evt.preventDefault();
		},

		dataLibraryButtonClicked: function(evt)
		{
			app.toggleDataLibrary();
			evt.preventDefault();
		},

		aboutGeoSenseClicked: function(evt)
		{
			app.showAbout();
			evt.preventDefault();
		},

		aboutMapClicked: function(evt)
		{
			app.showMapInfo();
			evt.preventDefault();
		},

		shareLinkClicked: function(evt)
		{
			app.showShareLink();
			evt.preventDefault();
		},

		postTwitterClicked: function(evt)
		{
			var mapInfo = this.model.attributes,
				tweet = {},
				url = app.genPublicURL(true);
			tweet.url = url;
			tweet.text = __('Check out the %(title)s map:', {
				url: url,
				title: mapInfo.title
			});
			if (mapInfo.twitter) {
				tweet.via = mapInfo.twitter;
			}

			var url = 'https://twitter.com/share?' + $.param(tweet);
			window.open(url, __('Tweet this post'), 'width=650,height=251,toolbar=0,scrollbars=0,status=0,resizable=0,location=0,menuBar=0');
			evt.preventDefault();
		},

		postFacebookClicked: function(evt)
		{
			var mapInfo = this.model.attributes,
				url = 'http://www.facebook.com/sharer.php?u='
			url += encodeURIComponent(app.genPublicURL(true));
			url += '&t=' +encodeURIComponent(__('Check out the %(title)s map', {
				title: mapInfo.title
			}));
			window.open('' + url, __('Share it on Facebook'), 'width=650,height=251,toolbar=0,scrollbars=0,status=0,resizable=0,location=0,menuBar=0');
			evt.preventDefault();
		},

		remove: function()
		{
			$(window).unbind();
			$(this.el).remove();
			return this;
		},

	});

	return HeaderView;
});
<commit_msg>Remove empty navigation elements<commit_after>define([
	'jquery',
	'underscore',
	'backbone',
	'config',
	'utils',
	'text!templates/header.html',
], function($, _, Backbone, config, utils, templateHtml) {
    "use strict";

	var HeaderView = Backbone.View.extend({

	    tagName: 'div',
		className: 'header-view',

	    events: {
			'click #aboutMap': 'aboutMapClicked',
			'click #aboutGeoSense' : 'aboutGeoSenseClicked',
			'click #shareLink' : 'shareLinkClicked',
			'click #postFacebook' : 'postFacebookClicked',
			'click #postTwitter' : 'postTwitterClicked',
			'click .map-tool.setup' : 'setupButtonClicked',
			'click .map-tool.data-library' : 'dataLibraryButtonClicked',
			'click .map-tool.data-import' : 'dataImportButtonClicked',
			'click .map-tool.timeline' : 'timelineButtonClicked',
			'click #mapView a' : 'mapViewToggleClicked',
			'click #viewBase .dropdown-menu a' : 'viewBaseToggleClicked',
			'click .view-style a' : 'viewStyleToggleClicked',
			'click #customizeViewOptions': 'customizeViewOptionsClicked',
			'click .search-form button': 'searchClicked'
	    },

	    initialize: function(options)
	    {
		    this.template = _.template(templateHtml);
			this.vent = options.vent;
			this.listenTo(this.model, 'sync', this.populateFromModel);
	    },

	    render: function()
	    {
			$(this.el).html(this.template());
			this.populateFromModel();

			this.$('.search-query').click(function() {
				$(this).select();
			});

			if (!app.isMapAdmin()) {
				this.$('.admin-tool').remove();
			}

			this.$('ul.nav').each(function() {
				if ($('li:visible', this).length == 0) {
					$(this).remove();
				}
			});

	        return this;
	    },

		searchClicked: function(event)
		{
			var address = this.$('.search-query').val();
			if (address != '') {
				app.zoomToAddress(address);
			}
			return false;
		},

	    populateFromModel: function()
	    {
			var mapInfo = this.model.attributes;
			var title = this.$('.map-title');
			title.text(mapInfo.title);
			title.attr('href', app.genPublicURL());
			title.click(function() {
				console.log(app.getDefaultVisibleMapArea());
				app.mapView.setVisibleMapArea(app.getDefaultVisibleMapArea());
				return false;
			});

			if (mapInfo.linkURL) {
				this.$('#authorLink').show();
				this.$('#authorLink a').attr('href', mapInfo.linkURL);
				this.$('#authorLink .text').text(mapInfo.linkTitle || mapInfo.title);
			} else {
				this.$('#authorLink').hide();
			}
		},

		mapViewToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var mapViewName = link.href.split('#')[1];
			if (mapViewName != app.mapViewName) {
				app.navigate(app.genMapURI(mapViewName), {trigger: true});
			}
			evt.preventDefault();
		},

		viewBaseToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var viewBase = link.href.split('#')[1];
			app.setViewBase(viewBase);
			evt.preventDefault();
		},

		viewStyleToggleClicked: function(evt)
		{
			var link = evt.currentTarget;
			var style = link.href.split('#')[1];
			app.setViewStyle(style);
			evt.preventDefault();
		},

		customizeViewOptionsClicked: function(evt)
		{
			app.showBaselayerEditor();
			evt.preventDefault();
		},

		timelineButtonClicked: function(evt)
		{
			evt.preventDefault();
		},

		setupButtonClicked: function(evt)
		{
			app.showSetupView();
			evt.preventDefault();
		},

		dataImportButtonClicked: function(evt)
		{
			app.toggleDataImport();
			evt.preventDefault();
		},

		dataLibraryButtonClicked: function(evt)
		{
			app.toggleDataLibrary();
			evt.preventDefault();
		},

		aboutGeoSenseClicked: function(evt)
		{
			app.showAbout();
			evt.preventDefault();
		},

		aboutMapClicked: function(evt)
		{
			app.showMapInfo();
			evt.preventDefault();
		},

		shareLinkClicked: function(evt)
		{
			app.showShareLink();
			evt.preventDefault();
		},

		postTwitterClicked: function(evt)
		{
			var mapInfo = this.model.attributes,
				tweet = {},
				url = app.genPublicURL(true);
			tweet.url = url;
			tweet.text = __('Check out the %(title)s map:', {
				url: url,
				title: mapInfo.title
			});
			if (mapInfo.twitter) {
				tweet.via = mapInfo.twitter;
			}

			var url = 'https://twitter.com/share?' + $.param(tweet);
			window.open(url, __('Tweet this post'), 'width=650,height=251,toolbar=0,scrollbars=0,status=0,resizable=0,location=0,menuBar=0');
			evt.preventDefault();
		},

		postFacebookClicked: function(evt)
		{
			var mapInfo = this.model.attributes,
				url = 'http://www.facebook.com/sharer.php?u='
			url += encodeURIComponent(app.genPublicURL(true));
			url += '&t=' +encodeURIComponent(__('Check out the %(title)s map', {
				title: mapInfo.title
			}));
			window.open('' + url, __('Share it on Facebook'), 'width=650,height=251,toolbar=0,scrollbars=0,status=0,resizable=0,location=0,menuBar=0');
			evt.preventDefault();
		},

		remove: function()
		{
			$(window).unbind();
			$(this.el).remove();
			return this;
		},

	});

	return HeaderView;
});
<|endoftext|>


<commit_before>/**
 * @fileOverview
 *
 * 一些常用的工具方法
 */

define(function() {

    /**
     * @class kity.Utils
     * @catalog core
     * @static
     * @description 提供常用的工具方法
     */
    var utils = {

        /**
         * @method each()
         * @for kity.Utils
         * @grammar each(obj, interator, context)
         * @param  {Object|Array} obj 要迭代的对象或数组
         * @param  {Function} iterator 迭代函数
         * @param  {Any} context  迭代函数的上下文
         *
         * @example 迭代数组
         *
         * ```js
         * kity.Utils.each([1, 2, 3, 4, 5], function(value, index, array) {
         *     console.log(value, index);
         * });
         * // 1, 0
         * // 2, 1
         * // 3, 2
         * // 4, 3
         * // 5, 4
         * ```
         *
         * @example 迭代对象
         *
         * ```js
         * var obj = {
         *     name: 'kity',
         *     version: '1.2.1'
         * };
         * var param = [];
         * kity.Utils.each(obj, function(value, key, obj) {
         *     param.push(key + '=' + value);
         * });
         * console.log(param.join('&')); // "name=kity&version=1.2.1"
         * ```
         */
        each: function(obj, iterator, context) {
            if (obj === null) {
                return;
            }
            if (obj.length === +obj.length) {
                for (var i = 0, l = obj.length; i < l; i++) {
                    if (iterator.call(context, obj[i], i, obj) === false) {
                        return false;
                    }
                }
            } else {
                for (var key in obj) {
                    if (obj.hasOwnProperty(key)) {
                        if (iterator.call(context, obj[key], key, obj) === false) {
                            return false;
                        }
                    }
                }
            }
        },

        /**
         * @method extend()
         * @for kity.Utils
         * @grammar extend(target, sources..., notCover) => {object}
         * @description 把源对象的属性合并到目标对象上
         * @param {object} target 目标对象
         * @param {parameter} sources 源对象
         * @param {boolean} notCover 是否不要覆盖源对象已有的属性
         *
         * @example
         *
         * ```js
         * var a = {
         *     key1: 'a1',
         *     key2: 'a2'
         * };
         *
         * var b = {
         *     key2: 'b2',
         *     key3: 'b3'
         * };
         *
         * var c = {
         *     key4: 'c4'
         * };
         *
         * var d = kity.extend(a, b, c);
         *
         * console.log(d === a); // true
         * console.log(a); // {key1: 'a1', key2: 'b2', key3: 'b3', key4: 'c4'}
         * ```
         */
        extend: function(t) {
            var a = arguments,
                notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false,
                len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length;
            for (var i = 1; i < len; i++) {
                var x = a[i];
                for (var k in x) {
                    if (!notCover || !t.hasOwnProperty(k)) {
                        t[k] = x[k];
                    }
                }
            }
            return t;
        },

        /**
         * @method deepExtend()
         * @for kity.Utils
         * @grammar deepExtend(target, sources..., notCover)
         * @description 把源对象的属性合并到目标对象上，如果属性是对象，会递归合并
         * @param {object} target 目标对象
         * @param {parameter} sources 源对象
         * @param {boolean} notCover 是否不要覆盖源对象已有的属性
         */
        deepExtend: function(t, s) {
            var a = arguments,
                notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false,
                len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length;
            for (var i = 1; i < len; i++) {
                var x = a[i];
                for (var k in x) {
                    if (!notCover || !t.hasOwnProperty(k)) {
                        if (this.isObject(t[k]) && this.isObject(x[k])) {
                            this.deepExtend(t[k], x[k], notCover);
                        } else {
                            t[k] = x[k];
                        }

                    }
                }
            }
            return t;
        },

        /**
         * @method clone()
         * @for kity.Utils
         * @grammar clone(obj) => {object}
         * @description 返回一个对象的克隆副本（非深度复制）
         * @param  {object} obj 要克隆的对象
         *
         * @example
         *
         * ```js
         * var source = {
         *     key1: {
         *         key2: 'value2'
         *     },
         *     key3: 'value3'
         * };
         *
         * var target = kity.Utils.clone(source);
         *
         * console.log(target === source); // false
         * console.log(target.key1 === source.key1); // true
         * console.log(target.key3 === source.key3); // true
         * ```
         */
        clone: function(obj) {
            var cloned = {};
            for (var m in obj) {
                if (obj.hasOwnProperty(m)) {
                    cloned[m] = obj[m];
                }
            }
            return cloned;
        },

        /**
         * @method copy()
         * @for kity.Utils
         * @grammar copy(obj) => {object}
         * @description 返回一个对象的拷贝副本（深度复制）
         * @param  {object} obj 要拷贝的对象
         *
         * @example
         *
         * ```js
         * var source = {
         *     key1: {
         *         key2: 'value2'
         *     },
         *     key3: 'value3'
         * };
         *
         * var target = kity.Utils.copy(source);
         *
         * console.log(target === source); // false
         * console.log(target.key1 === source.key1); // false
         * console.log(target.key3 === source.key3); // true，因为是值类型
         * ```
         */
        copy: function(obj) {
            if (typeof obj !== 'object') return obj;
            if (typeof obj === 'function') return null;
            return JSON.parse(JSON.stringify(obj));
        },

        queryPath: function(path, obj) {
            var arr = path.split('.');
            var i = 0,
                tmp = obj,
                l = arr.length;
            while (i < l) {
                if (arr[i] in tmp) {
                    tmp = tmp[arr[i]];
                    i++;
                    if (i >= l || tmp === undefined) {
                        return tmp;
                    }
                } else {
                    return undefined;
                }
            }
        },

        getValue: function(value, defaultValue) {

            return value !== undefined ? value : defaultValue;

        },

        /**
         * @method flatten()
         * @for kity.Utils
         * @grammar flatten(arr) => {Array}
         * @description 返回给定数组的扁平化版本
         * @param  {Array} arr 要扁平化的数组
         *
         * @example
         *
         * ```js
         * var flattened = kity.Utils.flatten([[1, 2], [2, 3], [[4, 5], [6, 7]]]);
         * console.log(flattened); // [1, 2, 3, 4, 5, 6, 7];
         * ```
         */
        flatten: function(arr) {
            var result = [],
                length = arr.length,
                i;
            for (i = 0; i < length; i++) {
                if (arr[i] instanceof Array) {
                    result = result.concat(utils.flatten(arr[i]));
                } else {
                    result.push(arr[i]);
                }
            }
            return result;
        },

        /**
         * @method paralle()
         * @for kity.Utils
         * @grammar paralle() => {Any}
         *
         * @description 平行地对 v1 和 v2 进行指定的操作
         *
         *    如果 v1 是数字，那么直接进行 op 操作
         *    如果 v1 是对象，那么返回一个对象，其元素是 v1 和 v2 同键值的每个元素平行地进行 op 操作的结果
         *    如果 v1 是数组，那么返回一个数组，其元素是 v1 和 v2 同索引的每个元素平行地进行 op 操作的结果
         *
         * @param  {Number|Object|Array} v1 第一个操作数
         * @param  {Number|Object|Array} v2 第二个操作数
         * @param  {Function} op 操作函数
         *
         *
         *
         * @example
         *
         * ```js
         * var a = {
         *     value1: 1,
         *     value2: 2,
         *     value3: [3, 4, 5]
         * };
         *
         * var b = {
         *     value1: 2,
         *     value2: 3,
         *     value3: [4, 5, 6]
         * };
         *
         * var c = kity.Utils.paralle(a, b, function(v1, v2) {
         *     return v1 + v2;
         * });
         *
         * console.log(c.value1); // 3
         * console.log(c.value2); // 5
         * console.log(c.value3); // [7, 9, 11]
         *
         * ```
         */
        paralle: function(v1, v2, op) {
            var Class, field, index, name, value;

            // 数组
            if (v1 instanceof Array) {
                value = [];
                for (index = 0; index < v1.length; index++) {
                    value.push(utils.paralle(v1[index], v2[index], op));
                }
                return value;
            }

            // 对象
            if (v1 instanceof Object) {

                // 如果值是一个支持原始表示的实例，获取其原始表示
                Class = v1.getClass && v1.getClass();

                if (Class && Class.parse) {
                    v1 = v1.valueOf();
                    v2 = v2.valueOf();
                    value = utils.paralle(v1, v2, op);
                    value = Class.parse(value);
                }

                // 否则作为字面量封箱
                else {
                    value = {};
                    for (name in v1) {
                        if (v1.hasOwnProperty(name) && v2.hasOwnProperty(name)) {
                            value[name] = utils.paralle(v1[name], v2[name], op);
                        }
                    }
                }

                return value;
            }

            // 是否数字
            if (false === isNaN(parseFloat(v1))) {
                return op(v1, v2);
            }

            return value;
        },

        /**
         * 创建 op 操作的一个平行化版本
         */
        parallelize: function(op) {
            return function(v1, v2) {
                return utils.paralle(v1, v2, op);
            };
        }
    };

    /**
     * @method isString()
     * @for kity.Utils
     * @grammar isString(unknown) => {boolean}
     * @description 判断一个值是否为字符串类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isFunction()
     * @for kity.Utils
     * @grammar isFunction(unknown) => {boolean}
     * @description 判断一个值是否为函数类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isArray()
     * @for kity.Utils
     * @grammar isArray(unknown) => {boolean}
     * @description 判断一个值是否为数组类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isNumber()
     * @for kity.Utils
     * @grammar isNumber(unknown) => {boolean}
     * @description 判断一个值是否为数字类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isRegExp()
     * @for kity.Utils
     * @grammar isRegExp(unknown) => {boolean}
     * @description 判断一个值是否为正则表达式类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isObject()
     * @for kity.Utils
     * @grammar isObject(unknown) => {boolean}
     * @description 判断一个值是否为对象类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isBoolean()
     * @for kity.Utils
     * @grammar isBoolean(unknown) => {boolean}
     * @description 判断一个值是否为布尔类型
     * @param  {any} unknown 要判断的值
     */
    utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object', 'Boolean'], function(v) {
        utils['is' + v] = function(obj) {
            return Object.prototype.toString.apply(obj) == '[object ' + v + ']';
        };
    });

    return utils;
});<commit_msg>add name<commit_after>/**
 * @fileOverview
 *
 * 一些常用的工具方法
 */

define(function() {

    /**
     * @class kity.Utils
     * @catalog core
     * @static
     * @description 提供常用的工具方法
     */
    var utils = {

        /**
         * @method each()
         * @for kity.Utils
         * @grammar each(obj, interator, context)
         * @param  {Object|Array} obj 要迭代的对象或数组
         * @param  {Function} iterator 迭代函数
         * @param  {Any} context  迭代函数的上下文
         *
         * @example 迭代数组
         *
         * ```js
         * kity.Utils.each([1, 2, 3, 4, 5], function(value, index, array) {
         *     console.log(value, index);
         * });
         * // 1, 0
         * // 2, 1
         * // 3, 2
         * // 4, 3
         * // 5, 4
         * ```
         *
         * @example 迭代对象
         *
         * ```js
         * var obj = {
         *     name: 'kity',
         *     version: '1.2.1'
         * };
         * var param = [];
         * kity.Utils.each(obj, function(value, key, obj) {
         *     param.push(key + '=' + value);
         * });
         * console.log(param.join('&')); // "name=kity&version=1.2.1"
         * ```
         */
        each: function each(obj, iterator, context) {
            if (obj === null) {
                return;
            }
            if (obj.length === +obj.length) {
                for (var i = 0, l = obj.length; i < l; i++) {
                    if (iterator.call(context, obj[i], i, obj) === false) {
                        return false;
                    }
                }
            } else {
                for (var key in obj) {
                    if (obj.hasOwnProperty(key)) {
                        if (iterator.call(context, obj[key], key, obj) === false) {
                            return false;
                        }
                    }
                }
            }
        },

        /**
         * @method extend()
         * @for kity.Utils
         * @grammar extend(target, sources..., notCover) => {object}
         * @description 把源对象的属性合并到目标对象上
         * @param {object} target 目标对象
         * @param {parameter} sources 源对象
         * @param {boolean} notCover 是否不要覆盖源对象已有的属性
         *
         * @example
         *
         * ```js
         * var a = {
         *     key1: 'a1',
         *     key2: 'a2'
         * };
         *
         * var b = {
         *     key2: 'b2',
         *     key3: 'b3'
         * };
         *
         * var c = {
         *     key4: 'c4'
         * };
         *
         * var d = kity.extend(a, b, c);
         *
         * console.log(d === a); // true
         * console.log(a); // {key1: 'a1', key2: 'b2', key3: 'b3', key4: 'c4'}
         * ```
         */
        extend: function extend(t) {
            var a = arguments,
                notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false,
                len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length;
            for (var i = 1; i < len; i++) {
                var x = a[i];
                for (var k in x) {
                    if (!notCover || !t.hasOwnProperty(k)) {
                        t[k] = x[k];
                    }
                }
            }
            return t;
        },

        /**
         * @method deepExtend()
         * @for kity.Utils
         * @grammar deepExtend(target, sources..., notCover)
         * @description 把源对象的属性合并到目标对象上，如果属性是对象，会递归合并
         * @param {object} target 目标对象
         * @param {parameter} sources 源对象
         * @param {boolean} notCover 是否不要覆盖源对象已有的属性
         */
        deepExtend: function(t, s) {
            var a = arguments,
                notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false,
                len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length;
            for (var i = 1; i < len; i++) {
                var x = a[i];
                for (var k in x) {
                    if (!notCover || !t.hasOwnProperty(k)) {
                        if (this.isObject(t[k]) && this.isObject(x[k])) {
                            this.deepExtend(t[k], x[k], notCover);
                        } else {
                            t[k] = x[k];
                        }

                    }
                }
            }
            return t;
        },

        /**
         * @method clone()
         * @for kity.Utils
         * @grammar clone(obj) => {object}
         * @description 返回一个对象的克隆副本（非深度复制）
         * @param  {object} obj 要克隆的对象
         *
         * @example
         *
         * ```js
         * var source = {
         *     key1: {
         *         key2: 'value2'
         *     },
         *     key3: 'value3'
         * };
         *
         * var target = kity.Utils.clone(source);
         *
         * console.log(target === source); // false
         * console.log(target.key1 === source.key1); // true
         * console.log(target.key3 === source.key3); // true
         * ```
         */
        clone: function clone(obj) {
            var cloned = {};
            for (var m in obj) {
                if (obj.hasOwnProperty(m)) {
                    cloned[m] = obj[m];
                }
            }
            return cloned;
        },

        /**
         * @method copy()
         * @for kity.Utils
         * @grammar copy(obj) => {object}
         * @description 返回一个对象的拷贝副本（深度复制）
         * @param  {object} obj 要拷贝的对象
         *
         * @example
         *
         * ```js
         * var source = {
         *     key1: {
         *         key2: 'value2'
         *     },
         *     key3: 'value3'
         * };
         *
         * var target = kity.Utils.copy(source);
         *
         * console.log(target === source); // false
         * console.log(target.key1 === source.key1); // false
         * console.log(target.key3 === source.key3); // true，因为是值类型
         * ```
         */
        copy: function copy(obj) {
            if (typeof obj !== 'object') return obj;
            if (typeof obj === 'function') return null;
            return JSON.parse(JSON.stringify(obj));
        },

        queryPath: function(path, obj) {
            var arr = path.split('.');
            var i = 0,
                tmp = obj,
                l = arr.length;
            while (i < l) {
                if (arr[i] in tmp) {
                    tmp = tmp[arr[i]];
                    i++;
                    if (i >= l || tmp === undefined) {
                        return tmp;
                    }
                } else {
                    return undefined;
                }
            }
        },

        getValue: function(value, defaultValue) {

            return value !== undefined ? value : defaultValue;

        },

        /**
         * @method flatten()
         * @for kity.Utils
         * @grammar flatten(arr) => {Array}
         * @description 返回给定数组的扁平化版本
         * @param  {Array} arr 要扁平化的数组
         *
         * @example
         *
         * ```js
         * var flattened = kity.Utils.flatten([[1, 2], [2, 3], [[4, 5], [6, 7]]]);
         * console.log(flattened); // [1, 2, 3, 4, 5, 6, 7];
         * ```
         */
        flatten: function flatten(arr) {
            var result = [],
                length = arr.length,
                i;
            for (i = 0; i < length; i++) {
                if (arr[i] instanceof Array) {
                    result = result.concat(utils.flatten(arr[i]));
                } else {
                    result.push(arr[i]);
                }
            }
            return result;
        },

        /**
         * @method paralle()
         * @for kity.Utils
         * @grammar paralle() => {Any}
         *
         * @description 平行地对 v1 和 v2 进行指定的操作
         *
         *    如果 v1 是数字，那么直接进行 op 操作
         *    如果 v1 是对象，那么返回一个对象，其元素是 v1 和 v2 同键值的每个元素平行地进行 op 操作的结果
         *    如果 v1 是数组，那么返回一个数组，其元素是 v1 和 v2 同索引的每个元素平行地进行 op 操作的结果
         *
         * @param  {Number|Object|Array} v1 第一个操作数
         * @param  {Number|Object|Array} v2 第二个操作数
         * @param  {Function} op 操作函数
         *
         *
         *
         * @example
         *
         * ```js
         * var a = {
         *     value1: 1,
         *     value2: 2,
         *     value3: [3, 4, 5]
         * };
         *
         * var b = {
         *     value1: 2,
         *     value2: 3,
         *     value3: [4, 5, 6]
         * };
         *
         * var c = kity.Utils.paralle(a, b, function(v1, v2) {
         *     return v1 + v2;
         * });
         *
         * console.log(c.value1); // 3
         * console.log(c.value2); // 5
         * console.log(c.value3); // [7, 9, 11]
         *
         * ```
         */
        paralle: function paralle(v1, v2, op) {
            var Class, field, index, name, value;

            // 数组
            if (v1 instanceof Array) {
                value = [];
                for (index = 0; index < v1.length; index++) {
                    value.push(utils.paralle(v1[index], v2[index], op));
                }
                return value;
            }

            // 对象
            if (v1 instanceof Object) {

                // 如果值是一个支持原始表示的实例，获取其原始表示
                Class = v1.getClass && v1.getClass();

                if (Class && Class.parse) {
                    v1 = v1.valueOf();
                    v2 = v2.valueOf();
                    value = utils.paralle(v1, v2, op);
                    value = Class.parse(value);
                }

                // 否则作为字面量封箱
                else {
                    value = {};
                    for (name in v1) {
                        if (v1.hasOwnProperty(name) && v2.hasOwnProperty(name)) {
                            value[name] = utils.paralle(v1[name], v2[name], op);
                        }
                    }
                }

                return value;
            }

            // 是否数字
            if (false === isNaN(parseFloat(v1))) {
                return op(v1, v2);
            }

            return value;
        },

        /**
         * 创建 op 操作的一个平行化版本
         */
        parallelize: function parallelize(op) {
            return function(v1, v2) {
                return utils.paralle(v1, v2, op);
            };
        }
    };

    /**
     * @method isString()
     * @for kity.Utils
     * @grammar isString(unknown) => {boolean}
     * @description 判断一个值是否为字符串类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isFunction()
     * @for kity.Utils
     * @grammar isFunction(unknown) => {boolean}
     * @description 判断一个值是否为函数类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isArray()
     * @for kity.Utils
     * @grammar isArray(unknown) => {boolean}
     * @description 判断一个值是否为数组类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isNumber()
     * @for kity.Utils
     * @grammar isNumber(unknown) => {boolean}
     * @description 判断一个值是否为数字类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isRegExp()
     * @for kity.Utils
     * @grammar isRegExp(unknown) => {boolean}
     * @description 判断一个值是否为正则表达式类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isObject()
     * @for kity.Utils
     * @grammar isObject(unknown) => {boolean}
     * @description 判断一个值是否为对象类型
     * @param  {any} unknown 要判断的值
     */
    /**
     * @method isBoolean()
     * @for kity.Utils
     * @grammar isBoolean(unknown) => {boolean}
     * @description 判断一个值是否为布尔类型
     * @param  {any} unknown 要判断的值
     */
    utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object', 'Boolean'], function(v) {
        utils['is' + v] = function typeCheck(obj) {
            return Object.prototype.toString.apply(obj) == '[object ' + v + ']';
        };
    });

    return utils;
});<|endoftext|>


<commit_before>/**
 * Synchronous transactions avoid unnecessary async/await calls by preloading and caching
 * all necessary key-value-pairs.
 *
 * WARNING: If not all required key-value-pairs are preloaded, the results of any call on a synchronous transaction
 * might be wrong. Only use synchronous transactions, if unavoidable.
 * @implements {IObjectStore}
 * @implements {ICommittable}
 * @extends {Transaction}
 */
class SynchronousTransaction extends Transaction {
    /**
     * This constructor should only be called by an ObjectStore object.
     * Our transactions have a watchdog enabled by default,
     * logging a warning after a certain time specified by WATCHDOG_TIMER.
     * This helps to detect unclosed transactions preventing to store the state in
     * the persistent backend.
     * @param {ObjectStore} objectStore The object store this transaction belongs to.
     * @param {IObjectStore} parent The backend on which the transaction is based,
     * i.e., another transaction or the real database.
     * @param {ICommittable} [managingBackend] The object store managing the transactions,
     * i.e., the ObjectStore object.
     * @param {boolean} [enableWatchdog] If this is is set to true (default),
     * a warning will be logged if left open for longer than WATCHDOG_TIMER.
     * @protected
     */
    constructor(objectStore, parent, managingBackend, enableWatchdog=true) {
        super(objectStore, parent, managingBackend, enableWatchdog);
        /** @type {Map.<string,*>} */
        this._cache = new Map();
    }

    /**
     * This method preloads a set of keys and caches them.
     * It can be called as often as needed.
     * @param {Array.<string>} keys The keys to preload.
     * @return {Promise}
     */
    async preload(keys) {
        keys = keys.filter(key => !this.isCached(key));

        const promises = keys.map(key => Transaction.prototype.get.call(this, key));
        const values = await Promise.all(promises);
        for (let i = 0; i < keys.length; ++i) {
            if (values[i]) {
                this._cache.set(keys[i], values[i]);
            }
        }
    }

    /**
     * A check whether a certain key is cached.
     * @param {string} key The key to check.
     * @return {boolean} A boolean indicating whether the key is already in the cache.
     */
    isCached(key) {
        return this._cache.has(key);
    }

    /**
     * @override
     */
    async get(key) {
        // Use cache or ask parent.
        let value = this._cache.get(key);
        if (!value) {
            value = await Transaction.prototype.get.call(this, key);
            this._cache.set(key, value);
        }
        return value;
    }

    /**
     * Internal method to query cache.
     * @param {string} key
     * @param {boolean} [expectPresence]
     * @return {*} The cached value.
     * @private
     */
    _getCached(key, expectPresence=true) {
        // Use cache only if the parent is not synchronous.
        if (this._parent instanceof SynchronousTransaction) {
            return this._parent.getSync(key, expectPresence);
        }

        const value = this._cache.get(key);
        if (expectPresence && !value) {
            throw new Error(`Missing key in cache: ${key}`);
        }
        return value;
    }

    /**
     * Returns a promise of the object stored under the given primary key.
     * Resolves to undefined if the key is not present in the object store.
     * @param {string} key The primary key to look for.
     * @param {boolean} [expectPresence] Method throws an error if key is not present in transaction or cache.
     * @returns {*} The object stored under the given key, or undefined if not present.
     */
    getSync(key, expectPresence=true) {
        // Order is as follows:
        // 1. check if removed,
        // 2. check if modified,
        // 3. check if truncated
        // 4. request from backend
        if (this._removed.has(key)) {
            return undefined;
        }
        if (this._modified.has(key)) {
            return this._modified.get(key);
        }
        if (this._truncated) {
            return undefined;
        }
        return this._getCached(key, expectPresence);
    }

    /**
     * @override
     */
    async put(key, value) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Inserts or replaces a key-value pair.
     * @param {string} key The primary key to associate the value with.
     * @param {*} value The value to write.
     */
    putSync(key, value) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        const oldValue = this.getSync(key, false);

        // Save for indices.
        if (!this._originalValues.has(key)) {
            this._originalValues.set(key, oldValue);
        }

        this._put(key, value, oldValue);
    }

    /**
     * @override
     */
    async remove(key) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Removes the key-value pair of the given key from the object store.
     * @param {string} key The primary key to delete along with the associated object.
     */
    removeSync(key) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        const oldValue = this.getSync(key, false);
        // Save for indices.
        if (!this._originalValues.has(key)) {
            this._originalValues.set(key, oldValue);
        }

        this._remove(key, oldValue);
    }

    /**
     * @override
     */
    async keys(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of a set of keys fulfilling the given query.
     * If the optional query is not given, it returns all keys in the object store.
     * If the query is of type KeyRange, it returns all keys of the object store being within this range.
     * If the query is of type Query, it returns all keys fulfilling the query.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Set.<string>} The set of keys relevant to the query.
     */
    keysSync(query=null) {
        if (query !== null && query instanceof Query) {
            throw new Error('Invalid call on SynchronousTransaction');
        }
        let keys = new Set();
        for (const key of this._cache.keys()) {
            if (query === null || query.includes(key)) {
                keys.add(key);
            }
        }
        for (const key of this._modified.keys()) {
            if (query === null || query.includes(key)) {
                keys.add(key);
            }
        }
        return keys;
    }

    /**
     * @override
     */
    async values(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of an array of objects whose primary keys fulfill the given query.
     * If the optional query is not given, it returns all objects in the object store.
     * If the query is of type KeyRange, it returns all objects whose primary keys are within this range.
     * If the query is of type Query, it returns all objects whose primary keys fulfill the query.
     * @param {Query|KeyRange} [query] Optional query to check keys against.
     * @returns {Array.<*>} The array of objects relevant to the query.
     */
    valuesSync(query=null) {
        if (query !== null && query instanceof Query) {
            throw new Error('Invalid call on SynchronousTransaction');
        }
        const keys = this.keysSync(query);
        const values = [];
        for (const key of keys) {
            values.push(this.getSync(key, true));
        }
        return values;
    }

    /**
     * @override
     */
    async keyStream(callback, ascending=true, query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async valueStream(callback, ascending=true, query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async maxValue(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the object whose primary key is maximal for the given range.
     * If the optional query is not given, it returns the object whose key is maximal.
     * If the query is of type KeyRange, it returns the object whose primary key is maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {*} The object relevant to the query.
     */
    maxValueSync(query=null) {
        const maxKey = this.maxKeySync(query);
        return this.getSync(maxKey);
    }

    /**
     * Returns a promise of the key being maximal for the given range.
     * If the optional query is not given, it returns the maximal key.
     * If the query is of type KeyRange, it returns the key being maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<string>} A promise of the key relevant to the query.
     * @override
     */
    async maxKey(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the key being maximal for the given range.
     * If the optional query is not given, it returns the maximal key.
     * If the query is of type KeyRange, it returns the key being maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {string} The key relevant to the query.
     */
    maxKeySync(query=null) {
        // Take underlying maxKey.
        let maxKey = undefined;
        if (!this._truncated) {
            for (const key of this._cache.keys()) {
                // Find better maxKey in modified data.
                if (!this._removed.has(key) && (query === null || query.includes(key)) && (maxKey === undefined || key > maxKey)) {
                    maxKey = key;
                }
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (maxKey === undefined || key > maxKey)) {
                maxKey = key;
            }
        }
        return maxKey;
    }

    /**
     * @override
     */
    async minValue(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the object whose primary key is minimal for the given range.
     * If the optional query is not given, it returns the object whose key is minimal.
     * If the query is of type KeyRange, it returns the object whose primary key is minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {*} The object relevant to the query.
     */
    minValueSync(query=null) {
        const minKey = this.minKeySync(query);
        return this.getSync(minKey, true);
    }

    /**
     * @override
     */
    async minKey(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the key being minimal for the given range.
     * If the optional query is not given, it returns the minimal key.
     * If the query is of type KeyRange, it returns the key being minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {string} The key relevant to the query.
     */
    minKeySync(query=null) {
        // Take underlying minKey.
        let minKey = undefined;
        if (!this._truncated) {
            for (const key of this._cache.keys()) {
                // Find better maxKey in modified data.
                if (!this._removed.has(key) && (query === null || query.includes(key)) && (minKey === undefined || key < minKey)) {
                    minKey = key;
                }
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (minKey === undefined || key < minKey)) {
                minKey = key;
            }
        }
        return minKey;
    }

    /**
     * @override
     */
    async count(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    snapshot() {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async _commitBackend() {
        // Fill original values.
        if (!this._truncated) {
            for (const key of this._modified.keys()) {
                if (this._originalValues.get(key)) continue;
                this._originalValues.set(key, this._getCached(key, false) || await this._parent.get(key));
            }

            for (const key of (new Set(this._removed)).values()) {
                if (this._originalValues.get(key)) continue;
                const value =  this._getCached(key, false) || await this._parent.get(key);
                if (value) {
                    this._originalValues.set(key, value);
                } else {
                    this._removed.delete(key);
                }
            }
        }
        return Transaction.prototype._commitBackend.call(this);
    }
}
Class.register(SynchronousTransaction);
<commit_msg>Improve logic of _getCached function.<commit_after>/**
 * Synchronous transactions avoid unnecessary async/await calls by preloading and caching
 * all necessary key-value-pairs.
 *
 * WARNING: If not all required key-value-pairs are preloaded, the results of any call on a synchronous transaction
 * might be wrong. Only use synchronous transactions, if unavoidable.
 * @implements {IObjectStore}
 * @implements {ICommittable}
 * @extends {Transaction}
 */
class SynchronousTransaction extends Transaction {
    /**
     * This constructor should only be called by an ObjectStore object.
     * Our transactions have a watchdog enabled by default,
     * logging a warning after a certain time specified by WATCHDOG_TIMER.
     * This helps to detect unclosed transactions preventing to store the state in
     * the persistent backend.
     * @param {ObjectStore} objectStore The object store this transaction belongs to.
     * @param {IObjectStore} parent The backend on which the transaction is based,
     * i.e., another transaction or the real database.
     * @param {ICommittable} [managingBackend] The object store managing the transactions,
     * i.e., the ObjectStore object.
     * @param {boolean} [enableWatchdog] If this is is set to true (default),
     * a warning will be logged if left open for longer than WATCHDOG_TIMER.
     * @protected
     */
    constructor(objectStore, parent, managingBackend, enableWatchdog=true) {
        super(objectStore, parent, managingBackend, enableWatchdog);
        /** @type {Map.<string,*>} */
        this._cache = new Map();
    }

    /**
     * This method preloads a set of keys and caches them.
     * It can be called as often as needed.
     * @param {Array.<string>} keys The keys to preload.
     * @return {Promise}
     */
    async preload(keys) {
        keys = keys.filter(key => !this.isCached(key));

        const promises = keys.map(key => Transaction.prototype.get.call(this, key));
        const values = await Promise.all(promises);
        for (let i = 0; i < keys.length; ++i) {
            if (values[i]) {
                this._cache.set(keys[i], values[i]);
            }
        }
    }

    /**
     * A check whether a certain key is cached.
     * @param {string} key The key to check.
     * @return {boolean} A boolean indicating whether the key is already in the cache.
     */
    isCached(key) {
        return this._cache.has(key);
    }

    /**
     * @override
     */
    async get(key) {
        // Use cache or ask parent.
        let value = this._cache.get(key);
        if (!value) {
            value = await Transaction.prototype.get.call(this, key);
            this._cache.set(key, value);
        }
        return value;
    }

    /**
     * Internal method to query cache.
     * @param {string} key
     * @param {boolean} [expectPresence]
     * @return {*} The cached value.
     * @private
     */
    _getCached(key, expectPresence=true) {
        const value = this._cache.get(key);

        // Use cache only if the parent is not synchronous.
        if (!value && this._parent instanceof SynchronousTransaction) {
            return this._parent.getSync(key, expectPresence);
        }

        if (expectPresence && !value) {
            throw new Error(`Missing key in cache: ${key}`);
        }
        return value;
    }

    /**
     * Returns a promise of the object stored under the given primary key.
     * Resolves to undefined if the key is not present in the object store.
     * @param {string} key The primary key to look for.
     * @param {boolean} [expectPresence] Method throws an error if key is not present in transaction or cache.
     * @returns {*} The object stored under the given key, or undefined if not present.
     */
    getSync(key, expectPresence=true) {
        // Order is as follows:
        // 1. check if removed,
        // 2. check if modified,
        // 3. check if truncated
        // 4. request from backend
        if (this._removed.has(key)) {
            return undefined;
        }
        if (this._modified.has(key)) {
            return this._modified.get(key);
        }
        if (this._truncated) {
            return undefined;
        }
        return this._getCached(key, expectPresence);
    }

    /**
     * @override
     */
    async put(key, value) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Inserts or replaces a key-value pair.
     * @param {string} key The primary key to associate the value with.
     * @param {*} value The value to write.
     */
    putSync(key, value) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        const oldValue = this.getSync(key, false);

        // Save for indices.
        if (!this._originalValues.has(key)) {
            this._originalValues.set(key, oldValue);
        }

        this._put(key, value, oldValue);
    }

    /**
     * @override
     */
    async remove(key) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Removes the key-value pair of the given key from the object store.
     * @param {string} key The primary key to delete along with the associated object.
     */
    removeSync(key) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        const oldValue = this.getSync(key, false);
        // Save for indices.
        if (!this._originalValues.has(key)) {
            this._originalValues.set(key, oldValue);
        }

        this._remove(key, oldValue);
    }

    /**
     * @override
     */
    async keys(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of a set of keys fulfilling the given query.
     * If the optional query is not given, it returns all keys in the object store.
     * If the query is of type KeyRange, it returns all keys of the object store being within this range.
     * If the query is of type Query, it returns all keys fulfilling the query.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Set.<string>} The set of keys relevant to the query.
     */
    keysSync(query=null) {
        if (query !== null && query instanceof Query) {
            throw new Error('Invalid call on SynchronousTransaction');
        }
        let keys = new Set();
        for (const key of this._cache.keys()) {
            if (query === null || query.includes(key)) {
                keys.add(key);
            }
        }
        for (const key of this._modified.keys()) {
            if (query === null || query.includes(key)) {
                keys.add(key);
            }
        }
        return keys;
    }

    /**
     * @override
     */
    async values(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of an array of objects whose primary keys fulfill the given query.
     * If the optional query is not given, it returns all objects in the object store.
     * If the query is of type KeyRange, it returns all objects whose primary keys are within this range.
     * If the query is of type Query, it returns all objects whose primary keys fulfill the query.
     * @param {Query|KeyRange} [query] Optional query to check keys against.
     * @returns {Array.<*>} The array of objects relevant to the query.
     */
    valuesSync(query=null) {
        if (query !== null && query instanceof Query) {
            throw new Error('Invalid call on SynchronousTransaction');
        }
        const keys = this.keysSync(query);
        const values = [];
        for (const key of keys) {
            values.push(this.getSync(key, true));
        }
        return values;
    }

    /**
     * @override
     */
    async keyStream(callback, ascending=true, query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async valueStream(callback, ascending=true, query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async maxValue(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the object whose primary key is maximal for the given range.
     * If the optional query is not given, it returns the object whose key is maximal.
     * If the query is of type KeyRange, it returns the object whose primary key is maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {*} The object relevant to the query.
     */
    maxValueSync(query=null) {
        const maxKey = this.maxKeySync(query);
        return this.getSync(maxKey);
    }

    /**
     * Returns a promise of the key being maximal for the given range.
     * If the optional query is not given, it returns the maximal key.
     * If the query is of type KeyRange, it returns the key being maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<string>} A promise of the key relevant to the query.
     * @override
     */
    async maxKey(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the key being maximal for the given range.
     * If the optional query is not given, it returns the maximal key.
     * If the query is of type KeyRange, it returns the key being maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {string} The key relevant to the query.
     */
    maxKeySync(query=null) {
        // Take underlying maxKey.
        let maxKey = undefined;
        if (!this._truncated) {
            for (const key of this._cache.keys()) {
                // Find better maxKey in modified data.
                if (!this._removed.has(key) && (query === null || query.includes(key)) && (maxKey === undefined || key > maxKey)) {
                    maxKey = key;
                }
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (maxKey === undefined || key > maxKey)) {
                maxKey = key;
            }
        }
        return maxKey;
    }

    /**
     * @override
     */
    async minValue(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the object whose primary key is minimal for the given range.
     * If the optional query is not given, it returns the object whose key is minimal.
     * If the query is of type KeyRange, it returns the object whose primary key is minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {*} The object relevant to the query.
     */
    minValueSync(query=null) {
        const minKey = this.minKeySync(query);
        return this.getSync(minKey, true);
    }

    /**
     * @override
     */
    async minKey(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * Returns a promise of the key being minimal for the given range.
     * If the optional query is not given, it returns the minimal key.
     * If the query is of type KeyRange, it returns the key being minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {string} The key relevant to the query.
     */
    minKeySync(query=null) {
        // Take underlying minKey.
        let minKey = undefined;
        if (!this._truncated) {
            for (const key of this._cache.keys()) {
                // Find better maxKey in modified data.
                if (!this._removed.has(key) && (query === null || query.includes(key)) && (minKey === undefined || key < minKey)) {
                    minKey = key;
                }
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (minKey === undefined || key < minKey)) {
                minKey = key;
            }
        }
        return minKey;
    }

    /**
     * @override
     */
    async count(query=null) {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    snapshot() {
        throw new Error('Invalid call on SynchronousTransaction');
    }

    /**
     * @override
     */
    async _commitBackend() {
        // Fill original values.
        if (!this._truncated) {
            for (const key of this._modified.keys()) {
                if (this._originalValues.get(key)) continue;
                this._originalValues.set(key, this._getCached(key, false) || await this._parent.get(key));
            }

            for (const key of (new Set(this._removed)).values()) {
                if (this._originalValues.get(key)) continue;
                const value =  this._getCached(key, false) || await this._parent.get(key);
                if (value) {
                    this._originalValues.set(key, value);
                } else {
                    this._removed.delete(key);
                }
            }
        }
        return Transaction.prototype._commitBackend.call(this);
    }
}
Class.register(SynchronousTransaction);
<|endoftext|>


<commit_before>/* jshint node:true */
/* global describe, it */
'use strict';

var qa = require('../mocha-qa.js');
var doneMock = require('./utils/done-mock.js');

var q = require('q');

describe('MochaJS-QA using q promise library', function () {
  describe('and it', function () {

    it ('will resolve the test on an immediately fullfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.it('it will test a promise', function () {
        return q.resolve();
      });
    });

    it ('will fail the test on an immediately failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.it('it will test a promise', function () {
        return q.reject(new Error('Fail'));
      });
    });

    it ('will resolve the test on an deferred fullfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.it('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.resolve();
        }, 0);

        return deferred.promise;
      });
    });

    it ('will fail the test on an deferred failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.it('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.reject(new Error('Fail'));
        }, 0);

        return deferred.promise;
      });
    });
  });

  describe('and catch', function () {
    it ('will resolve the test on an immediately failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.catch('it will test a promise', function () {
        return q.reject(new Error('Fail'));
      });
    });

    it ('will fail the test on an immediately fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.catch('it will test a promise', function () {
        return q.resolve();
      });
    });

    it ('will resolve the test on an deferred failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.catch('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.reject(new Error('Fail'));
        }, 0);

        return deferred.promise;
      });
    });

    it ('will fail the test on an deferred fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.catch('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.resolve();
        }, 0);

        return deferred.promise;
      });
    });
  });

  describe('and using setup hooks', function () {
    it ('will resolve a before hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.before(function hook () {
        return q.resolve();
      });
    });

    it ('will resolve a after hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.after(function hook () {
        return q.resolve();
      });
    });

    it ('will resolve a beforeEach hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.beforeEach(function hook () {
        return q.resolve();
      });
    });

    it ('will resolve a afterEach hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.afterEach(function hook () {
        return q.resolve();
      });
    });

    it ('will reject a before hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.before(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    it ('will reject a after hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.after(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    it ('will reject a beforeEach hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.beforeEach(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    it ('will reject a afterEach hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.afterEach(function hook () {
        return q.reject(new Error('Fail'));
      });
    });
  });
});<commit_msg>Updates tests to use global definitions<commit_after>/* jshint node:true */
/* global describe, it, qa.catchIt, before, beforeEach, after, afterEach */
'use strict';

var mocha = {};

mocha.it = it;
mocha.before = before;
mocha.beforeEach = beforeEach;
mocha.after = after;
mocha.afterEach = afterEach;

var qa = require('../mocha-qa.js');

it = qa.it;
before = qa.before;
beforeEach = qa.beforeEach;
after = qa.after;
afterEach = qa.afterEach;

var doneMock = require('./utils/done-mock.js');

var q = require('q');

describe('MochaJS-QA using q promise library', function () {
  describe('and it', function () {

    mocha.it ('will resolve the test on an immediately fullfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      it('it will test a promise', function () {
        return q.resolve();
      });
    });

    mocha.it ('will fail the test on an immediately failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      it('it will test a promise', function () {
        return q.reject(new Error('Fail'));
      });
    });

    mocha.it ('will resolve the test on an deferred fullfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      it('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.resolve();
        }, 0);

        return deferred.promise;
      });
    });

    mocha.it ('will fail the test on an deferred failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      it('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.reject(new Error('Fail'));
        }, 0);

        return deferred.promise;
      });
    });
  });

  describe('and catch', function () {
    mocha.it ('will resolve the test on an immediately failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.catchIt('it will test a promise', function () {
        return q.reject(new Error('Fail'));
      });
    });

    mocha.it ('will fail the test on an immediately fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.catchIt('it will test a promise', function () {
        return q.resolve();
      });
    });

    mocha.it ('will resolve the test on an deferred failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      qa.catchIt('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.reject(new Error('Fail'));
        }, 0);

        return deferred.promise;
      });
    });

    mocha.it ('will fail the test on an deferred fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      qa.catchIt('it will test a promise', function () {
        var deferred = q.defer();

        setTimeout(function () {
          deferred.resolve();
        }, 0);

        return deferred.promise;
      });
    });
  });

  describe('and using setup hooks', function () {
    mocha.it ('will resolve a before hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      before(function hook () {
        return q.resolve();
      });
    });

    mocha.it ('will resolve a after hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      after(function hook () {
        return q.resolve();
      });
    });

    mocha.it ('will resolve a beforeEach hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      beforeEach(function hook () {
        return q.resolve();
      });
    });

    mocha.it ('will resolve a afterEach hook on a fulfilling promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnDone(mochaDone));

      afterEach(function hook () {
        return q.resolve();
      });
    });

    mocha.it ('will reject a before hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      before(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    mocha.it ('will reject a after hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      after(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    mocha.it ('will reject a beforeEach hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      beforeEach(function hook () {
        return q.reject(new Error('Fail'));
      });
    });

    mocha.it ('will reject a afterEach hook on a failing promise', function (mochaDone) {

      qa._test(doneMock.it, doneMock.resolveOnError(mochaDone));

      afterEach(function hook () {
        return q.reject(new Error('Fail'));
      });
    });
  });
});<|endoftext|>


<commit_before>'use strict'

const chai = require('chai')
const expect = chai.expect
const sinon = require('sinon')
const proxyquire = require('proxyquire').noCallThru()

chai.use(require('sinon-chai'))

describe('mocker: rtm', function () {
  let rtm
  let wsMock
  let wsServerMock
  let wsClientMock
  let expressMock
  let expressAppMock
  let serverMock
  let loggerMock

  before(function () {
    wsClientMock = {
      send: sinon.stub(),
      on: sinon.stub()
    }

    wsServerMock = {
      on: sinon.stub(),
      close: sinon.stub(),
      clients: [wsClientMock]
    }

    wsMock = {
      Server: sinon.stub()
    }

    serverMock = 'server'

    expressAppMock = {
      listen: sinon.stub().returns(serverMock)
    }

    expressMock = sinon.stub().returns(expressAppMock)

    loggerMock = {
      error: sinon.stub(),
      info: sinon.stub(),
      debug: sinon.stub()
    }

    rtm = proxyquire('../../src/mocker/rtm', {
      'ws': wsMock,
      'express': expressMock,
      '../lib/logger': loggerMock
    })
  })

  beforeEach(function () {
    rtm.reset()

    loggerMock.error.reset()
    loggerMock.info.reset()
    loggerMock.debug.reset()

    wsClientMock.send.reset()
    wsClientMock.on.reset()
    wsClientMock.send.yields()

    wsServerMock.on.reset()
    wsServerMock.close.reset()
    wsServerMock.close.yields()

    wsMock.Server.reset()
    wsMock.Server.returns(wsServerMock)

    expressAppMock.listen.reset()
    expressAppMock.listen.yields()
    expressAppMock.listen.returns(serverMock)

    expressMock.reset()
    expressMock.returns(expressAppMock)
  })

  describe('init', function () {
    it('should start the express server', function () {
      rtm._.init({rtmPort: 9000})
      expect(expressAppMock.listen).to.have.been.calledWith(9000)
    })
  })

  describe('addToken', function () {
    let token

    beforeEach(function () {
      token = 'abc123'
      // create server
      rtm._.init({rtmPort: 9001})
    })

    afterEach(function () {
      rtm.stopServer('abc123')
    })

    it('should start the websocket client once', function () {
      const rtmUrl1 = rtm._.addToken(token)
      const rtmUrl2 = rtm._.addToken(token)

      expect(wsMock.Server).to.have.been.calledOnce
      expect(rtmUrl1).to.equal(rtmUrl2)
      expect(rtmUrl1).to.equal('ws://localhost:9001/abc123')

      expect(wsMock.Server).to.have.been.calledWith({
        server: serverMock,
        path: `/${token}`,
        clientTracking: true
      })
    })

    it('should register a message listener on the client', function () {
      rtm._.addToken(token)

      const connectionCb = wsServerMock.on.args[0][1]
      connectionCb(wsClientMock)

      expect(wsClientMock.on).to.have.been.calledWith('message')
    })

    describe('message recording', function () {
      let messageCb

      beforeEach(function () {
        rtm._.addToken(token)
        const connectionCb = wsServerMock.on.args[0][1]
        connectionCb(wsClientMock)
        messageCb = wsClientMock.on.args[0][1]
      })

      it('should record a message to the client', function () {
        const message = JSON.stringify({hello: 'world', token: token})
        messageCb(message)

        expect(rtm.calls).to.have.length(1)
        expect(rtm.calls[0]).to.have.keys(['rawMessage', 'message', 'token'])
        expect(rtm.calls[0].message.hello).to.equal('world')
        expect(rtm.calls[0].token).to.equal(token)
        expect(rtm.calls[0].rawMessage).to.equal(message)
      })

      it('should record a message even if it is unparseable', function () {
        const message = '{hello:'
        messageCb(message)

        expect(rtm.calls).to.have.length(1)
        expect(rtm.calls[0].message).to.equal(null)
        expect(rtm.calls[0].token).to.equal(null)
        expect(rtm.calls[0].rawMessage).to.equal('{hello:')
      })
    })
  })

  describe('send', function () {
    let token

    beforeEach(function () {
      token = 'crystalbluepersuation'
      rtm._.init({rtmPort: 9001}) // create server
      rtm._.addToken(token) // create server
    })

    it('should send a message', function () {
      return rtm.send(token, {walter: 'white'})
        .then(() => {
          expect(wsClientMock.send).to.have.been.calledWith(`{"walter":"white"}`)
        })
    })

    it('should reject if there is no server for a token', function () {
      return rtm.send('capncook', {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e.message).to.match(/client with token/)
        })
    })

    it('should reject if message could not be stringified', function () {
      const parseError = new Error('could not parse')

      sinon.stub(JSON, 'stringify')
      JSON.stringify.throws(parseError)

      return rtm.send(token, {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e).to.equal(parseError)
        })
        .then(restore, restore)

      function restore () {
        JSON.stringify.restore()
      }
    })

    it('should reject if there was an error with the send', function () {
      const sendError = new Error('could not send')
      wsClientMock.send.yields(sendError)

      return rtm.send(token, {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e).to.equal(sendError)
        })
    })
  })

  describe('reset', function () {
    it('should clear calls array', function () {
      rtm.calls.push(wsClientMock)
      rtm.reset()
      expect(rtm.calls).to.have.length(0)
    })
  })

  describe('startServer', function () {
    beforeEach(function () {
      sinon.stub(rtm._, 'addToken')
    })

    afterEach(function () {
      rtm._.addToken.restore()
    })

    it('should call addToken', function () {
      rtm.startServer('abc123')
      expect(rtm._.addToken).to.have.been.calledWith('abc123')
    })
  })

  describe('stopServer', function () {
    let token

    beforeEach(function () {
      token = 'heisenberg'
      rtm.startServer(token)
      wsServerMock.close.reset()
      wsServerMock.close.yields()
    })

    it('should stop the server if it exists', function () {
      return rtm.stopServer(token)
        .then(() => {
          expect(wsServerMock.close).to.have.been.called
        })
    })

    it('should re-create a server after server is stopped', function () {
      return rtm.stopServer(token)
        .then(() => {
          wsMock.Server.reset()
          rtm._.addToken('abc123')
        })
        .then(() => {
          expect(wsMock.Server).to.have.been.calledOnce
        })
    })

    it('should resolve if server does not exists', function () {
      return rtm.stopServer('notreal')
        .then(() => {
          expect(wsServerMock.close).not.to.have.been.called
        })
    })

    it('should reject if there is an error closing the server', function () {
      const err = new Error('GUS FRING')
      wsServerMock.close.yields(err)

      return rtm.stopServer(token)
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(wsServerMock.close).to.have.been.called
          expect(e).to.equal(err)
        })
    })
  })
})
<commit_msg>Fix tests<commit_after>'use strict'

const chai = require('chai')
const expect = chai.expect
const sinon = require('sinon')
const proxyquire = require('proxyquire').noCallThru()

chai.use(require('sinon-chai'))

describe('mocker: rtm', function () {
  let rtm
  let wsMock
  let wsServerMock
  let wsClientMock
  let expressMock
  let expressAppMock
  let serverMock
  let loggerMock

  before(function () {
    wsClientMock = {
      send: sinon.stub(),
      on: sinon.stub()
    }

    wsServerMock = {
      on: sinon.stub(),
      close: sinon.stub(),
      clients: [wsClientMock]
    }

    wsMock = {
      Server: sinon.stub()
    }

    serverMock = {
      close: sinon.stub().returns()
    }

    expressAppMock = {
      listen: sinon.stub().returns(serverMock),
      close: serverMock.close()
    }

    expressMock = sinon.stub().returns(expressAppMock)

    loggerMock = {
      error: sinon.stub(),
      info: sinon.stub(),
      debug: sinon.stub()
    }

    rtm = proxyquire('../../src/mocker/rtm', {
      'ws': wsMock,
      'express': expressMock,
      '../lib/logger': loggerMock
    })
  })

  beforeEach(function () {
    rtm.reset()

    loggerMock.error.reset()
    loggerMock.info.reset()
    loggerMock.debug.reset()

    wsClientMock.send.reset()
    wsClientMock.on.reset()
    wsClientMock.send.yields()

    wsServerMock.on.reset()
    wsServerMock.close.reset()
    wsServerMock.close.yields()

    wsMock.Server.reset()
    wsMock.Server.returns(wsServerMock)

    expressAppMock.listen.reset()
    expressAppMock.listen.yields()
    expressAppMock.listen.returns(serverMock)

    expressMock.reset()
    expressMock.returns(expressAppMock)
  })

  describe('init', function () {
    it('should start the express server', function () {
      rtm._.init({rtmPort: 9000})
      expect(expressAppMock.listen).to.have.been.calledWith(9000)
    })
  })

  describe('addToken', function () {
    let token

    beforeEach(function () {
      token = 'abc123'
      // create server
      rtm._.init({rtmPort: 9001})
    })

    afterEach(function () {
      rtm.stopServer('abc123')
    })

    it('should start the websocket client once', function () {
      const rtmUrl1 = rtm._.addToken(token)
      const rtmUrl2 = rtm._.addToken(token)

      expect(wsMock.Server).to.have.been.calledOnce
      expect(rtmUrl1).to.equal(rtmUrl2)
      expect(rtmUrl1).to.equal('ws://localhost:9001/abc123')

      expect(wsMock.Server).to.have.been.calledWith({
        server: serverMock,
        path: `/${token}`,
        clientTracking: true
      })
    })

    it('should register a message listener on the client', function () {
      rtm._.addToken(token)

      const connectionCb = wsServerMock.on.args[0][1]
      connectionCb(wsClientMock)

      expect(wsClientMock.on).to.have.been.calledWith('message')
    })

    describe('message recording', function () {
      let messageCb

      beforeEach(function () {
        rtm._.addToken(token)
        const connectionCb = wsServerMock.on.args[0][1]
        connectionCb(wsClientMock)
        messageCb = wsClientMock.on.args[0][1]
      })

      it('should record a message to the client', function () {
        const message = JSON.stringify({hello: 'world', token: token})
        messageCb(message)

        expect(rtm.calls).to.have.length(1)
        expect(rtm.calls[0]).to.have.keys(['rawMessage', 'message', 'token'])
        expect(rtm.calls[0].message.hello).to.equal('world')
        expect(rtm.calls[0].token).to.equal(token)
        expect(rtm.calls[0].rawMessage).to.equal(message)
      })

      it('should record a message even if it is unparseable', function () {
        const message = '{hello:'
        messageCb(message)

        expect(rtm.calls).to.have.length(1)
        expect(rtm.calls[0].message).to.equal(null)
        expect(rtm.calls[0].token).to.equal(null)
        expect(rtm.calls[0].rawMessage).to.equal('{hello:')
      })
    })
  })

  describe('send', function () {
    let token

    beforeEach(function () {
      token = 'crystalbluepersuation'
      rtm._.init({rtmPort: 9001}) // create server
      rtm._.addToken(token) // create server
    })

    it('should send a message', function () {
      return rtm.send(token, {walter: 'white'})
        .then(() => {
          expect(wsClientMock.send).to.have.been.calledWith(`{"walter":"white"}`)
        })
    })

    it('should reject if there is no server for a token', function () {
      return rtm.send('capncook', {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e.message).to.match(/client with token/)
        })
    })

    it('should reject if message could not be stringified', function () {
      const parseError = new Error('could not parse')

      sinon.stub(JSON, 'stringify')
      JSON.stringify.throws(parseError)

      return rtm.send(token, {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e).to.equal(parseError)
        })
        .then(restore, restore)

      function restore () {
        JSON.stringify.restore()
      }
    })

    it('should reject if there was an error with the send', function () {
      const sendError = new Error('could not send')
      wsClientMock.send.yields(sendError)

      return rtm.send(token, {walter: 'white'})
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(e).to.equal(sendError)
        })
    })
  })

  describe('reset', function () {
    it('should clear calls array', function () {
      rtm.calls.push(wsClientMock)
      rtm.reset()
      expect(rtm.calls).to.have.length(0)
    })
  })

  describe('startServer', function () {
    beforeEach(function () {
      sinon.stub(rtm._, 'addToken')
    })

    afterEach(function () {
      rtm._.addToken.restore()
    })

    it('should call addToken', function () {
      rtm.startServer('abc123')
      expect(rtm._.addToken).to.have.been.calledWith('abc123')
    })
  })

  describe('stopServer', function () {
    let token

    beforeEach(function () {
      token = 'heisenberg'
      rtm.startServer(token)
      wsServerMock.close.reset()
      wsServerMock.close.yields()
    })

    it('should stop the server if it exists', function () {
      return rtm.stopServer(token)
        .then(() => {
          expect(wsServerMock.close).to.have.been.called
        })
    })

    it('should re-create a server after server is stopped', function () {
      return rtm.stopServer(token)
        .then(() => {
          wsMock.Server.reset()
          rtm._.addToken('abc123')
        })
        .then(() => {
          expect(wsMock.Server).to.have.been.calledOnce
        })
    })

    it('should resolve if server does not exists', function () {
      return rtm.stopServer('notreal')
        .then(() => {
          expect(wsServerMock.close).not.to.have.been.called
        })
    })

    it('should reject if there is an error closing the server', function () {
      const err = new Error('GUS FRING')
      wsServerMock.close.yields(err)

      return rtm.stopServer(token)
        .then(() => {
          throw new Error('expected promise to fail.')
        })
        .catch((e) => {
          expect(wsServerMock.close).to.have.been.called
          expect(e).to.equal(err)
        })
    })
  })
})
<|endoftext|>


<commit_before>"use strict"

var assert = require('chai').assert
var xdTesting = require('../../lib/index')
var templates = require('../../lib/templates')
var q = require('q')

describe('MultiDevice - any', function () {
    let test = this

    test.baseUrl = "http://localhost:8090/"
    const buttonSelector = '#button'

    let urlWithButton = withButton => test.baseUrl + "?hideButton=" + (withButton ? 0 : 1)

    it('should not use `any` device selection per default @medium', function() {
        var options = {
            A: templates.devices.chrome()
        }

        return xdTesting.multiremote(options)
            .getAddressingOptions()
            .then(addr => assert.equal(addr.any, false))
            .end()
    })

    it('should set `any` device selection @medium', function () {
        var options = {
            A: templates.devices.chrome()
        }

        return xdTesting.multiremote(options)
            .any(devices => devices
                .getAddressingOptions()
                .then(addr => assert.equal(addr.any, true))
            )
            .end()
    })

    it('should call callback on a single device @medium', function () {
        var options = {
            A: templates.devices.chrome()
        }

        let counter = 0

        return xdTesting.multiremote(options)
            .any(devices => devices
                .forEach(device => counter++)
            )
            .then(() => assert.equal(counter, 1, "Failed asserting that callback was called exactly once."))
            .end()
    })

    it('should select a single matching device for each command with element selector @large', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        let clickCount = 0

        return xdTesting.multiremote(options).init()
            .url(urlWithButton(true))
            .any(devices => devices
                .click('#button')
            )
            .forEach((device, index) => device
                .getText('#counter').then(text => clickCount += +(text === '-' ? 0 : text))
            )
            .then(() => assert.equal(1, clickCount, "Failed asserting that exactly one button was clicked."))
            .end()
    })

    it('should execute callback in order @medium', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        var queue = ''
        return xdTesting.multiremote(options)
            .then(() => queue += '0')
            .any(device => {
                queue += '1'
                return device.then(() => {
                    queue += '2'
                })
            })
            .then(() => queue += '3')
            .then(() => assert.equal(queue, '0123'))
            .end()
    })

})
<commit_msg>Address open design questions<commit_after>"use strict"

var assert = require('chai').assert
var xdTesting = require('../../lib/index')
var templates = require('../../lib/templates')
var q = require('q')

describe('MultiDevice - any', function () {
    let test = this

    test.baseUrl = "http://localhost:8090/"
    const buttonSelector = '#button'

    let urlWithButton = withButton => test.baseUrl + "?hideButton=" + (withButton ? 0 : 1)

    it('should not use `any` device selection per default @medium', function() {
        var options = {
            A: templates.devices.chrome()
        }

        return xdTesting.multiremote(options)
            .getAddressingOptions()
            .then(addr => assert.equal(addr.any, false))
            .end()
    })

    it('should set `any` device selection @medium', function () {
        var options = {
            A: templates.devices.chrome()
        }

        return xdTesting.multiremote(options)
            .any(devices => devices
                .getAddressingOptions()
                .then(addr => assert.equal(addr.any, true))
            )
            .end()
    })

    it.skip('should call .forEach on a single device? @medium', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        let counter = 0

        return xdTesting.multiremote(options)
            .any(devices => devices
                .forEach(device => counter++)
            )
            .then(() => assert.equal(counter, 1, "Failed asserting that callback was called exactly once."))
            .end()
    })

    it.skip('should .getCount return 1? @medium', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        let counter = 0

        return xdTesting.multiremote(options)
            .any(devices => devices
                .forEach(device => counter++)
            )
            .then(() => assert.equal(counter, 1, "Failed asserting that callback was called exactly once."))
            .end()
    })

    it.skip('should return the selection when no callback argument is given')

    it('should select a single matching device for each command with element selector @large', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        let clickCount = 0

        return xdTesting.multiremote(options).init()
            .url(urlWithButton(true))
            .any(devices => devices
                .click('#button')
            )
            .forEach((device, index) => device
                .getText('#counter').then(text => clickCount += +(text === '-' ? 0 : text))
            )
            .then(() => assert.equal(1, clickCount, "Failed asserting that exactly one button was clicked."))
            .end()
    })

    it('should execute callback in order @medium', function () {
        var options = {
            A: templates.devices.chrome(),
            B: templates.devices.chrome()
        }

        var queue = ''
        return xdTesting.multiremote(options)
            .then(() => queue += '0')
            .any(device => {
                queue += '1'
                return device.then(() => {
                    queue += '2'
                })
            })
            .then(() => queue += '3')
            .then(() => assert.equal(queue, '0123'))
            .end()
    })

})
<|endoftext|>