feat: enabled setTimeout/clearTimeout APIs (#17445)

This commit is contained in:
arunvjn 2022-10-17 22:40:17 +05:30 committed by GitHub
parent 06f1b23625
commit 28138c18c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 756 additions and 328 deletions

View File

@ -308,8 +308,8 @@ describe("Theme validation usecases", function() {
cy.PublishtheApp();
//Bug Form backgroud colour reset in Publish mode
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
.and("eq", "rgb(126, 34, 206)");
.should("have.css", "background-color")
.and("eq", "rgb(126, 34, 206)");
cy.get(".bp3-button:contains('Sub')")
.invoke("css", "background-color")
.then((CurrentBackgroudColor) => {

View File

@ -129,6 +129,25 @@ describe("Button Widget Functionality", function() {
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(() => {
cy.goToEditFromPublish();
});

View File

@ -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");
});
});

View File

@ -893,8 +893,9 @@ export class AggregateHelper {
selector: ElementType,
text: string | RegExp,
exists: "exist" | "not.exist" = "exist",
timeout?: number,
) {
return this.GetElement(selector)
return this.GetElement(selector, timeout)
.contains(text)
.should(exists);
}

View File

@ -1,245 +1,255 @@
{
"!name": "browser",
"console": {
"assert": {
"!type": "fn(assertion: bool, text: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.assert",
"!doc": "Writes an error message to the console if the assertion is false."
},
"clear": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/clear",
"!doc": " Clear the console."
},
"count": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.count",
"!doc": "Logs the number of times that this particular call to count() has been called."
},
"debug": "console.log",
"dir": {
"!type": "fn(object: ?)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.dir",
"!doc": "Displays an interactive list of the properties of the specified JavaScript object."
},
"error": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.error",
"!doc": "Outputs an error message to the Web Console."
},
"group": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.group",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupCollapsed": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupCollapsed",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupEnd": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupEnd",
"!doc": "Exits the current inline group in the Web Console."
},
"info": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.info",
"!doc": "Outputs an informational message to the Web Console."
},
"log": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.log",
"!doc": "Outputs a message to the Web Console."
},
"table": {
"!type": "fn(data: []|?, columns?: [])",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/table",
"!doc": " Displays tabular data as a table."
},
"time": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.time",
"!doc": "Starts a timer you can use to track how long an operation takes."
},
"timeEnd": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.timeEnd",
"!doc": "Stops a timer that was previously started by calling console.time()."
},
"trace": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.trace",
"!doc": "Outputs a stack trace to the Web Console."
},
"warn": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.warn",
"!doc": "Outputs a warning message to the Web Console."
},
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console",
"!doc": "The console object provides access to the browser's debugging console. The specifics of how it works vary from browser to browser, but there is a de facto set of features that are typically provided."
},
"crypto": {
"getRandomValues": {
"!type": "fn([number])",
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"Blob": {
"!type": "fn(parts: [?], options?: ?)",
"prototype": {
"size": {
"!type": "number",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/size",
"!doc": "The size, in bytes, of the data contained in the Blob object. Read only."
},
"type": {
"!type": "string",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/type",
"!doc": "An ASCII-encoded string, in all lower case, indicating the MIME type of the data contained in the Blob. If the type is unknown, this string is empty. Read only."
},
"slice": {
"!type": "fn(start: number, end?: number, type?: string) -> +Blob",
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "Returns a new Blob object containing the data in the specified range of bytes of the source Blob."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system."
},
"FileReader": {
"!type": "fn()",
"prototype": {
"abort": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Aborts the read operation. Upon return, the readyState will be DONE."
},
"readAsArrayBuffer": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing an ArrayBuffer."
},
"readAsBinaryString": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing raw binary data."
},
"readAsDataURL": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a data: url."
},
"readAsText": {
"!type": "fn(blob: +Blob, encoding?: string)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a string."
},
"EMPTY": "number",
"LOADING": "number",
"DONE": "number",
"error": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The error that occurred while reading the file. Read only."
},
"readyState": {
"!type": "number",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Indicates the state of the FileReader. This will be one of the State constants. Read only."
},
"result": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The file's contents. This property is only valid after the read operation is complete, and the format of the data depends on which of the methods was used to initiate the read operation. Read only."
},
"onabort": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is aborted."
},
"onerror": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when an error occurs."
},
"onload": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is successfully completed."
},
"onloadend": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read is completed, whether successful or not. This is called after either onload or onerror."
},
"onloadstart": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when reading the data is about to begin."
},
"onprogress": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called periodically while the data is being read."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read. File objects may be obtained from a FileList object returned as a result of a user selecting files using the <input> element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement."
},
"FormData": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData",
"prototype": {
"append": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/append",
"!doc": "Appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist."
},
"delete": {
"!type": "fn(name: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/delete",
"!doc": "Deletes a key/value pair from a FormData object."
},
"entries": {
"!type": "fn() -> +iter[:t=[number, string|+Blob]]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries",
"!doc": "Returns an iterator allowing to go through all key/value pairs contained in this object."
},
"get": {
"!type": "fn(name: string) -> string|+Blob",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/get",
"!doc": "Returns the first value associated with a given key from within a FormData object."
},
"getAll": {
"!type": "fn(name: string) -> [string|+Blob]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/getAll",
"!doc": "Returns an array of all the values associated with a given key from within a FormData."
},
"has": {
"!type": "fn(name: string) -> bool",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/has",
"!doc": "Returns a boolean stating whether a FormData object contains a certain key/value pair."
},
"set": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/set",
"!doc": "Sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist."
},
"keys": {
"!type": "fn() -> +iter[:t=number]",
"!doc": "Returns an iterator allowing to go through all keys of the key/value pairs contained in this object.",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/keys"
},
"values": {
"!type": "fn() -> +iter[:t=string|blob]",
"!doc": "Returns an iterator allowing to go through all values of the key/value pairs contained in this object.",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/values"
}
}
}
"!name": "browser",
"console": {
"assert": {
"!type": "fn(assertion: bool, text: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.assert",
"!doc": "Writes an error message to the console if the assertion is false."
},
"clear": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/clear",
"!doc": " Clear the console."
},
"count": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.count",
"!doc": "Logs the number of times that this particular call to count() has been called."
},
"debug": "console.log",
"dir": {
"!type": "fn(object: ?)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.dir",
"!doc": "Displays an interactive list of the properties of the specified JavaScript object."
},
"error": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.error",
"!doc": "Outputs an error message to the Web Console."
},
"group": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.group",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupCollapsed": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupCollapsed",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupEnd": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupEnd",
"!doc": "Exits the current inline group in the Web Console."
},
"info": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.info",
"!doc": "Outputs an informational message to the Web Console."
},
"log": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.log",
"!doc": "Outputs a message to the Web Console."
},
"table": {
"!type": "fn(data: []|?, columns?: [])",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/table",
"!doc": " Displays tabular data as a table."
},
"time": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.time",
"!doc": "Starts a timer you can use to track how long an operation takes."
},
"timeEnd": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.timeEnd",
"!doc": "Stops a timer that was previously started by calling console.time()."
},
"trace": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.trace",
"!doc": "Outputs a stack trace to the Web Console."
},
"warn": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.warn",
"!doc": "Outputs a warning message to the Web Console."
},
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console",
"!doc": "The console object provides access to the browser's debugging console. The specifics of how it works vary from browser to browser, but there is a de facto set of features that are typically provided."
},
"crypto": {
"getRandomValues": {
"!type": "fn([number])",
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"Blob": {
"!type": "fn(parts: [?], options?: ?)",
"prototype": {
"size": {
"!type": "number",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/size",
"!doc": "The size, in bytes, of the data contained in the Blob object. Read only."
},
"type": {
"!type": "string",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/type",
"!doc": "An ASCII-encoded string, in all lower case, indicating the MIME type of the data contained in the Blob. If the type is unknown, this string is empty. Read only."
},
"slice": {
"!type": "fn(start: number, end?: number, type?: string) -> +Blob",
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "Returns a new Blob object containing the data in the specified range of bytes of the source Blob."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system."
},
"FileReader": {
"!type": "fn()",
"prototype": {
"abort": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Aborts the read operation. Upon return, the readyState will be DONE."
},
"readAsArrayBuffer": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing an ArrayBuffer."
},
"readAsBinaryString": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing raw binary data."
},
"readAsDataURL": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a data: url."
},
"readAsText": {
"!type": "fn(blob: +Blob, encoding?: string)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a string."
},
"EMPTY": "number",
"LOADING": "number",
"DONE": "number",
"error": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The error that occurred while reading the file. Read only."
},
"readyState": {
"!type": "number",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Indicates the state of the FileReader. This will be one of the State constants. Read only."
},
"result": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The file's contents. This property is only valid after the read operation is complete, and the format of the data depends on which of the methods was used to initiate the read operation. Read only."
},
"onabort": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is aborted."
},
"onerror": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when an error occurs."
},
"onload": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is successfully completed."
},
"onloadend": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read is completed, whether successful or not. This is called after either onload or onerror."
},
"onloadstart": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when reading the data is about to begin."
},
"onprogress": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called periodically while the data is being read."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read. File objects may be obtained from a FileList object returned as a result of a user selecting files using the <input> element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement."
},
"FormData": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData",
"prototype": {
"append": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/append",
"!doc": "Appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist."
},
"delete": {
"!type": "fn(name: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/delete",
"!doc": "Deletes a key/value pair from a FormData object."
},
"entries": {
"!type": "fn() -> +iter[:t=[number, string|+Blob]]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries",
"!doc": "Returns an iterator allowing to go through all key/value pairs contained in this object."
},
"get": {
"!type": "fn(name: string) -> string|+Blob",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/get",
"!doc": "Returns the first value associated with a given key from within a FormData object."
},
"getAll": {
"!type": "fn(name: string) -> [string|+Blob]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/getAll",
"!doc": "Returns an array of all the values associated with a given key from within a FormData."
},
"has": {
"!type": "fn(name: string) -> bool",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/has",
"!doc": "Returns a boolean stating whether a FormData object contains a certain key/value pair."
},
"set": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/set",
"!doc": "Sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist."
},
"keys": {
"!type": "fn() -> +iter[:t=number]",
"!doc": "Returns an iterator allowing to go through all keys of the key/value pairs contained in this object.",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/keys"
},
"values": {
"!type": "fn() -> +iter[:t=string|blob]",
"!doc": "Returns an iterator allowing to go through all values of the key/value pairs contained in this object.",
"!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()."
}
}

View File

@ -273,6 +273,7 @@ export function* evaluateAndExecuteDynamicTrigger(
callbackData,
globalContext,
eventType,
triggerMeta,
},
);
@ -353,6 +354,18 @@ export function* executeDynamicTriggerRequest(
mainThreadRequestChannel,
);
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 we have found a trigger, we need to execute it and respond back
log.debug({ trigger: requestData.trigger });
@ -450,7 +463,13 @@ export function* executeFunction(
evaluateAndExecuteDynamicTrigger,
functionCall,
EventType.ON_JS_FUNCTION_EXECUTE,
{},
{
source: {
id: collectionId,
name: `${collectionName}.${action.name}`,
},
triggerPropertyName: `${collectionName}.${action.name}`,
},
);
} catch (e) {
if (e instanceof UncaughtPromiseError) {

View File

@ -314,7 +314,6 @@ export const isThemeBoundProperty = (
};
export const unsafeFunctionForEval = [
"setTimeout",
"fetch",
"setInterval",
"clearInterval",

View 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);
},
});
}

View File

@ -1,126 +1,116 @@
import { uuid4 } from "@sentry/utils";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { LogObject, Methods, Severity } from "entities/AppsmithConsole";
import { klona } from "klona/lite";
import moment from "moment";
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
import { _internalClearTimeout, _internalSetTimeout } from "./TimeoutOverride";
class UserLog {
constructor() {
this.initiate();
}
private flushLogsTimerDelay = 0;
private logs: LogObject[] = [];
// initiates the log object with the default methods and their overrides
private initiate() {
private flushLogTimerId: number | undefined;
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;
console = {
...console,
table: (...args: any) => {
table.call(this, args);
const parsed = this.parseLogs("table", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("table", args);
},
error: (...args: any) => {
error.apply(this, args);
const parsed = this.parseLogs("error", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("error", args);
},
log: (...args: any) => {
log.apply(this, args);
const parsed = this.parseLogs("log", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("log", args);
},
debug: (...args: any) => {
debug.apply(this, args);
const parsed = this.parseLogs("debug", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("debug", args);
},
warn: (...args: any) => {
warn.apply(this, args);
const parsed = this.parseLogs("warn", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("warn", args);
},
info: (...args: any) => {
info.apply(this, args);
const parsed = this.parseLogs("info", args);
if (parsed) {
this.logs.push(parsed);
}
return;
this.saveLog("info", args);
},
};
}
public getTimestamp() {
return moment().format("hh:mm:ss");
}
public replaceFunctionWithNamesFromObjects(data: any) {
if (typeof data === "object") {
for (const key in data) {
if (typeof data[key] === "function") {
data[key] = `func() ${data[key].name}`;
} else if (data[key] instanceof Promise) {
data[key] = "Promise";
} else {
this.replaceFunctionWithNamesFromObjects(data[key]);
}
}
}
return data;
private replaceFunctionWithNamesFromObjects(data: any) {
if (typeof data === "function") return `func() ${data.name}`;
if (!data || typeof data !== "object") return data;
if (data instanceof Promise) return "Promise";
const acc: any =
Object.prototype.toString.call(data) === "[object Array]" ? [] : {};
return Object.keys(data).reduce((acc, key) => {
acc[key] = this.replaceFunctionWithNamesFromObjects(data[key]);
return acc;
}, acc);
}
// iterates over the data and if data is object/array, then it will remove any functions from it
public sanitizeData(data: any): any {
let returnData = [];
private sanitizeData(data: any): any {
try {
// cloning the object to avoid mutation
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;
});
const returnData = this.replaceFunctionWithNamesFromObjects(data);
return returnData;
} catch (e) {
returnData = [`There was some error: ${e} ${JSON.stringify(data)}`];
return [`There was some error: ${e} ${JSON.stringify(data)}`];
}
return returnData;
}
// returns the logs from the function execution after sanitising them and resets the logs object after that
public flushLogs(softFlush = false): LogObject[] {
const userLogs = this.logs;
if (!softFlush) this.resetLogs();
// sanitise the data key of the user logs
const sanitisedLogs = userLogs.map((log) => {
public flushLogs(): LogObject[] {
const sanitisedLogs = this.logs.map((log) => {
return {
...log,
data: this.sanitizeData(log.data),
};
});
this.resetLogs();
return sanitisedLogs;
}
// parses the incoming log and converts it to the log object
public parseLogs(method: Methods, data: any[]): LogObject {
// Create an ID
const id = uuid4();
const timestamp = this.getTimestamp();
const timestamp = moment().format("hh:mm:ss");
// Parse the methods
let output = data;
// For logs UI we only keep 3 levels of severity, info, warn, error

View File

@ -110,24 +110,24 @@ describe("evaluateSync", () => {
expect(response.result).toBe("value");
});
it("disallows unsafe function calls", () => {
const js = "setTimeout(() => {}, 100)";
const js = "setImmediate(() => {}, 100)";
const response = evaluate(js, dataTree, {}, false);
expect(response).toStrictEqual({
result: undefined,
logs: [],
errors: [
{
errorMessage: "TypeError: setTimeout is not a function",
errorMessage: "ReferenceError: setImmediate is not defined",
errorType: "PARSE",
raw: `
function closedFunction () {
const result = setTimeout(() => {}, 100)
const result = setImmediate(() => {}, 100)
return result;
}
closedFunction.call(THIS_CONTEXT)
`,
severity: "error",
originalBinding: "setTimeout(() => {}, 100)",
originalBinding: "setImmediate(() => {}, 100)",
},
],
});

View File

@ -14,6 +14,8 @@ import { completePromise } from "workers/PromisifyAction";
import { ActionDescription } from "entities/DataTree/actionTriggers";
import userLogs from "./UserLog";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import overrideTimeout from "./TimeoutOverride";
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
export type EvalResult = {
result: any;
@ -115,6 +117,8 @@ export function setupEvaluationEnvironment() {
// @ts-expect-error: Types are not available
self[func] = undefined;
});
userLogs.overrideConsoleAPI();
overrideTimeout();
}
const beginsWithLineBreakRegex = /^\s+|\s+$/;
@ -221,6 +225,7 @@ export type EvaluateContext = {
globalContext?: Record<string, any>;
requestId?: string;
eventType?: EventType;
triggerMeta?: TriggerMeta;
};
export const getUserScriptToEvaluate = (
@ -330,6 +335,11 @@ export async function evaluateAsync(
let logs;
/**** Setting the eval context ****/
userLogs.resetLogs();
userLogs.setCurrentRequestInfo({
requestId,
eventType: context?.eventType,
triggerMeta: context?.triggerMeta,
});
const GLOBAL_DATA: Record<string, any> = createGlobalData({
dataTree,
resolvedFunctions,

View File

@ -241,6 +241,7 @@ ctx.addEventListener(
dynamicTrigger,
eventType,
globalContext,
triggerMeta,
} = requestData;
if (!dataTreeEvaluator) {
return { triggers: [], errors: [] };
@ -258,6 +259,7 @@ ctx.addEventListener(
{
globalContext,
eventType,
triggerMeta,
},
);

View 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();
});
});