PromucFlow_constructor/app/client/src/utils/WidgetLoadingStateUtils.test.ts
Apeksha Bhosale 2b25e1e9b0
fix: Improving performance of JS evaluations by splitting the data tree (#21547)
## Description
This is the second phase of the split data tree. In the previous version, we collected all config paths in each entity and put them in the `__config__` property. All those config properties do get inserted into final data tree which we don't need at all. 
As part of this change, we will be creating another tree i.e **'configTree'**  which will contain all config of each entity. 

unEvalTree is split into 2 trees => 
1. unEvalTree 
2.  configTree

Example: 
previous unEvalTree Api1 content 
<img width="1766" alt="image" src="https://user-images.githubusercontent.com/7846888/215990868-0b095421-e7b8-44bc-89aa-065b35e237d6.png">


After this change
unEvalTree Api1 content
<img width="1758" alt="image" src="https://user-images.githubusercontent.com/7846888/215991045-506fb10a-645a-4aad-8e77-0f3786a86977.png">
Note- above example doesn't have '__config__' property

configTree Api1 content 
<img width="1760" alt="image" src="https://user-images.githubusercontent.com/7846888/215991169-a2e03443-5d6a-4ff1-97c5-a12593e46395.png">


## Type of change
- Chore (housekeeping or task changes that don't impact user perception)
- #11351


## How Has This Been Tested?
- Manual
- Jest
- Cypress

### Test Plan
> Add Testsmith test cases links that relate to this PR

### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR)


## Checklist:
### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or manual QA
- [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
2023-03-20 16:34:02 +05:30

523 lines
15 KiB
TypeScript

import { PluginType } from "entities/Action";
import type { WidgetEntity } from "entities/DataTree/dataTreeFactory";
import type { ActionEntity, JSActionEntity } from "entities/DataTree/types";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import {
findLoadingEntities,
getEntityDependantPaths,
groupAndFilterDependantsMap,
} from "utils/WidgetLoadingStateUtils";
import WidgetFactory from "./WidgetFactory";
const JS_object_tree: JSActionEntity = {
pluginType: PluginType.JS,
name: "",
ENTITY_TYPE: ENTITY_TYPE.JSACTION,
body: "",
meta: {},
dynamicBindingPathList: [],
bindingPaths: {},
reactivePaths: {},
variables: [],
dependencyMap: {},
actionId: "",
};
// @ts-expect-error: meta property not provided
const Select_tree: WidgetEntity = {
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
widgetId: "",
type: "",
widgetName: "",
renderMode: "CANVAS",
version: 0,
parentColumnSpace: 0,
parentRowSpace: 0,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
isLoading: false,
animateLoading: true,
};
const Query_tree: ActionEntity = {
data: {},
actionId: "",
config: {},
run: {},
clear: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
datasourceUrl: "",
responseMeta: {
isExecutionSuccess: true,
},
isLoading: false,
};
const Api_tree: ActionEntity = {
data: {},
actionId: "",
config: {},
run: {},
clear: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
datasourceUrl: "",
responseMeta: {
isExecutionSuccess: true,
},
isLoading: false,
};
const Table_tree: WidgetEntity = {
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
widgetId: "",
type: "TABLE_WIDGET",
widgetName: "",
renderMode: "CANVAS",
version: 0,
parentColumnSpace: 0,
parentRowSpace: 0,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
isLoading: false,
animateLoading: true,
meta: {},
};
const baseDataTree = {
JS_file: { ...JS_object_tree, name: "JS_file" },
Select1: { ...Select_tree, name: "Select1" },
Select2: { ...Select_tree, name: "Select2" },
Select3: { ...Select_tree, name: "Select3" },
Table1: { ...Table_tree, name: "Table1" },
Query1: { ...Query_tree, name: "Query1" },
Query2: { ...Query_tree, name: "Query2" },
Query3: { ...Query_tree, name: "Query3" },
Api1: { ...Api_tree, name: "Api1" },
};
describe("Widget loading state utils", () => {
describe("findLoadingEntites", () => {
// Select1.options -> JS_file.func1 -> Query1.data
// Select2.options -> JS_file.func2 -> Query2.data
// JS_file.func3 -> Query3.data
const baseInverseMap = {
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.data": ["JS_file.func1", "Query1"],
"Query2.config": ["Query2"],
"Query2.config.body": ["Query2.config"],
"Query2.data": ["JS_file.func2", "Query2"],
"Query3.config": ["Query3"],
"Query3.config.body": ["Query3.config"],
"Query3.data": ["JS_file.func3"],
"JS_file.func1": ["Select1.options"],
"JS_file.func2": ["Select2.options"],
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
"Select2.options": [
"Select2.selectedOptionValue",
"Select2.selectedOptionLabel",
"Select2",
],
"Select3.options": [
"Select3.selectedOptionValue",
"Select3.selectedOptionLabel",
"Select3",
],
};
beforeAll(() => {
// mock WidgetFactory.getLoadingProperties
const loadingPropertiesMap = new Map<string, RegExp[]>();
loadingPropertiesMap.set("TABLE_WIDGET", [/.tableData$/]);
jest
.spyOn(WidgetFactory, "getLoadingProperties")
.mockImplementation((widgetType) =>
loadingPropertiesMap.get(widgetType),
);
});
afterAll(() => {
jest.restoreAllMocks();
});
// Select1.options -> JS_file.func1 -> Query1.data
it("handles linear dependencies", () => {
const loadingEntites = findLoadingEntities(
["Query1"],
baseDataTree,
baseInverseMap,
);
expect(loadingEntites).toStrictEqual(new Set(["Select1"]));
});
// Select1.options -> JS_file.func1 -> Query1.data
// Select2.options -> JS_file.func2 -> Query2.data
// Select3.options -> none
it("handles multiple dependencies", () => {
const loadingEntites = findLoadingEntities(
["Query1", "Query2", "Query3"],
baseDataTree,
baseInverseMap,
);
expect(loadingEntites).toStrictEqual(new Set(["Select1", "Select2"]));
});
// none -> Query3.data
it("handles no dependencies", () => {
const loadingEntites = findLoadingEntities(
["Query3"],
baseDataTree,
baseInverseMap,
);
expect(loadingEntites).toStrictEqual(new Set([]));
});
// JS_file.func1 -> Query1.run
// Select1.options -> Query1.data
it("handles Query.run and Query.data dependency", () => {
const loadingEntites = findLoadingEntities(["Query1"], baseDataTree, {
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.run": ["JS_file.func1"],
"Query1.data": ["Select1.options", "Query1"],
"JS_file.func1": [],
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
});
expect(loadingEntites).toStrictEqual(new Set(["Select1"]));
});
// Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data
it("handles nested JS dependencies within same file", () => {
const loadingEntites = findLoadingEntities(["Query1"], baseDataTree, {
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.data": ["JS_file.internalFunc", "Query1"],
"JS_file.internalFunc": ["JS_file.func1"],
"JS_file.func1": ["Select1.options"],
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
});
expect(loadingEntites).toStrictEqual(new Set(["Select1"]));
});
// Select1.options -> JS_file1.func1 -> JS_file2.internalFunc -> Query1.data
it("handles nested JS dependencies between files", () => {
const loadingEntites = findLoadingEntities(
["Query1"],
{
...baseDataTree,
JS_file1: { ...JS_object_tree, name: "JS_file1" },
JS_file2: { ...JS_object_tree, name: "JS_file2" },
},
{
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.data": ["JS_file2.internalFunc", "Query1"],
"JS_file2.internalFunc": ["JS_file1.func1"],
"JS_file1.func1": ["Select1.options"],
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
},
);
expect(loadingEntites).toStrictEqual(new Set(["Select1"]));
});
/* Select1.options -> JS.func1 -> Query1.data,
Select2.options -> Query2.data,
JS.func2 -> Query2.run
When Query2 is called.
Only Select2 should be listed, not Select1.
*/
it("handles selective dependencies in same JS file", () => {
const loadingEntites = findLoadingEntities(["Query2"], baseDataTree, {
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.data": ["JS_file.func1"],
"Query2.config": ["Query2"],
"Query2.config.body": ["Query2.config"],
"Query2.data": ["JS_file.func2"],
"JS_file.func1": ["Select1.options"],
"JS_file.func2": ["Select2.options"],
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
"Select2.options": [
"Select2.selectedOptionValue",
"Select2.selectedOptionLabel",
"Select2",
],
});
expect(loadingEntites).toStrictEqual(new Set(["Select2"]));
});
it("includes loading properties", () => {
const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, {
"Api1.data": ["Table1.tableData"],
});
expect(loadingEntites).toStrictEqual(new Set(["Table1"]));
});
it("ignores non-loading properties", () => {
const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, {
"Api1.run": ["Table1.primaryColumns.action.onClick"],
});
expect(loadingEntites).toStrictEqual(new Set());
});
});
describe("groupAndFilterDependantsMap", () => {
it("groups entities and filters self-dependencies", () => {
const groupedDependantsMap = groupAndFilterDependantsMap(
{
"Query1.config": ["Query1"],
"Query1.config.body": ["Query1.config"],
"Query1.data": ["JS_file.func1", "Query1"], // dependant
"Query2.config": ["Query2"],
"Query2.config.body": ["Query2.config"],
"Query2.run": ["Query2", "JS_file.func2"], // dependant
"Query2.data": ["Query2", "Select2.options"], // dependant
"Query3.config": ["Query3"],
"Query3.config.body": ["Query3.config"],
"JS_file.func1": ["Select1.options"], // dependant
"Select1.options": [
"Select1.selectedOptionValue",
"Select1.selectedOptionLabel",
"Select1",
],
"Select2.options": [
"Select2.selectedOptionValue",
"Select2.selectedOptionLabel",
"Select2",
],
},
baseDataTree,
);
expect(groupedDependantsMap).toStrictEqual({
Query1: { "Query1.data": ["JS_file.func1"] },
Query2: {
"Query2.run": ["JS_file.func2"],
"Query2.data": ["Select2.options"],
},
JS_file: {
"JS_file.func1": ["Select1.options"],
},
});
});
it("includes JS object's self dependencies", () => {
const groupedDependantsMap = groupAndFilterDependantsMap(
{
"JS_file.func1": ["Select1.options"], // dependant
"JS_file.internalFunc": ["JS_file.func1"], // self-dependant JsObject
},
baseDataTree,
);
expect(groupedDependantsMap).toStrictEqual({
JS_file: {
"JS_file.func1": ["Select1.options"],
"JS_file.internalFunc": ["JS_file.func1"],
},
});
});
it("includes JS object's nested self dependencies", () => {
const groupedDependantsMap = groupAndFilterDependantsMap(
{
"JS_file.func1": ["Select1.options"], // dependant
"JS_file.internalFunc2": ["JS_file.func1"], // self-dependant JsObject
"JS_file.internalFunc1": ["JS_file.internalFunc2"], // self-dependant JsObject
},
baseDataTree,
);
expect(groupedDependantsMap).toStrictEqual({
JS_file: {
"JS_file.func1": ["Select1.options"],
"JS_file.internalFunc2": ["JS_file.func1"],
"JS_file.internalFunc1": ["JS_file.internalFunc2"],
},
});
});
});
describe("getEntityDependantPaths", () => {
// Select1.options -> JS_file.func1 -> Query1.data
it("handles simple dependency", () => {
const dependants = getEntityDependantPaths(
["Query1"],
{
Query1: {
"Query1.data": ["JS_file.func1"],
},
JS_file: {
"JS_file.func1": ["Select1.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(
new Set(["JS_file.func1", "Select1.options"]),
);
});
// Select1.options -> JS_file.func1 -> Query1.data
// Select2.options -> JS_file.func2 -> Query1.data
it("handles multiple dependencies", () => {
const dependants = getEntityDependantPaths(
["Query1"],
{
Query1: {
"Query1.data": ["JS_file.func1", "JS_file.func2"],
},
JS_file: {
"JS_file.func1": ["Select1.options"],
"JS_file.func2": ["Select2.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(
new Set([
"JS_file.func1",
"Select1.options",
"JS_file.func2",
"Select2.options",
]),
);
});
it("handles specific entity paths", () => {
const dependants = getEntityDependantPaths(
["JS_file.func2"], // specific path
{
Query1: {
"Query1.data": ["JS_file.func1"],
},
Query2: {
"Query2.data": ["JS_file.func2"],
},
JS_file: {
"JS_file.func1": ["Select1.options"],
"JS_file.func2": ["Select2.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(new Set(["Select2.options"]));
});
// Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data
it("handles JS self-dependencies", () => {
const dependants = getEntityDependantPaths(
["Query1"],
{
Query1: {
"Query1.data": ["JS_file.internalFunc"],
},
JS_file: {
"JS_file.internalFunc": ["JS_file.func1"],
"JS_file.func1": ["Select1.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(
new Set(["JS_file.internalFunc", "JS_file.func1", "Select1.options"]),
);
});
// Select1.options -> JS_file.func -> JS_file.internalFunc1 -> JS_file.internalFunc2 -> Query1.data
it("handles nested JS self-dependencies", () => {
const dependants = getEntityDependantPaths(
["Query1"],
{
Query1: {
"Query1.data": ["JS_file.internalFunc2"],
},
JS_file: {
"JS_file.internalFunc2": ["JS_file.internalFunc1"],
"JS_file.internalFunc1": ["JS_file.func"],
"JS_file.func": ["Select1.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(
new Set([
"JS_file.internalFunc1",
"JS_file.internalFunc2",
"JS_file.func",
"Select1.options",
]),
);
});
/* Select1.options -> JS.func1 -> Query1.data,
Select2.options -> Query2.data,
JS.func2 -> Query2.run
When Query2 is called.
Only Select2 should be listed, not Select1.
*/
it("handles selective dependencies in same JS file", () => {
const dependants = getEntityDependantPaths(
["Query2"],
{
Query1: {
"Query1.data": ["JS_file.func1"],
},
Query2: {
"Query2.data": ["JS_file.func2"],
},
JS_file: {
"JS_file.func1": ["Select1.options"],
"JS_file.func2": ["Select2.options"],
},
},
new Set<string>(),
);
expect(dependants).toStrictEqual(
new Set(["JS_file.func2", "Select2.options"]),
);
});
});
});