hlfcoding/hlf-dom-extensions

View on GitHub
tests/js/core.js

Summary

Maintainability
A
0 mins
Test Coverage
//
// HLF Extensions Core Tests
// =========================
// Offloads testing larger core components to other test modules.
//
// [Page](../../../tests/core.unit.html)
//
(function() {
  'use strict';
  if (window.guard && !guard.isNavigatorSupported) { return; }

  require.config({ baseUrl: '../', paths: { hlf: 'dist', test: 'tests/js' } });
  define(['hlf/core', 'test/base'], function(HLF, base) {
    const { module, test } = QUnit;

    // ---

    module('other');

    test('exports', function(assert) {
      assert.ok(HLF, 'Namespace should exist.');
      assert.ok(HLF.buildExtension, 'Method should exist.');
    });

    // ---

    module('extension core', {
      beforeEach(assert) {
        Object.assign(this, {
          assertInstanceMethods(instance, ...methodNames) {
            methodNames.forEach(methodName => assert.ok(
              typeof instance[methodName] === 'function',
              `Instance has generated API addition ${methodName}.`
            ));
          },
          buildTestExtension(extensionClass, options) {
            options = Object.assign({}, options);
            HLF.buildExtension(extensionClass, options);
            this.SomeExtension = extensionClass;
          },
          createSomeChildElement() {
            let someElement = document.createElement('div');
            someElement.classList.add('foo');
            this.someElement.appendChild(someElement);
            return someElement;
          },
          createTestExtensionClass(additions, defaults) {
            const { methods, onNew, staticMethods } = additions || {};
            class SomeExtension {
              constructor(element, options, contextElement) {
                if (onNew) { onNew.apply(this, arguments); }
              }
            }
            Object.assign(SomeExtension.prototype, methods);
            Object.assign(SomeExtension, staticMethods, {
              debug: false,
              defaults: Object.assign({}, defaults),
              toPrefix() { return 'se'; },
            });
            return SomeExtension;
          },
          createTestExtension({ classAdditions, createOptions, defaults } = {}) {
            createOptions = Object.assign({}, {
              name: 'someExtension',
              namespace: {
                debug: false,
                toString() { return 'se'; },
                defaults: Object.assign({}, defaults),
              },
              apiClass: this.createTestExtensionClass(classAdditions, defaults),
            }, createOptions);
            Object.assign(this, {
              extension: HLF.createExtension(createOptions),
              namespace: createOptions.namespace,
            });
          },
          someData: { key: 'value' },
          someElement: document.createElement('div'),
          someExtension: null,
        });
        document.getElementById('qunit-fixture').appendChild(this.someElement);
      },
      afterEach() {
        if (typeof this.someExtension === 'function') {
          this.someExtension('remove');
        }
        if (this.extension) {
          this.extension._deleteInstances();
        }
      },
    });

    test('initializers', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass({
        methods: { init() { this._didInit = true; } },
        staticMethods: { init() { this._didInit = true; } },
      }));
      this.someExtension = this.SomeExtension.extend(this.someElement);
      const { someExtension, SomeExtension } = this;
      assert.ok(someExtension._didInit,
        'Instance had initializer called.');
      assert.ok(SomeExtension._didInit,
        'Extension class had initializer called.');
      assert.strictEqual(someExtension.element, this.someElement,
        'Extension stores the element as property.');
      assert.strictEqual(someExtension.rootElement, someExtension.element,
        'Extension sets the element as root element.');
      assert.ok(someExtension.element.classList.contains('js-se'),
        'Extension gives the element the main class.');
    });

    test('de-initializers', function(assert) {
      let didDeinit = false;
      let didReceiveEvent = false;
      this.buildTestExtension(this.createTestExtensionClass({
        onNew() {
          this.eventListeners = {};
          this.eventListeners[this.eventName('someevent')] = this._onSomeEvent;
          this.resizeDelay = 100;
        },
        methods: {
          deinit() { didDeinit = true; },
          _onSomeEvent(event) { didReceiveEvent = true; },
          _onWindowResize(event) { didReceiveEvent = true; },
        },
      }), { autoListen: true });
      this.someExtension = this.SomeExtension.extend(this.someElement);
      this.someExtension.remove();
      assert.ok(didDeinit,
        'Extension performs "remove" action, calls custom deinit, if any.');
      this.someElement.dispatchEvent(new CustomEvent('sesomeevent'));
      window.dispatchEvent(new Event('resize'));
      assert.notOk(didReceiveEvent,
        'Extension automatically removes listeners from autoListen.');
      this.someExtension = null;
    });

    test('compacted options', function(assert) {
      const defaultSelectors = { someElement: '.foo' };
      const optionSelectors = { someOtherElement: '.bar' };
      this.buildTestExtension(this.createTestExtensionClass(
        null, { classNames: {}, selectors: defaultSelectors }
      ), { compactOptions: true });
      this.someExtension = this.SomeExtension.extend(this.someElement, { selectors: optionSelectors });
      assert.notOk(this.someExtension.options,
        'Extension no longer stores the final options as property.');
      assert.deepEqual(this.someExtension.classNames, this.SomeExtension.defaults.classNames,
        'Extension stores the classNames option as property.');
      assert.deepEqual(this.someExtension.selectors, Object.assign({}, defaultSelectors, optionSelectors),
        'Extension stores the selectors option as property.');
      const configureSelectors = { someOtherElement: '.baz' };
      this.someExtension.configure({ selectors: configureSelectors });
      assert.deepEqual(this.someExtension.selectors,
        Object.assign({}, defaultSelectors, optionSelectors, configureSelectors),
        'Extension performs "configure" action, compacts new options.');
      this.someExtension.configure({ selectors: 'default' });
      assert.deepEqual(this.someExtension.selectors, defaultSelectors,
        'Extension performs "configure" action, restores default options.');
    });

    test('custom options', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass(), { compactOptions: true });
      const options = { someOption: 'bar' };
      this.someElement.setAttribute('data-se', JSON.stringify(options));
      this.someExtension = this.SomeExtension.extend(this.someElement);
      assert.equal(this.someExtension.someOption, options.someOption,
        'Extension allows custom options via element data attribute.');
    });

    test('naming methods', function(assert) {
      const methodNames = ['attrName', 'className', 'eventName', 'varName'];
      this.buildTestExtension(this.createTestExtensionClass());
      this.someExtension = this.SomeExtension.extend(this.someElement);
      this.assertInstanceMethods(this.someExtension, ...methodNames);
      methodNames.forEach(methodName => assert.ok(
        typeof this.SomeExtension[methodName] === 'function',
        `Namespace has generated API addition ${methodName}.`
      ));
      assert.equal(this.someExtension.attrName('some-attribute'), 'data-se-some-attribute',
        'attrName namespaces attribute names correctly.');
      assert.equal(this.someExtension.className('some-class'), 'js-se-some-class',
        'className namespaces class names correctly.');
      assert.equal(this.someExtension.eventName('someevent'), 'sesomeevent',
        'eventName namespaces event names correctly.');
      assert.equal(this.someExtension.varName('some-var'), '--se-some-var',
        'varName namespaces CSS var names correctly.');
    });

    test('timing methods', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass());
      this.someExtension = this.SomeExtension.extend(this.someElement);
      let done = assert.async();
      const duration = 50;

      let elementTimeoutCompleted = false;
      this.someExtension.setElementTimeout(this.someElement, 'some-timeout', duration,
        () => { elementTimeoutCompleted = true; });
      assert.ok(this.someElement.hasAttribute('data-se-some-timeout'),
        'setElementTimeout stores timeout as element data attribute per name.');

      let timeoutCompleted = false;
      this.someExtension.setTimeout('_someTimeout', duration,
        () => { timeoutCompleted = true; });
      assert.ok(this.someExtension._someTimeout,
        'setTimeout stores timeout as property value per name.');

      let elementTimeoutCleared = true;
      this.someExtension.setElementTimeout(this.someElement, 'some-timeout-to-clear', duration,
        () => { elementTimeoutCleared = false; });
      this.someExtension.setElementTimeout(this.someElement, 'some-timeout-to-clear', null);
      assert.notOk(this.someElement.hasAttribute('data-se-some-timeout-to-clear'),
        'setElementTimeout clears timeout if value is null.');

      let timeoutCleared = true;
      this.someExtension.setTimeout('_someTimeoutToClear', duration,
        () => { timeoutCleared = false; });
      this.someExtension.setTimeout('_someTimeoutToClear', null);
      assert.notOk(this.someExtension._someTimeoutToClear,
        'setTimeout clears timeout if value is null.');

      setTimeout(() => {

        assert.ok(elementTimeoutCompleted, 'setElementTimeout calls callback.');
        assert.notOk(this.someElement.hasAttribute('data-se-some-timeout'),
          'setElementTimeout removes element data attribute upon completion.');

        assert.ok(timeoutCompleted, 'setTimeout calls callback.');
        assert.notOk(this.someExtension._someTimeout,
          'setTimeout removes property value upon completion.');

        assert.ok(elementTimeoutCleared,
          'setElementTimeout clears timeout if value is null.');
        assert.ok(timeoutCleared,
          'setTimeout clears timeout if value is null.');

        done();
      }, duration + 50);
    });

    test('css methods', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass(), { mixinNames: ['css'] });
      this.someExtension = this.SomeExtension.extend(this.someElement);
      let childElement = this.createSomeChildElement();

      this.someElement.style.setProperty('--se-some-size', '1px');
      assert.equal(this.someExtension.cssVariable('some-size'), '1px',
        'cssVariable returns variable value on root element by default.');
      childElement.style.setProperty('--se-some-size', '2px');
      assert.equal(this.someExtension.cssVariable('some-size', childElement), '2px',
        'cssVariable returns variable value on given element if any.');

      this.someElement.style.setProperty('transition-duration', '0.1s');
      assert.equal(this.someExtension.cssDuration('transition-duration'), 100,
        'cssDuration returns converted duration on root element by default.');
      childElement.style.setProperty('transition-duration', '0.2s');
      assert.equal(this.someExtension.cssDuration('transition-duration', childElement), 200,
        'cssDuration returns converted duration on given element if any.');
    });

    test('as shared instance', function(assert) {
      this.createSomeChildElement();
      this.buildTestExtension(this.createTestExtensionClass());
      let subject = (contextElement) => (contextElement.querySelectorAll('.foo'));
      this.someExtension = this.SomeExtension.extend(subject, { contextElement: this.someElement });
      assert.deepEqual(this.someExtension.elements, [...subject(this.someElement)],
        'Extension stores the elements as property.');
      assert.deepEqual(this.someExtension.options.querySelector(this.someElement), subject(this.someElement),
        'Extension stores custom query-selector subject as option.');
      assert.strictEqual(this.someExtension.contextElement, this.someElement,
        'Extension stores the context element as property.');
      assert.strictEqual(this.someExtension.rootElement, this.someExtension.contextElement,
        'Extension sets the context element as root element.');
      assert.ok(this.someExtension.contextElement.classList.contains('js-se'),
        'Extension gives the context element the main class.');
    });

    test('compactOptions option', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass(
        null, { someOption: 'foo' }
      ), { compactOptions: true });
      this.someExtension = this.SomeExtension.extend(this.someElement, { someOtherOption: 'bar' });
      assert.equal(this.someExtension.someOption, 'foo',
        'Instance has default options merged in as properties.');
      assert.equal(this.someExtension.someOtherOption, 'bar',
        'Instance has custom options merged in as properties.');
    });

    test('autoBind option', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass({
        methods: {
          _onSomeEvent() { this.someContext = this; }
        }
      }), { autoBind: true });
      this.someExtension = this.SomeExtension.extend(this.someElement);
      let { _onSomeEvent } = this.someExtension;
      _onSomeEvent();
      assert.strictEqual(this.someExtension.someContext, this.someExtension,
        'Instance has auto-bound methods.');
    });

    test('autoListen option', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass({
        onNew() {
          this.eventListeners = {};
          this.eventListeners[this.eventName('someevent')] = this._onSomeEvent;
          this.resizeDelay = 100;
          this._resizeCount = 0;
        },
        methods: {
          _onSomeEvent(event) { this._someEventDetail = event.detail; },
          _onWindowResize(event) { this._resizeCount += 1; },
        },
      }), { autoListen: true });
      this.someExtension = this.SomeExtension.extend(this.someElement);
      this.assertInstanceMethods(this.someExtension,
        'addEventListeners', 'removeEventListeners', 'toggleEventListeners',
        'createCustomEvent', 'dispatchCustomEvent');
      this.someExtension.dispatchCustomEvent('someevent', this.someData);
      assert.equal(this.someExtension._someEventDetail, this.someData,
        'Instance has auto-added auto-bound listeners based on eventListeners.');
      window.dispatchEvent(new Event('resize'));
      window.dispatchEvent(new Event('resize'));
      assert.equal(this.someExtension._resizeCount, 1,
        'Instance has auto-bound and throttled _onWindowResize listener.');
    });

    test('autoSelect option', function(assert) {
      this.buildTestExtension(this.createTestExtensionClass(
        null, { selectors: { someElement: '.foo', someElements: '.foo' } }
      ), { autoSelect: true });
      this.createSomeChildElement();
      this.someExtension = this.SomeExtension.extend(this.someElement);
      assert.ok(this.someExtension.someElement instanceof HTMLElement,
        'Instance has auto-selected sub element based on selectors option.');
      assert.ok(this.someExtension.someElements instanceof Array,
        'Instance has auto-selected sub elements based on selectors option.');
      this.assertInstanceMethods(this.someExtension,
        'selectByClass', 'selectAllByClass', 'selectToProperties');
    });

    // ---

    QUnit.start();
  });

}());