Home Reference Source

src/behaviors/entity-store.spec.js

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

import {
  Store,
  EntityStore,
  EntityStoreBehavior,
  Handler,
  TransformableCollection,
  Annotations
} from 'anglue/anglue';

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

  describe('EntityStoreBehavior', () => {
    let mockInstance, behavior, refreshSpy;
    beforeEach(() => {
      refreshSpy = jasmine.createSpy('refresh');
      module(angular.module('test', []).name);
      inject(_$q_ => {
        mockInstance = {
          $q: _$q_,
          transformables: {items: {refresh: refreshSpy, data: []}},
          emit: jasmine.createSpy()
        };
        behavior = new EntityStoreBehavior(mockInstance);
      });
    });

    it('should set default values for its config', () => {
      expect(behavior.idProperty).toEqual('id');
    });

    it('should set read the passed config', () => {
      behavior = new EntityStoreBehavior(mockInstance, {
        idProperty: 'fooId'
      });
      expect(behavior.idProperty).toEqual('fooId');
    });

    it('should write to the transformable data', () => {
      expect(behavior.items).toBe(mockInstance.transformables.items.data);
    });

    it('should read from the transformable data', () => {
      const newData = ['test'];
      behavior.items = newData;
      expect(mockInstance.transformables.items.data).toBe(newData);
    });

    it('should not be in isEmpty state if store has not successfully read/loaded once', () => {
      behavior.onLoadStarted();
      expect(behavior.isEmpty).toBe(false);
    });

    it('should not be in isEmpty state if store has read/loaded once and has items', () => {
      behavior.onLoadStarted();
      behavior.onLoadCompleted(['foo', 'bar']);
      expect(behavior.isEmpty).toBe(false);
    });

    it('should be in isEmpty state when store has read/loaded once and has no items', () => {
      behavior.onLoadStarted();
      behavior.onLoadCompleted([]);
      expect(behavior.isEmpty).toBe(true);
    });

    it('should be in isBusy state when current in any action', () => {
      behavior.onLoadStarted();
      expect(behavior.isBusy).toBe(true);
    });

    it('should not be in isBusy state when not currently in any action', () => {
      behavior.onLoadStarted();
      behavior.onReadStarted();
      behavior.onLoadCompleted([]);
      expect(behavior.isBusy).toBe(true);

      behavior.onReadCompleted({});
      expect(behavior.isBusy).toBe(false);
    });

    describe('load', () => {
      beforeEach(() => {
        behavior.onLoadStarted();
      });

      it('should replace the existing items collection on LOAD_COMPLETED', () => {
        behavior.items = ['foo'];
        behavior.onLoadCompleted(['bar']);
        expect(behavior.items).toEqual(['bar']);
      });

      it('should refresh the items collection on LOAD_COMPLETED', () => {
        behavior.onLoadCompleted(['bar']);
        expect(refreshSpy).toHaveBeenCalled();
      });

      it('should emit the changed event on the store on LOAD_COMPLETED', () => {
        behavior.onLoadCompleted(['bar']);
        expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'load', ['bar']);
      });

      it('should emit the error event on the store on LOAD_FAILED', () => {
        behavior.onLoadFailed('foo error');
        expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'load', 'foo error');
      });

      it('should reset the hasDetailSet', () => {
        behavior.hasDetailSet.add('foo');
        behavior.onLoadCompleted(['bar']);
        expect(behavior.hasDetailSet.size).toBe(0);
      });

      it('should define a loadPromise on LOAD_STARTED', () => {
        expect(behavior.loadPromise).toEqual(mockInstance.$q.defer().promise);
      });

      it('should resolve the loadPromise with the results on LOAD_COMPLETED', () => {
        const loadDeferred = behavior.loadDeferred;
        spyOn(loadDeferred, 'resolve');
        behavior.onLoadCompleted(['bar']);
        expect(loadDeferred.resolve).toHaveBeenCalledWith(['bar']);
      });

      it('should reject the loadPromise with the errors on LOAD_FAILED', () => {
        const loadDeferred = behavior.loadDeferred;
        spyOn(loadDeferred, 'reject');
        behavior.onLoadFailed('error');
        expect(loadDeferred.reject).toHaveBeenCalledWith('error');
      });

      it('should be in isLoading state on LOAD_STARTED', () => {
        expect(behavior.isLoading).toBe(true);
      });

      it('should not be in isLoading state on LOAD_COMPLETED', () => {
        behavior.onLoadCompleted();
        expect(behavior.isLoading).toBe(false);
      });

      it('should not be in isLoading state on LOAD_FAILED', () => {
        behavior.onLoadFailed();
        expect(behavior.isLoading).toBe(false);
      });

      it('should not be in isSet state after LOAD_STARTED', () => {
        expect(behavior.isSet).toBe(false);
      });

      it('should not be in isSet state after LOAD_FAILED', () => {
        behavior.onLoadFailed();
        expect(behavior.isSet).toBe(false);
      });

      it('should be in isSet state after LOAD_COMPLETED', () => {
        behavior.onLoadCompleted();
        expect(behavior.isSet).toBe(true);
      });

      it('should stay in isSet state after doing another load', () => {
        behavior.onLoadCompleted();
        behavior.onLoadStarted();
        behavior.onLoadFailed();
        expect(behavior.isSet).toBe(true);
      });
    });

    describe('create', () => {
      let entity;
      beforeEach(() => {
        entity = {id: 1, foo: 'bar'};
        behavior.onCreateStarted();
      });

      it('should add the entity to the collection on CREATE_COMPLETED', () => {
        behavior.onCreateCompleted(entity);
        expect(behavior.items).toEqual(jasmine.arrayContaining([entity]));
      });

      it('should apply to the entity to the collection on CREATE_COMPLETED if it exists', () => {
        const currentEntity = {id: 1, foo: 'old'};
        behavior.items.push(currentEntity);
        behavior.onCreateCompleted(entity);
        expect(currentEntity.foo).toEqual('bar');
      });

      it('should add itself to hasDetailSet on CREATE_COMPLETED', () => {
        behavior.onCreateCompleted({id: 1});
        expect(behavior.hasDetailSet.has(1)).toBe(true);
      });

      it('should emit the changed event on the store on CREATE_COMPLETED', () => {
        behavior.onCreateCompleted(entity);
        expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'create', entity);
      });

      it('should refresh the items collection on CREATE_COMPLETED', () => {
        behavior.onCreateCompleted(entity);
        expect(refreshSpy).toHaveBeenCalled();
      });

      it('should emit the error event on the store on CREATE_FAILED', () => {
        behavior.onCreateFailed('foo error');
        expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'create', 'foo error');
      });

      it('should define a createPromise on CREATE_STARTED', () => {
        expect(behavior.createPromise).toEqual(mockInstance.$q.defer().promise);
      });

      it('should resolve the createPromise with the results on CREATE_COMPLETED', () => {
        const createDeferred = behavior.createDeferred;
        spyOn(createDeferred, 'resolve');
        behavior.onCreateCompleted(entity);
        expect(createDeferred.resolve).toHaveBeenCalledWith(entity);
      });

      it('should reject the createPromise with the errors on CREATE_FAILED', () => {
        const createDeferred = behavior.createDeferred;
        spyOn(createDeferred, 'reject');
        behavior.onCreateFailed('error');
        expect(createDeferred.reject).toHaveBeenCalledWith('error');
      });

      it('should be in isCreating state on CREATE_STARTED', () => {
        expect(behavior.isCreating).toBe(true);
      });

      it('should not be in isCreating state on CREATE_COMPLETED', () => {
        behavior.onCreateCompleted(entity);
        expect(behavior.isCreating).toBe(false);
      });

      it('should not be in isCreating state on CREATE_FAILED', () => {
        behavior.onCreateFailed('error');
        expect(behavior.isCreating).toBe(false);
      });

      it('should not be in isSet state after CREATE_STARTED', () => {
        expect(behavior.isSet).toBe(false);
      });

      it('should not be in isSet state after CREATE_FAILED', () => {
        behavior.onCreateFailed('error');
        expect(behavior.isSet).toBe(false);
      });

      it('should be in isSet state after CREATE_COMPLETED', () => {
        behavior.onCreateCompleted(entity);
        expect(behavior.isSet).toBe(true);
      });

      it('should stay in isSet state after doing another create', () => {
        behavior.onCreateCompleted(entity);
        behavior.onCreateStarted();
        behavior.onCreateFailed('error');
        expect(behavior.isSet).toBe(true);
      });
    });

    describe('read', () => {
      let entity;
      beforeEach(() => {
        entity = {id: 1, foo: 'bar'};
        behavior.onReadStarted();
      });

      it('should add the entity to the collection on READ_COMPLETED', () => {
        behavior.onReadCompleted('foo');
        expect(behavior.items).toEqual(jasmine.arrayContaining(['foo']));
      });

      it('should update an existing entity in the collection on READ_COMPLETED', () => {
        behavior.items.push(entity);
        behavior.onReadCompleted({id: 1, foo: 'details'});
        expect(entity.foo).toEqual('details');
      });

      it('should add itself to hasDetailSet on READ_COMPLETED', () => {
        behavior.onReadCompleted({id: 1});
        expect(behavior.hasDetailSet.has(1)).toBe(true);
      });

      it('should emit the changed event on the store on READ_COMPLETED', () => {
        behavior.onReadCompleted(entity);
        expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'read', entity);
      });

      it('should emit the error event on the store on READ_FAILED', () => {
        behavior.onReadFailed('foo error');
        expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'read', 'foo error');
      });

      it('should refresh the items collection on READ_COMPLETED', () => {
        behavior.onReadCompleted(entity);
        expect(refreshSpy).toHaveBeenCalled();
      });

      it('should define a readPromise on READ_STARTED', () => {
        expect(behavior.readPromise).toEqual(mockInstance.$q.defer().promise);
      });

      it('should resolve the readPromise with the results on READ_COMPLETED', () => {
        const readDeferred = behavior.readDeferred;
        spyOn(readDeferred, 'resolve');
        behavior.onReadCompleted(entity);
        expect(readDeferred.resolve).toHaveBeenCalledWith(entity);
      });

      it('should reject the readPromise with the errors on READ_FAILED', () => {
        const readDeferred = behavior.readDeferred;
        spyOn(readDeferred, 'reject');
        behavior.onReadFailed('error');
        expect(readDeferred.reject).toHaveBeenCalledWith('error');
      });

      it('should be in isReading state on READ_STARTED', () => {
        expect(behavior.isReading).toBe(true);
      });

      it('should not be in isReading state on READ_COMPLETED', () => {
        behavior.onReadCompleted(entity);
        expect(behavior.isReading).toBe(false);
      });

      it('should not be in isReading state on READ_FAILED', () => {
        behavior.onReadFailed();
        expect(behavior.isReading).toBe(false);
      });

      it('should not be in isSet state after READ_STARTED', () => {
        expect(behavior.isSet).toBe(false);
      });

      it('should not be in isSet state after READ_FAILED', () => {
        behavior.onReadFailed();
        expect(behavior.isSet).toBe(false);
      });

      it('should be in isSet state after READ_COMPLETED', () => {
        behavior.onReadCompleted(entity);
        expect(behavior.isSet).toBe(true);
      });

      it('should stay in isSet state after doing another read', () => {
        behavior.onReadCompleted(entity);
        behavior.onReadStarted();
        behavior.onReadFailed();
        expect(behavior.isSet).toBe(true);
      });
    });

    describe('update', () => {
      let entity;
      beforeEach(() => {
        entity = {id: 1, foo: 'bar'};
        behavior.items.push(entity);
        behavior.onUpdateStarted();
      });

      it('should update an existing entity in the collection on UPDATE_COMPLETED', () => {
        behavior.onUpdateCompleted({id: 1, foo: 'updated'});
        expect(entity.foo).toEqual('updated');
      });

      it('should ignore updates to an unknown entity on UPDATE_COMPLETED', () => {
        behavior.onUpdateCompleted({id: 2, foo: 'ignored'});
        expect(behavior.items).toEqual([entity]);
        expect(behavior.instance.emit).not.toHaveBeenCalled();
      });

      it('should emit the changed event on UPDATE_COMPLETED', () => {
        behavior.onUpdateCompleted({id: 1, foo: 'updated'});
        expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'update', entity);
      });

      it('should emit the error event on the store on UPDATE_FAILED', () => {
        behavior.onUpdateFailed('foo error');
        expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'update', 'foo error');
      });

      it('should refresh the items collection on UPDATE_COMPLETED', () => {
        behavior.onUpdateCompleted(entity);
        expect(refreshSpy).toHaveBeenCalled();
      });

      it('should define a updatePromise on UPDATE_STARTED', () => {
        expect(behavior.updatePromise).toEqual(mockInstance.$q.defer().promise);
      });

      it('should resolve the updatePromise with the results on UPDATE_COMPLETED', () => {
        const updateDeferred = behavior.updateDeferred;
        spyOn(updateDeferred, 'resolve');
        behavior.onUpdateCompleted(entity);
        expect(updateDeferred.resolve).toHaveBeenCalledWith(entity);
      });

      it('should reject the updatePromise with the errors on UPDATE_FAILED', () => {
        const updateDeferred = behavior.updateDeferred;
        spyOn(updateDeferred, 'reject');
        behavior.onUpdateFailed('error');
        expect(updateDeferred.reject).toHaveBeenCalledWith('error');
      });

      it('should be in isUpdating state on UPDATE_STARTED', () => {
        expect(behavior.isUpdating).toBe(true);
      });

      it('should not be in isUpdating state on UPDATE_COMPLETED', () => {
        behavior.onUpdateCompleted(entity);
        expect(behavior.isUpdating).toBe(false);
      });

      it('should not be in isUpdating state on UPDATE_FAILED', () => {
        behavior.onUpdateFailed('error');
        expect(behavior.isUpdating).toBe(false);
      });
    });

    describe('delete', () => {
      let entity;
      beforeEach(() => {
        entity = {id: 1, foo: 'bar'};
        behavior.items.push(entity);
        behavior.onDeleteStarted();
      });

      it('should delete an existing entity from the collection on DELETE_COMPLETED', () => {
        behavior.onDeleteCompleted({id: 1});
        expect(behavior.items).toEqual([]);
      });

      it('should ignore deleting of an unknown entity on DELETE_COMPLETED', () => {
        behavior.onDeleteCompleted({id: 2, foo: 'ignored'});
        expect(behavior.items).toEqual([entity]);
        expect(behavior.instance.emit).not.toHaveBeenCalled();
      });

      it('should emit the changed event on DELETE_COMPLETED', () => {
        behavior.onDeleteCompleted({id: 1});
        expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'delete', entity);
      });

      it('should emit the error event on the store on DELETE_FAILED', () => {
        behavior.onDeleteFailed('foo error');
        expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'delete', 'foo error');
      });

      it('should refresh the items collection on DELETE_COMPLETED', () => {
        behavior.onDeleteCompleted(entity);
        expect(refreshSpy).toHaveBeenCalled();
      });

      it('should define a deletePromise on DELETE_STARTED', () => {
        expect(behavior.deletePromise).toEqual(mockInstance.$q.defer().promise);
      });

      it('should resolve the deletePromise with the results on DELETE_COMPLETED', () => {
        const deleteDeferred = behavior.deleteDeferred;
        spyOn(deleteDeferred, 'resolve');
        behavior.onDeleteCompleted(entity);
        expect(deleteDeferred.resolve).toHaveBeenCalledWith(entity);
      });

      it('should reject the deletePromise with the errors on DELETE_FAILED', () => {
        const deleteDeferred = behavior.deleteDeferred;
        spyOn(deleteDeferred, 'reject');
        behavior.onDeleteFailed('error');
        expect(deleteDeferred.reject).toHaveBeenCalledWith('error');
      });

      it('should be in isDeleting state on DELETE_STARTED', () => {
        expect(behavior.isDeleting).toBe(true);
      });

      it('should not be in isDeleting state on DELETE_COMPLETED', () => {
        behavior.onDeleteCompleted(entity);
        expect(behavior.isDeleting).toBe(false);
      });

      it('should not be in isDeleting state on DELETE_FAILED', () => {
        behavior.onDeleteFailed('error');
        expect(behavior.isDeleting).toBe(false);
      });
    });

    describe('getById()', () => {
      it('return null if there is no item with that id', () => {
        expect(behavior.getById(1)).toBe(null);
      });

      it('return the item with the id if its in the store', () => {
        const item = {id: 1};
        behavior.items.push(item);
        expect(behavior.getById(1)).toBe(item);
      });

      it('should leverage the idProperty to find items', () => {
        const item = {foo: 1};
        behavior.idProperty = 'foo';
        behavior.items.push(item);
        expect(behavior.getById(1)).toBe(item);
      });
    });

    describe('hasDetails()', () => {
      it('should return false if there are no details found for the item id', () => {
        expect(behavior.hasDetails(1)).toBe(false);
      });

      it('should return true if there are details', () => {
        const entity = {id: 1};
        behavior.onReadStarted(entity);
        behavior.onReadCompleted(entity);
        expect(behavior.hasDetails(1)).toBe(true);
      });

      it('should leverage the id property to find details', () => {
        const entity = {foo: 1};
        behavior.idProperty = 'foo';
        behavior.onReadStarted(entity);
        behavior.onReadCompleted(entity);
        expect(behavior.hasDetails(1)).toBe(true);
      });

      it('should return false if the store is currently reading an entity', () => {
        const entity = {foo: 1};
        behavior.onReadStarted(entity);
        expect(behavior.hasDetails(1)).toBe(false);
      });
    });

    describe('reset()', () => {
      it('should reset entities collection and details and isSet flag', () => {
        behavior.isSet = true;
        behavior.items.push('foo');
        behavior.hasDetailSet = new Set('foo');

        behavior.reset();

        expect(behavior.isSet).toBe(false);
        expect(behavior.items).toEqual([]);
        expect(behavior.hasDetailSet.size).toEqual(0);
      });
    });
  });

  describe('@EntityStore() decorator', () => {
    @Store() @EntityStore() class TestStore {}
    @Store() @EntityStore({idProperty: 'test'}) class IdPropertyStore {}
    @Store() @EntityStore({entity: 'custom'}) class CustomEntityStore {}
    @Store() @EntityStore('custom') class CustomEntityStringStore {}
    @Store() @EntityStore({actions: ['read', 'update']}) class ActionsStore {}
    @Store() @EntityStore({collection: 'foo'}) class CollectionStore {}

    let store, $q;
    let idPropertyStore, customEntityStore, customEntityStringStore, actionsStore, collectionStore;
    beforeEach(() => {
      angular.module('test', [
        'luxyflux',
        TestStore.annotation.module.name,
        IdPropertyStore.annotation.module.name,
        CustomEntityStore.annotation.module.name,
        CustomEntityStringStore.annotation.module.name,
        ActionsStore.annotation.module.name,
        CollectionStore.annotation.module.name
      ]).service('ApplicationDispatcher', () => {
        return {
          register() {},
          dispatch() {}
        };
      });

      module('test');
      inject((
        _TestStore_,
        _IdPropertyStore_,
        _CustomEntityStore_,
        _CustomEntityStringStore_,
        _ActionsStore_,
        _CollectionStore_,
        _$q_
      ) => {
        store = _TestStore_;
        idPropertyStore = _IdPropertyStore_;
        customEntityStore = _CustomEntityStore_;
        customEntityStringStore = _CustomEntityStringStore_;
        actionsStore = _ActionsStore_;
        collectionStore = _CollectionStore_;
        $q = _$q_;
      });
    });

    it('should define the EntityStore API methods on the store', () => {
      [
        'entityStore',
        'items',

        'isSet',
        'isEmpty',
        'isBusy',

        'loadPromise',
        'createPromise',
        'readPromise',
        'updatePromise',
        'deletePromise',

        'isLoading',
        'isCreating',
        'isReading',
        'isUpdating',
        'isDeleting',

        'reset',
        'hasDetails',
        'getById',

        'onTestLoadStarted',
        'onTestLoadCompleted',
        'onTestLoadFailed',
        'onTestCreateStarted',
        'onTestCreateCompleted',
        'onTestCreateFailed',
        'onTestReadStarted',
        'onTestReadCompleted',
        'onTestReadFailed',
        'onTestUpdateStarted',
        'onTestUpdateCompleted',
        'onTestUpdateFailed',
        'onTestDeleteStarted',
        'onTestDeleteCompleted',
        'onTestDeleteFailed'
      ].forEach(api => expect(store[api]).toBeDefined());
    });

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

    it('should make the collection transformable', () => {
      expect(store.transformables.items).toEqual(jasmine.any(TransformableCollection));
    });

    it('should have an instance of EntityStoreBehavior as the behavior property', () => {
      expect(store.entityStore).toEqual(jasmine.any(EntityStoreBehavior));
    });

    it('should use the class name to determine the crud entity by default', () => {
      expect(store.entityStore.config.entity).toEqual('Test');
    });

    it('should use id as the default the entity id property', () => {
      expect(store.entityStore.idProperty).toEqual('id');
    });

    it('should be possible to configure the entity id property', () => {
      expect(idPropertyStore.entityStore.idProperty).toEqual('test');
    });

    it('should be possible to configure the entity to manage', () => {
      expect(customEntityStore.entityStore.config.entity).toEqual('Custom');
    });

    it('should be possible to pass the entity property as a string', () => {
      expect(customEntityStringStore.entityStore.config.entity).toEqual('Custom');
    });

    it('should create properly named handlers when configuring the entity', () => {
      expect(customEntityStore.onCustomLoadCompleted).toBeDefined();
    });

    it('should manage all actions by default', () => {
      expect(store.entityStore.config.actions)
        .toEqual(['load', 'create', 'read', 'update', 'delete']);
    });

    it('should be possible to configure the actions the store manage', () => {
      expect(actionsStore.entityStore.config.actions).toEqual(['read', 'update']);
    });

    it('should use the items property to store entities in by default', () => {
      expect(store.items).toEqual(jasmine.any(Array));
    });

    it('should be possible to configure the collection property the entities are stored in', () => {
      expect(collectionStore.foo).toEqual(jasmine.any(Array));
    });

    it('should add handlers for actions', () => {
      expect(TestStore.handlers)
        .toEqual(jasmine.objectContaining({
          TEST_LOAD_STARTED: 'onTestLoadStarted',
          TEST_LOAD_COMPLETED: 'onTestLoadCompleted',
          TEST_LOAD_FAILED: 'onTestLoadFailed',
          TEST_CREATE_STARTED: 'onTestCreateStarted',
          TEST_CREATE_COMPLETED: 'onTestCreateCompleted',
          TEST_CREATE_FAILED: 'onTestCreateFailed',
          TEST_READ_STARTED: 'onTestReadStarted',
          TEST_READ_COMPLETED: 'onTestReadCompleted',
          TEST_READ_FAILED: 'onTestReadFailed',
          TEST_UPDATE_STARTED: 'onTestUpdateStarted',
          TEST_UPDATE_COMPLETED: 'onTestUpdateCompleted',
          TEST_UPDATE_FAILED: 'onTestUpdateFailed',
          TEST_DELETE_STARTED: 'onTestDeleteStarted',
          TEST_DELETE_COMPLETED: 'onTestDeleteCompleted',
          TEST_DELETE_FAILED: 'onTestDeleteFailed'
        }));
    });

    it('should only add handlers for the chosen actions', () => {
      @EntityStore({actions: ['read']}) class CustomStore {}
      expect(CustomStore.handlers)
        .not.toEqual(jasmine.objectContaining({
          TEST_LOAD_STARTED: 'onTestLoadStarted'
        }));
    });

    it('should not override any handlers already defined on the store', () => {
      @Store()
      @EntityStore() class CustomStore {
        @Handler('TEST_LOAD_FAILED') onCustomFailed() {}
      }
      expect(CustomStore.handlers).toEqual(jasmine.objectContaining({
        TEST_LOAD_FAILED: 'onCustomFailed'
      }));
    });
  });
});