Google Apps Script supports Triggers, that pass Events to trigger functions. Unfortunately, the development environment will let you test functions with no parameter passing, so you cannot simulate an event that way. If you try, you get an error like:
ReferenceError: 'e' is not defined.
Or
TypeError: Cannot read property *...* from undefined
(where e is undefined)
One could treat the event like an optional parameter, and insert a default value into the trigger function using any of the techniques from Is there a better way to do optional function parameters in JavaScript?. But that introduces a risk that a lazy programmer (hands up if that's you!) will leave that code behind, with unintended side effects.
Surely there are better ways?
You can write a test function that passes a simulated event to your trigger function. Here's an example that tests an onEdit() trigger function. It passes an event object with all the information described for "Spreadsheet Edit Events" in Understanding Events.
To use it, set your breakpoint in your target onEdit function, select function test_onEdit and hit Debug.
/**
* Test function for onEdit. Passes an event object to simulate an edit to
* a cell in a spreadsheet.
*
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*
* See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
*/
function test_onEdit() {
onEdit({
user : Session.getActiveUser().getEmail(),
source : SpreadsheetApp.getActiveSpreadsheet(),
range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
authMode : "LIMITED"
});
}
If you're curious, this was written to test the onEdit function for Google Spreadsheet conditional on three cells.
Here's a test function for Spreadsheet Form Submission events. It builds its simulated event by reading form submission data. This was originally written for Getting TypeError in onFormSubmit trigger?.
/**
* Test function for Spreadsheet Form Submit trigger functions.
* Loops through content of sheet, creating simulated Form Submit Events.
*
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*
* See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
*/
function test_onFormSubmit() {
var dataRange = SpreadsheetApp.getActiveSheet().getDataRange();
var data = dataRange.getValues();
var headers = data[0];
// Start at row 1, skipping headers in row 0
for (var row=1; row < data.length; row++) {
var e = {};
e.values = data[row].filter(Boolean); // filter: https://stackoverflow.com/a/19888749
e.range = dataRange.offset(row,0,1,data[0].length);
e.namedValues = {};
// Loop through headers to create namedValues object
// NOTE: all namedValues are arrays.
for (var col=0; col<headers.length; col++) {
e.namedValues[headers[col]] = [data[row][col]];
}
// Pass the simulated event to onFormSubmit
onFormSubmit(e);
}
}
Tips
When simulating events, take care to match the documented event objects as close as possible.
If you wish to validate the documentation, you can log the received event from your trigger function.
Logger.log( JSON.stringify( e , null, 2 ) );
In Spreadsheet form submission events:
all namedValues values are arrays.
Timestamps are Strings, and their format will be localized to the Form's locale. If read from a spreadsheet with default formatting*, they are Date objects. If your trigger function relies on the string format of the timestamp (which is a Bad Idea), take care to ensure you simulate the value appropriately.
If you've got columns in your spreadsheet that are not in your form, the technique in this script will simulate an "event" with those additional values included, which is not what you'll receive from a form submission.
As reported in Issue 4335, the values array skips over blank answers (in "new Forms" + "new Sheets"). The filter(Boolean) method is used to simulate this behavior.
*A cell formatted "plain text" will preserve the date as a string, and is not a Good Idea.
Update 2020-2021:
You don't need to use any kind of mocks events as suggested in the previous answers.
As said in the question, If you directly "run" the function in the script editor, Errors like
TypeError: Cannot read property ... from undefined
are thrown. These are not the real errors. This error is only because you ran the function without a event. If your function isn't behaving as expected, You need to figure out the actual error:
To test a trigger function,
Trigger the corresponding event manually: i.e., To test onEdit, edit a cell in sheet; To test onFormSubmit, submit a dummy form response; To test doGet, navigate your browser to the published webapp /exec url.
If there are any errors, it is logged to stackdriver. To view those logs,
In Script editor > Execution icon on the left bar(Legacy editor: View > Executions).
Alternatively, Click here > Click the project you're interested in > Click "Executions" icon on the left bar(the 4th one)
You'll find a list of executions in the executions page. Make sure to clear out any filters like "Ran as:Me" on the top left to show all executions. Click the execution you're interested in, it'll show the error that caused the trigger to fail in red.
Note: Sometimes, The logs are not visible due to bugs. This is true especially in case of webapp being run by anonymous users. In such cases, It is recommended to Switch Default Google cloud project to a standard Google cloud project and use View> Stackdriver logging directly. See here for more information.
For further debugging, You can use edit the code to add console.log(/*object you're interested in*/) after any line you're interested in to see details of that object. It is highly recommended that you stringify the object you're looking for: console.log(JSON.stringify(e)) as the log viewer has idiosyncrasies. After adding console.log(), repeat from Step 1. Repeat this cycle until you've narrowed down the problem.
Congrats! You've successfully figured out the problem and crossed the first obstacle.
2017 Update:
Debug the Event objects with Stackdriver Logging for Google Apps Script. From the menu bar in the script editor, goto:
View > Stackdriver Logging to view or stream the logs.
console.log() will write DEBUG level messages
Example onEdit():
function onEdit (e) {
var debug_e = {
authMode: e.authMode,
range: e.range.getA1Notation(),
source: e.source.getId(),
user: e.user,
value: e.value,
oldValue: e. oldValue
}
console.log({message: 'onEdit() Event Object', eventObject: debug_e});
}
Example onFormSubmit():
function onFormSubmit (e) {
var debug_e = {
authMode: e.authMode,
namedValues: e.namedValues,
range: e.range.getA1Notation(),
value: e.value
}
console.log({message: 'onFormSubmit() Event Object', eventObject: debug_e});
}
Example onChange():
function onChange (e) {
var debug_e = {
authMode: e.authMode,
changeType: changeType,
user: e.user
}
console.log({message: 'onChange() Event Object', eventObject: debug_e});
}
Then check the logs in the Stackdriver UI labeled as the message string to see the output
As an addition to the method mentioned above (Update 2020) in point 4.:
Here is a small routine which I use to trace triggered code and that has saved me a lot of time already. Also I have two windows open: One with the stackdriver (executions), and one with the code (which mostly resides in a library), so I can easily spot the culprit.
/**
*
* like Logger.log %s in text is replaced by subsequent (stringified) elements in array A
* #param {string | object} text %s in text is replaced by elements of A[], if text is not a string, it is stringified and A is ignored
* #param {object[]} A array of objects to insert in text, replaces %s
* #returns {string} text with objects from A inserted
*/
function Stringify(text, A) {
var i = 0 ;
return (typeof text == 'string') ?
text.replace(
/%s/g,
function(m) {
if( i >= A.length) return m ;
var a = A[i++] ;
return (typeof a == 'string') ? a : JSON.stringify(a) ;
} )
: (typeof text == 'object') ? JSON.stringify(text) : text ;
}
/* use Logger (or console) to display text and variables. */
function T(text) {
Logger.log.apply(Logger, arguments) ;
var Content = Stringify( text, Array.prototype.slice.call(arguments,1) ) ;
return Content ;
}
/**** EXAMPLE OF USE ***/
function onSubmitForm(e) {
T("responses:\n%s" , e.response.getItemResponses().map(r => r.getResponse()) ;
}
Related
In my JointJs application, I want to discard particular commands so that they are not added in the Undo/Redo stack.
I followed the exact same code snippet from the JointJs documentation like below:
var commandManager = new joint.dia.CommandManager({
graph: graph,
cmdBeforeAdd: function(cmdName, cell, graph, options) {
options = options || {};
return !options.ignoreCommandManager;
}
});
// ...
// Note that the last argument to set() is an options object that gets passed to the cmdBeforeAdd() function.
element.set({ 'z': 1 }, { ignoreCommandManager: true });
But when I look into the options object in the debug mode, it doesn't contain any property with the name ignoreCommandManager.
I have also tried the below call to set the z value but it didn't work either.
element.set('z', 1 , { ignoreCommandManager: true });
Any idea why the options object is missing this property to ignore the command, please?
Initially, It was failing in Firefox when I posted the question here. The cache was also disabled.
I tried in another browser (Chrome) today without introducing any new changes and it was working without any issues.
I have trouble correcting an error in my script, the console simply states "uncaught exception: undefined". Could you help me to identify the source of the problem ?
General explanation: I am coding a "copy to clipboard" share-button for a web-app. When the button is pressed, a link get generated and copied to the user clipboard. The link generation is dependent on external factors, reports that are stored in an Oracle database. There is an array named Reports that keeps track of them, but it needs to be up to date to generate a functional sharing-link. The anonymous function getterReports does it in an asynchronous way (JQuery + AJAX).
In order to copy my data to the clipboard, I am using the method described in this link: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
A snackbar (share_bar) is displayed on the screen to indicate that the operation is over.
Here is the raw code:
function copySharingURLToClipboard() {
let share_bar = document.getElementById("modal_share");
//getterReports ensures that the "Reports" array of reports objects is properly updated beforehand (AJAX <-> Database)
getterReports().then(function () { //this ".then" ensurea that the main code is only executed after getterReports is over
let report = getSavedReport(); //finds a report object by iterating the "Reports" array
if (report == undefined) { //no saved report were found
//snackbar related code :
document.getElementById("popup_message").innerHTML = "Temporary reports cannot be shared: please save your report first.";
share_bar.style.backgroundColor = "red";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove('show'); }, 4200);//removing the class after the animation is done
}
else {
let data = BaseURL + "/DisplayReportPage.aspx?id=" + report.id; //generating the string that we want to copy in the clipboard
navigator.clipboard.writeText(data).then(function () {
/* success */
//snackbar related code :
document.getElementById("popup_message").innerHTML = "URL copied to the clipboard successfully";
share_bar.style.backgroundColor = "lightgreen";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove("show"); }, 4200); //removing the class after the animation (it takes 4s + eventual minor latency)
}, function () {
/* failure case */
});
}
});
}
During the execution of copySharingURLToClipboard(), the "uncaught exception: undefined" error happens in the following line, at ".then":
navigator.clipboard.writeText(data).then(function () {
My Web-app doesn't crash though, but the code is simply skipped. The error is only displayed when going step by step in devtool (otherwise the portion of the code is simply skipped without any error message). My Firefox browser is up to date.
All involved function are working fine in other part of the code. I am confident in the fact that getterReports() is working fine on its own. The same is true for the copy to clipboard part of copySharingURLToClipboard(), without the getterReports() updating the "Reports" array, it works fine (but its results are not up to date obviously):
function copySharingURLToClipboard() {
let share_bar = document.getElementById("modal_share");
//getterReports ensures that the "Reports" array is properly updated beforehand (AJAX <-> Database)
let report = getSavedReport(); //finds a report object by iterating the "Reports" array
if (report == undefined) { //no saved report were found
//snackbar related code :
document.getElementById("popup_message").innerHTML = "Temporary reports cannot be shared: please save your report first.";
share_bar.style.backgroundColor = "red";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove('show'); }, 4200);//removing the class after the animation is done
}
else {
let data = BaseURL + "/DisplayReportPage.aspx?id=" + report.id; //generating the string that we want to copy in the clipboard
navigator.clipboard.writeText(data).then(function () {
/* success */
//snackbar related code :
document.getElementById("popup_message").innerHTML = "URL copied to the clipboard successfully";
share_bar.style.backgroundColor = "lightgreen";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove("show"); }, 4200); //removing the class after the animation (it takes 4s + eventual minor latency)
}, function () {
/* failure case */
});
}
}
Am I missing something simple ? A syntax error maybe ?
Firefox does not allow async copy-to-clipboard functionality.
Calling navigator.clipboard.writeText() in a promise resolve or an async function does not work intentionally. The code is working properly on other browsers.
After some research, it appears that there is still no available alternative implementation on Firefox. The logic of the project as a whole needs to be adapted in order to not use asynchronous functions when copying something in the clipboard.
I am trying to use Google Apps Script to create a form, where the fields allow for autocompletion. In other forms I’ve created, I’ve been able to pull an array of options from a google sheet, and use them to populate a drop down list, so I have to think it’s possible to do the same with an autocomplete process.
I’ve blatantly copied this example from w3schools, and it works exactly as needed, as long as I declare the array within the javascript (as done in the example). But what I haven’t been able to figure out is how to populate the array with options pulled from my google sheet.
Here is where I started:
var PMOSupport;
$(function() {
google.script.run.withSuccessHandler(buildDropDowns)
.getPMOSupport();
});
function buildDropDowns(data) {
PMOSupport = data;
console.log(PMOSupport);
}
function autocomplete(inp, arr) {
console.log("ENTER AUTO");
var currentFocus;
inp.addEventListener("input", function(e) {
// all of the remaining code is direct from the w3schools example
// I'm cutting it from here for brevity,
// and because I know this works, when using the declared array below
});
}
var countries = ["Afghanistan","Albania","Algeria","Andorra"];
// this line works fine, when using the array declared above
// autocomplete(document.getElementById("myInput"), countries);
// this line does not work, when trying to use the array populated from the google sheet
autocomplete(document.getElementById("myInput"), PMOSupport);
When I run this, the page creates, and as soon as I type into the entry field, I get a message in the console:
`Uncaught TypeError: Cannot read property 'length' of undefined`
at HTMLInputElement.<anonymous> (<anonymous>:32:28)
When I look into this, it’s saying that the ‘arr’ argument (PMOSupport) isn’t populated. That’s why I added the 2 console.log lines, to see what order things are happening. When I open the page, “ENTER AUTO” logs first, then the State Changes from Idle to Busy and Busy to Idle (while it calls getPMOSupport()), then the PMOSupport array logs in the console (also proving that I am in fact getting the correct data back from the sheet). So clearly, it’s entering function autocomplete() before it calls the google.script.run.withSuccessHandler(buildDropDowns).getPMOSupport() portion, which is why the 'arr' argument is undefined.
I’ve tried various ways of taking that out of the $(function() … }); block, to try to get the PMOSupport array populated before the autocomplete() function runs. Nothing I’ve done seems to be working.
I’m sure this is something simple, and caused by bad habits I’ve picked up over time (I’m not a developer, I just cobble things together for my team). But any help would be appreciated.
You need to call autocomplete AFTER the asynchronous code has returned. So, you need to invoke it from the callback.
function buildDropdowns(data, userObject) {
// probably you should indicate in data which field these data is for, or use
// the userObject parameter in the google.script.run API
autocomplete(document.getElementById("myInput"), data);
}
Alternately (I haven't and won't look at the w3schools code), declare your PMOSupport as a const array initially, and then add the entries from your callback into it (instead of reassigning it). This way, the variable is not undefined, it is just empty at the start. Depending on the implementation of the autocomplete code, this may or may not work for you.
const PMOSupport = [];
....
function buildDropdowns(data) {
PMOSupport.push(...data);
// or
// Array.prototype.push.apply(PMOSupport, data);
}
I have written a decorator/wrapper for window.console so that I, among other nifty stuff, can disable stray console.log's in my production environment.
What i am experiencing is that my wrapper now appears as the source of the actual log command. This makes debugging through the console a bit of a hassle since clicking the link to the far right in the console only leads to my own output function.
The following code is a simplified version of the real script where i have removed some features like enabling/disabling and caching/flushing of rows that have been hidden.
//Save reference to original function
var oConsole = window.console;
//Create custom console output method
var wConsole = function (method) {
return function () {
if (!window.console[method].enabled) {
//Apply log command to original console method
oConsole[method].apply(oConsole, Array.from(arguments)); //This is the row i get linked to
}
};
};
//Create a new console object for overriding original functions
var overrides = {
o: oConsole,
log: wConsole("log"),
debug: wConsole("debug"),
info: wConsole("info"),
warn: wConsole("warn"),
error: wConsole("error")
}
//Using jQuery i create a new instance and extend my defined overrides onto the original version
window.console = $.extend({}, window.console, overrides);
console.log("test 123"); //This is the row i want to link to
When i click the link to the right...
...i get linked to this row.
Is there a way to make a function "transparent" in such a way that the link refers to the callee of my wrapper function instead?
The solution only needs to work in Google Chrome, since I perform the majority of my development there.
Chrome has an option to "blackbox" script files. While this seems to be mostly intended to ignore framework scripts while debugging (blackboxed scripts will be skipped when stepping through code) it will help with your case as well since it will not show as the source for console output.
Open DevTools
Go to settings (F1 or through the main menu)
Open the Blackboxing tab
Enable the checkbox
Add a pattern that matches the file with your console override
Be happy
Source: https://developers.google.com/web/tools/chrome-devtools/javascript/guides/blackbox-chrome-extension-scripts
I have created a tree in Javascript (using jstree) and I am listening to an for events when someone selects a node. I take the type of node from the data that is passed in and want to execute some code if the type is not default.
When I step through this in the debugger, type is "default", yet it steps into the if statement. What am I missing?
$('#mainTree')
.on('select_node.jstree', function (e, data) {
var type = data.node.type;
if (type != "default") {
(I have tried !== as well)
Here is a demo: http://jsfiddle.net/DGAF4/514/
Change the if to make the alert appear. Are you sure you have the types plugin added, as in the demo above (the core.plugins config option)?