When test add-ons doesn't work

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"