PromucFlow_constructor/app/client/src/workers/evaluation.test.ts
Hetu Nandu 4387200262
Sub tree evaluation (#1841)
Co-authored-by: Trisha Anand <trisha@appsmith.com>
Co-authored-by: Piyush Mishra <piyush@codeitout.com>
Co-authored-by: Nikhil Nandagopal <nikhil@appsmith.com>
Co-authored-by: Akash N <akash@codemonk.in>
2021-01-04 15:46:08 +05:30

768 lines
21 KiB
TypeScript

import { DataTreeEvaluator } from "./evaluation.worker";
import {
DataTreeAction,
DataTreeWidget,
ENTITY_TYPE,
} from "../entities/DataTree/dataTreeFactory";
import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { RenderModes, WidgetTypes } from "../constants/WidgetConstants";
import { PluginType } from "../entities/Action";
const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = {
CONTAINER_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
TEXT_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
text: "TEXT",
textStyle: "TEXT",
shouldScroll: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {
value: "{{ this.text }}",
},
triggerProperties: {},
metaProperties: {},
},
BUTTON_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
text: "TEXT",
buttonStyle: "TEXT",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {
onClick: true,
},
metaProperties: {},
},
INPUT_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
inputType: "TEXT",
defaultText: "TEXT",
text: "TEXT",
regex: "REGEX",
errorMessage: "TEXT",
placeholderText: "TEXT",
maxChars: "NUMBER",
minNum: "NUMBER",
maxNum: "NUMBER",
label: "TEXT",
inputValidators: "ARRAY",
focusIndex: "NUMBER",
isAutoFocusEnabled: "BOOLEAN",
isRequired: "BOOLEAN",
isValid: "BOOLEAN",
},
defaultProperties: {
text: "defaultText",
},
derivedProperties: {
isValid:
'{{\n function(){\n let parsedRegex = null;\n if (this.regex) {\n /*\n * break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags\n * Example /test/i will be split into ["/test/gi", "/", "test", "gi"]\n */\n const regexParts = this.regex.match(/(\\/?)(.+)\\1([a-z]*)/i);\n if (!regexParts) {\n parsedRegex = new RegExp(this.regex);\n } else {\n /*\n * if we don\'t have a regex flags (gmisuy), convert provided string into regexp directly\n /*\n if (regexParts[3] && !/^(?!.*?(.).*?\\1)[gmisuy]+$/.test(regexParts[3])) {\n parsedRegex = RegExp(this.regex);\n }\n /*\n * if we have a regex flags, use it to form regexp\n */\n parsedRegex = new RegExp(regexParts[2], regexParts[3]);\n }\n }\n if (this.inputType === "EMAIL") {\n const emailRegex = new RegExp(/^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$/);\n return emailRegex.test(this.text);\n }\n else if (this.inputType === "NUMBER") {\n return !isNaN(this.text)\n }\n else if (this.isRequired) {\n if(this.text && this.text.length) {\n if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n } else {\n return false;\n }\n } if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n }()\n }}',
value: "{{this.text}}",
},
triggerProperties: {
onTextChanged: true,
},
metaProperties: {
isFocused: false,
isDirty: false,
},
},
CHECKBOX_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
label: "TEXT",
defaultCheckedState: "BOOLEAN",
},
defaultProperties: {
isChecked: "defaultCheckedState",
},
derivedProperties: {
value: "{{this.isChecked}}",
},
triggerProperties: {
onCheckChange: true,
},
metaProperties: {},
},
DROP_DOWN_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
placeholderText: "TEXT",
label: "TEXT",
options: "OPTIONS_DATA",
selectionType: "TEXT",
isRequired: "BOOLEAN",
selectedOptionValues: "ARRAY",
defaultOptionValue: "DEFAULT_OPTION_VALUE",
},
defaultProperties: {
selectedOptionValue: "defaultOptionValue",
selectedOptionValueArr: "defaultOptionValue",
},
derivedProperties: {
isValid:
"{{this.isRequired ? this.selectionType === 'SINGLE_SELECT' ? !!this.selectedOption : !!this.selectedIndexArr && this.selectedIndexArr.length > 0 : true}}",
selectedOption:
"{{ this.selectionType === 'SINGLE_SELECT' ? _.find(this.options, { value: this.selectedOptionValue }) : undefined}}",
selectedOptionArr:
'{{this.selectionType === "MULTI_SELECT" ? this.options.filter(opt => _.includes(this.selectedOptionValueArr, opt.value)) : undefined}}',
selectedIndex:
"{{ _.findIndex(this.options, { value: this.selectedOption.value } ) }}",
selectedIndexArr:
"{{ this.selectedOptionValueArr.map(o => _.findIndex(this.options, { value: o })) }}",
value:
"{{ this.selectionType === 'SINGLE_SELECT' ? this.selectedOptionValue : this.selectedOptionValueArr }}",
selectedOptionValues: "{{ this.selectedOptionValueArr }}",
},
triggerProperties: {
onOptionChange: true,
},
metaProperties: {},
},
RADIO_GROUP_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
label: "TEXT",
options: "OPTIONS_DATA",
selectedOptionValue: "TEXT",
defaultOptionValue: "TEXT",
isRequired: "BOOLEAN",
},
defaultProperties: {
selectedOptionValue: "defaultOptionValue",
},
derivedProperties: {
selectedOption:
"{{_.find(this.options, { value: this.selectedOptionValue })}}",
isValid: "{{ this.isRequired ? !!this.selectedOptionValue : true }}",
value: "{{this.selectedOptionValue}}",
},
triggerProperties: {
onSelectionChange: true,
},
metaProperties: {},
},
IMAGE_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
image: "TEXT",
imageShape: "TEXT",
defaultImage: "TEXT",
maxZoomLevel: "NUMBER",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {
onClick: true,
},
metaProperties: {},
},
TABLE_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
tableData: "TABLE_DATA",
nextPageKey: "TEXT",
prevPageKey: "TEXT",
label: "TEXT",
searchText: "TEXT",
defaultSearchText: "TEXT",
defaultSelectedRow: "DEFAULT_SELECTED_ROW",
},
defaultProperties: {
searchText: "defaultSearchText",
selectedRowIndex: "defaultSelectedRow",
selectedRowIndices: "defaultSelectedRow",
},
derivedProperties: {
selectedRow: `{{ _.get(this.filteredTableData, this.selectedRowIndex, _.mapValues(this.filteredTableData[0], () => undefined)) }}`,
selectedRows: `{{ this.filteredTableData.filter((item, i) => selectedRowIndices.includes(i) }); }}`,
},
triggerProperties: {
onRowSelected: true,
onPageChange: true,
onSearchTextChanged: true,
columnActions: true,
},
metaProperties: {
pageNo: 1,
selectedRow: {},
selectedRows: [],
},
},
VIDEO_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
url: "TEXT",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {
onEnd: true,
onPlay: true,
onPause: true,
},
metaProperties: {
playState: "NOT_STARTED",
},
},
FILE_PICKER_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
label: "TEXT",
maxNumFiles: "NUMBER",
allowedFileTypes: "ARRAY",
files: "ARRAY",
isRequired: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {
isValid: "{{ this.isRequired ? this.files.length > 0 : true }}",
value: "{{this.files}}",
},
triggerProperties: {
onFilesSelected: true,
},
metaProperties: {
files: [],
uploadedFileData: {},
},
},
DATE_PICKER_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
defaultDate: "DATE",
timezone: "TEXT",
enableTimePicker: "BOOLEAN",
dateFormat: "TEXT",
label: "TEXT",
datePickerType: "TEXT",
maxDate: "DATE",
minDate: "DATE",
isRequired: "BOOLEAN",
},
defaultProperties: {
selectedDate: "defaultDate",
},
derivedProperties: {
isValid: "{{ this.isRequired ? !!this.selectedDate : true }}",
value: "{{ this.selectedDate }}",
},
triggerProperties: {
onDateSelected: true,
},
metaProperties: {},
},
TABS_WIDGET: {
validations: {
tabs: "TABS_DATA",
defaultTab: "SELECTED_TAB",
},
defaultProperties: {},
derivedProperties: {
selectedTab:
"{{_.find(this.tabs, { widgetId: this.selectedTabWidgetId }).label}}",
},
triggerProperties: {
onTabSelected: true,
},
metaProperties: {},
},
MODAL_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
RICH_TEXT_EDITOR_WIDGET: {
validations: {
text: "TEXT",
placeholder: "TEXT",
defaultValue: "TEXT",
isDisabled: "BOOLEAN",
isVisible: "BOOLEAN",
},
defaultProperties: {
text: "defaultText",
},
derivedProperties: {
value: "{{this.text}}",
},
triggerProperties: {
onTextChange: true,
},
metaProperties: {},
},
CHART_WIDGET: {
validations: {
xAxisName: "TEXT",
yAxisName: "TEXT",
chartName: "TEXT",
isVisible: "BOOLEAN",
chartData: "CHART_DATA",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
FORM_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
FORM_BUTTON_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
text: "TEXT",
disabledWhenInvalid: "BOOLEAN",
buttonStyle: "TEXT",
buttonType: "TEXT",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {
onClick: true,
},
metaProperties: {},
},
MAP_WIDGET: {
validations: {
defaultMarkers: "MARKERS",
isDisabled: "BOOLEAN",
isVisible: "BOOLEAN",
enableSearch: "BOOLEAN",
enablePickLocation: "BOOLEAN",
allowZoom: "BOOLEAN",
zoomLevel: "NUMBER",
mapCenter: "OBJECT",
},
defaultProperties: {
markers: "defaultMarkers",
center: "mapCenter",
},
derivedProperties: {},
triggerProperties: {
onMarkerClick: true,
onCreateMarker: true,
},
metaProperties: {},
},
CANVAS_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
ICON_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {
onClick: true,
},
metaProperties: {},
},
SKELETON_WIDGET: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
},
defaultProperties: {},
derivedProperties: {},
triggerProperties: {},
metaProperties: {},
},
};
const BASE_WIDGET: DataTreeWidget = {
widgetId: "randomID",
widgetName: "randomName",
bottomRow: 0,
isLoading: false,
leftColumn: 0,
parentColumnSpace: 0,
parentRowSpace: 0,
renderMode: RenderModes.CANVAS,
rightColumn: 0,
topRow: 0,
type: WidgetTypes.SKELETON_WIDGET,
parentId: "0",
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
};
const BASE_ACTION: DataTreeAction = {
actionId: "randomId",
name: "randomName",
config: {
timeoutInMillisecond: 10,
},
dynamicBindingPathList: [],
isLoading: false,
pluginType: PluginType.API,
run: {},
data: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
};
describe("DataTreeEvaluator", () => {
const unEvalTree = {
Text1: {
...BASE_WIDGET,
widgetName: "Text1",
text: "Label",
type: WidgetTypes.TEXT_WIDGET,
},
Text2: {
...BASE_WIDGET,
widgetName: "Text2",
text: "{{Text1.text}}",
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
},
Text3: {
...BASE_WIDGET,
widgetName: "Text3",
text: "{{Text1.text}}",
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
},
Dropdown1: {
...BASE_WIDGET,
options: [
{
label: "test",
value: "valueTest",
},
{
label: "test2",
value: "valueTest2",
},
],
type: WidgetTypes.DROP_DOWN_WIDGET,
},
Table1: {
...BASE_WIDGET,
tableData: "{{Api1.data.map(datum => ({ ...datum, raw: Text1.text }) )}}",
dynamicBindingPathList: [{ key: "tableData" }],
type: WidgetTypes.TABLE_WIDGET,
},
Text4: {
...BASE_WIDGET,
text: "{{Table1.selectedRow.test}}",
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
},
};
const evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP);
evaluator.createFirstTree(unEvalTree);
it("Evaluates a binding in first run", () => {
const evaluation = evaluator.evalTree;
const dependencyMap = evaluator.dependencyMap;
expect(evaluation).toHaveProperty("Text2.text", "Label");
expect(evaluation).toHaveProperty("Text3.text", "Label");
expect(dependencyMap).toStrictEqual({
Text1: ["Text1.text"],
Text2: ["Text2.text"],
Text3: ["Text3.text"],
Text4: ["Text4.text"],
Table1: [
"Table1.tableData",
"Table1.searchText",
"Table1.selectedRowIndex",
"Table1.selectedRowIndices",
],
Dropdown1: [
"Dropdown1.selectedOptionValue",
"Dropdown1.selectedOptionValueArr",
],
"Text2.text": ["Text1.text"],
"Text3.text": ["Text1.text"],
"Dropdown1.selectedOptionValue": [],
"Dropdown1.selectedOptionValueArr": [],
"Table1.tableData": ["Text1.text"],
"Table1.searchText": [],
"Table1.selectedRowIndex": [],
"Table1.selectedRowIndices": [],
"Text4.text": [],
});
});
it("Evaluates a value change in update run", () => {
const updatedUnEvalTree = {
...unEvalTree,
Text1: {
...unEvalTree.Text1,
text: "Hey there",
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there");
expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there");
});
it("Evaluates a dependency change in update run", () => {
const updatedUnEvalTree = {
...unEvalTree,
Text3: {
...unEvalTree.Text3,
text: "Label 3",
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Text2.text", "Label");
expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3");
expect(updatedDependencyMap).toStrictEqual({
Text1: ["Text1.text"],
Text2: ["Text2.text"],
Text3: ["Text3.text"],
Text4: ["Text4.text"],
Table1: [
"Table1.tableData",
"Table1.searchText",
"Table1.selectedRowIndex",
"Table1.selectedRowIndices",
],
Dropdown1: [
"Dropdown1.selectedOptionValue",
"Dropdown1.selectedOptionValueArr",
],
"Text2.text": ["Text1.text"],
"Dropdown1.selectedOptionValue": [],
"Dropdown1.selectedOptionValueArr": [],
"Table1.tableData": ["Text1.text"],
"Table1.searchText": [],
"Table1.selectedRowIndex": [],
"Table1.selectedRowIndices": [],
"Text4.text": [],
});
});
it("Overrides with default value", () => {
const updatedUnEvalTree = {
...unEvalTree,
Input1: {
...BASE_WIDGET,
text: undefined,
defaultText: "Default value",
widgetName: "Input1",
type: WidgetTypes.INPUT_WIDGET,
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value");
});
it("Evaluates for value changes in nested diff paths", () => {
const updatedUnEvalTree = {
...unEvalTree,
Dropdown1: {
...BASE_WIDGET,
options: [
{
label: "newValue",
value: "valueTest",
},
{
label: "test2",
value: "valueTest2",
},
],
type: WidgetTypes.DROP_DOWN_WIDGET,
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
expect(updatedEvalTree).toHaveProperty(
"Dropdown1.options.0.label",
"newValue",
);
});
it("Adds an entity with a complicated binding", () => {
const updatedUnEvalTree = {
...unEvalTree,
Api1: {
...BASE_ACTION,
data: [
{
test: "Hey",
},
{
test: "Ho",
},
],
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Table1.tableData", [
{
test: "Hey",
raw: "Label",
},
{
test: "Ho",
raw: "Label",
},
]);
expect(updatedDependencyMap).toStrictEqual({
Api1: ["Api1.data"],
Input1: ["Input1.text"],
Text1: ["Text1.text"],
Text2: ["Text2.text"],
Text3: ["Text3.text"],
Text4: ["Text4.text"],
Table1: [
"Table1.tableData",
"Table1.searchText",
"Table1.selectedRowIndex",
"Table1.selectedRowIndices",
],
Dropdown1: [
"Dropdown1.selectedOptionValue",
"Dropdown1.selectedOptionValueArr",
],
"Text2.text": ["Text1.text"],
"Text3.text": ["Text1.text"],
"Dropdown1.selectedOptionValue": [],
"Dropdown1.selectedOptionValueArr": [],
"Table1.tableData": ["Api1.data", "Text1.text"],
"Table1.searchText": [],
"Table1.selectedRowIndex": [],
"Table1.selectedRowIndices": [],
"Text4.text": [],
"Input1.text": [],
});
});
it("Selects a row", () => {
const updatedUnEvalTree = {
...unEvalTree,
Table1: {
...unEvalTree.Table1,
selectedRowIndex: 0,
selectedRow: {
test: "Hey",
raw: "Label",
},
},
Api1: {
...BASE_ACTION,
data: [
{
test: "Hey",
},
{
test: "Ho",
},
],
},
};
const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
expect(updatedEvalTree).toHaveProperty("Table1.tableData", [
{
test: "Hey",
raw: "Label",
},
{
test: "Ho",
raw: "Label",
},
]);
expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey");
expect(updatedDependencyMap).toStrictEqual({
Api1: ["Api1.data"],
Text1: ["Text1.text"],
Text2: ["Text2.text"],
Text3: ["Text3.text"],
Text4: ["Text4.text"],
Table1: [
"Table1.tableData",
"Table1.selectedRowIndex",
"Table1.searchText",
"Table1.selectedRowIndices",
"Table1.selectedRow",
],
"Table1.selectedRow": ["Table1.selectedRow.test"],
Dropdown1: [
"Dropdown1.selectedOptionValue",
"Dropdown1.selectedOptionValueArr",
],
Input1: ["Input1.text"],
"Text2.text": ["Text1.text"],
"Text3.text": ["Text1.text"],
"Dropdown1.selectedOptionValue": [],
"Dropdown1.selectedOptionValueArr": [],
"Table1.tableData": ["Api1.data", "Text1.text"],
"Table1.searchText": [],
"Table1.selectedRowIndex": [],
"Table1.selectedRowIndices": [],
"Text4.text": ["Table1.selectedRow.test"],
"Input1.text": [],
});
});
});