test_browser/components/c_facets/Css_test.js

Summary

Maintainability
B
5 hrs
Test Coverage
'use strict';

/* eslint-env browser, commonjs, node, mocha */

var assert = require('assert')
    , async = require('async');

describe('Css facet', function() {
    milo.config.check = true; // Enable 'check' library so that inputs to the Css facet are validated

    var ComponentClass = milo.createComponentClass({
        className: 'CssComponent',
        facets: {
            model: undefined,
            css: {
                binding: {
                    facetName: 'model'
                },
                classes: {
                    // Used for simple tests
                    '.modelPath1': 'css-class-1',

                    // Used for object value lookup tests
                    '.modelPath2': {
                        'black': 'black-css-class',
                        'red': 'red-css-class',
                        'orange': '$-css-class'
                    },

                    // Used for function tests
                    '.modelPath3': function(data) {
                        return data ? data + '-class' : null;
                    },

                    // Used for template tests
                    '.modelPath4': '$-class',

                    // Used to test nested props
                    '.nested.property': 'nested-example'
                }
            }
        }
    });

    var component;
    var dataSource;
    var connector;

    beforeEach(function() {
        component = ComponentClass.createOnElement();
        dataSource = new milo.Model();
        connector && milo.minder.destroyConnector(connector);
        connector = milo.minder(dataSource, '->>>>>>', component.css);
    });

    it('should apply css class regardless of model path structure', function (done) {
        runTests.call(this, done, [
            test('.nested.property', true, ['nested-example']), // Add class
            test('.nested.property', false, []), // Remove class
            test('.nested.property', {}, ['nested-example']), // Add class (truthy value, not boolean true)
            test('.nested.property', '', []) // Remove class (falsey value, not boolean false)
        ]);
    });

    it('should apply css classes based on truthy values', function(done) {
        runTests.call(this, done, [
            test('.modelPath1', true, ['css-class-1']), // Add class
            test('.modelPath1', false, []), // Remove class
            test('.modelPath1', {}, ['css-class-1']), // Add class (truthy value, not boolean true)
            test('.modelPath1', '', []) // Remove class (falsey value, not boolean false)
        ]);
    });

    it('should setup minder connection based on binding config', function(done) {
        dataSource = component.model.m;
        runTests.call(this, almostDone, [
            test('.modelPath1', true, ['css-class-1']), // Add class
            test('.modelPath1', false, []), // Remove class
            test('.modelPath1', {}, ['css-class-1']), // Add class (truthy value, not boolean true)
            test('.modelPath1', '', []) // Remove class (falsey value, not boolean false)
        ]);

        function almostDone() {
            assert.deepEqual(component.model.m.get(), {'modelPath1':''});
            done();
        }
    });

    it('should apply css classes to element supplied with getClassList', function(done) {
        var TestClass = milo.createComponentClass({
            className: 'TestClass',
            facets: {
                css: {
                    getClassList: function () {return this.owner.el.querySelector('.inner').classList;},
                    classes: {'.test': 'test'}
                }
            }
        });

        var comp = TestClass.createOnElement(null, '<strong class="inner"></strong>');
        var m = new milo.Model();

        milo.minder(m, '->>', comp.css);

        comp.css.once('changed', function() {
            var innerClassList = comp.el.querySelector('.inner').classList;
            assert(innerClassList.contains('test'));
            done();
        });
        m('.test').set(true);
    });

    it('should apply classes based on model values in a lookup table', function(done) {
        runTests.call(this, done, [
            test('.modelPath2', 'black', ['black-css-class']), // Add
            test('.modelPath2', 'red', ['red-css-class']), // Replace
            test('.modelPath2', 'orange', ['orange-css-class']), // Replace (and is templated)
            test('.modelPath2', null, []), // Remove
            test('.modelPath2', 'green', []) // Not in lookup
        ]);
    });

    it('should apply classes based on the result of function calls', function(done) {
        runTests.call(this, done, [
            test('.modelPath3', 'apple', ['apple-class']), // Add
            test('.modelPath3', 'banana', ['banana-class']), // Replace
            test('.modelPath3', null, []), // Remove
        ]);
    });

    it('should template class names', function(done) {
        runTests.call(this, done, [
            test('.modelPath4', 'dog', ['dog-class']), // Add
            test('.modelPath4', 'cat', ['cat-class']), // Replace
            test('.modelPath4', null, [])
        ]);
    });

    it('should only remove classes when no other model value is applying the same class', function(done) {
        runTests.call(this, done, [
            test('.modelPath2', 'black', ['black-css-class']), // Add
            test('.modelPath3', 'black-css', ['black-css-class']), // Add same class (different model path)
            test('.modelPath4', 'black-css', ['black-css-class']), // Add same class (different model path
            test('.modelPath3', null, ['black-css-class']), // Null model value (class still applied due to other model values)
            test('.modelPath4', null, ['black-css-class']), // Null model value (class still applied due to other model values)
            test('.modelPath2', null, []) // Finally removed as no other model values result in the class being applied
        ]);
    });

    it('should allow model data to be set directly', function(done) {
        // Set directly
        component.css.set({
            '.modelPath1': true,
            '.modelPath2': 'red',
            '.modelPath3': 'pear',
            '.modelPath4': 'pig'
        });

        assertCssExists('css-class-1'); // modelPath1
        assertCssExists('red-css-class'); // modelPath2
        assertCssExists('pear-class'); // modelPath3
        assertCssExists('pig-class'); // modelPath4

        // Set via milo.binder connection
        dataSource.set({
            modelPath1: true,
            modelPath2: 'black',
            modelPath3: 'lemon',
            modelPath4: 'bear'
        });

        component.css.onSync('changedata', function() {
            assertCssExists('css-class-1'); // modelPath1
            assertCssExists('black-css-class'); // modelPath2
            assertCssExists('lemon-class'); // modelPath3
            assertCssExists('bear-class'); // modelPath4

            done();
        });

        function assertCssExists(className) {
            assert(component.el.classList.contains(className), 'Expected ' + className + ' css class to exist');
        }
    });

    it('should delete all classes when data is set to null/undefined', function() {
        testWith(null);
        testWith(undefined);

        function testWith(data) {
            component.css.set({
                '.modelPath1': true,
                '.modelPath2': 'red',
                '.modelPath3': 'pear',
                '.modelPath4': 'pig'
            });

            assertCssExists('css-class-1'); // modelPath1
            assertCssExists('red-css-class'); // modelPath2
            assertCssExists('pear-class'); // modelPath3
            assertCssExists('pig-class'); // modelPath4

            component.css.set(data);

            assert.equal(component.el.classList.length, 0, 'Expected all Css classes to have been removed');
        }

        function assertCssExists(className) {
            assert(component.el.classList.contains(className), 'Expected ' + className + ' css class to exist');
        }
    });

    it('should throw exception if supplied with invalid data', function() {
        // Valid inputs
        trySet({}, true);
        trySet(null, true);
        trySet(undefined, true);

        // Invalid inputs
        trySet(true, false);
        trySet(false, false);
        trySet('Hello world', false);
        trySet(1, false);

        function trySet(data, isValidInput) {
            var exceptionThrown = false;
            var message = (isValidInput ? 'Unexpected' : 'Expected') + ' exception when setting data type ' + typeof data;

            try {
               component.css.set(data);
            } catch(e) {
               exceptionThrown = true;
            }

            assert(isValidInput != exceptionThrown, message);
        }
    });

    function runTests(next, testSpecs) {
        this.timeout(10000);

        async.forEachSeries(testSpecs, runTest, next);

        function runTest(testSpec, next) {
            // Listen for the CSS facet to let us know it has updated the css classes
            component.css.onceSync('changed', onCssClassesChanged);

            // Update the model as per the test spec
            dataSource(testSpec.modelPath).set(testSpec.modelValue);

            function onCssClassesChanged(msg, data) {
                try {
                    assert.equal(testSpec.modelPath, data.modelPath);
                    assert.equal(testSpec.modelValue, data.modelValue);

                    var classList = component.el.classList;
                    var expectedClassList = testSpec.expectedCssClasses;

                    assert.equal(classList.length, expectedClassList.length,
                        'Class list mismatch.  Expected "' + expectedClassList.join(' ') + '" but got "' + classList.toString() + '"');

                    expectedClassList.forEach(function(cssClass) {
                        assert(classList.contains(cssClass),
                            'Missing expected class: ' + cssClass + '. ClassList was "' + classList.toString() + '"');
                    });

                    next();
                } catch(e) {
                    next(e);
                }
            }
        }
    }

    function test(modelPath, modelValue, expectedCssClasses) {
        return {
            modelPath: modelPath,
            modelValue: modelValue,
            expectedCssClasses: expectedCssClasses
        };
    }
});