}
+ {/*'translateY' - Prevents overlap of error message and button group in case of arrayField by moving the button group lower*/}
+ {!props.disableInput && props.displayExtraDateTimeButtons &&
+
+
+
+
+ }
)}
diff --git a/src/components/input-switch/input-field.tsx b/src/components/input-switch/input-field.tsx
index 2369219d5..b15fcda53 100644
--- a/src/components/input-switch/input-field.tsx
+++ b/src/components/input-switch/input-field.tsx
@@ -4,7 +4,7 @@ import '@isrd-isi-edu/chaise/src/assets/scss/_input-switch.scss';
import DisplayValue from '@isrd-isi-edu/chaise/src/components/display-value';
// hooks
-import { useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { useFormContext, useController, ControllerRenderProps, FieldValues, UseControllerReturn } from 'react-hook-form';
// utils
@@ -57,6 +57,23 @@ export type InputFieldProps = {
* by default, we are capturing the "enter" key event and stopping it
*/
allowEnter?: boolean
+ /**
+ * display required error regardless of whether the form has been submitted or not.
+ */
+ displayRequiredErrorBeforeSubmit? : boolean
+ /**
+ * `optional`additional controller rules for the input field.
+ * Check allowed rules here - https://react-hook-form.com/docs/useform/register#options
+ */
+ additionalControllerRules?: {
+ [key: string]: (string | number | boolean | RegExp | Function | Object) | {
+ value: (boolean | number | RegExp),
+ /**
+ * Error message
+ */
+ message: string
+ }
+ }
}
@@ -106,14 +123,16 @@ const InputField = ({
containerClasses = '',
styles,
allowEnter = false,
+ displayRequiredErrorBeforeSubmit,
onClear,
controllerRules,
checkHasValue,
handleChange,
- checkIsTouched
+ checkIsTouched,
+ additionalControllerRules,
}: InputFieldCompProps): JSX.Element => {
- const { setValue, control, clearErrors } = useFormContext();
+ const { setValue, control, clearErrors ,trigger} = useFormContext();
controllerRules = isObjectAndNotNull(controllerRules) ? controllerRules : {};
if (requiredInput) {
@@ -123,7 +142,7 @@ const InputField = ({
const formInput = useController({
name,
control,
- rules: controllerRules,
+ rules: {...controllerRules, ...additionalControllerRules},
});
const field = formInput?.field;
@@ -145,10 +164,10 @@ const InputField = ({
e.preventDefault();
clearErrors(name);
setValue(name, '');
+ trigger(name); // triggers validation on the form field
}
useEffect(() => {
-
const hasValue = checkHasValue ? checkHasValue(fieldValue) : Boolean(fieldValue);
if (showClear != hasValue) {
setShowClear(hasValue);
@@ -176,7 +195,8 @@ const InputField = ({
let showError = !!error?.message && displayErrors;
if (showError) {
if (error?.type === 'required') {
- showError = formInput.formState.isSubmitted;
+ // We always show this error for array-input fields. In case of other fields, we show this once form submit event is triggered.
+ showError = formInput.formState.isSubmitted || displayRequiredErrorBeforeSubmit;
} else {
showError = checkIsTouched ? checkIsTouched() : isTouched;
}
@@ -193,4 +213,4 @@ const InputField = ({
};
-export default InputField;
+export default React.memo(InputField);
diff --git a/src/components/input-switch/input-switch.tsx b/src/components/input-switch/input-switch.tsx
index 166bec3bd..2fa6bf31b 100644
--- a/src/components/input-switch/input-switch.tsx
+++ b/src/components/input-switch/input-switch.tsx
@@ -1,7 +1,5 @@
import '@isrd-isi-edu/chaise/src/assets/scss/_input-switch.scss';
-import { memo } from 'react';
-
// components
import ArrayField from '@isrd-isi-edu/chaise/src/components/input-switch/array-field';
import BooleanField from '@isrd-isi-edu/chaise/src/components/input-switch/boolean-field';
@@ -19,6 +17,8 @@ import IframeField from '@isrd-isi-edu/chaise/src/components/input-switch/iframe
// models
import { RecordeditColumnModel, RecordeditForeignkeyCallbacks } from '@isrd-isi-edu/chaise/src/models/recordedit';
+import React, { ForwardedRef, forwardRef } from 'react';
+import PropTypes from 'prop-types';
export type InputSwitchProps = {
/**
@@ -68,6 +68,10 @@ export type InputSwitchProps = {
* flag to show error below the input switch component
*/
displayErrors?: boolean,
+ /**
+ * display required error regardless of whether the form has been submitted or not.
+ */
+ displayRequiredErrorBeforeSubmit?: boolean
/**
* the handler function called on input change
*/
@@ -130,6 +134,18 @@ export type InputSwitchProps = {
* whether we should display the date/time labels
*/
displayDateTimeLabels?: boolean
+ /**
+ * `optional`additional controller rules for the input field.
+ * Check allowed rules here - https://react-hook-form.com/docs/useform/register#options
+ */
+ additionalControllerRules?: {
+ [key: string]: (string | number | boolean | RegExp | Function | Object) | RuleWithMessage
+ }
+};
+
+export type RuleWithMessage = {
+ value: (boolean | number | RegExp),
+ message: string
};
const InputSwitch = ({
@@ -145,6 +161,7 @@ const InputSwitch = ({
disableInput,
requiredInput,
displayErrors = true,
+ displayRequiredErrorBeforeSubmit = false,
styles = {},
columnModel,
appMode,
@@ -157,8 +174,9 @@ const InputSwitch = ({
waitingForForeignKeyData,
displayExtraDateTimeButtons,
displayDateTimeLabels,
+ additionalControllerRules,
foreignKeyCallbacks
-}: InputSwitchProps): JSX.Element | null => {
+}: InputSwitchProps ): JSX.Element | null => {
return (() => {
@@ -190,6 +208,7 @@ const InputSwitch = ({
baseArrayType={columnModel?.column.type.baseType.name}
type={type}
name={name}
+ columnModel={columnModel?.column.type.rootName === 'timestamptz' ? columnModel : undefined}
classes={classes}
inputClasses={inputClasses}
containerClasses={containerClasses}
@@ -198,6 +217,7 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
allowEnter={true}
/>;
@@ -216,6 +236,7 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
columnModel={columnModel}
appMode={appMode}
@@ -226,6 +247,7 @@ const InputSwitch = ({
parentLogStackPath={parentLogStackPath}
foreignKeyData={foreignKeyData}
waitingForForeignKeyData={waitingForForeignKeyData}
+ additionalControllerRules={additionalControllerRules}
foreignKeyCallbacks={foreignKeyCallbacks}
/>
case 'dropdown-select':
@@ -243,6 +265,7 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
columnModel={columnModel}
appMode={appMode}
@@ -270,8 +293,10 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
columnModel={columnModel}
+ additionalControllerRules={additionalControllerRules}
/>
case 'timestamp':
return ;
case 'date':
return ;
case 'integer2':
case 'integer4':
@@ -323,7 +352,9 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
+ additionalControllerRules={additionalControllerRules}
/>;
case 'boolean':
return ;
case 'markdown':
case 'longtext':
@@ -353,8 +386,10 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
allowEnter={true}
+ additionalControllerRules={additionalControllerRules}
/>;
case 'json':
case 'jsonb':
@@ -369,8 +404,10 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
allowEnter={true}
+ additionalControllerRules={additionalControllerRules}
/>;
case 'color':
return ;
case 'text':
default:
@@ -399,10 +438,12 @@ const InputSwitch = ({
requiredInput={requiredInput}
styles={styles}
displayErrors={displayErrors}
+ displayRequiredErrorBeforeSubmit={displayRequiredErrorBeforeSubmit}
placeholder={placeholder as string}
+ additionalControllerRules={additionalControllerRules}
/>
}
})();
};
-export default memo(InputSwitch);
+export default React.memo(InputSwitch);
diff --git a/src/components/input-switch/numeric-field.tsx b/src/components/input-switch/numeric-field.tsx
index 536bce886..9a3711aaf 100644
--- a/src/components/input-switch/numeric-field.tsx
+++ b/src/components/input-switch/numeric-field.tsx
@@ -31,4 +31,4 @@ const NumericField = (props: InputFieldProps): JSX.Element => {
);
};
-export default NumericField;
+export default NumericField;
\ No newline at end of file
diff --git a/src/components/recordedit/form-container.tsx b/src/components/recordedit/form-container.tsx
index bdef3a399..15ed9e993 100644
--- a/src/components/recordedit/form-container.tsx
+++ b/src/components/recordedit/form-container.tsx
@@ -1,17 +1,16 @@
// components
-import ChaiseTooltip from '@isrd-isi-edu/chaise/src/components/tooltip';
import FormRow from '@isrd-isi-edu/chaise/src/components/recordedit/form-row';
+import ChaiseTooltip from '@isrd-isi-edu/chaise/src/components/tooltip';
// hooks
+import useRecordedit from '@isrd-isi-edu/chaise/src/hooks/recordedit';
import { useLayoutEffect, useRef, useState } from 'react';
import { useFormContext } from 'react-hook-form';
-import useRecordedit from '@isrd-isi-edu/chaise/src/hooks/recordedit';
// models
import { RecordeditDisplayMode } from '@isrd-isi-edu/chaise/src/models/recordedit';
// utils
-import ResizeSensor from 'css-element-queries/src/ResizeSensor';
import { addTopHorizontalScroll } from '@isrd-isi-edu/chaise/src/utils/ui-utils';
type FormContainerProps = {
@@ -30,6 +29,7 @@ const FormContainer = ({
columnModels, config, forms, onSubmitValid, onSubmitInvalid, removeForm
} = useRecordedit();
+
const { handleSubmit } = useFormContext();
const formContainer = useRef(null);
diff --git a/src/utils/input-utils.ts b/src/utils/input-utils.ts
index 8544baaec..ffb2cf9a4 100644
--- a/src/utils/input-utils.ts
+++ b/src/utils/input-utils.ts
@@ -197,28 +197,28 @@ export function arrayFieldPlaceholder(baseType: string) {
let placeholder;
switch (baseType) {
case 'timestamptz':
- placeholder = 'example: [ \"2001-01-01T01:01:01-08:00\", \"2002-02-02T02:02:02-08:00\" ]'
+ placeholder = ['2001-01-01T01:01:01-08:00', '2002-02-02T02:02:02-08:00']
case 'timestamp':
- placeholder = 'example: [ \"2001-01-01T01:01:01\", \"2002-02-02T02:02:02\" ]'
+ placeholder = ['2001-01-01T01:01:01', '2002-02-02T02:02:02']
break;
case 'date':
- placeholder = 'example: [ \"2001-01-01\", \"2001-02-02\" ]'
+ placeholder = ['2001-01-01', '2001-02-02']
break;
case 'numeric':
case 'float4':
case 'float8':
- placeholder = 'example: [ 1, 2.2 ]'
+ placeholder = [1, 2.2]
break;
case 'int2':
case 'int4':
case 'int8':
- placeholder = 'example: [ 1, 2 ]'
+ placeholder = [1, 2]
break;
case 'boolean':
- placeholder = 'example: [ true, false ]'
+ placeholder = [true, false]
break;
default:
- placeholder = 'example: [ \"value1\", \"value2\" ]'
+ placeholder = ['value1', 'value2']
break;
}
@@ -342,4 +342,4 @@ export const VALIDATE_VALUE_BY_TYPE: {
export const hasVerticalScrollbar = (element: any) => {
if (!element) return;
return element.scrollHeight > element.clientHeight;
-};
\ No newline at end of file
+};
diff --git a/src/utils/recordedit-utils.ts b/src/utils/recordedit-utils.ts
index 0a4232b68..09a544a80 100644
--- a/src/utils/recordedit-utils.ts
+++ b/src/utils/recordedit-utils.ts
@@ -125,16 +125,16 @@ export function getColumnModelLogAction(action: string, colModel: RecordeditColu
function _copyOrClearValueForColumn(
column: any, values: any, foreignKeyData: any,
destFormValue: number, srcFormValue?: number, clearValue?: boolean,
- skipFkColumns?: boolean, setValue?: (formKey: string, value: string | number) => void
+ skipFkColumns?: boolean, setValue?: (formKey: string, value: any) => void
) {
const srcKey = typeof srcFormValue === 'number' ? `${srcFormValue}-${column.name}` : null;
const dstKey = `${destFormValue}-${column.name}`;
if (clearValue) {
if (setValue) {
- setValue(dstKey, '');
+ setValue(dstKey, column.type?.isArray ? [] : '');
} else {
- values[dstKey] = '';
+ values[dstKey] = column.type?.isArray ? [] : '';
}
} else if (srcKey) {
const tempVal = replaceNullOrUndefined(values[srcKey], '')
@@ -214,7 +214,7 @@ function _copyOrClearValueForColumn(
export function copyOrClearValue(
columnModel: RecordeditColumnModel, values: any, foreignKeyData: any,
destFormValue: number, srcFormValue?: number, clearValue?: boolean,
- skipFkColumns?: boolean, setValue?: (formKey: string, value: string | number) => void
+ skipFkColumns?: boolean, setValue?: (formKey: string, value: any) => void
) {
const column = columnModel.column;
@@ -418,8 +418,27 @@ function _populateEditInitialValueForAColumn(
// stringify the returned array value
if (column.type.isArray) {
+
if (usedValue !== null) {
- values[`${formValue}-${column.name}`] = JSON.stringify(usedValue, undefined, 2);
+
+ values[`${formValue}-${column.name}`] = usedValue.map((value: any) => {
+ let valueToAdd: any = {
+ 'val': value
+ }
+
+ if (getInputType({ name: column.type.baseType.name }) === 'timestamp') {
+ const DATE_TIME_FORMAT = column.type.rootName === 'timestamptz' ? dataFormats.datetime.return : dataFormats.timestamp;
+ const v = formatDatetime(value, { outputMomentFormat: DATE_TIME_FORMAT })
+
+ valueToAdd = {
+ 'val': v?.datetime,
+ 'val-date': v?.date,
+ 'val-time': v?.time
+ }
+ }
+
+ return valueToAdd
+ });
}
return;
}
@@ -595,9 +614,7 @@ export function populateSubmissionRow(reference: any, formNumber: number, formDa
// TODO col.isDisabled is wrong. it's always returning false
if (v && !col.isDisabled) {
- if (col.type.isArray) {
- v = JSON.parse(v);
- } else if (col.isAsset) {
+ if (col.isAsset) {
// dereference formData so we aren't modifying content in react-hook-form
// v is an object with `file`, `filename`, `filesize`, and `url` defined
const tempVal = { ...v };
@@ -610,6 +627,9 @@ export function populateSubmissionRow(reference: any, formNumber: number, formDa
}
v = tempVal;
+ } else if (col.type?.isArray) {
+ // array-field encodes the values inside '.val' prop
+ v = v.length ? v.map((i: any) => i.val) : '';
} else {
// Special cases for formatting data
switch (col.type.name) {
diff --git a/test/e2e/data_setup/schema/recordedit/multi-form-input.json b/test/e2e/data_setup/schema/recordedit/multi-form-input.json
index 35fe071d9..ed6df3dfd 100644
--- a/test/e2e/data_setup/schema/recordedit/multi-form-input.json
+++ b/test/e2e/data_setup/schema/recordedit/multi-form-input.json
@@ -110,17 +110,27 @@
"type": {
"typename": "text"
}
+ },
+ {
+ "name": "array_text",
+ "type": {
+ "is_array": true,
+ "typename": "text[]",
+ "base_type": {
+ "typename": "text"
+ }
+ }
}
],
"annotations": {
"tag:isrd.isi.edu,2016:visible-columns": {
"*": [
"id", "markdown_col", "text_col", "int_col", "float_col", "date_col", "timestamp_col",
- "boolean_col", ["multi-form-input", "main_fk1"], "asset_col"
+ "boolean_col", ["multi-form-input", "main_fk1"], "asset_col", "array_text"
],
"compact": [
"markdown_col", "text_col", "int_col", "float_col", "date_col", "timestamp_col",
- "boolean_col", ["multi-form-input", "main_fk1"], "asset_col"
+ "boolean_col", ["multi-form-input", "main_fk1"], "asset_col", "array_text"
]
}
}
diff --git a/test/e2e/data_setup/schema/recordedit/product-edit.json b/test/e2e/data_setup/schema/recordedit/product-edit.json
index 9e2041110..50f9d4447 100644
--- a/test/e2e/data_setup/schema/recordedit/product-edit.json
+++ b/test/e2e/data_setup/schema/recordedit/product-edit.json
@@ -358,7 +358,8 @@
"base_type": {
"typename": "text"
}
- }
+ },
+ "nullok":false
},
{
"name": "boolean_array",
diff --git a/test/e2e/docker/Dockerfile.chaise-test-env b/test/e2e/docker/Dockerfile.chaise-test-env
new file mode 100644
index 000000000..d8e2f5b47
--- /dev/null
+++ b/test/e2e/docker/Dockerfile.chaise-test-env
@@ -0,0 +1,52 @@
+# This creates a docker image with chaise and all the test dependencies installed. This can be directly used in a CI for headless testing.
+# for more information refer - docs/dev-docs/e2e-test-docker.md
+FROM ubuntu:20.04
+
+RUN apt-get update \
+ && apt-get install -y wget gnupg \
+ && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \
+ && sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
+ && apt-get update \
+ && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 dbus dbus-x11 \
+ --no-install-recommends \
+ && service dbus start \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN apt-get update && \
+ apt-get install -y \
+ curl \
+ make \
+ rsync \
+ git \
+ sudo \
+ openjdk-8-jdk
+
+# Install Node.js 16.x
+RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
+RUN apt-get install -y nodejs
+
+# Set the working directory
+WORKDIR /app
+
+# Copy chaise codebase
+COPY ./chaise /app/chaise
+
+# Install test dependencies
+RUN cd chaise && \
+ make deps-test
+
+RUN echo 'chaiseuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/chaiseuser
+
+RUN groupadd -r chaiseuser && useradd -rm -g chaiseuser -G audio,video chaiseuser && \
+ echo 'chaiseuser:test' | chpasswd && \
+ chmod -R 777 /app && \
+ chmod -R 777 /home
+
+ENV DBUS_SESSION_BUS_ADDRESS autolaunch:
+
+ENV DISPLAY host.docker.internal:0.0
+
+USER chaiseuser
+
+# Specify the command to run when the container starts
+CMD ["/bin/bash"]
diff --git a/test/e2e/docker/Dockerfile.chaise-test-env.local b/test/e2e/docker/Dockerfile.chaise-test-env.local
new file mode 100644
index 000000000..25586148e
--- /dev/null
+++ b/test/e2e/docker/Dockerfile.chaise-test-env.local
@@ -0,0 +1,49 @@
+# This creates a docker image with only chromedriver and its dependencies.
+# This can be directly used to run test cases locally both in HEADLESS and HEADFUL mode.
+# Make sure to use volume mounts to mount the chaise directory to the container.
+# for more information refer - docs/dev-docs/e2e-test-docker.md
+FROM ubuntu:20.04
+
+
+RUN apt-get update \
+ && apt-get install -y wget gnupg \
+ && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \
+ && sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
+ && apt-get update \
+ && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 dbus dbus-x11 \
+ --no-install-recommends \
+ && service dbus start \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN apt-get update && \
+ apt-get install -y \
+ curl \
+ make \
+ rsync \
+ git \
+ sudo \
+ openjdk-8-jdk
+
+# Install Node.js 16.x
+RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
+RUN apt-get install -y nodejs
+
+# Set the working directory
+WORKDIR /app
+
+RUN echo 'chaiseuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/chaiseuser
+
+RUN groupadd -r chaiseuser && useradd -rm -g chaiseuser -G audio,video chaiseuser && \
+ echo 'chaiseuser:test' | chpasswd && \
+ chmod -R 777 /app && \
+ chmod -R 777 /home
+
+
+ENV DBUS_SESSION_BUS_ADDRESS autolaunch:
+
+ENV DISPLAY host.docker.internal:0.0
+
+USER chaiseuser
+
+# Specify the command to run when the container starts
+CMD ["/bin/bash"]
diff --git a/test/e2e/locators/recordedit.ts b/test/e2e/locators/recordedit.ts
index da48de58e..09053fa8e 100644
--- a/test/e2e/locators/recordedit.ts
+++ b/test/e2e/locators/recordedit.ts
@@ -126,9 +126,11 @@ export default class RecordeditLocators {
* returns the cell (entity-value).
* this is useful if we want to test the extra classes attached to it.
*/
- static getFormInputCell(container: Locator | Page, name: string, formNumber: number): Locator {
+ static getFormInputCell(container: Locator | Page, name: string, formNumber: number, isArray?: boolean): Locator {
formNumber = formNumber || 1;
- // TODO does this work?
+ if (isArray) {
+ return container.locator(`.array-input-field-container-${formNumber}-${name}`).locator('xpath=..');
+ }
return container.locator(`.input-switch-container-${formNumber}-${name}`).locator('xpath=..')
}
@@ -344,4 +346,27 @@ export default class RecordeditLocators {
notes: iframe.locator('#notes'),
}
}
+
+ // ------------- array selectors ----------------- //
+ static getArrayFieldContainer(container: Locator | Page, name: string, formNumber: number) {
+ formNumber = formNumber || 1;
+ return container.locator(`.array-input-field-container-${formNumber}-${name}`);
+ }
+
+ /**
+ * TODO this only supports array of texts for now and should be changed later for other types.
+ */
+ static getArrayFieldElements(container: Locator | Page, name: string, formNumber: number, baseType: string) {
+ formNumber = formNumber || 1;
+ const fieldName = `${formNumber}-${name}`;
+ const elem = container.locator(`.array-input-field-container-${fieldName}`);
+ return {
+ container: elem,
+ addItemContainer: elem.locator('.add-element-container'),
+ addItemInput: elem.locator('.add-element-container input'),
+ addItemButton: elem.locator('.add-button'),
+ removeItemButtons: elem.locator('.array-remove-button'),
+ inputs: elem.locator('li input')
+ };
+ }
}
diff --git a/test/e2e/specs/all-features-confirmation/recordedit/add.spec.js b/test/e2e/specs/all-features-confirmation/recordedit/add.spec.js
index 310aba6aa..202aea627 100644
--- a/test/e2e/specs/all-features-confirmation/recordedit/add.spec.js
+++ b/test/e2e/specs/all-features-confirmation/recordedit/add.spec.js
@@ -42,9 +42,9 @@ var testParams = {
"rating": "1", "summary": "This is the summary of this column 1.", "description": "## Description 1",
"json_col": JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"no_of_rooms": "1", "opened_on": moment("2017-01-01 01:01:01", "YYYY-MM-DD hh:mm:ss"), "date_col": "2017-01-01", "luxurious": false,
- "text_array": "[\"v1\", \"v2\"]", "boolean_array": "[true]", "int4_array": "[1, 2]", "float4_array": "[1, 2.2]",
- "date_array": "[\"2001-01-01\", \"2002-02-02\"]", "timestamp_array": "[null, \"2001-01-01T01:01:01\"]",
- "timestamptz_array": "[null, \"2001-01-01T01:01:01-08:00\"]",
+ "text_array": ["v1", "v2"], "boolean_array": [true], "int4_array": [1, 2], "float4_array": [1, 2.2],
+ "date_array": ["2001-01-01", "2002-02-02"], "timestamp_array": ["2001-01-01T01:01:01"],
+ "timestamptz_array": ["2001-01-01T01:01:01-08:00"],
"color_rgb_hex_column": "#123456"
},
{
@@ -52,9 +52,9 @@ var testParams = {
"rating": "2", "summary": "This is the summary of this column 2.", "description": "## Description 2",
"json_col": JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"no_of_rooms": "2", "opened_on": moment("2017-02-02 02:02:02", "YYYY-MM-DD hh:mm:ss"), "date_col": "2017-02-02", "luxurious": true,
- "text_array": "[\"v2\", \"v3\"]", "boolean_array": "[false]", "int4_array": "[1, 2]", "float4_array": "[2, 3.3]",
- "date_array": "[\"2002-02-02\", null]", "timestamp_array": "[\"2002-02-02T02:02:02\"]",
- "timestamptz_array": "[\"2002-02-02T02:02:02-08:00\"]",
+ "text_array": ["v2", "v3"], "boolean_array": [false], "int4_array": [1, 2], "float4_array": [2, 3.3],
+ "date_array": ["2002-02-02"], "timestamp_array": ["2002-02-02T02:02:02"],
+ "timestamptz_array": ["2002-02-02T02:02:02-08:00"],
"color_rgb_hex_column": "#654321"
}
],
@@ -70,17 +70,18 @@ var testParams = {
"new title 1", {"link":"https://example1.com/", "value":"Link to Website"}, {"link":`${process.env.CHAISE_BASE_URL}/record/#${process.env.catalogId}/product-add:category/term=Hotel`, "value":"Hotel"},
"1.0000", "This is the summary of this column 1.", "Description 1", JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"1", "2017-01-01 01:01:01", "2017-01-01", "false",
- "v1, v2", "true", "1, 2", "1.0000, 2.2000", "2001-01-01, 2002-02-02", "No value, 2001-01-01 01:01:01", "No value, 2001-01-01 01:01:01", "#123456"
+ "v1, v2", "true", "1, 2", "1.0000, 2.2000", "2001-01-01, 2002-02-02", "2001-01-01 01:01:01", "2001-01-01 01:01:01", "#123456"
],
[
"new title 2", {"link":"https://example2.com/", "value":"Link to Website"}, {"link":`${process.env.CHAISE_BASE_URL}/record/#${process.env.catalogId}/product-add:category/term=Ranch`, "value":"Ranch"},
"2.0000", "This is the summary of this column 2.", "Description 2", JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"2", "2017-02-02 02:02:02", "2017-02-02", "true",
- "v2, v3", "false", "1, 2", "2.0000, 3.3000", "2002-02-02, No value", "2002-02-02 02:02:02", "2002-02-02 02:02:02", "#654321"
+ "v2, v3", "false", "1, 2", "2.0000, 3.3000", "2002-02-02", "2002-02-02 02:02:02", "2002-02-02 02:02:02", "#654321"
]
],
files: []
- }, {
+ },
+ {
comment: "uploading new files",
schema_name: "product-add",
table_name: "file",
@@ -128,7 +129,8 @@ var testParams = {
path: "testfile5MB.txt",
tooltip: "- testfile5MB.txt\n- 5 MB"
}]
- }, {
+ },
+ {
comment: "uploader when one file exists in hatrac and the other one is new",
schema_name: "product-add",
table_name: "file",
@@ -170,7 +172,8 @@ var testParams = {
path: "testfile500kb.png",
tooltip: "- testfile500kb.png\n- 500 kB"
}]
- }, {
+ },
+ {
comment: "uploader when all the files already exist in hatrac",
schema_name: "product-add",
table_name: "file",
@@ -212,7 +215,8 @@ var testParams = {
path: "testfile500kb.png",
tooltip: "- testfile500kb.png\n- 500 kB"
}]
- }]
+ }
+ ]
};
// keep track of namespaces that we use, so we can delete them afterwards
diff --git a/test/e2e/specs/all-features-confirmation/recordedit/edit.spec.js b/test/e2e/specs/all-features-confirmation/recordedit/edit.spec.js
index 52c63d5f2..93bc010f5 100644
--- a/test/e2e/specs/all-features-confirmation/recordedit/edit.spec.js
+++ b/test/e2e/specs/all-features-confirmation/recordedit/edit.spec.js
@@ -31,7 +31,7 @@ var testParams = {
{ name: "opened_on", title: "Operational Since", type: "timestamptz", nullok: false, inline_comment: "The exact time and date where this accommodation became available!" },
{ name: "date_col", title: "date_col", type: "date"},
{ name: "luxurious", title: "Is Luxurious", type: "boolean" },
- { name: "text_array", title: "text_array", type: "array", baseType: "text" },
+ { name: "text_array", title: "text_array", type: "array", baseType: "text", nullok:false },
{ name: "boolean_array", title: "boolean_array", type: "array", baseType: "boolean" },
{ name: "int4_array", title: "int4_array", type: "array", baseType: "integer" },
{ name: "float4_array", title: "float4_array", type: "array", baseType: "number" },
@@ -45,9 +45,9 @@ var testParams = {
"summary": "Sherathon Hotels is an international hotel company with more than 990 locations in 73 countries. The first Radisson Hotel was built in 1909 in Minneapolis, Minnesota, US. It is named after the 17th-century French explorer Pierre-Esprit Radisson.",
"description": "**CARING. SHARING. DARING.**", "json_col": null, "no_of_rooms": "23", "opened_on": moment("12/9/2008, 12:00:00 AM", "MM/DD/YYYY, HH:mm:ss A"),
"date_col": "2008-12-09", "luxurious": "true",
- "text_array": "[\n \"v2\",\n \"v3\"\n]", "boolean_array": "[\n false\n]", "int4_array": "[\n 1\n]", "float4_array": "[\n 1.1,\n 2.2\n]",
- "date_array": null, "timestamp_array": "[\n \"2003-03-03T03:03:03\"\n]",
- "timestamptz_array": "[\n \""+moment("2002-02-02T02:02:02-08:00", "YYYY-MM-DDTHH:mm:ssZ").format("YYYY-MM-DDTHH:mm:ssZ")+"\"\n]",
+ "text_array": ["v2","v3"], "boolean_array": [false], "int4_array": [1], "float4_array": [1.1,2.2],
+ "date_array": null, "timestamp_array": ["2003-03-03T03:03:03"],
+ "timestamptz_array": [moment("2002-02-02T02:02:02-08:00", "YYYY-MM-DDTHH:mm:ssZ").format("YYYY-MM-DDTHH:mm:ssZ")],
"color_rgb_hex_column": "#623456"
}
],
@@ -56,9 +56,9 @@ var testParams = {
"title": "new title 1", "website": "https://example1.com", "category": {index: 1, value: "Ranch"},
"rating": "1e0", "summary": "This is the summary of this column 1.", "description": "## Description 1", "json_col": JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"no_of_rooms": "1", "opened_on": moment("2017-01-01 01:01:01", "YYYY-MM-DD hh:mm:ss"), "date_col": "2017-01-01", "luxurious": false,
- "text_array": "[\"v1\", \"v2\"]", "boolean_array": "[true]", "int4_array": "[1, 2]", "float4_array": "[1, 2.2]",
- "date_array": "[\"2001-01-01\", \"2002-02-02\"]", "timestamp_array": "[null, \"2001-01-01T01:01:01\"]",
- "timestamptz_array": "[null, \"2001-01-01T01:01:01-08:00\"]",
+ "text_array": ["v1", "v2"], "boolean_array": [true,false], "int4_array": [1, 2], "float4_array": [1, 2.2],
+ "date_array": ["2001-01-01", "2002-02-02"], "timestamp_array": ["2001-03-02T01:01:01" , "2001-01-01T01:01:01"],
+ "timestamptz_array": ["2001-01-01T01:01:01-08:00"],
"color_rgb_hex_column": "#723456"
}
],
@@ -74,11 +74,12 @@ var testParams = {
{"link":`${process.env.CHAISE_BASE_URL}/record/#${process.env.catalogId}/product-edit:category/id=10004`, "value":"Castle"},
"1.0000", "This is the summary of this column 1.", "Description 1", JSON.stringify({"items": {"qty": 6,"product": "apple"},"customer": "Nitish Sahu"},undefined,2),
"1", "2017-01-01 01:01:01", "2017-01-01", "false",
- "v1, v2", "true", "1, 2", "1.0000, 2.2000", "2001-01-01, 2002-02-02", "No value, 2001-01-01T01:01:01", "No value, 2001-01-01 01:01:01", "#723456"
+ "v1, v2", "true, false", "1, 2", "1.0000, 2.2000", "2001-01-01, 2002-02-02", "No value, 2001-01-01T01:01:01", "No value, 2001-01-01 01:01:01", "#723456"
]
],
files: []
- }, {
+ },
+ {
schema_name: "product-edit",
table_name: "file",
record_displayname: "90008", //since this is in single-edit, displayname is rowname.
@@ -111,7 +112,8 @@ var testParams = {
path: "testfile500kb.png",
tooltip: "- testfile500kb.png\n- 500 kB"
}]
- }]
+ }
+ ]
};
if (!process.env.CI) {
diff --git a/test/e2e/specs/all-features/acls/dynamic-acl.spec.include.ts b/test/e2e/specs/all-features/acls/dynamic-acl.spec.include.ts
index 9b0e7b5ed..ae974abdf 100644
--- a/test/e2e/specs/all-features/acls/dynamic-acl.spec.include.ts
+++ b/test/e2e/specs/all-features/acls/dynamic-acl.spec.include.ts
@@ -598,7 +598,6 @@ const testRecordSetEditAndDeleteButtons = async (
});
await test.step(`should ${displayBulkEdit ? '' : 'not'} display the buld edit link`, async () => {
- await page.pause();
const link = RecordsetLocators.getBulkEditLink(page);
if (displayBulkEdit) {
await expect.soft(link).toBeVisible();
diff --git a/test/e2e/specs/default-config/multi-form-input/multi-form-input-create.spec.ts b/test/e2e/specs/default-config/multi-form-input/multi-form-input-create.spec.ts
index 3a71bf1e4..c7645d655 100644
--- a/test/e2e/specs/default-config/multi-form-input/multi-form-input-create.spec.ts
+++ b/test/e2e/specs/default-config/multi-form-input/multi-form-input-create.spec.ts
@@ -453,20 +453,82 @@ const testParams = {
''
]
}
- }
+ },
+ {
+ type: RecordeditInputType.ARRAY,
+ column_name: 'array_text',
+ column_displayname: 'array_text',
+ apply_to_all: {
+ value: ['all array text input 1', 'all array text input 2'],
+ column_values_after: [
+ ['all array text input 1', 'all array text input 2'],
+ ['all array text input 1', 'all array text input 2'],
+ ['all array text input 1', 'all array text input 2'],
+ ['all array text input 1', 'all array text input 2'],
+ ['all array text input 1', 'all array text input 2']
+ ]
+ },
+ apply_to_some: {
+ value: ['some value'],
+ deselected_forms: [1, 3],
+ column_values_after: [
+ ['all array text input 1', 'all array text input 2'],
+ ['some value'],
+ ['all array text input 1', 'all array text input 2'],
+ ['some value'],
+ ['some value']
+ ]
+ },
+ clear_some: {
+ deselected_forms: [4, 5],
+ column_values_after: [
+ ['all array text input 1', 'all array text input 2'],
+ '',
+ ['all array text input 1', 'all array text input 2'],
+ ['some value'],
+ ['some value']
+ ]
+ },
+ manual_test: {
+ value: ['manual1', 'manual2', 'manual3'],
+ formNumber: 5,
+ column_values_after: [
+ ['all array text input 1', 'all array text input 2'],
+ '',
+ ['all array text input 1', 'all array text input 2'],
+ ['some value'],
+ ['manual1', 'manual2', 'manual3']
+ ]
+ }
+ },
],
submission: {
tableDisplayname: 'main',
resultColumnNames: [
'markdown_col', 'text_col', 'int_col', 'float_col', 'date_col', 'timestamp_input', 'boolean_input',
- 'lIHKX0WnQgN1kJOKR0fK5A', 'asset_col'
+ 'lIHKX0WnQgN1kJOKR0fK5A', 'asset_col', 'array_text'
],
resultRowValues: [
- ['markdown value', 'all text input', '432', '12.2000', '2011-10-09', '2021-10-09 18:00:00', 'true', '1', 'testfile128kb_1.png'],
- ['markdown value', '', '', '12.2000', '2011-10-09', '2021-10-09 18:00:00', '', '1', 'testfile128kb_1.png'],
- ['some markdown', 'all text input', '432', '4.6500', '2022-06-06', '2012-11-10 06:00:00', 'true', '3', 'testfile128kb_2.png'],
- ['manual value', 'some value', '666', '5.0000', '2006-06-06', '2006-06-06 06:06:00', 'false', '4', 'testfile128kb_3.png'],
- ['', 'manual', '2', '', '', '', 'true', '', ''],
+ [
+ 'markdown value', 'all text input', '432', '12.2000', '2011-10-09', '2021-10-09 18:00:00', 'true', '1',
+ 'testfile128kb_1.png', 'all array text input 1, all array text input 2'
+ ],
+ [
+ 'markdown value', '', '', '12.2000', '2011-10-09', '2021-10-09 18:00:00', '', '1',
+ 'testfile128kb_1.png', ''
+ ],
+ [
+ 'some markdown', 'all text input', '432', '4.6500', '2022-06-06', '2012-11-10 06:00:00', 'true', '3',
+ 'testfile128kb_2.png', 'all array text input 1, all array text input 2'
+ ],
+ [
+ 'manual value', 'some value', '666', '5.0000', '2006-06-06', '2006-06-06 06:06:00', 'false', '4',
+ 'testfile128kb_3.png', 'some value'
+ ],
+ [
+ '', 'manual', '2', '', '', '', 'true', '',
+ '', 'manual1, manual2, manual3'
+ ],
]
}
@@ -619,7 +681,7 @@ test.describe('multi form input in create mode', () => {
await expect.soft(applybtn).toBeVisible();
// deselect the first form that is selected by default
- const cell = RecordeditLocators.getFormInputCell(page, params.column_name, 1);
+ const cell = RecordeditLocators.getFormInputCell(page, params.column_name, 1, params.type === RecordeditInputType.ARRAY);
await cell.click();
await expect.soft(cell).not.toHaveClass(/entity-active/);
@@ -642,7 +704,7 @@ test.describe('multi form input in create mode', () => {
await test.step('when some forms are selected, clicking on apply should apply change to selected forms', async () => {
// deselect some forms
for (const f of params.apply_to_some.deselected_forms) {
- await RecordeditLocators.getFormInputCell(page, params.column_name, f).click();
+ await RecordeditLocators.getFormInputCell(page, params.column_name, f, params.type === RecordeditInputType.ARRAY).click();
}
await setInputValue(page, MULI_FORM_INPUT_FORM_NUMBER, params.column_name, colDisplayname, params.type, params.apply_to_some.value);
@@ -653,7 +715,7 @@ test.describe('multi form input in create mode', () => {
await test.step('when some forms are selected, clicking on clear should clear values in selected forms', async () => {
// deselect some forms
for (const f of params.clear_some.deselected_forms) {
- await RecordeditLocators.getFormInputCell(page, params.column_name, f).click();
+ await RecordeditLocators.getFormInputCell(page, params.column_name, f, params.type === RecordeditInputType.ARRAY).click();
}
await clearBtn.click();
diff --git a/test/e2e/specs/default-config/multi-form-input/multi-form-input-edit.spec.ts b/test/e2e/specs/default-config/multi-form-input/multi-form-input-edit.spec.ts
index d1ff683e4..1d8c30416 100644
--- a/test/e2e/specs/default-config/multi-form-input/multi-form-input-edit.spec.ts
+++ b/test/e2e/specs/default-config/multi-form-input/multi-form-input-edit.spec.ts
@@ -11,17 +11,17 @@ const testParams = {
filter: 'id=9001;id=9002@sort(id)',
columnsWithToggle: [
'markdown_col', 'text_col', 'int_col',
- 'float_col', 'date_col', 'timestamp_col', 'boolean_col', 'fk_col'
+ 'float_col', 'date_col', 'timestamp_col', 'boolean_col', 'fk_col', 'array_text'
],
submission: {
tableDisplayname: 'main',
resultColumnNames: [
'markdown_col', 'text_col', 'int_col', 'float_col', 'date_col', 'timestamp_input', 'boolean_input',
- 'lIHKX0WnQgN1kJOKR0fK5A', 'asset_col'
+ 'lIHKX0WnQgN1kJOKR0fK5A', 'asset_col', 'array_text'
],
resultRowValues: [
- ['markdown value 9001', 'text value 9001', '666', '', '', '', '', '', ''],
- ['markdown value 9002', '', '9,002', '', '2023-11-11', '', '', '', ''],
+ ['markdown value 9001', 'text value 9001', '666', '', '', '', '', '', '', ''],
+ ['markdown value 9002', '', '9,002', '', '2023-11-11', '', '', '', '', ''],
]
}
},
diff --git a/test/e2e/utils/chaise.page.js b/test/e2e/utils/chaise.page.js
index 440b08429..b4173eed3 100644
--- a/test/e2e/utils/chaise.page.js
+++ b/test/e2e/utils/chaise.page.js
@@ -317,9 +317,13 @@ var recordEditPage = function() {
* returns the cell (entity-value).
* this is useful if we want to test the extra classes attached to it.
*/
- this.getFormInputCell = (name, index) => {
+ this.getFormInputCell = (name, index, isArray) => {
index = index || 1;
const inputName = index + '-' + name;
+
+ if(isArray){
+ return element(by.className('array-input-field-container ' + inputName)).element(by.xpath('..'));
+ }
return element(by.className('input-switch-container-' + inputName)).element(by.xpath('..'));
};
@@ -494,6 +498,163 @@ var recordEditPage = function() {
this.getRecordSetTable = function() {
return element(by.className('recordset-table'));
};
+
+ // ArrayField Selectors
+
+ /**
+ *
+ * @typedef {Object} ArrayFieldContainerElement
+ * @property {Function} getAddNewElementContainer - returns the add new element container for the given array field
+ * @property {Function} getAddNewValueInputElement - returns add new element input element for a given array field
+ * @property {Function} getClearInputButton - returns clear button for the current input
+ * @property {Function} getErrorMessageElement - returns error message element for the current input
+ * @property {Function} getAddButton - returns add button for a given array field
+ * @property {Function} getRemoveButton - returns remove button for a given array field
+ * @property {Function} getRemoveLastElementButton - returns remove button for last element in the arrayField
+ * @property {Function} getLastArrayItem - returns the element added to the array
+ * @property {Function} getArrayFieldValues - returns values of the arrayfield as an array
+ * @property {Function} isAddButtonDisabled - returns true if Add button is disabled, false if otherwise
+ */
+
+ /**
+ *
+ * @param {string} fieldName - name of the column
+ * @param {string} formNumber - form number
+ * @param {string} baseType - baseType of the Array
+ * @return {ArrayFieldContainerElement} arrayFieldContainer
+ *
+ */
+ this.getArrayFieldContainer = function(colName, formNumber, baseType){
+ formNumber = formNumber || 1;
+ const fieldName = `${formNumber}-${colName}`;
+
+ const elem = element(by.css(`.array-input-field-container-${fieldName}`));
+
+ elem.getAddNewElementContainer = function(){
+ return this.element(by.className("add-element-container"));
+ }
+
+ elem.getAddButton = function(){
+ const addNewContainer = this.getAddNewElementContainer()
+ return addNewContainer.element(by.className("add-button"))
+ }
+
+ elem.getRemoveButton = function () {
+ return this.element(by.css('.array-remove-button'));
+ }
+
+ elem.getRemoveLastElementButton = async function(){
+ try{
+ const buttons = this.all(by.css(".action-buttons .fa-trash"));
+
+ if(await buttons.count()){
+ return buttons.last()
+ }
+ return null
+
+ }catch (err){
+ return null
+ }
+ }
+
+ elem.getErrorMessageElement = function(){
+ return this.element(by.className("input-switch-error"));
+ }
+
+ elem.isAddButtonDisabled = async function(){
+ const addNewContainer = this.getAddNewElementContainer()
+ const addButton = await addNewContainer.element(by.css('.chaise-btn-sm.add-button'))
+ return !(await addButton.isEnabled());
+ }
+
+ switch(baseType){
+ case 'date':
+ case 'integer':
+ case 'number':
+ case 'text':
+ elem.getAddNewValueInputElement = function(){
+ const addNewContainer = this.getAddNewElementContainer()
+ return addNewContainer.element(by.className(" input-switch"))
+ }
+
+ elem.getLastArrayItem = function(){
+ return this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] input`)).last();
+ }
+
+ elem.getArrayFieldValues = async function(){
+ const arrElems = await this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] input`));
+ const extractedValues = []
+
+ for( let item of arrElems){
+ let val = await item.getAttribute('value')
+ extractedValues.push(/number|integer/.test(baseType) ? JSON.parse(val) : val)
+ }
+
+ return extractedValues.length ? extractedValues : null;
+ }
+
+ elem.getClearInputButton = function () {
+ const addNewContainer = this.getAddNewElementContainer();
+ return addNewContainer.element(by.className('remove-input-btn'));
+ }
+
+ break;
+ case 'boolean':
+ elem.getArrayFieldValues = async function(){
+ const arrElems = await this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] input`));
+ const extractedValues = []
+
+ for( let item of arrElems){
+ let val = await item.getAttribute('value')
+ extractedValues.push(JSON.parse(val))
+ }
+
+ return extractedValues.length ? extractedValues : null;
+ }
+ break;
+ case 'timestamp':
+ case 'timestamptz' :
+ elem.getAddNewValueInputElement = function(){
+ const addNewContainer = this.getAddNewElementContainer()
+ return [
+ addNewContainer.element(by.className("input-switch-date")).element(by.className(" input-switch")),
+ addNewContainer.element(by.className("input-switch-time")).element(by.className(" input-switch"))
+ ]
+ }
+
+ elem.getLastArrayItem = function(){
+ const dateInput = this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] .input-switch-date input`)).last();
+ const timeInput = this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] .input-switch-time input`)).last();
+
+ return [dateInput, timeInput];
+ }
+
+ elem.getArrayFieldValues = async function(){
+ const dateInputs = await this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] .input-switch-date input`));
+ const timeInputs = await this.all(by.css(`li [class*="${fieldName}-"][class$="-val"] .input-switch-time input`));
+
+ let dateTimeValues = []
+
+ for(let i =0; i< dateInputs.length; i++){
+ let dateVal = await dateInputs[i].getAttribute('value')
+ let timeVal = await timeInputs[i].getAttribute('value')
+ dateTimeValues.push([dateVal,timeVal])
+ }
+
+ return dateTimeValues.length ? dateTimeValues : null;
+ }
+
+ elem.getClearInputButton = function () {
+ const addNewContainer = this.getAddNewElementContainer();
+ return addNewContainer.element(by.className("date-time-clear-btn"));
+ }
+
+ break;
+ }
+ return elem;
+ };
+
+
};
var recordPage = function() {
diff --git a/test/e2e/utils/recordedit-helpers.js b/test/e2e/utils/recordedit-helpers.js
index f39b8b2f9..53a7fc54b 100644
--- a/test/e2e/utils/recordedit-helpers.js
+++ b/test/e2e/utils/recordedit-helpers.js
@@ -305,170 +305,273 @@ exports.testPresentationAndBasicValidation = function(tableParams, isEditMode) {
if (arrayCols.length > 0) {
describe("Array fields, ", function () {
- it ("should show textarea input with correct value.", function () {
- arrayCols.forEach(function(c) {
- const arrayTxtArea = chaisePage.recordEditPage.getTextAreaForAColumn(c.name, recordIndex+1);
- expect(arrayTxtArea.isDisplayed()).toBeTruthy(colError(c.name, "element not visible."));
- arrayTxtArea.column = c;
+ it("should show ArrayField input with correct value.", async function () {
+ for(let col of arrayCols){
+ let recordVals = getRecordValue(col.name)
- arrayDataTypeFields.push(arrayTxtArea);
- var value = getRecordValue(c.name);
+ if(!recordVals) continue;
- if (value != undefined) {
- if (c.name === "timestamptz_array") {
- arrayTxtArea.getAttribute('value').then(function (inputValue) {
- var parts = inputValue.split('"');
- inputValue = parts[0] + '"' + moment(parts[1], "YYYY-MM-DDTHH:mm:ssZ").format("YYYY-MM-DDTHH:mm:ssZ") + '"' + parts[2];
- expect(inputValue).toBe(value, colError(c.name , "Doesn't have the expected value."));
- })
- } else {
- expect(arrayTxtArea.getAttribute('value')).toBe(value, colError(c.name , "Doesn't have the expected value."));
- }
- }
- });
- });
+ // Check if ArrayField is rendered correctly
+ const arrayField = chaisePage.recordEditPage.getArrayFieldContainer(col.name, recordIndex+1, col.baseType);
+ let arrayFieldVals = await arrayField.getArrayFieldValues();
- // test the invalid values once
- if (recordIndex === 0) {
- var invalidArrayValues = {
- "timestamp": [
- {
- "value": "2001-01-01T01:01:01",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[\"2001-01-01T01:01:01\", \"a\"]",
- "error": "`a` is not a valid timestamp value."
- }
- ],
- "timestamptz": [
- {
- "value": "2001-01-01T01:01:01-08:00",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[\"2001-01-01T01:01:01-08:00\", \"a\"]",
- "error": "`a` is not a valid timestamp with timezone value."
- }
- ],
- "date": [
- {
- "value": "2001-01-01",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[\"2001-01-01\", \"a\"]",
- "error": "`a` is not a valid date value."
- }
- ],
- "integer": [
- {
- "value": "[123",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[1, \"a\"]",
- "error": "`a` is not a valid integer value."
- }
- ],
- "number": [
- {
- "value": "1.1",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[1, \"a\"]",
- "error": "`a` is not a valid number value."
- }
- ],
- "boolean": [
- {
- "value": "true",
- "error": "Please enter a valid array structure."
- },
- {
- "value": "[true, \"a\"]",
- "error": "`a` is not a valid boolean value."
- }
- ],
- "text": [
- {
- "value": "\"test\"",
- "error": "Please enter a valid array structure e.g. [\"value1\", \"value2\"]"
- },
- {
- "value": "[1, \"a\"]",
- "error": "`1` is not a valid text value."
- }
- ]
- };
+ arrayField.click()
+ expect(arrayField.isDisplayed()).toBeTruthy(colError(col.name, "element not visible"));
- it ("should validate invalid array input.", function () {
- arrayDataTypeFields.forEach(function(arrayInput) {
- var c = arrayInput.column;
+ const addNewValField = arrayField.getAddNewElementContainer()
- if (c.generated || c.immutable) return;
+ expect(addNewValField.isDisplayed()).toBeTruthy(colError(col.name, 'add new value field not visible'));
- // store the original value if in edit mode.
- var prevValue;
- if (tableParams.primary_keys.indexOf(c.name) != -1) {
- arrayInput.getAttribute("value").then(function(value) {
- prevValue = value + "";
- });
- }
- chaisePage.recordEditPage.clearInput(arrayInput);
-
- // test the required input
- if (c.nullok == false) {
- chaisePage.recordEditPage.submitForm();
- const errMessageSelector = chaisePage.recordEditPage.getInputErrorMessage(arrayInput, 'required');
- expect(errMessageSelector.isDisplayed()).toBeTruthy(colError(c.name , "Expected to show required error."));
- chaisePage.recordEditPage.getAlertErrorClose().click();
- }
+ expect(arrayFieldVals.length).toBe(recordVals.length, colError(col.name , "Doesn't have the expected values."))
- // test invalid values
- var testValues = invalidArrayValues[c.baseType];
- testValues.forEach(function (tv) {
- c._value = tv.value;
- chaisePage.recordEditPage.clearInput(arrayInput).then(function () {
- return arrayInput.sendKeys(tv.value);
- }).then(function () {
- return chaisePage.recordEditPage.getArrayInputErrorMessage(arrayInput).getText();
- }).then(function (text) {
- expect(text).toBe(tv.error, colError(c.name, "error missmatch for following value: " + tv.value));
- }).catch(function () {
- expect(true).toBe(false, colError(c.name, "failed while trying to test values."));
- })
- });
+ if(/timestamp|timestamptz/.test(col.baseType) ){
- // Clear value
- chaisePage.recordEditPage.clearInput(arrayInput);
- expect(arrayInput.getAttribute('value')).toBe("", colError(c.name, "Expected to not clear the input."));
+ for(let i = 0; i < recordVals.length; i++){
+ let testDateValue = recordVals[i].slice(0, 10)
+ let testTimeValue = recordVals[i].slice(11, 19)
- //Restore the value to the original one or a valid input
- if (prevValue) {
- arrayInput.sendKeys(prevValue);
- expect(arrayInput.getAttribute('value')).toBe(validNo, colError(c.name, "Couldn't change the value."));
+ expect(testDateValue).toBe(arrayFieldVals[i][0])
+ expect(testTimeValue).toBe(arrayFieldVals[i][1])
+ }
+ }else{
+
+ for(let i = 0; i < recordVals.length; i++){
+ expect(arrayFieldVals[i]).toBe(recordVals[i])
+ }
+ }
+ }
+ });
+
+ // test the invalid values once
+ if (recordIndex === 0) {
+ let invalidArrayValues = {
+ "time":
+ {
+ "value": "200113",
+ "error": "Please enter a valid time value in 24-hr HH:MM:SS format."
+ },
+ "date":
+ {
+ "value": "200113-01",
+ "error": "Please enter a valid date value in YYYY-MM-DD format."
+ }
+ ,
+ "integer":
+ {
+ "value": "1.23",
+ "error": "Please enter a valid integer value."
+ }
+ ,
+ "number":
+ {
+ "value": "1.1h",
+ "error": "Please enter a valid decimal value."
+ }
+ ,
+ "boolean":
+ {
+ "value": "true",
+ "error": "Please enter a valid array structure."
+ }
+ ,
+ "text":
+ {
+ "value": "\"test\"",
+ "error": "Please enter a valid array structure e.g. [\"value1\", \"value2\"]"
+ }
+
+ };
+
+ let validArrayValues = {
+ "time": new Date().toString().split(' ')[4],
+ "date": new Date().toISOString().slice(0, 10),
+ "integer":"235",
+ "number":"1.2556",
+ "text": "sample text"
+ };
+
+ it ("should validate invalid array input.", async function(){
+ for(let col of arrayCols){
+
+ if (col.skipValidation) continue;
+
+ if (col.generated || col.immutable) continue;
+
+ const arrayField = chaisePage.recordEditPage.getArrayFieldContainer(col.name, recordIndex + 1, col.baseType);
+
+ const addNewValField = arrayField.getAddNewElementContainer()
+ expect(addNewValField.isDisplayed()).toBeTruthy(colError(col.name, 'add new value field not visible'));
+
+ // Ensure Add button is disbled when input has no/null value
+ expect(arrayField.isAddButtonDisabled()).toBe(true);
+
+ let errorElement,clearInput;
+ switch (col.baseType) {
+ case 'date':
+ case 'integer':
+ case 'number':
+ let addNewValInput;
+ addNewValInput = arrayField.getAddNewValueInputElement();
+ clearInput = await arrayField.getClearInputButton()
+
+ await addNewValInput.sendKeys(invalidArrayValues[col.baseType].value)
+
+ errorElement = await arrayField.getErrorMessageElement()
+
+ expect(errorElement.getText()).toBe(invalidArrayValues[col.baseType].error)
+
+ // clear input after test
+ await clearInput.click()
+
+
+ break;
+ case 'text':
+
+ let addInput = await arrayField.getAddNewValueInputElement();
+
+ if(col?.nullok === false){
+ await addInput.sendKeys(validArrayValues[col.baseType])
+ const addButton = await arrayField.getAddButton()
+ await addButton.click();
+
+ let deleteButton = await arrayField.getRemoveLastElementButton();
+
+
+ await expect(deleteButton.isDisplayed()).toBeTruthy(colError(col.name, 'Adding sample value to array failed'));
+
+
+ while(deleteButton !== null){
+ await deleteButton.click()
+ deleteButton = await arrayField.getRemoveLastElementButton();
}
- });
- });
- }
- it ("should be able to set the correct value.", function () {
- arrayDataTypeFields.forEach(function(inp) {
- var c = inp.column;
+ let err = arrayField.getErrorMessageElement();
+ expect(err.getText()).toBe("Please enter a value for this Array field")
- if (c.generated || c.immutable) return;
+ }
+ break;
+ case 'timestamp':
+ case 'timestamptz':
+ let addNewValDateInput, addNewValTimeInput;
- chaisePage.recordEditPage.clearInput(inp);
- browser.sleep(10);
+ [addNewValDateInput, addNewValTimeInput] = await arrayField.getAddNewValueInputElement()
+ clearInput = await arrayField.getClearInputButton();
- var text = getRecordInput(c.name, "[]");
- inp.sendKeys(text);
+ // Input invalid Date
+ await addNewValDateInput.sendKeys(protractor.Key.BACK_SPACE,"200113-01");
- expect(inp.getAttribute('value')).toEqual(text, colError(c.name, "Couldn't change the value."));
- });
- });
+ errorElement = await arrayField.getErrorMessageElement()
+
+ expect(errorElement.getText()).toBe(invalidArrayValues["date"].error)
+
+ // Clear DateTime field Values
+ await clearInput.click();
+
+ // Input valid date and invalid time
+ await addNewValDateInput.sendKeys(protractor.Key.BACK_SPACE,validArrayValues["date"]);
+ await addNewValTimeInput.sendKeys("11111");
+
+ errorElement = await arrayField.getErrorMessageElement()
+
+ expect(errorElement.getText()).toBe(invalidArrayValues["time"].error)
+
+ // Clear Input after test
+ await clearInput.click()
+ break;
+ }
+
+ }
+
+ })
+ }
+
+ it ("should be able to set the correct value.", async function () {
+
+ for(let col of arrayCols){
+ if (col.generated || col.immutable) continue;
+
+ const arrayField = chaisePage.recordEditPage.getArrayFieldContainer(col.name, recordIndex + 1, col.baseType);
+
+ const addNewValField = arrayField.getAddNewElementContainer();
+ expect(await addNewValField.isDisplayed()).toBeTruthy(colError(col.name, 'add new value field not visible'));
+ const valuesToAdd = getRecordInput(col.name);
+ if (valuesToAdd === null) continue;
+
+ // make sure the input doesn't have any values before setting the new one
+ const removeValBtn = arrayField.getRemoveButton();
+ while (await removeValBtn.isPresent()) {
+ await removeValBtn.click();
+ }
+
+ let addButton;
+ switch (col.baseType) {
+ case 'date':
+ case 'integer':
+ case 'number':
+ case 'text':
+ const addNewValInput = arrayField.getAddNewValueInputElement();
+
+ for(let value of valuesToAdd){
+ await addNewValInput.sendKeys(value);
+ addButton = arrayField.getAddButton()
+ await addButton.click();
+ }
+
+ const valuesRendered = await arrayField.getArrayFieldValues();
+ expect(valuesRendered.length).toBe(valuesToAdd.length);
+ for(let i = 0;i < valuesToAdd.length;i++){
+ expect(valuesToAdd[i]).toBe(valuesRendered[i]);
+ }
+
+ break;
+ case 'boolean':
+ const addNewValueDropDown = await chaisePage.recordEditPage.getDropdownElementByName(`${col.name}-new-item`, recordIndex + 1);
+ addButton = await arrayField.getAddButton();
+
+ expect(addNewValueDropDown.isDisplayed()).toBeTruthy();
+
+ for(let value of valuesToAdd){
+ await chaisePage.recordEditPage.selectDropdownValue(addNewValueDropDown, value);
+ await addButton.click()
+ }
+
+ const boolsRendered = await arrayField.getArrayFieldValues();
+ await expect(boolsRendered.length).toBe(valuesToAdd.length);
+ for(let i = 0;i < valuesToAdd.length;i++){
+ expect(valuesToAdd[i]).toBe(boolsRendered[i]);
+ }
+ break;
+ case 'timestamp':
+ case 'timestamptz':
+ let addNewValDateInput, addNewValTimeInput;
+
+ [addNewValDateInput, addNewValTimeInput] = arrayField.getAddNewValueInputElement();
+
+ for(let timeStamp of valuesToAdd){
+
+ let dateValue = timeStamp.slice(0, 10)
+ let timeValue = timeStamp.slice(11, 19)
+
+ // Input Valid Date and Time
+ await addNewValDateInput.sendKeys(protractor.Key.BACK_SPACE,dateValue);
+ await addNewValTimeInput.sendKeys(timeValue)
+
+ addButton = arrayField.getAddButton();
+ await addButton.click();
+ }
+
+ const timeStampsRendered = await arrayField.getArrayFieldValues()
+ expect(timeStampsRendered.length).toBe(valuesToAdd.length);
+ for(let i = 0;i < valuesToAdd.length;i++){
+ let dateValue = valuesToAdd[i].slice(0, 10);
+ let timeValue = valuesToAdd[i].slice(11, 19);
+ expect(dateValue).toBe(timeStampsRendered[i][0]);
+ expect(timeValue).toBe(timeStampsRendered[i][1]);
+ }
+ break;
+ }
+ }
+ })
});
}
@@ -1992,6 +2095,9 @@ exports.testFileInput = function (colName, recordIndex, file, currentValue, prin
* modal_num_rows,
* modal_option_index,
*
+ * // array props (currently only supports array of text):
+ * value: string[]
+ *
* }
*
* @param {string} name input name
@@ -2073,6 +2179,35 @@ exports.setInputValue = (formNumber, name, displayname, displayType, valueProps)
exports.selectFileReturnPromise(valueProps.value, fileInput, fileTextInput).then(() => {
resolve();
}).catch(err => reject(err));
+ break;
+ case 'array':
+ // NOTE we're assuming it's array of text
+ const arrayField = chaisePage.recordEditPage.getArrayFieldContainer(name, formNumber,valueProps.baseType);
+
+ arrayField.getArrayItem().isPresent().then((isPresent)=>{
+
+ if(isPresent){
+
+ const arrayFieldItem = arrayField.getArrayItem();
+
+ arrayFieldItem.sendKeys(
+ protractor.Key.chord(protractor.Key.CONTROL, 'a'),
+ protractor.Key.BACK_SPACE,
+ valueProps.value
+ ).then(()=>{
+ resolve();
+ })
+
+ }else{
+
+ arrayField.getAddNewValueInputElement().sendKeys(valueProps.value).then(()=>{
+ return arrayField.getAddButton().click()
+ }).then(()=>{
+ resolve();
+ });
+ }
+ })
+
break;
default:
let inputEl;
@@ -2148,6 +2283,20 @@ exports.testFormValuesForAColumn = (name, displayname, displayType, allDisabled,
expect(inputControl.getAttribute('class')).toContain('input-disabled', `${message}: was not disabled.`);
}
expect(input.getText()).toBe(value, `${message}: value missmatch.`);
+ break;
+ case 'array':
+
+ const arrayField = chaisePage.recordEditPage.getArrayFieldContainer(name, formNumber, 'text');
+
+ arrayField.getArrayItem().isPresent().then((isPresent)=>{
+ if(isPresent){
+ const arrItem = arrayField.getArrayItem();
+ expect(arrItem.getAttribute('value')).toBe(value)
+ }else{
+ expect(value).toBe('')
+ }
+ })
+
break;
default:
const isTextArea = displayType === 'textarea';
diff --git a/test/e2e/utils/recordedit-utils.ts b/test/e2e/utils/recordedit-utils.ts
index 6b0d8c685..51edbd8f7 100644
--- a/test/e2e/utils/recordedit-utils.ts
+++ b/test/e2e/utils/recordedit-utils.ts
@@ -119,7 +119,9 @@ type SetInputValueProps = string | RecordeditFile | {
/**
*
- * expected types: 'timestamp', 'boolean', 'fk', 'fk-dropdown', any other string
+ * expected types: 'timestamp', 'boolean', 'fk', 'fk-dropdown', 'array' , or any other string
+ *
+ * NOTE: 'array' only supports array of texts for now.
*
* expected valueProps:
* {
@@ -140,7 +142,8 @@ type SetInputValueProps = string | RecordeditFile | {
* @returns
*/
export const setInputValue = async (
- page: Page, formNumber: number, name: string, displayname: string, inputType: RecordeditInputType, valueProps: SetInputValueProps
+ page: Page, formNumber: number, name: string, displayname: string, inputType: RecordeditInputType,
+ valueProps: SetInputValueProps | SetInputValueProps[]
) => {
switch (inputType) {
case RecordeditInputType.BOOLEAN:
@@ -197,6 +200,23 @@ export const setInputValue = async (
await selectFile(valueProps, fileInputBtn, fileTextInput);
break;
+ case RecordeditInputType.ARRAY:
+ if (!Array.isArray(valueProps)) return;
+ const elems = RecordeditLocators.getArrayFieldElements(page, name, formNumber, 'text');
+
+ // remove the existing value if there are any
+ while (await elems.removeItemButtons.count() > 0) {
+ await elems.removeItemButtons.nth(0).click();
+ }
+
+ // add the values one by one.
+ for (const val of valueProps) {
+ if (typeof val !== 'string') continue;
+ await elems.addItemInput.fill(val);
+ await elems.addItemButton.click();
+ }
+ break;
+
default:
if (typeof valueProps !== 'string') return;
@@ -219,16 +239,20 @@ export const setInputValue = async (
* expectedValues expected type will be different depending on the input type. for all the types expect the following
* it should be an array of strings.
* - timestamp: array of objects with date_value and time_value props
+ * - array: array of array of texts.
+ *
+ * NOTE: 'array' only supports array of texts for now.
*
* @param {string} name the column name
- * @param {string}} displayname the column displayname
+ * @param {string}}displayname the column displayname
* @param {string} displayType the display type (boolean, fk, timestamp, upload, "any other string")
* @param {boolean} allDisabled whether we should test that all the inputs are disabled or not
* @param {any[]} expectedValues the expected values
* @returns
*/
export const testFormValuesForAColumn = async (
- page: Page, name: string, displayname: string, inputType: RecordeditInputType, allDisabled: boolean, expectedValues: SetInputValueProps[]
+ page: Page, name: string, displayname: string, inputType: RecordeditInputType, allDisabled: boolean,
+ expectedValues: (SetInputValueProps |SetInputValueProps[])[]
) => {
let formNumber = 1, input;
@@ -276,6 +300,23 @@ export const testFormValuesForAColumn = async (
await expect.soft(input).toHaveText(value);
break;
+ case RecordeditInputType.ARRAY:
+ if (!Array.isArray(value)) return;
+ const elems = RecordeditLocators.getArrayFieldElements(page, name, formNumber, 'text');
+
+ let index = 0;
+ for (const val of value) {
+ if (typeof val !== 'string') continue;
+ input = elems.inputs.nth(index);
+ if (allDisabled) {
+ await expect.soft(input).toBeDisabled();
+ }
+ await expect.soft(input).toHaveValue(val);
+ index++;
+ }
+
+ break;
+
default:
if (typeof value !== 'string') return;