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'); }); }); });