function assertPlotSchema(callback) {
        var traces = plotSchema.traces;

        Object.keys(traces).forEach(function(traceName) {
            Plotly.PlotSchema.crawl(traces[traceName].attributes, callback);
        });

        Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback);
    }
    it('does not have yaxis-only attributes or mismatched x/yaxis attributes', function() {
        // in principle either of these should be allowable, but we don't currently
        // support them so lets simply test that we haven't added them accidentally!

        function delDescription(attr) { delete attr.description; }

        for(var key in Registry.componentsRegistry) {
            var _module = Registry.componentsRegistry[key];
            var schema = _module.schema;
            if(schema && schema.subplots && schema.subplots.yaxis) {
                expect(schema.subplots.xaxis).toBeDefined(_module.name);

                var xa = Lib.extendDeep({}, schema.subplots.xaxis);
                var ya = Lib.extendDeep({}, schema.subplots.yaxis);
                Plotly.PlotSchema.crawl(xa, delDescription);
                Plotly.PlotSchema.crawl(ya, delDescription);
                expect(JSON.stringify(xa)).toBe(JSON.stringify(ya), _module.name);
            }
        }
    });
    function assertLayoutSchema(callback) {
        Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback, 0, 'layout');

        var traces = plotSchema.traces;

        Object.keys(traces).forEach(function(traceName) {
            var layoutAttrs = traces[traceName].layoutAttributes;
            if(layoutAttrs) {
                Plotly.PlotSchema.crawl(layoutAttrs, callback, 0, traceName + ': layout');
            }
        });
    }
 Object.keys(traces).forEach(function(traceName) {
     var layoutAttrs = traces[traceName].layoutAttributes;
     if(layoutAttrs) {
         Plotly.PlotSchema.crawl(layoutAttrs, callback, 0, traceName + ': layout');
     }
 });
 Object.keys(transforms).forEach(function(transformName) {
     Plotly.PlotSchema.crawl(transforms[transformName].attributes, callback, 0, transformName);
 });
 it('should prune unsupported global-level trace attributes', function() {
     expect(Plotly.PlotSchema.get().traces.sankey.attributes.hoverinfo.flags.length).toBe(0);
 });
describe('plot schema', function() {
    'use strict';

    var plotSchema = Plotly.PlotSchema.get();
    var valObjects = plotSchema.defs.valObjects;

    var isValObject = Plotly.PlotSchema.isValObject;
    var isPlainObject = Lib.isPlainObject;

    var VALTYPES = Object.keys(valObjects);
    var ROLES = ['info', 'style', 'data'];
    var editType = plotSchema.defs.editType;

    function assertTraceSchema(callback) {
        var traces = plotSchema.traces;

        Object.keys(traces).forEach(function(traceName) {
            Plotly.PlotSchema.crawl(traces[traceName].attributes, callback, 0, traceName);
        });
    }

    function assertTransformSchema(callback) {
        var transforms = plotSchema.transforms;

        Object.keys(transforms).forEach(function(transformName) {
            Plotly.PlotSchema.crawl(transforms[transformName].attributes, callback, 0, transformName);
        });
    }

    function assertLayoutSchema(callback) {
        Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback, 0, 'layout');

        var traces = plotSchema.traces;

        Object.keys(traces).forEach(function(traceName) {
            var layoutAttrs = traces[traceName].layoutAttributes;
            if(layoutAttrs) {
                Plotly.PlotSchema.crawl(layoutAttrs, callback, 0, traceName + ': layout');
            }
        });
    }

    function assertPlotSchema(callback) {
        assertTraceSchema(callback);
        assertLayoutSchema(callback);
        assertTransformSchema(callback);
    }

    it('all attributes should have a valid `valType`', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    expect(VALTYPES.indexOf(attr.valType) !== -1).toBe(true, attr);
                }
            }
        );

    });

    it('all attributes should only have valid `role`', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    expect(ROLES.indexOf(attr.role) !== -1).toBe(true, attr);
                }
            }
        );
    });

    it('all nested objects should have the *object* `role`', function() {
        assertPlotSchema(
            function(attr, attrName) {
                if(!isValObject(attr) && isPlainObject(attr) && attrName !== 'items') {
                    expect(attr.role === 'object').toBe(true);
                }
            }
        );
    });

    it('all attributes should have the required options', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    var keys = Object.keys(attr);

                    valObjects[attr.valType].requiredOpts.forEach(function(opt) {
                        expect(keys.indexOf(opt) !== -1).toBe(true);
                    });
                }
            }
        );
    });

    it('all attributes should only have compatible options', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    var valObject = valObjects[attr.valType];
                    var opts = valObject.requiredOpts
                        .concat(valObject.otherOpts)
                        .concat([
                            'valType', 'description', 'role',
                            'editType', 'impliedEdits', 'anim',
                            '_compareAsJSON', '_noTemplating'
                        ]);

                    Object.keys(attr).forEach(function(key) {
                        expect(opts.indexOf(key) !== -1).toBe(true, key, attr);
                    });
                }
            }
        );
    });

    it('all subplot objects should contain _isSubplotObj', function() {
        var IS_SUBPLOT_OBJ = '_isSubplotObj';
        var cnt = 0;

        var astrs = [
            'xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar',
            // not really a 'subplot' object but supports yaxis, yaxis2, yaxis3,
            // ... counters, so list it here
            'xaxis.rangeslider.yaxis'
        ];

        // check if the subplot objects have '_isSubplotObj'
        astrs.forEach(function(astr) {
            expect(
                Lib.nestedProperty(
                    plotSchema.layout.layoutAttributes,
                    astr + '.' + IS_SUBPLOT_OBJ
                ).get()
            ).toBe(true);
        });

        // check that no other object has '_isSubplotObj'
        assertPlotSchema(
            function(attr, attrName) {
                if(attr && attr[IS_SUBPLOT_OBJ] === true) {
                    expect(astrs.indexOf(attrName)).not.toEqual(-1);
                    cnt++;
                }
            }
        );

        expect(cnt).toEqual(astrs.length);
    });

    it('should convert _isLinkedToArray attributes to items object', function() {
        var astrs = [
            'annotations', 'shapes', 'images',
            'xaxis.rangeselector.buttons',
            'updatemenus',
            'sliders',
            'mapbox.layers'
        ];

        astrs.forEach(function(astr) {
            var np = Lib.nestedProperty(
                plotSchema.layout.layoutAttributes, astr
            );

            var name = np.parts[np.parts.length - 1];
            var itemName = name.substr(0, name.length - 1);

            var itemsObj = np.get().items;
            var itemObj = itemsObj[itemName];

            // N.B. the specs below must be satisfied for plotly.py
            expect(isPlainObject(itemsObj)).toBe(true);
            expect(itemsObj.role).toBeUndefined();
            expect(Object.keys(itemsObj).length).toEqual(1);
            expect(isPlainObject(itemObj)).toBe(true);
            expect(itemObj.role).toBe('object');

            var role = np.get().role;
            expect(role).toEqual('object');
        });
    });

    it('includes xaxis-only items on only the x axis, not y or bare', function() {
        var items = ['rangeselector', 'rangeslider'];

        var layoutAttrs = plotSchema.layout.layoutAttributes;

        items.forEach(function(item) {
            expect(layoutAttrs.xaxis[item]).toBeDefined(item);
            expect(layoutAttrs.yaxis[item]).toBeUndefined(item);
            expect(layoutAttrs[item]).toBeUndefined(item);
        });
    });

    it('valObjects descriptions should be strings', function() {
        assertPlotSchema(
            function(attr) {
                var isValid;

                if(isValObject(attr)) {
                    // attribute don't have to have a description (for now)
                    isValid = (typeof attr.description === 'string') ||
                        (attr.description === undefined);

                    expect(isValid).toBe(true);
                }
            }
        );
    });

    it('deprecated attributes should have a `valType` and `role`', function() {
        var DEPRECATED = '_deprecated';

        assertPlotSchema(
            function(attr, attrName, attrs, level, attrString) {
                if(attr && isPlainObject(attr[DEPRECATED]) && isValObject(attr[DEPRECATED])) {
                    Object.keys(attr[DEPRECATED]).forEach(function(dAttrName) {
                        var dAttr = attr[DEPRECATED][dAttrName];

                        expect(VALTYPES.indexOf(dAttr.valType) !== -1)
                            .toBe(true, attrString + ': ' + dAttrName);
                        expect(ROLES.indexOf(dAttr.role) !== -1)
                            .toBe(true, attrString + ': ' + dAttrName);
                    });
                }
            }
        );
    });

    it('has valid or no `impliedEdits` in every attribute', function() {
        assertPlotSchema(function(attr, attrName, attrs, level, attrString) {
            if(attr && attr.impliedEdits !== undefined) {
                expect(isPlainObject(attr.impliedEdits))
                    .toBe(true, attrString + ': ' + JSON.stringify(attr.impliedEdits));
                // make sure it wasn't emptied out
                expect(Object.keys(attr.impliedEdits).length).not.toBe(0, attrString);
            }
        });
    });

    it('has valid `editType` in all attributes and containers', function() {
        function shouldHaveEditType(attr, attrName) {
            // ensure any object (container or regular val object) has editType
            // array containers have extra nesting where editType would be redundant
            return Lib.isPlainObject(attr) && attrName !== 'impliedEdits' &&
                attrName !== 'items' && !Lib.isPlainObject(attr.items);
        }

        assertTraceSchema(function(attr, attrName, attrs, level, attrString) {
            if(shouldHaveEditType(attr, attrName)) {
                expect(Lib.validate(attr.editType, editType.traces))
                    .toBe(true, attrString + ': ' + JSON.stringify(attr.editType));
            }
        });

        assertTransformSchema(function(attr, attrName, attrs, level, attrString) {
            if(shouldHaveEditType(attr, attrName)) {
                expect(Lib.validate(attr.editType, editType.traces))
                    .toBe(true, attrString + ': ' + JSON.stringify(attr.editType));
            }
        });

        assertLayoutSchema(function(attr, attrName, attrs, level, attrString) {
            if(shouldHaveEditType(attr, attrName)) {
                expect(Lib.validate(attr.editType, editType.layout))
                    .toBe(true, attrString + ': ' + JSON.stringify(attr.editType));
            }
        });
    });

    it('should work with registered transforms', function() {
        var valObjects = plotSchema.transforms.filter.attributes;
        var attrNames = Object.keys(valObjects);

        ['operation', 'value', 'target'].forEach(function(k) {
            expect(attrNames).toContain(k);
        });
    });

    it('should work with registered transforms (2)', function() {
        var valObjects = plotSchema.transforms.groupby.attributes;
        var items = valObjects.styles.items || {};

        expect(Object.keys(items)).toEqual(['style']);
    });

    it('should work with registered components', function() {
        expect(plotSchema.traces.scatter.attributes.xcalendar.valType).toEqual('enumerated');
        expect(plotSchema.traces.scatter3d.attributes.zcalendar.valType).toEqual('enumerated');

        expect(plotSchema.layout.layoutAttributes.calendar.valType).toEqual('enumerated');
        expect(plotSchema.layout.layoutAttributes.xaxis.calendar.valType).toEqual('enumerated');
        expect(plotSchema.layout.layoutAttributes.scene.xaxis.calendar.valType).toEqual('enumerated');

        expect(plotSchema.transforms.filter.attributes.valuecalendar.valType).toEqual('enumerated');
        expect(plotSchema.transforms.filter.attributes.targetcalendar.valType).toEqual('enumerated');
    });

    it('should list correct defs', function() {
        expect(plotSchema.defs.valObjects).toBeDefined();

        expect(plotSchema.defs.metaKeys)
            .toEqual([
                '_isSubplotObj', '_isLinkedToArray', '_arrayAttrRegexps',
                '_deprecated', 'description', 'role', 'editType', 'impliedEdits'
            ]);
    });

    it('should list the correct frame attributes', function() {
        expect(plotSchema.frames).toBeDefined();
        expect(plotSchema.frames.role).toEqual('object');
        expect(plotSchema.frames.items.frames_entry).toBeDefined();
        expect(plotSchema.frames.items.frames_entry.role).toEqual('object');
    });

    it('should list config attributes', function() {
        expect(plotSchema.config).toBeDefined();
        expect(plotSchema.config.scrollZoom).toBeDefined();
    });

    it('should list trace-dependent & direction-dependent error bar attributes', function() {
        var scatterSchema = plotSchema.traces.scatter.attributes;
        expect(scatterSchema.error_x.copy_ystyle).toBeDefined();
        expect(scatterSchema.error_x.copy_ystyle.editType).toBe('plot');
        expect(scatterSchema.error_x.copy_zstyle).toBeUndefined();
        expect(scatterSchema.error_y.copy_ystyle).toBeUndefined();
        expect(scatterSchema.error_y.copy_zstyle).toBeUndefined();

        var scatter3dSchema = plotSchema.traces.scatter3d.attributes;
        expect(scatter3dSchema.error_x.copy_ystyle).toBeUndefined();
        expect(scatter3dSchema.error_x.copy_zstyle).toBeDefined();
        expect(scatter3dSchema.error_x.copy_zstyle.editType).toBe('calc');
        expect(scatter3dSchema.error_y.copy_ystyle).toBeUndefined();
        expect(scatter3dSchema.error_y.copy_zstyle).toBeDefined();
        expect(scatter3dSchema.error_y.copy_zstyle.editType).toBe('calc');
        expect(scatter3dSchema.error_z.copy_ystyle).toBeUndefined();
        expect(scatter3dSchema.error_z.copy_zstyle).toBeUndefined();

        var scatterglSchema = plotSchema.traces.scattergl.attributes;
        expect(scatterglSchema.error_x.copy_ystyle).toBeDefined();
        expect(scatterglSchema.error_x.copy_ystyle.editType).toBe('calc');
        expect(scatterglSchema.error_x.copy_zstyle).toBeUndefined();
        expect(scatterglSchema.error_y.copy_ystyle).toBeUndefined();
        expect(scatterglSchema.error_y.copy_zstyle).toBeUndefined();
    });

    it('should convert regex valObject fields to strings', function() {
        var splomAttrs = plotSchema.traces.splom.attributes;

        expect(typeof splomAttrs.xaxes.items.regex).toBe('string');
        expect(splomAttrs.xaxes.items.regex).toBe('/^x([2-9]|[1-9][0-9]+)?$/');
        expect(typeof splomAttrs.yaxes.items.regex).toBe('string');
        expect(splomAttrs.yaxes.items.regex).toBe('/^y([2-9]|[1-9][0-9]+)?$/');
    });

    it('should prune unsupported global-level trace attributes', function() {
        expect(Plotly.PlotSchema.get().traces.sankey.attributes.hoverinfo.flags.length).toBe(0);
    });

});
describe('plot schema', function() {
    'use strict';

    var plotSchema = Plotly.PlotSchema.get(),
        valObjects = plotSchema.defs.valObjects;

    var isValObject = Plotly.PlotSchema.isValObject,
        isPlainObject = Lib.isPlainObject;

    var VALTYPES = Object.keys(valObjects),
        ROLES = ['info', 'style', 'data'];

    function assertPlotSchema(callback) {
        var traces = plotSchema.traces;

        Object.keys(traces).forEach(function(traceName) {
            Plotly.PlotSchema.crawl(traces[traceName].attributes, callback);
        });

        Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback);
    }

    it('all attributes should have a valid `valType`', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    expect(VALTYPES.indexOf(attr.valType) !== -1).toBe(true);
                }
            }
        );

    });

    it('all attributes should only have valid `role`', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    expect(ROLES.indexOf(attr.role) !== -1).toBe(true);
                }
            }
        );
    });

    it('all nested objects should have the *object* `role`', function() {
        assertPlotSchema(
            function(attr, attrName) {
                if(!isValObject(attr) && isPlainObject(attr) && attrName!=='items') {
                    expect(attr.role === 'object').toBe(true);
                }
            }
        );
    });

    it('all attributes should have the required options', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    var keys = Object.keys(attr);

                    valObjects[attr.valType].requiredOpts.forEach(function(opt) {
                        expect(keys.indexOf(opt) !== -1).toBe(true);
                    });
                }
            }
        );
    });

    it('all attributes should only have compatible options', function() {
        assertPlotSchema(
            function(attr) {
                if(isValObject(attr)) {
                    var valObject = valObjects[attr.valType],
                        opts = valObject.requiredOpts
                            .concat(valObject.otherOpts)
                            .concat(['valType', 'description', 'role']);

                    Object.keys(attr).forEach(function(key) {
                        // handle the histogram marker.color case
                        if(opts.indexOf(key)===-1 && opts[key]===undefined) return;

                        expect(opts.indexOf(key) !== -1).toBe(true);
                    });
                }
            }
        );
    });

    it('all subplot objects should contain _isSubplotObj', function() {
        var IS_SUBPLOT_OBJ = '_isSubplotObj',
            astrs = ['xaxis', 'yaxis', 'scene', 'geo', 'ternary'],
            list = [];

        // check if the subplot objects have '_isSubplotObj'
        astrs.forEach(function(astr) {
            expect(
                Lib.nestedProperty(
                    plotSchema.layout.layoutAttributes,
                    astr + '.' + IS_SUBPLOT_OBJ
                ).get()
            ).toBe(true);
        });

        // check that no other object has '_isSubplotObj'
        assertPlotSchema(
            function(attr, attrName) {
                if(attr[IS_SUBPLOT_OBJ] === true) list.push(attrName);
            }
        );
        expect(list).toEqual(astrs);
    });

    it('should convert _isLinkedToArray attributes to items object', function() {
        var astrs = [
            'annotations', 'shapes', 'images',
            'xaxis.rangeselector.buttons', 'yaxis.rangeselector.buttons'
        ];

        astrs.forEach(function(astr) {
            var np = Lib.nestedProperty(
                plotSchema.layout.layoutAttributes, astr
            );

            var name = np.parts[np.parts.length - 1],
                itemName = name.substr(0, name.length - 1);

            var itemsObj = np.get().items,
                itemObj = itemsObj[itemName];

            // N.B. the specs below must be satisfied for plotly.py
            expect(isPlainObject(itemsObj)).toBe(true);
            expect(itemsObj.role).toBeUndefined();
            expect(Object.keys(itemsObj).length).toEqual(1);
            expect(isPlainObject(itemObj)).toBe(true);
            expect(itemObj.role).toBe('object');

            var role = np.get().role;
            expect(role).toEqual('object');
        });
    });

    it('valObjects descriptions should be strings', function() {
        assertPlotSchema(
            function(attr) {
                var isValid;

                if(isValObject(attr)) {
                    // attribute don't have to have a description (for now)
                    isValid = (typeof attr.description === 'string') ||
                        (attr.description === undefined);

                    expect(isValid).toBe(true);
                }
            }
        );
    });

    it('deprecated attributes should have a `valType` and `role`', function() {
        var DEPRECATED = '_deprecated';

        assertPlotSchema(
            function(attr) {
                if(isPlainObject(attr[DEPRECATED])) {
                    Object.keys(attr[DEPRECATED]).forEach(function(dAttrName) {
                        var dAttr = attr[DEPRECATED][dAttrName];

                        expect(VALTYPES.indexOf(dAttr.valType) !== -1).toBe(true);
                        expect(ROLES.indexOf(dAttr.role) !== -1).toBe(true);
                    });
                }
            }
        );
    });

});
 Object.keys(traces).forEach(function(traceName) {
     Plotly.PlotSchema.crawl(traces[traceName].attributes, callback);
 });