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”