<commit_before>odoo.define('web.GraphRenderer', function (require) {
"use strict";

/**
 * The graph renderer turns the data from the graph model into a nice looking
 * svg chart.  This code uses the nvd3 library.
 *
 * Note that we use a custom build for the nvd3, with only the model we actually
 * use.
 */

var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var field_utils = require('web.field_utils');

var _t = core._t;
var qweb = core.qweb;

var CHART_TYPES = ['pie', 'bar', 'line'];

// hide top legend when too many items for device size
var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);

return AbstractRenderer.extend({
    className: "o_graph_svg_container",
    /**
     * @override
     * @param {Widget} parent
     * @param {Object} state
     * @param {Object} params
     * @param {boolean} params.stacked
     */
    init: function (parent, state, params) {
        this._super.apply(this, arguments);
        this.stacked = params.stacked;
        this.$el.css({minWidth: '100px', minHeight: '100px'});
    },
    /**
     * @override
     */
    destroy: function () {
        nv.utils.offWindowResize(this.to_remove);
        this._super();
    },
    /**
     * The graph view uses the nv(d3) lib to render the graph. This lib requires
     * that the rendering is done directly into the DOM (so that it can correctly
     * compute positions). However, the views are always rendered in fragments,
     * and appended to the DOM once ready (to prevent them from flickering). We
     * here use the on_attach_callback hook, called when the widget is attached
     * to the DOM, to perform the rendering. This ensures that the rendering is
     * always done in the DOM.
     *
     * @override
     */
    on_attach_callback: function () {
        this._super.apply(this, arguments);
        this.isInDOM = true;
        this._renderGraph();
    },
    /**
     * @override
     */
    on_detach_callback: function () {
        this._super.apply(this, arguments);
        this.isInDOM = false;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Render the chart.
     *
     * Note that This method is synchronous, but the actual rendering is done
     * asynchronously.  The reason for that is that nvd3/d3 needs to be in the
     * DOM to correctly render itself.  So, we trick Odoo by returning
     * immediately, then we render the chart when the widget is in the DOM.
     *
     * @returns {Deferred} The _super deferred is actually resolved immediately
     */
    _render: function () {
        if (this.to_remove) {
            nv.utils.offWindowResize(this.to_remove);
        }
        if (!_.contains(CHART_TYPES, this.state.mode)) {
            this.$el.empty();
            this.trigger_up('warning', {
                title: _t('Invalid mode for chart'),
                message: _t('Cannot render chart with mode : ') + this.state.mode
            });
        } else if (!this.state.data.length) {
            this.$el.empty();
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("No data to display"),
                description: _t("No data available for this chart. " +
                    "Try to add some records, or make sure that " +
                    "there is no active filter in the search bar."),
            }));
        } else if (this.isInDOM) {
            // only render the graph if the widget is already in the DOM (this
            // happens typically after an update), otherwise, it will be
            // rendered when the widget will be attached to the DOM (see
            // 'on_attach_callback')
            this._renderGraph();
        }
        return this._super.apply(this, arguments);
    },
    /**
     * Helper function to set up data properly for the multiBarChart model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderBarChart: function () {
        // prepare data for bar chart
        var self = this;
        var data, values;
        var measure = this.state.fields[this.state.measure].string;

        // zero groupbys
        if (this.state.groupedBy.length === 0) {
            data = [{
                values: [{
                    x: measure,
                    y: this.state.data[0].value}],
                key: measure
            }];
        }
        // one groupby
        if (this.state.groupedBy.length === 1) {
            values = this.state.data.map(function (datapt) {
                return {x: datapt.labels, y: datapt.value};
            });
            data = [
                {
                    values: values,
                    key: measure,
                }
            ];
        }
        if (this.state.groupedBy.length > 1) {
            var xlabels = [],
                series = [],
                label, serie, value;
            values = {};
            for (var i = 0; i < this.state.data.length; i++) {
                label = this.state.data[i].labels[0];
                serie = this.state.data[i].labels[1];
                value = this.state.data[i].value;
                if ((!xlabels.length) || (xlabels[xlabels.length-1] !== label)) {
                    xlabels.push(label);
                }
                series.push(this.state.data[i].labels[1]);
                if (!(serie in values)) {values[serie] = {};}
                values[serie][label] = this.state.data[i].value;
            }
            series = _.uniq(series);
            data = [];
            var current_serie, j;
            for (i = 0; i < series.length; i++) {
                current_serie = {values: [], key: series[i]};
                for (j = 0; j < xlabels.length; j++) {
                    current_serie.values.push({
                        x: xlabels[j],
                        y: values[series[i]][xlabels[j]] || 0,
                    });
                }
                data.push(current_serie);
            }
        }
        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(0);

        var chart = nv.models.multiBarChart();
        chart.options({
          margin: {left: 80, bottom: 100, top: 80, right: 0},
          delay: 100,
          transition: 10,
          showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
          showXAxis: true,
          showYAxis: true,
          rightAlignYAxis: false,
          stacked: this.stacked,
          reduceXTicks: false,
          rotateLabels: -20,
          showControls: (this.state.groupedBy.length > 1)
        });
        chart.yAxis.tickFormat(function (d) {
            var measure_field = self.state.fields[self.measure];
            return field_utils.format.float(d, {
                digits: measure_field && measure_field.digits || [69, 2],
            });
        });

        chart(svg);
        return chart;
    },
    /**
     * Helper function to set up data properly for the pieChart model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderPieChart: function () {
        var data = [];
        var all_negative = true;
        var some_negative = false;
        var all_zero = true;

        this.state.data.forEach(function (datapt) {
            all_negative = all_negative && (datapt.value < 0);
            some_negative = some_negative || (datapt.value < 0);
            all_zero = all_zero && (datapt.value === 0);
        });
        if (some_negative && !all_negative) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Invalid data"),
                description: _t("Pie chart cannot mix positive and negative numbers. " +
                    "Try to change your domain to only display positive results"),
            }));
            return;
        }
        if (all_zero) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Invalid data"),
                description: _t("Pie chart cannot display all zero numbers.. " +
                    "Try to change your domain to display positive results"),
            }));
            return;
        }
        if (this.state.groupedBy.length) {
            data = this.state.data.map(function (datapt) {
                return {x:datapt.labels.join("/"), y: datapt.value};
            });
        }
        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(100);

        var legend_right = config.device.size_class > config.device.SIZES.XS;

        var chart = nv.models.pieChart().labelType('percent');
        chart.options({
          delay: 250,
          showLegend: legend_right || _.size(data) <= MAX_LEGEND_LENGTH,
          legendPosition: legend_right ? 'right' : 'top',
          transition: 100,
          color: d3.scale.category10().range(),
        });

        chart(svg);
        return chart;
    },
    /**
     * Helper function to set up data properly for the line model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderLineChart: function () {
        if (this.state.data.length < 2) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Not enough data points"),
                description: "You need at least two data points to display a line chart."
            }));
            return;
        }
        var self = this;
        var data = [];
        var tickValues;
        var tickFormat;
        var measure = this.state.fields[this.state.measure].string;

        if (this.state.groupedBy.length === 1) {
            var values = this.state.data.map(function (datapt, index) {
                return {x: index, y: datapt.value};
            });
            data = [
                {
                    values: values,
                    key: measure,
                }
            ];
            tickValues = this.state.data.map(function (d, i) { return i;});
            tickFormat = function (d) {return self.state.data[d].labels;};
        }
        if (this.state.groupedBy.length > 1) {
            data = [];
            var data_dict = {};
            var tick = 0;
            var tickLabels = [];
            var serie, tickLabel;
            var identity = function (p) {return p;};
            tickValues = [];
            for (var i = 0; i < this.state.data.length; i++) {
                if (this.state.data[i].labels[0] !== tickLabel) {
                    tickLabel = this.state.data[i].labels[0];
                    tickValues.push(tick);
                    tickLabels.push(tickLabel);
                    tick++;
                }
                serie = this.state.data[i].labels[1];
                if (!data_dict[serie]) {
                    data_dict[serie] = {
                        values: [],
                        key: serie,
                    };
                }
                data_dict[serie].values.push({
                    x: tick - 1, y: this.state.data[i].value,
                });
                data = _.map(data_dict, identity);
            }
            tickFormat = function (d) {return tickLabels[d];};
        }

        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(0);

        var chart = nv.models.lineChart();
        chart.options({
          margin: {left: 80, bottom: 100, top: 80, right: 0},
          useInteractiveGuideline: true,
          showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
          showXAxis: true,
          showYAxis: true,
        });
        chart.xAxis.tickValues(tickValues)
            .tickFormat(tickFormat);
        chart.yAxis.tickFormat(function (d) {
            return field_utils.format.float(d, {
                digits : self.state.fields[self.state.measure] && self.state.fields[self.state.measure].digits || [69, 2],
            });
        });

        chart(svg);
        return chart;
    },
    /**
     * Renders the graph according to its type. This function must be called
     * when the renderer is in the DOM (for nvd3 to render the graph correctly).
     *
     * @private
     */
    _renderGraph: function () {
        this.$el.empty();
        var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart']();
        if (chart && chart.tooltip.chartContainer) {
            this.to_remove = chart.update;
            nv.utils.onWindowResize(chart.update);
            chart.tooltip.chartContainer(this.el);
        }
    },
});

});
<commit_msg>[FIX] web: graph_renderer: check graph data before rendering<commit_after>odoo.define('web.GraphRenderer', function (require) {
"use strict";

/**
 * The graph renderer turns the data from the graph model into a nice looking
 * svg chart.  This code uses the nvd3 library.
 *
 * Note that we use a custom build for the nvd3, with only the model we actually
 * use.
 */

var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var field_utils = require('web.field_utils');

var _t = core._t;
var qweb = core.qweb;

var CHART_TYPES = ['pie', 'bar', 'line'];

// hide top legend when too many items for device size
var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);

return AbstractRenderer.extend({
    className: "o_graph_svg_container",
    /**
     * @override
     * @param {Widget} parent
     * @param {Object} state
     * @param {Object} params
     * @param {boolean} params.stacked
     */
    init: function (parent, state, params) {
        this._super.apply(this, arguments);
        this.stacked = params.stacked;
        this.$el.css({minWidth: '100px', minHeight: '100px'});
    },
    /**
     * @override
     */
    destroy: function () {
        nv.utils.offWindowResize(this.to_remove);
        this._super();
    },
    /**
     * The graph view uses the nv(d3) lib to render the graph. This lib requires
     * that the rendering is done directly into the DOM (so that it can correctly
     * compute positions). However, the views are always rendered in fragments,
     * and appended to the DOM once ready (to prevent them from flickering). We
     * here use the on_attach_callback hook, called when the widget is attached
     * to the DOM, to perform the rendering. This ensures that the rendering is
     * always done in the DOM.
     *
     * @override
     */
    on_attach_callback: function () {
        this._super.apply(this, arguments);
        this.isInDOM = true;
        this._render();
    },
    /**
     * @override
     */
    on_detach_callback: function () {
        this._super.apply(this, arguments);
        this.isInDOM = false;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Render the chart.
     *
     * Note that This method is synchronous, but the actual rendering is done
     * asynchronously.  The reason for that is that nvd3/d3 needs to be in the
     * DOM to correctly render itself.  So, we trick Odoo by returning
     * immediately, then we render the chart when the widget is in the DOM.
     *
     * @returns {Deferred} The _super deferred is actually resolved immediately
     */
    _render: function () {
        if (this.to_remove) {
            nv.utils.offWindowResize(this.to_remove);
        }
        if (!_.contains(CHART_TYPES, this.state.mode)) {
            this.$el.empty();
            this.trigger_up('warning', {
                title: _t('Invalid mode for chart'),
                message: _t('Cannot render chart with mode : ') + this.state.mode
            });
        } else if (!this.state.data.length) {
            this.$el.empty();
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("No data to display"),
                description: _t("No data available for this chart. " +
                    "Try to add some records, or make sure that " +
                    "there is no active filter in the search bar."),
            }));
        } else if (this.isInDOM) {
            // only render the graph if the widget is already in the DOM (this
            // happens typically after an update), otherwise, it will be
            // rendered when the widget will be attached to the DOM (see
            // 'on_attach_callback')
            this._renderGraph();
        }
        return this._super.apply(this, arguments);
    },
    /**
     * Helper function to set up data properly for the multiBarChart model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderBarChart: function () {
        // prepare data for bar chart
        var self = this;
        var data, values;
        var measure = this.state.fields[this.state.measure].string;

        // zero groupbys
        if (this.state.groupedBy.length === 0) {
            data = [{
                values: [{
                    x: measure,
                    y: this.state.data[0].value}],
                key: measure
            }];
        }
        // one groupby
        if (this.state.groupedBy.length === 1) {
            values = this.state.data.map(function (datapt) {
                return {x: datapt.labels, y: datapt.value};
            });
            data = [
                {
                    values: values,
                    key: measure,
                }
            ];
        }
        if (this.state.groupedBy.length > 1) {
            var xlabels = [],
                series = [],
                label, serie, value;
            values = {};
            for (var i = 0; i < this.state.data.length; i++) {
                label = this.state.data[i].labels[0];
                serie = this.state.data[i].labels[1];
                value = this.state.data[i].value;
                if ((!xlabels.length) || (xlabels[xlabels.length-1] !== label)) {
                    xlabels.push(label);
                }
                series.push(this.state.data[i].labels[1]);
                if (!(serie in values)) {values[serie] = {};}
                values[serie][label] = this.state.data[i].value;
            }
            series = _.uniq(series);
            data = [];
            var current_serie, j;
            for (i = 0; i < series.length; i++) {
                current_serie = {values: [], key: series[i]};
                for (j = 0; j < xlabels.length; j++) {
                    current_serie.values.push({
                        x: xlabels[j],
                        y: values[series[i]][xlabels[j]] || 0,
                    });
                }
                data.push(current_serie);
            }
        }
        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(0);

        var chart = nv.models.multiBarChart();
        chart.options({
          margin: {left: 80, bottom: 100, top: 80, right: 0},
          delay: 100,
          transition: 10,
          showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
          showXAxis: true,
          showYAxis: true,
          rightAlignYAxis: false,
          stacked: this.stacked,
          reduceXTicks: false,
          rotateLabels: -20,
          showControls: (this.state.groupedBy.length > 1)
        });
        chart.yAxis.tickFormat(function (d) {
            var measure_field = self.state.fields[self.measure];
            return field_utils.format.float(d, {
                digits: measure_field && measure_field.digits || [69, 2],
            });
        });

        chart(svg);
        return chart;
    },
    /**
     * Helper function to set up data properly for the pieChart model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderPieChart: function () {
        var data = [];
        var all_negative = true;
        var some_negative = false;
        var all_zero = true;

        this.state.data.forEach(function (datapt) {
            all_negative = all_negative && (datapt.value < 0);
            some_negative = some_negative || (datapt.value < 0);
            all_zero = all_zero && (datapt.value === 0);
        });
        if (some_negative && !all_negative) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Invalid data"),
                description: _t("Pie chart cannot mix positive and negative numbers. " +
                    "Try to change your domain to only display positive results"),
            }));
            return;
        }
        if (all_zero) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Invalid data"),
                description: _t("Pie chart cannot display all zero numbers.. " +
                    "Try to change your domain to display positive results"),
            }));
            return;
        }
        if (this.state.groupedBy.length) {
            data = this.state.data.map(function (datapt) {
                return {x:datapt.labels.join("/"), y: datapt.value};
            });
        }
        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(100);

        var legend_right = config.device.size_class > config.device.SIZES.XS;

        var chart = nv.models.pieChart().labelType('percent');
        chart.options({
          delay: 250,
          showLegend: legend_right || _.size(data) <= MAX_LEGEND_LENGTH,
          legendPosition: legend_right ? 'right' : 'top',
          transition: 100,
          color: d3.scale.category10().range(),
        });

        chart(svg);
        return chart;
    },
    /**
     * Helper function to set up data properly for the line model in
     * nvd3.
     *
     * @returns {nvd3 chart}
     */
    _renderLineChart: function () {
        if (this.state.data.length < 2) {
            this.$el.append(qweb.render('GraphView.error', {
                title: _t("Not enough data points"),
                description: "You need at least two data points to display a line chart."
            }));
            return;
        }
        var self = this;
        var data = [];
        var tickValues;
        var tickFormat;
        var measure = this.state.fields[this.state.measure].string;

        if (this.state.groupedBy.length === 1) {
            var values = this.state.data.map(function (datapt, index) {
                return {x: index, y: datapt.value};
            });
            data = [
                {
                    values: values,
                    key: measure,
                }
            ];
            tickValues = this.state.data.map(function (d, i) { return i;});
            tickFormat = function (d) {return self.state.data[d].labels;};
        }
        if (this.state.groupedBy.length > 1) {
            data = [];
            var data_dict = {};
            var tick = 0;
            var tickLabels = [];
            var serie, tickLabel;
            var identity = function (p) {return p;};
            tickValues = [];
            for (var i = 0; i < this.state.data.length; i++) {
                if (this.state.data[i].labels[0] !== tickLabel) {
                    tickLabel = this.state.data[i].labels[0];
                    tickValues.push(tick);
                    tickLabels.push(tickLabel);
                    tick++;
                }
                serie = this.state.data[i].labels[1];
                if (!data_dict[serie]) {
                    data_dict[serie] = {
                        values: [],
                        key: serie,
                    };
                }
                data_dict[serie].values.push({
                    x: tick - 1, y: this.state.data[i].value,
                });
                data = _.map(data_dict, identity);
            }
            tickFormat = function (d) {return tickLabels[d];};
        }

        var svg = d3.select(this.$el[0]).append('svg');
        svg.datum(data);

        svg.transition().duration(0);

        var chart = nv.models.lineChart();
        chart.options({
          margin: {left: 80, bottom: 100, top: 80, right: 0},
          useInteractiveGuideline: true,
          showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
          showXAxis: true,
          showYAxis: true,
        });
        chart.xAxis.tickValues(tickValues)
            .tickFormat(tickFormat);
        chart.yAxis.tickFormat(function (d) {
            return field_utils.format.float(d, {
                digits : self.state.fields[self.state.measure] && self.state.fields[self.state.measure].digits || [69, 2],
            });
        });

        chart(svg);
        return chart;
    },
    /**
     * Renders the graph according to its type. This function must be called
     * when the renderer is in the DOM (for nvd3 to render the graph correctly).
     *
     * @private
     */
    _renderGraph: function () {
        this.$el.empty();
        var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart']();
        if (chart && chart.tooltip.chartContainer) {
            this.to_remove = chart.update;
            nv.utils.onWindowResize(chart.update);
            chart.tooltip.chartContainer(this.el);
        }
    },
});

});
<|endoftext|>


<commit_before>'use strict'
const test = require('tape')
const State = require('../')
const log = require('./util/log')

test('force - basic', (t) => {
  const state = new State({
    a: {
      b: {
        c: {
          d: '$root.b'
        }
      }
    },
    b: {
      title: 'its b',
      c: {
        d: {
          e: 'its e'
        }
      }
    }
  })
  const arr = []
  // make it with switch instant
  state.subscribe({
    a: {
      b: {
        c: {
          d: {
            title: { val: true },
            c: {
              d: {
                e: { val: true }
              }
            }
          }
          // test with nested
        }
      }
    }
  }, state => {
    arr.push(state.path())
    // console.log('update',
  })

  // allow array transformer maybe -- its a lot nicer

  var forceObj = {
    [state.a.sid()]: true, // option to add stamp when nessecary -- maybe clean
    [state.a.b.sid()]: true,
    [state.a.b.c.sid()]: true,
    [state.a.b.c.d.sid()]: true,
    [state.b.title.sid()]: true,
    [state.b.c.sid()]: true,
    [state.b.c.d.sid()]: true,
    [state.b.c.d.e.sid()]: true
  }
  state.emit('subscription', forceObj)
  t.same(arr, [
    [ 'b', 'title' ],
    [ 'b', 'c', 'd', 'e' ],
    [ 'b', 'title' ],
    [ 'b', 'c', 'd', 'e' ]
  ])
  t.end()
})
<commit_msg>standard<commit_after>'use strict'
const test = require('tape')
const State = require('../')

test('force', (t) => {
  const state = new State({
    a: {
      b: {
        c: {
          d: '$root.b'
        }
      }
    },
    b: {
      title: 'its b',
      c: {
        d: {
          e: 'its e'
        }
      }
    }
  })
  const arr = []
  // make it with switch instant
  state.subscribe({
    a: {
      b: {
        c: {
          d: {
            title: { val: true },
            c: {
              d: {
                e: { val: true }
              }
            }
          }
          // test with nested
        }
      }
    }
  }, state => {
    arr.push(state.path())
  })

  var forceObj = {
    [state.a.sid()]: true, // option to add stamp when nessecary -- maybe clean
    [state.a.b.sid()]: true,
    [state.a.b.c.sid()]: true,
    [state.a.b.c.d.sid()]: true,
    [state.b.title.sid()]: true,
    [state.b.c.sid()]: true,
    [state.b.c.d.sid()]: true,
    [state.b.c.d.e.sid()]: true
  }
  state.emit('subscription', forceObj)
  t.same(arr, [
    [ 'b', 'title' ],
    [ 'b', 'c', 'd', 'e' ],
    [ 'b', 'title' ],
    [ 'b', 'c', 'd', 'e' ]
  ])
  t.end()
})
<|endoftext|>


<commit_before>import mixed from '../src/mixed';
import object from '../src/object';
import string from '../src/string';
import number from '../src/number';
import reach from '../src/util/reach';

let noop = () => {}

describe('Mixed Types ', () => {

  it('should be immutable', () => {
    let inst = mixed(), next;
    let sub = inst.sub = mixed()

    inst.should.not.equal(next = inst.required())

    next.sub.should.equal(sub)
    inst.sub.should.equal(next.sub)

    inst.should.be.an.instanceOf(mixed)
    next.should.be.an.instanceOf(mixed)

    return Promise.all([
      inst.isValid().should.eventually().equal(true),
      next.isValid(null)
    ])
  })

  it('cast should return a default when undefined', () => {
    let inst = mixed().default('hello')

    inst.cast(undefined).should.equal('hello')
  })

  it('should warn about null types', async () => {
    let error = await string().strict()
      .validate(null).should.be.rejected()

    expect(error.message).to.match(/If "null" is intended/)
  })

  it('should print the original value', async () => {
    let error = await number()
      .validate('john').should.be.rejected()

    expect(error.message).to.match(/the final value was: `NaN`.+cast from the value `"john"`/)
  })

  it('should check types', async () => {
    let inst = string().strict().typeError('must be a ${type}!')

    let error = await inst.validate(5).should.be.rejected()

    error.type.should.equal('typeError')
    error.message.should.equal('must be a string!')
    error.inner.length.should.equal(0)

    error = await inst.validate(5, { abortEarly: false }).should.be.rejected()

    expect(error.type).to.not.exist()
    error.message.should.equal('must be a string!')
    error.inner.length.should.equal(1)
  })

  it('should limit values', async () => {
    let inst = mixed().oneOf([5, 'hello'])

    await inst.isValid(5).should.eventually().equal(true)
    await inst.isValid('hello').should.eventually().equal(true)

    let err = await inst.validate(6).should.be.rejected()

    err.errors[0].should.equal('this must be one the following values: 5, hello')
  })

  it('should ignore absent values', () => {
    return Promise.all([
      mixed()
        .oneOf(['hello'])
        .isValid(undefined)
        .should.eventually().equal(true),
      mixed()
        .nullable()
        .oneOf(['hello'])
        .isValid(null)
        .should.eventually().equal(false),
      mixed()
        .oneOf(['hello'])
        .required()
        .isValid(undefined)
        .should.eventually().equal(false),
      mixed()
        .nullable()
        .oneOf(['hello'])
        .required()
        .isValid(null)
        .should.eventually().equal(false)
    ])
  })

  it('should exclude values', () => {
    let inst = mixed().notOneOf([5, 'hello'])

    return Promise.all([
      inst.isValid(6).should.eventually().equal(true),
      inst.isValid('hfhfh').should.eventually().equal(true),

      inst.isValid(5).should.eventually().equal(false),

      inst.validate(5).should.be.rejected().then(err => {
        err.errors[0].should.equal('this must not be one the following values: 5, hello')
      }),
      inst.oneOf([5]).isValid(5).should.eventually().equal(true),

      inst.isValid(null).should.eventually().equal(true),
      inst.required().isValid(null).should.eventually().equal(false)
    ])
  })

  it('should run subset of validations first', () => {
    let called = false;
    let inst = string()
      .strict()
      .test('test', 'boom', () => called = true)

    return inst.validate(25).should.be.rejected()
      .then(() => {
        called.should.equal(false)
      })
  })

  it('should respect strict', () => {
    let inst = string().equals(['hello', '5'])

    return Promise.all([
      inst.isValid(5).should.eventually().equal(true),
      inst.strict().isValid(5).should.eventually().equal(false)
    ])
  })

  it('should respect abortEarly', () => {
    let inst = string().trim().min(10)

    return Promise.all([

      inst.strict().validate(' hi ').should.be.rejected()
        .then(err => {
          err.errors.length.should.equal(1)
        }),

      inst.strict().validate(' hi ', { abortEarly: false }).should.be.rejected()
        .then(err => {
          err.errors.length.should.equal(2)
        })
    ])
  })

  it('should overload test()', () => {
    let inst = mixed().test('test', noop)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.test.should.equal(noop)
    inst.tests[0].TEST.message.should.equal('${path} is invalid')
  })

  it('should allow non string messages', async () => {
    let message = { key: 'foo' };
    let inst = mixed().test('test', message, () => false)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.message.should.equal(message)

    let error = await inst.validate('foo').should.be.rejected();

    error.message.should.equal(message)
  })

  it('should dedupe tests with the same test function', () => {
    let inst = mixed()
      .test('test', ' ', noop)
      .test('test', 'asdasd', noop)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.message.should.equal('asdasd')
  })

  it('should not dedupe tests with the same test function and different type', () => {
    let inst = mixed()
      .test('test', ' ', noop)
      .test('test-two', 'asdasd', noop)

    inst.tests.length.should.equal(2)
  })

  it('should respect exclusive validation', () => {
    let inst = mixed()
      .test({ message: 'invalid', exclusive: true, name: 'test', test: () => {} })
      .test({ message: 'also invalid', name: 'test', test: () => {} })

    inst.tests.length.should.equal(1)

    inst = mixed()
      .test({ message: 'invalid', name: 'test', test: () => {} })
      .test({ message: 'also invalid', name: 'test', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('should non-exclusive tests should stack', () => {
    let inst = mixed()
      .test({ name: 'test', message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('should replace existing tests, with exclusive test ', () => {
    let inst = mixed()
      .test({ name: 'test', message: ' ', test: noop })
      .test({ name: 'test', exclusive: true, message: ' ', test: noop })

    inst.tests.length.should.equal(1)
  })

  it('should replace existing exclusive tests, with non-exclusive', () => {
    let inst = mixed()
      .test({ name: 'test', exclusive: true, message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('exclusive tests should throw without a name', () => {
    (() => {
      mixed().test({ message: 'invalid', exclusive: true, test: noop })
    }).should.throw()
  })

  it('exclusive tests should replace previous ones', async () => {
    let inst = mixed().test({
      message: 'invalid',
      exclusive: true,
      name: 'max',
      test: v => v < 5
    });

    (await inst.isValid(8)).should.equal(false);

    (await inst
      .test({ message: 'invalid', exclusive: true, name: 'max', test: v => v < 10 })
      .isValid(8)
    ).should.equal(true)
  })

  it('tests should be called with the correct `this`', async () => {
    let called = false;
    let inst = object({
      other: mixed(),
      test: mixed().test({
        message: 'invalid',
        exclusive: true,
        name: 'max',
        test() {
          this.path.should.equal('test')
          this.parent.should.eql({ other: 5, test: 'hi' })
          this.options.context.should.eql({ user: 'jason' })
          called = true;
          return true;
        }
      })
    })

    await inst.validate({ other: 5, test: 'hi' }, { context: { user: 'jason' } })

    called.should.equal(true)
  })

  it('tests can return an error', () => {
    let inst = mixed().test({
        message: 'invalid ${path}',
        name: 'max',
        test() {
          return this.createError({ path: 'my.path' })
        }
      })

    return inst.validate('')
      .should.be.rejected()
      .then(function(e){
        e.path.should.equal('my.path')
        e.errors[0].should.equal('invalid my.path')
      })
  })

  it('should use returned error path and message', () => {
    let inst = mixed().test({
        message: 'invalid ${path}',
        name: 'max',
        test: function(){
          return this.createError({ message: '${path} nope!', path: 'my.path' })
        }
      })

    return inst.validate({ other: 5, test : 'hi' })
      .should.be.rejected()
      .then(function(e){
        e.path.should.equal('my.path')
        e.errors[0].should.equal('my.path nope!')
      })
  })

  it('should allow custom validation', async () => {
    let inst = string()
      .test('name', 'test a', val =>
        Promise.resolve(val === 'jim')
      )

    return inst.validate('joe').should.be.rejected().then(e => {
      e.errors[0].should.equal('test a')
    })
  })

  describe('concat', () => {
    let next
    let inst = object({
      str: string().required(),
      obj: object({
        str: string()
      })
    })

    beforeEach(() => {
      next = inst.concat(object({
        str: string().required().trim(),
        str2: string().required(),
        obj: object({
          str: string().required()
        })
      }))
    })

    it('should have the correct number of tests', () => {
      reach(next, 'str').tests.length.should.equal(3) // presence, alt presence, and trim
    })

    it('should have the tests in the correct order', () => {
      reach(next, 'str').tests[0].TEST_NAME.should.equal('required')
    })

    it('should validate correctly', async () => {
      await inst
        .isValid({ str: 'hi', str2: 'hi', obj: {} })
        .should.become(true)

      ;(await next
        .validate({ str: ' hi  ', str2: 'hi', obj: { str: 'hi' } })
        .should.be.fulfilled())
        .should.deep.eql({ str: 'hi', str2: 'hi', obj: {str: 'hi'} })
    })

    it('should throw the correct validation errors', async () => {

      let result = await next
        .validate({ str: 'hi', str2: 'hi', obj: {} })
        .should.be.rejected()

      result.message.should.contain('obj.str is a required field')

      result = await next
        .validate({ str2: 'hi', obj: { str: 'hi'} })
        .should.be.rejected()

      result.message.should.contain('str is a required field')
    })

  })

  it('concat should fail on different types', function(){
    let inst = string().default('hi');

    (function(){
      inst.concat(object())
    }).should.throw(TypeError)
  })

  it('concat should allow mixed and other type', function(){
    let inst = mixed().default('hi');

    (function(){
      inst.concat(string())._type.should.equal('string')

    }).should.not.throw(TypeError)
  })

  it('concat should maintain undefined defaults', function(){
    let inst = string().default('hi')

    expect(
      inst.concat(string().default(undefined)).default()).to.equal(undefined)
  })

  it('defaults should be validated but not transformed', function(){
    let inst = string().trim().default('  hi  ')

    return inst.validate(undefined).should.be.rejected()
      .then(function(err){
        err.message.should.equal('this must be a trimmed string')
      })
  })

  it('should handle conditionals', async function(){
    let inst = mixed()
      .when('prop', { is: 5, then: mixed().required('from parent') })

    await inst.validate(undefined, { parent: { prop: 5 }}).should.be.rejected()
    await inst.validate(undefined, { parent: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hello', { parent: { prop: 5 }}).should.be.fulfilled()

    inst = string().when('prop', {
      is:        function(val) { return val === 5 },
      then:      string().required(),
      otherwise: string().min(4)
    })

    await inst.validate(undefined, { parent: { prop: 5 }}).should.be.rejected()
    await inst.validate('hello', { parent: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hel', { parent: { prop: 1 }}).should.be.rejected()
  })

  it('should handle multiple conditionals', function() {
    let called = false
    let inst = mixed()
      .when(['prop', 'other'], function(prop, other) {
        other.should.equal(true)
        prop.should.equal(1)
        called = true
      })

    inst.cast({}, { context: { prop: 1, other: true }})
    called.should.equal(true)

    inst = mixed().when(['prop', 'other'], {
      is: 5,
      then: mixed().required()
    })

    return inst
      .isValid(undefined, { context: { prop: 5, other: 5 }})
      .should.eventually().equal(false)

  })

  it('should require context when needed', async function(){
    let inst = mixed()
      .when('$prop', { is: 5, then: mixed().required('from context') })

    await inst.validate(undefined, { context: { prop: 5 }}).should.be.rejected()
    await inst.validate(undefined, { context: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hello',   { context: { prop: 5 }}).should.be.fulfilled()

    inst = string().when('$prop', {
      is:        function(val) { return val === 5 },
      then:      string().required(),
      otherwise: string().min(4)
    })

    await inst.validate(undefined, { context: { prop: 5 }}).should.be.rejected()
    await inst.validate('hello', { context: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hel', { context: { prop: 1 }}).should.be.rejected()
  })

  it('should not use context refs in object calculations', function(){
    let inst = object({
      prop: string().when('$prop', { is: 5, then: string().required('from context') })
    })

    inst.default().should.eql({ prop: undefined })
  })

  it('should use label in error message', async function () {
    let label = 'Label'
    let inst = object({
        prop: string().required().label(label)
    })

    await inst.validate({}).should.be.rejected().then(function (err) {
      err.message.should.equal(`${label} is a required field`)
    })
  })

  it('should add meta() data', () => {
    string()
      .meta({ input: 'foo' })
      .meta({ foo: 'bar' })
      .meta().should.eql({
        input: 'foo',
        foo: 'bar'
      })
  })

  it('should describe', () => {
    string().max(2)
      .meta({ input: 'foo' })
      .label('str!')
      .describe().should.eql({
        type: 'string',
        label: 'str!',
        tests: ['max'],
        meta: {
          input: 'foo'
        }
      })
  })

})
<commit_msg>fix test<commit_after>import mixed from '../src/mixed';
import object from '../src/object';
import string from '../src/string';
import number from '../src/number';
import reach from '../src/util/reach';

let noop = () => {}

describe('Mixed Types ', () => {

  it('should be immutable', () => {
    let inst = mixed(), next;
    let sub = inst.sub = mixed()

    inst.should.not.equal(next = inst.required())

    next.sub.should.equal(sub)
    inst.sub.should.equal(next.sub)

    inst.should.be.an.instanceOf(mixed)
    next.should.be.an.instanceOf(mixed)

    return Promise.all([
      inst.isValid().should.eventually().equal(true),
      next.isValid(null)
    ])
  })

  it('cast should return a default when undefined', () => {
    let inst = mixed().default('hello')

    inst.cast(undefined).should.equal('hello')
  })

  it('should warn about null types', async () => {
    let error = await string().strict()
      .validate(null).should.be.rejected()

    expect(error.message).to.match(/If "null" is intended/)
  })

  it('should print the original value', async () => {
    let error = await number()
      .validate('john').should.be.rejected()

    expect(error.message).to.match(/the final value was: `NaN`.+cast from the value `"john"`/)
  })

  it('should check types', async () => {
    let inst = string().strict().typeError('must be a ${type}!')

    let error = await inst.validate(5).should.be.rejected()

    error.type.should.equal('typeError')
    error.message.should.equal('must be a string!')
    error.inner.length.should.equal(0)

    error = await inst.validate(5, { abortEarly: false }).should.be.rejected()

    expect(error.type).to.not.exist()
    error.message.should.equal('must be a string!')
    error.inner.length.should.equal(1)
  })

  it('should limit values', async () => {
    let inst = mixed().oneOf([5, 'hello'])

    await inst.isValid(5).should.eventually().equal(true)
    await inst.isValid('hello').should.eventually().equal(true)

    let err = await inst.validate(6).should.be.rejected()

    err.errors[0].should.equal('this must be one of the following values: 5, hello')
  })

  it('should ignore absent values', () => {
    return Promise.all([
      mixed()
        .oneOf(['hello'])
        .isValid(undefined)
        .should.eventually().equal(true),
      mixed()
        .nullable()
        .oneOf(['hello'])
        .isValid(null)
        .should.eventually().equal(false),
      mixed()
        .oneOf(['hello'])
        .required()
        .isValid(undefined)
        .should.eventually().equal(false),
      mixed()
        .nullable()
        .oneOf(['hello'])
        .required()
        .isValid(null)
        .should.eventually().equal(false)
    ])
  })

  it('should exclude values', () => {
    let inst = mixed().notOneOf([5, 'hello'])

    return Promise.all([
      inst.isValid(6).should.eventually().equal(true),
      inst.isValid('hfhfh').should.eventually().equal(true),

      inst.isValid(5).should.eventually().equal(false),

      inst.validate(5).should.be.rejected().then(err => {
        err.errors[0].should.equal('this must not be one of the following values: 5, hello')
      }),
      inst.oneOf([5]).isValid(5).should.eventually().equal(true),

      inst.isValid(null).should.eventually().equal(true),
      inst.required().isValid(null).should.eventually().equal(false)
    ])
  })

  it('should run subset of validations first', () => {
    let called = false;
    let inst = string()
      .strict()
      .test('test', 'boom', () => called = true)

    return inst.validate(25).should.be.rejected()
      .then(() => {
        called.should.equal(false)
      })
  })

  it('should respect strict', () => {
    let inst = string().equals(['hello', '5'])

    return Promise.all([
      inst.isValid(5).should.eventually().equal(true),
      inst.strict().isValid(5).should.eventually().equal(false)
    ])
  })

  it('should respect abortEarly', () => {
    let inst = string().trim().min(10)

    return Promise.all([

      inst.strict().validate(' hi ').should.be.rejected()
        .then(err => {
          err.errors.length.should.equal(1)
        }),

      inst.strict().validate(' hi ', { abortEarly: false }).should.be.rejected()
        .then(err => {
          err.errors.length.should.equal(2)
        })
    ])
  })

  it('should overload test()', () => {
    let inst = mixed().test('test', noop)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.test.should.equal(noop)
    inst.tests[0].TEST.message.should.equal('${path} is invalid')
  })

  it('should allow non string messages', async () => {
    let message = { key: 'foo' };
    let inst = mixed().test('test', message, () => false)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.message.should.equal(message)

    let error = await inst.validate('foo').should.be.rejected();

    error.message.should.equal(message)
  })

  it('should dedupe tests with the same test function', () => {
    let inst = mixed()
      .test('test', ' ', noop)
      .test('test', 'asdasd', noop)

    inst.tests.length.should.equal(1)
    inst.tests[0].TEST.message.should.equal('asdasd')
  })

  it('should not dedupe tests with the same test function and different type', () => {
    let inst = mixed()
      .test('test', ' ', noop)
      .test('test-two', 'asdasd', noop)

    inst.tests.length.should.equal(2)
  })

  it('should respect exclusive validation', () => {
    let inst = mixed()
      .test({ message: 'invalid', exclusive: true, name: 'test', test: () => {} })
      .test({ message: 'also invalid', name: 'test', test: () => {} })

    inst.tests.length.should.equal(1)

    inst = mixed()
      .test({ message: 'invalid', name: 'test', test: () => {} })
      .test({ message: 'also invalid', name: 'test', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('should non-exclusive tests should stack', () => {
    let inst = mixed()
      .test({ name: 'test', message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('should replace existing tests, with exclusive test ', () => {
    let inst = mixed()
      .test({ name: 'test', message: ' ', test: noop })
      .test({ name: 'test', exclusive: true, message: ' ', test: noop })

    inst.tests.length.should.equal(1)
  })

  it('should replace existing exclusive tests, with non-exclusive', () => {
    let inst = mixed()
      .test({ name: 'test', exclusive: true, message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })
      .test({ name: 'test', message: ' ', test: () => {} })

    inst.tests.length.should.equal(2)
  })

  it('exclusive tests should throw without a name', () => {
    (() => {
      mixed().test({ message: 'invalid', exclusive: true, test: noop })
    }).should.throw()
  })

  it('exclusive tests should replace previous ones', async () => {
    let inst = mixed().test({
      message: 'invalid',
      exclusive: true,
      name: 'max',
      test: v => v < 5
    });

    (await inst.isValid(8)).should.equal(false);

    (await inst
      .test({ message: 'invalid', exclusive: true, name: 'max', test: v => v < 10 })
      .isValid(8)
    ).should.equal(true)
  })

  it('tests should be called with the correct `this`', async () => {
    let called = false;
    let inst = object({
      other: mixed(),
      test: mixed().test({
        message: 'invalid',
        exclusive: true,
        name: 'max',
        test() {
          this.path.should.equal('test')
          this.parent.should.eql({ other: 5, test: 'hi' })
          this.options.context.should.eql({ user: 'jason' })
          called = true;
          return true;
        }
      })
    })

    await inst.validate({ other: 5, test: 'hi' }, { context: { user: 'jason' } })

    called.should.equal(true)
  })

  it('tests can return an error', () => {
    let inst = mixed().test({
        message: 'invalid ${path}',
        name: 'max',
        test() {
          return this.createError({ path: 'my.path' })
        }
      })

    return inst.validate('')
      .should.be.rejected()
      .then(function(e){
        e.path.should.equal('my.path')
        e.errors[0].should.equal('invalid my.path')
      })
  })

  it('should use returned error path and message', () => {
    let inst = mixed().test({
        message: 'invalid ${path}',
        name: 'max',
        test: function(){
          return this.createError({ message: '${path} nope!', path: 'my.path' })
        }
      })

    return inst.validate({ other: 5, test : 'hi' })
      .should.be.rejected()
      .then(function(e){
        e.path.should.equal('my.path')
        e.errors[0].should.equal('my.path nope!')
      })
  })

  it('should allow custom validation', async () => {
    let inst = string()
      .test('name', 'test a', val =>
        Promise.resolve(val === 'jim')
      )

    return inst.validate('joe').should.be.rejected().then(e => {
      e.errors[0].should.equal('test a')
    })
  })

  describe('concat', () => {
    let next
    let inst = object({
      str: string().required(),
      obj: object({
        str: string()
      })
    })

    beforeEach(() => {
      next = inst.concat(object({
        str: string().required().trim(),
        str2: string().required(),
        obj: object({
          str: string().required()
        })
      }))
    })

    it('should have the correct number of tests', () => {
      reach(next, 'str').tests.length.should.equal(3) // presence, alt presence, and trim
    })

    it('should have the tests in the correct order', () => {
      reach(next, 'str').tests[0].TEST_NAME.should.equal('required')
    })

    it('should validate correctly', async () => {
      await inst
        .isValid({ str: 'hi', str2: 'hi', obj: {} })
        .should.become(true)

      ;(await next
        .validate({ str: ' hi  ', str2: 'hi', obj: { str: 'hi' } })
        .should.be.fulfilled())
        .should.deep.eql({ str: 'hi', str2: 'hi', obj: {str: 'hi'} })
    })

    it('should throw the correct validation errors', async () => {

      let result = await next
        .validate({ str: 'hi', str2: 'hi', obj: {} })
        .should.be.rejected()

      result.message.should.contain('obj.str is a required field')

      result = await next
        .validate({ str2: 'hi', obj: { str: 'hi'} })
        .should.be.rejected()

      result.message.should.contain('str is a required field')
    })

  })

  it('concat should fail on different types', function(){
    let inst = string().default('hi');

    (function(){
      inst.concat(object())
    }).should.throw(TypeError)
  })

  it('concat should allow mixed and other type', function(){
    let inst = mixed().default('hi');

    (function(){
      inst.concat(string())._type.should.equal('string')

    }).should.not.throw(TypeError)
  })

  it('concat should maintain undefined defaults', function(){
    let inst = string().default('hi')

    expect(
      inst.concat(string().default(undefined)).default()).to.equal(undefined)
  })

  it('defaults should be validated but not transformed', function(){
    let inst = string().trim().default('  hi  ')

    return inst.validate(undefined).should.be.rejected()
      .then(function(err){
        err.message.should.equal('this must be a trimmed string')
      })
  })

  it('should handle conditionals', async function(){
    let inst = mixed()
      .when('prop', { is: 5, then: mixed().required('from parent') })

    await inst.validate(undefined, { parent: { prop: 5 }}).should.be.rejected()
    await inst.validate(undefined, { parent: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hello', { parent: { prop: 5 }}).should.be.fulfilled()

    inst = string().when('prop', {
      is:        function(val) { return val === 5 },
      then:      string().required(),
      otherwise: string().min(4)
    })

    await inst.validate(undefined, { parent: { prop: 5 }}).should.be.rejected()
    await inst.validate('hello', { parent: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hel', { parent: { prop: 1 }}).should.be.rejected()
  })

  it('should handle multiple conditionals', function() {
    let called = false
    let inst = mixed()
      .when(['prop', 'other'], function(prop, other) {
        other.should.equal(true)
        prop.should.equal(1)
        called = true
      })

    inst.cast({}, { context: { prop: 1, other: true }})
    called.should.equal(true)

    inst = mixed().when(['prop', 'other'], {
      is: 5,
      then: mixed().required()
    })

    return inst
      .isValid(undefined, { context: { prop: 5, other: 5 }})
      .should.eventually().equal(false)

  })

  it('should require context when needed', async function(){
    let inst = mixed()
      .when('$prop', { is: 5, then: mixed().required('from context') })

    await inst.validate(undefined, { context: { prop: 5 }}).should.be.rejected()
    await inst.validate(undefined, { context: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hello',   { context: { prop: 5 }}).should.be.fulfilled()

    inst = string().when('$prop', {
      is:        function(val) { return val === 5 },
      then:      string().required(),
      otherwise: string().min(4)
    })

    await inst.validate(undefined, { context: { prop: 5 }}).should.be.rejected()
    await inst.validate('hello', { context: { prop: 1 }}).should.be.fulfilled()
    await inst.validate('hel', { context: { prop: 1 }}).should.be.rejected()
  })

  it('should not use context refs in object calculations', function(){
    let inst = object({
      prop: string().when('$prop', { is: 5, then: string().required('from context') })
    })

    inst.default().should.eql({ prop: undefined })
  })

  it('should use label in error message', async function () {
    let label = 'Label'
    let inst = object({
        prop: string().required().label(label)
    })

    await inst.validate({}).should.be.rejected().then(function (err) {
      err.message.should.equal(`${label} is a required field`)
    })
  })

  it('should add meta() data', () => {
    string()
      .meta({ input: 'foo' })
      .meta({ foo: 'bar' })
      .meta().should.eql({
        input: 'foo',
        foo: 'bar'
      })
  })

  it('should describe', () => {
    string().max(2)
      .meta({ input: 'foo' })
      .label('str!')
      .describe().should.eql({
        type: 'string',
        label: 'str!',
        tests: ['max'],
        meta: {
          input: 'foo'
        }
      })
  })

})
<|endoftext|>


<commit_before>// @flow
import test from 'ava';
import sinon from 'sinon';
import Image from '../src/domain/image/Image';
import Images from '../src/domain/image/Images';
import ImageRepository from '../src/domain/image/ImageRepository';
import Storage from '../src/domain/storage/Storage';

test('create image instance', (t) => {
  const image = new Image('http://example.com/1.png');
  t.is(image.src, 'http://example.com/1.png');
});

test('image src must be validate', (t) => {
  t.throws(() => new Image('not url'));
});

test('create images instance', (t) => {
  const images = new Images('');
  images.add(new Image('http://example.com/1.png'));
  images.add(new Image('http://example.com/2.png'));
  images.add(new Image('http://example.com/3.png'));
  images.add(new Image('http://example.com/4.png'));
  images.add(new Image('http://example.com/5.png'));
  t.is(images.images.length, 5);
  t.is(images.images[0].src, 'http://example.com/1.png');
});

test('create images with url text', (t) => {
  const images = new Images(`
http://example.com/1.png
http://example.com/2.png
not html line
http://example.com/3.png
`);
  t.is(images.images.length, 3);
  t.is(images.images[0].src, 'http://example.com/1.png');
});

test('check images are available', (t) => {
  const images = new Images('');
  t.true(images.isEmpty());
});

test('get random image from images', (t) => {
  const images = new Images('');
  images.add(new Image('http://example.com/1.png'));
  images.add(new Image('http://example.com/2.png'));
  images.add(new Image('http://example.com/3.png'));
  images.add(new Image('http://example.com/4.png'));
  images.add(new Image('http://example.com/5.png'));

  const img = images.getRandom();
  const result = /http:\/\/example\.com\/(\d)\.png/.exec(img.src);
  t.not(result, null);
  const resultFileNumber = parseInt(result[1], 10);
  t.true(resultFileNumber > 0 && resultFileNumber <= 5);
});

const mockStorage: Storage = {
  fetch: () => {
  },
  save: () => {
  },
};

test('fetch from image repository', (t) => {
  const spy = sinon.spy(mockStorage, 'fetch');
  const args = () => {
  };
  const repo = new ImageRepository(mockStorage);
  repo.fetch(args).subscribe();
  t.true(spy.calledOnce);
});

test('store from image repository', (t) => {
  const spy = sinon.spy(mockStorage, 'save');
  const args = new Images('');
  const repo = new ImageRepository(mockStorage);
  repo.store(args).subscribe();
  t.true(spy.calledOnce);
});
<commit_msg>Fix test<commit_after>// @flow
import test from 'ava';
import sinon from 'sinon';
import Image from '../src/domain/image/Image';
import Images from '../src/domain/image/Images';
import ImageRepository from '../src/domain/image/ImageRepository';
import Storage from '../src/domain/storage/Storage';

test('create image instance', (t) => {
  const image = new Image('http://example.com/1.png');
  t.is(image.src, 'http://example.com/1.png');
});

test('image src must be validate', (t) => {
  t.throws(() => new Image('not url'));
});

test('create images instance', (t) => {
  const images = new Images('');
  images.add(new Image('http://example.com/1.png'));
  images.add(new Image('http://example.com/2.png'));
  images.add(new Image('http://example.com/3.png'));
  images.add(new Image('http://example.com/4.png'));
  images.add(new Image('http://example.com/5.png'));
  t.is(images.images.length, 5);
  t.is(images.images[0].src, 'http://example.com/1.png');
});

test('create images with url text', (t) => {
  const images = new Images(`
http://example.com/1.png
http://example.com/2.png
not html line
http://example.com/3.png
`);
  t.is(images.images.length, 3);
  t.is(images.images[0].src, 'http://example.com/1.png');
});

test('check images are available', (t) => {
  const images = new Images('');
  t.true(images.isEmpty());
});

test('get random image from images', (t) => {
  const images = new Images('');
  images.add(new Image('http://example.com/1.png'));
  images.add(new Image('http://example.com/2.png'));
  images.add(new Image('http://example.com/3.png'));
  images.add(new Image('http://example.com/4.png'));
  images.add(new Image('http://example.com/5.png'));

  const img = images.getRandom();
  const result = /http:\/\/example\.com\/(\d)\.png/.exec(img.src);
  t.not(result, null);
  const resultFileNumber = parseInt(result[1], 10);
  t.true(resultFileNumber > 0 && resultFileNumber <= 5);
});

const mockStorage: Storage = {
  fetch: () => {
  },
  store: () => {
  },
};

test('fetch from image repository', (t) => {
  const spy = sinon.spy(mockStorage, 'fetch');
  const args = () => {
  };
  const repo = new ImageRepository(mockStorage);
  repo.fetch(args).subscribe();
  t.true(spy.calledOnce);
});

test('store from image repository', (t) => {
  const spy = sinon.spy(mockStorage, 'store');
  const args = new Images('');
  const repo = new ImageRepository(mockStorage);
  repo.store(args).subscribe();
  t.true(spy.calledOnce);
});
<|endoftext|>


<commit_before>"use strict";

var _ = require("underscore");
var util = require("substance-util");
var Operator = require('substance-operator');
var Selection = require("./selection");
var Annotator = require("./annotator");
var Clipboard = require("./clipboard");
var Transformer = require('./transformer');

// Document.Writer
// -----------------
//
// Provides means for editing a Substance.Document. It introduces a Selection API
// in order to move a cursor through the document, support copy and paste, etc.
//
// Note: it is quite intentional not to expose the full Substance.Document interface
//       to force us to explicitely take care of model adaptations.
//
// Example usage:
//
//     var doc = new Substance.Document();
//     var editor = new Substance.Document.Writer(doc);
//     var editor.insert("Hello World");

var Writer = function(document, options) {
  options = options || {};
  this.view = options.view || 'content';

  this.__document = document;

  this.chronicle = document.chronicle;
  this.annotator = new Annotator(document);

  // Document.Transformer
  // Contains higher level operations to transform (change) a document
  this.transformer = new Transformer(document);

  this.selection = new Selection(this.__document, null);
  this.clipboard = new Clipboard();

  this.listenTo(document, 'operation:applied', this.updateSelection);
};

Writer.Prototype = function() {

  // Document Facette
  // --------

  this.getNodes = function(idsOnly) {
    if (idsOnly) return this.__document.get([this.view, "nodes"]);
    else return this.__document.query([this.view, "nodes"]);
  };

  // Given a node id, get position in the document
  // --------
  //

  this.getPosition = function(id) {
    return this.__document.getPosition(this.view, id);
  };

  this.getNodeFromPosition = function(nodePos) {
    return this.__document.getNodeFromPosition(this.view, nodePos);
  };

  // See Annotator
  // --------
  //

  this.getAnnotations = function(options) {
    options = options || {};
    options.view = this.view;
    return this.annotator.getAnnotations(options);
  };

  var _delete = function(doc, direction) {

    var sel = new Selection(doc, this.selection);
    var transformer = this.transformer;
    var view = this.view;

    // Remove character (backspace behavior)
    // --------
    //

    function removeChar(direction) {
      sel.expand(direction, 'char');
      transformer.deleteSelection(doc, sel);
      sel.collapse("left");
    }

    // Attempt merge
    // --------
    //

    function attemptMerge(direction, select) {
      var node = sel.getRanges()[0].node;
      var sourceNode;
      var targetNode;
      var insertionPos;

      if (direction === "left") {
        sourceNode = node;
        targetNode = sel.getPredecessor();
        if (!targetNode) return;
        insertionPos = targetNode.content.length;
      } else {
        sourceNode = sel.getSuccessor();
        if (!sourceNode) return;
        targetNode = node;
      }

      var merged = transformer.mergeNodes(doc, sourceNode, targetNode);

      if (merged) {
        // Consider this API instead?
        // sel.setCursor([targetNode.id, insertionPos]);
        if (direction === "left") {
          sel.setCursor([doc.getPosition(view, targetNode.id), insertionPos]);
        }
      } else if(select) {
        sel.selectNode(targetNode.id);
      }
    }

    // Regular deletion
    // --------
    //

    function deleteSelection() {
      transformer.deleteSelection(doc, sel);
      sel.collapse("left");
    }

    if (sel.isCollapsed()) {
      var cursor = sel.cursor;
      if (cursor.isLeftBound() && direction === "left") {
        attemptMerge('left', true);
      } else if (cursor.isRightBound() && direction =="right") {
        attemptMerge('right', true);
      } else {
        removeChar(direction);
      }
    } else {
      var shouldMerge = sel.hasMultipleNodes();
      deleteSelection(direction);
      if (shouldMerge) attemptMerge("right", false);
    }

    this.selection.set(sel);
  };

  // Delete current selection
  // --------
  //

  this.delete = function(direction) {
    var doc = this.startSimulation();

    _delete.call(this, doc, direction);

    // commit changes
    doc.save();
  };

  // Copy current selection
  // --------
  //

  this.copy = function() {
    // Delegate
    var content = this.transformer.copy(this.__document, this.selection);
    this.clipboard.setContent(content);
  };

  // Cut current selection from document
  // --------
  //
  // Returns cutted content as a new Substance.Document

  this.cut = function() {
    this.copy();
    this.delete();
  };

  // Paste content from clipboard at current position
  // --------
  //

  this.paste = function() {
    var doc = this.startSimulation();

    if (!this.selection.isCollapsed()) {
      _delete.call(this, doc);
    }

    var sel = new Selection(doc, this.selection);
    this.transformer.paste(doc, this.clipboard.getContent(), sel);

    doc.save();
  };

  // Split
  // --------
  //

  this.modifyNode = function(type, data) {

    var doc = this.startSimulation();

    if (!this.selection.isCollapsed()) {
      _delete.call(this, doc);
    }

    var sel = new Selection(doc, this.selection);
    var cursor = sel.cursor;

    if (cursor.node.type === "constructor") {
      var charPos = cursor.charPos;
      var targetType = cursor.node.content[charPos].type;

      console.log('targetType', targetType);
      if (targetType) {
        this.transformer.morphNode(doc, sel, targetType, data);
      }
    } else {

      this.transformer.insertNode(doc, sel, type, data);
    }

    // Commit
    doc.save();
    this.selection.set(sel);
  };

  // Based on current selection, insert new node
  // --------
  //

  this.insertNode = function(type, data) {
    var doc = this.startSimulation();
    var sel = new Selection(doc, this.selection);

    // Remove selected text and get a cursor
    if (!sel.isCollapsed()) this.transformer.deleteSelection(doc, sel);

    this.transformer.insertNode(doc, sel, type, data);

    // Commit
    doc.save();
    this.selection.set(sel);
  };

  // Creates an annotation based on the current position
  // --------
  //

  this.annotate = function(type) {
    return this.annotator.annotate(this.selection, type);
  };

  this.startSimulation = function() {
    var doc = this.__document.startSimulation();
    new Annotator(doc, {withTransformation: true});
    return doc;
  };

  // Inserts text at the current position
  // --------
  //

  this.write = function(text) {
    if (this.selection.isNull()) {
      console.log("Can not write, as no position has been selected.");
      return;
    }

    if (!this.selection.isCollapsed()) {
      this.delete();
    }

    var node = this.selection.getNodes()[0];
    var nodePos = this.selection.start[0];
    var charPos = this.selection.start[1];

    // TODO: future. This only works for text nodes....
    var doc = this.startSimulation();
    doc.update([node.id, this.view], [charPos, text]);
    doc.save();
  };

  // Delegate getter
  this.get = function() {
    return this.__document.get.apply(this.__document, arguments);
  };

  this.undo = function() {
    this.chronicle.rewind();
  };

  this.redo = function() {
    this.chronicle.forward();
  };

  this.updateSelection = function(op) {

    if (op.type === "update" || op.type === "set") {
      var nodePos = -1;
      var charPos = -1;

      // handle Show/Hide of nodes
      if (op.path[0] === this.view && op.path[1] === "nodes") {
        var lastChange = Operator.Helpers.last(op.diff);
        if (lastChange.isMove()) {
          nodePos = lastChange.target;
        } else {
          nodePos = lastChange.pos;
        }
        charPos = 0;
      }

      // delegate node updates to the Node implementation
      else {
        var node = this.__document.get(op.path[0]);
        nodePos = this.getPosition(node.id);
        if (node.getUpdatedCharPos !== undefined) {
          charPos = node.getUpdatedCharPos(op);
        }
      }

      if (nodePos >= 0 && charPos >= 0) {
        this.selection.set([nodePos, charPos]);
      }
    }
  };
};

// Inherit the prototype of Substance.Document which extends util.Events
Writer.prototype = _.extend(new Writer.Prototype(), util.Events.Listener);

// Property accessors for convenient access of primary properties
Object.defineProperties(Writer.prototype, {
  id: {
    get: function() {
      return this.__document.id;
    },
    set: function() { throw "immutable property"; }
  },
  title: {
    get: function() {
      return this.__document.get('document').title;
    },
    set: function() { throw "immutable property"; }
  },
  updated_at: {
    get: function() {
      return this.__document.get('document').updated_at;
    },
    set: function() { throw "immutable property"; }
  },
  creator: {
    get: function() {
      return this.__document.get('document').creator;
    },
    set: function() { throw "immutable property"; }
  }
});

module.exports = Writer;
<commit_msg>Fix regression: now it is important to set cursor after doc.save().<commit_after>"use strict";

var _ = require("underscore");
var util = require("substance-util");
var Operator = require('substance-operator');
var Selection = require("./selection");
var Annotator = require("./annotator");
var Clipboard = require("./clipboard");
var Transformer = require('./transformer');

// Document.Writer
// -----------------
//
// Provides means for editing a Substance.Document. It introduces a Selection API
// in order to move a cursor through the document, support copy and paste, etc.
//
// Note: it is quite intentional not to expose the full Substance.Document interface
//       to force us to explicitely take care of model adaptations.
//
// Example usage:
//
//     var doc = new Substance.Document();
//     var editor = new Substance.Document.Writer(doc);
//     var editor.insert("Hello World");

var Writer = function(document, options) {
  options = options || {};
  this.view = options.view || 'content';

  this.__document = document;

  this.chronicle = document.chronicle;
  this.annotator = new Annotator(document);

  // Document.Transformer
  // Contains higher level operations to transform (change) a document
  this.transformer = new Transformer(document);

  this.selection = new Selection(this.__document, null);
  this.clipboard = new Clipboard();

  this.listenTo(document, 'operation:applied', this.updateSelection);
};

Writer.Prototype = function() {

  // Document Facette
  // --------

  this.getNodes = function(idsOnly) {
    if (idsOnly) return this.__document.get([this.view, "nodes"]);
    else return this.__document.query([this.view, "nodes"]);
  };

  // Given a node id, get position in the document
  // --------
  //

  this.getPosition = function(id) {
    return this.__document.getPosition(this.view, id);
  };

  this.getNodeFromPosition = function(nodePos) {
    return this.__document.getNodeFromPosition(this.view, nodePos);
  };

  // See Annotator
  // --------
  //

  this.getAnnotations = function(options) {
    options = options || {};
    options.view = this.view;
    return this.annotator.getAnnotations(options);
  };

  var _delete = function(doc, direction) {

    var sel = new Selection(doc, this.selection);
    var transformer = this.transformer;
    var view = this.view;

    // Remove character (backspace behavior)
    // --------
    //

    function removeChar(direction) {
      sel.expand(direction, 'char');
      transformer.deleteSelection(doc, sel);
    }

    // Attempt merge
    // --------
    //

    function attemptMerge(direction, select) {
      var node = sel.getRanges()[0].node;
      var sourceNode;
      var targetNode;
      var insertionPos;

      if (direction === "left") {
        sourceNode = node;
        targetNode = sel.getPredecessor();
        if (!targetNode) return;
        insertionPos = targetNode.content.length;
      } else {
        sourceNode = sel.getSuccessor();
        if (!sourceNode) return;
        targetNode = node;
      }

      var merged = transformer.mergeNodes(doc, sourceNode, targetNode);

      if (merged) {
        // Consider this API instead?
        // sel.setCursor([targetNode.id, insertionPos]);
        if (direction === "left") {
          sel.set([doc.getPosition(view, targetNode.id), insertionPos]);
        }
      } else if(select) {
        sel.selectNode(targetNode.id);
      }
    }

    // Regular deletion
    // --------
    //

    function deleteSelection() {
      transformer.deleteSelection(doc, sel);
      sel.collapse("left");
    }

    if (sel.isCollapsed()) {
      var cursor = sel.cursor;
      if (cursor.isLeftBound() && direction === "left") {
        attemptMerge('left', true);
      } else if (cursor.isRightBound() && direction =="right") {
        attemptMerge('right', true);
      } else {
        removeChar(direction);
      }
    } else {
      var shouldMerge = sel.hasMultipleNodes();
      deleteSelection(direction);
      if (shouldMerge) attemptMerge("right", false);
    }

    return sel;
  };

  // Delete current selection
  // --------
  //

  this.delete = function(direction) {
    var doc = this.startSimulation();

    var sel = _delete.call(this, doc, direction);

    // commit changes
    doc.save();

    // important to set this at last, as doc.save() will trigger implicit selection
    // changes
    this.selection.set(sel);
  };

  // Copy current selection
  // --------
  //

  this.copy = function() {
    // Delegate
    var content = this.transformer.copy(this.__document, this.selection);
    this.clipboard.setContent(content);
  };

  // Cut current selection from document
  // --------
  //
  // Returns cutted content as a new Substance.Document

  this.cut = function() {
    this.copy();
    this.delete();
  };

  // Paste content from clipboard at current position
  // --------
  //

  this.paste = function() {
    var doc = this.startSimulation();

    var sel;

    if (!this.selection.isCollapsed()) {
      sel = _delete.call(this, doc);
      this.selection.set(sel);
    }

    sel = sel || new Selection(doc, this.selection);
    this.transformer.paste(doc, this.clipboard.getContent(), sel);

    doc.save();
  };

  // Split
  // --------
  //

  this.modifyNode = function(type, data) {

    var doc = this.startSimulation();

    if (!this.selection.isCollapsed()) {
      _delete.call(this, doc);
    }

    var sel = new Selection(doc, this.selection);
    var cursor = sel.cursor;

    if (cursor.node.type === "constructor") {
      var charPos = cursor.charPos;
      var targetType = cursor.node.content[charPos].type;

      console.log('targetType', targetType);
      if (targetType) {
        this.transformer.morphNode(doc, sel, targetType, data);
      }
    } else {

      this.transformer.insertNode(doc, sel, type, data);
    }

    // Commit
    doc.save();
    this.selection.set(sel);
  };

  // Based on current selection, insert new node
  // --------
  //

  this.insertNode = function(type, data) {
    var doc = this.startSimulation();
    var sel = new Selection(doc, this.selection);

    // Remove selected text and get a cursor
    if (!sel.isCollapsed()) this.transformer.deleteSelection(doc, sel);

    this.transformer.insertNode(doc, sel, type, data);

    // Commit
    doc.save();
    this.selection.set(sel);
  };

  // Creates an annotation based on the current position
  // --------
  //

  this.annotate = function(type) {
    return this.annotator.annotate(this.selection, type);
  };

  this.startSimulation = function() {
    var doc = this.__document.startSimulation();
    new Annotator(doc, {withTransformation: true});
    return doc;
  };

  // Inserts text at the current position
  // --------
  //

  this.write = function(text) {
    if (this.selection.isNull()) {
      console.log("Can not write, as no position has been selected.");
      return;
    }

    if (!this.selection.isCollapsed()) {
      this.delete();
    }

    var node = this.selection.getNodes()[0];
    var nodePos = this.selection.start[0];
    var charPos = this.selection.start[1];

    // TODO: future. This only works for text nodes....
    var doc = this.startSimulation();
    doc.update([node.id, this.view], [charPos, text]);
    doc.save();
  };

  // Delegate getter
  this.get = function() {
    return this.__document.get.apply(this.__document, arguments);
  };

  this.undo = function() {
    this.chronicle.rewind();
  };

  this.redo = function() {
    this.chronicle.forward();
  };

  this.updateSelection = function(op) {

    if (op.type === "update" || op.type === "set") {
      var nodePos = -1;
      var charPos = -1;

      // handle Show/Hide of nodes
      if (op.path[0] === this.view && op.path[1] === "nodes") {
        var lastChange = Operator.Helpers.last(op.diff);
        if (lastChange.isMove()) {
          nodePos = lastChange.target;
        } else {
          nodePos = lastChange.pos;
        }
        charPos = 0;
      }

      // delegate node updates to the Node implementation
      else {
        var node = this.__document.get(op.path[0]);
        nodePos = this.getPosition(node.id);
        if (node.getUpdatedCharPos !== undefined) {
          charPos = node.getUpdatedCharPos(op);
        }
      }

      if (nodePos >= 0 && charPos >= 0) {
        this.selection.set([nodePos, charPos]);
      }
    }
  };
};

// Inherit the prototype of Substance.Document which extends util.Events
Writer.prototype = _.extend(new Writer.Prototype(), util.Events.Listener);

// Property accessors for convenient access of primary properties
Object.defineProperties(Writer.prototype, {
  id: {
    get: function() {
      return this.__document.id;
    },
    set: function() { throw "immutable property"; }
  },
  title: {
    get: function() {
      return this.__document.get('document').title;
    },
    set: function() { throw "immutable property"; }
  },
  updated_at: {
    get: function() {
      return this.__document.get('document').updated_at;
    },
    set: function() { throw "immutable property"; }
  },
  creator: {
    get: function() {
      return this.__document.get('document').creator;
    },
    set: function() { throw "immutable property"; }
  }
});

module.exports = Writer;
<|endoftext|>