PromucFlow_constructor/app/client/src/utils/WidgetLoadingStateUtils.test.ts
Ilia d6f249b42d
chore: add blank line eslint rule (#36369)
## Description
Added ESLint rule to force blank lines between statements. 


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!CAUTION]
> 🔴 🔴 🔴 Some tests have failed.
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/10924926728>
> Commit: 34f57714a1575ee04e94e03cbcaf95e57a96c86c
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10924926728&attempt=1&selectiontype=test&testsstatus=failed&specsstatus=fail"
target="_blank">Cypress dashboard</a>.
> Tags: @tag.All
> Spec: 
> The following are new failures, please fix them before merging the PR:
<ol>
> <li>cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilButtonWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilCheckboxGroupWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilCurrencyInputWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilIconButtonWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilInlineButtonWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilInputWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilParagraphWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilPhoneInputWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilStatsWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilSwitchGroupWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilSwitchWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilTableWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilToolbarButtonWidgetSnapshot_spec.ts
>
<li>cypress/e2e/Regression/ClientSide/Anvil/Widgets/AnvilZoneSectionWidgetSnapshot_spec.ts</ol>
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master"
target="_blank">List of identified flaky tests</a>.
> <hr>Wed, 18 Sep 2024 16:33:36 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No

---------

Co-authored-by: Valera Melnikov <valera@appsmith.com>
2024-09-18 19:35:28 +03:00

545 lines
15 KiB
TypeScript

import { PluginType } from "entities/Action";
import type {
WidgetEntity,
ActionEntity,
JSActionEntity,
} from "ee/entities/DataTree/types";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import {
findLoadingEntities,
getEntityDependantPaths,
groupAndFilterDependantsMap,
} from "utils/WidgetLoadingStateUtils";
import WidgetFactory from "../WidgetProvider/factory";
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"]),
);
});
});
});