Fixes #23057 Fixes #23054 ## Description TL;DR Added support for peeking on nested properties. e.g. `Api1.data[0].id`. This won't work when: - local variables are involved in the expression. e.g. `Api1.data[x].id` won't support peeking at the variable `[x]` or anything after that. - library code is involved e.g. `moment`, `_` etc... - when functions are called. e.g. Api1.data[0].id.toFixed() Because these cases requires evaluation. <img width="355" alt="image" src="https://github.com/appsmithorg/appsmith/assets/66776129/d09d1f0d-1692-46f5-8ec1-592f4fe75f7a"> #### Media (old vs new) https://www.loom.com/share/dedcf113439c4ee2a19028acca54045e ## Performance improvements: - Use AST to identify expressions instead marking text manually. - This reduces the number of markers we process (~ half). - Before  - After  - AST logs https://www.loom.com/share/ddde93233cc8470ea04309d8a8332240 #### Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) ## Testing > #### How Has This Been Tested? - [x] Manual - [x] Jest - [x] Cypress > > #### Test Plan https://github.com/appsmithorg/TestSmith/issues/2402 #### Issues raised during DP testing https://github.com/appsmithorg/appsmith/pull/23414#issuecomment-1553164908 ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [x] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change) have been covered - [x] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [x] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [x] Cypress test cases have been added and approved by SDET/manual QA - [x] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
193 lines
5.4 KiB
TypeScript
193 lines
5.4 KiB
TypeScript
import { SourceType } from "../constants/ast";
|
|
import { PeekOverlayExpressionIdentifier } from "./index";
|
|
|
|
describe("extractExpressionAtPositionWholeDoc", () => {
|
|
const scriptIdentifier = new PeekOverlayExpressionIdentifier({
|
|
sourceType: SourceType.script,
|
|
});
|
|
|
|
const jsObjectIdentifier = new PeekOverlayExpressionIdentifier({
|
|
sourceType: SourceType.module,
|
|
thisExpressionReplacement: "JsObject",
|
|
});
|
|
|
|
const checkExpressionAtScript = async (
|
|
pos: number,
|
|
resultString?: string,
|
|
) => {
|
|
let result;
|
|
try {
|
|
result = await scriptIdentifier.extractExpressionAtPosition(pos);
|
|
} catch (e) {
|
|
expect(e).toBe(
|
|
"PeekOverlayExpressionIdentifier - No expression found at position",
|
|
);
|
|
}
|
|
expect(result).toBe(resultString);
|
|
};
|
|
|
|
const checkExpressionAtJsObject = async (
|
|
pos: number,
|
|
resultString?: string,
|
|
) => {
|
|
let result;
|
|
try {
|
|
result = await jsObjectIdentifier.extractExpressionAtPosition(pos);
|
|
} catch (e) {
|
|
expect(e).toBe(
|
|
"PeekOverlayExpressionIdentifier - No expression found at position",
|
|
);
|
|
}
|
|
expect(result).toBe(resultString);
|
|
};
|
|
|
|
it("handles MemberExpressions", async () => {
|
|
// nested properties
|
|
scriptIdentifier.updateScript("Api1.data[0].id");
|
|
// at position 'A'
|
|
// 'A'pi1.data[0].id
|
|
checkExpressionAtScript(0, "Api1");
|
|
// Ap'i'1.data[0].id
|
|
checkExpressionAtScript(2, "Api1");
|
|
// Api1.'d'ata[0].id
|
|
checkExpressionAtScript(6, "Api1.data");
|
|
// Api1.data['0'].id
|
|
checkExpressionAtScript(11, "Api1.data[0]");
|
|
// Api1.data[0].i'd'
|
|
checkExpressionAtScript(14, "Api1.data[0].id");
|
|
|
|
// function call
|
|
// argument hover - Api1
|
|
scriptIdentifier.updateScript(`storeValue("abc", Api1.run)`);
|
|
checkExpressionAtScript(18, "Api1");
|
|
scriptIdentifier.updateScript("Api1.check.run()");
|
|
// Ap'i'1.check.run()
|
|
checkExpressionAtScript(2, "Api1");
|
|
// Api1.check.'r'un()
|
|
checkExpressionAtScript(12, "Api1.check.run");
|
|
|
|
// local varibles are filtered
|
|
scriptIdentifier.updateScript("Api1.check.data[x].id");
|
|
// Api1
|
|
checkExpressionAtScript(2, "Api1");
|
|
// check
|
|
checkExpressionAtScript(7, "Api1.check");
|
|
// data
|
|
checkExpressionAtScript(12, "Api1.check.data");
|
|
// x
|
|
checkExpressionAtScript(16);
|
|
// id
|
|
checkExpressionAtScript(19);
|
|
});
|
|
|
|
it("handles ExpressionStatements", async () => {
|
|
// simple statement
|
|
scriptIdentifier.updateScript("Api1");
|
|
// Ap'i'1
|
|
checkExpressionAtScript(2, "Api1");
|
|
|
|
// function call
|
|
scriptIdentifier.updateScript(`storeValue("abc", 123)`);
|
|
// storeValue("a'b'c", 123)
|
|
checkExpressionAtScript(13);
|
|
// st'o'reValue("abc", 123) - functionality not supported now
|
|
checkExpressionAtScript(2);
|
|
|
|
// consequent function calls
|
|
scriptIdentifier.updateScript(`Api1.data[0].id.toFixed().toString()`);
|
|
// toFixed
|
|
checkExpressionAtScript(16, "Api1.data[0].id.toFixed");
|
|
// toString
|
|
checkExpressionAtScript(26);
|
|
|
|
// function call argument hover
|
|
scriptIdentifier.updateScript(`storeValue("abc", Api1)`);
|
|
checkExpressionAtScript(18, "Api1");
|
|
});
|
|
|
|
it("handles BinaryExpressions", async () => {
|
|
// binary expression
|
|
scriptIdentifier.updateScript(
|
|
`Api1.data.users[0].id === "myData test" ? "Yes" : "No"`,
|
|
);
|
|
|
|
// id
|
|
checkExpressionAtScript(19, "Api1.data.users[0].id");
|
|
// myData
|
|
checkExpressionAtScript(27);
|
|
// ?
|
|
checkExpressionAtScript(40);
|
|
// Yes
|
|
checkExpressionAtScript(43);
|
|
// :
|
|
checkExpressionAtScript(48);
|
|
// No
|
|
checkExpressionAtScript(51);
|
|
|
|
// hardcoded LHS
|
|
scriptIdentifier.updateScript(`"sample" === "myData test" ? "Yes" : "No"`);
|
|
// sample
|
|
checkExpressionAtScript(1);
|
|
|
|
// nested expressions
|
|
scriptIdentifier.updateScript(
|
|
`"sample" === "myData test" ? "nested" === "nested check" ? "Yes" : "No" : "No"`,
|
|
);
|
|
// nested
|
|
checkExpressionAtScript(31);
|
|
// nested check
|
|
checkExpressionAtScript(44);
|
|
// Yes
|
|
checkExpressionAtScript(61);
|
|
// No
|
|
checkExpressionAtScript(69);
|
|
});
|
|
|
|
it("handles JsObject cases", async () => {
|
|
jsObjectIdentifier.updateScript(JsObjectWithThisKeyword);
|
|
|
|
// this keyword cases
|
|
// this
|
|
checkExpressionAtJsObject(140, "JsObject");
|
|
// numArray
|
|
checkExpressionAtJsObject(159, "JsObject.numArray");
|
|
// objectArray
|
|
checkExpressionAtJsObject(180, "JsObject.objectArray");
|
|
// [0]
|
|
checkExpressionAtJsObject(183, "JsObject.objectArray[0]");
|
|
// x
|
|
checkExpressionAtJsObject(186, "JsObject.objectArray[0].x");
|
|
// 'x'
|
|
checkExpressionAtJsObject(208, "JsObject.objectData['x']");
|
|
// 'a'
|
|
checkExpressionAtJsObject(238, "JsObject.objectData['x']['a']");
|
|
// b
|
|
checkExpressionAtJsObject(243, "JsObject.objectData['x']['a'].b");
|
|
|
|
// await keyword cases
|
|
// resetWidget
|
|
checkExpressionAtJsObject(255);
|
|
// "Switch1"
|
|
checkExpressionAtJsObject(266);
|
|
// Api1
|
|
checkExpressionAtJsObject(287, "Api1");
|
|
// run
|
|
checkExpressionAtJsObject(292, "Api1.run");
|
|
});
|
|
});
|
|
|
|
const JsObjectWithThisKeyword = `export default {
|
|
numArray: [1, 2, 3],
|
|
objectArray: [ {x: 123}, { y: "123"} ],
|
|
objectData: { x: 123, y: "123" },
|
|
myFun1: async () => {
|
|
this;
|
|
this.numArray;
|
|
this.objectArray[0].x;
|
|
this.objectData["x"];
|
|
this.objectData["x"]["a"].b;
|
|
await resetWidget("Switch1");
|
|
await Api1.run();
|
|
},
|
|
}`;
|