Home Reference Source

src/actions.spec.js

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

import {
  Actions,
  AsyncAction,
  Action,
  ActionsAnnotation,
  Inject,
  Annotations
} from 'anglue/anglue';

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

  describe('@Actions() decorator', () => {
    it('should create a actions annotation', () => {
      @Actions() class SimpleActions {}
      expect(SimpleActions.annotation)
        .toEqual(jasmine.any(ActionsAnnotation));
    });

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

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

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

    it('should be use the class name in the namespace by default', () => {
      @Actions() class NameSpacedActions {}
      expect(NameSpacedActions.actionNamespace)
        .toEqual('NAME_SPACED');
    });

    it('should be possible to pass a config with a namespace to the decorator', () => {
      @Actions({namespace: 'fooBar'}) class NamespacedActions {}
      expect(NamespacedActions.actionNamespace)
        .toEqual('FOO_BAR');
    });

    it('should be possible to set the namespace of a class false', () => {
      @Actions({namespace: false}) class NamespacedActions {}
      expect(NamespacedActions.actionNamespace)
        .toEqual(null);
    });

    it('should be possible to set the namespace of a class to an empty string', () => {
      @Actions({namespace: ''}) class NamespacedActions {}
      expect(NamespacedActions.actionNamespace)
        .toEqual(null);
    });

    it('should be possible to set the namespace of a class to null', () => {
      @Actions({namespace: null}) class NamespacedActions {}
      expect(NamespacedActions.actionNamespace)
        .toEqual(null);
    });
  });

  describe('ActionsAnnotation', () => {
    it('should set the angular module name correctly', () => {
      @Actions() class SimpleActions {}
      expect(SimpleActions.annotation.module.name)
        .toEqual('actions.simple');
    });

    it('should set the annotation service name correctly', () => {
      @Actions() class SimpleActions {}
      expect(SimpleActions.annotation.serviceName)
        .toEqual('SimpleActions');
    });
  });

  describe('@AsyncAction() decorator', () => {
    it('should add an action to the static class serviceActions getter', () => {
      @Actions() class AsyncActions {
        @AsyncAction() fooAction() {}

        @AsyncAction() barAction() {}
      }
      expect(AsyncActions.serviceActions)
        .toEqual({
          ASYNC_FOO_ACTION: 'fooAction',
          ASYNC_BAR_ACTION: 'barAction'
        });
    });

    it('should be possible to manually name the action of an async action', () => {
      @Actions() class AsyncActions {
        @AsyncAction('renamed') onSomeAction() {}
      }
      expect(AsyncActions.serviceActions)
        .toEqual({ASYNC_RENAMED: 'onSomeAction'});
    });
  });

  describe('instance', () => {
    let actions, $q, appDispatcher, $rootScope;
    let dispatcherDeferred, actionDeferred;

    @Actions() class ComplexActions {
      @Inject() $q;

      @Action() fooAction() {}

      @Action('NAMED') barAction() {
        return 'bar';
      }

      @AsyncAction() asyncFooAction() {
        return actionDeferred.promise;
      }
    }

    class MockApplicationDispatcher {
      dispatch() {}
    }

    angular
      .module('actionsApp', [
        'luxyflux',
        ComplexActions.annotation.module.name
      ])
      .service('ApplicationDispatcher', [
        function () {
          return new MockApplicationDispatcher();
        }
      ]);

    beforeEach(module('actionsApp'));
    beforeEach(inject((_ComplexActions_, _$q_, _$rootScope_, _ApplicationDispatcher_) => {
      actions = _ComplexActions_;
      appDispatcher = _ApplicationDispatcher_;
      $q = _$q_;
      $rootScope = _$rootScope_;

      dispatcherDeferred = $q.defer();
      dispatcherDeferred.resolve('resolvedFoo');
      spyOn(appDispatcher, 'dispatch')
        .and.returnValue(dispatcherDeferred.promise);

      actionDeferred = $q.defer();
    }));

    it('should inject into the actions', () => {
      expect(actions.$q).toBe($q);
    });

    it('should dispatch its actions on the ApplicationDispatcher', () => {
      actions.dispatch();
      expect(appDispatcher.dispatch).toHaveBeenCalled();
    });

    describe('actions', () => {
      it('should dispatch an action', () => {
        actions.fooAction();
        expect(appDispatcher.dispatch)
          .toHaveBeenCalledWith('COMPLEX_FOO_ACTION');
      });

      it('should dispatch a manually named action', () => {
        actions.barAction();
        expect(appDispatcher.dispatch)
          .toHaveBeenCalledWith('COMPLEX_NAMED');
      });

      it('should return the dispatch promise by default', () => {
        expect(actions.fooAction())
          .toEqual(dispatcherDeferred.promise);
      });

      it('should return a manually returned value in the action', () => {
        expect(actions.barAction())
          .toEqual('bar');
      });
    });

    describe('async actions', () => {
      it('should dispatch a started action', () => {
        actions.asyncFooAction();
        expect(appDispatcher.dispatch)
          .toHaveBeenCalledWith('COMPLEX_ASYNC_FOO_ACTION_STARTED');
      });

      it('should return a $q promise', () => {
        expect(actions.asyncFooAction()).toEqual($q.defer().promise);
      });

      it('should dispatch a COMPLETED action with the promise result when resolved', () => {
        actions.asyncFooAction().finally(() => {
          expect(appDispatcher.dispatch.calls.mostRecent().args)
            .toEqual(['COMPLEX_ASYNC_FOO_ACTION_COMPLETED', 'result']);
        });

        actionDeferred.resolve('result');
        $rootScope.$digest();
      });

      it('should dispatch a FAILED action with the promise result when resolved', () => {
        actions.asyncFooAction().finally(() => {
          expect(appDispatcher.dispatch.calls.mostRecent().args)
            .toEqual(['COMPLEX_ASYNC_FOO_ACTION_FAILED', 'error']);
        });

        actionDeferred.reject('error');
        $rootScope.$digest();
      });
    });
  });
});