Software dev, tech, mind hacks and the occasional personal bit

Category: JavaScript

The window.beforeUnload event – a sad tale

Well now, gather around children…

Once upon a time, the good developers wanted to save their users from losing changes if they left a form before submitting it. And as sometimes happens, Browsers looked kindly on the world, and provided the beforeUnload event, which could be hooked in Javascript so that changes could be automatically saved or a confirm dialog could be shown. The developers and users were overjoyed, and a golden age ensued.

But then enter the evil developers! These developers hooked the beforeUnload event to make the user’s life miserable. They coded event handlers so that people could never leave pages, and were stuck in a hell of eternal dialogs and never-ending pop-up windows.

The Browsers realised that beforeUnload had been a mistake, and something had to be done. The event was still needed by the good developers, but the evil developers had to be contained. So the event was changed to something “special”, and uncontrollable!

Thus code like this was born:

window.addEventListener('beforeunload', function(e) {
  e.preventDefault();
  e.returnValue = ''; // Triggers the browser's generic warning confirm, cannot be customised
  } 
});

And it mainly worked. The evil developers were stopped, and the users were freed from the inescapable prisons of popups and alerts.

But the good developers wanted to write automated browser tests to ensure that the beforeUnload event worked, and that users would be saved from losing their changes forever. And this is where the tale becomes even sadder.

In Selenium / Firefox, no confirm dialog is shown from beforeUnload in tests, even though the confirm dialog shows during a usual interactive session.

In Cuprite / Chrome the story is more complicated. A message is shown on the console:

Modal window with text `` has been opened, but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` | `dismiss_confirm` | `accept_alert`), accepting by default

But none of the ‘accept’ or ‘dismiss’ options work, or catch the dialog. They only work on normal JavaScript confirms and alerts. The only option is:

page.driver.browser.on(:dialog) do |dialog|
  # I DO GET CALLED! BUT ASYNCHRONOUSLY.
end

But the dialog object cannot be accepted or dismissed, unlike all other dialog objects. It also has an empty message. Alas, our story ends a testing tragedy. The console warning message is always shown, and no significant test can be written, as the Cancel (dismiss) button cannot be pressed.

And so, to this day, the good developers must test this code manually, like it was the 1990s.

PS – do let me know if this tale is wrong and there is a better way…

PPS – Chrome, Edge and Firefox support beforeUnload in this way. But Safari on iOS ignores it. Safari on Mac largely supports beforeUnload, but after a cancel in the dialog to stay on the page, it doesn’t seem like the page can do a programmatic redirect from JS later (which other browsers allow).

Review: JavaScript – The Good Parts by Douglas Crockford

“JavaScript: The Good Parts” was kindly lent to me by my friend and colleague Dave Cameron. It was a highly informative read, and a good length at just under 150 pages. The aim of the book is to define an elegant, recommended subset of JavaScript that allows you to do everything you need, and work around problems in the language. The book is aimed at people who already have a good grasp of programming in other languages.

I learnt quite a bit from the book. Here are a few of the most important parts that come to mind:

  • JavaScript has function scope, not block scope so it is best to declare variables at the top of functions.
  • It is important to put { on the same line as the block opening keyword (eg, function, if etc) rather than on the next line. Otherwise, you may run into problems with some JavaScript implementations auto-adding ; in the wrong place.
  • Using the === and !== operators are safer and better than the == and != operators as they do no coerce types.
  • The ‘new’ operator is a bad way to make new objects in JavaScript and should be avoided. Functions starting with a capital letter should always be called with the new operator by convention. Failing to do this will add all the functionality of the object you are trying to create to the global object (thanks to the this references)!
  • You can always pass any number of arguments to a function. Extra arguments are not bound to function parameters, missing arguments will be undefined. Inside the function all arguments are accessible using the ‘arguments’ variable which is an array-like object.
  • Lots of things are false. Eg, 0, NaN, empty string, null, and undefined.
  • hasOwnProperty(name) is great in for(property in object) loops to find members of your object rather inherited members.
  • Object.beget(someOtherObject) allows prototypal inheritance from someOtherObject.
  • JavaScript arrays are really just objects with numerical property names and the length property, so if you use a for in loop, you’ll get indices in random order.
  • It is a good idea to ‘namespace’ your code in a single global variable for your application to avoid conflicts with other libraries. Eg, myApp = {}; myApp.myVariable = 5;
  • If you don’t used var, a global variable is created.
  • Closures let you make information private and give you encapsulation.
  • Inner functions have access to the variables defined in an outer function.

Creating objects with private data:

var incrementer = function() {
  var value = 0;
  
  return {
    increment: function (inc) {
      value += typeof inc === 'number' ? inc : 1;
    },
    getValue: function() {
      return value;
    }
  };
};

var myIncrementer = incrementer();
 

Functional inheritance

var mammal = function(spec) {
  var that = {};
  
  that.getName = function() {
    return spec.name;
  };

  return that;
};

var myMammal = mammal({name: 'Fred'});
myMammal.getName() === 'Fred'

var cat = function(spec) {
  var that = mammal(spec);
  var super_getName = that.superior('getName');

  that.purr = function { /* new catty stuff */ };

  that.getName = function { 
    return super_getName() + ' the Cat!';
  };

  return that;
};

var myCat = cat({name: 'Kitty'});
myCat.getName() === 'Kitty the Cat!'

// Helpers

Object.method('superior', function(name) {
  var that = this, method = that[name];
  return function() { return method.apply(that, arguments); };
});

Function.prototype.method = function(name, func) {
  this.prototype[name] = func;
  return this;
};

There were a few things that I thought could be improved in the book. First of all, although the structure was adequate, it did lend itself to repetition. For example, scope is covered on p36 (in Functions section) and p102 (Awful parts), with very similar words. Secondly, I did not find the frequent syntax diagrams added much to the narrative.

Despite these small blemishes, I’m glad to have read Crockford’s book. I now understand much better which parts of JavaScript to use, and how to build good object oriented code in JavaScript.

Powered by WordPress & Theme by Anders Norén