import { mount } from 'enzyme';
import React from 'react';
import type { IFormInputProps, IStageForSpelPreview, IValidator } from '..';
import { SpelInput } from './SpelInput';
import { SpelService } from './SpelService';
function defer() {
let resolve: Function, reject: Function;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { promise, resolve, reject };
}
describe('', () => {
beforeEach(() => jasmine.clock().install());
afterEach(() => jasmine.clock().uninstall());
let inputProps: IFormInputProps;
let evaluateExpressionSpy: jasmine.Spy;
const previewStage: IStageForSpelPreview = {
stageId: '123',
executionId: 'abc',
executionLabel: 'execution ran yesterday',
};
beforeEach(() => {
inputProps = {
name: 'name',
onBlur: jasmine.createSpy('onBlur'),
onChange: jasmine.createSpy('onChange'),
value: 'abc123',
validation: {
revalidate: jasmine.createSpy('revalidate'),
addValidator: jasmine.createSpy('addValidator'),
removeValidator: jasmine.createSpy('removeValidator'),
touched: true,
messageNode: 'Theres an error',
hidden: false,
category: 'error',
},
};
evaluateExpressionSpy = spyOn(SpelService, 'evaluateExpression');
});
it('should render a text area with the value in it', () => {
const component = mount();
expect(component.render().is('textarea')).toBe(true);
expect(component.render().text()).toBe('abc123');
});
it('should eagerly fetch the preview from the server on initial load', () => {
mount();
expect(evaluateExpressionSpy).toHaveBeenCalledTimes(1);
});
it('should pass the value, pipeline, and stage ids to the SpelService', () => {
mount();
expect(evaluateExpressionSpy).toHaveBeenCalledWith('abc123', 'abc', '123');
});
it('should debounce preview fetches when the input value changes', async () => {
const deferred1 = defer();
evaluateExpressionSpy.and.callFake(() => deferred1.promise);
const component = mount();
expect(evaluateExpressionSpy).toHaveBeenCalledTimes(1);
// First preview request resolves
deferred1.resolve('async value');
await deferred1.promise;
component.setProps({});
// Update value -- evaluate is not called yet
component.setProps({ value: 'def456' });
expect(evaluateExpressionSpy).toHaveBeenCalledTimes(1);
// After debounce interval, evaluate is called again
jasmine.clock().tick(1000);
component.setProps({});
expect(evaluateExpressionSpy).toHaveBeenCalledTimes(2);
});
it('should call revalidate whenever an async event occurs', async () => {
const deferred1 = defer();
evaluateExpressionSpy.and.callFake(() => deferred1.promise);
const component = mount();
// [ NONE -> PENDING ] a promise was found, results pending
expect(inputProps.validation.revalidate).toHaveBeenCalledTimes(1);
// Result received from the server
deferred1.resolve('async value1');
await deferred1.promise;
component.setProps({});
// [ PENDING -> RESOLVED ]
expect(inputProps.validation.revalidate).toHaveBeenCalledTimes(2);
// Prepare the test for second async fetch
const deferred2 = defer();
evaluateExpressionSpy.and.callFake(() => deferred2.promise);
component.setProps({ value: 'def456' });
// [ notDebouncing -> isDebouncing ]
expect(inputProps.validation.revalidate).toHaveBeenCalledTimes(3);
jasmine.clock().tick(1000);
component.setProps({});
// [ isDebouncing -> notDebouncing ], [ RESOLVED -> PENDING ]
expect(inputProps.validation.revalidate).toHaveBeenCalledTimes(5);
deferred2.resolve('async value2');
await deferred2.promise;
component.setProps({});
// [ PENDING -> RESOLVED ]
expect(inputProps.validation.revalidate).toHaveBeenCalledTimes(6);
});
it('should add a validator on mount', () => {
mount();
expect(inputProps.validation.addValidator).toHaveBeenCalledTimes(1);
});
it('should remove the same validator on unmount as it added on mount', () => {
const addValidator = inputProps.validation.addValidator as jasmine.Spy;
const removeValidator = inputProps.validation.removeValidator as jasmine.Spy;
const component = mount();
expect(addValidator).toHaveBeenCalledTimes(1);
expect(removeValidator).toHaveBeenCalledTimes(0);
component.unmount();
expect(addValidator).toHaveBeenCalledTimes(1);
expect(removeValidator).toHaveBeenCalledTimes(1);
expect(addValidator.calls.mostRecent().args[0]).toBe(removeValidator.calls.mostRecent().args[0]);
});
describe('async validation', () => {
let validators: IValidator[];
let mockValidate: jasmine.Spy;
beforeEach(() => {
validators = [];
mockValidate = jasmine.createSpy('validate').and.callFake(() => {
return validators.map((v) => v(null)).filter((x) => !!x)[0];
});
const addValidator = inputProps.validation.addValidator as jasmine.Spy;
const removeValidator = inputProps.validation.removeValidator as jasmine.Spy;
const revalidate = inputProps.validation.revalidate as jasmine.Spy;
addValidator.and.callFake((v: IValidator) => validators.push(v));
removeValidator.and.callFake((v: IValidator) => (validators = validators.filter((x) => x !== v)));
revalidate.and.callFake(() => mockValidate());
});
it('should validate as "Async: *" when a SpelService fetch is pending', async () => {
evaluateExpressionSpy.and.callFake(() => new Promise(() => null));
mount();
expect(mockValidate).toHaveBeenCalledTimes(1);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Async: ');
});
it('should continue to render the previous result when a SpelService fetch is pending', async () => {
const result1 = new Promise((resolve) => resolve('preview result'));
evaluateExpressionSpy.and.callFake(() => result1);
const component = mount();
expect(mockValidate).toHaveBeenCalledTimes(1);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Async: ');
mockValidate.calls.reset();
await result1;
component.setProps({ value: 'some other value' });
expect(mockValidate).toHaveBeenCalledTimes(2);
expect(mockValidate.calls.first().returnValue).toMatch('Message: ');
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Async: ');
expect(mockValidate.calls.mostRecent().returnValue).toMatch('preview result');
});
it('should validate as "Message: *" when a SpelService fetch is resolved with a result', async () => {
const deferred = defer();
evaluateExpressionSpy.and.callFake(() => deferred.promise);
const component = mount();
expect(mockValidate).toHaveBeenCalledTimes(1);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Async: ');
deferred.resolve('expression result');
await deferred.promise;
component.setProps({});
expect(mockValidate).toHaveBeenCalledTimes(2);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Message: ');
expect(mockValidate.calls.mostRecent().returnValue).toMatch('expression result');
});
it('should validate as "Warning: *" when a SpelService fetch is rejected', async () => {
const deferred = defer();
evaluateExpressionSpy.and.callFake(() => deferred.promise);
const component = mount();
expect(mockValidate).toHaveBeenCalledTimes(1);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Async: ');
let caught = false;
deferred.reject('something bad happened');
try {
await deferred.promise;
} catch (error) {
caught = true;
}
expect(caught).toBe(true);
component.setProps({});
expect(mockValidate).toHaveBeenCalledTimes(2);
expect(mockValidate.calls.mostRecent().returnValue).toMatch('Warning: something bad happened');
});
});
});