Apps Script bring Document Properties as Global Variable - javascript

I want to use this global variable in my Apps Script Addon:
var sheet_properties = PropertiesService.getDocumentProperties();
I've place it with other string global variables (out of functions in code.gs). It works well if I save and open the addon sidebar in my test implementation document, but if I refresh the document the Addon Menu disappears and I can't access the sidebar (only help tab is showing). The only way to bring back the menu is deleting the variable or putting inside a function, but I don't want to call getDocumentProperties in every function that use sheet_properties.
How could I proceed?
Thanks!

Try the IIFE pattern:
function someFunctionThatUsesProperties() {
sheet_properties.setProperty('test', 'test value');
console.log(`sheet props: ${JSON.stringify(sheet_properties)}`);
}
// ... at the end of the script file:
let sheet_properties;
function setGlobals_() {
sheet_properties = PropertiesService.getDocumentProperties();
}
(function () { setGlobals_() })();
Another alternative is to use a global props object, and always access it through a getter function, like this:
function anotherFunctionThatUsesProperties() {
const props = getProps_();
pros.sheet.setProperty('test', 'test value');
console.log(`sheet props: ${JSON.stringify(props.sheet)}, user props: ${JSON.stringify(props.user)}`);
}
// ... at the end of the script file:
const props = {};
function getProps_() {
if (Object.keys(props).length) {
return props;
}
props.sheet = PropertiesService.getDocumentProperties();
props.user = PropertiesService.getUserProperties();
return props;
}

Related

Create a javascript function in such a way to proxy variable accesses to properties of an external object

This seems like a problem that requires some JS expertize that I'm apparently not in posses.
I'm writing a scripting module for an app. The scripting and the app are in Javascript.
It will be used by developers to provide scripting extensions to various modules that run on demand or when triggered by something.
I've got a request to alter the way it works in order to simplify the coding of the scripts.
And I'm kinda stuck since I don't know how to proxy local variables inside the script into an external object.
This is a sample of what works currently:
// this is part of the app developer API
function compileScript(userScript, argSignature) {
let _scriptFunc_ = null;
/* scripting API - there are available in scripts */
const print = function(message, window) {
const msg = document.createElement("span")
msg.innerText = message;
document.getElementById("output").append(msg)
document.getElementById("output").append(document.createElement("br"))
};
/* end Scripting API section */
try {
const scriptSource = `
(async () => {
try {
${userScript}
} catch (err) {
//_errEmit.fire(err)
}
})()
`; // wrap the execution in async so they can use await in their userScript
// argument signatures are defined by the module that "compiles" the script
// they call it with those and use them inside the userScript
eval("_scriptFunc_ = function(" + argSignature + ") {\n" + scriptSource + "\n}");
}
catch (err) {
//EvtScriptEmitEvalError.fire(err); // script "compilation" exception
return null;
}
return _scriptFunc_.bind(this);
}
// USAGE
// have some context with "variables" inside accessible by the script
// changes to this might be propagated elsewhere if it has property getters/setter
let scriptData = {
something: 10
};
// define the script
let userScript = `
for (let i = 0; i < count; i++) {
this.something++; // this changes scriptData
print(this.something)
}
`
// "compile" and run
const script = compileScript.call(scriptData, userScript, "count")
script(5) // output: 11,12,13,14,15
console.log(scriptData.something) // 15
<pre id="output">
</pre>
Note the usage of "this." inside the script to refer to scriptData members.
The request they have is to access the properties of the scriptData object inside the script by simply referring to its properties as if they were variables inside the script.
This is how they would want to write it (note there is no "this." before something):
let userScript = `
for (let i = 0; i < count; i++) {
something++; // this changes scriptData
print(something)
}
`
They are fine with possible name collisions between parameters and members of scriptData, it is developer work to set that up correctly.
My problem, tough, is that I don't have any idea how to modify "compileScript" in order to inject members of scriptData as plain variables inside the script is such a way that they proxy to the scriptData object.
It is easy to define a function in the scope of compileScript like "print", but I have no ideas on how to do this concept of "proxy variables".
"with" is not available in strict mode which the app runs in.
JS Proxy class does not seem useful.
Deconstructing scriptData into variables can be done but those variables are no longer going to the scriptData object, they are local.
Defining property getters/setters is available only for objects, not for the compileScript function...
I cannot modify the scriptData object, the user passes it as is. I can only tweak the generation of the script so that it behaves as required.
It should also work in a web worker (so no global scope / window), since a script could be triggered at the completion of a web worker.
Any ideas?
You're looking for the with statement. It's the only way to make variable assignments become interceptable as property assignments on your scriptData object (apart from going the full way to transpiling the script code with something like babel).
If writable variables are not necessary, you could also use this technique to inject values into the scope of a function. I would recommend to use it anyway, instead of eval.
/* scripting API - there are available in scripts */
const api = {
print(message, window) {
const msg = document.createElement("p");
msg.textContent = message;
document.getElementById("output").append(msg);
},
};
/* end Scripting API section */
// this is part of the app developer API
function compileScript(context, userScript, parameters) {
const scriptSource = `
const { ${Object.keys(api).join(', ')} } = api;
with (this) {
return async (${parameters.join(', ')}) => {
"use strict";
try {
${userScript}
} catch (err) {
//_errEmit.fire(err)
}
};
}
`;
try {
return new Function('api', scriptSource).call(context, api);
} catch (err) {
console.warn(err); // script "compilation" exception
return null;
}
}
let scriptData = {
something: 10
};
// define the script
let userScript = `
for (let i = 0; i < count; i++) {
something++; // this changes scriptData
print(something)
}
`;
// "compile" and run
const script = compileScript(scriptData, userScript, ["count"])
script(5) // output: 11,12,13,14,15
console.log(scriptData.something) // 15
<pre id="output">
</pre>

Why doesn't overriding a class function work sometimes?

I followed this https://wiki.gnome.org/Projects/GnomeShell/Extensions/StepByStepTutorial for Overwriting a function.
For example, I want to override the function _setupKeyboard() on the Keyboard class, but my override isn't invoked. The specific portion I want to change is this, to remove the if guard:
if (Meta.is_wayland_compositor()) {
this._connectSignal(this._keyboardController, 'emoji-visible',
this._onEmojiKeyVisible.bind(this));
}
I copied the function from the source, removed the part I didn't want, then set the replacement function like this:
const Keyboard = imports.ui.keyboard;
Keyboard.Keyboard.prototype._setupKeyboard = myOverride;
Why isn't my override being invoked and how can I achieve this?
There are two common reasons an override won't be invoked. If the method is invoked before your override is applied, or if the function is a callback set with Function.prototype.bind() which creates a new closure.
In this case, the function _setupKeyboard() is called before your override is applied. When GNOME Shell starts up, it creates an instance of Keyboard.KeyboardManager here:
// main.js, line #204
keyboard = new Keyboard.KeyboardManager();
By the time the keyboard variable has been assigned to the instance, a default Keyboard.Keyboard class has been created and the function _setupKeyboard() has already been called in Keyboard._init(), which is much sooner than your extension is loaded.
Since there's no way to easily fix that, your best option is to just re-create the one part of the code you want to run:
const Meta = imports.gi.Meta;
const Main = imports.ui.main;
const Keyboard = imports.ui.keyboard.Keyboard;
const originalSetup = Keyboard.prototype._setupKeyboard;
const modifiedSetup = function () {
originalSetup.call(this);
if (!Meta.is_wayland_compositor()) {
this._connectSignal(this._keyboardController, 'emoji-visible',
this._onEmojiKeyVisible.bind(this));
}
this._relayout();
};
function init() {
}
// Your extension's enable function (might be a class method)
function enable() {
let kbd = Main.keyboard.keyboardActor;
if (kbd !== null) {
if (!Meta.is_wayland_compositor()) {
kbd.__mySignalId = kbd._connectSignal(kbd._keyboardController, 'emoji-visible',
kbd._onEmojiKeyVisible.bind(kbd));
}
}
Keyboard.prototype._setupKeyboard = modifiedSetup;
}
function disable() {
let kbd = Main.keyboard.keyboardActor;
if (kbd !== null && kbd.__mySignalId) {
kbd.disconnect(kbd.__mySignalId);
kbd.__mySignalId = 0;
}
Keyboard.prototype._setupKeyboard = originalSetup;
}
This is not very pretty, but that is often the price of patching private code. I can also not guarantee that the code will do what you want, because I suspect the emoji key is hidden on X11 for a reason.

How to pass variables to IIFE function as global arguments?

I'm seeking to pass local variables to IIFE (Module Pattern) as a global arguments, but I have no idea how do I do this.
Here I have one of my modules that needs to receive some variables as the arguments outside of module scope:
const Event = (function(flag, length) {
console.log(flag, length);
function drive(target, event, callback) {
let isStringArray = Array.isArray(event);
if (isStringArray) {
event.forEach(string => register(target, string, callback))
}
}
function register(target, event, callback) {
target.forEach((item, index) => {
item.addEventListener(event, callback)
})
}
return {
drive: drive
}
})();
export default Event;
And this is my main code:
import Event from './event.js';
import Process from './process.js';
class Slider {
constructor(root, elem, event, process) {
/* Instance Arguments */
this.root = document.getElementById(root);
this.elem = this.root.querySelectorAll(elem);
/* Local Variables that need to pass to Module Pattern. */
this.flag = true;
this.length = this.elem.length;
/* Global Modules */
this.Event = event(this.flag, this.length); // doesn't work.
this.Process = process;
this.Setup;
}
get Setup() {
this.Event.drive(this.elem, ['mouseenter', 'click'], (e) => this.Process.finish(e));
}
}
let slider = new Slider('slider', '.image', Event, Process);
Full Code
Like I said, I want to pass the local variables to my module for being able to inspect the arguments globally inside of the scope.
I could just pass the variables to this.Event.drive(this.flag, this.length) directly like we normally do, but it would looks a bit dirty and ugly so I'm not going to use this way unless there are 0 ways.
Are there any ways to accomplish this?
Thanks to listen.
IIFE
as name says it immediate invoking function so you can pass variable as an argument because it already executed till then.
you can pass value in as below
var event = (function(name){return name})('value');

Multiple browsers and the Page Object pattern

We are using the Page Object pattern to organize our internal AngularJS application tests.
Here is an example page object we have:
var LoginPage = function () {
this.username = element(by.id("username"));
this.password = element(by.id("password"));
this.loginButton = element(by.id("submit"));
}
module.exports = LoginPage;
In a single-browser test, it is quite clear how to use it:
var LoginPage = require("./../po/login.po.js");
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should successfully log in a user", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
});
});
But, when it comes to a test when multiple browsers are instantiated and there is the need to switch between them in a single test, it is becoming unclear how to use the same page object with multiple browsers:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
// fire up a different browser and log in
var browser2 = browser.forkNewDriverInstance();
// the problem is here - scope.page.username.clear() would be applied to the main "browser"
});
});
Problem:
After we forked a new browser, how can we use the same Page Object fields and functions, but applied to a newly instantiated browser (browser2 in this case)?
In other words, all element() calls here would be applied to browser, but needed to be applied to browser2. How can we switch the context?
Thoughts:
one possible approach here would be to redefine the global element = browser2.element temporarily while being in the context of browser2. The problem with this approach is that we also have browser.wait() calls inside the page object functions. This means that browser = browser2 should be also set. In this case, we would need to remember the browser global object in a temp variable and restore it once we switch back to the main browser context..
another possible approach would be to pass the browser instance into the page object, something like:
var LoginPage = function (browserInstance) {
browser = browserInstance ? browserInstance : browser;
var element = browser.element;
// ...
}
but this would probably require to change every page object we have..
Hope the question is clear - let me know if it needs clarification.
Maybe you could write few functions to make the the browser registration/start/switch smoother. (Basically it is your first option with some support.)
For example:
var browserRegistry = [];
function openNewBrowser(){
if(typeof browserRegistry[0] == 'undefined'){
browseRegistry[0] = {
browser: browser,
element: element,
$: $,
$$: $$,
... whatever else you need.
}
}
var tmp = browser.forkNewDriverInstance();
var id = browserRegistry.length;
browseRegistry[id] = {
browser: tmp,
element: tmp.element,
$: tmp.$,
$$: tmp.$$,
... whatever else you need.
}
switchToBrowserContext(id);
return id;
}
function switchToBrowserContext(id){
browser=browseRegistry[id].browser;
element=browseRegistry[id].element;
$=browseRegistry[id].$;
$$=browseRegistry[id].$$;
}
And you use it this way in your example:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page1 = new LoginPage();
openNewBrowser();
browser.get("/#login");
scope.page2 = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page1.username.clear();
scope.page1.username.sendKeys(login);
scope.page1.password.sendKeys(password);
scope.page1.loginButton.click();
scope.page2.username.clear();
scope.page2.username.sendKeys(login);
scope.page2.password.sendKeys(password);
scope.page2.loginButton.click();
});
});
So you can leave your page objects as they are.
To be honest I think your second approach is cleaner...
Using global variables can bite back later.
But if you don't want to change your POs, this can also work.
(I did not test it... sorry for the likely typos/errors.)
(You can place the support functions to your protractor conf's onprepare section for example.)
Look at my solution. I simplified example, but we are using this approach in current project. My app has pages for both user permissions types, and i need to do some complex actions same time in both browsers. I hope this might show you some new, better way!
"use strict";
//In config, you should declare global browser roles. I only have 2 roles - so i make 2 global instances
//Somewhere in onPrepare() function
global.admin = browser;
admin.admin = true;
global.guest = browser.forkNewDriverInstance();
guest.guest = true;
//Notice that default browser will be 'admin' example:
// let someElement = $('someElement'); // this will be tried to be found in admin browser.
class BasePage {
//Other shared logic also can be added here.
constructor (browser = admin) {
//Simplified example
this._browser = browser
}
}
class HomePage extends BasePage {
//You will not directly create this object. Instead you should use .getPageFor(browser)
constructor(browser) {
super(browser);
this.rightToolbar = ToolbarFragment.getFragmentFor(this._browser);
this.chat = ChatFragment.getFragmentFor(this._browser);
this.someOtherNiceButton = this._browser.$('button.menu');
}
//This function relies on params that we have patched for browser instances in onPrepare();
static getPageFor(browser) {
if (browser.guest) return new GuestHomePage(browser);
else if (browser.admin) return new AdminHomePage(browser);
}
openProfileMenu() {
let menu = ProfileMenuFragment.getFragmentFor(this._browser);
this.someOtherNiceButton.click();
return menu;
}
}
class GuestHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
//Some feature that is only available for guest
login() {
// will be 'guest' browser in this case.
this._browser.$('input.login').sendKeys('sdkfj'); //blabla
this._browser.$('input.pass').sendKeys('2345'); //blabla
this._browser.$('button.login').click();
}
}
class AdminHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
acceptGuest() {
let acceptGuestButton = this._browser.$('.request-admission .control-btn.admit-user');
this._browser.wait(EC.elementToBeClickable(acceptGuestButton), 10000,
'Admin should be able to see and click accept guest button. ' +
'Make sure that guest is currently trying to connect to the page');
acceptGuestButton.click();
//Calling browser directly since we need to do complex action. Just example.
guest.wait(EC.visibilityOf(guest.$('.central-content')), 10000, 'Guest should be dropped to the page');
}
}
//Then in your tests
let guestHomePage = HomePage.getPageFor(guest);
guestHomePage.login();
let adminHomePage = HomePage.getPageFor(admin);
adminHomePage.acceptGuest();
adminHomePage.openProfileMenu();
guestHomePage.openProfileMenu();

mocking window.location.href in Javascript

I have some unit tests for a function that makes use of the window.location.href -- not ideal I would far rather have passed this in but its not possible in the implementation. I'm just wondering if its possible to mock this value without actually causing my test runner page to actually go to the URL.
window.location.href = "http://www.website.com?varName=foo";
expect(actions.paramToVar(test_Data)).toEqual("bar");
I'm using jasmine for my unit testing framework.
The best way to do this is to create a helper function somewhere and then mock that:
var mynamespace = mynamespace || {};
mynamespace.util = (function() {
function getWindowLocationHRef() {
return window.location.href;
}
return {
getWindowLocationHRef: getWindowLocationHRef
}
})();
Now instead of using window.location.href directly in your code simply use this instead. Then you can replace this method whenever you need to return a mocked value:
mynamespace.util.getWindowLocationHRef = function() {
return "http://mockhost/mockingpath"
};
If you want a specific part of the window location such as a query string parameter then create helper methods for that too and keep the parsing out of your main code. Some frameworks such as jasmine have test spies that can not only mock the function to return desired values, but can also verified it was called:
spyOn(mynamespace.util, 'getQueryStringParameterByName').andReturn("desc");
//...
expect(mynamespace.util.getQueryStringParameterByName).toHaveBeenCalledWith("sort");
I would propose two solutions which have already been hinted at in previous posts here:
Create a function around the access, use that in your production code, and stub this with Jasmine in your tests:
var actions = {
getCurrentURL: function () {
return window.location.href;
},
paramToVar: function (testData) {
...
var url = getCurrentURL();
...
}
};
// Test
var urlSpy = spyOn(actions, "getCurrentURL").andReturn("http://my/fake?param");
expect(actions.paramToVar(test_Data)).toEqual("bar");
Use a dependency injection and inject a fake in your test:
var _actions = function (window) {
return {
paramToVar: function (testData) {
...
var url = window.location.href;
...
}
};
};
var actions = _actions(window);
// Test
var fakeWindow = {
location: { href: "http://my/fake?param" }
};
var fakeActions = _actions(fakeWindow);
expect(fakeActions.paramToVar(test_Data)).toEqual("bar");
You need to simulate local context and create your own version of window and window.location objects
var localContext = {
"window":{
location:{
href: "http://www.website.com?varName=foo"
}
}
}
// simulated context
with(localContext){
console.log(window.location.href);
// http://www.website.com?varName=foo
}
//actual context
console.log(window.location.href);
// http://www.actual.page.url/...
If you use with then all variables (including window!) will firstly be looked from the context object and if not present then from the actual context.
Sometimes you may have a library that modifies window.location and you want to allow for it to function normally but also be tested. If this is the case, you can use a closure to pass your desired reference to your library such as this.
/* in mylib.js */
(function(view){
view.location.href = "foo";
}(self || window));
Then in your test, before including your library, you can redefine self globally, and the library will use the mock self as the view.
var self = {
location: { href: location.href }
};
In your library, you can also do something like the following, so you may redefine self at any point in the test:
/* in mylib.js */
var mylib = (function(href) {
function go ( href ) {
var view = self || window;
view.location.href = href;
}
return {go: go}
}());
In most if not all modern browsers, self is already a reference to window by default. In platforms that implement the Worker API, within a Worker self is a reference to the global scope. In node.js both self and window are not defined, so if you want you can also do this:
self || window || global
This may change if node.js really does implement the Worker API.
Below is the approach I have take to mock window.location.href and/or anything else which maybe on a global object.
First, rather than accessing it directly, encapsulate it in a module where the object is kept with a getter and setter. Below is my example. I am using require, but that is not necessary here.
define(["exports"], function(exports){
var win = window;
exports.getWindow = function(){
return win;
};
exports.setWindow = function(x){
win = x;
}
});
Now, where you have normally done in your code something like window.location.href, now you would do something like:
var window = global_window.getWindow();
var hrefString = window.location.href;
Finally the setup is complete and you can test your code by replacing the window object with a fake object you want to be in its place instead.
fakeWindow = {
location: {
href: "http://google.com?x=y"
}
}
w = require("helpers/global_window");
w.setWindow(fakeWindow);
This would change the win variable in the window module. It was originally set to the global window object, but it is not set to the fake window object you put in. So now after you replaced it, the code will get your fake window object and its fake href you had put it.
This works for me:
delete window.location;
window.location = Object.create(window);
window.location.href = 'my-url';
This is similar to cpimhoff's suggestion, but it uses dependency injection in Angular instead. I figured I would add this in case someone else comes here looking for an Angular solution.
In the module, probably the app.module add a window provider like this:
#NgModule({
...
providers: [
{
provide: Window,
useValue: window,
},
],
...
})
Then in your component that makes use of window, inject window in the constructor.
constructor(private window: Window)
Now instead of using window directly, use the component property when making use of window.
this.window.location.href = url
With that in place you can set the provider in Jasmine tests using TestBed.
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{
provide: Window,
useValue: {location: {href: ''}},
},
],
}).compileComponents();
});
IMO, this solution is a small improvement of cburgmer's in that it allows you to replace window.location.href with $window.location.href in the source. Granted I'm using Karma and not Jasmine, but I believe this approach would work with either. And I've added a dependency on sinon.
First a service / singleton:
function setHref(theHref) {
window.location.href = theHref;
}
function getHref(theHref) {
return window.location.href;
}
var $$window = {
location: {
setHref: setHref,
getHref: getHref,
get href() {
return this.getHref();
},
set href(v) {
this.setHref(v);
}
}
};
function windowInjectable() { return $$window; }
Now I can set location.href in code by injecting windowInjectable() as $window like this:
function($window) {
$window.location.href = "http://www.website.com?varName=foo";
}
and mocking it out in a unit test it looks like:
sinon.stub($window.location, 'setHref'); // this prevents the true window.location.href from being hit.
expect($window.location.setHref.args[0][0]).to.contain('varName=foo');
$window.location.setHref.restore();
The getter / setter syntax goes back to IE 9, and is otherwise widely supported according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
Here's my generic solution that requires an extra import in production code, but doesn't require dependency injection or writing individual wrapper functions like getHref().
Basically we toss the window into a separate file and then our prod code imports the window indirectly from that file.
In production, windowProxy === window.
In tests we can mutate the module which exports windowProxy and mock it with a new temporary value.
// windowProxy.js
/*
* This file exists solely as proxied reference to the window object
* so you can mock the window object during unit tests.
*/
export default window;
// prod/someCode.js
import windowProxy from 'path/to/windowProxy.js';
export function changeUrl() {
windowProxy.location.href = 'https://coolsite.com';
}
// tests/someCode.spec.js
import { changeUrl } from '../prod/someCode.js';
import * as windowProxy from '../prod/path/to/windowProxy.js';
describe('changeUrl', () => {
let mockWindow;
beforeEach(() => {
mockWindow = {};
windowProxy.default = myMockWindow;
});
afterEach(() => {
windowProxy.default = window;
});
it('changes the url', () => {
changeUrl();
expect(mockWindow.location.href).toEqual('https://coolsite.com');
});
});
You need to fake window.location.href while being on the same page.
In my case, this snipped worked perfectly:
$window.history.push(null, null, 'http://server/#/YOUR_ROUTE');
$location.$$absUrl = $window.location.href;
$location.replace();
// now, $location.path() will return YOUR_ROUTE even if there's no such route

Categories

Resources