feat: enabled setTimeout/clearTimeout APIs (#17445)
This commit is contained in:
parent
06f1b23625
commit
28138c18c8
|
|
@ -129,6 +129,25 @@ describe("Button Widget Functionality", function() {
|
||||||
cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
|
cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("5. Toggle JS - Button-Call-SetTimeout Validation", function() {
|
||||||
|
//creating a query and calling it from the onClickAction of the button widget.
|
||||||
|
// Creating a mock query
|
||||||
|
cy.testJsontext(
|
||||||
|
"onclick",
|
||||||
|
"{{setTimeout(() => showAlert('Hello from setTimeout after 3 seconds'), 3000)}}",
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.PublishtheApp();
|
||||||
|
|
||||||
|
// Clicking the button to verify the success message
|
||||||
|
cy.get(publishPage.buttonWidget).click();
|
||||||
|
cy.wait(3000);
|
||||||
|
cy.get(widgetsPage.apiCallToast).should(
|
||||||
|
"have.text",
|
||||||
|
"Hello from setTimeout after 3 seconds",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.goToEditFromPublish();
|
cy.goToEditFromPublish();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
|
||||||
|
const jsEditor = ObjectsRegistry.JSEditor;
|
||||||
|
const agHelper = ObjectsRegistry.AggregateHelper;
|
||||||
|
const locators = ObjectsRegistry.CommonLocators;
|
||||||
|
const apiPage = ObjectsRegistry.ApiPage;
|
||||||
|
const deployMode = ObjectsRegistry.DeployMode;
|
||||||
|
|
||||||
|
describe("Tests setTimeout API", function() {
|
||||||
|
it("Executes showAlert after 3 seconds and uses default value", () => {
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x = "default") => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showAlert("Hello world - " + x);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Hello world - default", "exist");
|
||||||
|
});
|
||||||
|
it("Executes all three alerts in parallel after 3 seconds", () => {
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x = "default") => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showAlert("Hello world - " + x);
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
myFun2: () => {
|
||||||
|
this.myFun1(1)
|
||||||
|
this.myFun1(2)
|
||||||
|
this.myFun1(3)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.SelectFunctionDropdown("myFun2");
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Hello world - 1", "exist");
|
||||||
|
agHelper.AssertContains("Hello world - 2", "exist");
|
||||||
|
agHelper.AssertContains("Hello world - 3", "exist");
|
||||||
|
});
|
||||||
|
it("Resolves promise after 3 seconds and shows alert", () => {
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x) => {
|
||||||
|
new Promise((res, rej) => setTimeout(() => res("resolved"), 3000)).then((res) => {
|
||||||
|
showAlert(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("resolved");
|
||||||
|
});
|
||||||
|
it("verifies code execution order when using setTimeout", () => {
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x) => {
|
||||||
|
console.log("Hey there!");
|
||||||
|
setTimeout(() => console.log("Working!"), 3000);
|
||||||
|
console.log("Bye!");
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
agHelper.GetNClick(locators._debuggerIcon);
|
||||||
|
agHelper.GetNClick(jsEditor._logsTab);
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.GetNAssertContains(locators._debuggerLogMessage, "Hey there!");
|
||||||
|
agHelper.GetNAssertContains(locators._debuggerLogMessage, "Bye!");
|
||||||
|
agHelper.GetNAssertContains(
|
||||||
|
locators._debuggerLogMessage,
|
||||||
|
"Working!",
|
||||||
|
"not.exist",
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.GetNAssertContains(locators._debuggerLogMessage, "Working!");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Resolves promise after 3 seconds and shows alert", () => {
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x) => {
|
||||||
|
new Promise((res, rej) => setTimeout(() => res("resolved"), 3000)).then((res) => {
|
||||||
|
showAlert(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("resolved");
|
||||||
|
});
|
||||||
|
it("Access to args passed into success/error callback functions in API.run when using setTimeout", () => {
|
||||||
|
apiPage.CreateAndFillApi("https://mock-api.appsmith.com/users");
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x) => {
|
||||||
|
Api1.run((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showAlert(res.users[0].name);
|
||||||
|
}, 3000);
|
||||||
|
}, (error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
myFun2: (x) => {
|
||||||
|
Api1.run().then((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showAlert(res.users[0].name);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jsEditor.RenameJSObjFromPane("Timeouts");
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Barty Crouch");
|
||||||
|
agHelper.Sleep(2000);
|
||||||
|
jsEditor.SelectFunctionDropdown("myFun2");
|
||||||
|
jsEditor.RunJSObj();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Barty Crouch");
|
||||||
|
});
|
||||||
|
it("Verifies whether setTimeout executes on page load", () => {
|
||||||
|
apiPage.CreateAndFillApi("https://mock-api.appsmith.com/users");
|
||||||
|
jsEditor.CreateJSObject(
|
||||||
|
`export default {
|
||||||
|
myVar1: [],
|
||||||
|
myVar2: {},
|
||||||
|
myFun1: (x) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Api1.run().then(() => showAlert("Success!"));
|
||||||
|
Timeouts.myFun2();
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
prettify: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jsEditor.EnableDisableAsyncFuncSettings("myFun1", true, false);
|
||||||
|
deployMode.DeployApp();
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Success!");
|
||||||
|
agHelper.Sleep(3000);
|
||||||
|
agHelper.AssertContains("Barty Crouch");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -893,8 +893,9 @@ export class AggregateHelper {
|
||||||
selector: ElementType,
|
selector: ElementType,
|
||||||
text: string | RegExp,
|
text: string | RegExp,
|
||||||
exists: "exist" | "not.exist" = "exist",
|
exists: "exist" | "not.exist" = "exist",
|
||||||
|
timeout?: number,
|
||||||
) {
|
) {
|
||||||
return this.GetElement(selector)
|
return this.GetElement(selector, timeout)
|
||||||
.contains(text)
|
.contains(text)
|
||||||
.should(exists);
|
.should(exists);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,5 +241,15 @@
|
||||||
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/values"
|
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/values"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"setTimeout": {
|
||||||
|
"!type": "fn(f: fn(), ms: number) -> number",
|
||||||
|
"!url": "https://developer.mozilla.org/en/docs/DOM/window.setTimeout",
|
||||||
|
"!doc": "Calls a function or executes a code snippet after specified delay."
|
||||||
|
},
|
||||||
|
"clearTimeout": {
|
||||||
|
"!type": "fn(timeout: number)",
|
||||||
|
"!url": "https://developer.mozilla.org/en/docs/DOM/window.clearTimeout",
|
||||||
|
"!doc": "Clears the delay set by window.setTimeout()."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,6 +273,7 @@ export function* evaluateAndExecuteDynamicTrigger(
|
||||||
callbackData,
|
callbackData,
|
||||||
globalContext,
|
globalContext,
|
||||||
eventType,
|
eventType,
|
||||||
|
triggerMeta,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -353,6 +354,18 @@ export function* executeDynamicTriggerRequest(
|
||||||
mainThreadRequestChannel,
|
mainThreadRequestChannel,
|
||||||
);
|
);
|
||||||
log.debug({ requestData });
|
log.debug({ requestData });
|
||||||
|
if (requestData?.logs) {
|
||||||
|
const { eventType, triggerMeta } = requestData;
|
||||||
|
yield call(
|
||||||
|
storeLogs,
|
||||||
|
requestData.logs,
|
||||||
|
triggerMeta?.source?.name || triggerMeta?.triggerPropertyName || "",
|
||||||
|
eventType === EventType.ON_JS_FUNCTION_EXECUTE
|
||||||
|
? ENTITY_TYPE.JSACTION
|
||||||
|
: ENTITY_TYPE.WIDGET,
|
||||||
|
triggerMeta?.source?.id || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
if (requestData?.trigger) {
|
if (requestData?.trigger) {
|
||||||
// if we have found a trigger, we need to execute it and respond back
|
// if we have found a trigger, we need to execute it and respond back
|
||||||
log.debug({ trigger: requestData.trigger });
|
log.debug({ trigger: requestData.trigger });
|
||||||
|
|
@ -450,7 +463,13 @@ export function* executeFunction(
|
||||||
evaluateAndExecuteDynamicTrigger,
|
evaluateAndExecuteDynamicTrigger,
|
||||||
functionCall,
|
functionCall,
|
||||||
EventType.ON_JS_FUNCTION_EXECUTE,
|
EventType.ON_JS_FUNCTION_EXECUTE,
|
||||||
{},
|
{
|
||||||
|
source: {
|
||||||
|
id: collectionId,
|
||||||
|
name: `${collectionName}.${action.name}`,
|
||||||
|
},
|
||||||
|
triggerPropertyName: `${collectionName}.${action.name}`,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof UncaughtPromiseError) {
|
if (e instanceof UncaughtPromiseError) {
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,6 @@ export const isThemeBoundProperty = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unsafeFunctionForEval = [
|
export const unsafeFunctionForEval = [
|
||||||
"setTimeout",
|
|
||||||
"fetch",
|
"fetch",
|
||||||
"setInterval",
|
"setInterval",
|
||||||
"clearInterval",
|
"clearInterval",
|
||||||
|
|
|
||||||
40
app/client/src/workers/TimeoutOverride.ts
Normal file
40
app/client/src/workers/TimeoutOverride.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { createGlobalData } from "./evaluate";
|
||||||
|
import { dataTreeEvaluator } from "./evaluation.worker";
|
||||||
|
|
||||||
|
export const _internalSetTimeout = self.setTimeout;
|
||||||
|
export const _internalClearTimeout = self.clearTimeout;
|
||||||
|
|
||||||
|
export default function overrideTimeout() {
|
||||||
|
Object.defineProperty(self, "setTimeout", {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: function(cb: (...args: any) => any, delay: number, ...args: any) {
|
||||||
|
if (!self.ALLOW_ASYNC) {
|
||||||
|
self.IS_ASYNC = true;
|
||||||
|
throw new Error("Async function called in a sync field");
|
||||||
|
}
|
||||||
|
const globalData = createGlobalData({
|
||||||
|
dataTree: dataTreeEvaluator?.evalTree || {},
|
||||||
|
resolvedFunctions: dataTreeEvaluator?.resolvedFunctions || {},
|
||||||
|
isTriggerBased: true,
|
||||||
|
});
|
||||||
|
return _internalSetTimeout(
|
||||||
|
function(...args: any) {
|
||||||
|
self.ALLOW_ASYNC = true;
|
||||||
|
Object.assign(self, globalData);
|
||||||
|
cb(...args);
|
||||||
|
},
|
||||||
|
delay,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(self, "clearTimeout", {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: function(timerId: number) {
|
||||||
|
return _internalClearTimeout(timerId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,126 +1,116 @@
|
||||||
import { uuid4 } from "@sentry/utils";
|
import { uuid4 } from "@sentry/utils";
|
||||||
|
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||||
import { LogObject, Methods, Severity } from "entities/AppsmithConsole";
|
import { LogObject, Methods, Severity } from "entities/AppsmithConsole";
|
||||||
import { klona } from "klona/lite";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
|
||||||
|
import { _internalClearTimeout, _internalSetTimeout } from "./TimeoutOverride";
|
||||||
|
|
||||||
class UserLog {
|
class UserLog {
|
||||||
constructor() {
|
private flushLogsTimerDelay = 0;
|
||||||
this.initiate();
|
|
||||||
}
|
|
||||||
private logs: LogObject[] = [];
|
private logs: LogObject[] = [];
|
||||||
// initiates the log object with the default methods and their overrides
|
private flushLogTimerId: number | undefined;
|
||||||
private initiate() {
|
private requestInfo: {
|
||||||
|
requestId?: string;
|
||||||
|
eventType?: EventType;
|
||||||
|
triggerMeta?: TriggerMeta;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
public setCurrentRequestInfo(requestInfo: {
|
||||||
|
requestId?: string;
|
||||||
|
eventType?: EventType;
|
||||||
|
triggerMeta?: TriggerMeta;
|
||||||
|
}) {
|
||||||
|
this.requestInfo = requestInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetFlushTimer() {
|
||||||
|
if (this.flushLogTimerId) _internalClearTimeout(this.flushLogTimerId);
|
||||||
|
this.flushLogTimerId = _internalSetTimeout(() => {
|
||||||
|
const logs = this.flushLogs();
|
||||||
|
self.postMessage({
|
||||||
|
promisified: true,
|
||||||
|
responseData: {
|
||||||
|
logs,
|
||||||
|
eventType: this.requestInfo?.eventType,
|
||||||
|
triggerMeta: this.requestInfo?.triggerMeta,
|
||||||
|
},
|
||||||
|
requestId: this.requestInfo?.requestId,
|
||||||
|
});
|
||||||
|
}, this.flushLogsTimerDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveLog(method: Methods, data: any[]) {
|
||||||
|
const parsed = this.parseLogs(method, data);
|
||||||
|
this.logs.push(parsed);
|
||||||
|
this.resetFlushTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public overrideConsoleAPI() {
|
||||||
const { debug, error, info, log, table, warn } = console;
|
const { debug, error, info, log, table, warn } = console;
|
||||||
console = {
|
console = {
|
||||||
...console,
|
...console,
|
||||||
table: (...args: any) => {
|
table: (...args: any) => {
|
||||||
table.call(this, args);
|
table.call(this, args);
|
||||||
const parsed = this.parseLogs("table", args);
|
this.saveLog("table", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
error: (...args: any) => {
|
error: (...args: any) => {
|
||||||
error.apply(this, args);
|
error.apply(this, args);
|
||||||
const parsed = this.parseLogs("error", args);
|
this.saveLog("error", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
log: (...args: any) => {
|
log: (...args: any) => {
|
||||||
log.apply(this, args);
|
log.apply(this, args);
|
||||||
const parsed = this.parseLogs("log", args);
|
this.saveLog("log", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
debug: (...args: any) => {
|
debug: (...args: any) => {
|
||||||
debug.apply(this, args);
|
debug.apply(this, args);
|
||||||
const parsed = this.parseLogs("debug", args);
|
this.saveLog("debug", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
warn: (...args: any) => {
|
warn: (...args: any) => {
|
||||||
warn.apply(this, args);
|
warn.apply(this, args);
|
||||||
const parsed = this.parseLogs("warn", args);
|
this.saveLog("warn", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
info: (...args: any) => {
|
info: (...args: any) => {
|
||||||
info.apply(this, args);
|
info.apply(this, args);
|
||||||
const parsed = this.parseLogs("info", args);
|
this.saveLog("info", args);
|
||||||
if (parsed) {
|
|
||||||
this.logs.push(parsed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
public getTimestamp() {
|
private replaceFunctionWithNamesFromObjects(data: any) {
|
||||||
return moment().format("hh:mm:ss");
|
if (typeof data === "function") return `func() ${data.name}`;
|
||||||
}
|
if (!data || typeof data !== "object") return data;
|
||||||
public replaceFunctionWithNamesFromObjects(data: any) {
|
if (data instanceof Promise) return "Promise";
|
||||||
if (typeof data === "object") {
|
const acc: any =
|
||||||
for (const key in data) {
|
Object.prototype.toString.call(data) === "[object Array]" ? [] : {};
|
||||||
if (typeof data[key] === "function") {
|
return Object.keys(data).reduce((acc, key) => {
|
||||||
data[key] = `func() ${data[key].name}`;
|
acc[key] = this.replaceFunctionWithNamesFromObjects(data[key]);
|
||||||
} else if (data[key] instanceof Promise) {
|
return acc;
|
||||||
data[key] = "Promise";
|
}, acc);
|
||||||
} else {
|
|
||||||
this.replaceFunctionWithNamesFromObjects(data[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
// iterates over the data and if data is object/array, then it will remove any functions from it
|
// iterates over the data and if data is object/array, then it will remove any functions from it
|
||||||
public sanitizeData(data: any): any {
|
private sanitizeData(data: any): any {
|
||||||
let returnData = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// cloning the object to avoid mutation
|
const returnData = this.replaceFunctionWithNamesFromObjects(data);
|
||||||
const dataObject = klona(data);
|
|
||||||
returnData = dataObject.map((item: any) => {
|
|
||||||
if (typeof item === "object") {
|
|
||||||
return this.replaceFunctionWithNamesFromObjects(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the item is a function, then remove it from the data and return it as name of the function
|
|
||||||
if (typeof item === "function") {
|
|
||||||
return `func() item.name`;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
returnData = [`There was some error: ${e} ${JSON.stringify(data)}`];
|
|
||||||
}
|
|
||||||
return returnData;
|
return returnData;
|
||||||
|
} catch (e) {
|
||||||
|
return [`There was some error: ${e} ${JSON.stringify(data)}`];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// returns the logs from the function execution after sanitising them and resets the logs object after that
|
// returns the logs from the function execution after sanitising them and resets the logs object after that
|
||||||
public flushLogs(softFlush = false): LogObject[] {
|
public flushLogs(): LogObject[] {
|
||||||
const userLogs = this.logs;
|
const sanitisedLogs = this.logs.map((log) => {
|
||||||
if (!softFlush) this.resetLogs();
|
|
||||||
// sanitise the data key of the user logs
|
|
||||||
const sanitisedLogs = userLogs.map((log) => {
|
|
||||||
return {
|
return {
|
||||||
...log,
|
...log,
|
||||||
data: this.sanitizeData(log.data),
|
data: this.sanitizeData(log.data),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
this.resetLogs();
|
||||||
return sanitisedLogs;
|
return sanitisedLogs;
|
||||||
}
|
}
|
||||||
// parses the incoming log and converts it to the log object
|
// parses the incoming log and converts it to the log object
|
||||||
public parseLogs(method: Methods, data: any[]): LogObject {
|
public parseLogs(method: Methods, data: any[]): LogObject {
|
||||||
// Create an ID
|
// Create an ID
|
||||||
const id = uuid4();
|
const id = uuid4();
|
||||||
const timestamp = this.getTimestamp();
|
const timestamp = moment().format("hh:mm:ss");
|
||||||
// Parse the methods
|
// Parse the methods
|
||||||
let output = data;
|
let output = data;
|
||||||
// For logs UI we only keep 3 levels of severity, info, warn, error
|
// For logs UI we only keep 3 levels of severity, info, warn, error
|
||||||
|
|
|
||||||
|
|
@ -110,24 +110,24 @@ describe("evaluateSync", () => {
|
||||||
expect(response.result).toBe("value");
|
expect(response.result).toBe("value");
|
||||||
});
|
});
|
||||||
it("disallows unsafe function calls", () => {
|
it("disallows unsafe function calls", () => {
|
||||||
const js = "setTimeout(() => {}, 100)";
|
const js = "setImmediate(() => {}, 100)";
|
||||||
const response = evaluate(js, dataTree, {}, false);
|
const response = evaluate(js, dataTree, {}, false);
|
||||||
expect(response).toStrictEqual({
|
expect(response).toStrictEqual({
|
||||||
result: undefined,
|
result: undefined,
|
||||||
logs: [],
|
logs: [],
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
errorMessage: "TypeError: setTimeout is not a function",
|
errorMessage: "ReferenceError: setImmediate is not defined",
|
||||||
errorType: "PARSE",
|
errorType: "PARSE",
|
||||||
raw: `
|
raw: `
|
||||||
function closedFunction () {
|
function closedFunction () {
|
||||||
const result = setTimeout(() => {}, 100)
|
const result = setImmediate(() => {}, 100)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
closedFunction.call(THIS_CONTEXT)
|
closedFunction.call(THIS_CONTEXT)
|
||||||
`,
|
`,
|
||||||
severity: "error",
|
severity: "error",
|
||||||
originalBinding: "setTimeout(() => {}, 100)",
|
originalBinding: "setImmediate(() => {}, 100)",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import { completePromise } from "workers/PromisifyAction";
|
||||||
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
||||||
import userLogs from "./UserLog";
|
import userLogs from "./UserLog";
|
||||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||||
|
import overrideTimeout from "./TimeoutOverride";
|
||||||
|
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
|
||||||
|
|
||||||
export type EvalResult = {
|
export type EvalResult = {
|
||||||
result: any;
|
result: any;
|
||||||
|
|
@ -115,6 +117,8 @@ export function setupEvaluationEnvironment() {
|
||||||
// @ts-expect-error: Types are not available
|
// @ts-expect-error: Types are not available
|
||||||
self[func] = undefined;
|
self[func] = undefined;
|
||||||
});
|
});
|
||||||
|
userLogs.overrideConsoleAPI();
|
||||||
|
overrideTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
||||||
|
|
@ -221,6 +225,7 @@ export type EvaluateContext = {
|
||||||
globalContext?: Record<string, any>;
|
globalContext?: Record<string, any>;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
eventType?: EventType;
|
eventType?: EventType;
|
||||||
|
triggerMeta?: TriggerMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserScriptToEvaluate = (
|
export const getUserScriptToEvaluate = (
|
||||||
|
|
@ -330,6 +335,11 @@ export async function evaluateAsync(
|
||||||
let logs;
|
let logs;
|
||||||
/**** Setting the eval context ****/
|
/**** Setting the eval context ****/
|
||||||
userLogs.resetLogs();
|
userLogs.resetLogs();
|
||||||
|
userLogs.setCurrentRequestInfo({
|
||||||
|
requestId,
|
||||||
|
eventType: context?.eventType,
|
||||||
|
triggerMeta: context?.triggerMeta,
|
||||||
|
});
|
||||||
const GLOBAL_DATA: Record<string, any> = createGlobalData({
|
const GLOBAL_DATA: Record<string, any> = createGlobalData({
|
||||||
dataTree,
|
dataTree,
|
||||||
resolvedFunctions,
|
resolvedFunctions,
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ ctx.addEventListener(
|
||||||
dynamicTrigger,
|
dynamicTrigger,
|
||||||
eventType,
|
eventType,
|
||||||
globalContext,
|
globalContext,
|
||||||
|
triggerMeta,
|
||||||
} = requestData;
|
} = requestData;
|
||||||
if (!dataTreeEvaluator) {
|
if (!dataTreeEvaluator) {
|
||||||
return { triggers: [], errors: [] };
|
return { triggers: [], errors: [] };
|
||||||
|
|
@ -258,6 +259,7 @@ ctx.addEventListener(
|
||||||
{
|
{
|
||||||
globalContext,
|
globalContext,
|
||||||
eventType,
|
eventType,
|
||||||
|
triggerMeta,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
120
app/client/src/workers/timeout.test.ts
Normal file
120
app/client/src/workers/timeout.test.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { PluginType } from "entities/Action";
|
||||||
|
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||||
|
import { createGlobalData } from "./evaluate";
|
||||||
|
import "./TimeoutOverride";
|
||||||
|
import overrideTimeout from "./TimeoutOverride";
|
||||||
|
|
||||||
|
describe("Expects appsmith setTimeout to pass the following criteria", () => {
|
||||||
|
overrideTimeout();
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.spyOn(self, "setTimeout");
|
||||||
|
self.postMessage = jest.fn();
|
||||||
|
it("returns a number a timerId", () => {
|
||||||
|
const timerId = setTimeout(jest.fn(), 1000);
|
||||||
|
expect(timerId).toBeDefined();
|
||||||
|
expect(typeof timerId).toBe("number");
|
||||||
|
});
|
||||||
|
it("Passes arguments into callback", () => {
|
||||||
|
const cb = jest.fn();
|
||||||
|
const args = [1, 2, "3", [4]];
|
||||||
|
setTimeout(cb, 1000, ...args);
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(cb).toHaveBeenCalledWith(...args);
|
||||||
|
});
|
||||||
|
it("Has weird behavior with 'this' keyword", () => {
|
||||||
|
const cb = jest.fn();
|
||||||
|
const error = jest.fn();
|
||||||
|
const obj = {
|
||||||
|
var1: "myVar1",
|
||||||
|
getVar() {
|
||||||
|
try {
|
||||||
|
cb(this.var1);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setTimeout(obj.getVar, 1000);
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(error).toBeCalled();
|
||||||
|
});
|
||||||
|
it("Has weird behavior with 'this' keyword", () => {
|
||||||
|
const cb = jest.fn();
|
||||||
|
const error = jest.fn();
|
||||||
|
const obj = {
|
||||||
|
var1: "myVar1",
|
||||||
|
getVar() {
|
||||||
|
try {
|
||||||
|
cb(this.var1);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setTimeout(obj.getVar.bind(obj), 1000);
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(cb).toBeCalledWith(obj.var1);
|
||||||
|
});
|
||||||
|
it("'this' behavior should be fixed by binding this", () => {
|
||||||
|
const cb = jest.fn();
|
||||||
|
const error = jest.fn();
|
||||||
|
const obj = {
|
||||||
|
var1: "myVar1",
|
||||||
|
getVar() {
|
||||||
|
try {
|
||||||
|
cb(this.var1);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setTimeout(obj.getVar.bind(obj), 1000);
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(cb).toBeCalledWith(obj.var1);
|
||||||
|
});
|
||||||
|
it("Checks the behavior of clearTimeout", () => {
|
||||||
|
const cb = jest.fn();
|
||||||
|
const timerId = setTimeout(cb, 1000);
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
clearTimeout(timerId);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(cb.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
it("Access to appsmith functions inside setTimeout", async () => {
|
||||||
|
const dataTree: DataTree = {
|
||||||
|
action1: {
|
||||||
|
actionId: "123",
|
||||||
|
pluginId: "",
|
||||||
|
data: {},
|
||||||
|
config: {},
|
||||||
|
datasourceUrl: "",
|
||||||
|
pluginType: PluginType.API,
|
||||||
|
dynamicBindingPathList: [],
|
||||||
|
name: "action1",
|
||||||
|
bindingPaths: {},
|
||||||
|
reactivePaths: {},
|
||||||
|
isLoading: false,
|
||||||
|
run: {},
|
||||||
|
clear: {},
|
||||||
|
responseMeta: { isExecutionSuccess: false },
|
||||||
|
ENTITY_TYPE: ENTITY_TYPE.ACTION,
|
||||||
|
dependencyMap: {},
|
||||||
|
logBlackList: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.ALLOW_ASYNC = true;
|
||||||
|
const dataTreeWithFunctions = createGlobalData({
|
||||||
|
dataTree,
|
||||||
|
resolvedFunctions: {},
|
||||||
|
isTriggerBased: true,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
setTimeout(() => dataTreeWithFunctions.action1.run(), 1000);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(self.postMessage).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user