/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { mount, ReactWrapper } from 'enzyme'; import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; import { findTestSubject } from 'test_utils/helpers'; import { Field, getEditableValue } from './field'; jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); jest.mock('brace/mode/markdown', () => 'brace/mode/markdown'); const defaults = { requiresPageReload: false, readOnly: false, category: ['category'], }; const exampleValues = { array: ['example_value'], boolean: false, image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=', json: { foo: 'bar2' }, markdown: 'Hello World', number: 1, select: 'banana', string: 'hello world', stringWithValidation: 'foo', }; const settings: Record<string, FieldSetting> = { array: { name: 'array:test:setting', ariaName: 'array test setting', displayName: 'Array test setting', description: 'Description for Array test setting', type: 'array', value: undefined, defVal: ['default_value'], isCustom: false, isOverridden: false, ...defaults, }, boolean: { name: 'boolean:test:setting', ariaName: 'boolean test setting', displayName: 'Boolean test setting', description: 'Description for Boolean test setting', type: 'boolean', value: undefined, defVal: true, isCustom: false, isOverridden: false, ...defaults, }, image: { name: 'image:test:setting', ariaName: 'image test setting', displayName: 'Image test setting', description: 'Description for Image test setting', type: 'image', value: undefined, defVal: null, isCustom: false, isOverridden: false, validation: { maxSize: { length: 1000, description: 'Description for 1 kB', }, }, ...defaults, }, json: { name: 'json:test:setting', ariaName: 'json test setting', displayName: 'Json test setting', description: 'Description for Json test setting', type: 'json', value: '{"foo": "bar"}', defVal: '{}', isCustom: false, isOverridden: false, ...defaults, }, markdown: { name: 'markdown:test:setting', ariaName: 'markdown test setting', displayName: 'Markdown test setting', description: 'Description for Markdown test setting', type: 'markdown', value: undefined, defVal: '', isCustom: false, isOverridden: false, ...defaults, }, number: { name: 'number:test:setting', ariaName: 'number test setting', displayName: 'Number test setting', description: 'Description for Number test setting', type: 'number', value: undefined, defVal: 5, isCustom: false, isOverridden: false, ...defaults, }, select: { name: 'select:test:setting', ariaName: 'select test setting', displayName: 'Select test setting', description: 'Description for Select test setting', type: 'select', value: undefined, defVal: 'orange', isCustom: false, isOverridden: false, options: ['apple', 'orange', 'banana'], optionLabels: { apple: 'Apple', orange: 'Orange', // Deliberately left out `banana` to test if it also works with missing labels }, ...defaults, }, string: { name: 'string:test:setting', ariaName: 'string test setting', displayName: 'String test setting', description: 'Description for String test setting', type: 'string', value: undefined, defVal: null, isCustom: false, isOverridden: false, ...defaults, }, stringWithValidation: { name: 'string:test-validation:setting', ariaName: 'string test validation setting', displayName: 'String test validation setting', description: 'Description for String test validation setting', type: 'string', validation: { regex: new RegExp('^foo'), message: 'must start with "foo"', }, value: undefined, defVal: 'foo-default', isCustom: false, isOverridden: false, ...defaults, }, }; const userValues = { array: ['user', 'value'], boolean: false, image: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', json: '{"hello": "world"}', markdown: '**bold**', number: 10, select: 'banana', string: 'foo', stringWithValidation: 'fooUserValue', }; const invalidUserValues = { stringWithValidation: 'invalidUserValue', }; const handleChange = jest.fn(); const clearChange = jest.fn(); const getFieldSettingValue = (wrapper: ReactWrapper, name: string, type: string) => { const field = findTestSubject(wrapper, `advancedSetting-editField-${name}`); if (type === 'boolean') { return field.props()['aria-checked']; } else { return field.props().value; } }; describe('Field', () => { Object.keys(settings).forEach((type) => { const setting = settings[type]; describe(`for ${type} setting`, () => { it('should render default value if there is no user value set', async () => { const component = shallowWithI18nProvider( <Field setting={setting} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); expect(component).toMatchSnapshot(); }); it('should render as read only with help text if overridden', async () => { const component = shallowWithI18nProvider( <Field setting={{ ...setting, // @ts-ignore value: userValues[type], isOverridden: true, }} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); expect(component).toMatchSnapshot(); }); it('should render as read only if saving is disabled', async () => { const component = shallowWithI18nProvider( <Field setting={setting} handleChange={handleChange} enableSaving={false} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); expect(component).toMatchSnapshot(); }); it('should render user value if there is user value is set', async () => { const component = shallowWithI18nProvider( <Field setting={{ ...setting, // @ts-ignore value: userValues[type], }} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); expect(component).toMatchSnapshot(); }); it('should render custom setting icon if it is custom', async () => { const component = shallowWithI18nProvider( <Field setting={{ ...setting, isCustom: true, }} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); expect(component).toMatchSnapshot(); }); it('should render unsaved value if there are unsaved changes', async () => { const component = shallowWithI18nProvider( <Field setting={{ ...setting, isCustom: true, }} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} unsavedChanges={{ // @ts-ignore value: exampleValues[setting.type], }} /> ); expect(component).toMatchSnapshot(); }); }); if (type === 'select') { it('should use options for rendering values and optionsLabels for rendering labels', () => { const component = mountWithI18nProvider( <Field setting={{ ...setting, isCustom: true, }} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); // @ts-ignore const values = select.find('option').map((option) => option.prop('value')); expect(values).toEqual(['apple', 'orange', 'banana']); // @ts-ignore const labels = select.find('option').map((option) => option.text()); expect(labels).toEqual(['Apple', 'Orange', 'banana']); }); } const setup = () => { const Wrapper = (props: Record<string, any>) => ( <I18nProvider> <Field setting={setting} clearChange={clearChange} handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} {...props} /> </I18nProvider> ); const wrapper = mount(<Wrapper />); const component = wrapper.find(I18nProvider).find(Field); return { wrapper, component, }; }; if (type === 'image') { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); const userValue = userValues[type]; (component.instance() as Field).getImageAsBase64 = ({}: Blob) => Promise.resolve(''); it('should be able to change value and cancel', async () => { (component.instance() as Field).onImageChange(([userValue] as unknown) as FileList); expect(handleChange).toBeCalled(); await wrapper.setProps({ unsavedChanges: { value: userValue, changeImage: true, }, setting: { ...(component.instance() as Field).props.setting, value: userValue, }, }); await (component.instance() as Field).cancelChangeImage(); expect(clearChange).toBeCalledWith(setting.name); wrapper.update(); }); it('should be able to change value from existing value', async () => { await wrapper.setProps({ unsavedChanges: {}, }); const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-changeImage-${setting.name}`).simulate('click'); const newUserValue = `${userValue}=`; await (component.instance() as Field).onImageChange(([ newUserValue, ] as unknown) as FileList); expect(handleChange).toBeCalled(); }); it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); expect(handleChange).toBeCalledWith(setting.name, { value: getEditableValue(setting.type, setting.defVal), changeImage: true, }); }); }); } else if (type === 'markdown' || type === 'json') { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); const userValue = userValues[type]; it('should be able to change value', async () => { (component.instance() as Field).onCodeEditorChange(userValue as UiSettingsType); expect(handleChange).toBeCalledWith(setting.name, { value: userValue }); await wrapper.setProps({ setting: { ...(component.instance() as Field).props.setting, value: userValue, }, }); wrapper.update(); }); it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); expect(handleChange).toBeCalledWith(setting.name, { value: getEditableValue(setting.type, setting.defVal), }); }); if (type === 'json') { it('should be able to clear value and have empty object populate', async () => { await (component.instance() as Field).onCodeEditorChange('' as UiSettingsType); wrapper.update(); expect(handleChange).toBeCalledWith(setting.name, { value: setting.defVal }); }); } }); } else { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); // @ts-ignore const userValue = userValues[type]; const fieldUserValue = type === 'array' ? userValue.join(', ') : userValue; if (setting.validation) { // @ts-ignore const invalidUserValue = invalidUserValues[type]; it('should display an error when validation fails', async () => { await (component.instance() as Field).onFieldChange(invalidUserValue); const expectedUnsavedChanges = { value: invalidUserValue, error: (setting.validation as StringValidation).message, isInvalid: true, }; expect(handleChange).toBeCalledWith(setting.name, expectedUnsavedChanges); wrapper.setProps({ unsavedChanges: expectedUnsavedChanges }); const updated = wrapper.update(); const errorMessage = updated.find('.euiFormErrorText').text(); expect(errorMessage).toEqual(expectedUnsavedChanges.error); }); } it('should be able to change value', async () => { await (component.instance() as Field).onFieldChange(fieldUserValue); const updated = wrapper.update(); expect(handleChange).toBeCalledWith(setting.name, { value: fieldUserValue }); updated.setProps({ unsavedChanges: { value: fieldUserValue } }); const currentValue = getFieldSettingValue(updated, setting.name, type); expect(currentValue).toEqual(fieldUserValue); }); it('should be able to reset to default value', async () => { await wrapper.setProps({ unsavedChanges: {}, setting: { ...setting, value: fieldUserValue }, }); const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); const expectedEditableValue = getEditableValue(setting.type, setting.defVal); expect(handleChange).toBeCalledWith(setting.name, { value: expectedEditableValue, }); updated.setProps({ unsavedChanges: { value: expectedEditableValue } }); const currentValue = getFieldSettingValue(updated, setting.name, type); expect(currentValue).toEqual(expectedEditableValue); }); }); } }); });