Those of you that write add-ons will know how to test them before deployment using
but sometimes this refuses to run. For example, if you have an add-on that includes an installable trigger, you’ll get this
ScriptError: The add-on attempted an action that is not permitted in Test as add-on mode. To use this action, you must deploy the add-on.
which seems pretty crazy since the whole point of testing before deployment is to avoid deploying. You can star this issue if it affects you too.
Using your add-on as a library
Of course you can do those all things in a container bound script, so it occurred to me that if I used my add-on as a library, and called it from a container bound script I would be able to test it to a deployable state.
It’s actually fairly simple. These are the steps.
- create a container bound script, contents follow later on
- save your add-on and reference it as a library in your container bound script
- expose any functions in from your add-on you’ll be calling with google.script.run
- create a function in in the add-on to serve the resolved htmlOutput that makes your add-on.
Container bound script.
Here’s an example – my add-on is published as a library “SheetEfxDemo” and referenced in this script.
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu("testing addons")
.addItem('Sheets efx demo', 'showViz')
.addToUi();
}
function showViz () {
var ui = SheetEfxDemo.libGetUi();
SpreadsheetApp.getUi().showSidebar(ui);
}
Expose functions you’ll be calling from client side.
Let’s say you have a function in your add-on called xyz that you will be calling from google.script.run. Just reference it in the global space of your container script, like this
var xyz = SheetEfxDemo.xyz;
Referencing the trigger
My add-on includes a reference to a function that exists in the add-on as the trigger to be loaded,
// add the trigger
Triggers.installChangeTrigger ("efxChanger");
but it doesn’t exist in the container bound script so we need a reference to that too
var efxChanger = SheetEfxDemo.efxChanger;
Serving the htmlOutput
Instead of the Add-on servicing the htmlOutput, you have to get the container bound script to do it. This is just a matter of creating a function to return it from your add-on. You’ll notice I referenced this in the showViz function earlier
function libGetUi() {
return HtmlService.createTemplateFromFile('index.html')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle("Sheets Efx demo");
}
Exposed namespaces
In my scripts I always use namespaces to bundle together functions, and use these namespaces to access them. You don’t have to do any of this, but for complex add-ons it’s a good idea, and it actually helps with this problem.
On the server side (this will need to be copied into your container bound script)
/**
* used to expose memebers of a namespace
* @param {string} namespace name
* @param {method} method name
*/
function exposeRun(namespace, method, argArray) {
var global = this;
var func = namespace ? global[namespace][method] : global[method];
if (argArray && argArray.length) {
return func.apply(this, argArray);
} else {
return func();
}
}
On the client side use like this
Provoke.run ('Server', 'init', someArgs, someMoreArgs)
.then (function (keys) {
// do something
})
['catch'](function(err) {
// do something about an error
});
};
and include this namespace client side
var Provoke =(function (ns) {
/**
* run something asynchronously
* @param {string} namespace the namespace (null for global)
* @param {string} method the method or function to call
* @param {[...]} the args
* @return {Promise} a promise
*/
ns.run = function (namespace,method) {
// the args to the server function
var runArgs = Array.prototype.slice.call(arguments).slice(2);
if (arguments.length<2) {
throw new Error ('need at least a namespace and method');
}
// this will return a promise
return new Promise(function ( resolve , reject ) {
google.script.run
.withFailureHandler (function(err) {
reject (err);
})
.withSuccessHandler (function(result) {
resolve (result);
})
.exposeRun (namespace,method,runArgs);
});
};
return ns;
})(Provoke || {});
Now in my container bound script I can expose all the methods in my Server namespace with
var Server = SheetEfxDemo.Server;
Putting it back together again
Actually, when you deploy your add-on there’s nothing that needs to be done. You’ve been able to test it without needing to use “Test as add-on”
Subpages
- Chord Snip
- Debugging Office JavaScript API add-ins
- Dicers
- Dicers Pro and advanced features
- Measuring round trip and execution times from add-ons
- Merging slide templates with tabular data
- Office Add-ins – first attempt
- Orchestrating competing google and Office framework loads
- Plotting maps with overlays Sheets add-on starter
- Promise implementation for Apps Script Stripe payments
- Repeatable add-on settings layouts and style
- Sheets API – Developer Metadata
- SlidesMerge add-on
- Unpicking the Google Picker
- Watching for changes in an Office add-in
- Polyfill for Apps Script properties service for the Office JavaScript API
- Sankey Snip