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"

Why not join our community , follow the blogtwitterG+  and let me know.

Comments