diff --git a/src/components/colorscale/is_valid_scale_array.js b/src/components/colorscale/is_valid_scale_array.js index 5c7bed6584e..95b0a625ef7 100644 --- a/src/components/colorscale/is_valid_scale_array.js +++ b/src/components/colorscale/is_valid_scale_array.js @@ -13,21 +13,23 @@ var tinycolor = require('tinycolor2'); module.exports = function isValidScaleArray(scl) { - var isValid = true, - highestVal = 0, - si; - - if(!Array.isArray(scl)) return false; - else { - if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false; - for(var i = 0; i < scl.length; i++) { - si = scl[i]; - if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) { - isValid = false; - break; - } - highestVal = +si[0]; + var highestVal = 0; + + if(!Array.isArray(scl) || scl.length < 2) return false; + + if(!scl[0] || !scl[scl.length - 1]) return false; + + if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false; + + for(var i = 0; i < scl.length; i++) { + var si = scl[i]; + + if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) { + return false; } - return isValid; + + highestVal = +si[0]; } + + return true; }; diff --git a/src/core.js b/src/core.js index bae6c1accad..13502eec6ba 100644 --- a/src/core.js +++ b/src/core.js @@ -33,6 +33,7 @@ exports.setPlotConfig = require('./plot_api/set_plot_config'); exports.register = Plotly.register; exports.toImage = require('./plot_api/to_image'); exports.downloadImage = require('./snapshot/download'); +exports.validate = require('./plot_api/validate'); // plot icons exports.Icons = require('../build/ploticon'); diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 9c67cb32014..7f7e65a5900 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -99,16 +99,14 @@ exports.valObjects = { // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'], coerceFunction: function(v, propOut, dflt, opts) { - if(opts.strict === true && typeof v !== 'string') { - propOut.set(dflt); - return; - } + if(typeof v !== 'string') { + var okToCoerce = (typeof v === 'number'); - var s = String(v); - if(v === undefined || (opts.noBlank === true && !s)) { - propOut.set(dflt); + if(opts.strict === true || !okToCoerce) propOut.set(dflt); + else propOut.set(String(v)); } - else propOut.set(s); + else if(opts.noBlank && !v) propOut.set(dflt); + else propOut.set(v); } }, color: { @@ -162,11 +160,11 @@ exports.valObjects = { subplotid: { description: [ 'An id string of a subplot type (given by dflt), optionally', - 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', + 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', '\'geo\', \'geo2\', \'geo3\', ...' ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], + requiredOpts: ['dflt'], + otherOpts: [], coerceFunction: function(v, propOut, dflt) { var dlen = dflt.length; if(typeof v === 'string' && v.substr(0, dlen) === dflt && @@ -175,6 +173,18 @@ exports.valObjects = { return; } propOut.set(dflt); + }, + validateFunction: function(v, opts) { + var dflt = opts.dflt, + dlen = dflt.length; + + if(v === dflt) return true; + if(typeof v !== 'string') return false; + if(v.substr(0, dlen) === dflt && idRegex.test(v.substr(dlen))) { + return true; + } + + return false; } }, flaglist: { @@ -239,6 +249,22 @@ exports.valObjects = { } propOut.set(vOut); + }, + validateFunction: function(v, opts) { + if(!Array.isArray(v)) return false; + + var items = opts.items; + + if(v.length !== items.length) return false; + + // valid when all items are valid + for(var i = 0; i < items.length; i++) { + var isItemValid = exports.validate(v[i], opts.items[i]); + + if(!isItemValid) return false; + } + + return true; } } }; @@ -309,3 +335,22 @@ exports.coerceFont = function(coerce, attr, dfltObj) { return out; }; + +exports.validate = function(value, opts) { + var valObject = exports.valObjects[opts.valType]; + + if(opts.arrayOk && Array.isArray(value)) return true; + + if(valObject.validateFunction) { + return valObject.validateFunction(value, opts); + } + + var failed = {}, + out = failed, + propMock = { set: function(v) { out = v; } }; + + // 'failed' just something mutable that won't be === anything else + + valObject.coerceFunction(value, propMock, failed, opts); + return out !== failed; +}; diff --git a/src/lib/index.js b/src/lib/index.js index 32f3f811a67..0a3d8bf7854 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -21,6 +21,7 @@ lib.valObjects = coerceModule.valObjects; lib.coerce = coerceModule.coerce; lib.coerce2 = coerceModule.coerce2; lib.coerceFont = coerceModule.coerceFont; +lib.validate = coerceModule.validate; var datesModule = require('./dates'); lib.dateTime2ms = datesModule.dateTime2ms; diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js new file mode 100644 index 00000000000..58dc5829934 --- /dev/null +++ b/src/plot_api/validate.js @@ -0,0 +1,309 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + + +var Lib = require('../lib'); +var Plots = require('../plots/plots'); +var PlotSchema = require('./plot_schema'); + +var isPlainObject = Lib.isPlainObject; +var isArray = Array.isArray; + + +/** + * Validate a data array and layout object. + * + * @param {array} data + * @param {object} layout + * + * @return {array} array of error objects each containing: + * - {string} code + * error code ('object', 'array', 'schema', 'unused', 'invisible' or 'value') + * - {string} container + * container where the error occurs ('data' or 'layout') + * - {number} trace + * trace index of the 'data' container where the error occurs + * - {array} path + * nested path to the key that causes the error + * - {string} astr + * attribute string variant of 'path' compatible with Plotly.restyle and + * Plotly.relayout. + * - {string} msg + * error message (shown in console in logger config argument is enable) + */ +module.exports = function valiate(data, layout) { + var schema = PlotSchema.get(), + errorList = [], + gd = {}; + + var dataIn, layoutIn; + + if(isArray(data)) { + gd.data = Lib.extendDeep([], data); + dataIn = data; + } + else { + gd.data = []; + dataIn = []; + errorList.push(format('array', 'data')); + } + + if(isPlainObject(layout)) { + gd.layout = Lib.extendDeep({}, layout); + layoutIn = layout; + } + else { + gd.layout = {}; + layoutIn = {}; + if(arguments.length > 1) { + errorList.push(format('object', 'layout')); + } + } + + // N.B. dataIn and layoutIn are in general not the same as + // gd.data and gd.layout after supplyDefaults as some attributes + // in gd.data and gd.layout (still) get mutated during this step. + + Plots.supplyDefaults(gd); + + var dataOut = gd._fullData, + len = dataIn.length; + + for(var i = 0; i < len; i++) { + var traceIn = dataIn[i], + base = ['data', i]; + + if(!isPlainObject(traceIn)) { + errorList.push(format('object', base)); + continue; + } + + var traceOut = dataOut[i], + traceType = traceOut.type, + traceSchema = schema.traces[traceType].attributes; + + // PlotSchema does something fancy with trace 'type', reset it here + // to make the trace schema compatible with Lib.validate. + traceSchema.type = { + valType: 'enumerated', + values: [traceType] + }; + + if(traceOut.visible === false && traceIn.visible !== false) { + errorList.push(format('invisible', base)); + } + + crawl(traceIn, traceOut, traceSchema, errorList, base); + } + + var layoutOut = gd._fullLayout, + layoutSchema = fillLayoutSchema(schema, dataOut); + + crawl(layoutIn, layoutOut, layoutSchema, errorList, 'layout'); + + // return undefined if no validation errors were found + return (errorList.length === 0) ? void(0) : errorList; +}; + +function crawl(objIn, objOut, schema, list, base, path) { + path = path || []; + + var keys = Object.keys(objIn); + + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + + var p = path.slice(); + p.push(k); + + var valIn = objIn[k], + valOut = objOut[k]; + + var nestedSchema = getNestedSchema(schema, k); + + if(!isInSchema(schema, k)) { + list.push(format('schema', base, p)); + } + else if(isPlainObject(valIn) && isPlainObject(valOut)) { + crawl(valIn, valOut, nestedSchema, list, base, p); + } + else if(nestedSchema.items && isArray(valIn)) { + var itemName = k.substr(0, k.length - 1); + + for(var j = 0; j < valIn.length; j++) { + var _nestedSchema = nestedSchema.items[itemName], + _p = p.slice(); + + _p.push(j); + + crawl(valIn[j], valOut[j], _nestedSchema, list, base, _p); + } + } + else if(!isPlainObject(valIn) && isPlainObject(valOut)) { + list.push(format('object', base, p, valIn)); + } + else if(!isArray(valIn) && isArray(valOut) && nestedSchema.valType !== 'info_array') { + list.push(format('array', base, p, valIn)); + } + else if(!(k in objOut)) { + list.push(format('unused', base, p, valIn)); + } + else if(!Lib.validate(valIn, nestedSchema)) { + list.push(format('value', base, p, valIn)); + } + } + + return list; +} + +// the 'full' layout schema depends on the traces types presents +function fillLayoutSchema(schema, dataOut) { + for(var i = 0; i < dataOut.length; i++) { + var traceType = dataOut[i].type, + traceLayoutAttr = schema.traces[traceType].layoutAttributes; + + if(traceLayoutAttr) { + Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr); + } + } + + return schema.layout.layoutAttributes; +} + +// validation error codes +var code2msgFunc = { + object: function(base, astr) { + var prefix; + + if(base === 'layout' && astr === '') prefix = 'The layout argument'; + else if(base[0] === 'data') { + prefix = 'Trace ' + base[1] + ' in the data argument'; + } + else prefix = inBase(base) + 'key ' + astr; + + return prefix + ' must be linked to an object container'; + }, + array: function(base, astr) { + var prefix; + + if(base === 'data') prefix = 'The data argument'; + else prefix = inBase(base) + 'key ' + astr; + + return prefix + ' must be linked to an array container'; + }, + schema: function(base, astr) { + return inBase(base) + 'key ' + astr + ' is not part of the schema'; + }, + unused: function(base, astr, valIn) { + var target = isPlainObject(valIn) ? 'container' : 'key'; + + return inBase(base) + target + ' ' + astr + ' did not get coerced'; + }, + invisible: function(base) { + return 'Trace ' + base[1] + ' got defaulted to be not visible'; + }, + value: function(base, astr, valIn) { + return [ + inBase(base) + 'key ' + astr, + 'is set to an invalid value (' + valIn + ')' + ].join(' '); + } +}; + +function inBase(base) { + if(isArray(base)) return 'In data trace ' + base[1] + ', '; + + return 'In ' + base + ', '; +} + +function format(code, base, path, valIn) { + path = path || ''; + + var container, trace; + + // container is either 'data' or 'layout + // trace is the trace index if 'data', null otherwise + + if(isArray(base)) { + container = base[0]; + trace = base[1]; + } + else { + container = base; + trace = null; + } + + var astr = convertPathToAttributeString(path), + msg = code2msgFunc[code](base, astr, valIn); + + // log to console if logger config option is enabled + Lib.log(msg); + + return { + code: code, + container: container, + trace: trace, + path: path, + astr: astr, + msg: msg + }; +} + +function isInSchema(schema, key) { + var parts = splitKey(key), + keyMinusId = parts.keyMinusId, + id = parts.id; + + if((keyMinusId in schema) && schema[keyMinusId]._isSubplotObj && id) { + return true; + } + + return (key in schema); +} + +function getNestedSchema(schema, key) { + var parts = splitKey(key); + + return schema[parts.keyMinusId]; +} + +function splitKey(key) { + var idRegex = /([2-9]|[1-9][0-9]+)$/; + + var keyMinusId = key.split(idRegex)[0], + id = key.substr(keyMinusId.length, key.length); + + return { + keyMinusId: keyMinusId, + id: id + }; +} + +function convertPathToAttributeString(path) { + if(!isArray(path)) return String(path); + + var astr = ''; + + for(var i = 0; i < path.length; i++) { + var p = path[i]; + + if(typeof p === 'number') { + astr = astr.substr(0, astr.length - 1) + '[' + p + ']'; + } + else { + astr += p; + } + + if(i < path.length - 1) astr += '.'; + } + + return astr; +} diff --git a/test/jasmine/tests/colorscale_test.js b/test/jasmine/tests/colorscale_test.js index dba9936b7a1..3610c17cdec 100644 --- a/test/jasmine/tests/colorscale_test.js +++ b/test/jasmine/tests/colorscale_test.js @@ -20,6 +20,13 @@ describe('Test colorscale:', function() { it('should accept only array of 2-item arrays', function() { expect(isValidScale('a')).toBe(false); + expect(isValidScale([])).toBe(false); + expect(isValidScale([null, undefined])).toBe(false); + expect(isValidScale([{}, [1, 'rgb(0, 0, 200']])).toBe(false); + expect(isValidScale([[0, 'rgb(200, 0, 0)'], {}])).toBe(false); + expect(isValidScale([[0, 'rgb(0, 0, 200)'], undefined])).toBe(false); + expect(isValidScale([null, [1, 'rgb(0, 0, 200)']])).toBe(false); + expect(isValidScale(['a', 'b'])).toBe(false); expect(isValidScale(['a'])).toBe(false); expect(isValidScale([['a'], ['b']])).toBe(false); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index b736d31a73e..9941494bfcc 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -526,13 +526,13 @@ describe('Test lib.js:', function() { .toEqual('42'); expect(coerce({s: [1, 2, 3]}, {}, stringAttrs, 's')) - .toEqual('1,2,3'); + .toEqual(dflt); expect(coerce({s: true}, {}, stringAttrs, 's')) - .toEqual('true'); + .toEqual(dflt); expect(coerce({s: {1: 2}}, {}, stringAttrs, 's')) - .toEqual('[object Object]'); // useless, but that's what it does!! + .toEqual(dflt); }); }); @@ -626,7 +626,22 @@ describe('Test lib.js:', function() { .toEqual([0.5, 1]); }); + it('should coerce unexpected input as best as it can', function() { + expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range')) + .toEqual([12]); + expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range', [-1, 20])) + .toEqual([12, 20]); + + expect(coerce({domain: [0.5]}, {}, infoArrayAttrs, 'domain')) + .toEqual([0.5, 1]); + + expect(coerce({range: ['-10', 100, 12]}, {}, infoArrayAttrs, 'range')) + .toEqual([-10, 100]); + + expect(coerce({domain: [0, 0.5, 1]}, {}, infoArrayAttrs, 'domain')) + .toEqual([0, 0.5]); + }); }); describe('subplotid valtype', function() { @@ -755,6 +770,222 @@ describe('Test lib.js:', function() { }); }); + describe('validate', function() { + + function assert(shouldPass, shouldFail, valObject) { + shouldPass.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(true, JSON.stringify(v) + ' should pass'); + }); + + shouldFail.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(false, JSON.stringify(v) + ' should fail'); + }); + } + + it('should work for valType \'data_array\' where', function() { + var shouldPass = [[20], []], + shouldFail = ['a', {}, 20, undefined, null]; + + assert(shouldPass, shouldFail, { + valType: 'data_array' + }); + + assert(shouldPass, shouldFail, { + valType: 'data_array', + dflt: [1, 2] + }); + }); + + it('should work for valType \'enumerated\' where', function() { + assert(['a', 'b'], ['c', 1, null, undefined, ''], { + valType: 'enumerated', + values: ['a', 'b'], + dflt: 'a' + }); + + assert([1, '1', 2, '2'], ['c', 3, null, undefined, ''], { + valType: 'enumerated', + values: [1, 2], + coerceNumber: true, + dflt: 1 + }); + + assert(['a', 'b', [1, 2]], ['c', 1, null, undefined, ''], { + valType: 'enumerated', + values: ['a', 'b'], + arrayOk: true, + dflt: 'a' + }); + }); + + it('should work for valType \'boolean\' where', function() { + var shouldPass = [true, false], + shouldFail = ['a', 1, {}, [], null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'boolean', + dflt: true + }); + + assert(shouldPass, shouldFail, { + valType: 'boolean', + dflt: false + }); + }); + + it('should work for valType \'number\' where', function() { + var shouldPass = [20, '20', 1e6], + shouldFail = ['a', [], {}, null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'number' + }); + + assert(shouldPass, shouldFail, { + valType: 'number', + dflt: null + }); + + assert([20, '20'], [-10, '-10', 25, '25'], { + valType: 'number', + dflt: 20, + min: 0, + max: 21 + }); + + assert([20, '20', [1, 2]], ['a', {}], { + valType: 'number', + dflt: 20, + arrayOk: true + }); + }); + + it('should work for valType \'integer\' where', function() { + assert([1, 2, '3', '4'], ['a', 1.321321, {}, [], null, 2 / 3, undefined, null], { + valType: 'integer', + dflt: 1 + }); + + assert([1, 2, '3', '4'], [-1, '-2', 2.121, null, undefined, [], {}], { + valType: 'integer', + min: 0, + dflt: 1 + }); + }); + + it('should work for valType \'string\' where', function() { + var date = new Date(2016, 1, 1); + + assert(['3', '4', 'a', 3, 1.2113, ''], [undefined, {}, [], null, date, false], { + valType: 'string', + dflt: 'a' + }); + + assert(['3', '4', 'a', 3, 1.2113], ['', undefined, {}, [], null, date, true], { + valType: 'string', + dflt: 'a', + noBlank: true + }); + + assert(['3', '4', ''], [undefined, 1, {}, [], null, date, true, false], { + valType: 'string', + dflt: 'a', + strict: true + }); + + assert(['3', '4'], [undefined, 1, {}, [], null, date, '', true, false], { + valType: 'string', + dflt: 'a', + strict: true, + noBlank: true + }); + }); + + it('should work for valType \'color\' where', function() { + var shouldPass = ['red', '#d3d3d3', 'rgba(0,255,255,0.1)'], + shouldFail = [1, {}, [], 'rgq(233,122,332,1)', null, undefined]; + + assert(shouldPass, shouldFail, { + valType: 'color' + }); + }); + + it('should work for valType \'colorscale\' where', function() { + var good = [ [0, 'red'], [1, 'blue'] ], + bad = [ [0.1, 'red'], [1, 'blue'] ], + bad2 = [ [0], [1] ], + bad3 = [ ['red'], ['blue']], + bad4 = ['red', 'blue']; + + var shouldPass = ['Viridis', 'Greens', good], + shouldFail = ['red', 1, undefined, null, {}, [], bad, bad2, bad3, bad4]; + + assert(shouldPass, shouldFail, { + valType: 'colorscale' + }); + }); + + it('should work for valType \'angle\' where', function() { + var shouldPass = ['auto', '120', 270], + shouldFail = [{}, [], 'red', null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'angle', + dflt: 0 + }); + }); + + it('should work for valType \'subplotid\' where', function() { + var shouldPass = ['sp', 'sp4', 'sp10'], + shouldFail = [{}, [], 'sp1', 'sp0', 'spee1', null, undefined, true]; + + assert(shouldPass, shouldFail, { + valType: 'subplotid', + dflt: 'sp' + }); + }); + + it('should work for valType \'flaglist\' where', function() { + var shouldPass = ['a', 'b', 'a+b', 'b+a', 'c'], + shouldFail = [{}, [], 'red', null, undefined, '', 'a + b']; + + assert(shouldPass, shouldFail, { + valType: 'flaglist', + flags: ['a', 'b'], + extras: ['c'] + }); + }); + + it('should work for valType \'any\' where', function() { + var shouldPass = ['', '120', null, false, {}, []], + shouldFail = [undefined]; + + assert(shouldPass, shouldFail, { + valType: 'any' + }); + }); + + it('should work for valType \'info_array\' where', function() { + var shouldPass = [[1, 2], [-20, '20']], + shouldFail = [ + {}, [], [10], [null, 10], ['aads', null], + 'red', null, undefined, '', + [1, 10, null] + ]; + + assert(shouldPass, shouldFail, { + valType: 'info_array', + items: [{ + valType: 'number', dflt: -20 + }, { + valType: 'number', dflt: 20 + }] + }); + }); + }); + describe('setCursor', function() { beforeEach(function() { diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js new file mode 100644 index 00000000000..4273b84a355 --- /dev/null +++ b/test/jasmine/tests/validate_test.js @@ -0,0 +1,251 @@ +var Plotly = require('@lib/index'); + + +describe('Plotly.validate', function() { + + function assertErrorContent(obj, code, cont, trace, path, astr, msg) { + expect(obj.code).toEqual(code); + expect(obj.container).toEqual(cont); + expect(obj.trace).toEqual(trace); + expect(obj.path).toEqual(path); + expect(obj.astr).toEqual(astr); + expect(obj.msg).toEqual(msg); + } + + it('should return undefined when no errors are found', function() { + var out = Plotly.validate([{ + type: 'scatter', + x: [1, 2, 3] + }], { + title: 'my simple graph' + }); + + expect(out).toBeUndefined(); + }); + + it('should report when data is not an array', function() { + var out = Plotly.validate({ + type: 'scatter', + x: [1, 2, 3] + }); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'array', 'data', null, '', '', + 'The data argument must be linked to an array container' + ); + }); + + it('should report when a data trace is not an object', function() { + var out = Plotly.validate([{ + type: 'scatter', + x: [1, 2, 3] + }, [1, 2, 3]]); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'object', 'data', 1, '', '', + 'Trace 1 in the data argument must be linked to an object container' + ); + }); + + it('should report when layout is not an object', function() { + var out = Plotly.validate([], [1, 2, 3]); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'object', 'layout', null, '', '', + 'The layout argument must be linked to an object container' + ); + }); + + it('should report when trace is defaulted to not be visible', function() { + var out = Plotly.validate([{ + type: 'scatter' + // missing 'x' and 'y + }], {}); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'invisible', 'data', 0, '', '', + 'Trace 0 got defaulted to be not visible' + ); + }); + + it('should report when trace contains keys not part of the schema', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + markerColor: 'blue' + }], {}); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'schema', 'data', 0, ['markerColor'], 'markerColor', + 'In data trace 0, key markerColor is not part of the schema' + ); + }); + + it('should report when trace contains keys that are not coerced', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + mode: 'lines', + marker: { color: 'blue' } + }, { + x: [1, 2, 3], + mode: 'markers', + marker: { + color: 'blue', + cmin: 10 + } + }], {}); + + expect(out.length).toEqual(2); + assertErrorContent( + out[0], 'unused', 'data', 0, ['marker'], 'marker', + 'In data trace 0, container marker did not get coerced' + ); + assertErrorContent( + out[1], 'unused', 'data', 1, ['marker', 'cmin'], 'marker.cmin', + 'In data trace 1, key marker.cmin did not get coerced' + ); + }); + + it('should report when trace contains keys set to invalid values', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + mode: 'lines', + line: { width: 'a big number' } + }, { + x: [1, 2, 3], + mode: 'markers', + marker: { color: 10 } + }], {}); + + expect(out.length).toEqual(2); + assertErrorContent( + out[0], 'value', 'data', 0, ['line', 'width'], 'line.width', + 'In data trace 0, key line.width is set to an invalid value (a big number)' + ); + assertErrorContent( + out[1], 'value', 'data', 1, ['marker', 'color'], 'marker.color', + 'In data trace 1, key marker.color is set to an invalid value (10)' + ); + }); + + it('should work with isLinkedToArray attributes', function() { + var out = Plotly.validate([], { + annotations: [{ + text: 'some text' + }, { + arrowSymbol: 'cat' + }, { + font: { color: 'wont-work' } + }], + xaxis: { + type: 'date', + rangeselector: { + buttons: [{ + label: '1 month', + step: 'all', + count: 10 + }, { + title: '1 month' + }] + } + }, + xaxis2: { + type: 'date', + rangeselector: { + buttons: [{ + title: '1 month' + }] + } + }, + xaxis3: { + type: 'date', + rangeselector: { + buttons: 'wont-work' + } + }, + shapes: [{ + opacity: 'none' + }] + }); + + expect(out.length).toEqual(7); + assertErrorContent( + out[0], 'schema', 'layout', null, + ['annotations', 1, 'arrowSymbol'], 'annotations[1].arrowSymbol', + 'In layout, key annotations[1].arrowSymbol is not part of the schema' + ); + assertErrorContent( + out[1], 'value', 'layout', null, + ['annotations', 2, 'font', 'color'], 'annotations[2].font.color', + 'In layout, key annotations[2].font.color is set to an invalid value (wont-work)' + ); + assertErrorContent( + out[2], 'unused', 'layout', null, + ['xaxis', 'rangeselector', 'buttons', 0, 'count'], + 'xaxis.rangeselector.buttons[0].count', + 'In layout, key xaxis.rangeselector.buttons[0].count did not get coerced' + ); + assertErrorContent( + out[3], 'schema', 'layout', null, + ['xaxis', 'rangeselector', 'buttons', 1, 'title'], + 'xaxis.rangeselector.buttons[1].title', + 'In layout, key xaxis.rangeselector.buttons[1].title is not part of the schema' + ); + assertErrorContent( + out[4], 'schema', 'layout', null, + ['xaxis2', 'rangeselector', 'buttons', 0, 'title'], + 'xaxis2.rangeselector.buttons[0].title', + 'In layout, key xaxis2.rangeselector.buttons[0].title is not part of the schema' + ); + assertErrorContent( + out[5], 'array', 'layout', null, + ['xaxis3', 'rangeselector', 'buttons'], + 'xaxis3.rangeselector.buttons', + 'In layout, key xaxis3.rangeselector.buttons must be linked to an array container' + ); + assertErrorContent( + out[6], 'value', 'layout', null, + ['shapes', 0, 'opacity'], 'shapes[0].opacity', + 'In layout, key shapes[0].opacity is set to an invalid value (none)' + ); + }); + + it('should work with isSubplotObj attributes', function() { + var out = Plotly.validate([], { + xaxis2: { + range: 30 + }, + scene10: { + bgcolor: 'red' + }, + geo0: {}, + yaxis5: 'sup' + }); + + expect(out.length).toEqual(4); + assertErrorContent( + out[0], 'value', 'layout', null, + ['xaxis2', 'range'], 'xaxis2.range', + 'In layout, key xaxis2.range is set to an invalid value (30)' + ); + assertErrorContent( + out[1], 'unused', 'layout', null, + ['scene10'], 'scene10', + 'In layout, container scene10 did not get coerced' + ); + assertErrorContent( + out[2], 'schema', 'layout', null, + ['geo0'], 'geo0', + 'In layout, key geo0 is not part of the schema' + ); + assertErrorContent( + out[3], 'object', 'layout', null, + ['yaxis5'], 'yaxis5', + 'In layout, key yaxis5 must be linked to an object container' + ); + }); +});