diff --git a/package.json b/package.json index 34eb83e..696d8e9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "gulp-inline": "0.0.15", "gulp-sass": "^2.1.1", "gulp-sourcemaps": "^1.6.0", + "joi": "^7.2.3", "karma": "^0.13.19", "karma-browserify": "^5.0.1", "karma-chrome-launcher": "^0.2.2", diff --git a/src/scripts/components/submit.js b/src/scripts/components/submit.js index a083933..fc2ee75 100644 --- a/src/scripts/components/submit.js +++ b/src/scripts/components/submit.js @@ -5,6 +5,7 @@ module.exports = { template: require('../../templates/components/submit.tpl'), manipulator: 'val', defaults: { + label: '', attributes: {} } }; diff --git a/src/scripts/components/toggle.js b/src/scripts/components/toggle.js index 7b5f453..f0b2d56 100644 --- a/src/scripts/components/toggle.js +++ b/src/scripts/components/toggle.js @@ -6,6 +6,7 @@ module.exports = { style: require('../../styles/clay/components/toggle.scss'), manipulator: 'checked', defaults: { + label: '', attributes: {} } }; diff --git a/src/scripts/config-page.js b/src/scripts/config-page.js index 4c1cf6e..edbfdd5 100755 --- a/src/scripts/config-page.js +++ b/src/scripts/config-page.js @@ -12,6 +12,7 @@ var returnTo = window.returnTo || 'pebblejs://close#'; var customFn = window.customFn || function() {}; var clayComponents = window.clayComponents || {}; +// Register the passed components _.eachObj(clayComponents, function(key, component) { ClayConfig.registerComponent(component); }); @@ -19,18 +20,11 @@ _.eachObj(clayComponents, function(key, component) { var $mainForm = $('#main-form'); var clayConfig = new ClayConfig(settings, config, $mainForm); -/* istanbul ignore next */ // @todo reassess how to do form submission -clayConfig.on(clayConfig.EVENTS.AFTER_BUILD, function() { - var self = this; - - // add listeners here - $mainForm.on('submit', function(event) { - // Set the return URL depending on the runtime environment - location.href = - returnTo + encodeURIComponent(JSON.stringify(self.getSettings())); - event.preventDefault(); - return false; - }); +// add listeners here +$mainForm.on('submit', function() { + // Set the return URL depending on the runtime environment + location.href = returnTo + + encodeURIComponent(JSON.stringify(clayConfig.getSettings())); }); // Run the custom function in the context of the ClayConfig diff --git a/src/scripts/lib/clay-config.js b/src/scripts/lib/clay-config.js index f6101ce..c13ae8e 100644 --- a/src/scripts/lib/clay-config.js +++ b/src/scripts/lib/clay-config.js @@ -190,10 +190,18 @@ function ClayConfig(settings, config, $rootContainer) { * @param {function} component.manipulator.get - get manipulator method * @param {{}} component.defaults - template defaults * @param {function} [component.initialize] - method to scaffold the component - * @return {void} + * @return {boolean} - Returns true if component was registered correctly */ ClayConfig.registerComponent = function(component) { var _component = _.copyObj(component); + + if (componentStore[_component.name]) { + console.warn('Component: ' + _component.name + + ' is already registered. If you wish to override the existing' + + ' functionality, you must provide a new name'); + return false; + } + if (typeof _component.manipulator === 'string') { _component.manipulator = manipulators[component.manipulator]; @@ -216,6 +224,7 @@ ClayConfig.registerComponent = function(component) { } componentStore[_component.name] = _component; + return true; }; module.exports = ClayConfig; diff --git a/test/fixture.js b/test/fixture.js index ec57986..157c482 100644 --- a/test/fixture.js +++ b/test/fixture.js @@ -5,66 +5,72 @@ var $ = require('../src/scripts/vendor/minified').$; var HTML = require('../src/scripts/vendor/minified').HTML; var ClayItem = require('../src/scripts/lib/clay-item'); var ClayConfig = require('../src/scripts/lib/clay-config'); +var components = require('../src/scripts/components'); +var componentRegistry = require('../src/scripts/lib/component-registry'); var idCounter = 0; -// add some components to the registry to test -ClayConfig.registerComponent(require('../src/scripts/components/text')); -ClayConfig.registerComponent(require('../src/scripts/components/input')); -ClayConfig.registerComponent(require('../src/scripts/components/toggle')); -ClayConfig.registerComponent(require('../src/scripts/components/footer')); -ClayConfig.registerComponent(require('../src/scripts/components/select')); - /** * @param {string|{}} config - * @returns {{}} + * @param {boolean} [autoRegister=true] + * @returns {Clay~ConfigItem} */ -module.exports.configItem = function(config) { +module.exports.configItem = function(config, autoRegister) { if (typeof config === 'string') { config = { type: config }; } - var basic = { + var result = _.extend({}, { label: config.type + '-label', appKey: 'appKey-' + idCounter, id: 'id-' + idCounter - }; + }, config); idCounter++; - return _.extend({}, basic, config); + if (autoRegister !== false && + !componentRegistry[result.type] && + result.type !== 'section') { + ClayConfig.registerComponent(components[result.type]); + } + + return result; }; /** - * @param {string|{}} [config] + * @param {string|{}} config + * @param {boolean} [autoRegister=true] * @returns {ClayItem} */ -module.exports.clayItem = function(config) { - return new ClayItem(module.exports.configItem(config)); +module.exports.clayItem = function(config, autoRegister) { + return new ClayItem(module.exports.configItem(config, autoRegister)); }; /** * @param {[]} types + * @param {boolean} [autoRegister=true] * @returns {*} */ -module.exports.config = function(types) { +module.exports.config = function(types, autoRegister) { return types.map(function(item) { return Array.isArray(item) ? - {type: 'section', items: module.exports.config(item)} : - module.exports.configItem(item); + {type: 'section', items: module.exports.config(item, autoRegister)} : + module.exports.configItem(item, autoRegister); }); }; /** * @param {[]} types - * @param {boolean} [noBuild=false] - don't run the build method on the result + * @param {boolean} [build=true] - run the build method on the result + * @param {boolean} [autoRegister=true] * @param {{}} [settings] - settings to pass to constructor * @returns {ClayConfig} */ -module.exports.clayConfig = function(types, noBuild, settings) { +module.exports.clayConfig = function(types, build, autoRegister, settings) { var clayConfig = new ClayConfig( settings || {}, - module.exports.config(types), $(HTML('
')) + module.exports.config(types, autoRegister), + $(HTML('
')) ); - return noBuild ? clayConfig : clayConfig.build(); + return build === false ? clayConfig : clayConfig.build(); }; diff --git a/test/karma.conf.js b/test/karma.conf.js index d80160d..f02e32f 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -39,9 +39,12 @@ module.exports = function(config) { [ 'browserify-istanbul', { - ignore: process.env.DEBUG ? - ['**/**'] : - ['**/test/**', '**/src/scripts/vendor/**'] + ignore: process.env.DEBUG ? ['**/**'] : + [ + '**/test/**', + '**/src/scripts/vendor/**', + '**/src/scripts/config-page.js' + ] } ] ] diff --git a/test/spec/components/index.js b/test/spec/components/index.js new file mode 100644 index 0000000..b7d8b0d --- /dev/null +++ b/test/spec/components/index.js @@ -0,0 +1,44 @@ +'use strict'; + +var assert = require('chai').assert; +var Joi = require('joi'); +var _ = require('../../../src/scripts/vendor/minified')._; +var HTML = require('../../../src/scripts/vendor/minified').HTML; +var components = require('../../../src/scripts/components'); +var manipulators = require('../../../src/scripts/lib/manipulators'); +var fixture = require('../../fixture'); + +var componentSchema = Joi.object().keys({ + name: Joi.string().required(), + template: Joi.string().required(), + style: Joi.string().optional(), + manipulator: Joi.alternatives().try( + Joi.string().valid(Object.keys(manipulators)), + Joi.object().keys({ + get: Joi.func().arity(0).required(), + set: Joi.func().arity(1).required() + }).required().unknown(true) + ), + defaults: Joi.object().optional(), + initialize: Joi.func().optional() +}).unknown(true); + +describe('components', function() { + _.eachObj(components, function(name, component) { + describe(name, function() { + it('has the correct structure', function() { + Joi.assert(component, componentSchema); + }); + + it('has all the necessary defaults', function() { + assert.doesNotThrow(function() { + HTML(component.template.trim(), component.defaults); + }); + }); + + it('is able to be passed to ClayConfig', function() { + fixture.clayConfig([component.name]); + }); + }); + }); +}); diff --git a/test/spec/lib/clay-config.js b/test/spec/lib/clay-config.js index e551a2b..ebdce17 100644 --- a/test/spec/lib/clay-config.js +++ b/test/spec/lib/clay-config.js @@ -1,6 +1,7 @@ 'use strict'; var assert = require('chai').assert; +var sinon = require('sinon'); var _ = require('../../../src/scripts/vendor/minified')._; var selectComponent = require('../../../src/scripts/components/select'); var componentRegistry = require('../../../src/scripts/lib/component-registry'); @@ -33,7 +34,7 @@ describe('ClayConfig', function() { 'getSettings' ].forEach(function(method) { it('.' + method + '()', function() { - var clayConfig = fixtures.clayConfig(['input', 'text'], true); + var clayConfig = fixtures.clayConfig(['input', 'text'], false); assert.throws(clayConfig[method], new RegExp(method)); }); }); @@ -93,7 +94,8 @@ describe('ClayConfig', function() { ]}, {type: 'toggle', appKey: 'test3'} ], - false, + true, + true, { test1: 'val-1' // set one of the values via settings } @@ -115,7 +117,7 @@ describe('ClayConfig', function() { function(done) { delete componentRegistry.select; assert.typeOf(componentRegistry.select, 'undefined'); - var clayConfig = fixtures.clayConfig(['select'], true); + var clayConfig = fixtures.clayConfig(['select'], false, false); clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { clayConfig.registerComponent(selectComponent); @@ -133,7 +135,8 @@ describe('ClayConfig', function() { it('throws if manipulator is a string and does not match built-in manipulator', function(done) { - var clayConfig = fixtures.clayConfig(['select'], true); + delete componentRegistry.select; + var clayConfig = fixtures.clayConfig(['select'], false, false); var _textComponent = _.copyObj(selectComponent); _textComponent.manipulator = 'not_real'; @@ -149,7 +152,8 @@ describe('ClayConfig', function() { it('throws if manipulator does not have a `get` and `set` method', function(done) { - var clayConfig = fixtures.clayConfig(['select'], true); + delete componentRegistry.select; + var clayConfig = fixtures.clayConfig(['select'], false, false); var _selectComponent = _.copyObj(selectComponent); _selectComponent.manipulator = {}; @@ -162,11 +166,31 @@ describe('ClayConfig', function() { clayConfig.build(); }); + + it('only registers the component once', function() { + delete componentRegistry.select; + var warnStub = sinon.stub(console, 'warn'); + var clayConfig = fixtures.clayConfig(['select'], false, false); + var _selectComponent1 = _.copyObj(selectComponent); + var _selectComponent2 = _.copyObj(selectComponent); + _selectComponent2.template = 'fake'; + + assert.strictEqual(clayConfig.registerComponent(_selectComponent1), true); + var styleCount = document.head.querySelectorAll('style').length; + assert.strictEqual(clayConfig.registerComponent(_selectComponent2), false); + + // make sure the styles were not added twice + assert.strictEqual(document.head.querySelectorAll('style').length, styleCount); + + // it should throw a warning + assert.strictEqual(warnStub.callCount, 1, 'console.warn not called once'); + warnStub.restore(); + }); }); describe('.build()', function() { it('dispatches the BEFORE_BUILD event at the right time', function(done) { - var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], true); + var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], false); clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { // this should throw because the config has not been built yet @@ -177,7 +201,7 @@ describe('ClayConfig', function() { }); it('dispatches the AFTER_BUILD event at the right time', function(done) { - var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], true); + var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], false); clayConfig.on(clayConfig.EVENTS.AFTER_BUILD, function() { // this should not throw because the config has been built diff --git a/test/spec/lib/clay-item.js b/test/spec/lib/clay-item.js index 39f1de4..afd5995 100644 --- a/test/spec/lib/clay-item.js +++ b/test/spec/lib/clay-item.js @@ -5,8 +5,7 @@ var sinon = require('sinon'); var checkReadOnly = require('../../test-utils').checkReadOnly; var ClayItem = require('../../../src/scripts/lib/clay-item'); var minified = require('../../../src/scripts/vendor/minified'); -var clayItemFixture = require('../../fixture').clayItem; -var configItemFixture = require('../../fixture').configItem; +var fixture = require('../../fixture'); var componentRegistry = require('../../../src/scripts/lib/component-registry'); describe('ClayItem', function() { @@ -22,20 +21,20 @@ describe('ClayItem', function() { 'trigger', 'initialize' ]; - var clayItem = clayItemFixture('input'); + var clayItem = fixture.clayItem('input'); checkReadOnly(clayItem, properties); }); it('attaches the manipulator methods', function() { Object.keys(componentRegistry).forEach(function(itemName) { - var clayItem = clayItemFixture(itemName); + var clayItem = fixture.clayItem(itemName); var manipulator = componentRegistry[itemName].manipulator; checkReadOnly(clayItem, Object.keys(manipulator)); }); }); it('throws if a component is not in the registry', function() { - var config = configItemFixture('fake'); + var config = fixture.configItem('fake', false); /* eslint-disable no-new */ assert.throws(function() { new ClayItem(config); }, /fake/); /* eslint-enable no-new */ @@ -43,33 +42,33 @@ describe('ClayItem', function() { describe('.id', function() { it('sets id if config has id', function() { - var config = configItemFixture('input'); + var config = fixture.configItem('input'); var clayItem = new ClayItem(config); assert.strictEqual(clayItem.id, config.id); }); it('sets id to null if there is no id in the config', function() { - var clayItem = clayItemFixture({type: 'input', id: undefined}); + var clayItem = fixture.clayItem({type: 'input', id: undefined}); assert.strictEqual(clayItem.id, null); }); }); describe('.appKey', function() { it('sets appKey correctly', function() { - var config = configItemFixture('input'); + var config = fixture.configItem('input'); var clayItem = new ClayItem(config); assert.strictEqual(clayItem.appKey, config.appKey); }); it('sets appKey to null if there is no appKey in the config', function() { - var clayItem = clayItemFixture({type: 'input', appKey: undefined}); + var clayItem = fixture.clayItem({type: 'input', appKey: undefined}); assert.strictEqual(clayItem.appKey, null); }); }); describe('.config', function() { it('sets appKey correctly', function() { - var config = configItemFixture('input'); + var config = fixture.configItem('input'); var clayItem = new ClayItem(config); assert.strictEqual(clayItem.appKey, config.appKey); }); @@ -77,7 +76,7 @@ describe('ClayItem', function() { describe('.$element', function() { it('sets $element correctly', function() { - var clayItem = clayItemFixture('input'); + var clayItem = fixture.clayItem('input'); assert.strictEqual(clayItem.$element[0].tagName, 'LABEL'); }); }); @@ -85,11 +84,11 @@ describe('ClayItem', function() { describe('.$manipulatorTarget', function() { it('sets the $manipulatorTarget to the root element if there are no children', function() { - var clayItem = clayItemFixture('footer'); + var clayItem = fixture.clayItem('footer'); assert.strictEqual(clayItem.$manipulatorTarget, clayItem.$element); }); it('sets the $manipulatorTarget to the correct child element', function() { - var clayItem = clayItemFixture('input'); + var clayItem = fixture.clayItem('input'); assert.strictEqual(clayItem.$manipulatorTarget[0].tagName, 'INPUT'); }); }); @@ -97,19 +96,19 @@ describe('ClayItem', function() { describe('.initialize()', function() { it('calls component initializer with the ClayItem as context', function() { var initializeSpy = sinon.spy(componentRegistry.select, 'initialize'); - var clayItem = clayItemFixture('select').initialize(); + var clayItem = fixture.clayItem('select').initialize(); assert(initializeSpy.alwaysCalledOn(clayItem)); assert(initializeSpy.alwaysCalledWith(minified)); initializeSpy.restore(); }); it('returns itself for chaining', function() { - var clayItem = clayItemFixture('select'); + var clayItem = fixture.clayItem('select'); assert.strictEqual(clayItem.initialize(), clayItem); }); it('does nothing if there is no initialize function', function() { - assert.doesNotThrow(clayItemFixture('input').initialize); + assert.doesNotThrow(fixture.clayItem('input').initialize); }); });