Home Reference Source

src/component.spec.js

/*eslint-env node, jasmine*//*global module, inject*/
import angular from 'angular';
import 'angular-mocks';

import {
  Component,
  ComponentAnnotation,
  Inject,
  Flag,
  Binding,
  Event,
  ComponentEvent,
  View,
  Annotations
} from 'anglue/anglue';

describe('Components', () => {
  // Clear the AnnotationCache for unit tests to ensure we create new annotations for each class.
  beforeEach(() => {
    Annotations.clear();
  });

  describe('@Component() decorator', () => {
    it('should create a component annotation', () => {
      @Component() class SimpleComponent {}
      expect(SimpleComponent.annotation)
        .toEqual(jasmine.any(ComponentAnnotation));
    });

    it('should leverage the class name by default as the component name', () => {
      @Component() class SimpleComponent {}
      expect(SimpleComponent.annotation.name)
        .toEqual('simple');
    });

    it('should be possible to pass the component name to the decorator', () => {
      @Component('renamed') class NamedComponent {}
      expect(NamedComponent.annotation.name)
        .toEqual('renamed');
    });

    it('should be possible to pass a config with the component name to the decorator', () => {
      @Component({name: 'renamed'}) class NamedComponent {}
      expect(NamedComponent.annotation.name)
        .toEqual('renamed');
    });

    it('should be possible to pass a config with dependencies to the decorator', () => {
      @Component({dependencies: ['dependency']}) class DependentComponent {}
      expect(DependentComponent.annotation.dependencies)
        .toEqual(['dependency']);
    });
  });

  describe('ComponentAnnotation', () => {
    it('should set the angular module name correctly', () => {
      @Component() class SimpleComponent {}
      expect(SimpleComponent.annotation.module.name)
        .toEqual('components.simple');
    });

    it('should set the angular module dependencies correctly', () => {
      @Component({dependencies: ['dependency']}) class DependentComponent {}
      expect(DependentComponent.annotation.module.requires)
        .toEqual(['dependency']);
    });
  });

  describe('directive', () => {
    @Component()
    @View({templateUrl: '/someUrl.html'})
    class TemplateUrlComponent {}

    @Component()
    @View({
      template: '<is-replaced></is-replaced>',
      replace: true,
      components: []
    })
    class ReplaceComponent {}

    @Component()
    @View({
      template: '[child]'
    })
    class ChildComponent {
      static get bindings() {
        return {
          isCompatible: 'isCompatible'
        };
      }
    }

    @Component()
    @View({
      template: `{{complex.myProperty}}<child ng-if="complex.showChild"></child>`,
      components: [ChildComponent]
    })
    class ComplexComponent {
      @Inject() $timeout;

      @Flag() fooFlag;
      @Flag('renamedFlag') barFlag;

      @Binding() fooBinding;
      @Binding('renamedBinding') barBinding;
      @Binding({attribute: 'configRenamed'}) configBinding;
      @Binding({expression: true}) expressionBinding;
      @Binding({string: true}) stringBinding;

      @Event() fooEvent;

      myProperty = 'foobar';

      activate = jasmine.createSpy('activate');
      onDestroy = jasmine.createSpy('onDestroy');
    }

    angular.module('componentApp', [
      ComplexComponent.annotation.module.name,
      TemplateUrlComponent.annotation.module.name,
      ReplaceComponent.annotation.module.name
    ]);

    let $compile, $rootScope, $timeout;
    beforeEach(module('componentApp'));
    beforeEach(inject((_$compile_, _$rootScope_, _$timeout_) => {
      $compile = _$compile_;
      $rootScope = _$rootScope_;
      $timeout = _$timeout_;
    }));

    describe('View', () => {
      it('should set the components static template getter url property', () => {
        expect(TemplateUrlComponent.template).toEqual(jasmine.objectContaining({
          url: '/someUrl.html'
        }));
      });

      it('should not replace the view element by default', () => {
        const el = compileTemplate('<complex></complex>', $compile, $rootScope);
        expect(el[0].tagName.toLowerCase()).toEqual('complex');
      });

      it('should replace the view element if replace is set to true on the view', () => {
        const el = compileTemplate('<replace></replace>', $compile, $rootScope);
        expect(el[0].tagName.toLowerCase()).toEqual('is-replaced');
      });

      it('should expose properties namespaced to the component template', () => {
        const el = compileTemplate('<complex></complex>', $compile, $rootScope);
        expect(el.text()).toEqual('foobar');
      });

      it('should support specifying child components', () => {
        const el = compileTemplate('<complex></complex>', $compile, $rootScope);
        const ctrl = el.controller('complex');

        ctrl.showChild = true;
        $rootScope.$digest();

        expect(el.text()).toEqual('foobar[child]');
      });
    });

    describe('Lifecycle', () => {
      it('should create an instance of our class as the directives controller', () => {
        expect(compileTemplate('<complex></complex>', $compile, $rootScope)
          .controller('complex'))
          .toEqual(jasmine.any(ComplexComponent));
      });

      it('should call the activate method', () => {
        expect(compileTemplate('<complex></complex>', $compile, $rootScope)
          .controller('complex').activate)
          .toHaveBeenCalled();
      });

      it('should inject into the component', () => {
        expect(compileTemplate('<complex></complex>', $compile, $rootScope)
          .controller('complex').$timeout)
          .toBe($timeout);
      });

      it('should call the onDestroy method', () => {
        const el = compileTemplate('<complex></complex>', $compile, $rootScope);
        $rootScope.$destroy();
        expect(el.controller('complex').onDestroy).toHaveBeenCalled();
      });
    });

    describe('Flags', () => {
      it('should set static flag and binding getter values', () => {
        expect(ComplexComponent.flags).toEqual(jasmine.objectContaining({
          fooFlag: 'fooFlag'
        }));
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          _fooFlagFlag: '@fooFlag'
        }));
      });

      it('should support manually setting the incoming attribute name', () => {
        expect(ComplexComponent.flags).toEqual(jasmine.objectContaining({
          barFlag: 'renamedFlag'
        }));
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          _barFlagFlag: '@renamedFlag'
        }));
      });

      it('should set a flag to false if its not defined', () => {
        expect(compileTemplate('<complex></complex>', $compile, $rootScope)
          .controller('complex').fooFlag)
          .toEqual(false);
      });

      it('should set a flag to false if set to the string false', () => {
        expect(compileTemplate('<complex foo-flag="false"></complex>', $compile, $rootScope)
          .controller('complex').fooFlag)
          .toEqual(false);
      });

      it('should set a flag to true if its defined', () => {
        expect(compileTemplate('<complex foo-flag></complex>', $compile, $rootScope)
          .controller('complex').fooFlag)
          .toEqual(true);
      });

      it('should set renamed flags properly', () => {
        expect(compileTemplate('<complex renamed-flag></complex>', $compile, $rootScope)
          .controller('complex').barFlag)
          .toEqual(true);
      });

      it('should update the flag value when the binding changes', () => {
        const el = compileTemplate('<complex foo-flag="{{flagInput}}"></complex>', $compile, $rootScope);
        const ctrl = el.controller('complex');

        $rootScope.flagInput = false;
        $rootScope.$digest();
        expect(ctrl.fooFlag).toBe(false);

        $rootScope.flagInput = true;
        $rootScope.$digest();
        expect(ctrl.fooFlag).toBe(true);
      });
    });

    describe('Events', () => {
      it('should set static event and binding getter values', () => {
        expect(ComplexComponent.events).toEqual({onFooEvent: 'fooEvent'});
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          _fooEventExpression: '&onFooEvent'
        }));
      });

      it('should create a ComponentEvent instance as the property', () => {
        expect(compileTemplate('<complex></complex>', $compile, $rootScope)
          .controller('complex').fooEvent)
          .toEqual(jasmine.any(ComponentEvent));
      });

      it('should call the expression when the event is fired and expose locals', () => {
        const el = compileTemplate('<complex on-foo-event="callExpression($foo)"></complex>',
          $compile, $rootScope);

        $rootScope.callExpression = jasmine.createSpy('callExpression');
        el.controller('complex').fooEvent.fire({$foo: 'foo'});
        expect($rootScope.callExpression).toHaveBeenCalledWith('foo');
      });

      it('should be backwards compatible and support fireComponentEvent() as well', () => {
        const el = compileTemplate('<complex on-foo-event="callExpression($bar)"></complex>',
          $compile, $rootScope);

        $rootScope.callExpression = jasmine.createSpy('backwardsCompatibleCallExpression');
        el.controller('complex').fireComponentEvent('fooEvent', {$bar: 'bar'});
        expect($rootScope.callExpression).toHaveBeenCalledWith('bar');
      });
    });

    describe('Bindings', () => {
      it('should set static bindings getter values', () => {
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          fooBinding: '=fooBinding'
        }));
      });

      it('should support manually setting the incoming attribute name as a string', () => {
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          barBinding: '=renamedBinding'
        }));
      });

      it('should support manually setting the incoming attribute name in a config', () => {
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          configBinding: '=configRenamed'
        }));
      });

      it('should setting an expression binding by using the config', () => {
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          expressionBinding: '&expressionBinding'
        }));
      });

      it('should setting an string binding by using the config', () => {
        expect(ComplexComponent.bindings).toEqual(jasmine.objectContaining({
          stringBinding: '@stringBinding'
        }));
      });

      it('should have backwards compatibility for manual static bindings getter', () => {
        expect(ChildComponent.bindings).toEqual(jasmine.objectContaining({
          isCompatible: 'isCompatible'
        }));
      });

      it('should make it a two-way binding by default in backwards compatibility bindings', () => {
        const el = compileTemplate('<child is-compatible="foo.bar"></child>',
          $compile, $rootScope);
        const ctrl = el.controller('child');

        $rootScope.foo = {bar: 'foo'};
        $rootScope.$digest();

        expect(ctrl.isCompatible).toEqual('foo');

        ctrl.isCompatible = 'bar';
        $rootScope.$digest();

        expect($rootScope.foo.bar).toEqual('bar');
      });
    });
  });
});


function compileTemplate(template, $compile, $rootScope) {
  const el = angular.element(template.trim());
  $compile(el)($rootScope.$new());
  $rootScope.$digest();
  return el;
}