ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/spec/miq-component/miq-component.spec.js

Summary

Maintainability
A
2 hrs
Test Coverage
import {
  getDefinition,
  sanitizeAndFreezeInstanceId,
  validateInstance,
  define,
  newInstance,
  getInstance,
  isDefined,
  getComponentNames,
  getComponentInstances,
  clearRegistry,
} from '../../miq-component/registry.js';


import {
  writeProxy,
  lockInstanceProperties,
} from '../../miq-component/utils.js';

describe('Component API', () => {
  let mountingElement;
  const mountId = 'mounting-point';

  beforeAll(() => {
    mountingElement = document.createElement('div');
    mountingElement.id = mountId;
    document.body.appendChild(mountingElement);
  });

  afterEach(() => {
    clearRegistry();
  });

  afterAll(() => {
    document.body.remove(mountingElement);
  });

  it('define method can be used to register new components', () => {
    define('FooComponent', {});
    define('BarComponent', {}, [{}]);
    expect(isDefined('FooComponent')).toBe(true);
    expect(isDefined('BarComponent')).toBe(true);
    expect(getComponentNames()).toEqual(['FooComponent', 'BarComponent']);
  });

  it('define method throws if the component name is already taken', () => {
    define('FooComponent', {});
    expect(() => {
      define('FooComponent', {});
    }).toThrow();
    expect(getComponentNames()).toEqual(['FooComponent']);
  });

  it('define method passes twice with override option', () => {
    define('FooComponent', {});
    define('FooComponent', {}, { override: true });
    expect(getComponentNames()).toEqual(['FooComponent']);
  });

  it('define method does nothing if the component name is not a string', () => {
    expect(() => {
      define(123, {});
    }).toThrow();
    expect(isDefined(123)).toBe(false);
    expect(getComponentNames()).toEqual([]);
  });

  it('define method can be used associate existing instances with the new component', () => {
    const testInstances = [
      { id: 'first' }, { id: 'second' },
    ];

    define('FooComponent', {}, { instances: testInstances });
    expect(getInstance('FooComponent', 'first')).toBe(testInstances[0]);
    expect(getInstance('FooComponent', 'second')).toBe(testInstances[1]);
  });

  it('when passing existing instances, define method ensures a sane instance id', () => {
    const testInstances = [
      { id: 'first' }, {}, {},
    ];

    define('FooComponent', {}, { instances: testInstances });

    const registeredInstances = getComponentInstances('FooComponent');
    expect(registeredInstances).toHaveLength(3);
    expect(registeredInstances[0].id).toBe('first');
    expect(registeredInstances[1].id).toBe('FooComponent-1');
    expect(registeredInstances[2].id).toBe('FooComponent-2');
  });

  it('when passing existing instances, define method ensures that instance id is frozen', () => {
    const testInstances = [
      { id: 'first' },
    ];

    define('FooComponent', {}, { instances: testInstances });
    expect(() => {
      testInstances[0].id = 'second';
    }).toThrow();
  });

  it('when passing existing instances, define method skips falsy values', () => {
    const testInstances = [
      false, '', null, undefined, {},
    ];

    define('FooComponent', {}, { instances: testInstances });

    const registeredInstances = getComponentInstances('FooComponent');
    expect(registeredInstances).toHaveLength(1);
    expect(registeredInstances[0].id).toBe('FooComponent-0');
  });

  it('when passing existing instances, define method throws in case of reference duplicity', () => {
    const testInstance = { id: 'first' };

    expect(() => {
      define('FooComponent', {}, { instances: [testInstance, testInstance] });
    }).toThrow();
  });

  it('when passing existing instances, define method throws in case of id duplicity', () => {
    const testInstances = [
      { id: 'first' }, { id: 'first' },
    ];

    expect(() => {
      define('FooComponent', {}, { instances: testInstances });
    }).toThrow();
  });

  it('newInstance method can be used to create new component instances', () => {
    const testInstances = [
      { id: 'first', elementId: mountId }, { id: 'second', elementId: mountId },
    ];

    const testBlueprint = {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => testInstances[0])
        .mockImplementationOnce(() => testInstances[1]),
    };

    define('FooComponent', testBlueprint);
    const resultingInstances = [
      newInstance('FooComponent', { bar: 123 }, mountingElement),
      newInstance('FooComponent', { baz: true }, mountingElement),
    ];

    expect(testBlueprint.create).toHaveBeenCalledTimes(2);
    expect(testBlueprint.create.mock.calls[0]).toEqual([
      { bar: 123 },
      mountingElement,
    ]);
    expect(testBlueprint.create.mock.calls[1]).toEqual([
      { baz: true },
      mountingElement,
    ]);

    expect(resultingInstances[0]).toBe(testInstances[0]);
    expect(resultingInstances[1]).toBe(testInstances[1]);

    expect(resultingInstances[0].id).toBe('first');
    expect(resultingInstances[1].id).toBe('second');

    expect(resultingInstances[0].props).toEqual({ bar: 123 });
    expect(resultingInstances[1].props).toEqual({ baz: true });

    resultingInstances.forEach((instance) => {
      expect(instance.update).toEqual(expect.any(Function));
      expect(instance.destroy).toEqual(expect.any(Function));
    });

    const registeredInstances = getComponentInstances('FooComponent');
    expect(registeredInstances).toHaveLength(2);
    expect(registeredInstances[0]).toBe(resultingInstances[0]);
    expect(registeredInstances[1]).toBe(resultingInstances[1]);
  });

  it('newInstance method does nothing if the component is not already defined', () => {
    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    expect(resultingInstance).toBeUndefined();
  });

  it('newInstance method does nothing if blueprint.create is not a function', () => {
    define('FooComponent', {});

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    expect(resultingInstance).toBeUndefined();
  });

  it('newInstance method throws if blueprint.create returns a falsy value', () => {
    define('FooComponent', {
      create() { return null; },
    });

    expect(() => {
      newInstance('FooComponent', { bar: 123 });
    }).toThrow();
  });

  it('newInstance method ensures a sane instance id', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first', elementId: mountId }))
        .mockImplementationOnce(() => ({ elementId: mountId }))
        .mockImplementationOnce(() => ({ elementId: mountId })),
    });

    const resultingInstances = [
      newInstance('FooComponent', { bar: 123, elementId: mountId }, mountingElement),
      newInstance('FooComponent', { baz: true, elementId: mountId }, mountingElement),
      newInstance('FooComponent', { qux: ['1337'], elementId: mountId }, mountingElement),
    ];

    expect(resultingInstances[0].id).toBe('first');
    expect(resultingInstances[1].id).toBe('FooComponent-1');
    expect(resultingInstances[2].id).toBe('FooComponent-2');
  });

  it('newInstance method ensures that instance id is frozen', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first' })),
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    expect(() => {
      resultingInstance.id = 'second';
    }).toThrow();
  });

  it('newInstance method throws if blueprint.create returns the same instance', () => {
    const testInstance = { id: 'first' };

    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => testInstance)
        .mockImplementationOnce(() => testInstance),
    });

    newInstance('FooComponent', { bar: 123 });

    expect(() => {
      newInstance('FooComponent', { bar: 123 });
    }).toThrow();
  });

  it('newInstance method throws if blueprint.create returns an instance with id already taken', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first', elementId: mountId }))
        .mockImplementationOnce(() => ({ id: 'first', elementId: mountId })),
    });
    newInstance('FooComponent', { bar: 123 }, mountingElement);
    expect(() => {
      newInstance('FooComponent', { bar: 123 }, mountingElement);
    }).toThrow();
  });

  it('trying to rewrite instance props throws an error', () => {
    define('FooComponent', {
      create() { return {}; },
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    expect(() => {
      resultingInstance.props = {};
    }).toThrow();
  });

  it('instance.update merges props and delegates to blueprint.update', () => {
    const testInstances = [
      { id: 'first', elementId: mountId }, { id: 'second', elementId: mountId },
    ];

    const testBlueprint = {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => testInstances[0])
        .mockImplementationOnce(() => testInstances[1]),
      update: jest.fn().mockName('testBlueprint.update'),
    };

    define('UpdateComponent', testBlueprint);

    const resultingInstances = [
      newInstance('UpdateComponent', { bar: 123 }, mountingElement),
      newInstance('UpdateComponent', { baz: true }, mountingElement),
    ];

    resultingInstances[0].update({ baz: true, qux: ['1337'] });
    resultingInstances[1].update({ baz: false });

    expect(testBlueprint.update).toHaveBeenCalledTimes(2);
    expect(testBlueprint.update.mock.calls[0]).toEqual([
      { bar: 123, baz: true, qux: ['1337'] },
      mountingElement,
    ]);

    expect(resultingInstances[0].props).toEqual({
      bar: 123, baz: true, qux: ['1337'],
    });
    expect(resultingInstances[1].props).toEqual({
      baz: false,
    });
  });

  it('instance.update does nothing if blueprint.update is not a function', () => {
    define('FooComponent', {
      create() { return {}; },
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    resultingInstance.update({ baz: true, qux: ['1337'] });

    expect(resultingInstance.props).toEqual({ bar: 123 });
  });

  it('multiple props modifications will trigger single instance.update', (done) => {
    const testBlueprint = {
      create() { return {}; },
      update: jest.fn().mockName('testBlueprint.update'),
    };

    define('FooComponent', testBlueprint);

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    resultingInstance.update = jest.fn().mockName('resultingInstance.update');

    resultingInstance.props.baz = true;
    resultingInstance.props.qux = ['1337'];

    setTimeout(() => {
      expect(resultingInstance.update).toHaveBeenCalledWith({ baz: true, qux: ['1337'] });
      done();
    });
  });

  it('instance.destroy delegates to blueprint.destroy', () => {
    const testInstances = [
      { id: 'first', elementId: mountId }, { id: 'second', elementId: mountId },
    ];

    const testBlueprint = {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => testInstances[0])
        .mockImplementationOnce(() => testInstances[1]),
      destroy: jest.fn().mockName('testBlueprint.destroy'),
      elementId: mountId,
    };

    define('FooComponent', testBlueprint);

    const resultingInstances = [
      newInstance('FooComponent', { bar: 123 }, mountingElement),
      newInstance('FooComponent', { baz: true }),
    ];

    resultingInstances[0].destroy();
    resultingInstances[1].destroy();

    expect(testBlueprint.destroy).toHaveBeenCalledTimes(2);
    expect(testBlueprint.destroy.mock.calls[0]).toEqual([
      resultingInstances[0],
      mountingElement,
    ]);
    expect(testBlueprint.destroy.mock.calls[0][0]).toBe(resultingInstances[0]);
    expect(testBlueprint.destroy.mock.calls[1]).toEqual([
      resultingInstances[1],
      undefined,
    ]);
    expect(testBlueprint.destroy.mock.calls[1][0]).toBe(resultingInstances[1]);
  });

  it('instance.destroy removes the component instance', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first' })),
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    resultingInstance.destroy();

    expect(getInstance('FooComponent', 'first')).toBeUndefined();
    expect(getComponentInstances('FooComponent')).toEqual([]);
  });

  it('instance.destroy prevents access to existing instance properties except for id', () => {
    define('FooComponent', {
      create() { return {}; },
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    resultingInstance.destroy();

    Object.keys(resultingInstance)
      .filter(propName => propName !== 'id')
      .forEach((propName) => {
        expect(() => resultingInstance[propName]).toThrow();
        expect(() => {
          resultingInstance[propName] = ['1337'];
        }).toThrow();
      });
  });

  it('getInstance method can be used to obtain existing component instances', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first' })),
    });

    const resultingInstance = newInstance('FooComponent', { bar: 123 });
    const obtainedInstance = getInstance('FooComponent', 'first');
    expect(obtainedInstance).toBe(resultingInstance);
    expect(getInstance('FooComponent', 'second')).toBeUndefined();
  });

  it('isDefined method can be used to determine whether a component is already defined', () => {
    define('FooComponent', {});
    expect(isDefined('FooComponent')).toBe(true);
    expect(isDefined('BarComponent')).toBe(false);
  });

  it('getDefinition returns the correct component definition', () => {
    const testBlueprint = {};
    define('FooComponent', testBlueprint);

    const definition = getDefinition('FooComponent');
    expect(definition.name).toBe('FooComponent');
    expect(definition.blueprint).toBe(testBlueprint);

    expect(getDefinition('BarComponent')).toBeUndefined();
  });

  it('sanitizeAndFreezeInstanceId ensures the instance id is sane and cannot be changed', () => {
    const testInstances = [
      { id: 'first' }, {}, {},
    ];

    define('FooComponent', {});
    const definition = getDefinition('FooComponent');

    testInstances.forEach((instance) => {
      sanitizeAndFreezeInstanceId(instance, definition);
    });

    expect(testInstances[0].id).toBe('first');
    expect(testInstances[1].id).toBe('FooComponent-0');
    expect(testInstances[2].id).toBe('FooComponent-0');

    testInstances.forEach((instance) => {
      expect(() => {
        instance.id = 'second'; // eslint-disable-line no-param-reassign, param reassing this is expected to fail, therefore eslint rule here does not make sense
      }).toThrow();
    });
  });

  it('validateInstance performs the necessary instance validations', () => {
    define('FooComponent', {
      create: jest.fn().mockName('testBlueprint.create')
        .mockImplementationOnce(() => ({ id: 'first' })),
    });

    const firstInstance = newInstance('FooComponent', { bar: 123 });
    const definition = getDefinition('FooComponent');

    expect(() => {
      validateInstance({ id: 'second' }, definition);
    }).not.toThrow();

    expect(() => {
      validateInstance(firstInstance, definition);
    }).toThrow();

    expect(() => {
      validateInstance({ id: 'first' }, definition);
    }).toThrow();
  });

  it('writeProxy can be used to proxy writes to properties of the given object', () => {
    const object = { bar: 123, baz: true };
    const onWrite = jest.fn().mockName('onWrite');

    const resultingObject = writeProxy(object, onWrite);
    expect(resultingObject).toEqual(object);

    resultingObject.bar = 456;
    resultingObject.qux = ['1337'];

    expect(resultingObject).toEqual(object);
    expect(object).toEqual({ bar: 456, baz: true, qux: ['1337'] });

    expect(onWrite).toHaveBeenCalledTimes(2);
    expect(onWrite.mock.calls[0]).toEqual([
      'bar', 456,
    ]);
    expect(onWrite.mock.calls[1]).toEqual([
      'qux', ['1337'],
    ]);
  });

  it('lockInstanceProperties prevents access to existing instance properties except for id', () => {
    const testInstance = { id: 'first', bar: 123, baz: true };

    const resultingInstance = lockInstanceProperties(testInstance);
    expect(resultingInstance.id).toBe('first');

    ['bar', 'baz'].forEach((propName) => {
      expect(() => resultingInstance[propName]).toThrow();
      expect(() => {
        resultingInstance[propName] = ['1337'];
      }).toThrow();
    });
  });
});