diff --git a/packages/form-js-viewer/src/Form.js b/packages/form-js-viewer/src/Form.js index a8bc11ff8..f483aa8ee 100644 --- a/packages/form-js-viewer/src/Form.js +++ b/packages/form-js-viewer/src/Form.js @@ -1,580 +1,580 @@ -import Ids from 'ids'; -import { get, isObject, isString, isUndefined, set } from 'min-dash'; - -import { - ExpressionLanguageModule, - MarkdownRendererModule, - ViewerCommandsModule, - RepeatRenderModule -} from './features'; - -import { CoreModule } from './core'; - -import { clone, createFormContainer, createInjector } from './util'; - -/** - * @typedef { import('./types').Injector } Injector - * @typedef { import('./types').Data } Data - * @typedef { import('./types').Errors } Errors - * @typedef { import('./types').Schema } Schema - * @typedef { import('./types').FormProperties } FormProperties - * @typedef { import('./types').FormProperty } FormProperty - * @typedef { import('./types').FormEvent } FormEvent - * @typedef { import('./types').FormOptions } FormOptions - * - * @typedef { { - * data: Data, - * initialData: Data, - * errors: Errors, - * properties: FormProperties, - * schema: Schema - * } } State - * - * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority - * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority - * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType - */ - -const ids = new Ids([ 32, 36, 1 ]); - -/** - * The form. - */ -export class Form { - - /** - * @constructor - * @param {FormOptions} options - */ - constructor(options = {}) { - - /** - * @public - * @type {OnEventType} - */ - this.on = this._onEvent; - - /** - * @public - * @type {String} - */ - this._id = ids.next(); - - /** - * @private - * @type {Element} - */ - this._container = createFormContainer(); - - const { - container, - injector = this._createInjector(options, this._container), - properties = {} - } = options; - - /** - * @private - * @type {State} - */ - this._state = { - initialData: null, - data: null, - properties, - errors: {}, - schema: null - }; - - this.get = injector.get; - - this.invoke = injector.invoke; - - this.get('eventBus').fire('form.init'); - - if (container) { - this.attachTo(container); - } - } - - clear() { - - // clear diagram services (e.g. EventBus) - this._emit('diagram.clear'); - - // clear form services - this._emit('form.clear'); - } - - /** - * Destroy the form, removing it from DOM, - * if attached. - */ - destroy() { - - // destroy form services - this.get('eventBus').fire('form.destroy'); - - // destroy diagram services (e.g. EventBus) - this.get('eventBus').fire('diagram.destroy'); - - this._detach(false); - } - - /** - * Open a form schema with the given initial data. - * - * @param {Schema} schema - * @param {Data} [data] - * - * @return Promise<{ warnings: Array }> - */ - importSchema(schema, data = {}) { - return new Promise((resolve, reject) => { - try { - this.clear(); - - const { - schema: importedSchema, - warnings - } = this.get('importer').importSchema(schema); - - const initializedData = this._getInitializedFieldData(clone(data)); - - this._setState({ - data: initializedData, - errors: {}, - schema: importedSchema, - initialData: clone(initializedData) - }); - - this._emit('import.done', { warnings }); - - return resolve({ warnings }); - } catch (error) { - this._emit('import.done', { - error, - warnings: error.warnings || [] - }); - - return reject(error); - } - }); - } - - /** - * Submit the form, triggering all field validations. - * - * @returns { { data: Data, errors: Errors } } - */ - submit() { - - const { - properties - } = this._getState(); - - if (properties.readOnly || properties.disabled) { - throw new Error('form is read-only'); - } - - const data = this._getSubmitData(); - - const errors = this.validate(); - - const result = { - data, - errors - }; - - this._emit('submit', result); - - return result; - } - - reset() { - this._emit('reset'); - - this._setState({ - data: clone(this._state.initialData), - errors: {} - }); - } - - /** - * @returns {Errors} - */ - validate() { - const formFields = this.get('formFields'), - formFieldRegistry = this.get('formFieldRegistry'), - pathRegistry = this.get('pathRegistry'), - validator = this.get('validator'); - - const { data } = this._getState(); - - const getErrorPath = (field, indexes) => [ field.id, ...Object.values(indexes || {}) ]; - - function validateFieldRecursively(errors, field, indexes) { - const { disabled, type, isRepeating } = field; - const { config: fieldConfig } = formFields.get(type); - - // (1) Skip disabled fields - if (disabled) { - return; - } - - // (2) Validate the field - const valuePath = pathRegistry.getValuePath(field, { indexes }); - const valueData = get(data, valuePath); - const fieldErrors = validator.validateField(field, valueData); - - if (fieldErrors.length) { - set(errors, getErrorPath(field, indexes), fieldErrors); - } - - // (3) Process parents - if (!Array.isArray(field.components)) { - return; - } - - // (4a) Recurse repeatable parents both across the indexes of repetition and the children - if (fieldConfig.repeatable && isRepeating) { - - if (!Array.isArray(valueData)) { - return; - } - - valueData.forEach((_, index) => { - field.components.forEach((component) => { - validateFieldRecursively(errors, component, { ...indexes, [field.id]: index }); - }); - }); - - return; - } - - // (4b) Recurse non-repeatable parents only across the children - field.components.forEach((component) => validateFieldRecursively(errors, component, indexes)); - } - - const workingErrors = {}; - validateFieldRecursively(workingErrors, formFieldRegistry.getForm()); - const filteredErrors = this._applyConditions(workingErrors, data, { getFilterPath: getErrorPath, leafNodeDeletionOnly: true }); - this._setState({ errors: filteredErrors }); - - return filteredErrors; - } - - /** - * @param {Element|string} parentNode - */ - attachTo(parentNode) { - if (!parentNode) { - throw new Error('parentNode required'); - } - - this.detach(); - - if (isString(parentNode)) { - parentNode = document.querySelector(parentNode); - } - - const container = this._container; - - parentNode.appendChild(container); - - this._emit('attach'); - } - - detach() { - this._detach(); - } - - /** - * @private - * - * @param {boolean} [emit] - */ - _detach(emit = true) { - const container = this._container, - parentNode = container.parentNode; - - if (!parentNode) { - return; - } - - if (emit) { - this._emit('detach'); - } - - parentNode.removeChild(container); - } - - /** - * @param {FormProperty} property - * @param {any} value - */ - setProperty(property, value) { - const properties = set(this._getState().properties, [ property ], value); - - this._setState({ properties }); - } - - /** - * @param {FormEvent} type - * @param {Function} handler - */ - off(type, handler) { - this.get('eventBus').off(type, handler); - } - - /** - * @private - * - * @param {FormOptions} options - * @param {Element} container - * - * @returns {Injector} - */ - _createInjector(options, container) { - const { - modules = this._getModules(), - additionalModules = [], - ...config - } = options; - - const enrichedConfig = { - ...config, - renderer: { - container - } - }; - - return createInjector([ - { config: [ 'value', enrichedConfig ] }, - { form: [ 'value', this ] }, - CoreModule, - ...modules, - ...additionalModules - ]); - } - - /** - * @private - */ - _emit(type, data) { - this.get('eventBus').fire(type, data); - } - - /** - * @internal - * - * @param { { add?: boolean, field: any, indexes: object, remove?: number, value?: any } } update - */ - _update(update) { - const { - field, - indexes, - value - } = update; - - const { - data, - errors - } = this._getState(); - - const validator = this.get('validator'), - pathRegistry = this.get('pathRegistry'); - - const fieldErrors = validator.validateField(field, value); - - const valuePath = pathRegistry.getValuePath(field, { indexes }); - - set(data, valuePath, value); - - set(errors, [ field.id, ...Object.values(indexes || {}) ], fieldErrors.length ? fieldErrors : undefined); - - this._setState({ - data: clone(data), - errors: clone(errors) - }); - } - - /** - * @internal - */ - _getState() { - return this._state; - } - - /** - * @internal - */ - _setState(state) { - this._state = { - ...this._state, - ...state - }; - - this._emit('changed', this._getState()); - } - - /** - * @internal - */ - _getModules() { - return [ - ExpressionLanguageModule, - MarkdownRendererModule, - ViewerCommandsModule, - RepeatRenderModule - ]; - } - - /** - * @internal - */ - _onEvent(type, priority, handler) { - this.get('eventBus').on(type, priority, handler); - } - - /** - * @internal - */ - _getSubmitData() { - const formFieldRegistry = this.get('formFieldRegistry'); - const formFields = this.get('formFields'); - const pathRegistry = this.get('pathRegistry'); - const formData = this._getState().data; - - function collectSubmitDataRecursively(submitData, formField, indexes) { - const { disabled, type } = formField; - const { config: fieldConfig } = formFields.get(type); - - // (1) Process keyed fields - if (!disabled && fieldConfig.keyed) { - const valuePath = pathRegistry.getValuePath(formField, { indexes }); - const value = get(formData, valuePath); - set(submitData, valuePath, value); - } - - // (2) Process parents - if (!Array.isArray(formField.components)) { - return; - } - - // (3a) Recurse repeatable parents both across the indexes of repetition and the children - if (fieldConfig.repeatable && formField.isRepeating) { - - const valueData = get(formData, pathRegistry.getValuePath(formField, { indexes })); - - if (!Array.isArray(valueData)) { - return; - } - - valueData.forEach((_, index) => { - formField.components.forEach((component) => { - collectSubmitDataRecursively(submitData, component, { ...indexes, [formField.id]: index }); - }); - }); - - return; - } - - // (3b) Recurse non-repeatable parents only across the children - formField.components.forEach((component) => collectSubmitDataRecursively(submitData, component, indexes)); - } - - const workingSubmitData = {}; - collectSubmitDataRecursively(workingSubmitData, formFieldRegistry.getForm(), {}); - return this._applyConditions(workingSubmitData, formData); - } - - /** - * @internal - */ - _applyConditions(toFilter, data, options = {}) { - const conditionChecker = this.get('conditionChecker'); - return conditionChecker.applyConditions(toFilter, data, options); - } - - /** - * @internal - */ - _getInitializedFieldData(data, options = {}) { - const formFieldRegistry = this.get('formFieldRegistry'); - const formFields = this.get('formFields'); - const pathRegistry = this.get('pathRegistry'); - - function initializeFieldDataRecursively(initializedData, formField, indexes) { - const { defaultValue, type, isRepeating } = formField; - const { config: fieldConfig } = formFields.get(type); - - const valuePath = pathRegistry.getValuePath(formField, { indexes }); - let valueData = get(data, valuePath); - - // (1) Process keyed fields - if (fieldConfig.keyed) { - - // (a) Retrieve and sanitize data from input - if (!isUndefined(valueData) && fieldConfig.sanitizeValue) { - valueData = fieldConfig.sanitizeValue({ formField, data, value: valueData }); - } - - // (b) Initialize field value in output data - const initializedFieldValue = !isUndefined(valueData) ? valueData : (!isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue); - set(initializedData, valuePath, initializedFieldValue); - } - - // (2) Process parents - if (!Array.isArray(formField.components)) { - return; - } - - if (fieldConfig.repeatable && isRepeating) { - - // (a) Sanitize repeatable parents data if it is not an array - if (!valueData || !Array.isArray(valueData)) { - valueData = new Array(isUndefined(formField.defaultRepetitions) ? 1 : formField.defaultRepetitions).fill().map(_ => ({})) || []; - } - - // (b) Ensure all elements of the array are objects - valueData = valueData.map((val) => isObject(val) ? val : {}); - - // (c) Initialize field value in output data - set(initializedData, valuePath, valueData); - - // (d) If indexed ahead of time, recurse repeatable simply across the children - if (!isUndefined(indexes[formField.id])) { - formField.components.forEach( - (component) => initializeFieldDataRecursively(initializedData, component, { ...indexes }) - ); - - return; - } - - // (e1) Recurse repeatable parents both across the indexes of repetition and the children - valueData.forEach((_, index) => { - formField.components.forEach( - (component) => initializeFieldDataRecursively(initializedData, component, { ...indexes, [formField.id]: index }) - ); - }); - - return; - } - - // (e2) Recurse non-repeatable parents only across the children - formField.components.forEach((component) => initializeFieldDataRecursively(initializedData, component, indexes)); - } - - // allows definition of a specific subfield to generate the data for - const container = options.container || formFieldRegistry.getForm(); - const indexes = options.indexes || {}; - const basePath = pathRegistry.getValuePath(container, { indexes }) || []; - - // if indexing ahead of time, we must add this index to the data path at the end - const path = !isUndefined(indexes[container.id]) ? [ ...basePath, indexes[container.id] ] : basePath; - - const workingData = clone(data); - initializeFieldDataRecursively(workingData, container, indexes); - return get(workingData, path, {}); - } - -} +import Ids from 'ids'; +import { get, isObject, isString, isUndefined, set } from 'min-dash'; + +import { + ExpressionLanguageModule, + MarkdownRendererModule, + ViewerCommandsModule, + RepeatRenderModule +} from './features'; + +import { CoreModule } from './core'; + +import { clone, createFormContainer, createInjector } from './util'; + +/** + * @typedef { import('./types').Injector } Injector + * @typedef { import('./types').Data } Data + * @typedef { import('./types').Errors } Errors + * @typedef { import('./types').Schema } Schema + * @typedef { import('./types').FormProperties } FormProperties + * @typedef { import('./types').FormProperty } FormProperty + * @typedef { import('./types').FormEvent } FormEvent + * @typedef { import('./types').FormOptions } FormOptions + * + * @typedef { { + * data: Data, + * initialData: Data, + * errors: Errors, + * properties: FormProperties, + * schema: Schema + * } } State + * + * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority + * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority + * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType + */ + +const ids = new Ids([ 32, 36, 1 ]); + +/** + * The form. + */ +export class Form { + + /** + * @constructor + * @param {FormOptions} options + */ + constructor(options = {}) { + + /** + * @public + * @type {OnEventType} + */ + this.on = this._onEvent; + + /** + * @public + * @type {String} + */ + this._id = ids.next(); + + /** + * @private + * @type {Element} + */ + this._container = createFormContainer(); + + const { + container, + injector = this._createInjector(options, this._container), + properties = {} + } = options; + + /** + * @private + * @type {State} + */ + this._state = { + initialData: null, + data: null, + properties, + errors: {}, + schema: null + }; + + this.get = injector.get; + + this.invoke = injector.invoke; + + this.get('eventBus').fire('form.init'); + + if (container) { + this.attachTo(container); + } + } + + clear() { + + // clear diagram services (e.g. EventBus) + this._emit('diagram.clear'); + + // clear form services + this._emit('form.clear'); + } + + /** + * Destroy the form, removing it from DOM, + * if attached. + */ + destroy() { + + // destroy form services + this.get('eventBus').fire('form.destroy'); + + // destroy diagram services (e.g. EventBus) + this.get('eventBus').fire('diagram.destroy'); + + this._detach(false); + } + + /** + * Open a form schema with the given initial data. + * + * @param {Schema} schema + * @param {Data} [data] + * + * @return Promise<{ warnings: Array }> + */ + importSchema(schema, data = {}) { + return new Promise((resolve, reject) => { + try { + this.clear(); + + const { + schema: importedSchema, + warnings + } = this.get('importer').importSchema(schema); + + const initializedData = this._getInitializedFieldData(clone(data)); + + this._setState({ + data: initializedData, + errors: {}, + schema: importedSchema, + initialData: clone(initializedData) + }); + + this._emit('import.done', { warnings }); + + return resolve({ warnings }); + } catch (error) { + this._emit('import.done', { + error, + warnings: error.warnings || [] + }); + + return reject(error); + } + }); + } + + /** + * Submit the form, triggering all field validations. + * + * @returns { { data: Data, errors: Errors } } + */ + submit() { + + const { + properties + } = this._getState(); + + if (properties.readOnly || properties.disabled) { + throw new Error('form is read-only'); + } + + const data = this._getSubmitData(); + + const errors = this.validate(); + + const result = { + data, + errors + }; + + this._emit('submit', result); + + return result; + } + + reset() { + this._emit('reset'); + + this._setState({ + data: clone(this._state.initialData), + errors: {} + }); + } + + /** + * @returns {Errors} + */ + validate() { + const formFields = this.get('formFields'), + formFieldRegistry = this.get('formFieldRegistry'), + pathRegistry = this.get('pathRegistry'), + validator = this.get('validator'); + + const { data } = this._getState(); + + const getErrorPath = (field, indexes) => [ field.id, ...Object.values(indexes || {}) ]; + + function validateFieldRecursively(errors, field, indexes) { + const { disabled, type, isRepeating } = field; + const { config: fieldConfig } = formFields.get(type); + + // (1) Skip disabled fields + if (disabled) { + return; + } + + // (2) Validate the field + const valuePath = pathRegistry.getValuePath(field, { indexes }); + const valueData = get(data, valuePath); + const fieldErrors = validator.validateField(field, valueData); + + if (fieldErrors.length) { + set(errors, getErrorPath(field, indexes), fieldErrors); + } + + // (3) Process parents + if (!Array.isArray(field.components)) { + return; + } + + // (4a) Recurse repeatable parents both across the indexes of repetition and the children + if (fieldConfig.repeatable && isRepeating) { + + if (!Array.isArray(valueData)) { + return; + } + + valueData.forEach((_, index) => { + field.components.forEach((component) => { + validateFieldRecursively(errors, component, { ...indexes, [field.id]: index }); + }); + }); + + return; + } + + // (4b) Recurse non-repeatable parents only across the children + field.components.forEach((component) => validateFieldRecursively(errors, component, indexes)); + } + + const workingErrors = {}; + validateFieldRecursively(workingErrors, formFieldRegistry.getForm()); + const filteredErrors = this._applyConditions(workingErrors, data, { getFilterPath: getErrorPath, leafNodeDeletionOnly: true }); + this._setState({ errors: filteredErrors }); + + return filteredErrors; + } + + /** + * @param {Element|string} parentNode + */ + attachTo(parentNode) { + if (!parentNode) { + throw new Error('parentNode required'); + } + + this.detach(); + + if (isString(parentNode)) { + parentNode = document.querySelector(parentNode); + } + + const container = this._container; + + parentNode.appendChild(container); + + this._emit('attach'); + } + + detach() { + this._detach(); + } + + /** + * @private + * + * @param {boolean} [emit] + */ + _detach(emit = true) { + const container = this._container, + parentNode = container.parentNode; + + if (!parentNode) { + return; + } + + if (emit) { + this._emit('detach'); + } + + parentNode.removeChild(container); + } + + /** + * @param {FormProperty} property + * @param {any} value + */ + setProperty(property, value) { + const properties = set(this._getState().properties, [ property ], value); + + this._setState({ properties }); + } + + /** + * @param {FormEvent} type + * @param {Function} handler + */ + off(type, handler) { + this.get('eventBus').off(type, handler); + } + + /** + * @private + * + * @param {FormOptions} options + * @param {Element} container + * + * @returns {Injector} + */ + _createInjector(options, container) { + const { + modules = this._getModules(), + additionalModules = [], + ...config + } = options; + + const enrichedConfig = { + ...config, + renderer: { + container + } + }; + + return createInjector([ + { config: [ 'value', enrichedConfig ] }, + { form: [ 'value', this ] }, + CoreModule, + ...modules, + ...additionalModules + ]); + } + + /** + * @private + */ + _emit(type, data) { + this.get('eventBus').fire(type, data); + } + + /** + * @internal + * + * @param { { add?: boolean, field: any, indexes: object, remove?: number, value?: any } } update + */ + _update(update) { + const { + field, + indexes, + value + } = update; + + const { + data, + errors + } = this._getState(); + + const validator = this.get('validator'), + pathRegistry = this.get('pathRegistry'); + + const fieldErrors = validator.validateField(field, value); + + const valuePath = pathRegistry.getValuePath(field, { indexes }); + + set(data, valuePath, value); + + set(errors, [ field.id, ...Object.values(indexes || {}) ], fieldErrors.length ? fieldErrors : undefined); + + this._setState({ + data: clone(data), + errors: clone(errors) + }); + } + + /** + * @internal + */ + _getState() { + return this._state; + } + + /** + * @internal + */ + _setState(state) { + this._state = { + ...this._state, + ...state + }; + + this._emit('changed', this._getState()); + } + + /** + * @internal + */ + _getModules() { + return [ + ExpressionLanguageModule, + MarkdownRendererModule, + ViewerCommandsModule, + RepeatRenderModule + ]; + } + + /** + * @internal + */ + _onEvent(type, priority, handler) { + this.get('eventBus').on(type, priority, handler); + } + + /** + * @internal + */ + _getSubmitData() { + const formFieldRegistry = this.get('formFieldRegistry'); + const formFields = this.get('formFields'); + const pathRegistry = this.get('pathRegistry'); + const formData = this._getState().data; + + function collectSubmitDataRecursively(submitData, formField, indexes) { + const { disabled, type } = formField; + const { config: fieldConfig } = formFields.get(type); + + // (1) Process keyed fields + if (!disabled && fieldConfig.keyed) { + const valuePath = pathRegistry.getValuePath(formField, { indexes }); + const value = get(formData, valuePath); + set(submitData, valuePath, value); + } + + // (2) Process parents + if (!Array.isArray(formField.components)) { + return; + } + + // (3a) Recurse repeatable parents both across the indexes of repetition and the children + if (fieldConfig.repeatable && formField.isRepeating) { + + const valueData = get(formData, pathRegistry.getValuePath(formField, { indexes })); + + if (!Array.isArray(valueData)) { + return; + } + + valueData.forEach((_, index) => { + formField.components.forEach((component) => { + collectSubmitDataRecursively(submitData, component, { ...indexes, [formField.id]: index }); + }); + }); + + return; + } + + // (3b) Recurse non-repeatable parents only across the children + formField.components.forEach((component) => collectSubmitDataRecursively(submitData, component, indexes)); + } + + const workingSubmitData = {}; + collectSubmitDataRecursively(workingSubmitData, formFieldRegistry.getForm(), {}); + return this._applyConditions(workingSubmitData, formData); + } + + /** + * @internal + */ + _applyConditions(toFilter, data, options = {}) { + const conditionChecker = this.get('conditionChecker'); + return conditionChecker.applyConditions(toFilter, data, options); + } + + /** + * @internal + */ + _getInitializedFieldData(data, options = {}) { + const formFieldRegistry = this.get('formFieldRegistry'); + const formFields = this.get('formFields'); + const pathRegistry = this.get('pathRegistry'); + + function initializeFieldDataRecursively(initializedData, formField, indexes) { + const { defaultValue, type, isRepeating } = formField; + const { config: fieldConfig } = formFields.get(type); + + const valuePath = pathRegistry.getValuePath(formField, { indexes }); + let valueData = get(data, valuePath); + + // (1) Process keyed fields + if (fieldConfig.keyed) { + + // (a) Retrieve and sanitize data from input + if (!isUndefined(valueData) && fieldConfig.sanitizeValue) { + valueData = fieldConfig.sanitizeValue({ formField, data, value: valueData }); + } + + // (b) Initialize field value in output data + const initializedFieldValue = !isUndefined(valueData) ? valueData : (!isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue); + set(initializedData, valuePath, initializedFieldValue); + } + + // (2) Process parents + if (!Array.isArray(formField.components)) { + return; + } + + if (fieldConfig.repeatable && isRepeating) { + + // (a) Sanitize repeatable parents data if it is not an array + if (!valueData || !Array.isArray(valueData)) { + valueData = new Array(isUndefined(formField.defaultRepetitions) ? 1 : formField.defaultRepetitions).fill().map(_ => ({})) || []; + } + + // (b) Ensure all elements of the array are objects + valueData = valueData.map((val) => isObject(val) ? val : {}); + + // (c) Initialize field value in output data + set(initializedData, valuePath, valueData); + + // (d) If indexed ahead of time, recurse repeatable simply across the children + if (!isUndefined(indexes[formField.id])) { + formField.components.forEach( + (component) => initializeFieldDataRecursively(initializedData, component, { ...indexes }) + ); + + return; + } + + // (e1) Recurse repeatable parents both across the indexes of repetition and the children + valueData.forEach((_, index) => { + formField.components.forEach( + (component) => initializeFieldDataRecursively(initializedData, component, { ...indexes, [formField.id]: index }) + ); + }); + + return; + } + + // (e2) Recurse non-repeatable parents only across the children + formField.components.forEach((component) => initializeFieldDataRecursively(initializedData, component, indexes)); + } + + // allows definition of a specific subfield to generate the data for + const container = options.container || formFieldRegistry.getForm(); + const indexes = options.indexes || {}; + const basePath = pathRegistry.getValuePath(container, { indexes }) || []; + + // if indexing ahead of time, we must add this index to the data path at the end + const path = !isUndefined(indexes[container.id]) ? [ ...basePath, indexes[container.id] ] : basePath; + + const workingData = clone(data); + initializeFieldDataRecursively(workingData, container, indexes); + return get(workingData, path, {}); + } + +} diff --git a/packages/form-js-viewer/src/render/components/util/optionsUtil.js b/packages/form-js-viewer/src/render/components/util/optionsUtil.js index 071d5e8b0..2323dbeda 100644 --- a/packages/form-js-viewer/src/render/components/util/optionsUtil.js +++ b/packages/form-js-viewer/src/render/components/util/optionsUtil.js @@ -1,16 +1,65 @@ import { get, isObject, isString, isNil } from 'min-dash'; -// parses the options data from the provided form field and form data -export function getOptionsData(formField, formData) { - const { valuesKey: optionsKey, values: staticOptions } = formField; +/** + * Returns the options data for the provided if they can be simply determined, ignoring expression defined options. + * + * @param {object} formField + * @param {object} formData + */ +function getSimpleOptionsData(formField, formData) { + + const { + valuesExpression: optionsExpression, + valuesKey: optionsKey, + values: staticOptions + } = formField; + + if (optionsExpression) { + return null; + } + return optionsKey ? get(formData, [ optionsKey ]) : staticOptions; } -// transforms the provided options into a normalized format, trimming invalid options -export function normalizeOptionsData(optionsData) { +/** + * Normalizes the provided options data to a format that can be used by the select components. + * If the options data is not valid, it is filtered out. + * + * @param {any[]} optionsData + * + * @returns {object[]} + */ +function normalizeOptionsData(optionsData) { return optionsData.filter(_isAllowedValue).map(_normalizeOption).filter(o => !isNil(o)); } +/** + * Creates an options object with default values if no options are provided. + * + * @param {object} options + * + * @returns {object} + */ +function createEmptyOptions(options = {}) { + + const defaults = {}; + + // provide default options if valuesKey and valuesExpression are not set + if (!options.valuesKey && !options.valuesExpression) { + defaults.values = [ + { + label: 'Value', + value: 'value' + } + ]; + } + + return { + ...defaults, + ...options + }; +} + /** * Converts the provided option to a normalized format. * If the option is not valid, null is returned. @@ -43,7 +92,6 @@ function _normalizeOption(option) { } } - return null; } @@ -66,22 +114,4 @@ function _isAllowedValue(value) { return _isAllowedPrimitive(value); } -export function createEmptyOptions(options = {}) { - - const defaults = {}; - - // provide default options if valuesKey and valuesExpression are not set - if (!options.valuesKey && !options.valuesExpression) { - defaults.values = [ - { - label: 'Value', - value: 'value' - } - ]; - } - - return { - ...defaults, - ...options - }; -} \ No newline at end of file +export { getSimpleOptionsData, normalizeOptionsData, createEmptyOptions }; diff --git a/packages/form-js-viewer/src/render/components/util/sanitizerUtil.js b/packages/form-js-viewer/src/render/components/util/sanitizerUtil.js index aa3b0e27c..47207f375 100644 --- a/packages/form-js-viewer/src/render/components/util/sanitizerUtil.js +++ b/packages/form-js-viewer/src/render/components/util/sanitizerUtil.js @@ -1,7 +1,7 @@ import isEqual from 'lodash/isEqual'; import { DATETIME_SUBTYPES } from '../../../util/constants/DatetimeConstants'; import { isDateInputInformationMatching, isDateTimeInputInformationSufficient, isInvalidDateString, parseIsoTime } from './dateTimeUtil'; -import { getOptionsData, normalizeOptionsData } from './optionsUtil'; +import { getSimpleOptionsData, normalizeOptionsData } from './optionsUtil'; const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape const ALLOWED_IFRAME_SRC_PATTERN = /^(https):\/\/*/i; // eslint-disable-line no-useless-escape @@ -39,8 +39,19 @@ export function sanitizeSingleSelectValue(options) { value } = options; + const { + valuesExpression: optionsExpression + } = formField; + try { - const validValues = normalizeOptionsData(getOptionsData(formField, data)).map(v => v.value); + + // if options are expression evaluated, we don't need to sanitize the value against the options + // and defer to the field's internal validation + if (optionsExpression) { + return value; + } + + const validValues = normalizeOptionsData(getSimpleOptionsData(formField, data)).map(v => v.value); return hasEqualValue(value, validValues) ? value : null; } catch (error) { @@ -57,8 +68,19 @@ export function sanitizeMultiSelectValue(options) { value } = options; + const { + valuesExpression: optionsExpression + } = formField; + try { - const validValues = normalizeOptionsData(getOptionsData(formField, data)).map(v => v.value); + + // if options are expression evaluated, we don't need to sanitize the values against the options + // and defer to the field's internal validation + if (optionsExpression) { + return value; + } + + const validValues = normalizeOptionsData(getSimpleOptionsData(formField, data)).map(v => v.value); return value.filter(v => hasEqualValue(v, validValues)); } catch (error) { diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/Checklist.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/Checklist.spec.js index b9a60759c..1050ea92a 100644 --- a/packages/form-js-viewer/test/spec/render/components/form-fields/Checklist.spec.js +++ b/packages/form-js-viewer/test/spec/render/components/form-fields/Checklist.spec.js @@ -1,652 +1,695 @@ -import { - fireEvent, - render -} from '@testing-library/preact/pure'; - -import { Checklist } from '../../../../../src/render/components/form-fields/Checklist'; - -import { - createFormContainer, - expectNoViolations -} from '../../../../TestHelper'; - -import { MockFormContext } from '../helper'; - -const spy = sinon.spy; - -let container; - -describe('Checklist', function() { - - beforeEach(function() { - container = createFormContainer(); - }); - - afterEach(function() { - container.remove(); - }); - - - it('should render', function() { - - // when - const { container } = createChecklist({ - value: [ 'approver' ] - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; - - const inputs = container.querySelectorAll('input[type="checkbox"]'); - - expect(inputs).to.have.length(3); - expect(inputs[ 0 ].id).to.equal('test-checklist-0'); - expect(inputs[ 1 ].id).to.equal('test-checklist-1'); - expect(inputs[ 2 ].id).to.equal('test-checklist-2'); - - expect(inputs[ 0 ].checked).to.be.true; - expect(inputs[ 1 ].checked).to.be.false; - expect(inputs[ 2 ].checked).to.be.false; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(4); - expect(labels[ 0 ].textContent).to.equal('Email data to'); - expect(labels[ 1 ].htmlFor).to.equal('test-checklist-0'); - expect(labels[ 2 ].htmlFor).to.equal('test-checklist-1'); - expect(labels[ 3 ].htmlFor).to.equal('test-checklist-2'); - }); - - - it('should render required label', function() { - - // when - const { container } = createChecklist({ - field: { - ...defaultField, - label: 'Required', - validate: { - required: true - } - } - }); - - // then - const label = container.querySelector('label'); - - expect(label).to.exist; - expect(label.textContent).to.equal('Required*'); - }); - - - it('should render dynamically', function() { - - // when - const { container } = createChecklist({ - value: [ 'dynamicValue1' ], - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; - - const inputs = container.querySelectorAll('input[type="checkbox"]'); - - expect(inputs).to.have.length(3); - expect(inputs[0].id).to.equal('test-checklist-0'); - expect(inputs[1].id).to.equal('test-checklist-1'); - expect(inputs[2].id).to.equal('test-checklist-2'); - - expect(inputs[0].checked).to.be.true; - expect(inputs[1].checked).to.be.false; - expect(inputs[2].checked).to.be.false; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(4); - expect(labels[0].textContent).to.equal('Email data to'); - expect(labels[1].htmlFor).to.equal('test-checklist-0'); - expect(labels[2].htmlFor).to.equal('test-checklist-1'); - expect(labels[3].htmlFor).to.equal('test-checklist-2'); - }); - - - it('should render dynamically with simplified values', function() { - - // when - const { container } = createChecklist({ - value: [ 'dynamicValue1' ], - field: dynamicField, - initialData: dynamicFieldInitialDataSimplified - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; - - const inputs = container.querySelectorAll('input[type="checkbox"]'); - expect(inputs).to.have.length(3); - expect(inputs[0].id).to.equal('test-checklist-0'); - expect(inputs[1].id).to.equal('test-checklist-1'); - expect(inputs[2].id).to.equal('test-checklist-2'); - - expect(inputs[0].checked).to.be.true; - expect(inputs[1].checked).to.be.false; - expect(inputs[2].checked).to.be.false; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(4); - expect(labels[0].textContent).to.equal('Email data to'); - expect(labels[1].htmlFor).to.equal('test-checklist-0'); - expect(labels[2].htmlFor).to.equal('test-checklist-1'); - expect(labels[3].htmlFor).to.equal('test-checklist-2'); - }); - - - it('should render dynamically with object values', function() { - - // when - const { container } = createChecklist({ - value: [ { - id: 'user3', - name: 'User 3', - email: 'user3@email.com' - } ], - field: dynamicField, - initialData: dynamicFieldInitialDataObjectValues - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; - - const inputs = container.querySelectorAll('input[type="checkbox"]'); - expect(inputs).to.have.length(3); - expect(inputs[0].id).to.equal('test-checklist-0'); - expect(inputs[1].id).to.equal('test-checklist-1'); - expect(inputs[2].id).to.equal('test-checklist-2'); - - expect(inputs[0].checked).to.be.false; - expect(inputs[1].checked).to.be.false; - expect(inputs[2].checked).to.be.true; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(4); - expect(labels[0].textContent).to.equal('Email data to'); - expect(labels[1].htmlFor).to.equal('test-checklist-0'); - expect(labels[2].htmlFor).to.equal('test-checklist-1'); - expect(labels[3].htmlFor).to.equal('test-checklist-2'); - }); - - - it('should render default value (undefined)', function() { - - // when - const { container } = createChecklist(); - - // then - const inputs = container.querySelectorAll('input[type="checkbox"]'); - - inputs.forEach(input => { - expect(input.checked).to.be.false; - }); - }); - - - it('should render disabled', function() { - - // when - const { container } = createChecklist({ - disabled: true - }); - - // then - const inputs = container.querySelectorAll('input[type="checkbox"]'); - - inputs.forEach(input => { - expect(input.disabled).to.be.true; - }); - }); - - - it('should render readonly', function() { - - // when - const { container } = createChecklist({ - readonly: true - }); - - // then - const inputs = container.querySelectorAll('input[type="checkbox"]'); - - inputs.forEach(input => { - expect(input.readOnly).to.be.true; - }); - }); - - - it('should render description', function() { - - // when - const { container } = createChecklist({ - field: { - ...defaultField, - description: 'foo' - } - }); - - // then - const description = container.querySelector('.fjs-form-field-description'); - - expect(description).to.exist; - expect(description.textContent).to.equal('foo'); - }); - - - describe('handle change (static)', function() { - - it('should handle change', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ 'approver' ] - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[ 1 ]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: defaultField, - value: [ 'approver', 'manager' ] - }); - }); - - - it('should handle toggle', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ 'approver' ] - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[ 0 ]; - - fireEvent.click(input, { target: { checked: false } }); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: defaultField, - value: [] - }); - }); - - }); - - - describe('handle change (dynamic)', function() { - - it('should handle change', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ 'dynamicValue1' ], - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[1]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: [ 'dynamicValue1', 'dynamicValue2' ] - }); - }); - - - it('should handle change simplified values', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ 'dynamicValue1' ], - field: dynamicField, - initialData: dynamicFieldInitialDataSimplified - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[1]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: [ 'dynamicValue1', 'dynamicValue2' ] - }); - - }); - - - it('should handle change object values', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ { - id: 'user3', - name: 'User 3', - email: 'user3@email.com' - } ], - field: dynamicField, - initialData: dynamicFieldInitialDataObjectValues - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[1]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: [ { - id: 'user3', - name: 'User 3', - email: 'user3@email.com' - }, { - id: 'user2', - name: 'User 2', - email: 'user2@email.com' - } ] - }); - - }); - - - it('should handle toggle', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ 'dynamicValue1' ], - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[0]; - - fireEvent.click(input, { target: { checked: false } }); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: [] - }); - }); - - - it('should handle toggle object values', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createChecklist({ - onChange: onChangeSpy, - value: [ { - id: 'user3', - name: 'User 3', - email: 'user3@email.com' - } ], - field: dynamicField, - initialData: dynamicFieldInitialDataObjectValues - }); - - // when - const input = container.querySelectorAll('input[type="checkbox"]')[2]; - - fireEvent.click(input, { target: { checked: false } }); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: [] - }); - - }); - - }); - - - it('#create', function() { - - // assume - const { config } = Checklist; - expect(config.type).to.eql('checklist'); - expect(config.label).to.eql('Checkbox group'); - expect(config.group).to.eql('selection'); - expect(config.keyed).to.be.true; - - // when - const field = config.create(); - - // then - expect(field).to.eql({ - values: [ - { - label: 'Value', - value: 'value' - } - ] - }); - - // but when - const customField = config.create({ - custom: true - }); - - // then - expect(customField).to.contain({ - custom: true - }); - }); - - - describe('a11y', function() { - - it('should have no violations', async function() { - - // given - this.timeout(10000); - - const { container } = createChecklist({ - value: [ 'approver' ] - }); - - // then - await expectNoViolations(container); - }); - - - it('should have no violations for readonly', async function() { - - // given - this.timeout(10000); - - const { container } = createChecklist({ - value: [ 'approver' ], - readonly: true - }); - - // then - await expectNoViolations(container); - }); - - - it('should have no violations for errors', async function() { - - // given - this.timeout(10000); - - const { container } = createChecklist({ - value: [ 'approver' ], - errors: [ 'Something went wrong' ] - }); - - // then - await expectNoViolations(container); - }); - - }); - -}); - -// helpers ////////// - -const defaultField = { - id: 'Checklist_1', - key: 'mailto', - label: 'Email data to', - type: 'checklist', - description: 'checklist', - values: [ - { - label: 'Approver', - value: 'approver' - }, - { - label: 'Manager', - value: 'manager' - }, - { - label: 'Regional Manager', - value: 'regional-manager' - } - ] -}; - -const dynamicField = { - id: 'Checklist_1', - key: 'mailto', - label: 'Email data to', - type: 'checklist', - valuesKey: 'dynamicValues' -}; - -const dynamicFieldInitialData = { - dynamicValues: [ - { - label: 'Dynamic Value 1', - value: 'dynamicValue1' - }, - { - label: 'Dynamic Value 2', - value: 'dynamicValue2' - }, - { - label: 'Dynamic Value 3', - value: 'dynamicValue3' - } - ] -}; - -const dynamicFieldInitialDataSimplified = { - dynamicValues: [ - 'dynamicValue1', - 'dynamicValue2', - 'dynamicValue3' - ] -}; - -const dynamicFieldInitialDataObjectValues = { - dynamicValues: [ - { - label: 'User 1', - value: { - id: 'user1', - name: 'User 1', - email: 'user1@email.com' - } - }, - { - label: 'User 2', - value: { - id: 'user2', - name: 'User 2', - email: 'user2@email.com' - } - }, - { - label: 'User 3', - value: { - id: 'user3', - name: 'User 3', - email: 'user3@email.com' - } - } - ] -}; - -function createChecklist({ services, ...restOptions } = {}) { - - const options = { - domId: 'test-checklist', - field: defaultField, - onChange: () => {}, - ...restOptions - }; - - return render( - - - , { - container: options.container || container.querySelector('.fjs-form') - } - ); +import { + fireEvent, + render +} from '@testing-library/preact/pure'; + +import { Checklist } from '../../../../../src/render/components/form-fields/Checklist'; + +import { + createFormContainer, + expectNoViolations +} from '../../../../TestHelper'; + +import { MockFormContext } from '../helper'; + +const spy = sinon.spy; + +let container; + +describe('Checklist', function() { + + beforeEach(function() { + container = createFormContainer(); + }); + + afterEach(function() { + container.remove(); + }); + + + it('should render', function() { + + // when + const { container } = createChecklist({ + value: [ 'approver' ] + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; + + const inputs = container.querySelectorAll('input[type="checkbox"]'); + + expect(inputs).to.have.length(3); + expect(inputs[ 0 ].id).to.equal('test-checklist-0'); + expect(inputs[ 1 ].id).to.equal('test-checklist-1'); + expect(inputs[ 2 ].id).to.equal('test-checklist-2'); + + expect(inputs[ 0 ].checked).to.be.true; + expect(inputs[ 1 ].checked).to.be.false; + expect(inputs[ 2 ].checked).to.be.false; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(4); + expect(labels[ 0 ].textContent).to.equal('Email data to'); + expect(labels[ 1 ].htmlFor).to.equal('test-checklist-0'); + expect(labels[ 2 ].htmlFor).to.equal('test-checklist-1'); + expect(labels[ 3 ].htmlFor).to.equal('test-checklist-2'); + }); + + + it('should render required label', function() { + + // when + const { container } = createChecklist({ + field: { + ...defaultField, + label: 'Required', + validate: { + required: true + } + } + }); + + // then + const label = container.querySelector('label'); + + expect(label).to.exist; + expect(label.textContent).to.equal('Required*'); + }); + + + it('should render dynamically', function() { + + // when + const { container } = createChecklist({ + value: [ 'dynamicValue1' ], + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; + + const inputs = container.querySelectorAll('input[type="checkbox"]'); + + expect(inputs).to.have.length(3); + expect(inputs[0].id).to.equal('test-checklist-0'); + expect(inputs[1].id).to.equal('test-checklist-1'); + expect(inputs[2].id).to.equal('test-checklist-2'); + + expect(inputs[0].checked).to.be.true; + expect(inputs[1].checked).to.be.false; + expect(inputs[2].checked).to.be.false; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(4); + expect(labels[0].textContent).to.equal('Email data to'); + expect(labels[1].htmlFor).to.equal('test-checklist-0'); + expect(labels[2].htmlFor).to.equal('test-checklist-1'); + expect(labels[3].htmlFor).to.equal('test-checklist-2'); + }); + + + it('should render dynamically with simplified values', function() { + + // when + const { container } = createChecklist({ + value: [ 'dynamicValue1' ], + field: dynamicField, + initialData: dynamicFieldInitialDataSimplified + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; + + const inputs = container.querySelectorAll('input[type="checkbox"]'); + expect(inputs).to.have.length(3); + expect(inputs[0].id).to.equal('test-checklist-0'); + expect(inputs[1].id).to.equal('test-checklist-1'); + expect(inputs[2].id).to.equal('test-checklist-2'); + + expect(inputs[0].checked).to.be.true; + expect(inputs[1].checked).to.be.false; + expect(inputs[2].checked).to.be.false; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(4); + expect(labels[0].textContent).to.equal('Email data to'); + expect(labels[1].htmlFor).to.equal('test-checklist-0'); + expect(labels[2].htmlFor).to.equal('test-checklist-1'); + expect(labels[3].htmlFor).to.equal('test-checklist-2'); + }); + + + it('should render dynamically with object values', function() { + + // when + const { container } = createChecklist({ + value: [ { + id: 'user3', + name: 'User 3', + email: 'user3@email.com' + } ], + field: dynamicField, + initialData: dynamicFieldInitialDataObjectValues + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-checklist')).to.be.true; + + const inputs = container.querySelectorAll('input[type="checkbox"]'); + expect(inputs).to.have.length(3); + expect(inputs[0].id).to.equal('test-checklist-0'); + expect(inputs[1].id).to.equal('test-checklist-1'); + expect(inputs[2].id).to.equal('test-checklist-2'); + + expect(inputs[0].checked).to.be.false; + expect(inputs[1].checked).to.be.false; + expect(inputs[2].checked).to.be.true; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(4); + expect(labels[0].textContent).to.equal('Email data to'); + expect(labels[1].htmlFor).to.equal('test-checklist-0'); + expect(labels[2].htmlFor).to.equal('test-checklist-1'); + expect(labels[3].htmlFor).to.equal('test-checklist-2'); + }); + + + it('should render default value (undefined)', function() { + + // when + const { container } = createChecklist(); + + // then + const inputs = container.querySelectorAll('input[type="checkbox"]'); + + inputs.forEach(input => { + expect(input.checked).to.be.false; + }); + }); + + + it('should render disabled', function() { + + // when + const { container } = createChecklist({ + disabled: true + }); + + // then + const inputs = container.querySelectorAll('input[type="checkbox"]'); + + inputs.forEach(input => { + expect(input.disabled).to.be.true; + }); + }); + + + it('should render readonly', function() { + + // when + const { container } = createChecklist({ + readonly: true + }); + + // then + const inputs = container.querySelectorAll('input[type="checkbox"]'); + + inputs.forEach(input => { + expect(input.readOnly).to.be.true; + }); + }); + + + it('should render description', function() { + + // when + const { container } = createChecklist({ + field: { + ...defaultField, + description: 'foo' + } + }); + + // then + const description = container.querySelector('.fjs-form-field-description'); + + expect(description).to.exist; + expect(description.textContent).to.equal('foo'); + }); + + + describe('handle change (static)', function() { + + it('should handle change', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ 'approver' ] + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[ 1 ]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: defaultField, + value: [ 'approver', 'manager' ] + }); + }); + + + it('should handle toggle', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ 'approver' ] + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[ 0 ]; + + fireEvent.click(input, { target: { checked: false } }); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: defaultField, + value: [] + }); + }); + + }); + + + describe('handle change (dynamic)', function() { + + it('should handle change', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ 'dynamicValue1' ], + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[1]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: [ 'dynamicValue1', 'dynamicValue2' ] + }); + }); + + + it('should handle change simplified values', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ 'dynamicValue1' ], + field: dynamicField, + initialData: dynamicFieldInitialDataSimplified + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[1]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: [ 'dynamicValue1', 'dynamicValue2' ] + }); + + }); + + + it('should handle change object values', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ { + id: 'user3', + name: 'User 3', + email: 'user3@email.com' + } ], + field: dynamicField, + initialData: dynamicFieldInitialDataObjectValues + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[1]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: [ { + id: 'user3', + name: 'User 3', + email: 'user3@email.com' + }, { + id: 'user2', + name: 'User 2', + email: 'user2@email.com' + } ] + }); + + }); + + + it('should handle toggle', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ 'dynamicValue1' ], + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[0]; + + fireEvent.click(input, { target: { checked: false } }); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: [] + }); + }); + + + it('should handle toggle object values', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createChecklist({ + onChange: onChangeSpy, + value: [ { + id: 'user3', + name: 'User 3', + email: 'user3@email.com' + } ], + field: dynamicField, + initialData: dynamicFieldInitialDataObjectValues + }); + + // when + const input = container.querySelectorAll('input[type="checkbox"]')[2]; + + fireEvent.click(input, { target: { checked: false } }); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: [] + }); + + }); + + }); + + + it('#create', function() { + + // assume + const { config } = Checklist; + expect(config.type).to.eql('checklist'); + expect(config.label).to.eql('Checkbox group'); + expect(config.group).to.eql('selection'); + expect(config.keyed).to.be.true; + + // when + const field = config.create(); + + // then + expect(field).to.eql({ + values: [ + { + label: 'Value', + value: 'value' + } + ] + }); + + // but when + const customField = config.create({ + custom: true + }); + + // then + expect(customField).to.contain({ + custom: true + }); + }); + + + describe('#sanitizeValue', function() { + + it('should sanitize value if options are not contained (static)', function() { + + // given + const { sanitizeValue } = Checklist.config; + + // when + const sanitizedValue = sanitizeValue({ value: [ 'camunda-not-platform' ], data: {}, formField: defaultField }); + + // then + expect(sanitizedValue).to.deep.equal([]); + }); + + + it('should sanitize value if options are not contained (dynamic)', function() { + + // given + const { sanitizeValue } = Checklist.config; + + // when + const sanitizedValue = sanitizeValue({ value: [ 'dynamicValue3', 'dynamicValue4' ], data: dynamicFieldInitialData, formField: dynamicField }); + + // then + expect(sanitizedValue).to.deep.equal([ 'dynamicValue3' ]); + }); + + + it('should not try to sanitize value if options are expression evaluated', function() { + + // given + const { sanitizeValue } = Checklist.config; + + // when + const sanitizedValue = sanitizeValue({ value: [ 'camunda-not-platform' ], data: {}, formField: { ...defaultField, valuesExpression: '=someExpression' } }); + + // then + expect(sanitizedValue).to.deep.equal([ 'camunda-not-platform' ]); + }); + + }); + + + describe('a11y', function() { + + it('should have no violations', async function() { + + // given + this.timeout(10000); + + const { container } = createChecklist({ + value: [ 'approver' ] + }); + + // then + await expectNoViolations(container); + }); + + + it('should have no violations for readonly', async function() { + + // given + this.timeout(10000); + + const { container } = createChecklist({ + value: [ 'approver' ], + readonly: true + }); + + // then + await expectNoViolations(container); + }); + + + it('should have no violations for errors', async function() { + + // given + this.timeout(10000); + + const { container } = createChecklist({ + value: [ 'approver' ], + errors: [ 'Something went wrong' ] + }); + + // then + await expectNoViolations(container); + }); + + }); + +}); + +// helpers ////////// + +const defaultField = { + id: 'Checklist_1', + key: 'mailto', + label: 'Email data to', + type: 'checklist', + description: 'checklist', + values: [ + { + label: 'Approver', + value: 'approver' + }, + { + label: 'Manager', + value: 'manager' + }, + { + label: 'Regional Manager', + value: 'regional-manager' + } + ] +}; + +const dynamicField = { + id: 'Checklist_1', + key: 'mailto', + label: 'Email data to', + type: 'checklist', + valuesKey: 'dynamicValues' +}; + +const dynamicFieldInitialData = { + dynamicValues: [ + { + label: 'Dynamic Value 1', + value: 'dynamicValue1' + }, + { + label: 'Dynamic Value 2', + value: 'dynamicValue2' + }, + { + label: 'Dynamic Value 3', + value: 'dynamicValue3' + } + ] +}; + +const dynamicFieldInitialDataSimplified = { + dynamicValues: [ + 'dynamicValue1', + 'dynamicValue2', + 'dynamicValue3' + ] +}; + +const dynamicFieldInitialDataObjectValues = { + dynamicValues: [ + { + label: 'User 1', + value: { + id: 'user1', + name: 'User 1', + email: 'user1@email.com' + } + }, + { + label: 'User 2', + value: { + id: 'user2', + name: 'User 2', + email: 'user2@email.com' + } + }, + { + label: 'User 3', + value: { + id: 'user3', + name: 'User 3', + email: 'user3@email.com' + } + } + ] +}; + +function createChecklist({ services, ...restOptions } = {}) { + + const options = { + domId: 'test-checklist', + field: defaultField, + onChange: () => {}, + ...restOptions + }; + + return render( + + + , { + container: options.container || container.querySelector('.fjs-form') + } + ); } \ No newline at end of file diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/Radio.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/Radio.spec.js index 983557815..43ea99d32 100644 --- a/packages/form-js-viewer/test/spec/render/components/form-fields/Radio.spec.js +++ b/packages/form-js-viewer/test/spec/render/components/form-fields/Radio.spec.js @@ -1,477 +1,519 @@ -import { - fireEvent, - render -} from '@testing-library/preact/pure'; - -import { Radio } from '../../../../../src/render/components/form-fields/Radio'; - -import { - createFormContainer, - expectNoViolations -} from '../../../../TestHelper'; - -import { MockFormContext } from '../helper'; - -const spy = sinon.spy; - -let container; - - -describe('Radio', function() { - - beforeEach(function() { - container = createFormContainer(); - }); - - afterEach(function() { - container.remove(); - }); - - - it('should render', function() { - - // when - const { container } = createRadio({ - value: 'camunda-platform' - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-radio')).to.be.true; - - const inputs = container.querySelectorAll('input[type="radio"]'); - - expect(inputs).to.have.length(2); - expect(inputs[ 0 ].id).to.equal('test-radio-0'); - expect(inputs[ 1 ].id).to.equal('test-radio-1'); - expect(inputs[ 0 ].checked).to.be.true; - expect(inputs[ 1 ].checked).to.be.false; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(3); - expect(labels[ 0 ].textContent).to.equal('Product'); - expect(labels[ 1 ].htmlFor).to.equal('test-radio-0'); - expect(labels[ 2 ].htmlFor).to.equal('test-radio-1'); - }); - - - it('should render required label', function() { - - // when - const { container } = createRadio({ - field: { - ...defaultField, - label: 'Required', - validate: { - required: true - } - } - }); - - // then - const label = container.querySelector('label'); - - expect(label).to.exist; - expect(label.textContent).to.equal('Required*'); - }); - - - it('should render dynamically', function() { - - // when - const { container } = createRadio({ - value: 'dynamicValue1', - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // then - const formField = container.querySelector('.fjs-form-field'); - - expect(formField).to.exist; - expect(formField.classList.contains('fjs-form-field-radio')).to.be.true; - - const inputs = container.querySelectorAll('input[type="radio"]'); - - expect(inputs).to.have.length(2); - expect(inputs[0].id).to.equal('test-radio-0'); - expect(inputs[1].id).to.equal('test-radio-1'); - expect(inputs[0].checked).to.be.true; - expect(inputs[1].checked).to.be.false; - - const labels = container.querySelectorAll('label'); - - expect(labels).to.have.length(3); - expect(labels[0].textContent).to.equal('Product'); - expect(labels[1].htmlFor).to.equal('test-radio-0'); - expect(labels[2].htmlFor).to.equal('test-radio-1'); - }); - - - it('should render selection properly when using object values', function() { - - // when - const { container } = createRadio({ - value: { a: 1, b: 2 }, - field: dynamicField, - initialData: objectDynamicFieldInitialData - }); - - // then - const inputs = container.querySelectorAll('input[type="radio"]'); - expect(inputs[0].checked).to.be.false; - expect(inputs[1].checked).to.be.true; - }); - - - it('should render default value (undefined)', function() { - - // when - const { container } = createRadio(); - - // then - const inputs = container.querySelectorAll('input[type="radio"]'); - - inputs.forEach(input => { - expect(input.checked).to.be.false; - }); - }); - - - it('should render value', function() { - - // when - const { container } = createRadio({ - value: null - }); - - // then - const inputs = container.querySelectorAll('input[type="radio"]'); - - inputs.forEach(input => { - expect(input.checked).to.be.false; - }); - }); - - - it('should render disabled', function() { - - // when - const { container } = createRadio({ - disabled: true - }); - - // then - const inputs = container.querySelectorAll('input[type="radio"]'); - - inputs.forEach(input => { - expect(input.disabled).to.be.true; - }); - }); - - - it('should render readonly', function() { - - // when - const { container } = createRadio({ - readonly: true - }); - - // then - const inputs = container.querySelectorAll('input[type="radio"]'); - - inputs.forEach(input => { - expect(input.readOnly).to.be.true; - }); - }); - - - it('should render description', function() { - - // when - const { container } = createRadio({ - field: { - ...defaultField, - description: 'foo' - } - }); - - // then - const description = container.querySelector('.fjs-form-field-description'); - - expect(description).to.exist; - expect(description.textContent).to.equal('foo'); - }); - - - describe('handle change (dynamic)', function() { - - it('should handle change', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createRadio({ - onChange: onChangeSpy, - value: 'dynamicValue1', - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // when - const input = container.querySelectorAll('input[type="radio"]')[ 1 ]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: 'dynamicValue2' - }); - }); - - - it('should handle toggle', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createRadio({ - onChange: onChangeSpy, - value: 'dynamicValue1', - field: dynamicField, - initialData: dynamicFieldInitialData - }); - - // when - const input = container.querySelectorAll('input[type="radio"]')[ 0 ]; - - fireEvent.click(input, { target: { checked: false } }); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: dynamicField, - value: 'dynamicValue1' - }); - }); - - }); - - - describe('handle change (static)', function() { - - it('should handle change', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createRadio({ - onChange: onChangeSpy, - value: 'camunda-platform' - }); - - // when - const input = container.querySelectorAll('input[type="radio"]')[1]; - - fireEvent.click(input); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: defaultField, - value: 'camunda-cloud' - }); - }); - - - it('should handle toggle', function() { - - // given - const onChangeSpy = spy(); - - const { container } = createRadio({ - onChange: onChangeSpy, - value: 'camunda-platform' - }); - - // when - const input = container.querySelectorAll('input[type="radio"]')[0]; - - fireEvent.click(input, { target: { checked: false } }); - - // then - expect(onChangeSpy).to.have.been.calledWith({ - field: defaultField, - value: 'camunda-platform' - }); - }); - - }); - - - it('#create', function() { - - // assume - const { config } = Radio; - expect(config.type).to.eql('radio'); - expect(config.label).to.eql('Radio group'); - expect(config.group).to.eql('selection'); - expect(config.keyed).to.be.true; - - // when - const field = config.create(); - - // then - expect(field).to.eql({ - values: [ - { - label: 'Value', - value: 'value' - } - ] - }); - - // but when - const customField = config.create({ - custom: true - }); - - // then - expect(customField).to.contain({ - custom: true - }); - }); - - - describe('a11y', function() { - - it('should have no violations', async function() { - - // given - this.timeout(10000); - - const { container } = createRadio({ - value: 'camunda-platform' - }); - - // then - await expectNoViolations(container); - }); - - - it('should have no violations for readonly', async function() { - - // given - this.timeout(10000); - - const { container } = createRadio({ - value: 'camunda-platform', - readonly: true - }); - - // then - await expectNoViolations(container); - }); - - - it('should have no violations for errors', async function() { - - // given - this.timeout(10000); - - const { container } = createRadio({ - value: 'camunda-platform', - errors: [ 'Something went wrong' ] - }); - - // then - await expectNoViolations(container); - }); - - }); - -}); - -// helpers ////////// - -const defaultField = { - id: 'Radio_1', - key: 'product', - label: 'Product', - type: 'radio', - description: 'radio', - values: [ - { - label: 'Camunda Platform', - value: 'camunda-platform' - }, - { - label: 'Camunda Cloud', - value: 'camunda-cloud' - } - ] -}; - -const dynamicField = { - id: 'Radio_1', - key: 'product', - label: 'Product', - type: 'radio', - valuesKey: 'dynamicValues' -}; - -const dynamicFieldInitialData = { - dynamicValues: [ - { - label: 'Dynamic Value 1', - value: 'dynamicValue1' - }, - { - label: 'Dynamic Value 2', - value: 'dynamicValue2' - } - ] -}; - -const objectDynamicFieldInitialData = { - dynamicValues: [ - { - label: 'Dynamic Value 1', - value: { a: 2, b: 1 } - }, - { - label: 'Dynamic Value 2', - value: { a: 1, b: 2 } - } - ] -}; - -function createRadio({ services, ...restOptions } = {}) { - - const options = { - domId: 'test-radio', - field: defaultField, - onChange: () => {}, - ...restOptions - }; - - return render( - - - , { - container: options.container || container.querySelector('.fjs-form') - } - ); +import { + fireEvent, + render +} from '@testing-library/preact/pure'; + +import { Radio } from '../../../../../src/render/components/form-fields/Radio'; + +import { + createFormContainer, + expectNoViolations +} from '../../../../TestHelper'; + +import { MockFormContext } from '../helper'; + +const spy = sinon.spy; + +let container; + + +describe('Radio', function() { + + beforeEach(function() { + container = createFormContainer(); + }); + + afterEach(function() { + container.remove(); + }); + + + it('should render', function() { + + // when + const { container } = createRadio({ + value: 'camunda-platform' + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-radio')).to.be.true; + + const inputs = container.querySelectorAll('input[type="radio"]'); + + expect(inputs).to.have.length(2); + expect(inputs[ 0 ].id).to.equal('test-radio-0'); + expect(inputs[ 1 ].id).to.equal('test-radio-1'); + expect(inputs[ 0 ].checked).to.be.true; + expect(inputs[ 1 ].checked).to.be.false; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(3); + expect(labels[ 0 ].textContent).to.equal('Product'); + expect(labels[ 1 ].htmlFor).to.equal('test-radio-0'); + expect(labels[ 2 ].htmlFor).to.equal('test-radio-1'); + }); + + + it('should render required label', function() { + + // when + const { container } = createRadio({ + field: { + ...defaultField, + label: 'Required', + validate: { + required: true + } + } + }); + + // then + const label = container.querySelector('label'); + + expect(label).to.exist; + expect(label.textContent).to.equal('Required*'); + }); + + + it('should render dynamically', function() { + + // when + const { container } = createRadio({ + value: 'dynamicValue1', + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // then + const formField = container.querySelector('.fjs-form-field'); + + expect(formField).to.exist; + expect(formField.classList.contains('fjs-form-field-radio')).to.be.true; + + const inputs = container.querySelectorAll('input[type="radio"]'); + + expect(inputs).to.have.length(2); + expect(inputs[0].id).to.equal('test-radio-0'); + expect(inputs[1].id).to.equal('test-radio-1'); + expect(inputs[0].checked).to.be.true; + expect(inputs[1].checked).to.be.false; + + const labels = container.querySelectorAll('label'); + + expect(labels).to.have.length(3); + expect(labels[0].textContent).to.equal('Product'); + expect(labels[1].htmlFor).to.equal('test-radio-0'); + expect(labels[2].htmlFor).to.equal('test-radio-1'); + }); + + + it('should render selection properly when using object values', function() { + + // when + const { container } = createRadio({ + value: { a: 1, b: 2 }, + field: dynamicField, + initialData: objectDynamicFieldInitialData + }); + + // then + const inputs = container.querySelectorAll('input[type="radio"]'); + expect(inputs[0].checked).to.be.false; + expect(inputs[1].checked).to.be.true; + }); + + + it('should render default value (undefined)', function() { + + // when + const { container } = createRadio(); + + // then + const inputs = container.querySelectorAll('input[type="radio"]'); + + inputs.forEach(input => { + expect(input.checked).to.be.false; + }); + }); + + + it('should render value', function() { + + // when + const { container } = createRadio({ + value: null + }); + + // then + const inputs = container.querySelectorAll('input[type="radio"]'); + + inputs.forEach(input => { + expect(input.checked).to.be.false; + }); + }); + + + it('should render disabled', function() { + + // when + const { container } = createRadio({ + disabled: true + }); + + // then + const inputs = container.querySelectorAll('input[type="radio"]'); + + inputs.forEach(input => { + expect(input.disabled).to.be.true; + }); + }); + + + it('should render readonly', function() { + + // when + const { container } = createRadio({ + readonly: true + }); + + // then + const inputs = container.querySelectorAll('input[type="radio"]'); + + inputs.forEach(input => { + expect(input.readOnly).to.be.true; + }); + }); + + + it('should render description', function() { + + // when + const { container } = createRadio({ + field: { + ...defaultField, + description: 'foo' + } + }); + + // then + const description = container.querySelector('.fjs-form-field-description'); + + expect(description).to.exist; + expect(description.textContent).to.equal('foo'); + }); + + + describe('handle change (dynamic)', function() { + + it('should handle change', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createRadio({ + onChange: onChangeSpy, + value: 'dynamicValue1', + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // when + const input = container.querySelectorAll('input[type="radio"]')[ 1 ]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: 'dynamicValue2' + }); + }); + + + it('should handle toggle', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createRadio({ + onChange: onChangeSpy, + value: 'dynamicValue1', + field: dynamicField, + initialData: dynamicFieldInitialData + }); + + // when + const input = container.querySelectorAll('input[type="radio"]')[ 0 ]; + + fireEvent.click(input, { target: { checked: false } }); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: dynamicField, + value: 'dynamicValue1' + }); + }); + + }); + + + describe('handle change (static)', function() { + + it('should handle change', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createRadio({ + onChange: onChangeSpy, + value: 'camunda-platform' + }); + + // when + const input = container.querySelectorAll('input[type="radio"]')[1]; + + fireEvent.click(input); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: defaultField, + value: 'camunda-cloud' + }); + }); + + + it('should handle toggle', function() { + + // given + const onChangeSpy = spy(); + + const { container } = createRadio({ + onChange: onChangeSpy, + value: 'camunda-platform' + }); + + // when + const input = container.querySelectorAll('input[type="radio"]')[0]; + + fireEvent.click(input, { target: { checked: false } }); + + // then + expect(onChangeSpy).to.have.been.calledWith({ + field: defaultField, + value: 'camunda-platform' + }); + }); + + }); + + + it('#create', function() { + + // assume + const { config } = Radio; + expect(config.type).to.eql('radio'); + expect(config.label).to.eql('Radio group'); + expect(config.group).to.eql('selection'); + expect(config.keyed).to.be.true; + + // when + const field = config.create(); + + // then + expect(field).to.eql({ + values: [ + { + label: 'Value', + value: 'value' + } + ] + }); + + // but when + const customField = config.create({ + custom: true + }); + + // then + expect(customField).to.contain({ + custom: true + }); + }); + + describe('#sanitizeValue', function() { + + it('should sanitize value if options are not contained (static)', function() { + + // given + const { sanitizeValue } = Radio.config; + + // when + const sanitizedValue = sanitizeValue({ value: 'camunda-not-platform', data: {}, formField: defaultField }); + + // then + expect(sanitizedValue).to.equal(null); + }); + + + it('should sanitize value if options are not contained (dynamic)', function() { + + // given + const { sanitizeValue } = Radio.config; + + // when + const sanitizedValue = sanitizeValue({ value: 'dynamicValue3', data: dynamicFieldInitialData, formField: dynamicField }); + + // then + expect(sanitizedValue).to.equal(null); + }); + + + it('should not try to sanitize value if options are expression evaluated', function() { + + // given + const { sanitizeValue } = Radio.config; + + // when + const sanitizedValue = sanitizeValue({ value: 'camunda-not-platform', data: {}, formField: { ...defaultField, valuesExpression: '=someExpression' } }); + + // then + expect(sanitizedValue).to.equal('camunda-not-platform'); + }); + + }); + + + describe('a11y', function() { + + it('should have no violations', async function() { + + // given + this.timeout(10000); + + const { container } = createRadio({ + value: 'camunda-platform' + }); + + // then + await expectNoViolations(container); + }); + + + it('should have no violations for readonly', async function() { + + // given + this.timeout(10000); + + const { container } = createRadio({ + value: 'camunda-platform', + readonly: true + }); + + // then + await expectNoViolations(container); + }); + + + it('should have no violations for errors', async function() { + + // given + this.timeout(10000); + + const { container } = createRadio({ + value: 'camunda-platform', + errors: [ 'Something went wrong' ] + }); + + // then + await expectNoViolations(container); + }); + + }); + +}); + +// helpers ////////// + +const defaultField = { + id: 'Radio_1', + key: 'product', + label: 'Product', + type: 'radio', + description: 'radio', + values: [ + { + label: 'Camunda Platform', + value: 'camunda-platform' + }, + { + label: 'Camunda Cloud', + value: 'camunda-cloud' + } + ] +}; + +const dynamicField = { + id: 'Radio_1', + key: 'product', + label: 'Product', + type: 'radio', + valuesKey: 'dynamicValues' +}; + +const dynamicFieldInitialData = { + dynamicValues: [ + { + label: 'Dynamic Value 1', + value: 'dynamicValue1' + }, + { + label: 'Dynamic Value 2', + value: 'dynamicValue2' + } + ] +}; + +const objectDynamicFieldInitialData = { + dynamicValues: [ + { + label: 'Dynamic Value 1', + value: { a: 2, b: 1 } + }, + { + label: 'Dynamic Value 2', + value: { a: 1, b: 2 } + } + ] +}; + +function createRadio({ services, ...restOptions } = {}) { + + const options = { + domId: 'test-radio', + field: defaultField, + onChange: () => {}, + ...restOptions + }; + + return render( + + + , { + container: options.container || container.querySelector('.fjs-form') + } + ); } \ No newline at end of file