From d19cb8de7f3f50f328f8c4830e66b42aa4e69430 Mon Sep 17 00:00:00 2001 From: Niklas Kiefer Date: Thu, 14 Sep 2023 14:15:07 +0200 Subject: [PATCH] test: provide custom form field tests Closes #123 --- packages/form-js-playground/karma.conf.js | 34 ++++ .../form-js-playground/test/custom/editor.js | 152 ++++++++++++++++++ .../form-js-playground/test/custom/range.svg | 19 +++ .../form-js-playground/test/custom/styles.css | 4 + .../form-js-playground/test/custom/viewer.js | 122 ++++++++++++++ .../test/spec/Playground.spec.js | 37 ++++- .../form-js-playground/test/spec/custom.json | 28 ++++ .../src/render/components/index.js | 13 +- .../form-js-viewer/test/spec/Form.spec.js | 26 +-- .../form-js-viewer/test/spec/custom/index.js | 76 +++++++-- .../form-js-viewer/test/spec/customField.json | 26 +++ 11 files changed, 512 insertions(+), 25 deletions(-) create mode 100644 packages/form-js-playground/test/custom/editor.js create mode 100644 packages/form-js-playground/test/custom/range.svg create mode 100644 packages/form-js-playground/test/custom/styles.css create mode 100644 packages/form-js-playground/test/custom/viewer.js create mode 100644 packages/form-js-playground/test/spec/custom.json create mode 100644 packages/form-js-viewer/test/spec/customField.json diff --git a/packages/form-js-playground/karma.conf.js b/packages/form-js-playground/karma.conf.js index 42adc3351..1440a922e 100644 --- a/packages/form-js-playground/karma.conf.js +++ b/packages/form-js-playground/karma.conf.js @@ -1,3 +1,9 @@ +const path = require('path'); + +const { + NormalModuleReplacementPlugin +} = require('webpack'); + const coverage = process.env.COVERAGE; // configures browsers to run test against @@ -63,6 +69,10 @@ module.exports = function(karma) { 'css-loader' ] }, + { + test: /\.svg$/, + use: [ '@svgr/webpack' ] + }, { test: /\.m?js$/, exclude: /node_modules/, @@ -90,6 +100,30 @@ module.exports = function(karma) { } ] }, + plugins: [ + new NormalModuleReplacementPlugin( + /^(..\/preact|preact)(\/[^/]+)?$/, + function(resource) { + + const replMap = { + 'preact/hooks': path.resolve('../../node_modules/preact/hooks/dist/hooks.module.js'), + 'preact/jsx-runtime': path.resolve('../../node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'), + 'preact': path.resolve('../../node_modules/preact/dist/preact.module.js'), + '../preact/hooks': path.resolve('../../node_modules/preact/hooks/dist/hooks.module.js'), + '../preact/jsx-runtime': path.resolve('../../node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'), + '../preact': path.resolve('../../node_modules/preact/dist/preact.module.js') + }; + + const replacement = replMap[resource.request]; + + if (!replacement) { + return; + } + + resource.request = replacement; + } + ), + ], devtool: 'eval-source-map' } }; diff --git a/packages/form-js-playground/test/custom/editor.js b/packages/form-js-playground/test/custom/editor.js new file mode 100644 index 000000000..c913a77b4 --- /dev/null +++ b/packages/form-js-playground/test/custom/editor.js @@ -0,0 +1,152 @@ +import { get, set } from 'min-dash'; + +import { + NumberFieldEntry, + isNumberFieldEntryEdited +} from '@bpmn-io/properties-panel'; + + +class CustomPropertiesProvider { + constructor(propertiesPanel) { + propertiesPanel.registerProvider(this, 500); + } + + getGroups(field, editField) { + return (groups) => { + + if (field.type !== 'range') { + return groups; + } + + const generalIdx = findGroupIdx(groups, 'general'); + + groups.splice(generalIdx + 1, 0, { + id: 'range', + label: 'Range', + entries: RangeEntries(field, editField) + }); + + return groups; + }; + } +} + +CustomPropertiesProvider.$inject = [ 'propertiesPanel' ]; + +function RangeEntries(field, editField) { + + const onChange = (key) => { + return (value) => { + const range = get(field, [ 'range' ], {}); + + editField(field, [ 'range' ], set(range, [ key ], value)); + }; + }; + + const getValue = (key) => { + return () => { + return get(field, [ 'range', key ]); + }; + }; + + return [ + { + id: 'range-min', + component: Min, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + }, + { + id: 'range-max', + component: Max, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + }, + { + id: 'range-step', + component: Step, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + } + ]; + +} + +function Min(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('min'), + id, + label: 'Minimum', + setValue: onChange('min') + }); +} + +function Max(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('max'), + id, + label: 'Maximum', + setValue: onChange('max') + }); +} + +function Step(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('step'), + id, + min: 0, + label: 'Step', + setValue: onChange('step') + }); +} + + +export default { + __init__: [ 'customPropertiesProvider' ], + customPropertiesProvider: [ 'type', CustomPropertiesProvider ] +}; + +// helper ////////////////////// + +function findGroupIdx(groups, id) { + return groups.findIndex(g => g.id === id); +} \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/range.svg b/packages/form-js-playground/test/custom/range.svg new file mode 100644 index 000000000..a3dbe11f6 --- /dev/null +++ b/packages/form-js-playground/test/custom/range.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/styles.css b/packages/form-js-playground/test/custom/styles.css new file mode 100644 index 000000000..244247ef4 --- /dev/null +++ b/packages/form-js-playground/test/custom/styles.css @@ -0,0 +1,4 @@ +.range-group { + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/viewer.js b/packages/form-js-playground/test/custom/viewer.js new file mode 100644 index 000000000..8b31f5199 --- /dev/null +++ b/packages/form-js-playground/test/custom/viewer.js @@ -0,0 +1,122 @@ + +import { + Errors, + FormContext, + Numberfield, + Description, + Label +} from '@bpmn-io/form-js-viewer'; + +import { useContext } from 'preact/hooks'; + +import classNames from 'classnames'; + +import RangeIcon from './range.svg'; + +const rangeType = 'range'; + +function RangeRenderer(props) { + + const { + disabled, + errors = [], + field, + readonly, + value + } = props; + + const { + description, + range = {}, + id, + label + } = field; + + const { + min, + max, + step + } = range; + + const { formId } = useContext(FormContext); + + const errorMessageId = errors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`; + + const onChange = ({ target }) => { + props.onChange({ + field, + value: Number(target.value) + }); + }; + + return
+
; +} + +RangeRenderer.config = { + ...Numberfield.config, + type: rangeType, + keyed: true, + label: 'Range', + group: 'basic-input', + propertiesPanelEntries: [ + 'key', + 'label', + 'description', + 'min', + 'max' + ], + icon: RangeIcon +}; + +class CustomFormFields { + constructor(formFields) { + formFields.register(rangeType, RangeRenderer); + } +} + +export default { + __init__: [ 'customFormFields' ], + customFormFields: [ 'type', CustomFormFields ] +}; + + +// helper ////////////////////// + +function formFieldClasses(type, { errors = [], disabled = false, readonly = false } = {}) { + if (!type) { + throw new Error('type required'); + } + + return classNames('fjs-form-field', `fjs-form-field-${type}`, { + 'fjs-has-errors': errors.length > 0, + 'fjs-disabled': disabled, + 'fjs-readonly': readonly + }); +} + +function prefixId(id, formId) { + if (formId) { + return `fjs-form-${ formId }-${ id }`; + } + + return `fjs-form-${ id }`; +} \ No newline at end of file diff --git a/packages/form-js-playground/test/spec/Playground.spec.js b/packages/form-js-playground/test/spec/Playground.spec.js index a6f2b80a1..c291bed8a 100644 --- a/packages/form-js-playground/test/spec/Playground.spec.js +++ b/packages/form-js-playground/test/spec/Playground.spec.js @@ -17,6 +17,11 @@ import { import schema from './form.json'; import otherSchema from './other-form.json'; import rowsSchema from './rows-form.json'; +import customSchema from './custom.json'; + +import customViewerModule from '../custom/viewer'; +import customEditorModule from '../custom/editor'; +import customStyles from '../custom/styles.css'; import { insertCSS, @@ -32,9 +37,12 @@ insertCSS('Test.css', ` } `); +insertCSS('custom.css', customStyles); + const singleStartBasic = isSingleStart('basic'); const singleStartRows = isSingleStart('rows'); -const singleStart = singleStartBasic || singleStartRows; +const singleStartCustom = isSingleStart('custom'); +const singleStart = singleStartBasic || singleStartRows || singleStartCustom; describe('playground', function() { @@ -128,6 +136,33 @@ describe('playground', function() { }); + (singleStartCustom ? it.only : it)('should support custom element', async function() { + + // given + const data = { + creditor: 'John Doe Company', + amount: 25 + }; + + // when + // viewer and editor + playground = new Playground({ + container, + schema: customSchema, + data, + additionalModules: [ + customViewerModule + ], + editorAdditionalModules: [ + customEditorModule + ] + }); + + // then + expect(playground).to.exist; + }); + + it('should NOT attach to empty parent', async function() { // given diff --git a/packages/form-js-playground/test/spec/custom.json b/packages/form-js-playground/test/spec/custom.json new file mode 100644 index 000000000..3b98dbe42 --- /dev/null +++ b/packages/form-js-playground/test/spec/custom.json @@ -0,0 +1,28 @@ +{ + "components": [ + { + "key": "creditor", + "label": "Creditor", + "type": "textfield", + "validate": { + "required": true + } + }, + { + "key": "amount", + "type": "range", + "label": "Amount", + "range": { + "min": 0, + "max": 100, + "step": 5 + } + }, + { + "type": "button", + "action": "submit", + "label": "Submit" + } + ], + "type": "default" +} \ No newline at end of file diff --git a/packages/form-js-viewer/src/render/components/index.js b/packages/form-js-viewer/src/render/components/index.js index 02300edeb..1af123343 100644 --- a/packages/form-js-viewer/src/render/components/index.js +++ b/packages/form-js-viewer/src/render/components/index.js @@ -16,6 +16,16 @@ import Text from './form-fields/Text'; import Textfield from './form-fields/Textfield'; import Textarea from './form-fields/Textarea'; +import Label from './Label'; +import Description from './Description'; +import Errors from './Errors'; + +export { + Label, + Description, + Errors +}; + export { Button, Checkbox, @@ -54,4 +64,5 @@ export const formFields = [ Textarea ]; -export * from './icons'; \ No newline at end of file +export * from './icons'; +export * from './Sanitizer'; \ No newline at end of file diff --git a/packages/form-js-viewer/test/spec/Form.spec.js b/packages/form-js-viewer/test/spec/Form.spec.js index c1aa544d2..a36066e7c 100644 --- a/packages/form-js-viewer/test/spec/Form.spec.js +++ b/packages/form-js-viewer/test/spec/Form.spec.js @@ -24,6 +24,7 @@ import textSchema from './text.json'; import textTemplateSchema from './text-template.json'; import stress from './stress.json'; import rowsSchema from './rows.json'; +import customFieldSchema from './customField.json'; import { insertCSS, @@ -41,14 +42,15 @@ const singleStartStress = isSingleStart('stress'); const singleStartRows = isSingleStart('rows'); const singleStartTheme = isSingleStart('theme'); const singleStartNoTheme = isSingleStart('no-theme'); - +const singleStartCustom = isSingleStart('custom'); const singleStart = singleStartBasic || singleStartGroups || singleStartStress || singleStartRows || singleStartTheme || - singleStartNoTheme; + singleStartNoTheme || + singleStartCustom; describe('Form', function() { @@ -860,33 +862,31 @@ describe('Form', function() { }); - it('should be customizable', async function() { + (singleStartCustom ? it.only : it)('should be customizable', async function() { // given const data = { creditor: 'John Doe Company', - amount: 456, - invoiceNumber: 'C-123', - approved: true, - approvedBy: 'John Doe', - mailto: [ 'regional-manager', 'approver' ], - product: 'camunda-cloud', - tags: [ 'tag1', 'tag2', 'tag3' ], - language: 'english' + amount: 25 }; // when - await createForm({ + const form = await createForm({ container, data, - schema, + schema: customFieldSchema, additionalModules: [ customButtonModule ] }); + form.on('changed', event => { + console.log('Form ', event); + }); + // then expect(document.querySelector('.custom-button')).to.exist; + expect(document.querySelector('.fjs-form-field-range')).to.exist; }); diff --git a/packages/form-js-viewer/test/spec/custom/index.js b/packages/form-js-viewer/test/spec/custom/index.js index 30a26a17a..15fcd7265 100644 --- a/packages/form-js-viewer/test/spec/custom/index.js +++ b/packages/form-js-viewer/test/spec/custom/index.js @@ -1,6 +1,16 @@ -import { formFieldClasses } from '../../../src/render/components/Util'; +import { + formFieldClasses, + prefixId +} from '../../../src/render/components/Util'; -const type = 'button'; +import { Numberfield, Button } from '../../../src'; + +import { FormContext } from '../../../src/render/context'; + +import { useContext } from 'preact/hooks'; + +const btnType = Button.config.type; +const rangeType = 'range'; function CustomButton(props) { const { @@ -10,29 +20,75 @@ function CustomButton(props) { const { action = 'submit' } = field; - return
+ return
; } CustomButton.config = { - type, - keyed: true, + ...Button.config, label: 'Custom Button', - group: 'action', create: (options = {}) => ({ action: 'submit', ...options }) }; -class CustomButtonRegistrer { +function Range(props) { + + const { + field, + value + } = props; + + const { + min, + max, + step, + id, + label + } = field; + + const { formId } = useContext(FormContext); + + const onChange = ({ target }) => { + props.onChange({ + field, + value: Number(target.value) + }); + }; + + return
+ + +
; +} + +Range.config = { + ...Numberfield.config, + type: rangeType, + keyed: true, + label: 'Range', + group: 'basic-input' +}; + +class CustomFormFields { constructor(formFields) { - formFields.register(type, CustomButton); + formFields.register(btnType, CustomButton); + formFields.register(rangeType, Range); } } export default { - __init__: [ 'customButtonRegisterer' ], - customButtonRegisterer: [ 'type', CustomButtonRegistrer ] + __init__: [ 'customFormFields' ], + customFormFields: [ 'type', CustomFormFields ] }; \ No newline at end of file diff --git a/packages/form-js-viewer/test/spec/customField.json b/packages/form-js-viewer/test/spec/customField.json new file mode 100644 index 000000000..635ccf621 --- /dev/null +++ b/packages/form-js-viewer/test/spec/customField.json @@ -0,0 +1,26 @@ +{ + "components": [ + { + "key": "creditor", + "label": "Creditor", + "type": "textfield", + "validate": { + "required": true + } + }, + { + "key": "amount", + "type": "range", + "label": "Amount", + "min": 0, + "max": 100, + "step": 5 + }, + { + "type": "button", + "action": "submit", + "label": "Submit" + } + ], + "type": "default" +} \ No newline at end of file