├── .eslintignore ├── .eslintrc.js ├── .github ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── deploy-package.yml │ ├── sonarqube.yml │ └── storybook.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js ├── manager.js └── preview.js ├── README.md ├── babel.config.json ├── cypress.config.js ├── favicon.ico ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── sonar-project.properties ├── src ├── App.vue ├── CountElements.js ├── DataProvider.js ├── ValidationsFactory.js ├── VariableDataTypeProperties.js ├── assets │ ├── Shape.svg │ ├── css │ │ ├── custom.css │ │ └── tabs.css │ ├── icons │ │ ├── Button.png │ │ ├── Bypass.svg │ │ ├── Checkbox.png │ │ ├── Checkboxes.png │ │ ├── Date.png │ │ ├── Dropdown.png │ │ ├── ErrorMessage.svg │ │ ├── FileUpload.png │ │ ├── HiddenField.png │ │ ├── Image.png │ │ ├── Label.png │ │ ├── Link.png │ │ ├── Location.png │ │ ├── Panel.png │ │ ├── QRCode.png │ │ ├── RadioButton.png │ │ ├── Signature.png │ │ ├── Subform.png │ │ ├── SubmitButton.png │ │ ├── Subtitle.png │ │ ├── SuggestField.png │ │ ├── Table.png │ │ ├── TextArea.png │ │ ├── TextField.png │ │ ├── Title.png │ │ ├── Unbypass.svg │ │ ├── UploadAudio.png │ │ ├── UploadImage.png │ │ ├── UploadVideo.png │ │ ├── angle-double-right-solid.svg │ │ ├── calendar-regular.svg │ │ ├── caret-square-down-solid.svg │ │ ├── check-square-solid.svg │ │ ├── columns-solid.svg │ │ ├── font-solid.svg │ │ ├── list-ul-solid.svg │ │ ├── paragraph-solid.svg │ │ ├── share-square-solid.svg │ │ ├── square-regular.svg │ │ └── th-list-solid.svg │ ├── logo.png │ ├── pencil-square.svg │ ├── priority-header.svg │ ├── table_empty.svg │ └── welcome_default.png ├── bootstrap.js ├── components │ ├── ClipboardButton.vue │ ├── CssIcon.vue │ ├── ScreenTemplateCard.vue │ ├── ScreenTemplates.vue │ ├── ScreenToolbar.vue │ ├── SelectUserGroup.vue │ ├── SimpleErrorMessage.vue │ ├── TabsBar.vue │ ├── VariableNameGenerator.js │ ├── accordions.js │ ├── basic-search.vue │ ├── computed-properties.vue │ ├── custom-css-output.js │ ├── custom-css.vue │ ├── editor │ │ ├── index.js │ │ ├── loop.vue │ │ ├── multi-column.vue │ │ └── pagesDropdown.vue │ ├── index.js │ ├── inspector │ │ ├── analytics-selector.vue │ │ ├── collection-data-source.vue │ │ ├── collection-designer-mode.vue │ │ ├── collection-display-mode.vue │ │ ├── collection-records-list.vue │ │ ├── collection-select-list.vue │ │ ├── color-select-modern.vue │ │ ├── color-select.vue │ │ ├── column-setup.vue │ │ ├── container-columns.vue │ │ ├── data-mapping.vue │ │ ├── data-source-types.js │ │ ├── default-value-editor.vue │ │ ├── device-visibility.vue │ │ ├── edit-option.vue │ │ ├── encrypted-config.vue │ │ ├── form-multiselect.vue │ │ ├── image-upload.vue │ │ ├── image-variable.vue │ │ ├── index.js │ │ ├── input-variable.vue │ │ ├── label-submit-button.vue │ │ ├── loading-submit-button.vue │ │ ├── loop.vue │ │ ├── mustache-helper.vue │ │ ├── options-list.vue │ │ ├── outbound-config.vue │ │ ├── page-select.vue │ │ ├── screen-selector.vue │ │ ├── select-data-type-mask.vue │ │ ├── tooltip.vue │ │ └── validation-select.vue │ ├── renderer │ │ ├── add-loop-row.vue │ │ ├── avatar-dropdown.vue │ │ ├── card.vue │ │ ├── file-download.vue │ │ ├── file-upload.vue │ │ ├── form-analytics-chart.vue │ │ ├── form-avatar.vue │ │ ├── form-button.vue │ │ ├── form-collection-record-control.vue │ │ ├── form-collection-view-control.vue │ │ ├── form-empty-table.vue │ │ ├── form-image.vue │ │ ├── form-input-masked.vue │ │ ├── form-list-table.vue │ │ ├── form-loop.vue │ │ ├── form-masked-input.vue │ │ ├── form-multi-column.vue │ │ ├── form-nested-screen.vue │ │ ├── form-new-request.vue │ │ ├── form-record-list-static.vue │ │ ├── form-record-list.vue │ │ ├── form-requests.vue │ │ ├── form-tasks.vue │ │ ├── form-text.vue │ │ ├── index.js │ │ ├── link-button.vue │ │ ├── new-form-multi-column.vue │ │ └── screen-renderer-error.vue │ ├── screen-renderer.vue │ ├── screen-variable-selector.vue │ ├── sortable │ │ ├── Sortable.vue │ │ ├── sortable.scss │ │ ├── sortableList │ │ │ ├── SortableList.vue │ │ │ └── sortableList.scss │ │ └── tableStyles.scss │ ├── task.vue │ ├── utils │ │ ├── default-loading-spinner.vue │ │ ├── index.js │ │ ├── multiple-uploads-checkbox.vue │ │ └── required-checkbox.vue │ ├── vue-form-builder.vue │ ├── vue-form-renderer.vue │ ├── watchers-form.vue │ ├── watchers-list.vue │ ├── watchers-popup.vue │ └── watchers-synchronous.vue ├── currency.json ├── customLogs.js ├── factories │ ├── CustomValidationRules.js │ └── ValidatorFactory.js ├── form-builder-controls.js ├── form-control-common-properties.js ├── form-input-mask-config.js ├── global-properties.js ├── itemProcessingUtils.js ├── main.js ├── mixins │ ├── Clipboard.js │ ├── CurrentPageProperty.js │ ├── DataReference.js │ ├── DeviceDetector.js │ ├── HasColorProperty.js │ ├── Json2Vue.js │ ├── ScreenBase.js │ ├── ValidationRules.js │ ├── VisibilityRule.js │ ├── canOpenJsonFile.js │ ├── computedFields.js │ ├── datatable.js │ ├── defaultValues.js │ ├── extensions │ │ ├── AccordionContainer.js │ │ ├── ComputedFields.js │ │ ├── CustomCss.js │ │ ├── DataManager.js │ │ ├── DefaultValues.js │ │ ├── InputText.js │ │ ├── LoadFieldComponents.js │ │ ├── LoopContainer.js │ │ ├── MultiColumn.js │ │ ├── PageNavigate.js │ │ ├── Submit.js │ │ ├── ValidationRules.js │ │ ├── VisibilityRule.js │ │ ├── Watchers.js │ │ └── index.js │ ├── focusErrors.js │ ├── formWatchers.js │ ├── getValidPath.js │ ├── index.js │ ├── multiselectApi.js │ ├── mustacheEvaluation.js │ ├── shouldElementBeVisible.js │ ├── testing.js │ └── watchers.js ├── store │ └── modules │ │ ├── ClipboardManager.js │ │ ├── clipboardModule.js │ │ ├── globalErrorsModule.js │ │ └── undoRedoModule.js └── stories │ ├── ClipboardButton.stories.js │ ├── ColorSelect.stories.js │ ├── Configure.mdx │ ├── DropdownAndPages.stories.js │ ├── PageTabs.stories.js │ ├── PagesDropdown.stories.js │ ├── ScreenToolbar.stories.js │ └── Sortable.stories.js ├── tests ├── components │ ├── RenderScreen.vue │ ├── RenderScreen2.vue │ ├── TaskAssigned.vue │ ├── TaskMobile.vue │ ├── TaskMultiInstance.vue │ ├── TaskRedirect.vue │ ├── TaskWebEntry.vue │ ├── WebEntry.vue │ └── index.js ├── e2e │ ├── fixtures │ │ ├── Expense_Report_Launcher_Main_13_screen.json │ │ ├── Expense_Report_Launcher_Main_13_task.json │ │ ├── FOUR-13453.json │ │ ├── FOUR-13457.json │ │ ├── FOUR-16958_watchers.json │ │ ├── FOUR-4849.json │ │ ├── FOUR-4853.json │ │ ├── FOUR-5086.json │ │ ├── FOUR-5139-calcError.json │ │ ├── FOUR-5139.json │ │ ├── FOUR-5161.json │ │ ├── FOUR-6523.json │ │ ├── FOUR-6788_screen_performance.json │ │ ├── FOUR-6788_screen_performance_2.json │ │ ├── FOUR-6910_Watcher_Checkbox.json │ │ ├── FOUR-6910_Watcher_Radio.json │ │ ├── FOUR-6990_Double_Loop.json │ │ ├── FOUR-7587_LoopNestedLoop.json │ │ ├── FPP_PFP_CHAIRS_SCREEN.json │ │ ├── LoopFileUploadScreen.json │ │ ├── MultiInstanceLoopContext.json │ │ ├── Nested screen FOUR-7257 4.4.0RC2.json │ │ ├── NestedScreenFOUR-7257.json │ │ ├── ParentNestedScreen.json │ │ ├── RAOS1.0.0AccountOpeningCustomerForm.json │ │ ├── RAOS_1.0.0_-_People_2.json │ │ ├── Screen _parent in default value.json │ │ ├── Screen error nested calc.json │ │ ├── Screen nested calc inside 2 loops.json │ │ ├── Screen parent in record list.json │ │ ├── TCP4-4444.json │ │ ├── TCP4-4446.json │ │ ├── TCP4-4452.json │ │ ├── TCP4-4454.json │ │ ├── Test Record List.json │ │ ├── Test Validation with Nested side effects.json │ │ ├── UUID_compatibility.json │ │ ├── avatar.jpeg │ │ ├── byPassFalse.json │ │ ├── byPassTrue.json │ │ ├── complex_screen.json │ │ ├── computed_datetime.json │ │ ├── default_values_with_computed_fields.json │ │ ├── deviceVisibilityDynamic.json │ │ ├── displayScreenNext.json │ │ ├── file1.jpeg │ │ ├── file1.png │ │ ├── file2.jpeg │ │ ├── file2.png │ │ ├── file3.jpeg │ │ ├── file3.png │ │ ├── interstitial_screen.json │ │ ├── large_screen_warning.json │ │ ├── loop_select_list.json │ │ ├── loops_validations_with_parent_rules.json │ │ ├── multi_loop_validations.json │ │ ├── multiple_file_download.json │ │ ├── multiple_upload.json │ │ ├── multiselect_with_string_value.json │ │ ├── nested_calc_properties.json │ │ ├── nested_calc_radio_freeze.json │ │ ├── nested_screen_with_event_submit.json │ │ ├── nested_validations.json │ │ ├── nested_validations_with_hidden_rules.json │ │ ├── parent_record_list_and_loop.json │ │ ├── parent_variable.json │ │ ├── recordListDeviceVisibility.json │ │ ├── record_list.json │ │ ├── record_list_date_input.json │ │ ├── record_list_fileupload.json │ │ ├── record_list_fileupload_loops.json │ │ ├── record_list_fileupload_required.json │ │ ├── record_list_invalid.json │ │ ├── record_list_multicolumn_loop.json │ │ ├── record_list_single_input.json │ │ ├── refresh_nested_nested_screen.json │ │ ├── refresh_nested_nested_screen_2.json │ │ ├── refresh_nested_screen.json │ │ ├── refresh_nested_screen_2.json │ │ ├── required_if_with_checkbox.json │ │ ├── required_unless_with_checkbox.json │ │ ├── screen2TCP4-4446.json │ │ ├── screen_parent_signature.json │ │ ├── screen_with_readonly_fields.json │ │ ├── select_list_array_data.json │ │ ├── select_list_checkbox_collection.json │ │ ├── select_list_collection.json │ │ ├── select_list_datasource.json │ │ ├── select_list_default_value.json │ │ ├── select_list_dependent.json │ │ ├── select_list_dependent_collection.json │ │ ├── select_list_multiselect_collection.json │ │ ├── select_list_mustache.json │ │ ├── select_list_mustache_custom_key.json │ │ ├── select_list_mustache_request_data.json │ │ ├── select_list_radio_collection.json │ │ ├── single_file_download.json │ │ ├── single_line_input.json │ │ ├── single_select_with_invalid_value.json │ │ ├── subtotals_with_loops.json │ │ ├── subtotals_with_loops_currency.json │ │ ├── subtotals_with_loops_decimal.json │ │ ├── subtotasl_calc_props.json │ │ ├── subtotasl_calc_props_currency.json │ │ ├── test_parent_in_validations.json │ │ ├── validation rules loop.json │ │ ├── validation_nested_variable.json │ │ ├── validation_rules.json │ │ ├── validation_rules_and_recordlist.json │ │ ├── validation_rules_name_object.json │ │ ├── watcher_inside_loop.json │ │ ├── watcher_on_loop_inside_recordlist.json │ │ ├── watcher_on_loop_new_array.json │ │ ├── watcher_select_list.json │ │ └── webentry.json │ ├── specs │ │ ├── Builder.spec.js │ │ ├── CalcDragAndDrop.spec.js │ │ ├── Clipboard.spec.js │ │ ├── ClipboardBwCompatibility.spec.js │ │ ├── ClipboardControl.spec.js │ │ ├── ClipboardDragPaste.spec.js │ │ ├── ClipboardFormTypeDisplay.spec.js │ │ ├── ClipboardManager.spec.js │ │ ├── ClipboardTabClearAll.spec.js │ │ ├── ClipboardTabDuplicate.spec.js │ │ ├── ClipboardTestCases.spec.js │ │ ├── ComplexScreen.spec.js │ │ ├── ComputedDateTime.spec.js │ │ ├── ComputedFields.spec.js │ │ ├── ComputedFieldsReadOnly.spec.js │ │ ├── CustomCss.spec.js │ │ ├── DatePicker.spec.js │ │ ├── DatePickerTimezone.spec.js │ │ ├── DefaulValueWithNestedAndRecordList.js │ │ ├── DefaultValueFromCalculatedField.spec.js │ │ ├── DefaultValues.spec.js │ │ ├── DefaultValuesWithComputedFields.spec.js │ │ ├── DeviceVisivilityInspector.spec.js │ │ ├── DoubleLoop.spec.js │ │ ├── EncryptedField.spec.js │ │ ├── EndEventRedirection.spec.js │ │ ├── ExpenseReport.spec.js │ │ ├── FOUR-16958_watchers.spec.js │ │ ├── FOUR-6721_RAOS_People2.spec.js │ │ ├── FOUR-7027_RecordList_nested_calc.spec.js │ │ ├── FOUR-7029_Parent_in_default_value.spec.js │ │ ├── FOUR-7587_LoopNestedLoop.spec.js │ │ ├── FOUR4849.spec.js │ │ ├── FOUR6788_ScreenPerformanceTests.spec.js │ │ ├── FileDownload.spec.js │ │ ├── FileUpload.spec.js │ │ ├── FormImage.spec.js │ │ ├── FormInput.spec.js │ │ ├── FormSelectList.spec.js │ │ ├── FormTextArea.spec.js │ │ ├── InterstitialRedirect.spec.js │ │ ├── LoadComplexData.spec.js │ │ ├── Loop.spec.js │ │ ├── LoopFileUpload.spec.js │ │ ├── LoopSelectList.spec.js │ │ ├── MediaQuery.spec.js │ │ ├── MultiColumn.spec.js │ │ ├── MultiInstanceLoopContext.spec.js │ │ ├── MultipleUpload.spec.js │ │ ├── MultiselectWithStringValue.spec.js │ │ ├── NestedCalcProperties.spec.js │ │ ├── NestedCalcRadioFreeze.spec.js │ │ ├── NestedScreen.spec.js │ │ ├── NestedScreenFOUR-7257.spec.js │ │ ├── NestedValidationRules.spec.js │ │ ├── Pages.spec.js │ │ ├── Pagination.spec.js │ │ ├── ParentAccessTests.spec.js │ │ ├── ParentVariable.spec.js │ │ ├── RAOS1.0.0AccountOpeningCustomerForm.spec.js │ │ ├── RecordList.spec.js │ │ ├── RecordListMultiColumnLoop.spec.js │ │ ├── RecordListWithLoops.js │ │ ├── RefreshNestedScreen.spec.js │ │ ├── ResponsivePreview.spec.js │ │ ├── RichText.spec.js │ │ ├── ScreenBuilder.spec.js │ │ ├── ScreenErrorNestedCalc.spec.js │ │ ├── ScreenTemplateSection.spec.js │ │ ├── ScreenWarnings.spec.js │ │ ├── SelectListCollection.spec.js │ │ ├── SelectListDataSource.spec.js │ │ ├── SelectListDefaultValues.spec.js │ │ ├── SelectListDependent.spec.js │ │ ├── SelectListDependentCollection.spec.js │ │ ├── SelectListMustache.spec.js │ │ ├── SelectListWatcher.spec.js │ │ ├── SingleSelectWithInvalidValue.spec.js │ │ ├── Subtotals.spec.js │ │ ├── TCP4-4444VerifyAddingMultipleContentToClipboard.spec.js │ │ ├── TCP4-4446VerifyMulticolumnControlsClipboard.spec.js │ │ ├── TCP4-4447VerifyUpdateConfigurationClipboard.spec.js │ │ ├── TCP4-4452VerifyReloadPageClipboard.spec.js │ │ ├── TCP4-4454DragAndPasteClipboard.spec.js │ │ ├── TCP4-4458ClearClipboard.spec.js │ │ ├── TCP4-4462VerifyAddComponentsToClipboard.spec.js │ │ ├── Task.spec.js │ │ ├── TaskRedirect.spec.js │ │ ├── TestValidationWithNestedSideEffects.spec.js │ │ ├── UndoRedo.spec.js │ │ ├── ValidationCalcsAndLoop.spec.js │ │ ├── ValidationNestedVariable.spec.js │ │ ├── ValidationRules.spec.js │ │ ├── ValidationRulesAdvanced.spec.js │ │ ├── ValidationRulesAndRecordlist.spec.js │ │ ├── ValidationRulesNameObject.js │ │ ├── ValidationShownOnSubmit.spec.js │ │ ├── VariableNames.js │ │ ├── VisibilityRule.spec.js │ │ ├── WatcherInsideLoop.spec.js │ │ ├── WatcherOnLoops.js │ │ ├── Watchers.spec.js │ │ ├── WatchersBypass.spec.js │ │ ├── WatchersDragAndDrop.spec.js │ │ ├── WebEntryEndEventRedirect.spec.js │ │ ├── WrongConfigurationErrors.js │ │ └── pagesDropdown.spec.js │ ├── support │ │ ├── commands.js │ │ ├── constants.js │ │ ├── e2e.js │ │ ├── index.js │ │ └── utils.js │ └── tsconfig.json └── unit │ ├── .eslintrc.js │ ├── ConditionalHide.spec.js │ ├── CustomCss.spec.js │ ├── DefaultValues.spec.js │ ├── FormImage.spec.js │ ├── OptionsList.spec.js │ ├── ValidationFactory.spec.js │ ├── VueFormBuilder.spec.js │ └── fixtures │ ├── VueFormRendererWrapper.vue │ ├── defaults.json │ └── screen_validation.json ├── vite.config.js ├── vue.config.js └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/.circleci 3 | **/.github 4 | **/.nyc_output 5 | **/coverage 6 | **/cypress 7 | **/dist 8 | **/public 9 | *.json 10 | *.svg 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | node: true 6 | }, 7 | 8 | globals: { 9 | ProcessMaker: "readonly" 10 | }, 11 | 12 | extends: [ 13 | "plugin:vue/recommended", 14 | "airbnb-base", 15 | "plugin:prettier/recommended", 16 | "plugin:storybook/recommended" 17 | ], 18 | 19 | parserOptions: { 20 | ecmaVersion: 6, 21 | sourceType: "module", 22 | parser: "@babel/eslint-parser" 23 | }, 24 | 25 | plugins: ["vue", "prettier"], 26 | 27 | rules: { 28 | "prettier/prettier": ["error", { trailingComma: "none" }], 29 | "no-unexpected-multiline": "error", 30 | "no-param-reassign": 1, 31 | eqeqeq: "error", 32 | "max-len": ["error", { code: 140, ignoreUrls: true }], 33 | "comma-dangle": ["error", "never"], 34 | quotes: [ 35 | "error", 36 | "double", 37 | { avoidEscape: true, allowTemplateLiterals: true } 38 | ], 39 | "import/no-extraneous-dependencies": "warn", 40 | "consistent-return": "warn", 41 | "no-plusplus": 0, 42 | "no-underscore-dangle": 0, 43 | "no-restricted-syntax": "warn", 44 | "no-continue": "warn", 45 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 46 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 47 | }, 48 | 49 | root: true, 50 | 51 | overrides: [ 52 | { 53 | files: ["**/tests/**/*.{j,t}s?(x)", "**/tests/e2e/**/*.spec.{j,t}s?(x)"], 54 | plugins: ["cypress"], 55 | env: { 56 | mocha: true, 57 | "cypress/globals": true 58 | }, 59 | rules: { 60 | strict: "off" 61 | } 62 | }, 63 | { 64 | files: ["**/tests/unit/**/*.spec.{j,t}s?(x)"], 65 | env: { 66 | jest: true 67 | } 68 | } 69 | ] 70 | }; 71 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Issue & Reproduction Steps 2 | 3 | Expected behavior: 4 | 5 | Actual behavior: 6 | 7 | ## Solution 8 | - 9 | 10 | ## How to Test 11 | Test the steps above 12 | 13 | ## Related Tickets & Packages 14 | - 15 | 16 | ## Code Review Checklist 17 | - [ ] I have pulled this code locally and tested it on my instance, along with any associated packages. 18 | - [ ] This code adheres to [ProcessMaker Coding Guidelines](https://github.com/ProcessMaker/processmaker/wiki/Coding-Guidelines). 19 | - [ ] This code includes a unit test or an E2E test that tests its functionality, or is covered by an existing test. 20 | - [ ] This solution fixes the bug reported in the original ticket. 21 | - [ ] This solution does not alter the expected output of a component in a way that would break existing Processes. 22 | - [ ] This solution does not implement any breaking changes that would invalidate documentation or cause existing Processes to fail. 23 | - [ ] This solution has been tested with enterprise packages that rely on its functionality and does not introduce bugs in those packages. 24 | - [ ] This code does not duplicate functionality that already exists in the framework or in ProcessMaker. 25 | - [ ] This ticket conforms to the PRD associated with this part of ProcessMaker. 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-package.yml: -------------------------------------------------------------------------------- 1 | name: Build Package Screen Builder 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, edited, closed] 5 | workflow_dispatch: 6 | jobs: 7 | run_deploy: 8 | name: Run Build PM4-workflow 9 | uses: processmaker/processmaker/.github/workflows/deploy-pm4.yml@develop 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yml: -------------------------------------------------------------------------------- 1 | name: SonarQube 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | name: Scan 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 14 | - uses: actions/download-artifact@v4 15 | with: 16 | name: code-coverage-report 17 | - name: Fix code coverage paths 18 | working-directory: ./coverage 19 | run: | 20 | sed -i 's@'$GITHUB_WORKSPACE'@/github/workspace/@g' lcov.info 21 | - uses: sonarsource/sonarqube-scan-action@master 22 | env: 23 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 24 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 25 | # If you wish to fail your job when the Quality Gate is red, uncomment the 26 | # following lines. This would typically be used to fail a deployment. 27 | # We do not recommend to use this in a pull request. Prefer using pull request 28 | # decoration instead. 29 | # - uses: sonarsource/sonarqube-quality-gate-action@master 30 | # timeout-minutes: 5 31 | # env: 32 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Storybook 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, edited] 5 | jobs: 6 | test: 7 | timeout-minutes: 60 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 20 14 | cache: 'npm' 15 | - name: Install dependencies 16 | run: npm install 17 | 18 | - name: Get installed Playwright version 19 | id: playwright-version 20 | run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV 21 | - name: Cache playwright binaries 22 | uses: actions/cache@v3 23 | id: playwright-cache 24 | with: 25 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 26 | path: | 27 | ~/.cache/ms-playwright 28 | - name: Install Playwright Browsers 29 | run: npx playwright install --with-deps chromium 30 | if: steps.playwright-cache.outputs.cache-hit != 'true' 31 | 32 | - name: Build Storybook 33 | run: npm run build-storybook --quiet 34 | - name: Serve Storybook and run tests 35 | run: | 36 | npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ 37 | "npx http-server storybook-static --port 6006 --silent" \ 38 | "npx wait-on tcp:6006 && npm run test-storybook --coverage" 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | .nyc_output/ 5 | coverage/ 6 | tests/e2e/screenshots/ 7 | tests/e2e/videos/ 8 | tests/e2e/downloads 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.code-workspace 23 | .editorconfig 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw* 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/dist 3 | 4 | # Ignore artifacts: 5 | build 6 | coverage 7 | 8 | # Ignore all HTML files: 9 | *.html 10 | 11 | # Ignore all fixtures 12 | tests/e2e/fixtures/ 13 | 14 | # Ignore local env files 15 | .env.local 16 | .env.*.local 17 | 18 | # Ignore log files 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Ignore editor directories and files 24 | .idea 25 | .vscode 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw* 31 | 32 | *.bpmn 33 | *.svg 34 | 35 | .circleci 36 | .github 37 | .nyc_output 38 | cypress 39 | public 40 | reports 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none" 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/vue-vite').StorybookConfig } */ 2 | const config = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-docs", 8 | "@storybook/addon-interactions" 9 | ], 10 | framework: { 11 | name: "@storybook/vue-vite", 12 | options: {} 13 | }, 14 | docs: { 15 | autodocs: "tag" 16 | } 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | 3 | addons.setConfig({ 4 | // Place the controls panel at the bottom of the canvas 5 | panelPosition: 'bottom', 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/vue').Preview } */ 2 | const preview = { 3 | parameters: { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i 9 | } 10 | } 11 | } 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProcessMaker Screen Builder 2 | 3 | `@processmaker/screen-builder` is a VueJS powered Screen Builder that produces compatible JSON for our vue-form-renderer. 4 | 5 | - [ProcessMaker Screen Builder](#processmaker-screen-builder) 6 | - [Project setup](#project-setup) 7 | - [Testing](#testing) 8 | 9 | ## Project setup 10 | 11 | Clone the repository and `cd` into the `screen-builder` directory: 12 | 13 | ```bash 14 | git clone git@github.com:ProcessMaker/screen-builder.git 15 | cd screen-builder 16 | ``` 17 | 18 | Install dependencies using NPM, then run the local development server: 19 | 20 | ```bash 21 | npm ci 22 | npm run dev 23 | ``` 24 | 25 | ## Testing 26 | 27 | Unit tests are set up using jest and end-to-end tests are set up using Cypress. Tests can be run locally with the following commands: 28 | 29 | ```bash 30 | # Run the Jest unit test suite 31 | npm test 32 | 33 | # Open Cypress to run the end-to-end (e2e) test suite 34 | npm run open-cypress 35 | ``` 36 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["istanbul"] 13 | } 14 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | const { prepareAudit, lighthouse } = require("@cypress-audit/lighthouse"); 3 | const { pa11y } = require("@cypress-audit/pa11y"); 4 | const globby = require("globby"); 5 | 6 | module.exports = defineConfig({ 7 | projectId: "c4pb2f", 8 | viewportWidth: 1140, 9 | viewportHeight: 668, 10 | video: false, 11 | fixturesFolder: "tests/e2e/fixtures", 12 | screenshotsFolder: "tests/e2e/screenshots", 13 | videosFolder: "tests/e2e/videos", 14 | downloadsFolder: "tests/e2e/downloads", 15 | experimentalMemoryManagement: true, 16 | numTestsKeptInMemory: 20, 17 | e2e: { 18 | // We've imported your old cypress plugins here. 19 | // You may want to clean this up later by importing these. 20 | setupNodeEvents(on, config) { 21 | require("@cypress/code-coverage/task")(on, config); 22 | // include any other plugin code... 23 | on("before:browser:launch", (browser = {}, launchOptions) => { 24 | prepareAudit(launchOptions); 25 | }); 26 | on("task", { 27 | lighthouse: lighthouse(), 28 | pa11y: pa11y(), 29 | // a task to find one file matching the given mask 30 | // returns just the first matching file 31 | async findFiles(mask) { 32 | if (!mask) { 33 | throw new Error("Missing a file mask to search"); 34 | } 35 | 36 | console.log("searching for files %s", mask); 37 | const list = await globby(mask); 38 | 39 | if (!list.length) { 40 | console.log("found no files"); 41 | 42 | return null; 43 | } 44 | 45 | console.log("found %d files, first one %s", list.length, list[0]); 46 | 47 | return list[0]; 48 | } 49 | }); 50 | // It's IMPORTANT to return the config object 51 | // with any changed environment variables 52 | return config; 53 | }, 54 | testIsolation: false, 55 | baseUrl: "http://localhost:5173", 56 | specPattern: "tests/e2e/specs/**/*.{js,jsx,ts,tsx}", 57 | supportFile: "tests/e2e/support/index.js", 58 | waitForAnimations: true 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-form-builder 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue', 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest', 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1', 15 | '\\.(css|less)$': 'identity-obj-proxy', 16 | }, 17 | snapshotSerializers: [ 18 | 'jest-serializer-vue', 19 | ], 20 | testMatch: [ 21 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', 22 | ], 23 | testURL: 'http://localhost/', 24 | transformIgnorePatterns: [ 25 | 'node_modules/(?!(vuetable-2|vue-uniq-ids|@processmaker/vue-form-elements/src)/)', 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=ProcessMaker_screen-builder_AYlHjxBBxYvY_isvKt7h 2 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info 3 | sonar.exclusions=node_modules/**, dist/**, public/**, home/**, storybook-static/** 4 | sonar.sources=src 5 | sonar.tests=tests 6 | sonar.cpd.exclusions=src/stories/** 7 | sonar.coverage.exclusions=src/stories/** 8 | -------------------------------------------------------------------------------- /src/VariableDataTypeProperties.js: -------------------------------------------------------------------------------- 1 | import SelectDataTypeMask from './components/inspector/select-data-type-mask'; 2 | 3 | const string = { value: 'string', content: 'Text' }; 4 | const int = { value: 'int', content: 'Integer' }; 5 | const currency = { value: 'currency', content: 'Currency' }; 6 | const password = { value: 'password', content: 'Password' }; 7 | const percentage = { value: 'percentage', content: 'Percentage' }; 8 | const float = { value: 'float', content: 'Decimal' }; 9 | const datetime = { value: 'datetime', content: 'Datetime' }; 10 | const date = { value: 'date', content: 'Date' }; 11 | const boolean = { value: 'boolean', content: 'Boolean' }; 12 | 13 | const allOptions = [string, int, currency, percentage, float, datetime, date, password]; 14 | const allOptionsWithoutDate = [string, int, float]; 15 | 16 | function dataTypeFactory(options) { 17 | return { 18 | type: 'FormMultiselect', 19 | field: 'dataFormat', 20 | config: { 21 | label: 'Data Type', 22 | name: 'Data Type', 23 | helper: 'The data type specifies what kind of data is stored in the variable.', 24 | validation: 'required', 25 | options, 26 | }, 27 | }; 28 | } 29 | 30 | function dataFormatFactory() { 31 | 32 | return { 33 | type: 'SelectDataTypeMask', 34 | field: 'dataMask', 35 | config: { 36 | label: 'Data Format', 37 | name: 'Data Format', 38 | helper: 'The data format for the selected type.', 39 | }, 40 | }; 41 | } 42 | 43 | export const DataTypeProperty = dataTypeFactory(allOptions); 44 | export const DataTypeWithoutDateProperty = dataTypeFactory(allOptionsWithoutDate); 45 | export const DataTypeBooleanProperty = dataTypeFactory([boolean]); 46 | export const DataTypeDateTimeProperty = dataTypeFactory([date, datetime]); 47 | export const DataFormatProperty = dataFormatFactory(); 48 | -------------------------------------------------------------------------------- /src/assets/Shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .btn-platform { 2 | background-color: #ffff; 3 | color: #6a7888; 4 | padding: 8px 8px 2px 8px; 5 | } 6 | .btn-platform:hover { 7 | color: #6a7888; 8 | } 9 | .page-dropdown-menu { 10 | min-width: 333px; 11 | max-height: 26rem; 12 | overflow-y: auto; 13 | scrollbar-width: thin; 14 | } -------------------------------------------------------------------------------- /src/assets/icons/Button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Button.png -------------------------------------------------------------------------------- /src/assets/icons/Bypass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/Checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Checkbox.png -------------------------------------------------------------------------------- /src/assets/icons/Checkboxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Checkboxes.png -------------------------------------------------------------------------------- /src/assets/icons/Date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Date.png -------------------------------------------------------------------------------- /src/assets/icons/Dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Dropdown.png -------------------------------------------------------------------------------- /src/assets/icons/FileUpload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/FileUpload.png -------------------------------------------------------------------------------- /src/assets/icons/HiddenField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/HiddenField.png -------------------------------------------------------------------------------- /src/assets/icons/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Image.png -------------------------------------------------------------------------------- /src/assets/icons/Label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Label.png -------------------------------------------------------------------------------- /src/assets/icons/Link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Link.png -------------------------------------------------------------------------------- /src/assets/icons/Location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Location.png -------------------------------------------------------------------------------- /src/assets/icons/Panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Panel.png -------------------------------------------------------------------------------- /src/assets/icons/QRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/QRCode.png -------------------------------------------------------------------------------- /src/assets/icons/RadioButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/RadioButton.png -------------------------------------------------------------------------------- /src/assets/icons/Signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Signature.png -------------------------------------------------------------------------------- /src/assets/icons/Subform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Subform.png -------------------------------------------------------------------------------- /src/assets/icons/SubmitButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/SubmitButton.png -------------------------------------------------------------------------------- /src/assets/icons/Subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Subtitle.png -------------------------------------------------------------------------------- /src/assets/icons/SuggestField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/SuggestField.png -------------------------------------------------------------------------------- /src/assets/icons/Table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Table.png -------------------------------------------------------------------------------- /src/assets/icons/TextArea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/TextArea.png -------------------------------------------------------------------------------- /src/assets/icons/TextField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/TextField.png -------------------------------------------------------------------------------- /src/assets/icons/Title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/Title.png -------------------------------------------------------------------------------- /src/assets/icons/Unbypass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/UploadAudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/UploadAudio.png -------------------------------------------------------------------------------- /src/assets/icons/UploadImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/UploadImage.png -------------------------------------------------------------------------------- /src/assets/icons/UploadVideo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/icons/UploadVideo.png -------------------------------------------------------------------------------- /src/assets/icons/angle-double-right-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/calendar-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/caret-square-down-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/check-square-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/columns-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/font-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/list-ul-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/paragraph-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/share-square-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/square-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/th-list-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/pencil-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/priority-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/welcome_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/src/assets/welcome_default.png -------------------------------------------------------------------------------- /src/components/ClipboardButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 64 | 65 | 78 | -------------------------------------------------------------------------------- /src/components/CssIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/SimpleErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | 43 | -------------------------------------------------------------------------------- /src/components/VariableNameGenerator.js: -------------------------------------------------------------------------------- 1 | export default class VariableNameGenerator { 2 | names = []; 3 | 4 | GetVariableNames(configuration) { 5 | configuration.forEach(item => { 6 | 7 | //If the element has containers 8 | if (Array.isArray(item)) { 9 | this.GetVariableNames(item); 10 | } 11 | 12 | //If the element has items 13 | if (item.items) { 14 | this.GetVariableNames(item.items); 15 | } 16 | 17 | //If the element has its variable name set 18 | if (item.config && item.config.name) { 19 | this.names.push(`${item.config.name}`); 20 | } 21 | 22 | }); 23 | 24 | } 25 | 26 | generate(config, component) { 27 | this.names = []; 28 | this.GetVariableNames(config); 29 | let definitionId = this.generateNameId(this.snakeCase(component), 1); 30 | this.names.push(definitionId); 31 | 32 | return [this.names, definitionId]; 33 | } 34 | 35 | snakeCase(name) { 36 | return name.replace(/\W+/g, ' ') 37 | .split(/ |\B(?=[A-Z])/) 38 | .map(word => word.toLowerCase()) 39 | .join('_'); 40 | } 41 | 42 | generateNameId = (name, id) => { 43 | let generated = name + '_' + id; 44 | if (this.names.indexOf(generated) !== -1) { 45 | id++; 46 | generated = this.generateNameId(name, id); 47 | } 48 | return generated; 49 | }; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/components/basic-search.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 53 | 54 | 92 | -------------------------------------------------------------------------------- /src/components/custom-css-output.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'custom-css-output', 3 | render(createElement) { 4 | return createElement('style', this.$slots.default); 5 | }, 6 | }; -------------------------------------------------------------------------------- /src/components/custom-css.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 71 | 72 | 86 | -------------------------------------------------------------------------------- /src/components/editor/index.js: -------------------------------------------------------------------------------- 1 | export { default as Loop } from "./loop.vue"; 2 | export { default as MultiColumn } from "./multi-column.vue"; 3 | -------------------------------------------------------------------------------- /src/components/inspector/analytics-selector.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /src/components/inspector/collection-designer-mode.vue: -------------------------------------------------------------------------------- 1 | 14 | 74 | -------------------------------------------------------------------------------- /src/components/inspector/collection-display-mode.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 84 | -------------------------------------------------------------------------------- /src/components/inspector/color-select.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 86 | 87 | 96 | -------------------------------------------------------------------------------- /src/components/inspector/data-source-types.js: -------------------------------------------------------------------------------- 1 | export const dataSources = [ 2 | { value: 'provideData', text: 'Provide Values' }, 3 | { value: 'dataObject', text: 'Request Data' }, 4 | { value: 'dataConnector', text: 'Data Connector' }, 5 | { value: 'collection', text: 'Collection' }, 6 | ]; 7 | 8 | export const dataSourceValues = dataSources.reduce((values, source) => { 9 | values[source.value] = source.value; 10 | return values; 11 | }, {}); 12 | -------------------------------------------------------------------------------- /src/components/inspector/device-visibility.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 62 | 63 | 72 | -------------------------------------------------------------------------------- /src/components/inspector/edit-option.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 45 | -------------------------------------------------------------------------------- /src/components/inspector/encrypted-config.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 76 | 77 | 79 | -------------------------------------------------------------------------------- /src/components/inspector/form-multiselect.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /src/components/inspector/image-upload.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | 53 | 62 | -------------------------------------------------------------------------------- /src/components/inspector/image-variable.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 63 | -------------------------------------------------------------------------------- /src/components/inspector/index.js: -------------------------------------------------------------------------------- 1 | export { default as CollectionSelectList } from "./collection-select-list.vue"; 2 | export { default as CollectionRecordsList } from "./collection-records-list.vue"; 3 | export { default as collectionDataSource } from "./collection-data-source.vue"; 4 | export { default as CollectionDisplayMode } from "./collection-display-mode.vue"; 5 | export { default as CollectionDesignerMode } from "./collection-designer-mode.vue"; 6 | export { default as ColorSelect } from "./color-select.vue"; 7 | export { default as ColorSelectRecord } from "./color-select.vue"; 8 | export { default as ColorSelectModern } from "./color-select-modern.vue"; 9 | export { default as ColumnSetup } from "./column-setup.vue"; 10 | export { default as ContainerColumns } from "./container-columns.vue"; 11 | export { default as DataMapping } from "./data-mapping.vue"; 12 | export { dataSources, dataSourceValues } from "./data-source-types"; 13 | export { default as DefaultValueEditor } from "./default-value-editor.vue"; 14 | export { default as DeviceVisibility } from "./device-visibility.vue"; 15 | export { default as EditOption } from "./edit-option.vue"; 16 | export { default as FormMultiselect } from "./form-multiselect.vue"; 17 | export { default as ImageUpload } from "./image-upload.vue"; 18 | export { default as ImageVariable } from "./image-variable.vue"; 19 | export { default as InputVariable } from "./input-variable.vue"; 20 | export { default as LoopInspector } from "./loop.vue"; 21 | export { default as MustacheHelper } from "./mustache-helper.vue"; 22 | export { default as OptionsList } from "./options-list.vue"; 23 | export { default as OutboundConfig } from "./outbound-config.vue"; 24 | export { default as PageSelect } from "./page-select.vue"; 25 | export { default as ScreenSelector } from "./screen-selector.vue"; 26 | export { default as SelectDataTypeMask } from "./select-data-type-mask.vue"; 27 | export { default as Tooltip } from "./tooltip.vue"; 28 | export { default as ValidationSelect } from "./validation-select.vue"; 29 | export { default as LoadingSubmitButton } from "./loading-submit-button.vue"; 30 | export { default as LabelSubmitButton } from "./label-submit-button.vue"; 31 | export { default as AnalyticsSelector } from "./analytics-selector.vue"; 32 | export { default as EncryptedConfig } from "./encrypted-config.vue"; 33 | -------------------------------------------------------------------------------- /src/components/inspector/label-submit-button.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 50 | -------------------------------------------------------------------------------- /src/components/inspector/loading-submit-button.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 48 | -------------------------------------------------------------------------------- /src/components/inspector/mustache-helper.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /src/components/inspector/page-select.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 45 | -------------------------------------------------------------------------------- /src/components/inspector/select-data-type-mask.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 51 | -------------------------------------------------------------------------------- /src/components/renderer/add-loop-row.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 64 | -------------------------------------------------------------------------------- /src/components/renderer/avatar-dropdown.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/components/renderer/form-analytics-chart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 58 | 59 | 83 | -------------------------------------------------------------------------------- /src/components/renderer/form-avatar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 69 | 70 | 79 | 80 | -------------------------------------------------------------------------------- /src/components/renderer/form-empty-table.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | 30 | 45 | -------------------------------------------------------------------------------- /src/components/renderer/form-image.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /src/components/renderer/form-input-masked.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 90 | 91 | 93 | -------------------------------------------------------------------------------- /src/components/renderer/form-new-request.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 84 | -------------------------------------------------------------------------------- /src/components/renderer/form-record-list-static.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/components/renderer/form-text.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | -------------------------------------------------------------------------------- /src/components/renderer/index.js: -------------------------------------------------------------------------------- 1 | export { default as AddLoopRow } from "./add-loop-row.vue"; 2 | export { default as FileDownload } from "./file-download.vue"; 3 | export { default as FileUpload } from "./file-upload.vue"; 4 | export { default as FormAvatar } from "./form-avatar.vue"; 5 | export { default as FormButton } from "./form-button.vue"; 6 | export { default as FormImage } from "./form-image.vue"; 7 | export { default as FormInputMasked } from "./form-masked-input.vue"; 8 | export { default as FormLoop } from "./form-loop.vue"; 9 | export { default as FormMaskedInput } from "./form-masked-input.vue"; 10 | // export { default as FormMultiColumn } from "./form-multi-column.vue"; 11 | export { default as FormNestedScreen } from "./form-nested-screen.vue"; 12 | export { default as FormRecordList } from "./form-record-list.vue"; 13 | export { default as FormRecordListStatic } from "./form-record-list-static.vue"; 14 | export { default as FormText } from "./form-text.vue"; 15 | export { default as NewFormMultiColumn } from "./new-form-multi-column.vue"; 16 | export { default as ScreenRendererError } from "./screen-renderer-error.vue"; 17 | export { default as FormListTable } from "./form-list-table.vue"; 18 | export { default as FormAnalyticsChart } from "./form-analytics-chart.vue"; 19 | export { default as FormRequests } from "./form-requests.vue"; 20 | export { default as FormTasks } from "./form-tasks.vue"; 21 | export { default as LinkButton } from "./link-button.vue"; 22 | export { default as FormCollectionRecordControl } from "./form-collection-record-control.vue"; 23 | export { default as FormCollectionViewControl } from "./form-collection-view-control.vue"; 24 | -------------------------------------------------------------------------------- /src/components/renderer/link-button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 31 | -------------------------------------------------------------------------------- /src/components/renderer/new-form-multi-column.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/components/renderer/screen-renderer-error.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/sortable/sortable.scss: -------------------------------------------------------------------------------- 1 | .sortable { 2 | &-box { 3 | font-family: "Open Sans", sans-serif !important; 4 | overflow-x: auto; 5 | } 6 | 7 | &-search-box { 8 | display: flex; 9 | align-items: center; 10 | border-color: #cdddee !important; 11 | 12 | & > input.form-control:focus { 13 | border: 0 none !important; 14 | } 15 | } 16 | 17 | &-search-icon { 18 | margin: { 19 | left: 16px; 20 | right: 8px; 21 | } 22 | color: #6A7888; 23 | } 24 | 25 | &-btn-new { 26 | background: #1572C2; 27 | color: #ffffff; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/components/sortable/tableStyles.scss: -------------------------------------------------------------------------------- 1 | $primary-bg-color: #f8fbff; 2 | $primary-border-color: #e6efff; 3 | 4 | $success-bg-color: #71d188; 5 | $success-border-color: #28a745; 6 | 7 | $warning-bg-color: #fff3cd; 8 | $warning-border-color: #ffc107; 9 | 10 | $secondary-bg-color: #fbfbfc; 11 | $secondary-border-color: #f3f5f7; 12 | 13 | @mixin record-list-table($bg-color, $border-color) { 14 | @extend .record-list-table-base; 15 | 16 | thead th { 17 | background-color: $bg-color; 18 | border-top-color: $border-color; 19 | border-bottom-color: $border-color; 20 | &:first-child { 21 | border-left-color: $border-color; 22 | } 23 | &:last-child { 24 | border-right-color: $border-color; 25 | } 26 | } 27 | 28 | tbody tr { 29 | td { 30 | border-color: $border-color; 31 | border-top: solid 0; 32 | } 33 | 34 | td:first-child { 35 | border-left-color: $border-color; 36 | } 37 | 38 | td:last-child { 39 | border-right-color: $border-color; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/utils/default-loading-spinner.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/components/utils/index.js: -------------------------------------------------------------------------------- 1 | import DefaultLoadingSpinner from "./default-loading-spinner.vue"; 2 | import MultipleUploadsCheckbox from "./multiple-uploads-checkbox.vue"; 3 | import RequiredCheckbox from "./required-checkbox.vue"; 4 | 5 | export { DefaultLoadingSpinner, MultipleUploadsCheckbox, RequiredCheckbox }; 6 | -------------------------------------------------------------------------------- /src/components/utils/multiple-uploads-checkbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | -------------------------------------------------------------------------------- /src/components/utils/required-checkbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | -------------------------------------------------------------------------------- /src/customLogs.js: -------------------------------------------------------------------------------- 1 | export default class CustomLog { 2 | static logWithStyle(icon, type, name, status, message, color) { 3 | const baseColor = color === '255, 0, 0' ? 'red' : 'green'; 4 | const style = `background-color: rgba(${color}, 0.1); color: ${baseColor}; padding: 2px 4px; border-radius: 3px;`; 5 | const transparentStyle = 'background-color: transparent'; 6 | 7 | const logPrefix = `%c${icon} %c${type} "${name}" has`; 8 | const logStatus = `%c${status}`; 9 | 10 | if (status === 'RUN') { 11 | console.log(`${logPrefix} ${logStatus}`, style, transparentStyle, style); 12 | } else if (status === 'FAILED') { 13 | console.groupCollapsed(`${logPrefix} ${logStatus}`, style, transparentStyle, style); 14 | console.log(`%c${message}`, style); 15 | console.groupEnd(); 16 | } 17 | } 18 | 19 | static success(type, name) { 20 | this.logWithStyle('\u2705', type, name, 'RUN', '', '0, 128, 0'); 21 | } 22 | 23 | static error(type, name, message) { 24 | this.logWithStyle('\u274C', type, name, 'FAILED', message, '255, 0, 0'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/form-input-mask-config.js: -------------------------------------------------------------------------------- 1 | import currencies from './currency.json'; 2 | 3 | export default { 4 | defaultMask: { 5 | label: 'Data Format', 6 | options: [], 7 | config: {}, 8 | }, 9 | currency: { 10 | label: 'Currency Format', 11 | options: currencies, 12 | optionsLabel: 'code', 13 | config: currencies, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/global-properties.js: -------------------------------------------------------------------------------- 1 | export const formTypes = { 2 | form: 'form', 3 | display: 'display', 4 | }; 5 | 6 | export default [ 7 | { 8 | inspector: [ 9 | { 10 | type: 'FormInput', 11 | field: 'conditionalHide', 12 | config: { 13 | label: 'Visibility Rule', 14 | helper: 'This control is hidden until this expression is true', 15 | }, 16 | }, 17 | { 18 | type: 'DeviceVisibility', 19 | field: 'deviceVisibility', 20 | config: { 21 | label: 'Device Visibility', 22 | helper: 'This control is hidden until this expression is true', 23 | }, 24 | }, 25 | { 26 | type: 'FormInput', 27 | field: 'customFormatter', 28 | config: { 29 | label: 'Custom Format String', 30 | helper: 'Use the Mask Pattern format
Date ##/##/####
SSN ###-##-####
Phone (###) ###-####', 31 | validation: '', 32 | }, 33 | }, 34 | { 35 | type: 'FormInput', 36 | field: 'customCssSelector', 37 | config: { 38 | label: 'CSS Selector Name', 39 | helper: 'Use this in your custom css rules', 40 | validation: 'regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]', 41 | }, 42 | }, 43 | { 44 | type: 'FormInput', 45 | field: 'ariaLabel', 46 | config: { 47 | label: 'Aria Label', 48 | helper: 'Attribute designed to help assistive technology (e.g. screen readers) attach a label', 49 | }, 50 | }, 51 | { 52 | type: 'FormInput', 53 | field: 'tabindex', 54 | config: { 55 | label: 'Tab Order', 56 | helper: 'Order in which a user will move focus from one control to another by pressing the Tab key', 57 | validation: 'regex: [0-9]*', 58 | }, 59 | }, 60 | { 61 | type: 'EncryptedConfig', 62 | field: 'encryptedConfig', 63 | config: { 64 | label: 'Encrypted', 65 | helper: '', 66 | }, 67 | }, 68 | ], 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /src/itemProcessingUtils.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import moment from 'moment-timezone'; 3 | 4 | function processFormItem(item) { 5 | if (item.component !== 'FormMultiColumn') { 6 | return item; 7 | } 8 | 9 | return item.items.flatMap(processFormItem); 10 | } 11 | 12 | export function getItemsFromConfig(config) { 13 | return config 14 | .flatMap(page => page.items) 15 | .flatMap(processFormItem); 16 | } 17 | 18 | export function getDefaultValueForItem(item) { 19 | let defaultValue = null; 20 | 21 | if (['FormInput', 'FormTextArea', 'FormText'].includes(item.component)) { 22 | defaultValue = ''; 23 | } 24 | 25 | if (item.component === 'FormCheckbox') { 26 | defaultValue = item.config.initiallyChecked || false; 27 | } 28 | 29 | if (item.component === 'FormRecordList') { 30 | defaultValue = []; 31 | } 32 | 33 | if (item.component === 'FormDatePicker') { 34 | defaultValue = generateNewDate(item.config.dataFormat); 35 | } 36 | 37 | if (item.component === 'FormButton' && item.config.event === 'script') { 38 | defaultValue = 0; 39 | } 40 | 41 | return defaultValue; 42 | } 43 | 44 | function generateNewDate(dataFormat) { 45 | let timezone = moment.tz.guess(); 46 | 47 | if (typeof window.ProcessMaker !== 'undefined' && window.ProcessMaker.user && window.ProcessMaker.user.timezone) { 48 | timezone = window.ProcessMaker.user.timezone; 49 | } 50 | 51 | const date = moment.tz(timezone); 52 | 53 | if (dataFormat !== 'datetime') { 54 | date.startOf('day'); 55 | } 56 | 57 | return date.toISOString(); 58 | } 59 | -------------------------------------------------------------------------------- /src/mixins/CurrentPageProperty.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | currentPage: null, 4 | }, 5 | methods: { 6 | updateCurrentPage() { 7 | if (this.currentPage!==undefined && this.currentPage!==null) { 8 | this.setCurrentPage(this.currentPage); 9 | } 10 | }, 11 | }, 12 | watch: { 13 | currentPage() { 14 | this.updateCurrentPage(); 15 | }, 16 | }, 17 | mounted() { 18 | this.$nextTick(() => { 19 | this.updateCurrentPage(); 20 | }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/mixins/DeviceDetector.js: -------------------------------------------------------------------------------- 1 | export const MAX_MOBILE_WIDTH = 480; 2 | export const originalDevicePixelRatio = window.devicePixelRatio; 3 | export default { 4 | created() { 5 | window.addEventListener("resize", this.resizeHandler); 6 | }, 7 | destroyed() { 8 | window.removeEventListener("resize", this.resizeHandler); 9 | }, 10 | mounted() { 11 | this.$nextTick(() => { 12 | this.checkIfIsMobile(); 13 | }); 14 | }, 15 | methods: { 16 | resizeHandler() { 17 | this.checkIfIsMobile(); 18 | }, 19 | checkIfIsMobile() { 20 | const renderer = document.getElementById("vue-form-renderer"); 21 | const isModelerInspector = this.data && this.data.$type && this.data.$type.startsWith("bpmn:"); 22 | if (this.definition && !isModelerInspector) { 23 | this.definition.isMobile = 24 | renderer && 25 | renderer.offsetWidth <= MAX_MOBILE_WIDTH && 26 | originalDevicePixelRatio === window.devicePixelRatio; 27 | } 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/mixins/HasColorProperty.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | methods: { 4 | elementCssClass(element) { 5 | const css = []; 6 | element.config.bgcolor ? css.push(element.config.bgcolor) : null; 7 | element.config.color ? css.push(element.config.color) : null; 8 | element.config.bgcolormodern ? css.push(element.config.bgcolormodern) : null; 9 | return css.join(' '); 10 | }, 11 | elementCssClassModern(element) { 12 | const css = []; 13 | element.config.bgcolormodern ? css.push(element.config.bgcolormodern) : null; 14 | return css.join(' '); 15 | } 16 | }, 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /src/mixins/VisibilityRule.js: -------------------------------------------------------------------------------- 1 | import { Parser } from 'expr-eval'; 2 | 3 | export default { 4 | methods: { 5 | visibilityRuleIsVisible(rule, name, deviceVisibility) { 6 | const visibility = deviceVisibility || { showForDesktop: true, showForMobile: true, isMobile: false }; 7 | const visibleInDevice = 8 | (visibility.isMobile && visibility.showForMobile) || (!visibility.isMobile && visibility.showForDesktop); 9 | 10 | try { 11 | if (rule && rule.trim().length > 0) { 12 | const dataWithParent = this.getDataReference(); 13 | const isVisible = Boolean(Parser.evaluate(rule, dataWithParent)); 14 | 15 | return isVisible && visibleInDevice; 16 | } 17 | 18 | return visibleInDevice; 19 | } catch (e) { 20 | // empty. 21 | } 22 | 23 | return false; 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/mixins/canOpenJsonFile.js: -------------------------------------------------------------------------------- 1 | import FileUpload from 'vue-upload-component'; 2 | 3 | const reader = new FileReader(); 4 | 5 | export default { 6 | components: { FileUpload }, 7 | data() { 8 | return { 9 | uploadedJson: null, 10 | jsonFiles: [], 11 | }; 12 | }, 13 | watch: { 14 | jsonFiles([fileObject]) { 15 | if (fileObject) { 16 | reader.readAsText(fileObject.file); 17 | } 18 | }, 19 | }, 20 | methods: { 21 | loadScreenPackage() { 22 | const json = JSON.parse(this.uploadedJson); 23 | let screen; 24 | if (json instanceof Array) { 25 | screen = { config:json, computed: [], customCSS: null }; 26 | } else if (json && json.screens instanceof Array) { 27 | screen = json.screens[0]; 28 | if (window.exampleScreens instanceof Array) { 29 | window.exampleScreens = json.screens; 30 | } 31 | } else if (json && json.screens && json.screens.config) { 32 | screen = json.screens; 33 | } 34 | this.$refs.builder.config.splice(0, Infinity, ...screen.config); 35 | this.$refs.builder.migrateConfig(); 36 | this.computed.splice(0, Infinity, ...screen.computed); 37 | this.customCSS = screen.customCSS; 38 | }, 39 | clearUpload() { 40 | this.uploadedJson = null; 41 | this.jsonFiles = []; 42 | }, 43 | setUploadedJson(event) { 44 | this.uploadedJson = event.target.result; 45 | }, 46 | }, 47 | created() { 48 | reader.onload = this.setUploadedJson; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/mixins/computedFields.js: -------------------------------------------------------------------------------- 1 | import { Parser } from "expr-eval"; 2 | import CustomLog from '../customLogs'; 3 | 4 | export default { 5 | methods: { 6 | evaluateExpression(expression, type) { 7 | let value = null; 8 | 9 | try { 10 | // Monitor if variable belongs to data (defined variables) or 11 | // vdata (external variables)in this way the event is not 12 | // executed again when the variable is update 13 | const data = this.getDataReference(null, () => { 14 | throw new Error( 15 | "You are not allowed to set properties from inside an expression" 16 | ); 17 | }); 18 | 19 | if (type === "expression") { 20 | value = Parser.evaluate(expression, data); 21 | } else { 22 | // Create a new function with the expression and bind the data context 23 | // eslint-disable-next-line no-new-func 24 | value = new Function(expression).bind(data); 25 | return { result: value(), error: null }; 26 | } 27 | 28 | if (value instanceof Date) { 29 | value = value.toISOString(); 30 | } 31 | return { result: value, error: null }; 32 | } catch (error) { 33 | // Catch any errors and return them 34 | return { result: null, error }; 35 | } 36 | }, 37 | /** 38 | * Logs an error message with a custom format. 39 | * @param {string} name - The name of the calculation. 40 | * @param {string} message - The error message. 41 | */ 42 | customErrorLog(name, message) { 43 | CustomLog.error('Calc', name, message); 44 | }, 45 | 46 | /** 47 | * Logs a success message with a custom format. 48 | * @param {string} name - The name of the calculation. 49 | */ 50 | customSuccessLog(name) { 51 | CustomLog.success('Calc', name); 52 | }, 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/mixins/extensions/AccordionContainer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted() { 3 | this.extensions.push({ 4 | onloadproperties({ componentName, properties, element }) { 5 | if (componentName === 'FormAccordion') { 6 | properties[':value'] = this.byRef(element.items); 7 | } 8 | }, 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/mixins/extensions/ComputedFields.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | /** 4 | * Implements computed fields like this: 5 | * 6 | * calcProperty() { 7 | * let value = this.evaluateExpression('return formula();', 'javascript'); 8 | * value = this.addNonDefinedComputedAttributes(value); 9 | * this.setValueAsync("calcProperty", value, this.vdata); 10 | * } 11 | */ 12 | computedFields(screen, definition, logsEnabled = true) { 13 | // For each computed field defined 14 | definition.computed.forEach((computed) => { 15 | if (computed.byPass) { 16 | // If the computed field has bypass set to true, skip it 17 | return; 18 | } 19 | const formula = JSON.stringify(computed.formula); 20 | const type = JSON.stringify(computed.type); 21 | const name = JSON.stringify(computed.property); 22 | const safeDotName = this.safeDotName(computed.property); 23 | const code = ` 24 | const evaluatedExpression = this.evaluateExpression(${formula}, ${type}); 25 | // Handle errors if any 26 | if (evaluatedExpression.error) { 27 | if (${logsEnabled}) { 28 | this.customErrorLog(${name}, evaluatedExpression.error); 29 | } 30 | } else { 31 | // Add non-defined computed attributes 32 | const value = this.addNonDefinedComputedAttributes(evaluatedExpression.result); 33 | // Set the value 34 | this.setValue(${name}, value, this.vdata); 35 | 36 | // Log the successful calculation if logging is enabled 37 | if (${logsEnabled}) { 38 | this.customSuccessLog(${name}); 39 | } 40 | 41 | // Return the result 42 | return value; 43 | } 44 | `; 45 | this.addComputed(screen, safeDotName, code, ""); 46 | // required to enable reactivity of computed field 47 | this.addWatch(screen, safeDotName, ""); 48 | }); 49 | } 50 | }, 51 | mounted() { 52 | this.extensions.push({ 53 | onbuild({ screen, definition }) { 54 | if (definition.computed) { 55 | this.computedFields(screen, definition); 56 | } 57 | } 58 | }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/mixins/extensions/CustomCss.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted() { 3 | this.extensions.push({ 4 | onloaditems({ element, wrapper }) { 5 | if (element.config.customCssSelector) { 6 | wrapper.setAttribute('selector', element.config.customCssSelector); 7 | } 8 | }, 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/mixins/extensions/DataManager.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | dataFields(screen, definition) { 4 | const localVariables = this.variables.filter( 5 | (v) => !this.isComputedVariable(v.name, definition) 6 | ); 7 | localVariables.forEach((v) => { 8 | const { component } = v.element; 9 | const dataFormat = v.config.dataFormat || null; 10 | const safeDotName = this.safeDotName(v.name); 11 | this.addData( 12 | screen, 13 | safeDotName, 14 | ` 15 | this.getValue(${JSON.stringify(v.name)}, this.vdata) || 16 | this.getValue(${JSON.stringify(v.name)}, data) || 17 | this.initialValue( 18 | '${component}', 19 | '${dataFormat}', 20 | ${JSON.stringify(v.config)}) 21 | `, 22 | v.name 23 | ); 24 | this.addWatch( 25 | screen, 26 | `vdata.${v.name}`, 27 | `if (this.canUpdate("${safeDotName}")) { 28 | this.${safeDotName} = value; 29 | }` 30 | ); 31 | }); 32 | this.addProp(screen, "vdata", null); 33 | }, 34 | /** 35 | * Replace `.` by `_DOT_` in a variable name 36 | * @param {string} name 37 | * @returns {string} 38 | */ 39 | safeDotName(name) { 40 | // if starts with _parent returns as is 41 | if (name.startsWith("_parent")) { 42 | return name; 43 | } 44 | return name.replace(/\./g, "_DOT_"); 45 | } 46 | }, 47 | mounted() { 48 | this.extensions.push({ 49 | onbuild({ screen, definition }) { 50 | this.dataFields(screen, definition); 51 | } 52 | }); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/mixins/extensions/InputText.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | mounted() { 4 | this.alias['FormInput'] = 'form-masked-input'; 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/mixins/extensions/MultiColumn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | loadMultiColumnProperties({ properties, element }) { 4 | properties.class = this.elementCssClass(element); 5 | properties.ref = 'container'; 6 | properties.selected = 'selected'; 7 | //properties['v-model'] = "element.items"; 8 | //@submit="submit" 9 | properties[':config'] = JSON.stringify(element.config); 10 | //:ancestor-screens="ancestorScreens" 11 | properties.name = element.config && element.config.name !== undefined ? element.config.name : null; 12 | //@pageNavigate="pageNavigate" 13 | //v-bind="element.config" 14 | //:is="element.component" 15 | properties[':form-config'] = '$parent && $parent.definition.config'; 16 | //:mode="mode" 17 | }, 18 | loadMultiColumnItems({ element, node, screen, definition, formIndex }) { 19 | element.items.forEach((col, index) => { 20 | const column = this.createComponent('div', { 21 | class: `col-sm-${element.config.options[index].content}`, 22 | }); 23 | this.loadItems(col, column, screen, definition, formIndex); 24 | node.appendChild(column); 25 | }); 26 | }, 27 | }, 28 | mounted() { 29 | this.extensions.push({ 30 | onloadproperties(params) { 31 | if (params.element.container && params.componentName === 'FormMultiColumn') { 32 | this.loadMultiColumnProperties(params); 33 | } 34 | }, 35 | onloaditems(params) { 36 | if (params.element.container && params.componentName === 'FormMultiColumn') { 37 | this.loadMultiColumnItems(params); 38 | } 39 | }, 40 | }); 41 | this.alias['FormMultiColumn'] = 'NewFormMultiColumn'; 42 | this.alias['MultiColumn'] = 'NewFormMultiColumn'; 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/mixins/extensions/PageNavigate.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | /* istanbul ignore next */ 4 | pageNavigate() {}, 5 | pageNavigationProperties({ properties }) { 6 | properties['@page-navigate'] = 'pageNavigate'; 7 | properties[':validate'] = '$v'; 8 | }, 9 | pageNavigationBuild(screen) { 10 | this.addData(screen, 'currentPage__', 'this._initialPage'); 11 | screen.methods.pageNavigate = function(page) { 12 | // Skip navigate button if page is not defined 13 | if (!this.$parent.definition.config[page]) { 14 | return; 15 | } 16 | this.$parent.$emit("updatePage"); 17 | this.currentPage__ = page; 18 | }; 19 | }, 20 | }, 21 | mounted() { 22 | this.extensions.push({ 23 | onloadproperties(params) { 24 | if (params.element.component === 'FormButton' && params.element.config.event === 'pageNavigate') { 25 | this.pageNavigationProperties(params); 26 | } 27 | }, 28 | onbuild({ screen }) { 29 | this.pageNavigationBuild(screen); 30 | }, 31 | }); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/mixins/extensions/Submit.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted() { 3 | this.extensions.push({ 4 | onloadproperties({ element, properties }) { 5 | if (element.component === 'FormButton' && element.config.event === 'submit') { 6 | properties[':validate'] = '$v'; 7 | } 8 | }, 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/mixins/extensions/VisibilityRule.js: -------------------------------------------------------------------------------- 1 | import VisibilityRule from '../VisibilityRule'; 2 | 3 | export default { 4 | mounted() { 5 | this.extensions.push({ 6 | onloaditems({ element, wrapper, definition }) { 7 | const visibility = element.config.deviceVisibility || { showForDesktop: true, showForMobile: true } 8 | const restrictDeviceVisibility = !visibility.showForDesktop || !visibility.showForMobile; 9 | 10 | 11 | element.visibleInDevice = 12 | (definition.isMobile && visibility.showForMobile) || 13 | (!definition.isMobile && visibility.showForDesktop); 14 | 15 | if (element.config.conditionalHide || restrictDeviceVisibility) { 16 | const deviceVisibility = JSON.stringify( { ...visibility, isMobile: definition.isMobile } ); 17 | wrapper.setAttribute( 18 | 'v-show', 19 | `visibilityRuleIsVisible(${JSON.stringify(element.config.conditionalHide)}, 20 | ${JSON.stringify(element.config.name)}, ${deviceVisibility})` 21 | ); 22 | } 23 | }, 24 | onbuild({ screen }) { 25 | screen.mixins.push(VisibilityRule); 26 | }, 27 | }); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/mixins/extensions/Watchers.js: -------------------------------------------------------------------------------- 1 | import watchersMixin from '../../mixins/watchers'; 2 | 3 | export default { 4 | methods: { 5 | filterWatchers(watcher) { 6 | const inContext = !watcher.watching || this.variables.find(({name}) => name === watcher.watching); 7 | return inContext; 8 | }, 9 | addWatcherVariables(definition) { 10 | if (definition.watchers) { 11 | definition.watchers.filter(this.filterWatchers).forEach((watcher) => { 12 | const inContext = !watcher.watching || this.variables.find(({name}) => name === watcher.watching); 13 | if (inContext) { 14 | this.registerVariable(watcher.output_variable, {}); 15 | } 16 | }); 17 | } 18 | }, 19 | watchers(screen, definition) { 20 | if (definition.watchers) { 21 | screen.mixins.push(watchersMixin); 22 | definition.watchers.filter(this.filterWatchers).forEach((watcher) => { 23 | if (watcher?.byPass) { 24 | // If the watcher has bypass set to true, skip it 25 | return; 26 | } 27 | this.addMounted(screen, ` 28 | this.$nextTick(() => this.$watch('vdata.${watcher.watching}', (newValue) => { 29 | if (typeof newValue !== 'undefined') { 30 | this.queueWatcher(${JSON.stringify(watcher)}); 31 | } 32 | })); 33 | `); 34 | 35 | if (watcher.run_onload) { 36 | this.addMounted(screen, ` 37 | this.queueWatcher(${JSON.stringify(watcher)}); 38 | `); 39 | } 40 | }); 41 | } 42 | }, 43 | }, 44 | mounted() { 45 | this.extensions.push({ 46 | onbuild({ screen, definition }) { 47 | this.watchers(screen, definition); 48 | }, 49 | onparse({ definition }) { 50 | this.addWatcherVariables(definition); 51 | }, 52 | }); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/mixins/extensions/index.js: -------------------------------------------------------------------------------- 1 | const mixins = []; 2 | const modules = import.meta.glob("./*.js", { eager: true }); 3 | 4 | Object.entries(modules).forEach(([path, m]) => { 5 | mixins.push(m.default); 6 | }); 7 | 8 | export default mixins; 9 | -------------------------------------------------------------------------------- /src/mixins/getValidPath.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | getValidPath(objectPath) { 4 | return this.objectPathHasError(objectPath) 5 | ? `["${objectPath}"]` 6 | : objectPath; 7 | }, 8 | objectPathHasError(objectPath) { 9 | try { 10 | this.$vueSet({}, objectPath); 11 | return false; 12 | } catch (error) { 13 | return true; 14 | } 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | export { default as canOpenJsonFile } from "./canOpenJsonFile"; 2 | export { default as computedFields } from "./computedFields"; 3 | export { default as CurrentPageProperty } from "./CurrentPageProperty"; 4 | export { default as DataReference } from "./DataReference"; 5 | export { default as datatable } from "./datatable"; 6 | export { default as defaultValues } from "./defaultValues"; 7 | export { default as DeviceDetector } from "./DeviceDetector"; 8 | export { default as focusErrors } from "./focusErrors"; 9 | export { default as formWatchers } from "./formWatchers"; 10 | export { default as getValidPath } from "./getValidPath"; 11 | export { default as HasColorProperty } from "./HasColorProperty"; 12 | export { default as Json2Vue } from "./Json2Vue"; 13 | export { default as multiselectApi } from "./multiselectApi"; 14 | export { default as mustacheEvaluation } from "./mustacheEvaluation"; 15 | export { default as ScreenBase } from "./ScreenBase"; 16 | export { default as shouldElementBeVisible } from "./shouldElementBeVisible"; 17 | export { default as testing } from "./testing"; 18 | export { ValidationMsg, validators } from "./ValidationRules"; 19 | export { default as VisibilityRule } from "./VisibilityRule"; 20 | export { default as watchers } from "./watchers"; 21 | export { default as Clipboard } from "./Clipboard"; -------------------------------------------------------------------------------- /src/mixins/multiselectApi.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash'; 2 | import debounce from 'lodash/debounce'; 3 | 4 | export default { 5 | props: { 6 | value: null, 7 | placeholder: String, 8 | trackBy: { 9 | type: String, 10 | default: 'id', 11 | }, 12 | label: { 13 | type: String, 14 | default: 'name', 15 | }, 16 | api: { 17 | type: String, 18 | default: 'process', 19 | }, 20 | multiple: { 21 | type: Boolean, 22 | default: false, 23 | }, 24 | storeId: { 25 | type: Boolean, 26 | default: true, 27 | }, 28 | }, 29 | data() { 30 | return { 31 | pmql: null, 32 | options: [], 33 | selectedOption: null, 34 | fields: null, 35 | }; 36 | }, 37 | watch: { 38 | value: { 39 | immediate: true, 40 | handler(value) { 41 | this.selectedOption = this.storeId 42 | ? this.options.find(option => get(option, this.trackBy) == value) 43 | : value; 44 | value && !this.selectedOption ? this.loadSelected(value) : null; 45 | }, 46 | }, 47 | }, 48 | methods: { 49 | change(value) { 50 | this.$emit('input', this.storeId ? get(value, this.trackBy) : value); 51 | }, 52 | loadOptions(filter) { 53 | const pmql = this.pmql; 54 | const fields = this.fields || undefined; 55 | window.ProcessMaker.apiClient 56 | .get(this.api, { params: { filter, pmql, fields } }) 57 | .then(response => { 58 | this.options = response.data.data || []; 59 | }); 60 | }, 61 | loadSelected(value) { 62 | window.ProcessMaker.apiClient 63 | .get(`${this.api}/${value}`) 64 | .then(response => { 65 | this.selectedOption = response.data; 66 | }); 67 | }, 68 | }, 69 | mounted() { 70 | this.loadOptions = debounce(this.loadOptions, 1000); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/mixins/mustacheEvaluation.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Mustache from 'mustache'; 3 | 4 | export default { 5 | methods: { 6 | mustache(expression, data) { 7 | const value = _.get(data, expression); 8 | try { 9 | return expression.indexOf('{{') > -1 ? Mustache.render(expression, data) : value; 10 | } catch (error) { 11 | return value; 12 | } 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/mixins/shouldElementBeVisible.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Parser } from 'expr-eval'; 3 | 4 | export default { 5 | methods: { 6 | shouldElementBeVisible(element) { 7 | const { conditionalHide } = element.config; 8 | 9 | if (!conditionalHide) { 10 | return true; 11 | } 12 | 13 | try { 14 | return !!Parser.evaluate(conditionalHide, this.transientData); 15 | } catch (error) { 16 | return false; 17 | } 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/mixins/testing.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash'; 2 | 3 | // TESTING HELPERS 4 | let mounted; 5 | window.testing = { 6 | builder: null, 7 | onready: new Promise(resolve => mounted = resolve), 8 | addControlByType(type, target = null) { 9 | const control = this.builder.controls.find(control => control.component === type); 10 | this.builder.testingAddControl(control, target); 11 | }, 12 | addControlByLabel(label, target = null) { 13 | const control = this.builder.controls.find(control => control.label === label); 14 | this.builder.testingAddControl(control, target); 15 | }, 16 | removeControl(index) { 17 | this.builder.testingDeleteItem(index); 18 | }, 19 | showControlsTypes() { 20 | const types = this.builder.controls.map(control => control.component); 21 | // eslint-disable-next-line no-console 22 | console.log(types); 23 | }, 24 | showControlsLabels() { 25 | const labels = this.builder.controls.map(control => control.label); 26 | // eslint-disable-next-line no-console 27 | console.log(labels); 28 | }, 29 | }; 30 | 31 | export default { 32 | data() { 33 | window.testing.builder = this; 34 | return {}; 35 | }, 36 | methods: { 37 | testingAddControl(control, target) { 38 | const clone = this.cloneControl(control); 39 | if (!target) { 40 | this.config[this.currentPage].items.push(clone); 41 | } else { 42 | const items = get(this, target); 43 | items.push(clone); 44 | } 45 | this.updateState(); 46 | this.inspect(clone); 47 | }, 48 | testingDeleteItem(index) { 49 | this.config[this.currentPage].items.splice(index, 1); 50 | this.updateState(); 51 | }, 52 | }, 53 | mounted() { 54 | this.$nextTick(() => mounted()); 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/store/modules/undoRedoModule.js: -------------------------------------------------------------------------------- 1 | const undoRedoModule = { 2 | namespaced: true, 3 | state() { 4 | return { 5 | stack: [], 6 | position: null 7 | }; 8 | }, 9 | getters: { 10 | canUndo(state) { 11 | return state.position > 0; 12 | }, 13 | canRedo(state) { 14 | return state.position < state.stack.length - 1; 15 | }, 16 | currentState(state, getters) { 17 | const currentState = state.stack[state.position]; 18 | 19 | if (getters.nextState && getters.nextState.deletedPage) { 20 | currentState.currentPage = getters.nextState.currentPage; 21 | } 22 | 23 | return currentState; 24 | }, 25 | nextState(state, getters) { 26 | if (getters.canRedo) { 27 | return state.stack[state.position + 1]; 28 | } 29 | 30 | return false; 31 | } 32 | }, 33 | mutations: { 34 | setPosition(state, position) { 35 | state.position = position; 36 | }, 37 | setState(state, newState) { 38 | state.stack.push(newState); 39 | } 40 | }, 41 | methods: { 42 | emitChanges() { 43 | window.ProcessMaker.EventBus.$emit("screen-change"); 44 | } 45 | }, 46 | actions: { 47 | pushState({ state, getters, commit }, newState) { 48 | if (newState.config === getters.currentState) { 49 | return; 50 | } 51 | 52 | commit("setState", newState); 53 | commit("setPosition", state.stack.length - 1); 54 | undoRedoModule.methods.emitChanges(); 55 | }, 56 | undo({ state, getters, commit }) { 57 | if (!getters.canUndo) { 58 | return; 59 | } 60 | 61 | commit("setPosition", state.position - 1); 62 | undoRedoModule.methods.emitChanges(); 63 | }, 64 | redo({ state, getters, commit }) { 65 | if (!getters.canRedo) { 66 | return; 67 | } 68 | 69 | commit("setPosition", state.position + 1); 70 | undoRedoModule.methods.emitChanges(); 71 | } 72 | } 73 | }; 74 | 75 | export default undoRedoModule; 76 | -------------------------------------------------------------------------------- /src/stories/ClipboardButton.stories.js: -------------------------------------------------------------------------------- 1 | import ClipboardButton from '../components/ClipboardButton.vue'; 2 | import { within, userEvent, expect } from "@storybook/test"; 3 | 4 | export default { 5 | title: 'Components/ClipboardButton', 6 | tags: ["autodocs"], 7 | component: ClipboardButton, 8 | argTypes: { 9 | isInClipboard: { control: 'boolean' }, 10 | }, 11 | }; 12 | 13 | const Template = (args) => ({ 14 | components: { ClipboardButton }, 15 | setup() { 16 | return { args }; 17 | }, 18 | template: '', 19 | }); 20 | 21 | // Story for when the item is in the clipboard 22 | export const InClipboard = Template.bind({}); 23 | InClipboard.args = { 24 | isInClipboard: true, 25 | index: 1, 26 | addTitle: 'Add to clipboard', 27 | removeTitle: 'Remove from clipboard', 28 | addToClipboard: () => {}, 29 | removeFromClipboard: () => {}, 30 | }; 31 | InClipboard.play = async ({ canvasElement }) => { 32 | const canvas = within(canvasElement); 33 | 34 | // Expect the "Remove from clipboard" button to be rendered 35 | const removeButton = await canvas.getByRole('button', { name: /Remove from clipboard/i }); 36 | expect(removeButton).toBeInTheDocument(); 37 | 38 | // Simulate a click on the remove button 39 | await userEvent.click(removeButton); 40 | 41 | // Ensure that the button remains visible after interaction 42 | expect(removeButton).toBeVisible(); 43 | }; 44 | 45 | // Story for when the item is not in the clipboard 46 | export const NotInClipboard = Template.bind({}); 47 | NotInClipboard.args = { 48 | isInClipboard: false, 49 | index: 1, 50 | addToClipboard: () => {}, 51 | removeFromClipboard: () => {}, 52 | }; 53 | 54 | NotInClipboard.play = async ({ canvasElement }) => { 55 | const canvas = within(canvasElement); 56 | 57 | // Expect the "Add to clipboard" button to be rendered 58 | const addButton = await canvas.getByRole('button', { name: /Add to clipboard/i }); 59 | expect(addButton).toBeInTheDocument(); 60 | 61 | // Simulate a click on the add button 62 | await userEvent.click(addButton); 63 | 64 | // Ensure that the button remains visible after interaction 65 | expect(addButton).toBeVisible(); 66 | }; 67 | -------------------------------------------------------------------------------- /tests/components/RenderScreen.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /tests/components/RenderScreen2.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /tests/components/WebEntry.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 87 | 88 | 94 | -------------------------------------------------------------------------------- /tests/components/index.js: -------------------------------------------------------------------------------- 1 | const components = {}; 2 | const modules = import.meta.glob("./*.vue"); 3 | Object.entries(modules).forEach(([path, m]) => { 4 | components[path.substr(2, path.length - 6)] = m; 5 | }); 6 | 7 | export default components; 8 | -------------------------------------------------------------------------------- /tests/e2e/fixtures/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/avatar.jpeg -------------------------------------------------------------------------------- /tests/e2e/fixtures/file1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file1.jpeg -------------------------------------------------------------------------------- /tests/e2e/fixtures/file1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file1.png -------------------------------------------------------------------------------- /tests/e2e/fixtures/file2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file2.jpeg -------------------------------------------------------------------------------- /tests/e2e/fixtures/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file2.png -------------------------------------------------------------------------------- /tests/e2e/fixtures/file3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file3.jpeg -------------------------------------------------------------------------------- /tests/e2e/fixtures/file3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProcessMaker/screen-builder/5035e14771d97d4189c2ec777725b4f38855f1e6/tests/e2e/fixtures/file3.png -------------------------------------------------------------------------------- /tests/e2e/specs/ClipboardBwCompatibility.spec.js: -------------------------------------------------------------------------------- 1 | import ClipboardManager from "../../../src/store/modules/ClipboardManager"; 2 | 3 | describe("Clipboard backward compatibility", () => { 4 | beforeEach(() => { 5 | cy.visit("/"); 6 | }); 7 | 8 | it("FOUR-19630: Check clipboard with a screen from a previous version", () => { 9 | ClipboardManager.saveToLocalStorage([]); 10 | cy.loadFromJson("UUID_compatibility.json", 1); 11 | 12 | // for each control, check if it can be added to the clipboard 13 | cy.get('.control-item').each(($el, index) => { 14 | cy.wrap($el).click({ force: true }); 15 | cy.get('[data-cy="addToClipboard"]').click({ force: true }); 16 | cy.get('[data-cy="addToClipboard"]').should("not.exist").then(() => { 17 | // Verify it was added to the clipboard storage 18 | const content = ClipboardManager.loadFromLocalStorage(); 19 | expect(content.length).to.equal(index + 1); 20 | }); 21 | }); 22 | 23 | // Go to second page 24 | cy.get('[data-test="page-dropdown"] button').click({force: true}); 25 | cy.get('[data-test="page-popup"]').click({force: true}); 26 | cy.wait(1000); 27 | const elementsAddedFromMainPage = 17; 28 | 29 | // check if each control in the second page can be added to the clipboard 30 | cy.get('.control-item').each(($el, index) => { 31 | cy.wrap($el).click({ force: true }); 32 | cy.get('[data-cy="addToClipboard"]').click({ force: true }); 33 | cy.get('[data-cy="addToClipboard"]').should("not.exist").then(() => { 34 | // Verify it was added to the clipboard storage 35 | const content = ClipboardManager.loadFromLocalStorage(); 36 | expect(content.length).to.equal(elementsAddedFromMainPage + index + 1); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/e2e/specs/ClipboardControl.spec.js: -------------------------------------------------------------------------------- 1 | describe("Add Clipboard Section to Control Menu", () => { 2 | 3 | /** 4 | * Test to verify that the Clipboard section is present in the control menu. 5 | */ 6 | it('should display the "Clipboard" section in the control menu', () => { 7 | // Step 1: Navigate to the homepage 8 | cy.visit("/"); 9 | 10 | // Step 2: Open the clipboard acordeon 11 | cy.openAcordeonByLabel("Clipboard"); 12 | 13 | // Step 3: Verify that the Clipboard section is visible in the control menu 14 | cy.contains("Clipboard").should("be.visible"); 15 | }); 16 | 17 | /** 18 | * Test to verify that the Clipboard accordion expands and shows the "Drag to Paste" control. 19 | */ 20 | it('should expand the Clipboard accordion and display the "Drag to Paste" control', () => { 21 | // Step 1: Navigate to the homepage 22 | cy.visit("/"); 23 | 24 | // Step 2: Open the clipboard acordeon 25 | cy.openAcordeonByLabel("Clipboard"); 26 | 27 | // Step 3: Verify that the Clipboard accordion exists and is visible 28 | cy.get('[data-cy="controls-Clipboard"]').should("exist").should("be.visible"); 29 | }); 30 | 31 | /** 32 | * Test to verify the visibility of the popover when hovering over the "Drag to Paste" control. 33 | */ 34 | it('should display the popover when hovering over the "Drag to Paste" control', () => { 35 | // Step 1: Navigate to the homepage 36 | cy.visit("/"); 37 | 38 | // Step 2: Open the clipboard acordeon 39 | cy.openAcordeonByLabel("Clipboard"); 40 | 41 | // Step 3: Click to open the Clipboard accordion 42 | cy.get('[data-cy="controls-Clipboard"]').click(); 43 | 44 | // Step 4: Verify that the popover associated with the "Drag to Paste" control is visible 45 | cy.get('.custom-popover').should("be.visible"); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/e2e/specs/ClipboardFormTypeDisplay.spec.js: -------------------------------------------------------------------------------- 1 | import ClipboardManager from "../../../src/store/modules/ClipboardManager"; 2 | 3 | describe("Clipboard Form Type Display", () => { 4 | before(() => { 5 | // Visit the home page and load the JSON data before the tests 6 | cy.visit('/'); 7 | cy.loadFromJson('displayScreenNext.json', 0, 'display'); 8 | }); 9 | 10 | const checkAddToClipboardNotExist = (childIndex) => { 11 | cy.get(`:nth-child(${childIndex}) > [data-cy="screen-element-container"]`) 12 | .click({ force: true }); 13 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 14 | }; 15 | 16 | it("Verify add clipboard button does not exist in display mode", () => { 17 | // Verify that the button does not exist for specific child elements 18 | checkAddToClipboardNotExist(2); 19 | checkAddToClipboardNotExist(3); 20 | }); 21 | }); -------------------------------------------------------------------------------- /tests/e2e/specs/ComputedDateTime.spec.js: -------------------------------------------------------------------------------- 1 | describe("Computed datetime", () => { 2 | it("", () => { 3 | cy.visit("/", { 4 | onBeforeLoad(win) { 5 | cy.stub(win.console, "log").as("consoleLog"); 6 | cy.stub(win.console, "error").as("consoleError"); 7 | } 8 | }); 9 | cy.loadFromJson("computed_datetime.json", 0); 10 | 11 | // Enter preview mode 12 | cy.get("[data-cy=mode-preview]").click(); 13 | 14 | // verify that no console errors are registerd when using the computed 15 | // property 16 | cy.get("@consoleError").should("not.to.be.called"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/e2e/specs/DatePickerTimezone.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | describe("Date Picker", () => { 4 | beforeEach(() => { 5 | // Set date to midnight January 1, 2024. 6 | // The timezone is UTC. We set it to UTC in package.json `TZ=UTC` 7 | cy.clock(new Date(2024, 0, 0, 0, 0, 0), ['Date']); 8 | cy.visit("/"); 9 | cy.openAcordeon("collapse-2"); 10 | 11 | // Set the User's timezone to Los Angeles. This is what DatePicker will use 12 | cy.window().then((win) => { 13 | win.ProcessMaker.user.timezone = "America/Los_Angeles"; 14 | }); 15 | }); 16 | 17 | it("should convert the date from ther users timezone to UTC", () => { 18 | cy.get("[data-cy=controls-FormDatePicker]").drag( 19 | "[data-cy=screen-drop-zone]", 20 | "bottom" 21 | ); 22 | cy.get("[data-cy=screen-element-container]").click(); 23 | cy.setMultiselect("[data-cy=inspector-dataFormat]", "Datetime"); 24 | cy.get("[data-cy=mode-preview]").click(); 25 | cy.get( 26 | "[data-cy=preview-content] [data-cy='screen-field-form_date_picker_1'] input" 27 | ).click(); 28 | // Choose today. In Los Angeles, that would be December 31, 2023 29 | cy.get( 30 | "[data-cy=preview-content] [data-cy='screen-field-form_date_picker_1'] .selectable.today" 31 | ).click(); 32 | // Set time to 8:15 PM Los Angeles time 33 | cy.get( 34 | "[data-cy=preview-content] [data-cy='screen-field-form_date_picker_1'] .vdpHoursInput" 35 | ).type("8"); 36 | cy.get( 37 | "[data-cy=preview-content] [data-cy='screen-field-form_date_picker_1'] .vdpMinutesInput" 38 | ).type("{moveToEnd}15"); 39 | cy.get( 40 | "[data-cy=preview-content] [data-cy='screen-field-form_date_picker_1'] .vdp12HourToggleBtn" 41 | ).then((toggle) => { 42 | // Change to AM/PM - Time is now 8:15 PM, Los Angeles time 43 | if (toggle.is(".vdp12HourToggleBtn")) { 44 | cy.get(toggle).click(); 45 | } 46 | }); 47 | 48 | cy.assertPreviewData({ 49 | // 8:15 PM on December 31, 2023 in Los Angeles is 04:15 (4:15am) the next day (January 1, 2024) in UTC 50 | form_date_picker_1: "2024-01-01T04:15:00.000Z" 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/e2e/specs/DefaulValueWithNestedAndRecordList.js: -------------------------------------------------------------------------------- 1 | describe("Validation Default value", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify with mustache in nested and record list", () => { 7 | cy.loadFromJson("parent_record_list_and_loop.json", 0); 8 | cy.get("[data-cy=mode-preview]").click(); 9 | 10 | fillInputText("screen-field-parentValue", 0, 130); 11 | 12 | cy.assertPreviewData({ 13 | parentValue: "130", 14 | loop_1: [ 15 | { 16 | form_record_list_1: null 17 | } 18 | ], 19 | loop_2: [ 20 | { 21 | form_input_1: "130" 22 | } 23 | ] 24 | }); 25 | 26 | cy.get("[data-cy=add-row]").click(); 27 | cy.get( 28 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_1]" 29 | ).should("have.value", "130"); 30 | }); 31 | }); 32 | 33 | function fillInputText(dataCy, index = null, value = "test") { 34 | if (index === null) { 35 | cy.get(`[data-cy=preview-content] [data-cy="${dataCy}"]`) 36 | .clear() 37 | .type(value); 38 | } else { 39 | cy.get(`[data-cy=preview-content] [data-cy="${dataCy}"]`) 40 | .eq(index) 41 | .clear() 42 | .type(value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/e2e/specs/DefaultValueFromCalculatedField.spec.js: -------------------------------------------------------------------------------- 1 | describe("Computed field and default values", () => { 2 | it("Test default values with computed fields", () => { 3 | cy.visit("/"); 4 | 5 | cy.loadFromJson("FOUR-6523.json", 0); 6 | 7 | // Preview 8 | cy.get("[data-cy=mode-preview]").click(); 9 | 10 | cy.assertPreviewData({ 11 | c: null, 12 | a: "0", 13 | b: 0, 14 | total: 0 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/e2e/specs/DefaultValuesWithComputedFields.spec.js: -------------------------------------------------------------------------------- 1 | describe("Computed field and default values", () => { 2 | it("Test default values with computed fields", () => { 3 | cy.visit("/"); 4 | 5 | cy.loadFromJson("default_values_with_computed_fields.json", 0); 6 | 7 | // Preview 8 | cy.get("[data-cy=mode-preview]").click(); 9 | 10 | // Assertion: Calculated properties are correct: 11 | // - varA = 1 12 | // - res = ok 13 | // Control with default value is correct: 14 | // - uno = {{varA}} = "1" 15 | cy.assertPreviewData({ 16 | uno: "1", 17 | varA: 1, 18 | res: "ok" 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/e2e/specs/DoubleLoop.spec.js: -------------------------------------------------------------------------------- 1 | describe("Double loop tests", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify access to _parent._parent inside a double loop", () => { 7 | cy.loadFromJson("FOUR-6990_Double_Loop.json", 0); 8 | // set init screen test data 9 | cy.setPreviewDataInput({ 10 | accounts: [ 11 | { 12 | accountUid: "one", 13 | accountLabel: "one" 14 | }, 15 | { 16 | accountUid: "two", 17 | accountLabel: "two" 18 | } 19 | ] 20 | }); 21 | cy.get("[data-cy=mode-preview]").click(); 22 | 23 | // Select option "one" on select inside loop 2 24 | cy.get( 25 | "[data-cy=preview-content] [data-cy=screen-field-form_select_list_1]" 26 | ).selectOption("one"); 27 | 28 | // Select option "one" on select inside loop 3 29 | cy.get( 30 | "[data-cy=preview-content] [data-cy=screen-field-form_select_list_2]" 31 | ).selectOption("two"); 32 | 33 | // Check the data of the screen 34 | cy.assertPreviewData({ 35 | accounts: [ 36 | { 37 | accountUid: "one", 38 | accountLabel: "one" 39 | }, 40 | { 41 | accountUid: "two", 42 | accountLabel: "two" 43 | } 44 | ], 45 | loop_1: [ 46 | { 47 | form_select_list_1: "one", 48 | loop_3: [ 49 | { 50 | form_select_list_2: "two" 51 | } 52 | ] 53 | } 54 | ] 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/e2e/specs/FOUR-16958_watchers.spec.js: -------------------------------------------------------------------------------- 1 | describe("FOUR-16958_watchers ", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify that watcher clean data from the watcher works", () => { 7 | // Mock script response 8 | 9 | cy.intercept( 10 | "POST", 11 | "/api/1.0/scripts/execute/2272", 12 | JSON.stringify({ 13 | output: { 14 | "input1": "", 15 | "input2": "", 16 | "q1comment": "", 17 | "q2comment": "", 18 | "form_text_area_1": "" 19 | } 20 | }) 21 | ); 22 | cy.loadFromJson("FOUR-16958_watchers.json", 0); 23 | 24 | cy.get("[data-cy=mode-preview]").click(); 25 | 26 | cy.get('[data-cy="screen-field-nyc.q1comment"]').type("1"); 27 | cy.get('[data-cy="screen-field-nyc.form_text_area_1"]').type("2"); 28 | cy.get('[data-cy="screen-field-nyc.input1"]').type("3"); 29 | cy.get('[data-cy="screen-field-nyc.input2"]').type("4"); 30 | cy.get("[name=resetfields]").click(); 31 | cy.wait(800); 32 | cy.get('[data-cy="screen-field-nyc.input2"]').type("5"); 33 | cy.get('[data-cy="screen-field-nyc.q1comment"]').should('be.empty'); 34 | cy.get('[data-cy="screen-field-nyc.form_text_area_1"]').should('be.empty'); 35 | cy.get('[data-cy="screen-field-nyc.input1"]').should('be.empty'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/specs/FOUR-7027_RecordList_nested_calc.spec.js: -------------------------------------------------------------------------------- 1 | describe("screen error nested calc", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify calc can access to _parent inside Record List with nested screen", () => { 7 | // @link https://processmaker.atlassian.net/browse/FOUR-7027 8 | cy.loadFromJson("Screen parent in record list.json", 0); 9 | // set init screen test data 10 | cy.get("[data-cy=mode-preview]").click(); 11 | 12 | cy.get("[data-cy=preview-content] [name='parentInput']") 13 | .clear() 14 | .type("new value to parent"); 15 | 16 | // Click ADD record in Record List 17 | cy.get( 18 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=add-row]" 19 | ).click(); 20 | 21 | // Click OK button to insert the row 22 | cy.get( 23 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] button.btn-primary" 24 | ).click(); 25 | 26 | // Check the data of the screen 27 | cy.assertPreviewData({ 28 | result: "new value to parent", 29 | parentInput: "new value to parent", 30 | form_record_list_1: [ 31 | { 32 | result: "new value to parent" 33 | } 34 | ] 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/specs/FOUR-7029_Parent_in_default_value.spec.js: -------------------------------------------------------------------------------- 1 | describe("access to _parent variable in default value", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Test access to _parent variable in default value inside a nested inside a loop", () => { 7 | cy.loadFromJson("Screen _parent in default value.json", 0); 8 | // set init screen test data 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | cy.get("[data-cy=preview-content] [name='parentInput']") 12 | .clear() 13 | .type("new value to parent"); 14 | 15 | cy.assertPreviewData({ 16 | parentInput: "new value to parent", 17 | loop_1: [ 18 | { 19 | inputDefault: "new value to parent" 20 | } 21 | ] 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/e2e/specs/FOUR-7587_LoopNestedLoop.spec.js: -------------------------------------------------------------------------------- 1 | describe("FOUR-7587 Loop Nested Loop", () => { 2 | // initial data to test the screen 3 | const initialData = { 4 | businesses: [ 5 | { 6 | legalStructure: { 7 | id: 5, 8 | coreCode: "170", 9 | isActive: true, 10 | bizChexCode: "05", 11 | businessType: "Corporation", 12 | requiredDocs: [ 13 | { 14 | file: null, 15 | document: { 16 | id: 15, 17 | label: 18 | "Recent Statement of Information (Domestic Stock Corporation) filed with Secretary of State", 19 | coreCode: "recentStatementInfo" 20 | }, 21 | required: true 22 | }, 23 | { 24 | file: null, 25 | document: { 26 | id: 16, 27 | label: "Certificate of Status (if available)", 28 | coreCode: "statusCertificate" 29 | }, 30 | required: false 31 | }, 32 | { 33 | file: null, 34 | document: { 35 | id: 17, 36 | label: "Corporate Resolution", 37 | coreCode: "corporateResolution" 38 | }, 39 | required: true 40 | } 41 | ] 42 | } 43 | } 44 | ] 45 | }; 46 | 47 | beforeEach(() => { 48 | cy.visit("/", { 49 | onBeforeLoad(win) { 50 | cy.stub(win.console, "log").as("consoleLog"); 51 | cy.stub(win.console, "error").as("consoleError"); 52 | } 53 | }); 54 | }); 55 | 56 | it("Test render of screen with Loop>Nested>Loop", () => { 57 | cy.loadFromJson("FOUR-7587_LoopNestedLoop.json", 1); 58 | 59 | // set initial data to test the screen 60 | cy.setPreviewDataInput(initialData); 61 | cy.get("[data-cy=mode-preview]").click(); 62 | // Check no errors in console 63 | cy.get("@consoleError").should("not.to.be.called"); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/e2e/specs/FOUR6788_ScreenPerformanceTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("FOUR-6788 screen performance", () => { 2 | // @todo Improve the boot-time of the stand alone app (general and within cypress) 3 | const avgBootTime = 22000; 4 | const minimumPerformanceScore = 12; 5 | const accessibility = 50; 6 | 7 | // This test includes a Loop with 6 iterations, multi-column, select lists, rich texts and text areas 8 | it("Verify FOUR-6788 screen performance: select list, rich text", () => { 9 | const maximumScreenRenderTime = 6000; 10 | 11 | cy.loadFromJson("FOUR-6788_screen_performance.json"); 12 | cy.visit("/?scenario=RenderScreen"); 13 | const customThresholds = { 14 | performance: minimumPerformanceScore, 15 | accessibility, 16 | interactive: avgBootTime + maximumScreenRenderTime 17 | }; 18 | 19 | const desktopConfig = { 20 | formFactor: "desktop", 21 | screenEmulation: { disabled: true } 22 | }; 23 | 24 | cy.lighthouse(customThresholds, desktopConfig); 25 | }); 26 | 27 | // This test includes a Loop with 6 iterations, multi-column, select lists, rich texts, 28 | // text areas, input texts, validations rules, visibility rules and a submit button 29 | it("Verify FOUR-6788 screen performance: input text, validations, visibility rules", () => { 30 | const maximumScreenRenderTime = 6000; 31 | 32 | cy.loadFromJson("FOUR-6788_screen_performance_2.json"); 33 | cy.visit("/?scenario=RenderScreen2"); 34 | const customThresholds = { 35 | performance: minimumPerformanceScore, 36 | accessibility, 37 | interactive: avgBootTime + maximumScreenRenderTime 38 | }; 39 | 40 | const desktopConfig = { 41 | formFactor: "desktop", 42 | screenEmulation: { disabled: true } 43 | }; 44 | 45 | cy.lighthouse(customThresholds, desktopConfig); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/e2e/specs/FileUpload.spec.js: -------------------------------------------------------------------------------- 1 | describe("File Upload", () => { 2 | it("Automatically sets a variable name", () => { 3 | cy.visit("/"); 4 | cy.openAcordeon("collapse-6"); 5 | cy.get("[data-cy=controls-FileUpload]").drag("[data-cy=screen-drop-zone]", { 6 | position: "bottom" 7 | }); 8 | cy.get("[data-cy=screen-element-container]").click(); 9 | 10 | cy.get('[data-cy="screen-element-container"] .card-body').then((div) => { 11 | const data = div[0].__vue__.name; 12 | expect(data).to.eql("file_upload_1"); 13 | }); 14 | 15 | cy.get("[data-cy=mode-preview]").click(); 16 | cy.get("[data-cy=file-upload-button]").should("not.have.attr", "disabled"); 17 | }); 18 | 19 | it("Disables when task is self service", () => { 20 | cy.visit("/"); 21 | cy.openAcordeon("collapse-6"); 22 | cy.window().then((win) => { 23 | win.ProcessMaker.isSelfService = true; 24 | }); 25 | cy.get("[data-cy=controls-FileUpload]").drag("[data-cy=screen-drop-zone]", { 26 | position: "bottom" 27 | }); 28 | cy.get("[data-cy=mode-preview]").click(); 29 | cy.get("[data-cy=file-upload-button]").should("have.attr", "disabled"); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/e2e/specs/MediaQuery.spec.js: -------------------------------------------------------------------------------- 1 | const sizes = [ 2 | [700, 800], 3 | [1000, 800], 4 | [1200, 768] 5 | ]; 6 | const backgroundColor = ["rgb(0, 0, 255)", "rgb(255, 0, 0)", "rgb(0, 128, 0)"]; 7 | 8 | describe("Media Query CSS", () => { 9 | before(() => { 10 | // run these tests as if in a desktop 11 | cy.visit("/"); 12 | cy.openAcordeon("collapse-2"); 13 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 14 | position: "bottom" 15 | }); 16 | cy.get("[data-cy=screen-element-container]").click(); 17 | cy.get("[data-cy=accordion-Advanced]").click(); 18 | cy.get("[data-cy=inspector-customCssSelector]").type("new_input_css"); 19 | cy.get("[data-cy=topbar-css]").click(); 20 | cy.wait(500); 21 | // write the media query in the custom css panel 22 | cy.get("#custom-css").type( 23 | `@media (max-width: 480px) {{}{del} 24 | div[selector="new_input_css"] {{}{del} 25 | background-color: blue; 26 | } 27 | } 28 | 29 | @media (min-width: 481px) and (max-width: 810px) {{}{del} 30 | div[selector="new_input_css"] {{}{del} 31 | background-color: red; 32 | } 33 | } 34 | 35 | @media (min-width: 811px) {{}{del} 36 | div[selector="new_input_css"] {{}{del} 37 | background-color: green; 38 | } 39 | } 40 | `, 41 | { parseSpecialCharSequences: true } 42 | ); 43 | 44 | cy.wait(500); 45 | cy.get("[data-cy=save-button]").click(); 46 | // preview 47 | cy.get("[data-cy=mode-preview]").click(); 48 | }); 49 | 50 | sizes.forEach((size, index) => { 51 | // make assertions on the screen using 52 | // an array of different viewports 53 | it(`Should display screen on ${size} screen`, () => { 54 | if (Cypress._.isArray(size)) { 55 | cy.viewport(size[0], size[1]); 56 | } else { 57 | cy.viewport(size); 58 | } 59 | 60 | cy.wait(300); 61 | cy.get("[selector=new_input_css]") 62 | .should("have.css", "background-color") 63 | .and("eq", backgroundColor[index]); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/e2e/specs/MultiselectWithStringValue.spec.js: -------------------------------------------------------------------------------- 1 | describe("multiselect with string value", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Multiselect checkbox with string value without user interaction keep its value", () => { 7 | cy.loadFromJson("multiselect_with_string_value.json", 0); 8 | 9 | // init screen data 10 | cy.setPreviewDataInput({ person: "foo" }); 11 | cy.get("[data-cy=mode-preview]").click(); 12 | 13 | // Check the data of the screen 14 | cy.assertPreviewData({ 15 | person: "foo" 16 | }); 17 | }); 18 | 19 | it("Multiselect checkbox with string value after user interaction change to array", () => { 20 | cy.loadFromJson("multiselect_with_string_value.json", 0); 21 | 22 | // init screen data 23 | cy.setPreviewDataInput({ person: "foo" }); 24 | cy.get("[data-cy=mode-preview]").click(); 25 | 26 | cy.get("[data-cy=preview-content] [name=person]").eq(0).click(); 27 | cy.get("[data-cy=preview-content] [name=person]").eq(1).click(); 28 | cy.get("[data-cy=preview-content] [name=person]").eq(0).click(); 29 | 30 | // Check the data of the screen 31 | cy.assertPreviewData({ 32 | person: [ 33 | { 34 | content: "two", 35 | value: "two" 36 | } 37 | ] 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/e2e/specs/NestedCalcProperties.spec.js: -------------------------------------------------------------------------------- 1 | describe("nested calculated properties", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | // @link: https://processmaker.atlassian.net/browse/FOUR-4873 7 | it("Verify nested calculated properties", () => { 8 | cy.loadFromJson("nested_calc_properties.json", 1); 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | cy.get('[data-cy=preview] input[name="varA"]').type(10); 12 | cy.get('[data-cy=preview] input[name="varB"]').type(5); 13 | cy.get( 14 | '[data-cy=preview-content] [data-cy="screen-field-operation"]' 15 | ).selectOption("Addition"); 16 | 17 | // Wait until you load the screen 18 | cy.wait(500); 19 | 20 | // Check the data of the screen 21 | cy.assertPreviewData({ 22 | equal: "=", 23 | operation: 1, 24 | varA: 10, 25 | varB: 5, 26 | varR: 15 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/specs/NestedCalcRadioFreeze.spec.js: -------------------------------------------------------------------------------- 1 | describe("nested calc radio freeze", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | // @link: https://processmaker.atlassian.net/browse/FOUR-4873 7 | it("Verify nested calc radio do not freeze", () => { 8 | cy.loadFromJson("nested_calc_radio_freeze.json", 0); 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | cy.get("[data-cy=preview-content] button:contains(Continue)").click(); 12 | // Wait until you load the screen 13 | cy.wait(500); 14 | 15 | // Check the data of the screen 16 | cy.assertPreviewData({ 17 | jointOwner: [ 18 | { 19 | varb: null, 20 | form_input_1: null, 21 | selectLIst1: null, 22 | form_select_list_1: [], 23 | selectIsArray: true 24 | } 25 | ] 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/e2e/specs/Pages.spec.js: -------------------------------------------------------------------------------- 1 | describe("Pages and navigations", () => { 2 | it("Basic default value", () => { 3 | cy.visit("/"); 4 | cy.openAllAcordeon(); 5 | cy.get("[data-test=page-dropdown]").click(); 6 | cy.get("[data-test=add-page]").click({ force: true }); 7 | // Define Page 2 8 | cy.get("[data-cy=add-page-name]").clear().type("Page 2"); 9 | cy.get("[data-cy=add-page-modal] button.btn").eq(1).click(); 10 | cy.wait(500); 11 | cy.get('[data-cy=controls-FormButton]:contains("Page")').drag( 12 | "[data-cy=screen-drop-zone]", 13 | { position: "bottom" } 14 | ); 15 | cy.get("[data-cy=screen-element-container]").click(); 16 | cy.get("[data-cy=inspector-label]").clear().type("Go to Page 1"); 17 | cy.get("[data-cy=accordion-Configuration]").click(); 18 | cy.setMultiselect("[data-cy=inspector-eventData]", "Default"); 19 | // Define Page 1 20 | cy.get("[data-test=page-dropdown]").click(); 21 | cy.get("[data-cy=page-0]").click({ force: true }); 22 | cy.wait(500); 23 | cy.get('[data-cy=controls-FormButton]:contains("Page")').drag( 24 | "[data-cy=screen-drop-zone]", 25 | { position: "bottom" } 26 | ); 27 | cy.get("[data-cy=screen-element-container]").click(); 28 | cy.get("[data-cy=inspector-label]").clear().type("Go to Page 2"); 29 | cy.get("[data-cy=accordion-Configuration]").click(); 30 | cy.setMultiselect("[data-cy=inspector-eventData]", "Page 2"); 31 | // Preview 32 | cy.get("[data-cy=mode-preview]").click(); 33 | cy.get("[data-cy=preview-content]").should("contain.text", "Go to Page 2"); 34 | cy.get("[data-cy=preview-content] button").click(); 35 | cy.get("[data-cy=preview-content]").should("contain.text", "Go to Page 1"); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/specs/ParentAccessTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Test access to _parent", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify calc can access to _parent inside Record List with nested screen and Loops", () => { 7 | cy.loadFromJson("ParentNestedScreen.json", 1); 8 | // set init screen test data 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | cy.get("[data-cy=preview-content] [name='parentInput']") 12 | .eq(0) 13 | .clear() 14 | .type("value in parent") 15 | .blur(); 16 | 17 | cy.get("[data-cy=preview-content] [name='form_input_1']") 18 | .eq(0) 19 | .type(":value in loop"); 20 | 21 | // Click ADD record in Record List 22 | cy.get( 23 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=add-row]" 24 | ).click(); 25 | 26 | // Type in form_input_1 inside record list 27 | cy.get( 28 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_1]" 29 | ) 30 | .type(":value in record list + loop") 31 | .blur(); 32 | 33 | // Click OK button to insert the row 34 | cy.get( 35 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] button.btn-primary" 36 | ).click(); 37 | 38 | // Check the data of the screen 39 | cy.assertPreviewData({ 40 | parentInput: "value in parent", 41 | loop_1: [ 42 | { 43 | parentInput: "value in parent", 44 | loop_1: [ 45 | { 46 | form_input_1: "value in parent:value in loop" 47 | } 48 | ] 49 | } 50 | ], 51 | form_record_list_1: [ 52 | { 53 | parentInput: "value in parent", 54 | loop_1: [ 55 | { 56 | form_input_1: "value in parent:value in record list + loop" 57 | } 58 | ] 59 | } 60 | ] 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/e2e/specs/ParentVariable.spec.js: -------------------------------------------------------------------------------- 1 | describe("_parent variable", () => { 2 | beforeEach(() => {}); 3 | 4 | it("Test _parent in loop>nested", () => { 5 | cy.visit("/"); 6 | cy.loadFromJson("parent_variable.json", 1); 7 | cy.get("[data-cy=mode-preview]").click(); 8 | 9 | cy.get( 10 | '[data-cy=preview-content] [data-cy="screen-field-select_padre"]' 11 | ).selectOption("aaaa"); 12 | 13 | cy.get( 14 | '[data-cy=preview-content] [data-cy="screen-field-select_hijo"]' 15 | ).selectOption("aaaa"); 16 | cy.get('[data-cy=preview-content] [data-cy="screen-field-input_hijo"]') 17 | .clear() 18 | .type("test"); 19 | 20 | cy.assertPreviewData({ 21 | select_padre: [ 22 | { 23 | value: "a", 24 | content: "aaaa" 25 | } 26 | ], 27 | loop_1: [ 28 | { 29 | input_hijo: "test", 30 | select_hijo: "a" 31 | } 32 | ], 33 | form_record_list_1: null 34 | }); 35 | }); 36 | 37 | it("Test _parent in record list", () => { 38 | cy.visit("/"); 39 | cy.loadFromJson("parent_variable.json", 1); 40 | cy.get("[data-cy=mode-preview]").click(); 41 | 42 | cy.get( 43 | '[data-cy=preview-content] [data-cy="screen-field-select_padre"]' 44 | ).selectOption("aaaa"); 45 | 46 | cy.get( 47 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=add-row]" 48 | ).click(); 49 | cy.get( 50 | '[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy="screen-field-form_select_list_1"]' 51 | ).selectOption("aaaa"); 52 | cy.get( 53 | "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] button.btn-primary" 54 | ).click(); 55 | 56 | cy.assertPreviewData({ 57 | select_padre: [ 58 | { 59 | value: "a", 60 | content: "aaaa" 61 | } 62 | ], 63 | loop_1: [ 64 | { 65 | input_hijo: "", 66 | select_hijo: null 67 | } 68 | ], 69 | form_record_list_1: [ 70 | { 71 | form_select_list_1: "a" 72 | } 73 | ] 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/e2e/specs/RAOS1.0.0AccountOpeningCustomerForm.spec.js: -------------------------------------------------------------------------------- 1 | describe("RAOS 1.0.0 Account Opening Customer Form", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify Calcs with visibility rules update immediately on the same screen", () => { 7 | cy.loadFromJson("RAOS1.0.0AccountOpeningCustomerForm.json", 1); 8 | 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | // add a new account 12 | cy.get( 13 | "[data-cy=preview-content] [data-cy='loop-additionalAccounts-add']" 14 | ).click(); 15 | // select the first account type: "Checking Account" 16 | cy.get( 17 | "[data-cy=preview-content] [data-cy='screen-field-productType']" 18 | ).selectOption("Checking Account"); 19 | // click on continue 20 | cy.get("[data-cy=preview-content] [aria-label='Continue']").eq(0).click(); 21 | // choose funding method 22 | cy.get("[data-cy=preview-content] [data-cy='screen-field-fundingMethod']") 23 | .eq(0) 24 | .click(); 25 | // "initialDepositOther" field must be visible 26 | cy.get("[data-cy=preview-content] [name='initialDepositOther']") 27 | .eq(0) 28 | .should("be.visible"); 29 | 30 | // check that ONLY requireInitialDepositOther property was initialized in accounts variable 31 | cy.assertPreviewData({ 32 | accounts: [ 33 | { 34 | product: "Checking", 35 | initialDepositOther: 0, 36 | requireInitialDepositOther: "Yes" 37 | } 38 | ], 39 | showFundingAccountLoop: true, 40 | additionalAccounts: [ 41 | { 42 | productType: "Checking" 43 | } 44 | ], 45 | fundingMethod: "Transfer from Existing Quaint Oak Account", 46 | externalBank: "", 47 | externalAccountType: null, 48 | externalAccountName: "", 49 | externalRoutingNumber: "", 50 | externalAccountNumber: "", 51 | internalAccountType: null, 52 | internalAccountNumber: "" 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/e2e/specs/RichText.spec.js: -------------------------------------------------------------------------------- 1 | describe("Rich Text control", () => { 2 | it("Rich text with mustache", () => { 3 | cy.visit("/"); 4 | cy.openAcordeon("collapse-3"); 5 | cy.get("[data-cy=controls-FormHtmlViewer]").drag( 6 | "[data-cy=screen-drop-zone]", 7 | { position: "bottom" } 8 | ); 9 | cy.get("[data-cy=screen-element-container]").click(); 10 | cy.get("[data-cy=inspector-content]") 11 | .focus() 12 | .clear() 13 | .type("

Hello {{ name }}

", { parseSpecialCharSequences: false }); 14 | cy.get("[data-cy=mode-preview]").click(); 15 | cy.setPreviewDataInput('{"name":"World"}'); 16 | cy.get("[data-cy=preview-content]").should( 17 | "contain.html", 18 | "

Hello World

" 19 | ); 20 | }); 21 | 22 | it("Rich text render HTML from a Variable", () => { 23 | cy.visit("/"); 24 | cy.openAcordeon("collapse-3"); 25 | cy.get("[data-cy=controls-FormHtmlViewer]").drag( 26 | "[data-cy=screen-drop-zone]", 27 | { position: "bottom" } 28 | ); 29 | cy.get("[data-cy=screen-element-container]").click(); 30 | cy.get("[data-cy=inspector-content]") 31 | .focus() 32 | .clear() 33 | .type("{{ name }}", { parseSpecialCharSequences: false }); 34 | cy.get("[data-cy=inspector-renderVarHtml]").click(); 35 | cy.get("[data-cy=mode-preview]").click(); 36 | cy.setPreviewDataInput('{"name":"

Hello World

"}'); 37 | cy.get("[data-cy=preview-content]").should( 38 | "contain.html", 39 | "

Hello World

" 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/e2e/specs/ScreenBuilder.spec.js: -------------------------------------------------------------------------------- 1 | describe("ScreenBuilder", () => { 2 | it("Visits the app root url and renders form builder", () => { 3 | cy.visit("/"); 4 | cy.contains("Place your controls here.").should("be.visible"); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/e2e/specs/ScreenWarnings.spec.js: -------------------------------------------------------------------------------- 1 | describe("Screen Warnings", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Large screen warning", () => { 7 | cy.loadFromJson("large_screen_warning.json", 1); 8 | 9 | cy.get('[data-cy="open-console"] .badge-warning').should( 10 | "contain.text", 11 | "1" 12 | ); 13 | cy.get('[data-cy="open-console"]').click(); 14 | cy.get('[data-cy="validation-panel"]').should( 15 | "contain.text", 16 | "We recommend using fewer than 25 form elements in your screen for optimal performance." 17 | ); 18 | }); 19 | 20 | it("Use script warning", () => { 21 | cy.openAcordeon("collapse-2"); 22 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 23 | position: "bottom" 24 | }); 25 | cy.get('[data-cy="topbar-watchers"]').click(); 26 | cy.get('[data-cy="watchers-add-watcher"]').click(); 27 | cy.get('[data-cy="watchers-watcher-name"]').type("test"); 28 | cy.get('[data-cy="watchers-watcher-variable"]').selectOption( 29 | "form_input_1" 30 | ); 31 | cy.get('[data-cy="watchers-accordion-source"]').click(); 32 | cy.get("#watcherSource").should( 33 | "not.contain.text", 34 | "Using watchers with Scripts can slow the performance of your screen" 35 | ); 36 | cy.get('[data-cy="watchers-watcher-source"]').selectOption("Test Script"); 37 | cy.get("#watcherSource").should( 38 | "contain.text", 39 | "Using watchers with Scripts can slow the performance of your screen" 40 | ); 41 | cy.get('[data-cy="watchers-button-save"]').click(); 42 | cy.get('[data-cy="watchers-modal"] .close').click(); 43 | cy.get('[data-cy="open-console"] .badge-warning').should( 44 | "contain.text", 45 | "1" 46 | ); 47 | cy.get('[data-cy="open-console"]').click(); 48 | cy.get('[data-cy="validation-panel"]').should( 49 | "contain.text", 50 | "Using watchers with Scripts can slow the performance of your screen." 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/e2e/specs/SelectListDefaultValues.spec.js: -------------------------------------------------------------------------------- 1 | describe("Select List default Value", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Verify default value not existing in option list set value to null", () => { 7 | cy.loadFromJson("select_list_default_value.json", 0); 8 | 9 | // Configure a non existing default value in option list 10 | cy.get("[data-cy=screen-element-container]").click(); 11 | cy.get("[data-cy=accordion-Advanced]").click(); 12 | cy.get("[data-cy=inspector-defaultValue-basicValue]").clear().type("c"); 13 | 14 | cy.get("[data-cy=mode-preview]").click(); 15 | 16 | // Assert value was configured in null 17 | cy.assertPreviewData({ 18 | form_select_list_1: null 19 | }); 20 | 21 | // Select a valid option 22 | cy.get('[data-cy="screen-field-form_select_list_1"]').selectOption("a"); 23 | 24 | // Assert value is set correctly 25 | cy.assertPreviewData({ 26 | form_select_list_1: "a" 27 | }); 28 | 29 | cy.get("[data-cy=mode-editor]").click(); 30 | 31 | // Configure an existing default value in option list 32 | cy.get("[data-cy=screen-element-container]").click(); 33 | cy.get("[data-cy=accordion-Advanced]").click(); 34 | cy.get("[data-cy=inspector-defaultValue-basicValue]").clear().type("b"); 35 | 36 | cy.get("[data-cy=mode-preview]").click(); 37 | 38 | // Assert default valid value is set correctly 39 | cy.assertPreviewData({ 40 | form_select_list_1: "b" 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/e2e/specs/SingleSelectWithInvalidValue.spec.js: -------------------------------------------------------------------------------- 1 | describe("single select with invalid initial value", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("Single select radio with array value without user interaction keep its value", () => { 7 | cy.loadFromJson("single_select_with_invalid_value.json", 0); 8 | 9 | // init screen data 10 | cy.setPreviewDataInput({ person: [] }); 11 | cy.get("[data-cy=mode-preview]").click(); 12 | 13 | // Check the data of the screen 14 | cy.assertPreviewData({ 15 | person: [] 16 | }); 17 | }); 18 | 19 | it("Single select radio with array value after user interaction change to the selected option", () => { 20 | cy.loadFromJson("single_select_with_invalid_value.json", 0); 21 | 22 | // init screen data 23 | cy.setPreviewDataInput({ person: [] }); 24 | cy.get("[data-cy=mode-preview]").click(); 25 | 26 | cy.get("[data-cy=preview-content] [name=person]").eq(0).click(); 27 | cy.get("[data-cy=preview-content] [name=person]").eq(1).click(); 28 | cy.get("[data-cy=preview-content] [name=person]").eq(0).click(); 29 | 30 | // Check the data of the screen 31 | cy.assertPreviewData({ 32 | person: { 33 | content: "one", 34 | value: "one" 35 | } 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/e2e/specs/TCP4-4444VerifyAddingMultipleContentToClipboard.spec.js: -------------------------------------------------------------------------------- 1 | describe("Verify Adding Multiple Content To The Clipboard", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | cy.showValidationOnLoad(); 5 | cy.clearLocalStorage(); 6 | }); 7 | 8 | it("TCP4-4446: Content selected should remain flagged as selected for pasting", () => { 9 | // Step 1: Load the initial JSON data and check screen content 10 | cy.loadFromJson("TCP4-4446.json", 0); 11 | cy.get("[data-cy=screen-drop-zone]").should("not.contain.text", "Place your controls here."); 12 | 13 | cy.get(':nth-child(1) > [data-cy="screen-element-container"]').click({ force: true }); 14 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click(); 15 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 16 | 17 | // Step 2: Load a new screen and repeat the process for adding to clipboard 18 | cy.loadFromJson("screen2TCP4-4446.json", 0); 19 | cy.wait(100); 20 | cy.get(':nth-child(1) > [data-cy="screen-element-container"]').click({ force: true }); 21 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click(); 22 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 23 | 24 | // Step 3: Reload the first screen and verify that the element is flagged as selected 25 | cy.loadFromJson("TCP4-4446.json", 0); 26 | cy.wait(100); 27 | cy.get(':nth-child(1) > [data-cy="screen-element-container"]').click({ force: true }); 28 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 29 | 30 | // Step 4: Reload the second screen and verify clipboard status remains unchanged 31 | cy.loadFromJson("screen2TCP4-4446.json", 0); 32 | cy.wait(100); 33 | cy.get(':nth-child(1) > [data-cy="screen-element-container"]').click({ force: true }); 34 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/e2e/specs/TCP4-4452VerifyReloadPageClipboard.spec.js: -------------------------------------------------------------------------------- 1 | describe("TCP4-4452 Verify reload page, remove button still present in the copied elements", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | cy.showValidationOnLoad(); 5 | cy.clearLocalStorage(); 6 | }); 7 | 8 | it("Verify that after reloading the page, all controls within each page are displayed with (-) button", () => { 9 | // Step 1: Load the initial JSON data and check screen content 10 | cy.loadFromJson("TCP4-4452.json", 0); 11 | cy.get(':nth-child(1) > :nth-child(1) > :nth-child(1) > .m-2 > .form-group').click({ force: true }); 12 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click(); 13 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 14 | 15 | cy.get(':nth-child(1) > :nth-child(2) > :nth-child(1) > .m-2').click({ force: true }); 16 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click(); 17 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 18 | 19 | 20 | cy.get('#form_record_list_1 > .m-2').click({ force: true }); 21 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click({ force: true }); 22 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 23 | // reload page 24 | cy.loadFromJson("TCP4-4452.json", 0); 25 | cy.get(':nth-child(1) > :nth-child(1) > :nth-child(1) > .m-2 > .form-group').click({ force: true }); 26 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 27 | 28 | cy.get(':nth-child(1) > :nth-child(2) > :nth-child(1) > .m-2').click({ force: true }); 29 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 30 | 31 | cy.get('#form_record_list_1 > .m-2').click({ force: true }); 32 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/e2e/specs/TCP4-4458ClearClipboard.spec.js: -------------------------------------------------------------------------------- 1 | describe("TCP4-4458 Verify clear all components in clipboard", () => { 2 | beforeEach(() => { 3 | // Step 1: Navigate to the homepage, show validation, and clear local storage 4 | cy.visit("/"); 5 | cy.showValidationOnLoad(); 6 | cy.clearLocalStorage(); 7 | }); 8 | 9 | it("Verify that after clicking Clear All, all controls are deleted.", () => { 10 | // Step 2: Load the JSON data (TCP4-4454) 11 | cy.loadFromJson("TCP4-4454.json", 0); 12 | 13 | // Step 3: Function to add a screen element to the clipboard 14 | const addToClipboard = (index) => { 15 | // Select screen element based on index, click it, then click the add to clipboard button 16 | cy.get(`:nth-child(${index}) > [data-cy="screen-element-container"]`).click({ force: true }); 17 | cy.get('[data-cy="addToClipboard"]').should("be.visible").click(); 18 | cy.get('[data-cy="addToClipboard"]').should("not.exist"); 19 | }; 20 | 21 | // Step 4: Loop through elements (1-11) and add each to the clipboard 22 | for (let i = 1; i <= 11; i++) { 23 | addToClipboard(i); 24 | } 25 | 26 | // Step 5: Open the clipboard menu 27 | cy.get("[data-test=page-dropdown]").click(); 28 | cy.get("[data-test=clipboard]").should("exist").click({ force: true }); 29 | 30 | // Step 6: Verify that the Clipboard tab is active 31 | cy.get('a[role="tab"]') 32 | .contains('Clipboard') 33 | .should('have.class', 'active') 34 | .and('be.visible'); 35 | 36 | // Step 7: Click 'Clear All' button in the clipboard 37 | cy.get('[data-test="tab-content"] > .btn-link').click(); 38 | 39 | // Step 8: Confirm the action to clear the clipboard 40 | cy.contains('button', 'Confirm').click(); 41 | 42 | // Step 9: Verify that all clipboard components have been deleted 43 | cy.get('[data-cy="editor-content"]').children().should('have.length', 0); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/e2e/specs/TCP4-4462VerifyAddComponentsToClipboard.spec.js: -------------------------------------------------------------------------------- 1 | describe("TCP4-4462 Verify clear all components in clipboard", () => { 2 | beforeEach(() => { 3 | // Step 1: Navigate to the homepage, show validation, and clear local storage 4 | cy.visit("/"); 5 | cy.showValidationOnLoad(); 6 | cy.clearLocalStorage(); 7 | }); 8 | 9 | it("Verify that after clicking Clear All, all controls are deleted.", () => { 10 | 11 | 12 | cy.get("[data-test=page-dropdown]").click(); 13 | cy.get("[data-test=clipboard]").should("exist").click({ force: true }); 14 | 15 | cy.get('a[role="tab"]') 16 | .contains('Clipboard') 17 | .should('have.class', 'active') 18 | .and('be.visible'); 19 | 20 | cy.get("[data-cy=controls-FormSelectList]").drag("[data-cy=editor-content]", { position: "top", force: true }); 21 | cy.get("[data-cy=controls-FormButton]").drag("[data-cy=screen-element-container]", { position: "top", force: true }); 22 | cy.get("[data-cy=controls-FormTextArea]").drag("[data-cy=screen-element-container]", { position: "top", force: true }); 23 | cy.get("[data-cy=controls-FormDatePicker]").drag("[data-cy=screen-element-container]", { position: "top", force: true }); 24 | cy.get("[data-cy=controls-FormCheckbox]").drag("[data-cy=screen-element-container]", { position: "top", force: true }); 25 | 26 | cy.get('[data-cy="screen-element-container"]') 27 | .children() 28 | .should('have.length', 5); 29 | cy.visit("/"); 30 | cy.openAcordeonByLabel("Content Fields"); 31 | 32 | cy.get("[data-cy=controls-FormHtmlViewer]").drag("[data-cy=screen-drop-zone]", { position: "bottom" }); 33 | cy.get("[data-cy=controls-FormImage]").drag("[data-cy=screen-element-container]", { position: "top" }); 34 | cy.get("[data-cy=controls-FormRecordList]").drag("[data-cy=screen-element-container]", { position: "top" }); 35 | cy.get("[data-cy=controls-FormLoop]").drag("[data-cy=screen-element-container]", { position: "top" }); 36 | cy.get("[data-cy=controls-FormNestedScreen]").drag("[data-cy=screen-element-container]", { position: "top" }); 37 | cy.get("[data-cy=controls-FormMultiColumn]").drag("[data-cy=screen-element-container]", { position: "top" }); 38 | cy.get('[data-cy="screen-element-container"]') 39 | .children() 40 | .should('have.length', 6); 41 | 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/e2e/specs/TestValidationWithNestedSideEffects.spec.js: -------------------------------------------------------------------------------- 1 | describe("test validation with nested side effects", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | cy.showValidationOnLoad(); 5 | }); 6 | 7 | it("Verify test validation with nested side effects", () => { 8 | cy.loadFromJson("Test Validation with Nested side effects.json", 1); 9 | // click on preview 10 | cy.get("[data-cy=mode-preview]").click(); 11 | // click on New Submit button 12 | cy.get("[data-cy=preview-content] button[aria-label='Submit']").click(); 13 | // In editor: ensure standard required field displays error while readonly required field does not 14 | cy.get("[data-cy=preview-content] [name='form_input_2']") 15 | .parent() 16 | .find(".invalid-feedback") 17 | .should("be.visible"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/e2e/specs/UndoRedo.spec.js: -------------------------------------------------------------------------------- 1 | describe("Undo and Redo", () => { 2 | it("Can Undo", () => { 3 | cy.visit("/"); 4 | cy.openAcordeon("collapse-2"); 5 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 6 | position: "bottom" 7 | }); 8 | cy.get("[data-cy=toolbar-undo]").click(); 9 | cy.get("[data-cy=screen-drop-zone]").should( 10 | "contain.text", 11 | "Place your controls here." 12 | ); 13 | }); 14 | 15 | it("Can Redo", () => { 16 | cy.visit("/"); 17 | cy.openAcordeon("collapse-2"); 18 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 19 | position: "bottom" 20 | }); 21 | cy.get("[data-cy=toolbar-undo]").click(); 22 | cy.get("[data-cy=toolbar-redo]").click(); 23 | // Check that New Input control was restored 24 | cy.get("[data-cy=screen-element-container]").should( 25 | "contain.text", 26 | "New Input" 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/specs/ValidationRulesNameObject.js: -------------------------------------------------------------------------------- 1 | describe("Validation Rules", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | cy.showValidationOnLoad(); 5 | }); 6 | 7 | it("Variable name object and rules", () => { 8 | cy.loadFromJson("validation_rules_name_object.json", 0); 9 | cy.get("[data-cy=mode-preview]").click(); 10 | 11 | // ACCEPTED INPUT 12 | cy.get( 13 | '[data-cy=preview-content] [data-cy="screen-field-form_input_2.res"]' 14 | ) 15 | .parent() 16 | .should("contain.text", "Field is required"); 17 | cy.get("[data-cy=preview-content] [name=form_input_1]").type("12"); 18 | cy.get( 19 | '[data-cy=preview-content] [data-cy="screen-field-form_input_2.res"]' 20 | ) 21 | .parent() 22 | .should("not.contain.text", "Field is required"); 23 | 24 | // ACCEPTED INPUT 25 | cy.get('[data-cy=preview-content] [data-cy="screen-field-form_input_21"]') 26 | .parent() 27 | .should("contain.text", "Field is required"); 28 | cy.get("[data-cy=preview-content] [name=form_input_11]").type("13"); 29 | cy.get('[data-cy=preview-content] [data-cy="screen-field-form_input_21"]') 30 | .parent() 31 | .should("not.contain.text", "Field is required"); 32 | 33 | // REJECTED INPUT 34 | cy.get( 35 | '[data-cy=preview-content] [data-cy="screen-field-form_input_4.res"]' 36 | ) 37 | .parent() 38 | .should("not.contain.text", "Must be same as form_input_3"); 39 | cy.get("[data-cy=preview-content] [name=form_input_3]").type("13"); 40 | cy.get( 41 | '[data-cy=preview-content] [data-cy="screen-field-form_input_4.res"]' 42 | ) 43 | .parent() 44 | .should("contain.text", "Must be same as form_input_3"); 45 | 46 | // REJECTED INPUT 47 | cy.get( 48 | '[data-cy=preview-content] [data-cy="screen-field-form_input_6.res"]' 49 | ) 50 | .parent() 51 | .should("not.contain.text", "Field is required"); 52 | cy.get("[data-cy=preview-content] [name=form_input_5]").type("14"); 53 | cy.get( 54 | '[data-cy=preview-content] [data-cy="screen-field-form_input_6.res"]' 55 | ) 56 | .parent() 57 | .should("contain.text", "Field is required"); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/e2e/specs/ValidationShownOnSubmit.spec.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | describe("Validation Rules", () => { 4 | beforeEach(() => { 5 | cy.visit("/"); 6 | }); 7 | 8 | it("Invalid default values", () => { 9 | cy.loadFromJson("validation_rules.json", 0); 10 | cy.get("[data-cy=mode-preview]").click(); 11 | 12 | cy.shouldHaveValidationErrors("screen-field-form_checkbox_1"); 13 | cy.shouldHaveValidationErrors("screen-field-form_input_1"); 14 | cy.shouldHaveValidationErrors("screen-field-form_input_2"); 15 | cy.shouldNotHaveValidationErrors("screen-field-form_input_3"); 16 | cy.shouldHaveValidationErrors("screen-field-form_input_4"); 17 | cy.shouldNotHaveValidationErrors("screen-field-form_input_5"); 18 | cy.shouldNotHaveValidationErrors("screen-field-form_input_6"); 19 | 20 | cy.get("[data-cy=preview-content] .page button").click(); 21 | 22 | cy.shouldHaveValidationErrors("screen-field-form_input_1"); 23 | 24 | cy.get('[data-cy=preview-content] [data-cy="screen-field-form_input_1"]') 25 | .clear() 26 | .type("on"); 27 | 28 | cy.shouldHaveValidationErrors("screen-field-form_checkbox_1"); 29 | cy.shouldNotHaveValidationErrors("screen-field-form_input_1"); 30 | cy.shouldHaveValidationErrors("screen-field-form_input_2"); 31 | cy.shouldNotHaveValidationErrors("screen-field-form_input_3"); 32 | cy.shouldHaveValidationErrors("screen-field-form_input_4"); 33 | cy.shouldNotHaveValidationErrors("screen-field-form_input_5"); 34 | cy.shouldNotHaveValidationErrors("screen-field-form_input_6"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/e2e/specs/VariableNames.js: -------------------------------------------------------------------------------- 1 | describe("Default values", () => { 2 | it("Variable names with dots 2 levels", () => { 3 | cy.visit("/"); 4 | cy.openAcordeon("collapse-2"); 5 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 6 | position: "bottom" 7 | }); 8 | cy.get("[data-cy=screen-element-container]").click(); 9 | cy.get("[data-cy=inspector-name]").clear().type("user.address"); 10 | cy.get("[data-cy=mode-preview]").click(); 11 | cy.assertPreviewData({ 12 | user: { 13 | address: "" 14 | } 15 | }); 16 | }); 17 | it("Variable names with dots 3 levels", () => { 18 | cy.visit("/"); 19 | cy.openAcordeon("collapse-2"); 20 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 21 | position: "bottom" 22 | }); 23 | cy.get("[data-cy=screen-element-container]").click(); 24 | cy.get("[data-cy=inspector-name]").clear().type("user.address.city"); 25 | cy.get("[data-cy=mode-preview]").click(); 26 | cy.get('[data-cy=preview-content] [name="user.address.city"]').type( 27 | "La Paz" 28 | ); 29 | cy.assertPreviewData({ 30 | user: { 31 | address: { 32 | city: "La Paz" 33 | } 34 | } 35 | }); 36 | }); 37 | it("Variable names with dots and one attribute same as the name", () => { 38 | cy.visit("/"); 39 | cy.openAcordeon("collapse-2"); 40 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 41 | position: "bottom" 42 | }); 43 | cy.get("[data-cy=screen-element-container]").click(); 44 | cy.get("[data-cy=inspector-name]").clear().type("address.address.city"); 45 | cy.get("[data-cy=mode-preview]").click(); 46 | cy.get('[data-cy=preview-content] [name="address.address.city"]').type( 47 | "La Paz" 48 | ); 49 | cy.assertPreviewData({ 50 | address: { 51 | address: { 52 | city: "La Paz" 53 | } 54 | } 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/e2e/specs/VisibilityRule.spec.js: -------------------------------------------------------------------------------- 1 | describe("Default values", () => { 2 | it("Check visible", () => { 3 | cy.visit("/"); 4 | cy.setPreviewDataInput({ name: "world" }); 5 | cy.openAcordeon("collapse-2"); 6 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 7 | position: "bottom" 8 | }); 9 | cy.get("[data-cy=screen-element-container]").click(); 10 | cy.get("[data-cy=accordion-Advanced]").click(); 11 | cy.get("[data-cy=inspector-conditionalHide]").clear().type("name"); 12 | cy.get("[data-cy=mode-preview]").click(); 13 | cy.get("[data-cy=preview-content] [name=form_input_1]").should( 14 | "be.visible" 15 | ); 16 | }); 17 | it("Check hidden", () => { 18 | cy.visit("/"); 19 | cy.setPreviewDataInput({ name: "" }); 20 | cy.openAcordeon("collapse-2"); 21 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 22 | position: "bottom" 23 | }); 24 | cy.get("[data-cy=screen-element-container]").click(); 25 | cy.get("[data-cy=accordion-Advanced]").click(); 26 | cy.get("[data-cy=inspector-conditionalHide]").clear().type("name"); 27 | cy.get("[data-cy=mode-preview]").click(); 28 | cy.get("[data-cy=preview-content] [name=form_input_1]").should( 29 | "not.be.visible" 30 | ); 31 | }); 32 | it("Check dynamic visibility rule", () => { 33 | cy.visit("/"); 34 | cy.setPreviewDataInput({ name: "" }); 35 | cy.openAcordeon("collapse-2"); 36 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 37 | position: "bottom" 38 | }); 39 | // Add a second input field 40 | cy.get("[data-cy=controls-FormInput]").drag( 41 | "[data-cy=screen-element-container]", 42 | { position: "bottom" } 43 | ); 44 | cy.get("[data-cy=screen-element-container]").first().click(); 45 | cy.get("[data-cy=accordion-Advanced]").click(); 46 | cy.get("[data-cy=inspector-conditionalHide]").clear().type("form_input_2"); 47 | cy.get("[data-cy=mode-preview]").click(); 48 | cy.get("[data-cy=preview-content] [name=form_input_1]").should( 49 | "not.be.visible" 50 | ); 51 | cy.get("[data-cy=preview-content] [name=form_input_2]") 52 | .clear() 53 | .type("show next"); 54 | cy.get("[data-cy=preview-content] [name=form_input_1]").should( 55 | "be.visible" 56 | ); 57 | cy.get("[data-cy=preview-content] [name=form_input_1]") 58 | .clear() 59 | .type("visible"); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/e2e/specs/WebEntryEndEventRedirect.spec.js: -------------------------------------------------------------------------------- 1 | import SingleScreen from "../fixtures/single_line_input.json"; 2 | 3 | function initializeTaskAndScreenIntercepts(method, url, response) { 4 | cy.intercept(method, url.replace(",screen,", ",").replace(",nested,", ","), response).as('getTask'); 5 | cy.intercept(method, url.replace(/\?.*$/, "/screen?include=screen,nested"), response.screen).as('getScreen'); 6 | } 7 | 8 | describe("End Event Redirect (Process completed)", () => { 9 | beforeEach(() => { 10 | // Reset the intercepts and the state before each test 11 | cy.visit("/?scenario=TaskWebEntry", {}); 12 | }); 13 | 14 | it("Element destination type is summaryScreen and process was opened from webEntry", () => { 15 | const taskUrl = "http://localhost:5173/api/1.1/tasks/1?include=data,user,draft,requestor,processRequest,component,screen,requestData,loopContext,bpmnTagName,interstitial,definition,nested,userRequestPermission,elementDestination"; 16 | 17 | initializeTaskAndScreenIntercepts("GET", taskUrl, { 18 | id: 1, 19 | advanceStatus: "open", 20 | component: "task-screen", 21 | allow_interstitial: false, 22 | elementDestination: { 23 | type: "taskList", 24 | value: "http://localhost:5173/tasks", 25 | }, 26 | user: { 27 | id: 1, 28 | }, 29 | screen: SingleScreen.screens[0], 30 | process_request: { 31 | id: 1, 32 | status: "ACTIVE", 33 | }, 34 | }); 35 | 36 | // Interact with the UI 37 | cy.get('.form-group > .btn').click(); 38 | 39 | // Emit socket event and verify the URL change 40 | cy.socketEventNext("ProcessMaker\\Events\\RedirectTo", { 41 | params: [ 42 | { 43 | endEventDestination: { 44 | type: "summaryScreen", 45 | value: null, 46 | }, 47 | }, 48 | ], 49 | method: "processCompletedRedirect", 50 | }) 51 | cy.wait(1000); 52 | // This code will execute after the socket event is finished 53 | cy.url().should("eq", "http://localhost:5173/?scenario=TaskWebEntry"); 54 | 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/e2e/specs/WrongConfigurationErrors.js: -------------------------------------------------------------------------------- 1 | describe.skip("Wrong Configuration Errors", () => { 2 | it("Screen with rendering problem in a component should show a warning", () => { 3 | cy.visit("/", { 4 | onBeforeLoad(win) { 5 | cy.stub(win.console, "warn").as("consoleWarn"); 6 | cy.stub(win.console, "error").as("consoleError"); 7 | } 8 | }); 9 | cy.openAcordeon("collapse-2"); 10 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 11 | position: "bottom" 12 | }); 13 | cy.get("[data-cy=screen-element-container]").click(); 14 | cy.get("[data-cy=inspector-name]").clear().type("s.1"); 15 | cy.get("[data-cy=mode-preview]").click(); 16 | cy.get("[data-cy=mode-editor]").click(); 17 | cy.get("@consoleWarn").should( 18 | "be.calledWith", 19 | "There was a problem rendering the screen" 20 | ); 21 | }); 22 | 23 | it("Screen with rendering problem in a component should not show a warning after the problematic component is deleted", () => { 24 | cy.visit("/", { 25 | onBeforeLoad(win) { 26 | cy.stub(win.console, "warn").as("consoleWarn"); 27 | cy.stub(win.console, "error").as("consoleError"); 28 | } 29 | }); 30 | cy.openAcordeon("collapse-2"); 31 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 32 | position: "bottom" 33 | }); 34 | cy.get("[data-cy=screen-element-container]").click(); 35 | cy.get("[data-cy=inspector-name]").clear().type("s.1"); 36 | cy.get("[data-cy=mode-preview]").click(); 37 | cy.get("[data-cy=mode-editor]").click(); 38 | cy.get("@consoleWarn").should( 39 | "be.calledWith", 40 | "There was a problem rendering the screen" 41 | ); 42 | cy.get("[data-cy=screen-element-container] .ml-auto > .btn-danger").click(); 43 | cy.get("[data-cy=controls-FormInput]").drag("[data-cy=screen-drop-zone]", { 44 | position: "bottom" 45 | }); 46 | cy.get("[data-cy=mode-preview]").click(); 47 | cy.get("[data-cy=mode-editor]").click(); 48 | cy.get("@consoleError").should("not.to.be.called"); 49 | cy.get("@consoleWarn").should("not.to.be.calledTwice"); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/e2e/specs/pagesDropdown.spec.js: -------------------------------------------------------------------------------- 1 | describe("Page Dropdown", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | it("Basic default value", () => { 6 | cy.openAllAcordeon(); 7 | cy.get("[data-test=page-dropdown]").click(); 8 | cy.get("[data-test=add-page]").click({ force: true }); 9 | // Define Page 2 10 | cy.get("[data-cy=add-page-name]").clear().type("Page_2"); 11 | cy.get("[data-cy=add-page-modal] button.btn").eq(1).click(); 12 | cy.wait(300); 13 | cy.get('[data-cy=controls-FormButton]:contains("Page")').drag( 14 | "[data-cy=screen-drop-zone]", 15 | { position: "bottom" } 16 | ); 17 | cy.get("[data-cy=screen-element-container]").click(); 18 | cy.get("[data-cy=inspector-label]").clear().type("Go to Page 1"); 19 | cy.get("[data-cy=accordion-Configuration]").click(); 20 | cy.setMultiselect("[data-cy=inspector-eventData]", "Default"); 21 | // Define Page 1 22 | cy.get("[data-test=page-dropdown]").click(); 23 | cy.get("[data-test=page-Default]").click({ force: true }); 24 | cy.get('[data-cy=controls-FormButton]:contains("Page")').drag( 25 | "[data-cy=screen-drop-zone]", 26 | { position: "bottom" } 27 | ); 28 | cy.get("[data-cy=screen-element-container]").click(); 29 | cy.get("[data-cy=inspector-label]").clear().type("Go to Page 2"); 30 | cy.get("[data-cy=accordion-Configuration]").click(); 31 | cy.setMultiselect("[data-cy=inspector-eventData]", "Page_2"); 32 | // Navigate Default 33 | cy.get("[data-test=page-dropdown]").click(); 34 | cy.get("[data-test=page-Default]").click({ force: true }); 35 | cy.get("[data-cy=editor-content]").should("contain.text", "Go to Page 2"); 36 | // Navigate Page 2 37 | cy.get("[data-test=page-dropdown]").click(); 38 | cy.get("[data-test=page-Page_2]").click({ force: true }); 39 | cy.get("[data-cy=editor-content]").should("contain.text", "Go to Page 1"); 40 | // Preview 41 | cy.get("[data-cy=mode-preview]").click(); 42 | cy.get("[data-cy=preview-content]").should("contain.text", "Go to Page 2"); 43 | cy.get("[data-cy=preview-content] button").click(); 44 | cy.get("[data-cy=preview-content]").should("contain.text", "Go to Page 1"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/e2e/support/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node types are controls of the screen 3 | */ 4 | export const nodeControls = { 5 | checkbox: "[data-cy=controls-FormCheckbox]", 6 | datePicker: "[data-cy=controls-FormDatePicker]", 7 | fileDownload: "[data-cy=controls-FileDownload]", 8 | fileUpload: "[data-cy=controls-FileUpload]", 9 | formImage: "[data-cy=controls-FormImage]", 10 | formInput: "[data-cy=controls-FormInput]", 11 | formLoop: "[data-cy=controls-FormLoop]", 12 | formMultiColumn: "[data-cy=controls-FormMultiColumn]", 13 | formNestedScreen: "[data-cy=controls-FormNestedScreen]", 14 | formRecordList: "[data-cy=controls-FormRecordList]", 15 | formHtmlViewer: "[data-cy=controls-FormHtmlViewer]", 16 | formSelectList: "[data-cy=controls-FormSelectList]", 17 | formSubmit: "[data-cy=controls-FormButton]", 18 | formTextArea: "[data-cy=controls-FormTextArea]", 19 | clipboard: "[data-cy=controls-Clipboard]" 20 | }; 21 | -------------------------------------------------------------------------------- /tests/e2e/support/e2e.js: -------------------------------------------------------------------------------- 1 | import "@cypress/code-coverage/support"; 2 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | import "./commands"; 2 | import "./e2e"; 3 | -------------------------------------------------------------------------------- /tests/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": "../../node_modules", 5 | "types": [ 6 | "cypress" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['jest'] , 3 | env: { 4 | 'jest/globals': true, 5 | node: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /tests/unit/CustomCss.spec.js: -------------------------------------------------------------------------------- 1 | import { createLocalVue, mount } from '@vue/test-utils'; 2 | import Renderer from '../../src/components/vue-form-renderer'; 3 | import BootstrapVue from 'bootstrap-vue'; 4 | 5 | const localVue = createLocalVue(); 6 | localVue.use(BootstrapVue); 7 | 8 | describe('Test custom css', () => { 9 | 10 | // Now mount the component and you have the wrapper 11 | const wrapper = mount(Renderer, { 12 | localVue, 13 | mocks: { 14 | $t: text => text, 15 | }, 16 | sync: false, 17 | propsData: { 18 | config: [{ 19 | items: [ 20 | { 21 | 'config': { 22 | 'label': 'Field test 1', 23 | 'name': 'field1', 24 | 'placeholder': '', 25 | 'validation': '', 26 | 'helper': null, 27 | 'type': 'text', 28 | }, 29 | 'label': 'Line Input', 30 | }, 31 | { 32 | 'config': { 33 | 'label': 'Field test 2', 34 | 'name': 'field2', 35 | 'placeholder': '', 36 | 'validation': '', 37 | 'helper': null, 38 | 'type': 'text', 39 | }, 40 | 'label': 'Line Input', 41 | }], 42 | }], 43 | data: {}, 44 | page: 0, 45 | computed: [], 46 | customCss: 'div {background: green;} .form-control {color: gray;}', 47 | }, 48 | }); 49 | 50 | it('Test custom CSS inclusion', () => { 51 | // Test if the rendered form contains the custom CSS 52 | expect(wrapper.html()).toMatch(/