patternjavascriptMinor
Creates a menu and sidebar in Google Spreadsheets
Viewed 0 times
createsgooglesidebarmenuandspreadsheets
Problem
In an effort to make my code more testable, I'm trying to make use of constructors and prototypes.
Here is a working Google Apps Script (GAS) for creating a new submenu under the Addon main menu in a Google Spreadsheet. If a user clicks the 'Show' menu item, a sidebar will appear.
Questions:
-
I'd like to be able to write unit tests for both
Here is a working Google Apps Script (GAS) for creating a new submenu under the Addon main menu in a Google Spreadsheet. If a user clicks the 'Show' menu item, a sidebar will appear.
var UI = function (menuName, menuFunction, sidebarFile, sidebarTitle) {
this.menuName = menuName;
this.menuFunction = menuFunction;
this.sidebarFile = sidebarFile;
this.sidebarTitle = sidebarTitle;
};
UI.prototype.createAddonMenu = function () {
try {
SpreadsheetApp.getUi()
.createAddonMenu()
.addItem(this.menuName, this.menuFunction)
.addToUi();
Logger.log('onOpen (success): building menu');
} catch (e) {
Logger.log('onOpen (fail): ' + e);
}
};
UI.prototype.showSidebar = function () {
try {
var ui = HtmlService.createTemplateFromFile(this.sidebarFile)
.evaluate()
.setTitle(this.sidebarTitle)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi()
.showSidebar(ui);
Logger.log('showSidebar (success): showing sidebar');
} catch (e) {
Logger.log('showSidebar (fail): ' + e);
}
};
var ui = new UI('Show', 'showSidebar', 'index', 'Awesome Title');
// onOpen is a built-in Google Apps Script function that gets called when the spreadsheet is opened
function onOpen(e) {
return ui.createAddonMenu();
}
// showSidebar is a standalone function and returns the ui showSidebar method. needs to be this way in order to be called from createAddonMenu
function showSidebar() {
return ui.showSidebar();
}Questions:
-
I'd like to be able to write unit tests for both
createAddonMenu and showSidebar, but at this moment I don't believe it's possible as I'm not returning a value from either method; I'm simply just calling Google's third party code for creating a menu and showing a sidebar. WoSolution
- Testing:
Although your functions don't return a value and they catch any error that is thrown, the functions do log whether the call was successful or not. You could use the Logger to test if they fail like so:
Logger.clear();
ui.showSidebar();
var string = Logger.getLog();
if (string.indeOf("(fail)") === -1) throw "test for 'showSidebar' failed";Note: If your unit tests are run in the script editor some of the third party functions could throw errors that wouldn't occur when called from the spreadsheet. The ones in your current functions are all fine last I checked, but function calls like
SpreadsheetApp.getActive() cannot be made from the script editor.Writing wrapper classes would be the safest way to ensure that your functions are correct, but that assumes that your wrappers and mocks are perfectly written. You have to consider whether the work it would take to write/debug/test the wrapper and mock classes is worth the value of having unit tests for your functions--especially since there are currently only two and they are rather small.
- OOP in Google Apps Script
Where to call your constructor
Declaring variables in the global scope is dangerous since Google doesn't offer specifics on the order that it's interpreter/compiler (who knows which) reads the code. Basically things like:
var UI = function () {}and
UI.prototype.showSidebar = function () {};and
var ui = new UI();aren't guaranteed to be executed in the intended order, specifically if they're in separate script files. If you wish to dabble in this dark art (who doesn't?), please note the following:
- Functions are hoisted
- The rest of the code appears to be executed in the order that your script files were created
I would recommend making one of your script files a sort of "main" file; make this file the only place you preform logic on the global scope to ensure that all the global logic is executed in the intended order.
Note: Global logic is executed every time you execute a script.
Constructor and Prototype in Google Apps Scripts
The way your code creates the UI class depends on global logic. If you declare a UI instance at the global scope in a separate script file, the UI class or it's prototype might not yet be defined.
A way around this would be to use a factory function instead:
function createUI() {
return {
createAddonMenu : function () {},
showSidebar : function () {}
};
}The factory function will be hoisted and all is well in the global scope.
To many parameters
Consider passing a single object for your constructor's parameter:
function createUI(settings) {
var menuName = settings.menu.name;
var menuFunction = settings.menu.action;
// ...
}
var ui = createUI({
menu : {
name : "Show",
action : "showSidebar"
}
sidebar : {
// ...
}
});This gives names to the parameters at the caller's end, it makes it so the order of the parameters is irrelevant, and it makes optional values easier to work with since the order of other parameters won't be affected if one is omitted. When using this technique you may want to validate that all required parameters exist inside the parameter object.
Code Snippets
Logger.clear();
ui.showSidebar();
var string = Logger.getLog();
if (string.indeOf("(fail)") === -1) throw "test for 'showSidebar' failed";var UI = function () {}UI.prototype.showSidebar = function () {};var ui = new UI();function createUI() {
return {
createAddonMenu : function () {},
showSidebar : function () {}
};
}Context
StackExchange Code Review Q#117728, answer score: 2
Revisions (0)
No revisions yet.