fix: show js function execution errors in debugger (#14555)
* Show js function execution error logs * remove unused function * improve check for async functions * clear errors for deleted jsActions * fix typescript error * modify js function execution error logging * test that execution parse errors are logged in the debugger * Add test to show that js execution errors are logged in the debugger * re-order js execution tests * Add type to jsObj variable * update cypress tests * update cypress test
This commit is contained in:
parent
703b0efda6
commit
41789c71bc
|
|
@ -10,7 +10,8 @@ const jsEditor = ObjectsRegistry.JSEditor,
|
|||
|
||||
let onPageLoadAndConfirmExecuteFunctionsLength: number,
|
||||
getJSObject: any,
|
||||
functionsLength: number, jsObj: any;
|
||||
functionsLength: number,
|
||||
jsObj: string;
|
||||
|
||||
describe("JS Function Execution", function() {
|
||||
interface IFunctionSettingData {
|
||||
|
|
@ -50,6 +51,22 @@ describe("JS Function Execution", function() {
|
|||
ee.DragDropWidgetNVerify("tablewidget", 300, 300);
|
||||
ee.NavigateToSwitcher("explorer");
|
||||
});
|
||||
function assertAsyncFunctionsOrder(data: IFunctionSettingData[]) {
|
||||
// sorts functions alphabetically
|
||||
const sortFunctions = (data: IFunctionSettingData[]) =>
|
||||
data.sort((a, b) => a.name.localeCompare(b.name));
|
||||
cy.get(jsEditor._asyncJSFunctionSettings).then(function($lis) {
|
||||
const asyncFunctionLength = $lis.length;
|
||||
// Assert number of async functions
|
||||
expect(asyncFunctionLength).to.equal(functionsLength);
|
||||
Object.values(sortFunctions(data)).forEach((functionSetting, idx) => {
|
||||
// Assert alphabetical order
|
||||
expect($lis.eq(idx)).to.have.id(
|
||||
jsEditor._getJSFunctionSettingsId(functionSetting.name),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("1. Allows execution of js function when lint warnings(not errors) are present in code", function() {
|
||||
jsEditor.CreateJSObject(
|
||||
|
|
@ -170,8 +187,75 @@ describe("JS Function Execution", function() {
|
|||
assertInvalidJSObjectStart(jsObjectStartingWithANewLine, jsObjectStartLine);
|
||||
assertInvalidJSObjectStart(jsObjectStartingWithASpace, jsObjectStartLine);
|
||||
});
|
||||
it("5. Verify that js function execution errors are logged in debugger and removed when function is deleted", () => {
|
||||
const JS_OBJECT_WITH_PARSE_ERROR = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
myFun1: () => {
|
||||
//write code here
|
||||
return Table1.unknown.name
|
||||
}
|
||||
}`;
|
||||
|
||||
it("5. Supports the use of large JSON data (doesn't crash)", () => {
|
||||
const JS_OBJECT_WITHOUT_PARSE_ERROR = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
myFun1: () => {
|
||||
//write code here
|
||||
return Table1.unknown
|
||||
}
|
||||
}`;
|
||||
|
||||
const JS_OBJECT_WITH_DELETED_FUNCTION = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {}
|
||||
}`;
|
||||
|
||||
// Create js object
|
||||
jsEditor.CreateJSObject(JS_OBJECT_WITH_PARSE_ERROR, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: true,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
// Assert that there is a function execution parse error
|
||||
jsEditor.AssertParseError(true, true);
|
||||
// click the debug icon
|
||||
agHelper.GetNClick(jsEditor._debugCTA);
|
||||
// Assert that errors tab is not empty
|
||||
cy.contains("No signs of trouble here!").should("not.exist");
|
||||
// Assert presence of typeError
|
||||
cy.contains(
|
||||
"TypeError: Cannot read properties of undefined (reading 'name')",
|
||||
).should("exist");
|
||||
|
||||
// Fix parse error and assert that debugger error is removed
|
||||
jsEditor.EditJSObj(JS_OBJECT_WITHOUT_PARSE_ERROR);
|
||||
agHelper.GetNClick(jsEditor._runButton);
|
||||
jsEditor.AssertParseError(false, true);
|
||||
agHelper.GetNClick(locator._errorTab);
|
||||
cy.contains(
|
||||
"TypeError: Cannot read properties of undefined (reading 'name')",
|
||||
).should("not.exist");
|
||||
|
||||
// Switch back to response tab
|
||||
agHelper.GetNClick(locator._responseTab);
|
||||
// Re-introduce parse errors
|
||||
jsEditor.EditJSObj(JS_OBJECT_WITH_PARSE_ERROR);
|
||||
agHelper.GetNClick(jsEditor._runButton);
|
||||
// Assert that there is a function execution parse error
|
||||
jsEditor.AssertParseError(true, true);
|
||||
|
||||
// Delete function
|
||||
jsEditor.EditJSObj(JS_OBJECT_WITH_DELETED_FUNCTION);
|
||||
// Assert that parse error is removed from debugger when function is deleted
|
||||
agHelper.GetNClick(locator._errorTab);
|
||||
cy.contains(
|
||||
"TypeError: Cannot read properties of undefined (reading 'name')",
|
||||
).should("not.exist");
|
||||
});
|
||||
it("6. Supports the use of large JSON data (doesn't crash)", () => {
|
||||
const jsObjectWithLargeJSONData = `export default{
|
||||
largeData: ${JSON.stringify(largeJSONData)},
|
||||
myfun1: ()=> this.largeData
|
||||
|
|
@ -211,7 +295,7 @@ describe("JS Function Execution", function() {
|
|||
});
|
||||
});
|
||||
|
||||
it("6. Doesn't cause cyclic dependency when function name is edited", () => {
|
||||
it("7. Doesn't cause cyclic dependency when function name is edited", () => {
|
||||
const syncJSCode = `export default {
|
||||
myFun1 :()=>{
|
||||
return "yes"
|
||||
|
|
@ -275,8 +359,7 @@ describe("JS Function Execution", function() {
|
|||
jsEditor.EditJSObj(asyncJSCodeWithRenamedFunction2);
|
||||
agHelper.AssertElementAbsence(locator._toastMsg);
|
||||
});
|
||||
|
||||
it("7. Maintains order of async functions in settings tab alphabetically at all times", function() {
|
||||
it("8. Maintains order of async functions in settings tab alphabetically at all times", function() {
|
||||
functionsLength = FUNCTIONS_SETTINGS_DEFAULT_DATA.length;
|
||||
// Number of functions set to run on page load and should also confirm before execute
|
||||
onPageLoadAndConfirmExecuteFunctionsLength = FUNCTIONS_SETTINGS_DEFAULT_DATA.filter(
|
||||
|
|
@ -341,7 +424,7 @@ describe("JS Function Execution", function() {
|
|||
assertAsyncFunctionsOrder(FUNCTIONS_SETTINGS_DEFAULT_DATA);
|
||||
});
|
||||
|
||||
it("8. Verify Asyn methods alphabetical order after clone page and after rename", () => {
|
||||
it("9. Verify Async methods have alphabetical order after cloning page and renaming it", () => {
|
||||
const FUNCTIONS_SETTINGS_RENAMED_DATA: IFunctionSettingData[] = [
|
||||
{
|
||||
name: "newGetId",
|
||||
|
|
@ -379,7 +462,7 @@ describe("JS Function Execution", function() {
|
|||
agHelper.Sleep();
|
||||
}
|
||||
|
||||
ee.SelectEntityByName(jsObj as string, "QUERIES/JS");
|
||||
ee.SelectEntityByName(jsObj, "QUERIES/JS");
|
||||
|
||||
agHelper.GetNClick(jsEditor._settingsTab);
|
||||
assertAsyncFunctionsOrder(FUNCTIONS_SETTINGS_DEFAULT_DATA);
|
||||
|
|
@ -391,21 +474,4 @@ describe("JS Function Execution", function() {
|
|||
agHelper.GetNClick(jsEditor._settingsTab);
|
||||
assertAsyncFunctionsOrder(FUNCTIONS_SETTINGS_RENAMED_DATA);
|
||||
});
|
||||
|
||||
function assertAsyncFunctionsOrder(data: IFunctionSettingData[]) {
|
||||
// sorts functions alphabetically
|
||||
const sortFunctions = (data: IFunctionSettingData[]) =>
|
||||
data.sort((a, b) => a.name.localeCompare(b.name));
|
||||
cy.get(jsEditor._asyncJSFunctionSettings).then(function($lis) {
|
||||
const asyncFunctionLength = $lis.length;
|
||||
// Assert number of async functions
|
||||
expect(asyncFunctionLength).to.equal(functionsLength);
|
||||
Object.values(sortFunctions(data)).forEach((functionSetting, idx) => {
|
||||
// Assert alphabetical order
|
||||
expect($lis.eq(idx)).to.have.id(
|
||||
jsEditor._getJSFunctionSettingsId(functionSetting.name),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export class CommonLocators {
|
|||
_uploadBtn = "button.uppy-StatusBar-actionBtn--upload"
|
||||
_debuggerIcon = ".t--debugger svg"
|
||||
_errorTab = "[data-cy=t--tab-ERROR]"
|
||||
_responseTab = "[data-cy=t--tab-response]"
|
||||
_debugErrorMsg = ".t--debugger-message"
|
||||
_debuggerLabel = "span.debugger-label"
|
||||
_modal = ".t--modal-widget"
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export class JSEditor {
|
|||
_getJSFunctionSettingsId = (JSFunctionName: string) =>
|
||||
`${JSFunctionName}-settings`;
|
||||
_asyncJSFunctionSettings = `.t--async-js-function-settings`;
|
||||
_debugCTA = `button.js-editor-debug-cta`;
|
||||
//#endregion
|
||||
|
||||
//#region constants
|
||||
|
|
|
|||
|
|
@ -199,7 +199,6 @@ function JSResponseView(props: Props) {
|
|||
});
|
||||
dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setResponseStatus(
|
||||
getJSResponseViewState(
|
||||
|
|
@ -213,7 +212,7 @@ function JSResponseView(props: Props) {
|
|||
}, [responses, isExecuting, currentFunction, isSaving, isDirty]);
|
||||
const tabs = [
|
||||
{
|
||||
key: "body",
|
||||
key: "response",
|
||||
title: "Response",
|
||||
panelComponent: (
|
||||
<>
|
||||
|
|
@ -229,7 +228,10 @@ function JSResponseView(props: Props) {
|
|||
fill
|
||||
label={
|
||||
<FailedMessage>
|
||||
<DebugButton onClick={onDebugClick} />
|
||||
<DebugButton
|
||||
className="js-editor-debug-cta"
|
||||
onClick={onDebugClick}
|
||||
/>
|
||||
</FailedMessage>
|
||||
}
|
||||
text={
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export enum ENTITY_TYPE {
|
|||
|
||||
export enum PLATFORM_ERROR {
|
||||
PLUGIN_EXECUTION = "PLUGIN_EXECUTION",
|
||||
JS_FUNCTION_EXECUTION = "JS_FUNCTION_EXECUTION",
|
||||
}
|
||||
|
||||
export type ErrorType = PropertyEvaluationErrorType | PLATFORM_ERROR;
|
||||
|
|
|
|||
|
|
@ -314,6 +314,7 @@ function* logDebuggerErrorAnalyticsSaga(
|
|||
getJSCollection,
|
||||
payload.entityId,
|
||||
);
|
||||
if (!action) return;
|
||||
const plugin: Plugin = yield select(getPlugin, action.pluginId);
|
||||
const pluginName = plugin?.name?.replace(/ /g, "");
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
} from "actions/evaluationActions";
|
||||
import {
|
||||
evalErrorHandler,
|
||||
handleJSFunctionExecutionErrorLog,
|
||||
logSuccessfulBindings,
|
||||
postEvalActionDispatcher,
|
||||
updateTernDefinitions,
|
||||
|
|
@ -350,7 +351,11 @@ export function* clearEvalCache() {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function* executeFunction(collectionName: string, action: JSAction) {
|
||||
export function* executeFunction(
|
||||
collectionName: string,
|
||||
action: JSAction,
|
||||
collectionId: string,
|
||||
) {
|
||||
const functionCall = `${collectionName}.${action.name}()`;
|
||||
const { isAsync } = action.actionConfiguration;
|
||||
let response: {
|
||||
|
|
@ -381,7 +386,13 @@ export function* executeFunction(collectionName: string, action: JSAction) {
|
|||
const { errors, result } = response;
|
||||
const isDirty = !!errors.length;
|
||||
|
||||
yield call(evalErrorHandler, errors);
|
||||
yield call(
|
||||
handleJSFunctionExecutionErrorLog,
|
||||
collectionId,
|
||||
collectionName,
|
||||
action,
|
||||
errors,
|
||||
);
|
||||
return { result, isDirty };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,6 +270,10 @@ function* updateJSCollection(data: {
|
|||
jsCollection,
|
||||
createMessage(JS_FUNCTION_DELETE_SUCCESS),
|
||||
);
|
||||
// delete all execution error logs for deletedActions if present
|
||||
deletedActions.forEach((action) =>
|
||||
AppsmithConsole.deleteError(`${jsCollection.id}-${action.id}`),
|
||||
);
|
||||
}
|
||||
|
||||
yield put(
|
||||
|
|
@ -353,6 +357,7 @@ export function* handleExecuteJSFunctionSaga(data: {
|
|||
executeFunction,
|
||||
collectionName,
|
||||
action,
|
||||
collectionId,
|
||||
);
|
||||
yield put({
|
||||
type: ReduxActionTypes.EXECUTE_JS_FUNCTION_SUCCESS,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { ENTITY_TYPE, Log, Severity } from "entities/AppsmithConsole";
|
||||
import {
|
||||
ENTITY_TYPE,
|
||||
Log,
|
||||
PLATFORM_ERROR,
|
||||
Severity,
|
||||
} from "entities/AppsmithConsole";
|
||||
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import {
|
||||
DataTreeDiff,
|
||||
|
|
@ -31,6 +36,7 @@ import {
|
|||
ERROR_EVAL_ERROR_GENERIC,
|
||||
JS_OBJECT_BODY_INVALID,
|
||||
VALUE_IS_INVALID,
|
||||
JS_EXECUTION_FAILURE,
|
||||
} from "@appsmith/constants/messages";
|
||||
import log from "loglevel";
|
||||
import { AppState } from "reducers";
|
||||
|
|
@ -40,6 +46,7 @@ import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreato
|
|||
import TernServer from "utils/autocomplete/TernServer";
|
||||
import { selectFeatureFlags } from "selectors/usersSelectors";
|
||||
import FeatureFlags from "entities/FeatureFlags";
|
||||
import { JSAction } from "entities/JSCollection";
|
||||
|
||||
const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
|
||||
/**
|
||||
|
|
@ -392,3 +399,31 @@ export function* updateTernDefinitions(
|
|||
log.debug("Tern definitions updated took ", (end - start).toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleJSFunctionExecutionErrorLog(
|
||||
collectionId: string,
|
||||
collectionName: string,
|
||||
action: JSAction,
|
||||
errors: any[],
|
||||
) {
|
||||
errors.length
|
||||
? AppsmithConsole.addError({
|
||||
id: `${collectionId}-${action.id}`,
|
||||
logType: LOG_TYPE.EVAL_ERROR,
|
||||
text: `${createMessage(JS_EXECUTION_FAILURE)}: ${collectionName}.${
|
||||
action.name
|
||||
}`,
|
||||
messages: errors.map((error) => ({
|
||||
message: error.errorMessage || error.message,
|
||||
type: PLATFORM_ERROR.JS_FUNCTION_EXECUTION,
|
||||
subType: error.errorType,
|
||||
})),
|
||||
source: {
|
||||
id: action.id,
|
||||
name: `${collectionName}.${action.name}`,
|
||||
type: ENTITY_TYPE.JSACTION,
|
||||
propertyPath: `${collectionName}.${action.name}`,
|
||||
},
|
||||
})
|
||||
: AppsmithConsole.deleteError(`${collectionId}-${action.id}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,12 +404,17 @@ export function isFunctionAsync(
|
|||
});
|
||||
try {
|
||||
if (typeof userFunction === "function") {
|
||||
const returnValue = userFunction();
|
||||
if (!!returnValue && returnValue instanceof Promise) {
|
||||
self.IS_ASYNC = true;
|
||||
}
|
||||
if (self.TRIGGER_COLLECTOR.length) {
|
||||
if (userFunction.constructor.name === "AsyncFunction") {
|
||||
// functions declared with an async keyword
|
||||
self.IS_ASYNC = true;
|
||||
} else {
|
||||
const returnValue = userFunction();
|
||||
if (!!returnValue && returnValue instanceof Promise) {
|
||||
self.IS_ASYNC = true;
|
||||
}
|
||||
if (self.TRIGGER_COLLECTOR.length) {
|
||||
self.IS_ASYNC = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function messageEventListener(
|
|||
errors: [
|
||||
{
|
||||
type: EvalErrorTypes.CLONE_ERROR,
|
||||
message: e,
|
||||
message: (e as Error)?.message,
|
||||
context: JSON.stringify(rest),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user