merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sun, 03 Sep 2017 23:57:05 +0200
changeset 428172 8e05298328da75f3056a9f1f9609938870d756a0
parent 427995 dbf9f7430406ca3220529c5b4c05b26511efa3dc (current diff)
parent 428171 4f2d0ed90f58f56b61c2cbd5247783ebb5a7df4f (diff)
child 428186 fe227347dd47814f7673d8b85d7cde4fb80f7350
child 428222 3b975fe2591bc3a6954b036790b9ec169adfeb44
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone57.0a1
first release with
nightly linux32
8e05298328da / 57.0a1 / 20170903220032 / files
nightly linux64
8e05298328da / 57.0a1 / 20170903220032 / files
nightly mac
8e05298328da / 57.0a1 / 20170903220032 / files
nightly win32
8e05298328da / 57.0a1 / 20170903220032 / files
nightly win64
8e05298328da / 57.0a1 / 20170903220032 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: 6jtVMJA4WMu
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,5 +1,5 @@
 This is the PDF.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.9.512
+Current extension version is: 1.9.523
 
-Taken from upstream commit: 066fea9c
+Taken from upstream commit: 1c9af00b
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -96,17 +96,17 @@ return /******/ (function(modules) { // 
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.unreachable = exports.warn = exports.utf8StringToString = exports.stringToUTF8String = exports.stringToPDFString = exports.stringToBytes = exports.string32 = exports.shadow = exports.setVerbosityLevel = exports.ReadableStream = exports.removeNullCharacters = exports.readUint32 = exports.readUint16 = exports.readInt8 = exports.log2 = exports.loadJpegStream = exports.isEvalSupported = exports.isLittleEndian = exports.createValidAbsoluteUrl = exports.isSameOrigin = exports.isNodeJS = exports.isSpace = exports.isString = exports.isNum = exports.isInt = exports.isEmptyObj = exports.isBool = exports.isArrayBuffer = exports.isArray = exports.info = exports.getVerbosityLevel = exports.getLookupTableFactory = exports.deprecated = exports.createObjectURL = exports.createPromiseCapability = exports.createBlob = exports.bytesToString = exports.assert = exports.arraysToBytes = exports.arrayByteLength = exports.FormatError = exports.XRefParseException = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.StatTimer = exports.PasswordResponses = exports.PasswordException = exports.PageViewport = exports.NotImplementedException = exports.NativeImageDecoding = exports.MissingPDFException = exports.MissingDataException = exports.MessageHandler = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VERBOSITY_LEVELS = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = undefined;
+exports.unreachable = exports.warn = exports.utf8StringToString = exports.stringToUTF8String = exports.stringToPDFString = exports.stringToBytes = exports.string32 = exports.shadow = exports.setVerbosityLevel = exports.ReadableStream = exports.removeNullCharacters = exports.readUint32 = exports.readUint16 = exports.readInt8 = exports.log2 = exports.loadJpegStream = exports.isEvalSupported = exports.isLittleEndian = exports.createValidAbsoluteUrl = exports.isSameOrigin = exports.isNodeJS = exports.isSpace = exports.isString = exports.isNum = exports.isEmptyObj = exports.isBool = exports.isArrayBuffer = exports.info = exports.getVerbosityLevel = exports.getLookupTableFactory = exports.deprecated = exports.createObjectURL = exports.createPromiseCapability = exports.createBlob = exports.bytesToString = exports.assert = exports.arraysToBytes = exports.arrayByteLength = exports.FormatError = exports.XRefParseException = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.StatTimer = exports.PasswordResponses = exports.PasswordException = exports.PageViewport = exports.NotImplementedException = exports.NativeImageDecoding = exports.MissingPDFException = exports.MissingDataException = exports.MessageHandler = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VERBOSITY_LEVELS = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = undefined;
 
 __w_pdfjs_require__(16);
 
 var _streams_polyfill = __w_pdfjs_require__(17);
 
 var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 const NativeImageDecoding = {
   NONE: 'none',
@@ -709,17 +709,17 @@ var Util = function UtilClosure() {
     }
     return result;
   };
   Util.sign = function Util_sign(num) {
     return num < 0 ? -1 : 1;
   };
   var ROMAN_NUMBER_MAP = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'];
   Util.toRoman = function Util_toRoman(number, lowerCase) {
-    assert(isInt(number) && number > 0, 'The number should be a positive integer.');
+    assert(Number.isInteger(number) && number > 0, 'The number should be a positive integer.');
     var pos,
         romanBuf = [];
     while (number >= 1000) {
       number -= 1000;
       romanBuf.push('M');
     }
     pos = number / 100 | 0;
     number %= 100;
@@ -882,28 +882,22 @@ function isEmptyObj(obj) {
   for (var key in obj) {
     return false;
   }
   return true;
 }
 function isBool(v) {
   return typeof v === 'boolean';
 }
-function isInt(v) {
-  return typeof v === 'number' && (v | 0) === v;
-}
 function isNum(v) {
   return typeof v === 'number';
 }
 function isString(v) {
   return typeof v === 'string';
 }
-function isArray(v) {
-  return v instanceof Array;
-}
 function isArrayBuffer(v) {
   return typeof v === 'object' && v !== null && v.byteLength !== undefined;
 }
 function isSpace(ch) {
   return ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A;
 }
 function isNodeJS() {
   return typeof process === 'object' && process + '' === '[object process]';
@@ -1425,21 +1419,19 @@ exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.createBlob = createBlob;
 exports.createPromiseCapability = createPromiseCapability;
 exports.createObjectURL = createObjectURL;
 exports.deprecated = deprecated;
 exports.getLookupTableFactory = getLookupTableFactory;
 exports.getVerbosityLevel = getVerbosityLevel;
 exports.info = info;
-exports.isArray = isArray;
 exports.isArrayBuffer = isArrayBuffer;
 exports.isBool = isBool;
 exports.isEmptyObj = isEmptyObj;
-exports.isInt = isInt;
 exports.isNum = isNum;
 exports.isString = isString;
 exports.isSpace = isSpace;
 exports.isNodeJS = isNodeJS;
 exports.isSameOrigin = isSameOrigin;
 exports.createValidAbsoluteUrl = createValidAbsoluteUrl;
 exports.isLittleEndian = isLittleEndian;
 exports.isEvalSupported = isEvalSupported;
@@ -3509,17 +3501,17 @@ var WorkerTransport = function WorkerTra
         }
         return this.CMapReaderFactory.fetch({ name: data.name });
       }, this);
     },
     getData: function WorkerTransport_getData() {
       return this.messageHandler.sendWithPromise('GetData', null);
     },
     getPage: function WorkerTransport_getPage(pageNumber, capability) {
-      if (!(0, _util.isInt)(pageNumber) || pageNumber <= 0 || pageNumber > this.numPages) {
+      if (!Number.isInteger(pageNumber) || pageNumber <= 0 || pageNumber > this.numPages) {
         return Promise.reject(new Error('Invalid page request'));
       }
       var pageIndex = pageNumber - 1;
       if (pageIndex in this.pagePromises) {
         return this.pagePromises[pageIndex];
       }
       var promise = this.messageHandler.sendWithPromise('GetPage', { pageIndex }).then(pageInfo => {
         if (this.destroyed) {
@@ -3789,18 +3781,18 @@ var _UnsupportedManager = function Unsup
       for (var i = 0, ii = listeners.length; i < ii; i++) {
         listeners[i](featureId);
       }
     }
   };
 }();
 var version, build;
 {
-  exports.version = version = '1.9.512';
-  exports.build = build = '066fea9c';
+  exports.version = version = '1.9.523';
+  exports.build = build = '1c9af00b';
 }
 exports.getDocument = getDocument;
 exports.LoopbackPort = LoopbackPort;
 exports.PDFDataRangeTransport = PDFDataRangeTransport;
 exports.PDFWorker = PDFWorker;
 exports.PDFDocumentProxy = PDFDocumentProxy;
 exports.PDFPageProxy = PDFPageProxy;
 exports.setPDFNetworkStreamClass = setPDFNetworkStreamClass;
@@ -4846,18 +4838,18 @@ var _svg = __w_pdfjs_require__(5);
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 var isWorker = typeof window === 'undefined';
 if (!_global_scope2.default.PDFJS) {
   _global_scope2.default.PDFJS = {};
 }
 var PDFJS = _global_scope2.default.PDFJS;
 {
-  PDFJS.version = '1.9.512';
-  PDFJS.build = '066fea9c';
+  PDFJS.version = '1.9.523';
+  PDFJS.build = '1c9af00b';
 }
 PDFJS.pdfBug = false;
 if (PDFJS.verbosity !== undefined) {
   (0, _util.setVerbosityLevel)(PDFJS.verbosity);
 }
 delete PDFJS.verbosity;
 Object.defineProperty(PDFJS, 'verbosity', {
   get() {
@@ -9290,21 +9282,21 @@ var CanvasGraphics = function CanvasGrap
       throw new Error('Should not call beginInlineImage');
     },
     beginImageData: function CanvasGraphics_beginImageData() {
       throw new Error('Should not call beginImageData');
     },
     paintFormXObjectBegin: function CanvasGraphics_paintFormXObjectBegin(matrix, bbox) {
       this.save();
       this.baseTransformStack.push(this.baseTransform);
-      if ((0, _util.isArray)(matrix) && matrix.length === 6) {
+      if (Array.isArray(matrix) && matrix.length === 6) {
         this.transform.apply(this, matrix);
       }
       this.baseTransform = this.ctx.mozCurrentTransform;
-      if ((0, _util.isArray)(bbox) && bbox.length === 4) {
+      if (Array.isArray(bbox) && bbox.length === 4) {
         var width = bbox[2] - bbox[0];
         var height = bbox[3] - bbox[1];
         this.ctx.rect(bbox[0], bbox[1], width, height);
         this.clip();
         this.endPath();
       }
     },
     paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() {
@@ -9402,17 +9394,17 @@ var CanvasGraphics = function CanvasGrap
     },
     endAnnotations: function CanvasGraphics_endAnnotations() {
       this.restore();
     },
     beginAnnotation: function CanvasGraphics_beginAnnotation(rect, transform, matrix) {
       this.save();
       resetCtxToDefault(this.ctx);
       this.current = new CanvasExtraState();
-      if ((0, _util.isArray)(rect) && rect.length === 4) {
+      if (Array.isArray(rect) && rect.length === 4) {
         var width = rect[2] - rect[0];
         var height = rect[3] - rect[1];
         this.ctx.rect(rect[0], rect[1], width, height);
         this.clip();
         this.endPath();
       }
       this.transform.apply(this, transform);
       this.transform.apply(this, matrix);
@@ -10132,17 +10124,17 @@ var TilingPattern = function TilingPatte
       var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
       graphics.transform.apply(graphics, tmpScale);
     },
     scaleToContext: function TilingPattern_scaleToContext() {
       var scale = this.scale;
       this.ctx.scale(1 / scale[0], 1 / scale[1]);
     },
     clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
-      if ((0, _util.isArray)(bbox) && bbox.length === 4) {
+      if (Array.isArray(bbox) && bbox.length === 4) {
         var bboxWidth = x1 - x0;
         var bboxHeight = y1 - y0;
         graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
         graphics.clip();
         graphics.endPath();
       }
     },
     setFillAndStrokeStyleToContext: function setFillAndStrokeStyleToContext(context, paintType, color) {
@@ -10413,18 +10405,18 @@ exports.PDFDataTransportStream = PDFData
 
 /***/ }),
 /* 15 */
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
 
 
-var pdfjsVersion = '1.9.512';
-var pdfjsBuild = '066fea9c';
+var pdfjsVersion = '1.9.523';
+var pdfjsBuild = '1c9af00b';
 var pdfjsSharedUtil = __w_pdfjs_require__(0);
 var pdfjsDisplayGlobal = __w_pdfjs_require__(9);
 var pdfjsDisplayAPI = __w_pdfjs_require__(4);
 var pdfjsDisplayTextLayer = __w_pdfjs_require__(6);
 var pdfjsDisplayAnnotationLayer = __w_pdfjs_require__(3);
 var pdfjsDisplayDOMUtils = __w_pdfjs_require__(1);
 var pdfjsDisplaySVG = __w_pdfjs_require__(5);
 ;
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -96,17 +96,17 @@ return /******/ (function(modules) { // 
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.unreachable = exports.warn = exports.utf8StringToString = exports.stringToUTF8String = exports.stringToPDFString = exports.stringToBytes = exports.string32 = exports.shadow = exports.setVerbosityLevel = exports.ReadableStream = exports.removeNullCharacters = exports.readUint32 = exports.readUint16 = exports.readInt8 = exports.log2 = exports.loadJpegStream = exports.isEvalSupported = exports.isLittleEndian = exports.createValidAbsoluteUrl = exports.isSameOrigin = exports.isNodeJS = exports.isSpace = exports.isString = exports.isNum = exports.isInt = exports.isEmptyObj = exports.isBool = exports.isArrayBuffer = exports.isArray = exports.info = exports.getVerbosityLevel = exports.getLookupTableFactory = exports.deprecated = exports.createObjectURL = exports.createPromiseCapability = exports.createBlob = exports.bytesToString = exports.assert = exports.arraysToBytes = exports.arrayByteLength = exports.FormatError = exports.XRefParseException = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.StatTimer = exports.PasswordResponses = exports.PasswordException = exports.PageViewport = exports.NotImplementedException = exports.NativeImageDecoding = exports.MissingPDFException = exports.MissingDataException = exports.MessageHandler = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VERBOSITY_LEVELS = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = undefined;
+exports.unreachable = exports.warn = exports.utf8StringToString = exports.stringToUTF8String = exports.stringToPDFString = exports.stringToBytes = exports.string32 = exports.shadow = exports.setVerbosityLevel = exports.ReadableStream = exports.removeNullCharacters = exports.readUint32 = exports.readUint16 = exports.readInt8 = exports.log2 = exports.loadJpegStream = exports.isEvalSupported = exports.isLittleEndian = exports.createValidAbsoluteUrl = exports.isSameOrigin = exports.isNodeJS = exports.isSpace = exports.isString = exports.isNum = exports.isEmptyObj = exports.isBool = exports.isArrayBuffer = exports.info = exports.getVerbosityLevel = exports.getLookupTableFactory = exports.deprecated = exports.createObjectURL = exports.createPromiseCapability = exports.createBlob = exports.bytesToString = exports.assert = exports.arraysToBytes = exports.arrayByteLength = exports.FormatError = exports.XRefParseException = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.StatTimer = exports.PasswordResponses = exports.PasswordException = exports.PageViewport = exports.NotImplementedException = exports.NativeImageDecoding = exports.MissingPDFException = exports.MissingDataException = exports.MessageHandler = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VERBOSITY_LEVELS = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = undefined;
 
 __w_pdfjs_require__(36);
 
 var _streams_polyfill = __w_pdfjs_require__(37);
 
 var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 const NativeImageDecoding = {
   NONE: 'none',
@@ -709,17 +709,17 @@ var Util = function UtilClosure() {
     }
     return result;
   };
   Util.sign = function Util_sign(num) {
     return num < 0 ? -1 : 1;
   };
   var ROMAN_NUMBER_MAP = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'];
   Util.toRoman = function Util_toRoman(number, lowerCase) {
-    assert(isInt(number) && number > 0, 'The number should be a positive integer.');
+    assert(Number.isInteger(number) && number > 0, 'The number should be a positive integer.');
     var pos,
         romanBuf = [];
     while (number >= 1000) {
       number -= 1000;
       romanBuf.push('M');
     }
     pos = number / 100 | 0;
     number %= 100;
@@ -882,28 +882,22 @@ function isEmptyObj(obj) {
   for (var key in obj) {
     return false;
   }
   return true;
 }
 function isBool(v) {
   return typeof v === 'boolean';
 }
-function isInt(v) {
-  return typeof v === 'number' && (v | 0) === v;
-}
 function isNum(v) {
   return typeof v === 'number';
 }
 function isString(v) {
   return typeof v === 'string';
 }
-function isArray(v) {
-  return v instanceof Array;
-}
 function isArrayBuffer(v) {
   return typeof v === 'object' && v !== null && v.byteLength !== undefined;
 }
 function isSpace(ch) {
   return ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A;
 }
 function isNodeJS() {
   return typeof process === 'object' && process + '' === '[object process]';
@@ -1425,21 +1419,19 @@ exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.createBlob = createBlob;
 exports.createPromiseCapability = createPromiseCapability;
 exports.createObjectURL = createObjectURL;
 exports.deprecated = deprecated;
 exports.getLookupTableFactory = getLookupTableFactory;
 exports.getVerbosityLevel = getVerbosityLevel;
 exports.info = info;
-exports.isArray = isArray;
 exports.isArrayBuffer = isArrayBuffer;
 exports.isBool = isBool;
 exports.isEmptyObj = isEmptyObj;
-exports.isInt = isInt;
 exports.isNum = isNum;
 exports.isString = isString;
 exports.isSpace = isSpace;
 exports.isNodeJS = isNodeJS;
 exports.isSameOrigin = isSameOrigin;
 exports.createValidAbsoluteUrl = createValidAbsoluteUrl;
 exports.isLittleEndian = isLittleEndian;
 exports.isEvalSupported = isEvalSupported;
@@ -1465,20 +1457,16 @@ exports.unreachable = unreachable;
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.isStream = exports.isRefsEqual = exports.isRef = exports.isName = exports.isDict = exports.isCmd = exports.isEOF = exports.RefSetCache = exports.RefSet = exports.Ref = exports.Name = exports.Dict = exports.Cmd = exports.EOF = undefined;
-
-var _util = __w_pdfjs_require__(0);
-
 var EOF = {};
 var Name = function NameClosure() {
   function Name(name) {
     this.name = name;
   }
   Name.prototype = {};
   var nameCache = Object.create(null);
   Name.get = function Name_get(name) {
@@ -1548,17 +1536,17 @@ var Dict = function DictClosure() {
         return xref.fetchIfRefAsync(value, suppressEncryption);
       }
       return Promise.resolve(value);
     },
     getArray: function Dict_getArray(key1, key2, key3) {
       var value = this.get(key1, key2, key3);
       var xref = this.xref,
           suppressEncryption = this.suppressEncryption;
-      if (!(0, _util.isArray)(value) || !xref) {
+      if (!Array.isArray(value) || !xref) {
         return value;
       }
       value = value.slice();
       for (var i = 0, ii = value.length; i < ii; i++) {
         if (!isRef(value[i])) {
           continue;
         }
         value[i] = xref.fetch(value[i], suppressEncryption);
@@ -2398,17 +2386,17 @@ var JpegStream = function JpegStreamClos
     configurable: true
   });
   JpegStream.prototype.ensureBuffer = function JpegStream_ensureBuffer(req) {
     if (this.bufferLength) {
       return;
     }
     var jpegImage = new _jpg.JpegImage();
     var decodeArr = this.dict.getArray('Decode', 'D');
-    if (this.forceRGB && (0, _util.isArray)(decodeArr)) {
+    if (this.forceRGB && Array.isArray(decodeArr)) {
       var bitsPerComponent = this.dict.get('BitsPerComponent') || 8;
       var decodeArrLength = decodeArr.length;
       var transform = new Int32Array(decodeArrLength);
       var transformNeeded = false;
       var maxValue = (1 << bitsPerComponent) - 1;
       for (var i = 0; i < decodeArrLength; i += 2) {
         transform[i] = (decodeArr[i + 1] - decodeArr[i]) * 256 | 0;
         transform[i + 1] = decodeArr[i] * maxValue | 0;
@@ -2417,17 +2405,17 @@ var JpegStream = function JpegStreamClos
         }
       }
       if (transformNeeded) {
         jpegImage.decodeTransform = transform;
       }
     }
     if ((0, _primitives.isDict)(this.params)) {
       var colorTransform = this.params.get('ColorTransform');
-      if ((0, _util.isInt)(colorTransform)) {
+      if (Number.isInteger(colorTransform)) {
         jpegImage.colorTransform = colorTransform;
       }
     }
     jpegImage.parse(this.bytes);
     var data = jpegImage.getData(this.drawWidth, this.drawHeight, this.forceRGB);
     this.buffer = data;
     this.bufferLength = data.length;
     this.eof = true;
@@ -3521,17 +3509,17 @@ var ColorSpace = function ColorSpaceClos
   ColorSpace.parse = function ColorSpace_parse(cs, xref, res) {
     var IR = ColorSpace.parseToIR(cs, xref, res);
     if (IR instanceof AlternateCS) {
       return IR;
     }
     return ColorSpace.fromIR(IR);
   };
   ColorSpace.fromIR = function ColorSpace_fromIR(IR) {
-    var name = (0, _util.isArray)(IR) ? IR[0] : IR;
+    var name = Array.isArray(IR) ? IR[0] : IR;
     var whitePoint, blackPoint, gamma;
     switch (name) {
       case 'DeviceGrayCS':
         return this.singletons.gray;
       case 'DeviceRgbCS':
         return this.singletons.rgb;
       case 'DeviceCmykCS':
         return this.singletons.cmyk;
@@ -3594,17 +3582,17 @@ var ColorSpace = function ColorSpaceClos
         case 'CMYK':
           return 'DeviceCmykCS';
         case 'Pattern':
           return ['PatternCS', null];
         default:
           throw new _util.FormatError(`unrecognized colorspace ${cs.name}`);
       }
     }
-    if ((0, _util.isArray)(cs)) {
+    if (Array.isArray(cs)) {
       var mode = xref.fetchIfRef(cs[0]).name;
       var numComps, params, alt, whitePoint, blackPoint, gamma;
       switch (mode) {
         case 'DeviceGray':
         case 'G':
           return 'DeviceGrayCS';
         case 'DeviceRGB':
         case 'RGB':
@@ -3659,34 +3647,34 @@ var ColorSpace = function ColorSpaceClos
           var lookup = xref.fetchIfRef(cs[3]);
           if ((0, _primitives.isStream)(lookup)) {
             lookup = lookup.getBytes();
           }
           return ['IndexedCS', baseIndexedCS, hiVal, lookup];
         case 'Separation':
         case 'DeviceN':
           var name = xref.fetchIfRef(cs[1]);
-          numComps = (0, _util.isArray)(name) ? name.length : 1;
+          numComps = Array.isArray(name) ? name.length : 1;
           alt = ColorSpace.parseToIR(cs[2], xref, res);
           var tintFnIR = _function.PDFFunction.getIR(xref, xref.fetchIfRef(cs[3]));
           return ['AlternateCS', numComps, alt, tintFnIR];
         case 'Lab':
           params = xref.fetchIfRef(cs[1]);
           whitePoint = params.getArray('WhitePoint');
           blackPoint = params.getArray('BlackPoint');
           var range = params.getArray('Range');
           return ['LabCS', whitePoint, blackPoint, range];
         default:
           throw new _util.FormatError(`unimplemented color space object "${mode}"`);
       }
     }
     throw new _util.FormatError(`unrecognized color space object: "${cs}"`);
   };
   ColorSpace.isDefaultDecode = function ColorSpace_isDefaultDecode(decode, n) {
-    if (!(0, _util.isArray)(decode)) {
+    if (!Array.isArray(decode)) {
       return true;
     }
     if (n * 2 !== decode.length) {
       (0, _util.warn)('The decode map is not the correct length');
       return true;
     }
     for (var i = 0, ii = decode.length; i < ii; i += 2) {
       if (decode[i] !== 0 || decode[i + 1] !== 1) {
@@ -4462,19 +4450,19 @@ var Parser = function ParserClosure() {
               return this.allowStreams ? this.makeStream(dict, cipherTransform) : dict;
             }
             this.shift();
             return dict;
           default:
             return buf1;
         }
       }
-      if ((0, _util.isInt)(buf1)) {
+      if (Number.isInteger(buf1)) {
         var num = buf1;
-        if ((0, _util.isInt)(this.buf1) && (0, _primitives.isCmd)(this.buf2, 'R')) {
+        if (Number.isInteger(this.buf1) && (0, _primitives.isCmd)(this.buf2, 'R')) {
           var ref = new _primitives.Ref(num, this.buf1);
           this.shift();
           this.shift();
           return ref;
         }
         return num;
       }
       if ((0, _util.isString)(buf1)) {
@@ -4682,17 +4670,17 @@ var Parser = function ParserClosure() {
           break;
         }
         dict.set(key, this.getObj(cipherTransform));
       }
       var filter = dict.get('Filter', 'F'),
           filterName;
       if ((0, _primitives.isName)(filter)) {
         filterName = filter.name;
-      } else if ((0, _util.isArray)(filter)) {
+      } else if (Array.isArray(filter)) {
         var filterZero = this.xref.fetchIfRef(filter[0]);
         if ((0, _primitives.isName)(filterZero)) {
           filterName = filterZero.name;
         }
       }
       var startPos = stream.pos,
           length,
           i,
@@ -4740,17 +4728,17 @@ var Parser = function ParserClosure() {
       return imageStream;
     },
     makeStream: function Parser_makeStream(dict, cipherTransform) {
       var lexer = this.lexer;
       var stream = lexer.stream;
       lexer.skipToNextLine();
       var pos = stream.pos - 1;
       var length = dict.get('Length');
-      if (!(0, _util.isInt)(length)) {
+      if (!Number.isInteger(length)) {
         (0, _util.info)('Bad ' + length + ' attribute in stream');
         length = 0;
       }
       stream.pos = pos + length;
       lexer.nextChar();
       if (this.tryShift() && (0, _primitives.isCmd)(this.buf2, 'endstream')) {
         this.shift();
       } else {
@@ -4805,32 +4793,32 @@ var Parser = function ParserClosure() {
       stream = this.filter(stream, dict, length);
       stream.dict = dict;
       return stream;
     },
     filter: function Parser_filter(stream, dict, length) {
       var filter = dict.get('Filter', 'F');
       var params = dict.get('DecodeParms', 'DP');
       if ((0, _primitives.isName)(filter)) {
-        if ((0, _util.isArray)(params)) {
+        if (Array.isArray(params)) {
           params = this.xref.fetchIfRef(params[0]);
         }
         return this.makeFilter(stream, filter.name, length, params);
       }
       var maybeLength = length;
-      if ((0, _util.isArray)(filter)) {
+      if (Array.isArray(filter)) {
         var filterArray = filter;
         var paramsArray = params;
         for (var i = 0, ii = filterArray.length; i < ii; ++i) {
           filter = this.xref.fetchIfRef(filterArray[i]);
           if (!(0, _primitives.isName)(filter)) {
             throw new _util.FormatError('Bad filter name: ' + filter);
           }
           params = null;
-          if ((0, _util.isArray)(paramsArray) && i in paramsArray) {
+          if (Array.isArray(paramsArray) && i in paramsArray) {
             params = this.xref.fetchIfRef(paramsArray[i]);
           }
           stream = this.makeFilter(stream, filter.name, maybeLength, params);
           maybeLength = null;
         }
       }
       return stream;
     },
@@ -5280,42 +5268,42 @@ var Lexer = function LexerClosure() {
     }
   };
   return Lexer;
 }();
 var Linearization = {
   create: function LinearizationCreate(stream) {
     function getInt(name, allowZeroValue) {
       var obj = linDict.get(name);
-      if ((0, _util.isInt)(obj) && (allowZeroValue ? obj >= 0 : obj > 0)) {
+      if (Number.isInteger(obj) && (allowZeroValue ? obj >= 0 : obj > 0)) {
         return obj;
       }
       throw new Error('The "' + name + '" parameter in the linearization ' + 'dictionary is invalid.');
     }
     function getHints() {
       var hints = linDict.get('H'),
           hintsLength,
           item;
-      if ((0, _util.isArray)(hints) && ((hintsLength = hints.length) === 2 || hintsLength === 4)) {
+      if (Array.isArray(hints) && ((hintsLength = hints.length) === 2 || hintsLength === 4)) {
         for (var index = 0; index < hintsLength; index++) {
-          if (!((0, _util.isInt)(item = hints[index]) && item > 0)) {
+          if (!(Number.isInteger(item = hints[index]) && item > 0)) {
             throw new Error('Hint (' + index + ') in the linearization dictionary is invalid.');
           }
         }
         return hints;
       }
       throw new Error('Hint array in the linearization dictionary is invalid.');
     }
     var parser = new Parser(new Lexer(stream), false, null);
     var obj1 = parser.getObj();
     var obj2 = parser.getObj();
     var obj3 = parser.getObj();
     var linDict = parser.getObj();
     var obj, length;
-    if (!((0, _util.isInt)(obj1) && (0, _util.isInt)(obj2) && (0, _primitives.isCmd)(obj3, 'obj') && (0, _primitives.isDict)(linDict) && (0, _util.isNum)(obj = linDict.get('Linearized')) && obj > 0)) {
+    if (!(Number.isInteger(obj1) && Number.isInteger(obj2) && (0, _primitives.isCmd)(obj3, 'obj') && (0, _primitives.isDict)(linDict) && (0, _util.isNum)(obj = linDict.get('Linearized')) && obj > 0)) {
       return null;
     } else if ((length = getInt('L')) !== stream.length) {
       throw new Error('The "L" parameter in the linearization dictionary ' + 'does not equal the stream length.');
     }
     return {
       length,
       hints: getHints(),
       objectNumberFirst: getInt('O'),
@@ -9941,17 +9929,17 @@ var PDFFunction = function PDFFunctionCl
           return this.constructPostScriptFromIR(IR);
       }
     },
     parse: function PDFFunction_parse(xref, fn) {
       var IR = this.getIR(xref, fn);
       return this.fromIR(IR);
     },
     parseArray: function PDFFunction_parseArray(xref, fnObj) {
-      if (!(0, _util.isArray)(fnObj)) {
+      if (!Array.isArray(fnObj)) {
         return this.parse(xref, fnObj);
       }
       var fnArray = [];
       for (var j = 0, jj = fnObj.length; j < jj; j++) {
         var obj = xref.fetchIfRef(fnObj[j]);
         fnArray.push(PDFFunction.parse(xref, obj));
       }
       return function (src, srcOffset, dest, destOffset) {
@@ -10059,17 +10047,17 @@ var PDFFunction = function PDFFunctionCl
           dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]), range[j][1]);
         }
       };
     },
     constructInterpolated: function PDFFunction_constructInterpolated(str, dict) {
       var c0 = dict.getArray('C0') || [0];
       var c1 = dict.getArray('C1') || [1];
       var n = dict.get('N');
-      if (!(0, _util.isArray)(c0) || !(0, _util.isArray)(c1)) {
+      if (!Array.isArray(c0) || !Array.isArray(c1)) {
         throw new _util.FormatError('Illegal dictionary for interpolated function');
       }
       var length = c0.length;
       var diff = [];
       for (var i = 0; i < length; ++i) {
         diff.push(c1[i] - c0[i]);
       }
       return [CONSTRUCT_INTERPOLATED, c0, diff, n];
@@ -13803,17 +13791,17 @@ var CFFParser = function CFFParserClosur
       parentDict.privateDict = privateDict;
     },
     parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) {
       if (!parentDict.hasName('Private')) {
         this.emptyPrivateDictionary(parentDict);
         return;
       }
       var privateOffset = parentDict.getByName('Private');
-      if (!(0, _util.isArray)(privateOffset) || privateOffset.length !== 2) {
+      if (!Array.isArray(privateOffset) || privateOffset.length !== 2) {
         parentDict.removeByName('Private');
         return;
       }
       var size = privateOffset[0];
       var offset = privateOffset[1];
       if (size === 0 || offset >= this.bytes.length) {
         this.emptyPrivateDictionary(parentDict);
         return;
@@ -14120,22 +14108,22 @@ var CFFDict = function CFFDictClosure() 
       nameToKeyMap: {},
       defaults: {},
       types: {},
       opcodes: {},
       order: []
     };
     for (var i = 0, ii = layout.length; i < ii; ++i) {
       var entry = layout[i];
-      var key = (0, _util.isArray)(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0];
+      var key = Array.isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0];
       tables.keyToNameMap[key] = entry[1];
       tables.nameToKeyMap[entry[1]] = key;
       tables.types[key] = entry[2];
       tables.defaults[key] = entry[3];
-      tables.opcodes[key] = (0, _util.isArray)(entry[0]) ? entry[0] : [entry[0]];
+      tables.opcodes[key] = Array.isArray(entry[0]) ? entry[0] : [entry[0]];
       tables.order.push(key);
     }
     return tables;
   };
   return CFFDict;
 }();
 var CFFTopDict = function CFFTopDictClosure() {
   var layout = [[[12, 30], 'ROS', ['sid', 'sid', 'num'], null], [[12, 20], 'SyntheticBase', 'num', null], [0, 'version', 'sid', null], [1, 'Notice', 'sid', null], [[12, 0], 'Copyright', 'sid', null], [2, 'FullName', 'sid', null], [3, 'FamilyName', 'sid', null], [4, 'Weight', 'sid', null], [[12, 1], 'isFixedPitch', 'num', 0], [[12, 2], 'ItalicAngle', 'num', 0], [[12, 3], 'UnderlinePosition', 'num', -100], [[12, 4], 'UnderlineThickness', 'num', 50], [[12, 5], 'PaintType', 'num', 0], [[12, 6], 'CharstringType', 'num', 2], [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'], [0.001, 0, 0, 0.001, 0, 0]], [13, 'UniqueID', 'num', null], [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]], [[12, 8], 'StrokeWidth', 'num', 0], [14, 'XUID', 'array', null], [15, 'charset', 'offset', 0], [16, 'Encoding', 'offset', 0], [17, 'CharStrings', 'offset', 0], [18, 'Private', ['offset', 'offset'], null], [[12, 21], 'PostScript', 'sid', null], [[12, 22], 'BaseFontName', 'sid', null], [[12, 23], 'BaseFontBlend', 'delta', null], [[12, 31], 'CIDFontVersion', 'num', 0], [[12, 32], 'CIDFontRevision', 'num', 0], [[12, 33], 'CIDFontType', 'num', 0], [[12, 34], 'CIDCount', 'num', 8720], [[12, 35], 'UIDBase', 'num', null], [[12, 37], 'FDSelect', 'offset', null], [[12, 36], 'FDArray', 'offset', null], [[12, 38], 'FontName', 'sid', null]];
@@ -14435,20 +14423,20 @@ var CFFCompiler = function CFFCompilerCl
       var order = dict.order;
       for (var i = 0; i < order.length; ++i) {
         var key = order[i];
         if (!(key in dict.values)) {
           continue;
         }
         var values = dict.values[key];
         var types = dict.types[key];
-        if (!(0, _util.isArray)(types)) {
+        if (!Array.isArray(types)) {
           types = [types];
         }
-        if (!(0, _util.isArray)(values)) {
+        if (!Array.isArray(values)) {
           values = [values];
         }
         if (values.length === 0) {
           continue;
         }
         for (var j = 0, jj = types.length; j < jj; ++j) {
           var type = types[j];
           var value = values[j];
@@ -14993,17 +14981,17 @@ var ChunkedStreamManager = function Chun
         if (this.stream.numChunksLoaded === 1) {
           var lastChunk = this.stream.numChunks - 1;
           if (!this.stream.hasChunk(lastChunk)) {
             nextEmptyChunk = lastChunk;
           }
         } else {
           nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
         }
-        if ((0, _util.isInt)(nextEmptyChunk)) {
+        if (Number.isInteger(nextEmptyChunk)) {
           this._requestChunks([nextEmptyChunk]);
         }
       }
       for (i = 0; i < loadedRequests.length; ++i) {
         requestId = loadedRequests[i];
         var capability = this.promisesByRequest[requestId];
         delete this.promisesByRequest[requestId];
         capability.resolve();
@@ -16474,17 +16462,17 @@ var CipherTransformFactory = function Ci
   var identityName = _primitives.Name.get('Identity');
   function CipherTransformFactory(dict, fileId, password) {
     var filter = dict.get('Filter');
     if (!(0, _primitives.isName)(filter, 'Standard')) {
       throw new _util.FormatError('unknown encryption method');
     }
     this.dict = dict;
     var algorithm = dict.get('V');
-    if (!(0, _util.isInt)(algorithm) || algorithm !== 1 && algorithm !== 2 && algorithm !== 4 && algorithm !== 5) {
+    if (!Number.isInteger(algorithm) || algorithm !== 1 && algorithm !== 2 && algorithm !== 4 && algorithm !== 5) {
       throw new _util.FormatError('unsupported encryption algorithm');
     }
     this.algorithm = algorithm;
     var keyLength = dict.get('Length');
     if (!keyLength) {
       if (algorithm <= 3) {
         keyLength = 40;
       } else {
@@ -16495,17 +16483,17 @@ var CipherTransformFactory = function Ci
           var handlerDict = cfDict.get(streamCryptoName.name);
           keyLength = handlerDict && handlerDict.get('Length') || 128;
           if (keyLength < 40) {
             keyLength <<= 3;
           }
         }
       }
     }
-    if (!(0, _util.isInt)(keyLength) || keyLength < 40 || keyLength % 8 !== 0) {
+    if (!Number.isInteger(keyLength) || keyLength < 40 || keyLength % 8 !== 0) {
       throw new _util.FormatError('invalid key length');
     }
     var ownerPassword = (0, _util.stringToBytes)(dict.get('O')).subarray(0, 32);
     var userPassword = (0, _util.stringToBytes)(dict.get('U')).subarray(0, 32);
     var flags = dict.get('P');
     var revision = dict.get('R');
     var encryptMetadata = (algorithm === 4 || algorithm === 5) && dict.get('EncryptMetadata') !== false;
     this.encryptMetadata = encryptMetadata;
@@ -17934,17 +17922,17 @@ var PartialEvaluator = function PartialE
               }
               if (type.name !== 'Form') {
                 skipEmptyXObjs[name] = true;
                 break;
               }
               var currentState = stateManager.state.clone();
               var xObjStateManager = new StateManager(currentState);
               var matrix = xobj.dict.getArray('Matrix');
-              if ((0, _util.isArray)(matrix) && matrix.length === 6) {
+              if (Array.isArray(matrix) && matrix.length === 6) {
                 xObjStateManager.transform(matrix);
               }
               enqueueChunk();
               let sinkWrapper = {
                 enqueueInvoked: false,
                 enqueue(chunk, size) {
                   this.enqueueInvoked = true;
                   sink.enqueue(chunk, size);
@@ -18251,17 +18239,17 @@ var PartialEvaluator = function PartialE
       var i, ii, j, jj, start, code, widths;
       if (properties.composite) {
         defaultWidth = dict.get('DW') || 1000;
         widths = dict.get('W');
         if (widths) {
           for (i = 0, ii = widths.length; i < ii; i++) {
             start = xref.fetchIfRef(widths[i++]);
             code = xref.fetchIfRef(widths[i]);
-            if ((0, _util.isArray)(code)) {
+            if (Array.isArray(code)) {
               for (j = 0, jj = code.length; j < jj; j++) {
                 glyphsWidths[start++] = xref.fetchIfRef(code[j]);
               }
             } else {
               var width = xref.fetchIfRef(widths[++i]);
               for (j = start; j <= code; j++) {
                 glyphsWidths[j] = width;
               }
@@ -18271,17 +18259,17 @@ var PartialEvaluator = function PartialE
         if (properties.vertical) {
           var vmetrics = dict.getArray('DW2') || [880, -1000];
           defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]];
           vmetrics = dict.get('W2');
           if (vmetrics) {
             for (i = 0, ii = vmetrics.length; i < ii; i++) {
               start = xref.fetchIfRef(vmetrics[i++]);
               code = xref.fetchIfRef(vmetrics[i]);
-              if ((0, _util.isArray)(code)) {
+              if (Array.isArray(code)) {
                 for (j = 0, jj = code.length; j < jj; j++) {
                   glyphsVMetrics[start++] = [xref.fetchIfRef(code[j++]), xref.fetchIfRef(code[j++]), xref.fetchIfRef(code[j])];
                 }
               } else {
                 var vmetric = [xref.fetchIfRef(vmetrics[++i]), xref.fetchIfRef(vmetrics[++i]), xref.fetchIfRef(vmetrics[++i])];
                 for (j = start; j <= code; j++) {
                   glyphsVMetrics[j] = vmetric;
                 }
@@ -18386,17 +18374,17 @@ var PartialEvaluator = function PartialE
       }
       var composite = false;
       var uint8array;
       if (type.name === 'Type0') {
         var df = dict.get('DescendantFonts');
         if (!df) {
           throw new _util.FormatError('Descendant fonts are not specified');
         }
-        dict = (0, _util.isArray)(df) ? this.xref.fetchIfRef(df[0]) : df;
+        dict = Array.isArray(df) ? this.xref.fetchIfRef(df[0]) : df;
         type = dict.get('Subtype');
         if (!(0, _primitives.isName)(type)) {
           throw new _util.FormatError('invalid font Subtype');
         }
         composite = true;
       }
       var descriptor = dict.get('FontDescriptor');
       if (descriptor) {
@@ -18409,17 +18397,17 @@ var PartialEvaluator = function PartialE
         } else if ((0, _primitives.isDict)(encoding)) {
           var keys = encoding.getKeys();
           for (var i = 0, ii = keys.length; i < ii; i++) {
             var entry = encoding.getRaw(keys[i]);
             if ((0, _primitives.isName)(entry)) {
               hash.update(entry.name);
             } else if ((0, _primitives.isRef)(entry)) {
               hash.update(entry.toString());
-            } else if ((0, _util.isArray)(entry)) {
+            } else if (Array.isArray(entry)) {
               var diffLength = entry.length,
                   diffBuf = new Array(diffLength);
               for (var j = 0; j < diffLength; j++) {
                 var diffEntry = entry[j];
                 if ((0, _primitives.isName)(diffEntry)) {
                   diffBuf[j] = diffEntry.name;
                 } else if ((0, _util.isNum)(diffEntry) || (0, _primitives.isRef)(diffEntry)) {
                   diffBuf[j] = diffEntry.toString();
@@ -21673,17 +21661,17 @@ var Catalog = function CatalogClosure() 
           destDict: outlineDict,
           resultObj: data,
           docBaseUrl: this.pdfManager.docBaseUrl
         });
         var title = outlineDict.get('Title');
         var flags = outlineDict.get('F') || 0;
         var color = outlineDict.getArray('C'),
             rgbColor = blackColor;
-        if ((0, _util.isArray)(color) && color.length === 3 && (color[0] !== 0 || color[1] !== 0 || color[2] !== 0)) {
+        if (Array.isArray(color) && color.length === 3 && (color[0] !== 0 || color[1] !== 0 || color[2] !== 0)) {
           rgbColor = _colorspace.ColorSpace.singletons.rgb.getRgb(color, 0);
         }
         var outlineItem = {
           dest: data.dest,
           url: data.url,
           unsafeUrl: data.unsafeUrl,
           newWindow: data.newWindow,
           title: (0, _util.stringToPDFString)(title),
@@ -21710,17 +21698,17 @@ var Catalog = function CatalogClosure() 
           });
           processed.put(obj);
         }
       }
       return root.items.length > 0 ? root.items : null;
     },
     get numPages() {
       var obj = this.toplevelPagesDict.get('Count');
-      if (!(0, _util.isInt)(obj)) {
+      if (!Number.isInteger(obj)) {
         throw new _util.FormatError('page count in top level pages object is not an integer');
       }
       return (0, _util.shadow)(this, 'numPages', obj);
     },
     get destinations() {
       function fetchDestination(dest) {
         return (0, _primitives.isDict)(dest) ? dest.get('D') : dest;
       }
@@ -21818,17 +21806,17 @@ var Catalog = function CatalogClosure() 
           }
           style = s ? s.name : null;
           var p = labelDict.get('P');
           if (p && !(0, _util.isString)(p)) {
             throw new _util.FormatError('Invalid prefix in PageLabel dictionary.');
           }
           prefix = p ? (0, _util.stringToPDFString)(p) : '';
           var st = labelDict.get('St');
-          if (st && !((0, _util.isInt)(st) && st >= 1)) {
+          if (st && !(Number.isInteger(st) && st >= 1)) {
             throw new _util.FormatError('Invalid start in PageLabel dictionary.');
           }
           currentIndex = st || 1;
         }
         switch (style) {
           case 'D':
             currentLabel = currentIndex;
             break;
@@ -22005,17 +21993,17 @@ var Catalog = function CatalogClosure() 
           if (objId && !pageKidsCountCache.has(objId)) {
             pageKidsCountCache.put(objId, count);
           }
           if (currentPageIndex + count <= pageIndex) {
             currentPageIndex += count;
             continue;
           }
           var kids = currentNode.get('Kids');
-          if (!(0, _util.isArray)(kids)) {
+          if (!Array.isArray(kids)) {
             capability.reject(new _util.FormatError('page dictionary kids object is not an array'));
             return;
           }
           for (var last = kids.length - 1; last >= 0; last--) {
             nodesToVisit.push(kids[last]);
           }
         }
         capability.reject(new Error('Page index ' + pageIndex + ' not found.'));
@@ -22157,17 +22145,17 @@ var Catalog = function CatalogClosure() 
           if (remoteDest) {
             if ((0, _primitives.isName)(remoteDest)) {
               remoteDest = remoteDest.name;
             }
             if ((0, _util.isString)(url)) {
               let baseUrl = url.split('#')[0];
               if ((0, _util.isString)(remoteDest)) {
                 url = baseUrl + '#' + remoteDest;
-              } else if ((0, _util.isArray)(remoteDest)) {
+              } else if (Array.isArray(remoteDest)) {
                 url = baseUrl + '#' + JSON.stringify(remoteDest);
               }
             }
           }
           var newWindow = action.get('NewWindow');
           if ((0, _util.isBool)(newWindow)) {
             resultObj.newWindow = newWindow;
           }
@@ -22212,17 +22200,17 @@ var Catalog = function CatalogClosure() 
         resultObj.url = absoluteUrl.href;
       }
       resultObj.unsafeUrl = url;
     }
     if (dest) {
       if ((0, _primitives.isName)(dest)) {
         dest = dest.name;
       }
-      if ((0, _util.isString)(dest) || (0, _util.isArray)(dest)) {
+      if ((0, _util.isString)(dest) || Array.isArray(dest)) {
         resultObj.dest = dest;
       }
     }
   };
   return Catalog;
 }();
 var XRef = function XRefClosure() {
   function XRef(stream, pdfManager) {
@@ -22296,34 +22284,34 @@ var XRef = function XRefClosure() {
           if ((0, _primitives.isCmd)(obj = parser.getObj(), 'trailer')) {
             break;
           }
           tableState.firstEntryNum = obj;
           tableState.entryCount = parser.getObj();
         }
         var first = tableState.firstEntryNum;
         var count = tableState.entryCount;
-        if (!(0, _util.isInt)(first) || !(0, _util.isInt)(count)) {
+        if (!Number.isInteger(first) || !Number.isInteger(count)) {
           throw new _util.FormatError('Invalid XRef table: wrong types in subsection header');
         }
         for (var i = tableState.entryNum; i < count; i++) {
           tableState.streamPos = stream.pos;
           tableState.entryNum = i;
           tableState.parserBuf1 = parser.buf1;
           tableState.parserBuf2 = parser.buf2;
           var entry = {};
           entry.offset = parser.getObj();
           entry.gen = parser.getObj();
           var type = parser.getObj();
           if ((0, _primitives.isCmd)(type, 'f')) {
             entry.free = true;
           } else if ((0, _primitives.isCmd)(type, 'n')) {
             entry.uncompressed = true;
           }
-          if (!(0, _util.isInt)(entry.offset) || !(0, _util.isInt)(entry.gen) || !(entry.free || entry.uncompressed)) {
+          if (!Number.isInteger(entry.offset) || !Number.isInteger(entry.gen) || !(entry.free || entry.uncompressed)) {
             throw new _util.FormatError(`Invalid entry in XRef subsection: ${first}, ${count}`);
           }
           if (i === 0 && entry.free && first === 1) {
             first = 0;
           }
           if (!this.entries[i + first]) {
             this.entries[i + first] = entry;
           }
@@ -22366,20 +22354,20 @@ var XRef = function XRefClosure() {
       var byteWidths = streamState.byteWidths;
       var typeFieldWidth = byteWidths[0];
       var offsetFieldWidth = byteWidths[1];
       var generationFieldWidth = byteWidths[2];
       var entryRanges = streamState.entryRanges;
       while (entryRanges.length > 0) {
         var first = entryRanges[0];
         var n = entryRanges[1];
-        if (!(0, _util.isInt)(first) || !(0, _util.isInt)(n)) {
+        if (!Number.isInteger(first) || !Number.isInteger(n)) {
           throw new _util.FormatError(`Invalid XRef range fields: ${first}, ${n}`);
         }
-        if (!(0, _util.isInt)(typeFieldWidth) || !(0, _util.isInt)(offsetFieldWidth) || !(0, _util.isInt)(generationFieldWidth)) {
+        if (!Number.isInteger(typeFieldWidth) || !Number.isInteger(offsetFieldWidth) || !Number.isInteger(generationFieldWidth)) {
           throw new _util.FormatError(`Invalid XRef entry fields length: ${first}, ${n}`);
         }
         for (i = streamState.entryNum; i < n; ++i) {
           streamState.entryNum = i;
           streamState.streamPos = stream.pos;
           var type = 0,
               offset = 0,
               generation = 0;
@@ -22556,39 +22544,39 @@ var XRef = function XRefClosure() {
           var obj = parser.getObj();
           var dict;
           if ((0, _primitives.isCmd)(obj, 'xref')) {
             dict = this.processXRefTable(parser);
             if (!this.topDict) {
               this.topDict = dict;
             }
             obj = dict.get('XRefStm');
-            if ((0, _util.isInt)(obj)) {
+            if (Number.isInteger(obj)) {
               var pos = obj;
               if (!(pos in this.xrefstms)) {
                 this.xrefstms[pos] = 1;
                 this.startXRefQueue.push(pos);
               }
             }
-          } else if ((0, _util.isInt)(obj)) {
-            if (!(0, _util.isInt)(parser.getObj()) || !(0, _primitives.isCmd)(parser.getObj(), 'obj') || !(0, _primitives.isStream)(obj = parser.getObj())) {
+          } else if (Number.isInteger(obj)) {
+            if (!Number.isInteger(parser.getObj()) || !(0, _primitives.isCmd)(parser.getObj(), 'obj') || !(0, _primitives.isStream)(obj = parser.getObj())) {
               throw new _util.FormatError('Invalid XRef stream');
             }
             dict = this.processXRefStream(obj);
             if (!this.topDict) {
               this.topDict = dict;
             }
             if (!dict) {
               throw new _util.FormatError('Failed to read XRef stream');
             }
           } else {
             throw new _util.FormatError('Invalid XRef stream header');
           }
           obj = dict.get('Prev');
-          if ((0, _util.isInt)(obj)) {
+          if (Number.isInteger(obj)) {
             this.startXRefQueue.push(obj);
           } else if ((0, _primitives.isRef)(obj)) {
             this.startXRefQueue.push(obj.num);
           }
           this.startXRefQueue.shift();
         }
         return this.topDict;
       } catch (e) {
@@ -22685,33 +22673,33 @@ var XRef = function XRefClosure() {
     fetchCompressed: function XRef_fetchCompressed(xrefEntry, suppressEncryption) {
       var tableOffset = xrefEntry.offset;
       var stream = this.fetch(new _primitives.Ref(tableOffset, 0));
       if (!(0, _primitives.isStream)(stream)) {
         throw new _util.FormatError('bad ObjStm stream');
       }
       var first = stream.dict.get('First');
       var n = stream.dict.get('N');
-      if (!(0, _util.isInt)(first) || !(0, _util.isInt)(n)) {
+      if (!Number.isInteger(first) || !Number.isInteger(n)) {
         throw new _util.FormatError('invalid first and n parameters for ObjStm stream');
       }
       var parser = new _parser.Parser(new _parser.Lexer(stream), false, this);
       parser.allowStreams = true;
       var i,
           entries = [],
           num,
           nums = [];
       for (i = 0; i < n; ++i) {
         num = parser.getObj();
-        if (!(0, _util.isInt)(num)) {
+        if (!Number.isInteger(num)) {
           throw new _util.FormatError(`invalid object number in the ObjStm stream: ${num}`);
         }
         nums.push(num);
         var offset = parser.getObj();
-        if (!(0, _util.isInt)(offset)) {
+        if (!Number.isInteger(offset)) {
           throw new _util.FormatError(`invalid object offset in the ObjStm stream: ${offset}`);
         }
       }
       for (i = 0; i < n; ++i) {
         entries.push(parser.getObj());
         if ((0, _primitives.isCmd)(parser.buf1, 'endobj')) {
           parser.shift();
         }
@@ -22784,17 +22772,17 @@ var NameOrNumberTree = function NameOrNu
               throw new _util.FormatError(`Duplicate entry in "${this._type}" tree.`);
             }
             queue.push(kid);
             processed.put(kid);
           }
           continue;
         }
         var entries = obj.get(this._type);
-        if ((0, _util.isArray)(entries)) {
+        if (Array.isArray(entries)) {
           for (i = 0, n = entries.length; i < n; i += 2) {
             dict[xref.fetchIfRef(entries[i])] = xref.fetchIfRef(entries[i + 1]);
           }
         }
       }
       return dict;
     },
     get: function NameOrNumberTree_get(key) {
@@ -22807,17 +22795,17 @@ var NameOrNumberTree = function NameOrNu
       var MAX_LEVELS = 10;
       var l, r, m;
       while (kidsOrEntries.has('Kids')) {
         if (++loopCount > MAX_LEVELS) {
           (0, _util.warn)('Search depth limit reached for "' + this._type + '" tree.');
           return null;
         }
         var kids = kidsOrEntries.get('Kids');
-        if (!(0, _util.isArray)(kids)) {
+        if (!Array.isArray(kids)) {
           return null;
         }
         l = 0;
         r = kids.length - 1;
         while (l <= r) {
           m = l + r >> 1;
           var kid = xref.fetchIfRef(kids[m]);
           var limits = kid.get('Limits');
@@ -22830,17 +22818,17 @@ var NameOrNumberTree = function NameOrNu
             break;
           }
         }
         if (l > r) {
           return null;
         }
       }
       var entries = kidsOrEntries.get(this._type);
-      if ((0, _util.isArray)(entries)) {
+      if (Array.isArray(entries)) {
         l = 0;
         r = entries.length - 2;
         while (l <= r) {
           m = l + r & ~1;
           var currentKey = xref.fetchIfRef(entries[m]);
           if (key < currentKey) {
             r = m - 2;
           } else if (key > currentKey) {
@@ -22942,29 +22930,29 @@ var FileSpec = function FileSpecClosure(
         content: this.content
       };
     }
   };
   return FileSpec;
 }();
 let ObjectLoader = function () {
   function mayHaveChildren(value) {
-    return (0, _primitives.isRef)(value) || (0, _primitives.isDict)(value) || (0, _util.isArray)(value) || (0, _primitives.isStream)(value);
+    return (0, _primitives.isRef)(value) || (0, _primitives.isDict)(value) || Array.isArray(value) || (0, _primitives.isStream)(value);
   }
   function addChildren(node, nodesToVisit) {
     if ((0, _primitives.isDict)(node) || (0, _primitives.isStream)(node)) {
       let dict = (0, _primitives.isDict)(node) ? node : node.dict;
       let dictKeys = dict.getKeys();
       for (let i = 0, ii = dictKeys.length; i < ii; i++) {
         let rawValue = dict.getRaw(dictKeys[i]);
         if (mayHaveChildren(rawValue)) {
           nodesToVisit.push(rawValue);
         }
       }
-    } else if ((0, _util.isArray)(node)) {
+    } else if (Array.isArray(node)) {
       for (let i = 0, ii = node.length; i < ii; i++) {
         let value = node[i];
         if (mayHaveChildren(value)) {
           nodesToVisit.push(value);
         }
       }
     }
   }
@@ -27430,31 +27418,31 @@ class Annotation {
   }
   get printable() {
     if (this.flags === 0) {
       return false;
     }
     return this._isPrintable(this.flags);
   }
   setFlags(flags) {
-    this.flags = (0, _util.isInt)(flags) && flags > 0 ? flags : 0;
+    this.flags = Number.isInteger(flags) && flags > 0 ? flags : 0;
   }
   hasFlag(flag) {
     return this._hasFlag(this.flags, flag);
   }
   setRectangle(rectangle) {
-    if ((0, _util.isArray)(rectangle) && rectangle.length === 4) {
+    if (Array.isArray(rectangle) && rectangle.length === 4) {
       this.rectangle = _util.Util.normalizeRect(rectangle);
     } else {
       this.rectangle = [0, 0, 0, 0];
     }
   }
   setColor(color) {
     let rgbColor = new Uint8Array(3);
-    if (!(0, _util.isArray)(color)) {
+    if (!Array.isArray(color)) {
       this.color = rgbColor;
       return;
     }
     switch (color.length) {
       case 0:
         this.color = null;
         break;
       case 1:
@@ -27484,17 +27472,17 @@ class Annotation {
       let dictType = dict.get('Type');
       if (!dictType || (0, _primitives.isName)(dictType, 'Border')) {
         this.borderStyle.setWidth(dict.get('W'));
         this.borderStyle.setStyle(dict.get('S'));
         this.borderStyle.setDashArray(dict.getArray('D'));
       }
     } else if (borderStyle.has('Border')) {
       let array = borderStyle.getArray('Border');
-      if ((0, _util.isArray)(array) && array.length >= 3) {
+      if (Array.isArray(array) && array.length >= 3) {
         this.borderStyle.setHorizontalCornerRadius(array[0]);
         this.borderStyle.setVerticalCornerRadius(array[1]);
         this.borderStyle.setWidth(array[2]);
         if (array.length === 4) {
           this.borderStyle.setDashArray(array[3]);
         }
       }
     } else {
@@ -27599,17 +27587,17 @@ class AnnotationBorderStyle {
       case 'U':
         this.style = _util.AnnotationBorderStyleType.UNDERLINE;
         break;
       default:
         break;
     }
   }
   setDashArray(dashArray) {
-    if ((0, _util.isArray)(dashArray) && dashArray.length > 0) {
+    if (Array.isArray(dashArray) && dashArray.length > 0) {
       let isValid = true;
       let allZeros = true;
       for (let i = 0, len = dashArray.length; i < len; i++) {
         let element = dashArray[i];
         let validNumber = +element >= 0;
         if (!validNumber) {
           isValid = false;
           break;
@@ -27646,17 +27634,17 @@ class WidgetAnnotation extends Annotatio
     data.fieldName = this._constructFieldName(dict);
     data.fieldValue = _util.Util.getInheritableProperty(dict, 'V', true);
     data.alternativeText = (0, _util.stringToPDFString)(dict.get('TU') || '');
     data.defaultAppearance = _util.Util.getInheritableProperty(dict, 'DA') || '';
     let fieldType = _util.Util.getInheritableProperty(dict, 'FT');
     data.fieldType = (0, _primitives.isName)(fieldType) ? fieldType.name : null;
     this.fieldResources = _util.Util.getInheritableProperty(dict, 'DR') || _primitives.Dict.empty;
     data.fieldFlags = _util.Util.getInheritableProperty(dict, 'Ff');
-    if (!(0, _util.isInt)(data.fieldFlags) || data.fieldFlags < 0) {
+    if (!Number.isInteger(data.fieldFlags) || data.fieldFlags < 0) {
       data.fieldFlags = 0;
     }
     data.readOnly = this.hasFieldFlag(_util.AnnotationFieldFlag.READONLY);
     if (data.fieldType === 'Sig') {
       this.setFlags(_util.AnnotationFlag.HIDDEN);
     }
   }
   _constructFieldName(dict) {
@@ -27693,22 +27681,22 @@ class WidgetAnnotation extends Annotatio
     return super.getOperatorList(evaluator, task, renderForms);
   }
 }
 class TextWidgetAnnotation extends WidgetAnnotation {
   constructor(params) {
     super(params);
     this.data.fieldValue = (0, _util.stringToPDFString)(this.data.fieldValue || '');
     let alignment = _util.Util.getInheritableProperty(params.dict, 'Q');
-    if (!(0, _util.isInt)(alignment) || alignment < 0 || alignment > 2) {
+    if (!Number.isInteger(alignment) || alignment < 0 || alignment > 2) {
       alignment = null;
     }
     this.data.textAlignment = alignment;
     let maximumLength = _util.Util.getInheritableProperty(params.dict, 'MaxLen');
-    if (!(0, _util.isInt)(maximumLength) || maximumLength < 0) {
+    if (!Number.isInteger(maximumLength) || maximumLength < 0) {
       maximumLength = null;
     }
     this.data.maxLen = maximumLength;
     this.data.multiLine = this.hasFieldFlag(_util.AnnotationFieldFlag.MULTILINE);
     this.data.comb = this.hasFieldFlag(_util.AnnotationFieldFlag.COMB) && !this.hasFieldFlag(_util.AnnotationFieldFlag.MULTILINE) && !this.hasFieldFlag(_util.AnnotationFieldFlag.PASSWORD) && !this.hasFieldFlag(_util.AnnotationFieldFlag.FILESELECT) && this.data.maxLen !== null;
   }
   getOperatorList(evaluator, task, renderForms) {
     if (renderForms || this.appearance) {
@@ -27767,28 +27755,28 @@ class ButtonWidgetAnnotation extends Wid
     }
   }
 }
 class ChoiceWidgetAnnotation extends WidgetAnnotation {
   constructor(params) {
     super(params);
     this.data.options = [];
     let options = _util.Util.getInheritableProperty(params.dict, 'Opt');
-    if ((0, _util.isArray)(options)) {
+    if (Array.isArray(options)) {
       let xref = params.xref;
       for (let i = 0, ii = options.length; i < ii; i++) {
         let option = xref.fetchIfRef(options[i]);
-        let isOptionArray = (0, _util.isArray)(option);
+        let isOptionArray = Array.isArray(option);
         this.data.options[i] = {
           exportValue: isOptionArray ? xref.fetchIfRef(option[0]) : option,
           displayValue: isOptionArray ? xref.fetchIfRef(option[1]) : option
         };
       }
     }
-    if (!(0, _util.isArray)(this.data.fieldValue)) {
+    if (!Array.isArray(this.data.fieldValue)) {
       this.data.fieldValue = [this.data.fieldValue];
     }
     this.data.combo = this.hasFieldFlag(_util.AnnotationFieldFlag.COMBO);
     this.data.multiSelect = this.hasFieldFlag(_util.AnnotationFieldFlag.MULTISELECT);
   }
 }
 class TextAnnotation extends Annotation {
   constructor(parameters) {
@@ -28309,28 +28297,28 @@ var IdentityCMap = function IdentityCMap
     },
     mapBfRangeToArray(low, high, array) {
       throw new Error('should not call mapBfRangeToArray');
     },
     mapOne(src, dst) {
       throw new Error('should not call mapCidOne');
     },
     lookup(code) {
-      return (0, _util.isInt)(code) && code <= 0xffff ? code : undefined;
+      return Number.isInteger(code) && code <= 0xffff ? code : undefined;
     },
     contains(code) {
-      return (0, _util.isInt)(code) && code <= 0xffff;
+      return Number.isInteger(code) && code <= 0xffff;
     },
     forEach(callback) {
       for (var i = 0; i <= 0xffff; i++) {
         callback(i, i);
       }
     },
     charCodeOf(value) {
-      return (0, _util.isInt)(value) && value <= 0xffff ? value : -1;
+      return Number.isInteger(value) && value <= 0xffff ? value : -1;
     },
     getMap() {
       var map = new Array(0x10000);
       for (var i = 0; i <= 0xffff; i++) {
         map[i] = i;
       }
       return map;
     },
@@ -28615,17 +28603,17 @@ var CMapFactory = function CMapFactoryCl
     return a >>> 0;
   }
   function expectString(obj) {
     if (!(0, _util.isString)(obj)) {
       throw new _util.FormatError('Malformed CMap: expected string.');
     }
   }
   function expectInt(obj) {
-    if (!(0, _util.isInt)(obj)) {
+    if (!Number.isInteger(obj)) {
       throw new _util.FormatError('Malformed CMap: expected int.');
     }
   }
   function parseBfChar(cMap, lexer) {
     while (true) {
       var obj = lexer.getObj();
       if ((0, _primitives.isEOF)(obj)) {
         break;
@@ -28651,18 +28639,18 @@ var CMapFactory = function CMapFactoryCl
         return;
       }
       expectString(obj);
       var low = strToInt(obj);
       obj = lexer.getObj();
       expectString(obj);
       var high = strToInt(obj);
       obj = lexer.getObj();
-      if ((0, _util.isInt)(obj) || (0, _util.isString)(obj)) {
-        var dstLow = (0, _util.isInt)(obj) ? String.fromCharCode(obj) : obj;
+      if (Number.isInteger(obj) || (0, _util.isString)(obj)) {
+        var dstLow = Number.isInteger(obj) ? String.fromCharCode(obj) : obj;
         cMap.mapBfRange(low, high, dstLow);
       } else if ((0, _primitives.isCmd)(obj, '[')) {
         obj = lexer.getObj();
         var array = [];
         while (!(0, _primitives.isCmd)(obj, ']') && !(0, _primitives.isEOF)(obj)) {
           array.push(obj);
           obj = lexer.getObj();
         }
@@ -28729,17 +28717,17 @@ var CMapFactory = function CMapFactoryCl
       }
       var high = strToInt(obj);
       cMap.addCodespaceRange(obj.length, low, high);
     }
     throw new _util.FormatError('Invalid codespace range.');
   }
   function parseWMode(cMap, lexer) {
     var obj = lexer.getObj();
-    if ((0, _util.isInt)(obj)) {
+    if (Number.isInteger(obj)) {
       cMap.vertical = !!obj;
     }
   }
   function parseCMapName(cMap, lexer) {
     var obj = lexer.getObj();
     if ((0, _primitives.isName)(obj) && (0, _util.isString)(obj.name)) {
       cMap.name = obj.name;
     }
@@ -28959,24 +28947,24 @@ var Page = function PageClosure() {
     get content() {
       return this.getPageProp('Contents');
     },
     get resources() {
       return (0, _util.shadow)(this, 'resources', this.getInheritedPageProp('Resources') || _primitives.Dict.empty);
     },
     get mediaBox() {
       var mediaBox = this.getInheritedPageProp('MediaBox', true);
-      if (!(0, _util.isArray)(mediaBox) || mediaBox.length !== 4) {
+      if (!Array.isArray(mediaBox) || mediaBox.length !== 4) {
         return (0, _util.shadow)(this, 'mediaBox', LETTER_SIZE_MEDIABOX);
       }
       return (0, _util.shadow)(this, 'mediaBox', mediaBox);
     },
     get cropBox() {
       var cropBox = this.getInheritedPageProp('CropBox', true);
-      if (!(0, _util.isArray)(cropBox) || cropBox.length !== 4) {
+      if (!Array.isArray(cropBox) || cropBox.length !== 4) {
         return (0, _util.shadow)(this, 'cropBox', this.mediaBox);
       }
       return (0, _util.shadow)(this, 'cropBox', cropBox);
     },
     get userUnit() {
       var obj = this.getPageProp('UserUnit');
       if (!(0, _util.isNum)(obj) || obj <= 0) {
         obj = DEFAULT_USER_UNIT;
@@ -29001,17 +28989,17 @@ var Page = function PageClosure() {
       } else if (rotate < 0) {
         rotate = (rotate % 360 + 360) % 360;
       }
       return (0, _util.shadow)(this, 'rotate', rotate);
     },
     getContentStream: function Page_getContentStream() {
       var content = this.content;
       var stream;
-      if ((0, _util.isArray)(content)) {
+      if (Array.isArray(content)) {
         var xref = this.xref;
         var i,
             n = content.length;
         var streams = [];
         for (i = 0; i < n; ++i) {
           streams.push(xref.fetchIfRef(content[i]));
         }
         stream = new _stream.StreamsSequenceStream(streams);
@@ -29196,17 +29184,17 @@ var PDFDocument = function PDFDocumentCl
       if ((0, _primitives.isName)(version)) {
         this.pdfFormatVersion = version.name;
       }
       try {
         this.acroForm = this.catalog.catDict.get('AcroForm');
         if (this.acroForm) {
           this.xfa = this.acroForm.get('XFA');
           var fields = this.acroForm.get('Fields');
-          if ((!fields || !(0, _util.isArray)(fields) || fields.length === 0) && !this.xfa) {
+          if ((!fields || !Array.isArray(fields) || fields.length === 0) && !this.xfa) {
             this.acroForm = null;
           }
         }
       } catch (ex) {
         if (ex instanceof _util.MissingDataException) {
           throw ex;
         }
         (0, _util.info)('Something wrong with AcroForm entry');
@@ -29343,17 +29331,17 @@ var PDFDocument = function PDFDocumentCl
       }
       return (0, _util.shadow)(this, 'documentInfo', docInfo);
     },
     get fingerprint() {
       var xref = this.xref,
           hash,
           fileID = '';
       var idArray = xref.trailer.get('ID');
-      if (idArray && (0, _util.isArray)(idArray) && idArray[0] && (0, _util.isString)(idArray[0]) && idArray[0] !== EMPTY_FINGERPRINT) {
+      if (Array.isArray(idArray) && idArray[0] && (0, _util.isString)(idArray[0]) && idArray[0] !== EMPTY_FINGERPRINT) {
         hash = (0, _util.stringToBytes)(idArray[0]);
       } else {
         if (this.stream.ensureRange) {
           this.stream.ensureRange(0, Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end));
         }
         hash = (0, _crypto.calculateMD5)(this.stream.bytes.subarray(0, FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES);
       }
       for (var i = 0, n = hash.length; i < n; i++) {
@@ -30308,17 +30296,17 @@ var IdentityToUnicodeMap = function Iden
     },
     get(i) {
       if (this.firstChar <= i && i <= this.lastChar) {
         return String.fromCharCode(i);
       }
       return undefined;
     },
     charCodeOf(v) {
-      return (0, _util.isInt)(v) && v >= this.firstChar && v <= this.lastChar ? v : -1;
+      return Number.isInteger(v) && v >= this.firstChar && v <= this.lastChar ? v : -1;
     },
     amend(map) {
       throw new Error('Should not call amend()');
     }
   };
   return IdentityToUnicodeMap;
 }();
 var OpenTypeFileBuilder = function OpenTypeFileBuilderClosure() {
@@ -32468,17 +32456,17 @@ var Type1Font = function Type1FontClosur
       privateDict.setByName('Subrs', null);
       var fields = ['BlueValues', 'OtherBlues', 'FamilyBlues', 'FamilyOtherBlues', 'StemSnapH', 'StemSnapV', 'BlueShift', 'BlueFuzz', 'BlueScale', 'LanguageGroup', 'ExpansionFactor', 'ForceBold', 'StdHW', 'StdVW'];
       for (i = 0, ii = fields.length; i < ii; i++) {
         var field = fields[i];
         if (!(field in properties.privateData)) {
           continue;
         }
         var value = properties.privateData[field];
-        if ((0, _util.isArray)(value)) {
+        if (Array.isArray(value)) {
           for (var j = value.length - 1; j > 0; j--) {
             value[j] -= value[j - 1];
           }
         }
         privateDict.setByName(field, value);
       }
       cff.topDict.privateDict = privateDict;
       var subrIndex = new _cff_parser.CFFIndex();
@@ -32715,17 +32703,17 @@ var PDFImage = function PDFImageClosure(
     if (smask) {
       smaskPromise = handleImageData(smask, nativeDecoder);
       maskPromise = Promise.resolve(null);
     } else {
       smaskPromise = Promise.resolve(null);
       if (mask) {
         if ((0, _primitives.isStream)(mask)) {
           maskPromise = handleImageData(mask, nativeDecoder);
-        } else if ((0, _util.isArray)(mask)) {
+        } else if (Array.isArray(mask)) {
           maskPromise = Promise.resolve(mask);
         } else {
           (0, _util.warn)('Unsupported mask format.');
           maskPromise = Promise.resolve(null);
         }
       } else {
         maskPromise = Promise.resolve(null);
       }
@@ -32876,17 +32864,17 @@ var PDFImage = function PDFImageClosure(
           mask.numComps = 1;
           mask.fillGrayBuffer(alphaBuf);
           for (i = 0, ii = sw * sh; i < ii; ++i) {
             alphaBuf[i] = 255 - alphaBuf[i];
           }
           if (sw !== width || sh !== height) {
             alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height);
           }
-        } else if ((0, _util.isArray)(mask)) {
+        } else if (Array.isArray(mask)) {
           alphaBuf = new Uint8Array(width * height);
           var numComps = this.numComps;
           for (i = 0, ii = width * height; i < ii; ++i) {
             var opacity = 0;
             var imageOffset = i * numComps;
             for (j = 0; j < numComps; ++j) {
               var color = image[imageOffset + j];
               var maskOffset = j * 2;
@@ -40015,18 +40003,18 @@ exports.Type1Parser = Type1Parser;
 
 /***/ }),
 /* 35 */
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
 
 
-var pdfjsVersion = '1.9.512';
-var pdfjsBuild = '066fea9c';
+var pdfjsVersion = '1.9.523';
+var pdfjsBuild = '1c9af00b';
 var pdfjsCoreWorker = __w_pdfjs_require__(17);
 exports.WorkerMessageHandler = pdfjsCoreWorker.WorkerMessageHandler;
 
 /***/ }),
 /* 36 */
 /***/ (function(module, exports, __w_pdfjs_require__) {
 
 "use strict";
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -86,17 +86,17 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.localized = exports.animationStarted = exports.normalizeWheelEventDelta = exports.binarySearchFirstItem = exports.watchScroll = exports.scrollIntoView = exports.getOutputScale = exports.approximateFraction = exports.roundToDivide = exports.getVisibleElements = exports.parseQueryString = exports.noContextMenuHandler = exports.getPDFFileNameFromURL = exports.ProgressBar = exports.EventBus = exports.NullL10n = exports.mozL10n = exports.RendererType = exports.cloneObj = exports.VERTICAL_PADDING = exports.SCROLLBAR_PADDING = exports.MAX_AUTO_SCALE = exports.UNKNOWN_SCALE = exports.MAX_SCALE = exports.MIN_SCALE = exports.DEFAULT_SCALE = exports.DEFAULT_SCALE_VALUE = exports.CSS_UNITS = undefined;
+exports.waitOnEventOrTimeout = exports.WaitOnType = exports.localized = exports.animationStarted = exports.normalizeWheelEventDelta = exports.binarySearchFirstItem = exports.watchScroll = exports.scrollIntoView = exports.getOutputScale = exports.approximateFraction = exports.roundToDivide = exports.getVisibleElements = exports.parseQueryString = exports.noContextMenuHandler = exports.getPDFFileNameFromURL = exports.ProgressBar = exports.EventBus = exports.NullL10n = exports.mozL10n = exports.RendererType = exports.cloneObj = exports.VERTICAL_PADDING = exports.SCROLLBAR_PADDING = exports.MAX_AUTO_SCALE = exports.UNKNOWN_SCALE = exports.MAX_SCALE = exports.MIN_SCALE = exports.DEFAULT_SCALE = exports.DEFAULT_SCALE_VALUE = exports.CSS_UNITS = undefined;
 
 var _pdfjsLib = __webpack_require__(1);
 
 const CSS_UNITS = 96.0 / 72.0;
 const DEFAULT_SCALE_VALUE = 'auto';
 const DEFAULT_SCALE = 1.0;
 const MIN_SCALE = 0.25;
 const MAX_SCALE = 10.0;
@@ -377,16 +377,46 @@ function cloneObj(obj) {
   let result = Object.create(null);
   for (let i in obj) {
     if (Object.prototype.hasOwnProperty.call(obj, i)) {
       result[i] = obj[i];
     }
   }
   return result;
 }
+const WaitOnType = {
+  EVENT: 'event',
+  TIMEOUT: 'timeout'
+};
+function waitOnEventOrTimeout({ target, name, delay = 0 }) {
+  if (typeof target !== 'object' || !(name && typeof name === 'string') || !(Number.isInteger(delay) && delay >= 0)) {
+    return Promise.reject(new Error('waitOnEventOrTimeout - invalid paramaters.'));
+  }
+  let capability = (0, _pdfjsLib.createPromiseCapability)();
+  function handler(type) {
+    if (target instanceof EventBus) {
+      target.off(name, eventHandler);
+    } else {
+      target.removeEventListener(name, eventHandler);
+    }
+    if (timeout) {
+      clearTimeout(timeout);
+    }
+    capability.resolve(type);
+  }
+  let eventHandler = handler.bind(null, WaitOnType.EVENT);
+  if (target instanceof EventBus) {
+    target.on(name, eventHandler);
+  } else {
+    target.addEventListener(name, eventHandler);
+  }
+  let timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
+  let timeout = setTimeout(timeoutHandler, delay);
+  return capability.promise;
+}
 let animationStarted = new Promise(function (resolve) {
   window.requestAnimationFrame(resolve);
 });
 let mozL10n;
 let localized = Promise.resolve();
 class EventBus {
   constructor() {
     this._listeners = Object.create(null);
@@ -500,16 +530,18 @@ exports.roundToDivide = roundToDivide;
 exports.approximateFraction = approximateFraction;
 exports.getOutputScale = getOutputScale;
 exports.scrollIntoView = scrollIntoView;
 exports.watchScroll = watchScroll;
 exports.binarySearchFirstItem = binarySearchFirstItem;
 exports.normalizeWheelEventDelta = normalizeWheelEventDelta;
 exports.animationStarted = animationStarted;
 exports.localized = localized;
+exports.WaitOnType = WaitOnType;
+exports.waitOnEventOrTimeout = waitOnEventOrTimeout;
 
 /***/ }),
 /* 1 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
@@ -833,17 +865,16 @@ const DefaultExternalServices = {
   supportsDocumentColors: true,
   supportedMouseWheelZoomModifierKeys: {
     ctrlKey: true,
     metaKey: true
   }
 };
 let PDFViewerApplication = {
   initialBookmark: document.location.hash.substring(1),
-  initialDestination: null,
   initialized: false,
   fellback: false,
   appConfig: null,
   pdfDocument: null,
   pdfLoadingTask: null,
   printService: null,
   pdfViewer: null,
   pdfThumbnailViewer: null,
@@ -1371,29 +1402,24 @@ let PDFViewerApplication = {
     let firstPagePromise = pdfViewer.firstPagePromise;
     let pagesPromise = pdfViewer.pagesPromise;
     let onePageRendered = pdfViewer.onePageRendered;
     let pdfThumbnailViewer = this.pdfThumbnailViewer;
     pdfThumbnailViewer.setDocument(pdfDocument);
     firstPagePromise.then(pdfPage => {
       this.loadingBar.setWidth(this.appConfig.viewerContainer);
       if (!_pdfjsLib.PDFJS.disableHistory && !this.isViewerEmbedded) {
-        if (!this.viewerPrefs['showPreviousViewOnLoad']) {
-          this.pdfHistory.clearHistoryState();
-        }
-        this.pdfHistory.initialize(this.documentFingerprint);
-        if (this.pdfHistory.initialDestination) {
-          this.initialDestination = this.pdfHistory.initialDestination;
-        } else if (this.pdfHistory.initialBookmark) {
+        let resetHistory = !this.viewerPrefs['showPreviousViewOnLoad'];
+        this.pdfHistory.initialize(id, resetHistory);
+        if (this.pdfHistory.initialBookmark) {
           this.initialBookmark = this.pdfHistory.initialBookmark;
         }
       }
       let initialParams = {
-        destination: this.initialDestination,
-        bookmark: this.initialBookmark,
+        bookmark: null,
         hash: null
       };
       let storePromise = store.getMultiple({
         exists: false,
         page: '1',
         zoom: _ui_utils.DEFAULT_SCALE_VALUE,
         scrollLeft: '0',
         scrollTop: '0',
@@ -1409,30 +1435,30 @@ let PDFViewerApplication = {
         if (pageMode && !this.viewerPrefs['disablePageMode']) {
           sidebarView = sidebarView || apiPageModeToSidebarView(pageMode);
         }
         return {
           hash,
           sidebarView
         };
       }).then(({ hash, sidebarView }) => {
+        initialParams.bookmark = this.initialBookmark;
+        initialParams.hash = hash;
         this.setInitialView(hash, { sidebarView });
-        initialParams.hash = hash;
         if (!this.isViewerEmbedded) {
           pdfViewer.focus();
         }
         return pagesPromise;
       }).then(() => {
-        if (!initialParams.destination && !initialParams.bookmark && !initialParams.hash) {
+        if (!initialParams.bookmark && !initialParams.hash) {
           return;
         }
         if (pdfViewer.hasEqualPageSizes) {
           return;
         }
-        this.initialDestination = initialParams.destination;
         this.initialBookmark = initialParams.bookmark;
         pdfViewer.currentScaleValue = pdfViewer.currentScaleValue;
         this.setInitialView(initialParams.hash);
       }).then(function () {
         pdfViewer.update();
       });
     });
     pdfDocument.getPageLabels().then(labels => {
@@ -1525,22 +1551,18 @@ let PDFViewerApplication = {
         generator: generatorId,
         formType
       });
     });
   },
   setInitialView(storedHash, { sidebarView } = {}) {
     this.isInitialViewSet = true;
     this.pdfSidebar.setInitialView(sidebarView);
-    if (this.initialDestination) {
-      this.pdfLinkService.navigateTo(this.initialDestination);
-      this.initialDestination = null;
-    } else if (this.initialBookmark) {
+    if (this.initialBookmark) {
       this.pdfLinkService.setHash(this.initialBookmark);
-      this.pdfHistory.push({ hash: this.initialBookmark }, true);
       this.initialBookmark = null;
     } else if (storedHash) {
       this.pdfLinkService.setHash(storedHash);
     }
     this.toolbar.setPageNumber(this.pdfViewer.currentPageNumber, this.pdfViewer.currentPageLabel);
     this.secondaryToolbar.setPageNumber(this.pdfViewer.currentPageNumber);
     if (!this.pdfViewer.currentScaleValue) {
       this.pdfViewer.currentScaleValue = _ui_utils.DEFAULT_SCALE_VALUE;
@@ -1933,43 +1955,40 @@ function webViewerUpdateViewarea(evt) {
       'zoom': location.scale,
       'scrollLeft': location.left,
       'scrollTop': location.top
     }).catch(function () {});
   }
   let href = PDFViewerApplication.pdfLinkService.getAnchorUrl(location.pdfOpenParams);
   PDFViewerApplication.appConfig.toolbar.viewBookmark.href = href;
   PDFViewerApplication.appConfig.secondaryToolbar.viewBookmarkButton.href = href;
-  PDFViewerApplication.pdfHistory.updateCurrentBookmark(location.pdfOpenParams, location.pageNumber);
   let currentPage = PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1);
   let loading = currentPage.renderingState !== _pdf_rendering_queue.RenderingStates.FINISHED;
   PDFViewerApplication.toolbar.updateLoadingIndicatorState(loading);
 }
 function webViewerResize() {
   let { pdfDocument, pdfViewer } = PDFViewerApplication;
   if (!pdfDocument) {
     return;
   }
   let currentScaleValue = pdfViewer.currentScaleValue;
   if (currentScaleValue === 'auto' || currentScaleValue === 'page-fit' || currentScaleValue === 'page-width') {
     pdfViewer.currentScaleValue = currentScaleValue;
   }
   pdfViewer.update();
 }
 function webViewerHashchange(evt) {
-  if (PDFViewerApplication.pdfHistory.isHashChangeUnlocked) {
-    let hash = evt.hash;
-    if (!hash) {
-      return;
-    }
-    if (!PDFViewerApplication.isInitialViewSet) {
-      PDFViewerApplication.initialBookmark = hash;
-    } else {
-      PDFViewerApplication.pdfLinkService.setHash(hash);
-    }
+  let hash = evt.hash;
+  if (!hash) {
+    return;
+  }
+  if (!PDFViewerApplication.isInitialViewSet) {
+    PDFViewerApplication.initialBookmark = hash;
+  } else if (!PDFViewerApplication.pdfHistory.popStateInProgress) {
+    PDFViewerApplication.pdfLinkService.setHash(hash);
   }
 }
 let webViewerFileInputChange;
 ;
 function webViewerPresentationMode() {
   PDFViewerApplication.requestPresentationMode();
 }
 function webViewerOpenFile() {
@@ -2297,32 +2316,16 @@ function webViewerKeyDown(evt) {
         break;
     }
   }
   if (!handled && !isViewerInPresentationMode) {
     if (evt.keyCode >= 33 && evt.keyCode <= 40 || evt.keyCode === 32 && curElementTagName !== 'BUTTON') {
       ensureViewerFocused = true;
     }
   }
-  if (cmd === 2) {
-    switch (evt.keyCode) {
-      case 37:
-        if (isViewerInPresentationMode) {
-          PDFViewerApplication.pdfHistory.back();
-          handled = true;
-        }
-        break;
-      case 39:
-        if (isViewerInPresentationMode) {
-          PDFViewerApplication.pdfHistory.forward();
-          handled = true;
-        }
-        break;
-    }
-  }
   if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) {
     pdfViewer.focus();
   }
   if (handled) {
     evt.preventDefault();
   }
 }
 function apiPageModeToSidebarView(mode) {
@@ -2419,27 +2422,28 @@ class PDFLinkService {
       } else {
         console.error(`PDFLinkService.navigateTo: "${destRef}" is not ` + `a valid destination reference, for dest="${dest}".`);
         return;
       }
       if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) {
         console.error(`PDFLinkService.navigateTo: "${pageNumber}" is not ` + `a valid page number, for dest="${dest}".`);
         return;
       }
+      if (this.pdfHistory) {
+        this.pdfHistory.pushCurrentPosition();
+        this.pdfHistory.push({
+          namedDest,
+          explicitDest,
+          pageNumber
+        });
+      }
       this.pdfViewer.scrollPageIntoView({
         pageNumber,
         destArray: explicitDest
       });
-      if (this.pdfHistory) {
-        this.pdfHistory.push({
-          dest: explicitDest,
-          hash: namedDest,
-          page: pageNumber
-        });
-      }
     };
     new Promise((resolve, reject) => {
       if (typeof dest === 'string') {
         this.pdfDocument.getDestination(dest).then(destArray => {
           resolve({
             namedDest: dest,
             explicitDest: destArray
           });
@@ -2478,19 +2482,16 @@ class PDFLinkService {
       if ('search' in params) {
         this.eventBus.dispatch('findfromurlhash', {
           source: this,
           query: params['search'].replace(/"/g, ''),
           phraseSearch: params['phrase'] === 'true'
         });
       }
       if ('nameddest' in params) {
-        if (this.pdfHistory) {
-          this.pdfHistory.updateNextHashParam(params.nameddest);
-        }
         this.navigateTo(params.nameddest);
         return;
       }
       if ('page' in params) {
         pageNumber = params.page | 0 || 1;
       }
       if ('zoom' in params) {
         let zoomArgs = params.zoom.split(',');
@@ -2533,19 +2534,16 @@ class PDFLinkService {
       dest = unescape(hash);
       try {
         dest = JSON.parse(dest);
         if (!(dest instanceof Array)) {
           dest = dest.toString();
         }
       } catch (ex) {}
       if (typeof dest === 'string' || isValidExplicitDestination(dest)) {
-        if (this.pdfHistory) {
-          this.pdfHistory.updateNextHashParam(dest);
-        }
         this.navigateTo(dest);
         return;
       }
       console.error(`PDFLinkService.setHash: "${unescape(hash)}" is not ` + 'a valid destination.');
     }
   }
   executeNamedAction(action) {
     switch (action) {
@@ -4405,312 +4403,355 @@ exports.PDFFindBar = PDFFindBar;
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.PDFHistory = undefined;
+exports.isDestsEqual = exports.PDFHistory = undefined;
+
+var _ui_utils = __webpack_require__(0);
 
 var _dom_events = __webpack_require__(2);
 
-function PDFHistory(options) {
-  this.linkService = options.linkService;
-  this.eventBus = options.eventBus || (0, _dom_events.getGlobalEventBus)();
-  this.initialized = false;
-  this.initialDestination = null;
-  this.initialBookmark = null;
+const HASH_CHANGE_TIMEOUT = 1000;
+const POSITION_UPDATED_THRESHOLD = 50;
+const UPDATE_VIEWAREA_TIMEOUT = 2000;
+function getCurrentHash() {
+  return document.location.hash;
+}
+function parseCurrentHash(linkService) {
+  let hash = unescape(getCurrentHash()).substring(1);
+  let params = (0, _ui_utils.parseQueryString)(hash);
+  let page = params.page | 0;
+  if (!(Number.isInteger(page) && page > 0 && page <= linkService.pagesCount)) {
+    page = null;
+  }
+  return {
+    hash,
+    page
+  };
 }
-PDFHistory.prototype = {
-  initialize: function pdfHistoryInitialize(fingerprint) {
-    this.initialized = true;
-    this.reInitialized = false;
-    this.allowHashChange = true;
-    this.historyUnlocked = true;
-    this.isViewerInPresentationMode = false;
-    this.previousHash = window.location.hash.substring(1);
-    this.currentBookmark = '';
-    this.currentPage = 0;
-    this.updatePreviousBookmark = false;
-    this.previousBookmark = '';
-    this.previousPage = 0;
-    this.nextHashParam = '';
+class PDFHistory {
+  constructor({ linkService, eventBus }) {
+    this.linkService = linkService;
+    this.eventBus = eventBus || (0, _dom_events.getGlobalEventBus)();
+    this.initialized = false;
+    this.initialBookmark = null;
+    this._boundEvents = Object.create(null);
+    this._isViewerInPresentationMode = false;
+    this._isPagesLoaded = false;
+    this.eventBus.on('presentationmodechanged', evt => {
+      this._isViewerInPresentationMode = evt.active || evt.switchInProgress;
+    });
+    this.eventBus.on('pagesloaded', evt => {
+      this._isPagesLoaded = !!evt.pagesCount;
+    });
+  }
+  initialize(fingerprint, resetHistory = false) {
+    if (!fingerprint || typeof fingerprint !== 'string') {
+      console.error('PDFHistory.initialize: The "fingerprint" must be a non-empty string.');
+      return;
+    }
+    let reInitialized = this.initialized && this.fingerprint !== fingerprint;
     this.fingerprint = fingerprint;
-    this.currentUid = this.uid = 0;
-    this.current = {};
-    var state = window.history.state;
-    if (this._isStateObjectDefined(state)) {
-      if (state.target.dest) {
-        this.initialDestination = state.target.dest;
-      } else {
-        this.initialBookmark = state.target.hash;
-      }
-      this.currentUid = state.uid;
-      this.uid = state.uid + 1;
-      this.current = state.target;
-    } else {
-      if (state && state.fingerprint && this.fingerprint !== state.fingerprint) {
-        this.reInitialized = true;
-      }
-      this._pushOrReplaceState({ fingerprint: this.fingerprint }, true);
-    }
-    var self = this;
-    window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
-      if (!self.historyUnlocked) {
-        return;
-      }
-      if (evt.state) {
-        self._goTo(evt.state);
+    if (!this.initialized) {
+      this._bindEvents();
+    }
+    let state = window.history.state;
+    this.initialized = true;
+    this.initialBookmark = null;
+    this._popStateInProgress = false;
+    this._blockHashChange = 0;
+    this._currentHash = getCurrentHash();
+    this._numPositionUpdates = 0;
+    this._currentUid = this._uid = 0;
+    this._destination = null;
+    this._position = null;
+    if (!this._isValidState(state) || resetHistory) {
+      let { hash, page } = parseCurrentHash(this.linkService);
+      if (!hash || reInitialized || resetHistory) {
+        this._pushOrReplaceState(null, true);
         return;
       }
-      if (self.uid === 0) {
-        var previousParams = self.previousHash && self.currentBookmark && self.previousHash !== self.currentBookmark ? {
-          hash: self.currentBookmark,
-          page: self.currentPage
-        } : { page: 1 };
-        replacePreviousHistoryState(previousParams, function () {
-          updateHistoryWithCurrentHash();
-        });
-      } else {
-        updateHistoryWithCurrentHash();
-      }
-    });
-    function updateHistoryWithCurrentHash() {
-      self.previousHash = window.location.hash.slice(1);
-      self._pushToHistory({ hash: self.previousHash }, false, true);
-      self._updatePreviousBookmark();
-    }
-    function replacePreviousHistoryState(params, callback) {
-      self.historyUnlocked = false;
-      self.allowHashChange = false;
-      window.addEventListener('popstate', rewriteHistoryAfterBack);
-      history.back();
-      function rewriteHistoryAfterBack() {
-        window.removeEventListener('popstate', rewriteHistoryAfterBack);
-        window.addEventListener('popstate', rewriteHistoryAfterForward);
-        self._pushToHistory(params, false, true);
-        history.forward();
-      }
-      function rewriteHistoryAfterForward() {
-        window.removeEventListener('popstate', rewriteHistoryAfterForward);
-        self.allowHashChange = true;
-        self.historyUnlocked = true;
-        callback();
-      }
-    }
-    function pdfHistoryBeforeUnload() {
-      var previousParams = self._getPreviousParams(null, true);
-      if (previousParams) {
-        var replacePrevious = !self.current.dest && self.current.hash !== self.previousHash;
-        self._pushToHistory(previousParams, false, replacePrevious);
-        self._updatePreviousBookmark();
-      }
-      window.removeEventListener('beforeunload', pdfHistoryBeforeUnload);
-    }
-    window.addEventListener('beforeunload', pdfHistoryBeforeUnload);
-    window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
-      window.addEventListener('beforeunload', pdfHistoryBeforeUnload);
-    });
-    self.eventBus.on('presentationmodechanged', function (e) {
-      self.isViewerInPresentationMode = e.active;
-    });
-  },
-  clearHistoryState: function pdfHistory_clearHistoryState() {
-    this._pushOrReplaceState(null, true);
-  },
-  _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
-    return state && state.uid >= 0 && state.fingerprint && this.fingerprint === state.fingerprint && state.target && state.target.hash ? true : false;
-  },
-  _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, replace) {
-    if (replace) {
-      window.history.replaceState(stateObj, '');
-    } else {
-      window.history.pushState(stateObj, '');
-    }
-  },
-  get isHashChangeUnlocked() {
-    if (!this.initialized) {
-      return true;
-    }
-    return this.allowHashChange;
-  },
-  _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() {
-    if (this.updatePreviousBookmark && this.currentBookmark && this.currentPage) {
-      this.previousBookmark = this.currentBookmark;
-      this.previousPage = this.currentPage;
-      this.updatePreviousBookmark = false;
-    }
-  },
-  updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, pageNum) {
-    if (this.initialized) {
-      this.currentBookmark = bookmark.substring(1);
-      this.currentPage = pageNum | 0;
-      this._updatePreviousBookmark();
-    }
-  },
-  updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
-    if (this.initialized) {
-      this.nextHashParam = param;
-    }
-  },
-  push: function pdfHistoryPush(params, isInitialBookmark) {
-    if (!(this.initialized && this.historyUnlocked)) {
+      this._pushOrReplaceState({
+        hash,
+        page
+      }, true);
       return;
     }
-    if (params.dest && !params.hash) {
-      params.hash = this.current.hash && this.current.dest && this.current.dest === params.dest ? this.current.hash : this.linkService.getDestinationHash(params.dest).split('#')[1];
-    }
-    if (params.page) {
-      params.page |= 0;
-    }
-    if (isInitialBookmark) {
-      var target = window.history.state.target;
-      if (!target) {
-        this._pushToHistory(params, false);
-        this.previousHash = window.location.hash.substring(1);
-      }
-      this.updatePreviousBookmark = this.nextHashParam ? false : true;
-      if (target) {
-        this._updatePreviousBookmark();
-      }
-      return;
-    }
-    if (this.nextHashParam) {
-      if (this.nextHashParam === params.hash) {
-        this.nextHashParam = null;
-        this.updatePreviousBookmark = true;
-        return;
-      }
-      this.nextHashParam = null;
-    }
-    if (params.hash) {
-      if (this.current.hash) {
-        if (this.current.hash !== params.hash) {
-          this._pushToHistory(params, true);
-        } else {
-          if (!this.current.page && params.page) {
-            this._pushToHistory(params, false, true);
-          }
-          this.updatePreviousBookmark = true;
-        }
-      } else {
-        this._pushToHistory(params, true);
-      }
-    } else if (this.current.page && params.page && this.current.page !== params.page) {
-      this._pushToHistory(params, true);
-    }
-  },
-  _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, beforeUnload) {
-    if (!(this.currentBookmark && this.currentPage)) {
-      return null;
-    } else if (this.updatePreviousBookmark) {
-      this.updatePreviousBookmark = false;
-    }
-    if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
-      return null;
-    }
-    if (!this.current.dest && !onlyCheckPage || beforeUnload) {
-      if (this.previousBookmark === this.currentBookmark) {
-        return null;
-      }
-    } else if (this.current.page || onlyCheckPage) {
-      if (this.previousPage === this.currentPage) {
-        return null;
-      }
-    } else {
-      return null;
-    }
-    var params = {
-      hash: this.currentBookmark,
-      page: this.currentPage
-    };
-    if (this.isViewerInPresentationMode) {
-      params.hash = null;
-    }
-    return params;
-  },
-  _stateObj: function pdfHistory_stateObj(params) {
-    return {
-      fingerprint: this.fingerprint,
-      uid: this.uid,
-      target: params
-    };
-  },
-  _pushToHistory: function pdfHistory_pushToHistory(params, addPrevious, overwrite) {
+    let destination = state.destination;
+    this._updateInternalState(destination, state.uid, true);
+    if (destination.dest) {
+      this.initialBookmark = JSON.stringify(destination.dest);
+      this._destination.page = null;
+    } else if (destination.hash) {
+      this.initialBookmark = destination.hash;
+    } else if (destination.page) {
+      this.initialBookmark = `page=${destination.page}`;
+    }
+  }
+  push({ namedDest, explicitDest, pageNumber }) {
     if (!this.initialized) {
       return;
     }
-    if (!params.hash && params.page) {
-      params.hash = 'page=' + params.page;
-    }
-    if (addPrevious && !overwrite) {
-      var previousParams = this._getPreviousParams();
-      if (previousParams) {
-        var replacePrevious = !this.current.dest && this.current.hash !== this.previousHash;
-        this._pushToHistory(previousParams, false, replacePrevious);
-      }
-    }
-    this._pushOrReplaceState(this._stateObj(params), overwrite || this.uid === 0);
-    this.currentUid = this.uid++;
-    this.current = params;
-    this.updatePreviousBookmark = true;
-  },
-  _goTo: function pdfHistory_goTo(state) {
-    if (!(this.initialized && this.historyUnlocked && this._isStateObjectDefined(state))) {
+    if (namedDest && typeof namedDest !== 'string' || !(explicitDest instanceof Array) || !(Number.isInteger(pageNumber) && pageNumber > 0 && pageNumber <= this.linkService.pagesCount)) {
+      console.error('PDFHistory.push: Invalid parameters.');
+      return;
+    }
+    let hash = namedDest || JSON.stringify(explicitDest);
+    if (!hash) {
+      return;
+    }
+    let forceReplace = false;
+    if (this._destination && (this._destination.hash === hash || isDestsEqual(this._destination.dest, explicitDest))) {
+      if (this._destination.page) {
+        return;
+      }
+      forceReplace = true;
+    }
+    if (this._popStateInProgress && !forceReplace) {
+      return;
+    }
+    this._pushOrReplaceState({
+      dest: explicitDest,
+      hash,
+      page: pageNumber
+    }, forceReplace);
+  }
+  pushCurrentPosition() {
+    if (!this.initialized || this._popStateInProgress) {
+      return;
+    }
+    this._tryPushCurrentPosition();
+  }
+  back() {
+    if (!this.initialized || this._popStateInProgress) {
+      return;
+    }
+    let state = window.history.state;
+    if (this._isValidState(state) && state.uid > 0) {
+      window.history.back();
+    }
+  }
+  forward() {
+    if (!this.initialized || this._popStateInProgress) {
+      return;
+    }
+    let state = window.history.state;
+    if (this._isValidState(state) && state.uid < this._uid - 1) {
+      window.history.forward();
+    }
+  }
+  get popStateInProgress() {
+    return this.initialized && (this._popStateInProgress || this._blockHashChange > 0);
+  }
+  _pushOrReplaceState(destination, forceReplace = false) {
+    let shouldReplace = forceReplace || !this._destination;
+    let newState = {
+      fingerprint: this.fingerprint,
+      uid: shouldReplace ? this._currentUid : this._uid,
+      destination
+    };
+    this._updateInternalState(destination, newState.uid);
+    if (shouldReplace) {
+      window.history.replaceState(newState, '');
+    } else {
+      window.history.pushState(newState, '');
+    }
+  }
+  _tryPushCurrentPosition(temporary = false) {
+    if (!this._position) {
+      return;
+    }
+    let position = this._position;
+    if (temporary) {
+      position = (0, _ui_utils.cloneObj)(this._position);
+      position.temporary = true;
+    }
+    if (!this._destination) {
+      this._pushOrReplaceState(position);
+      return;
+    }
+    if (this._destination.temporary) {
+      this._pushOrReplaceState(position, true);
+      return;
+    }
+    if (this._destination.hash === position.hash) {
+      return;
+    }
+    if (!this._destination.page && (POSITION_UPDATED_THRESHOLD <= 0 || this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) {
       return;
     }
-    if (!this.reInitialized && state.uid < this.currentUid) {
-      var previousParams = this._getPreviousParams(true);
-      if (previousParams) {
-        this._pushToHistory(this.current, false);
-        this._pushToHistory(previousParams, false);
-        this.currentUid = state.uid;
+    let forceReplace = false;
+    if (this._destination.page === position.first || this._destination.page === position.page) {
+      if (this._destination.dest || !this._destination.first) {
+        return;
+      }
+      forceReplace = true;
+    }
+    this._pushOrReplaceState(position, forceReplace);
+  }
+  _isValidState(state) {
+    if (!state) {
+      return false;
+    }
+    if (state.fingerprint !== this.fingerprint) {
+      return false;
+    }
+    if (!Number.isInteger(state.uid) || state.uid < 0) {
+      return false;
+    }
+    if (state.destination === null || typeof state.destination !== 'object') {
+      return false;
+    }
+    return true;
+  }
+  _updateInternalState(destination, uid, removeTemporary = false) {
+    if (removeTemporary && destination && destination.temporary) {
+      delete destination.temporary;
+    }
+    this._destination = destination;
+    this._currentUid = uid;
+    this._uid = this._currentUid + 1;
+    this._numPositionUpdates = 0;
+  }
+  _updateViewarea({ location }) {
+    if (this._updateViewareaTimeout) {
+      clearTimeout(this._updateViewareaTimeout);
+      this._updateViewareaTimeout = null;
+    }
+    this._position = {
+      hash: this._isViewerInPresentationMode ? `page=${location.pageNumber}` : location.pdfOpenParams.substring(1),
+      page: this.linkService.page,
+      first: location.pageNumber
+    };
+    if (this._popStateInProgress) {
+      return;
+    }
+    if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && this._destination && !this._destination.page) {
+      this._numPositionUpdates++;
+    }
+    if (UPDATE_VIEWAREA_TIMEOUT > 0) {
+      this._updateViewareaTimeout = setTimeout(() => {
+        if (!this._popStateInProgress) {
+          this._tryPushCurrentPosition(true);
+        }
+        this._updateViewareaTimeout = null;
+      }, UPDATE_VIEWAREA_TIMEOUT);
+    }
+  }
+  _popState({ state }) {
+    let newHash = getCurrentHash(),
+        hashChanged = this._currentHash !== newHash;
+    this._currentHash = newHash;
+    if (!state || false) {
+      this._currentUid = this._uid;
+      let { hash, page } = parseCurrentHash(this.linkService);
+      this._pushOrReplaceState({
+        hash,
+        page
+      }, true);
+      return;
+    }
+    if (!this._isValidState(state)) {
+      return;
+    }
+    this._popStateInProgress = true;
+    if (hashChanged) {
+      this._blockHashChange++;
+      (0, _ui_utils.waitOnEventOrTimeout)({
+        target: window,
+        name: 'hashchange',
+        delay: HASH_CHANGE_TIMEOUT
+      }).then(() => {
+        this._blockHashChange--;
+      });
+    }
+    if (state.uid < this._currentUid && this._position && this._destination) {
+      let shouldGoBack = false;
+      if (this._destination.temporary) {
+        this._pushOrReplaceState(this._position);
+        shouldGoBack = true;
+      } else if (this._destination.page && this._destination.page !== this._position.first && this._destination.page !== this._position.page) {
+        this._pushOrReplaceState(this._destination);
+        this._pushOrReplaceState(this._position);
+        shouldGoBack = true;
+      }
+      if (shouldGoBack) {
+        this._currentUid = state.uid;
         window.history.back();
         return;
       }
     }
-    this.historyUnlocked = false;
-    if (state.target.dest) {
-      this.linkService.navigateTo(state.target.dest);
-    } else {
-      this.linkService.setHash(state.target.hash);
-    }
-    this.currentUid = state.uid;
-    if (state.uid > this.uid) {
-      this.uid = state.uid;
-    }
-    this.current = state.target;
-    this.updatePreviousBookmark = true;
-    var currentHash = window.location.hash.substring(1);
-    if (this.previousHash !== currentHash) {
-      this.allowHashChange = false;
-    }
-    this.previousHash = currentHash;
-    this.historyUnlocked = true;
-  },
-  back: function pdfHistoryBack() {
-    this.go(-1);
-  },
-  forward: function pdfHistoryForward() {
-    this.go(1);
-  },
-  go: function pdfHistoryGo(direction) {
-    if (this.initialized && this.historyUnlocked) {
-      var state = window.history.state;
-      if (direction === -1 && state && state.uid > 0) {
-        window.history.back();
-      } else if (direction === 1 && state && state.uid < this.uid - 1) {
-        window.history.forward();
-      }
-    }
-  }
-};
+    let destination = state.destination;
+    this._updateInternalState(destination, state.uid, true);
+    if (destination.dest) {
+      this.linkService.navigateTo(destination.dest);
+    } else if (destination.hash) {
+      this.linkService.setHash(destination.hash);
+    } else if (destination.page) {
+      this.linkService.page = destination.page;
+    }
+    Promise.resolve().then(() => {
+      this._popStateInProgress = false;
+    });
+  }
+  _bindEvents() {
+    let { _boundEvents, eventBus } = this;
+    _boundEvents.updateViewarea = this._updateViewarea.bind(this);
+    _boundEvents.popState = this._popState.bind(this);
+    _boundEvents.pageHide = evt => {
+      if (!this._destination) {
+        this._tryPushCurrentPosition();
+      }
+    };
+    eventBus.on('updateviewarea', _boundEvents.updateViewarea);
+    window.addEventListener('popstate', _boundEvents.popState);
+    window.addEventListener('pagehide', _boundEvents.pageHide);
+  }
+}
+function isDestsEqual(firstDest, secondDest) {
+  function isEntryEqual(first, second) {
+    if (typeof first !== typeof second) {
+      return false;
+    }
+    if (first instanceof Array || second instanceof Array) {
+      return false;
+    }
+    if (first !== null && typeof first === 'object' && second !== null) {
+      if (Object.keys(first).length !== Object.keys(second).length) {
+        return false;
+      }
+      for (var key in first) {
+        if (!isEntryEqual(first[key], second[key])) {
+          return false;
+        }
+      }
+      return true;
+    }
+    return first === second || Number.isNaN(first) && Number.isNaN(second);
+  }
+  if (!(firstDest instanceof Array && secondDest instanceof Array)) {
+    return false;
+  }
+  if (firstDest.length !== secondDest.length) {
+    return false;
+  }
+  for (let i = 0, ii = firstDest.length; i < ii; i++) {
+    if (!isEntryEqual(firstDest[i], secondDest[i])) {
+      return false;
+    }
+  }
+  return true;
+}
 exports.PDFHistory = PDFHistory;
+exports.isDestsEqual = isDestsEqual;
 
 /***/ }),
 /* 19 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/.gitignore
@@ -0,0 +1,5 @@
+/target
+/Cargo.lock
+*~
+*.swp
+*.swo
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/.travis.yml
@@ -0,0 +1,9 @@
+language: rust
+
+rust:
+    - nightly
+    - stable
+
+script:
+    - cargo build --verbose
+    - cargo test --verbose
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "webdriver"
+version = "0.30.0"
+authors = ["Mozilla Tools and Automation <tools@lists.mozilla.com>"]
+description = "Library implementing the wire protocol for the W3C WebDriver specification"
+documentation = "https://docs.rs/webdriver"
+repository = "https://github.com/mozilla/webdriver-rust"
+readme = "README.md"
+keywords = ["webdriver", "browser", "automation", "protocol", "w3c"]
+license = "MPL-2.0"
+
+[dependencies]
+backtrace = "0.3"
+cookie = {version = "0.9", default-features = false}
+hyper = "0.10"
+log = "0.3"
+regex = "0.2"
+rustc-serialize = "0.3"
+time = "0.1"
+url = "1"
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/README.md
@@ -0,0 +1,6 @@
+# webdriver Rust library [![Crate version](https://img.shields.io/crates/v/webdriver.svg)](https://crates.io/crates/webdriver) [![Documentation](https://docs.rs/webdriver/badge.svg)](https://docs.rs/webdriver/) [![Build status](https://travis-ci.org/mozilla/webdriver-rust.svg?branch=master)](https://travis-ci.org/mozilla/webdriver-rust) 
+
+As of right now this is an implementation
+for the server side of the WebDriver API in Rust,
+not the client side.
+
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/capabilities.rs
@@ -0,0 +1,551 @@
+use command::Parameters;
+use error::{ErrorStatus, WebDriverError, WebDriverResult};
+use rustc_serialize::json::{Json, ToJson};
+use std::collections::BTreeMap;
+use url::Url;
+
+pub type Capabilities = BTreeMap<String, Json>;
+
+/// Trait for objects that can be used to inspect browser capabilities
+///
+/// The main methods in this trait are called with a Capabilites object
+/// resulting from a full set of potential capabilites for the session.
+/// Given those Capabilities they return a property of the browser instance
+/// that would be initiated. In many cases this will be independent of the
+/// input, but in the case of e.g. browser version, it might depend on a
+/// path to the binary provided as a capability.
+pub trait BrowserCapabilities {
+    /// Set up the Capabilites object
+    ///
+    /// Typically used to create any internal caches
+    fn init(&mut self, &Capabilities);
+
+    /// Name of the browser
+    fn browser_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Version number of the browser
+    fn browser_version(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Compare actual browser version to that provided in a version specifier
+    ///
+    /// Parameters are the actual browser version and the comparison string,
+    /// respectively. The format of the comparison string is implementation-defined.
+    fn compare_browser_version(&mut self, version: &str, comparison: &str) -> WebDriverResult<bool>;
+    /// Name of the platform/OS
+    fn platform_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Whether insecure certificates are supported
+    fn accept_insecure_certs(&mut self, &Capabilities) -> WebDriverResult<bool>;
+
+    fn accept_proxy(&mut self, proxy_settings: &BTreeMap<String, Json>, &Capabilities) -> WebDriverResult<bool>;
+
+    /// Type check custom properties
+    ///
+    /// Check that custom properties containing ":" have the correct data types.
+    /// Properties that are unrecognised must be ignored i.e. return without
+    /// error.
+    fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()>;
+    /// Check if custom properties are accepted capabilites
+    ///
+    /// Check that custom properties containing ":" are compatible with
+    /// the implementation.
+    fn accept_custom(&mut self, name: &str, value: &Json, merged: &Capabilities) -> WebDriverResult<bool>;
+}
+
+/// Trait to abstract over various version of the new session parameters
+///
+/// This trait is expected to be implemented on objects holding the capabilities
+/// from a new session command.
+pub trait CapabilitiesMatching {
+    /// Match the BrowserCapabilities against some candidate capabilites
+    ///
+    /// Takes a BrowserCapabilites object and returns a set of capabilites that
+    /// are valid for that browser, if any, or None if there are no matching
+    /// capabilities.
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>>;
+}
+
+#[derive(Debug, PartialEq)]
+pub struct SpecNewSessionParameters {
+    pub alwaysMatch: Capabilities,
+    pub firstMatch: Vec<Capabilities>,
+}
+
+impl SpecNewSessionParameters {
+    fn validate<T: BrowserCapabilities>(&self,
+                                        mut capabilities: Capabilities,
+                                        browser_capabilities: &T) -> WebDriverResult<Capabilities> {
+        // Filter out entries with the value `null`
+        let null_entries = capabilities
+            .iter()
+            .filter(|&(_, ref value)| **value == Json::Null)
+            .map(|(k, _)| k.clone())
+            .collect::<Vec<String>>();
+        for key in null_entries {
+            capabilities.remove(&key);
+        }
+
+        for (key, value) in capabilities.iter() {
+            match &**key {
+                "acceptInsecureCerts" => if !value.is_boolean() {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       "acceptInsecureCerts was not a boolean"))
+                    },
+                x @ "browserName" |
+                x @ "browserVersion" |
+                x @ "platformName" => if !value.is_string() {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} was not a boolean", x)))
+                    },
+                "pageLoadStrategy" => {
+                    try!(SpecNewSessionParameters::validate_page_load_strategy(value))
+                }
+                "proxy" => {
+                    try!(SpecNewSessionParameters::validate_proxy(value))
+                },
+                "timeouts" => {
+                    try!(SpecNewSessionParameters::validate_timeouts(value))
+                },
+                "unhandledPromptBehavior" => {
+                    try!(SpecNewSessionParameters::validate_unhandled_prompt_behaviour(value))
+                }
+                x => {
+                    if !x.contains(":") {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} was not a the name of a known capability or a valid extension capability", x)))
+                    } else {
+                        try!(browser_capabilities.validate_custom(x, value));
+                    }
+                }
+            }
+        }
+        Ok(capabilities)
+    }
+
+    fn validate_page_load_strategy(value: &Json) -> WebDriverResult<()> {
+        match value {
+            &Json::String(ref x) => {
+                match &**x {
+                    "normal" |
+                    "eager" |
+                    "none" => {},
+                    x => {
+                        return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("\"{}\" not a valid page load strategy", x)))
+                    }
+                }
+            }
+            _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "pageLoadStrategy was not a string"))
+        }
+        Ok(())
+    }
+
+    fn validate_proxy(proxy_value: &Json) -> WebDriverResult<()> {
+        let obj = try_opt!(proxy_value.as_object(),
+                           ErrorStatus::InvalidArgument,
+                           "proxy was not an object");
+        for (key, value) in obj.iter() {
+            match &**key {
+                "proxyType" => match value.as_string() {
+                    Some("pac") |
+                    Some("direct") |
+                    Some("autodetect") |
+                    Some("system") |
+                    Some("manual") => {},
+                    Some(x) => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("{} was not a valid proxyType value", x))),
+                    None => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "proxyType value was not a string")),
+                },
+                "proxyAutoconfigUrl" => match value.as_string() {
+                    Some(x) => {
+                        try!(Url::parse(x).or(Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            "proxyAutoconfigUrl was not a valid url"))));
+                    },
+                    None => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "proxyAutoconfigUrl was not a string"
+                    ))
+                },
+                "ftpProxy" => try!(SpecNewSessionParameters::validate_host(value)),
+                "httpProxy" => try!(SpecNewSessionParameters::validate_host(value)),
+                "noProxy" => try!(SpecNewSessionParameters::validate_no_proxy(value)),
+                "sslProxy" => try!(SpecNewSessionParameters::validate_host(value)),
+                "socksProxy" => try!(SpecNewSessionParameters::validate_host(value)),
+                "socksVersion" => if !value.is_number() {
+                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                   "socksVersion was not a number"))
+                },
+                "socksUsername" => if !value.is_string() {
+                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                   "socksUsername was not a string"))
+                },
+                "socksPassword" => if !value.is_string() {
+                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                   "socksPassword was not a string"))
+                },
+                x => return Err(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    format!("{} was not a valid proxy configuration capability", x)))
+            }
+        }
+        Ok(())
+    }
+
+    fn validate_no_proxy(value: &Json) -> WebDriverResult<()> {
+        match value.as_array() {
+            Some(hosts) => {
+                for host in hosts {
+                    match host.as_string() {
+                        Some(_) => {},
+                        None => return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("{} was not a string", host)
+                        ))
+                    }
+                }
+            },
+            None => return Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                format!("{} was not an array", value)
+            ))
+        }
+
+        Ok(())
+    }
+
+    /// Validate whether a named capability is JSON value is a string containing a host
+    /// and possible port
+    fn validate_host(value: &Json) -> WebDriverResult<()> {
+        match value.as_string() {
+            Some(host) => {
+                if host.contains("://") {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("{} contains a scheme", host)));
+                }
+
+                // Temporarily add a scheme so the host can be parsed as URL
+                let s = String::from(format!("http://{}", host));
+                let url = try!(Url::parse(s.as_str()).or(Err(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    format!("{} is not a valid host", host)))));
+
+                if url.username() != "" ||
+                    url.password() != None ||
+                    url.path() != "/" ||
+                    url.query() != None ||
+                    url.fragment() != None {
+                        return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("{} was not of the form host[:port]", host)));
+                    }
+            },
+            None => return Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                format!("{} was not a string", value)
+            ))
+        }
+        Ok(())
+    }
+
+    fn validate_timeouts(value: &Json) -> WebDriverResult<()> {
+        let obj = try_opt!(value.as_object(),
+                           ErrorStatus::InvalidArgument,
+                           "timeouts capability was not an object");
+        for (key, value) in obj.iter() {
+            match &**key {
+                x @ "script" |
+                x @ "pageLoad" |
+                x @ "implicit" => {
+                    let timeout = try_opt!(value.as_i64(),
+                                           ErrorStatus::InvalidArgument,
+                                           format!("{} timeouts value was not an integer", x));
+                    if timeout < 0 {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} timeouts value was negative", x)))
+                    }
+                },
+                x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                    format!("{} was not a valid timeouts capability", x)))
+            }
+        }
+        Ok(())
+    }
+
+    fn validate_unhandled_prompt_behaviour(value: &Json) -> WebDriverResult<()> {
+        let behaviour = try_opt!(value.as_string(),
+                                 ErrorStatus::InvalidArgument,
+                                 "unhandledPromptBehavior capability was not a string");
+        match behaviour {
+            "dismiss" |
+            "accept" => {},
+            x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                format!("{} was not a valid unhandledPromptBehavior value", x)))        }
+        Ok(())
+    }
+}
+
+impl Parameters for SpecNewSessionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<SpecNewSessionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let capabilities = try_opt!(
+            try_opt!(data.get("capabilities"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'capabilities' parameter").as_object(),
+            ErrorStatus::InvalidArgument,
+                     "'capabilities' parameter is not an object");
+
+        let default_always_match = Json::Object(Capabilities::new());
+        let always_match = try_opt!(capabilities.get("alwaysMatch")
+                                   .unwrap_or(&default_always_match)
+                                   .as_object(),
+                                   ErrorStatus::InvalidArgument,
+                                   "'alwaysMatch' parameter is not an object");
+        let default_first_matches = Json::Array(vec![]);
+        let first_matches = try!(
+            try_opt!(capabilities.get("firstMatch")
+                     .unwrap_or(&default_first_matches)
+                     .as_array(),
+                     ErrorStatus::InvalidArgument,
+                     "'firstMatch' parameter is not an array")
+                .iter()
+                .map(|x| x.as_object()
+                     .map(|x| x.clone())
+                     .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "'firstMatch' entry is not an object")))
+                .collect::<WebDriverResult<Vec<Capabilities>>>());
+
+        return Ok(SpecNewSessionParameters {
+            alwaysMatch: always_match.clone(),
+            firstMatch: first_matches
+        });
+    }
+}
+
+impl ToJson for SpecNewSessionParameters {
+    fn to_json(&self) -> Json {
+        let mut body = BTreeMap::new();
+        let mut capabilities = BTreeMap::new();
+        capabilities.insert("alwaysMatch".into(), self.alwaysMatch.to_json());
+        capabilities.insert("firstMatch".into(), self.firstMatch.to_json());
+        body.insert("capabilities".into(), capabilities.to_json());
+        Json::Object(body)
+    }
+}
+
+impl CapabilitiesMatching for SpecNewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        let default = vec![BTreeMap::new()];
+        let capabilities_list = if self.firstMatch.len() > 0 {
+            &self.firstMatch
+        } else {
+            &default
+        };
+
+        let merged_capabilities = try!(capabilities_list
+            .iter()
+            .map(|first_match_entry| {
+                if first_match_entry.keys().any(|k| {
+                    self.alwaysMatch.contains_key(k)
+                }) {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "'firstMatch' key shadowed a value in 'alwaysMatch'"));
+                }
+                let mut merged = self.alwaysMatch.clone();
+                merged.append(&mut first_match_entry.clone());
+                Ok(merged)
+            })
+            .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
+            .collect::<WebDriverResult<Vec<Capabilities>>>());
+
+        let selected = merged_capabilities
+            .iter()
+            .filter_map(|merged| {
+                browser_capabilities.init(merged);
+
+                for (key, value) in merged.iter() {
+                    match &**key {
+                        "browserName" => {
+                            let browserValue = browser_capabilities
+                                .browser_name(merged)
+                                .ok()
+                                .and_then(|x| x);
+
+                            if value.as_string() != browserValue.as_ref().map(|x| &**x) {
+                                    return None;
+                            }
+                        },
+                        "browserVersion" => {
+                            let browserValue = browser_capabilities
+                                .browser_version(merged)
+                                .ok()
+                                .and_then(|x| x);
+                            // We already validated this was a string
+                            let version_cond = value.as_string().unwrap_or("");
+                            if let Some(version) = browserValue {
+                                if !browser_capabilities
+                                    .compare_browser_version(&*version, version_cond)
+                                    .unwrap_or(false) {
+                                        return None;
+                                    }
+                            } else {
+                                return None
+                            }
+                        },
+                        "platformName" => {
+                            let browserValue = browser_capabilities
+                                .platform_name(merged)
+                                .ok()
+                                .and_then(|x| x);
+                            if value.as_string() != browserValue.as_ref().map(|x| &**x) {
+                                return None;
+                            }
+                        }
+                        "acceptInsecureCerts" => {
+                            if value.as_boolean().unwrap_or(false) &&
+                                !browser_capabilities
+                                .accept_insecure_certs(merged)
+                                .unwrap_or(false) {
+                                return None;
+                            }
+                        },
+                        "proxy" => {
+                            let default = BTreeMap::new();
+                            let proxy = value.as_object().unwrap_or(&default);
+                            if !browser_capabilities.accept_proxy(&proxy,
+                                                                  merged)
+                                .unwrap_or(false) {
+                                return None
+                            }
+                        },
+                        name => {
+                            if name.contains(":") {
+                                if !browser_capabilities
+                                    .accept_custom(name, value, merged)
+                                    .unwrap_or(false) {
+                                        return None
+                                    }
+                            } else {
+                                // Accept the capability
+                            }
+                        }
+                    }
+                }
+
+                return Some(merged)
+            })
+            .next()
+            .map(|x| x.clone());
+            Ok(selected)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct LegacyNewSessionParameters {
+    pub desired: Capabilities,
+    pub required: Capabilities,
+}
+
+impl CapabilitiesMatching for LegacyNewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        /* For now don't do anything much, just merge the
+        desired and required and return the merged list. */
+
+        let mut capabilities: Capabilities = BTreeMap::new();
+        self.required.iter()
+            .chain(self.desired.iter())
+            .fold(&mut capabilities,
+                  |mut caps, (key, value)| {
+                      if !caps.contains_key(key) {
+                          caps.insert(key.clone(), value.clone());
+                      }
+                      caps});
+        browser_capabilities.init(&capabilities);
+        Ok(Some(capabilities))
+    }
+}
+
+impl Parameters for LegacyNewSessionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<LegacyNewSessionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let desired_capabilities =
+            if let Some(capabilities) = data.get("desiredCapabilities") {
+                try_opt!(capabilities.as_object(),
+                         ErrorStatus::InvalidArgument,
+                         "'desiredCapabilities' parameter is not an object").clone()
+            } else {
+                BTreeMap::new()
+            };
+
+        let required_capabilities =
+            if let Some(capabilities) = data.get("requiredCapabilities") {
+                try_opt!(capabilities.as_object(),
+                         ErrorStatus::InvalidArgument,
+                         "'requiredCapabilities' parameter is not an object").clone()
+            } else {
+                BTreeMap::new()
+            };
+
+        Ok(LegacyNewSessionParameters {
+            desired: desired_capabilities,
+            required: required_capabilities
+        })
+    }
+}
+
+impl ToJson for LegacyNewSessionParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("desiredCapabilities".to_owned(), self.desired.to_json());
+        data.insert("requiredCapabilities".to_owned(), self.required.to_json());
+        Json::Object(data)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rustc_serialize::json::Json;
+    use super::{WebDriverResult, SpecNewSessionParameters};
+
+    fn validate_proxy(value: &str) -> WebDriverResult<()> {
+        let data = Json::from_str(value).unwrap();
+        SpecNewSessionParameters::validate_proxy(&data)
+    }
+
+    #[test]
+    fn test_validate_proxy() {
+        // proxy hosts
+        validate_proxy("{\"httpProxy\": \"127.0.0.1\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"127.0.0.1:\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"127.0.0.1:3128\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"localhost\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"localhost:3128\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"[2001:db8::1]\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"[2001:db8::1]:3128\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"example.org\"}").unwrap();
+        validate_proxy("{\"httpProxy\": \"example.org:3128\"}").unwrap();
+
+        assert!(validate_proxy("{\"httpProxy\": \"http://example.org\"}").is_err());
+        assert!(validate_proxy("{\"httpProxy\": \"example.org:-1\"}").is_err());
+        assert!(validate_proxy("{\"httpProxy\": \"2001:db8::1\"}").is_err());
+
+        // no proxy for manual proxy type
+        validate_proxy("{\"noProxy\": [\"foo\"]}").unwrap();
+
+        assert!(validate_proxy("{\"noProxy\": \"foo\"}").is_err());
+        assert!(validate_proxy("{\"noProxy\": [42]}").is_err());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/command.rs
@@ -0,0 +1,1743 @@
+use capabilities::{SpecNewSessionParameters, LegacyNewSessionParameters,
+                   CapabilitiesMatching, BrowserCapabilities, Capabilities};
+use common::{Date, Nullable, WebElement, FrameId, LocatorStrategy};
+use error::{WebDriverResult, WebDriverError, ErrorStatus};
+use httpapi::{Route, WebDriverExtensionRoute, VoidWebDriverExtensionRoute};
+use regex::Captures;
+use rustc_serialize::json;
+use rustc_serialize::json::{ToJson, Json};
+use std::collections::BTreeMap;
+use std::default::Default;
+
+#[derive(Debug, PartialEq)]
+pub enum WebDriverCommand<T: WebDriverExtensionCommand> {
+    NewSession(NewSessionParameters),
+    DeleteSession,
+    Get(GetParameters),
+    GetCurrentUrl,
+    GoBack,
+    GoForward,
+    Refresh,
+    GetTitle,
+    GetPageSource,
+    GetWindowHandle,
+    GetWindowHandles,
+    CloseWindow,
+    GetWindowRect,
+    SetWindowRect(WindowRectParameters),
+    MinimizeWindow,
+    MaximizeWindow,
+    FullscreenWindow,
+    SwitchToWindow(SwitchToWindowParameters),
+    SwitchToFrame(SwitchToFrameParameters),
+    SwitchToParentFrame,
+    FindElement(LocatorParameters),
+    FindElements(LocatorParameters),
+    FindElementElement(WebElement, LocatorParameters),
+    FindElementElements(WebElement, LocatorParameters),
+    GetActiveElement,
+    IsDisplayed(WebElement),
+    IsSelected(WebElement),
+    GetElementAttribute(WebElement, String),
+    GetElementProperty(WebElement, String),
+    GetCSSValue(WebElement, String),
+    GetElementText(WebElement),
+    GetElementTagName(WebElement),
+    GetElementRect(WebElement),
+    IsEnabled(WebElement),
+    ExecuteScript(JavascriptCommandParameters),
+    ExecuteAsyncScript(JavascriptCommandParameters),
+    GetCookies,
+    GetNamedCookie(String),
+    AddCookie(AddCookieParameters),
+    DeleteCookies,
+    DeleteCookie(String),
+    GetTimeouts,
+    SetTimeouts(TimeoutsParameters),
+    ElementClick(WebElement),
+    ElementTap(WebElement),
+    ElementClear(WebElement),
+    ElementSendKeys(WebElement, SendKeysParameters),
+    PerformActions(ActionsParameters),
+    ReleaseActions,
+    DismissAlert,
+    AcceptAlert,
+    GetAlertText,
+    SendAlertText(SendKeysParameters),
+    TakeScreenshot,
+    TakeElementScreenshot(WebElement),
+    Status,
+    Extension(T)
+}
+
+pub trait WebDriverExtensionCommand : Clone + Send + PartialEq {
+    fn parameters_json(&self) -> Option<Json>;
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct VoidWebDriverExtensionCommand;
+
+impl WebDriverExtensionCommand for VoidWebDriverExtensionCommand {
+    fn parameters_json(&self) -> Option<Json> {
+        panic!("No extensions implemented");
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct WebDriverMessage <U: WebDriverExtensionRoute=VoidWebDriverExtensionRoute> {
+    pub session_id: Option<String>,
+    pub command: WebDriverCommand<U::Command>,
+}
+
+impl<U: WebDriverExtensionRoute> WebDriverMessage<U> {
+    pub fn new(session_id: Option<String>,
+               command: WebDriverCommand<U::Command>)
+               -> WebDriverMessage<U> {
+        WebDriverMessage {
+            session_id: session_id,
+            command: command,
+        }
+    }
+
+    pub fn from_http(match_type: Route<U>,
+                     params: &Captures,
+                     raw_body: &str,
+                     requires_body: bool)
+                     -> WebDriverResult<WebDriverMessage<U>> {
+        let session_id = WebDriverMessage::<U>::get_session_id(params);
+        let body_data = try!(WebDriverMessage::<U>::decode_body(raw_body, requires_body));
+
+        let command = match match_type {
+            Route::NewSession => {
+                let parameters: NewSessionParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::NewSession(parameters)
+            },
+            Route::DeleteSession => WebDriverCommand::DeleteSession,
+            Route::Get => {
+                let parameters: GetParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::Get(parameters)
+            },
+            Route::GetCurrentUrl => WebDriverCommand::GetCurrentUrl,
+            Route::GoBack => WebDriverCommand::GoBack,
+            Route::GoForward => WebDriverCommand::GoForward,
+            Route::Refresh => WebDriverCommand::Refresh,
+            Route::GetTitle => WebDriverCommand::GetTitle,
+            Route::GetPageSource => WebDriverCommand::GetPageSource,
+            Route::GetWindowHandle => WebDriverCommand::GetWindowHandle,
+            Route::GetWindowHandles => WebDriverCommand::GetWindowHandles,
+            Route::CloseWindow => WebDriverCommand::CloseWindow,
+            Route::GetTimeouts => WebDriverCommand::GetTimeouts,
+            Route::SetTimeouts => {
+                let parameters: TimeoutsParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::SetTimeouts(parameters)
+            },
+            Route::GetWindowRect | Route::GetWindowPosition | Route::GetWindowSize => WebDriverCommand::GetWindowRect,
+            Route::SetWindowRect | Route::SetWindowPosition | Route::SetWindowSize => {
+                let parameters: WindowRectParameters = Parameters::from_json(&body_data)?;
+                WebDriverCommand::SetWindowRect(parameters)
+            },
+            Route::MinimizeWindow => WebDriverCommand::MinimizeWindow,
+            Route::MaximizeWindow => WebDriverCommand::MaximizeWindow,
+            Route::FullscreenWindow => WebDriverCommand::FullscreenWindow,
+            Route::SwitchToWindow => {
+                let parameters: SwitchToWindowParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::SwitchToWindow(parameters)
+            }
+            Route::SwitchToFrame => {
+                let parameters: SwitchToFrameParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::SwitchToFrame(parameters)
+            },
+            Route::SwitchToParentFrame => WebDriverCommand::SwitchToParentFrame,
+            Route::FindElement => {
+                let parameters: LocatorParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::FindElement(parameters)
+            },
+            Route::FindElements => {
+                let parameters: LocatorParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::FindElements(parameters)
+            },
+            Route::FindElementElement => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let parameters: LocatorParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::FindElementElement(element, parameters)
+            },
+            Route::FindElementElements => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let parameters: LocatorParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::FindElementElements(element, parameters)
+            },
+            Route::GetActiveElement => WebDriverCommand::GetActiveElement,
+            Route::IsDisplayed => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::IsDisplayed(element)
+            },
+            Route::IsSelected => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::IsSelected(element)
+            },
+            Route::GetElementAttribute => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let attr = try_opt!(params.name("name"),
+                                    ErrorStatus::InvalidArgument,
+                                    "Missing name parameter").as_str();
+                WebDriverCommand::GetElementAttribute(element, attr.into())
+            },
+            Route::GetElementProperty => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let property = try_opt!(params.name("name"),
+                                        ErrorStatus::InvalidArgument,
+                                        "Missing name parameter").as_str();
+                WebDriverCommand::GetElementProperty(element, property.into())
+            },
+            Route::GetCSSValue => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let property = try_opt!(params.name("propertyName"),
+                                        ErrorStatus::InvalidArgument,
+                                        "Missing propertyName parameter").as_str();
+                WebDriverCommand::GetCSSValue(element, property.into())
+            },
+            Route::GetElementText => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::GetElementText(element)
+            },
+            Route::GetElementTagName => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::GetElementTagName(element)
+            },
+            Route::GetElementRect => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::GetElementRect(element)
+            },
+            Route::IsEnabled => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::IsEnabled(element)
+            },
+            Route::ElementClick => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::ElementClick(element)
+            },
+            Route::ElementTap => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::ElementTap(element)
+            },
+            Route::ElementClear => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::ElementClear(element)
+            },
+            Route::ElementSendKeys => {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                let parameters: SendKeysParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::ElementSendKeys(element, parameters)
+            },
+            Route::ExecuteScript => {
+                let parameters: JavascriptCommandParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::ExecuteScript(parameters)
+            },
+            Route::ExecuteAsyncScript => {
+                let parameters: JavascriptCommandParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::ExecuteAsyncScript(parameters)
+            },
+            Route::GetCookies => {
+                WebDriverCommand::GetCookies
+            },
+            Route::GetNamedCookie => {
+                let name = try_opt!(params.name("name"),
+                                    ErrorStatus::InvalidArgument,
+                                    "Missing 'name' parameter").as_str().into();
+                WebDriverCommand::GetNamedCookie(name)
+            },
+            Route::AddCookie => {
+                let parameters: AddCookieParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::AddCookie(parameters)
+            },
+            Route::DeleteCookies => {
+                WebDriverCommand::DeleteCookies
+            },
+            Route::DeleteCookie => {
+                let name = try_opt!(params.name("name"),
+                                    ErrorStatus::InvalidArgument,
+                                    "Missing name parameter").as_str().into();
+                WebDriverCommand::DeleteCookie(name)
+            },
+            Route::PerformActions => {
+                let parameters: ActionsParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::PerformActions(parameters)
+            },
+            Route::ReleaseActions => {
+                WebDriverCommand::ReleaseActions
+            },
+            Route::DismissAlert => {
+                WebDriverCommand::DismissAlert
+            },
+            Route::AcceptAlert => {
+                WebDriverCommand::AcceptAlert
+            },
+            Route::GetAlertText => {
+                WebDriverCommand::GetAlertText
+            },
+            Route::SendAlertText => {
+                let parameters: SendKeysParameters = try!(Parameters::from_json(&body_data));
+                WebDriverCommand::SendAlertText(parameters)
+            },
+            Route::TakeScreenshot => WebDriverCommand::TakeScreenshot,
+            Route::TakeElementScreenshot =>  {
+                let element_id = try_opt!(params.name("elementId"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing elementId parameter");
+                let element = WebElement::new(element_id.as_str().into());
+                WebDriverCommand::TakeElementScreenshot(element)
+            },
+            Route::Status => WebDriverCommand::Status,
+            Route::Extension(ref extension) => {
+                try!(extension.command(params, &body_data))
+            }
+        };
+        Ok(WebDriverMessage::new(session_id, command))
+    }
+
+    fn get_session_id(params: &Captures) -> Option<String> {
+        params.name("sessionId").map(|x| x.as_str().into())
+    }
+
+    fn decode_body(body: &str, requires_body: bool) -> WebDriverResult<Json> {
+        if requires_body {
+            match Json::from_str(body) {
+                Ok(x @ Json::Object(_)) => Ok(x),
+                Ok(_) => {
+                    Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                            "Body was not a JSON Object"))
+                }
+                Err(json::ParserError::SyntaxError(_, line, col)) => {
+                    let msg = format!("Failed to decode request as JSON: {}", body);
+                    let stack = format!("Syntax error at :{}:{}", line, col);
+                    Err(WebDriverError::new_with_stack(ErrorStatus::InvalidArgument, msg, stack))
+                }
+                Err(json::ParserError::IoError(e)) => {
+                    Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                            format!("I/O error whilst decoding body: {}", e)))
+                }
+            }
+        } else {
+            Ok(Json::Null)
+        }
+    }
+}
+
+impl <U:WebDriverExtensionRoute> ToJson for WebDriverMessage<U> {
+    fn to_json(&self) -> Json {
+        let parameters = match self.command {
+            WebDriverCommand::AcceptAlert |
+            WebDriverCommand::CloseWindow |
+            WebDriverCommand::ReleaseActions |
+            WebDriverCommand::DeleteCookie(_) |
+            WebDriverCommand::DeleteCookies |
+            WebDriverCommand::DeleteSession |
+            WebDriverCommand::DismissAlert |
+            WebDriverCommand::ElementClear(_) |
+            WebDriverCommand::ElementClick(_) |
+            WebDriverCommand::ElementTap(_) |
+            WebDriverCommand::GetActiveElement |
+            WebDriverCommand::GetAlertText |
+            WebDriverCommand::GetNamedCookie(_) |
+            WebDriverCommand::GetCookies |
+            WebDriverCommand::GetCSSValue(_, _) |
+            WebDriverCommand::GetCurrentUrl |
+            WebDriverCommand::GetElementAttribute(_, _) |
+            WebDriverCommand::GetElementProperty(_, _) |
+            WebDriverCommand::GetElementRect(_) |
+            WebDriverCommand::GetElementTagName(_) |
+            WebDriverCommand::GetElementText(_) |
+            WebDriverCommand::GetPageSource |
+            WebDriverCommand::GetTimeouts |
+            WebDriverCommand::GetTitle |
+            WebDriverCommand::GetWindowHandle |
+            WebDriverCommand::GetWindowHandles |
+            WebDriverCommand::GetWindowRect |
+            WebDriverCommand::GoBack |
+            WebDriverCommand::GoForward |
+            WebDriverCommand::IsDisplayed(_) |
+            WebDriverCommand::IsEnabled(_) |
+            WebDriverCommand::IsSelected(_) |
+            WebDriverCommand::MinimizeWindow |
+            WebDriverCommand::MaximizeWindow |
+            WebDriverCommand::FullscreenWindow |
+            WebDriverCommand::NewSession(_) |
+            WebDriverCommand::Refresh |
+            WebDriverCommand::Status |
+            WebDriverCommand::SwitchToParentFrame |
+            WebDriverCommand::TakeElementScreenshot(_) |
+            WebDriverCommand::TakeScreenshot => {
+                None
+            },
+
+            WebDriverCommand::AddCookie(ref x) => Some(x.to_json()),
+            WebDriverCommand::ElementSendKeys(_, ref x) => Some(x.to_json()),
+            WebDriverCommand::ExecuteAsyncScript(ref x) |
+            WebDriverCommand::ExecuteScript(ref x) => Some(x.to_json()),
+            WebDriverCommand::FindElementElement(_, ref x) => Some(x.to_json()),
+            WebDriverCommand::FindElementElements(_, ref x) => Some(x.to_json()),
+            WebDriverCommand::FindElement(ref x) => Some(x.to_json()),
+            WebDriverCommand::FindElements(ref x) => Some(x.to_json()),
+            WebDriverCommand::Get(ref x) => Some(x.to_json()),
+            WebDriverCommand::PerformActions(ref x) => Some(x.to_json()),
+            WebDriverCommand::SendAlertText(ref x) => Some(x.to_json()),
+            WebDriverCommand::SetTimeouts(ref x) => Some(x.to_json()),
+            WebDriverCommand::SetWindowRect(ref x) => Some(x.to_json()),
+            WebDriverCommand::SwitchToFrame(ref x) => Some(x.to_json()),
+            WebDriverCommand::SwitchToWindow(ref x) => Some(x.to_json()),
+            WebDriverCommand::Extension(ref x) => x.parameters_json(),
+        };
+
+        let mut data = BTreeMap::new();
+        if let Some(parameters) = parameters {
+            data.insert("parameters".to_string(), parameters);
+        }
+        Json::Object(data)
+    }
+}
+
+pub trait Parameters: Sized {
+    fn from_json(body: &Json) -> WebDriverResult<Self>;
+}
+
+/// Wrapper around the two supported variants of new session paramters
+///
+/// The Spec variant is used for storing spec-compliant parameters whereas
+/// the legacy variant is used to store desiredCapabilities/requiredCapabilities
+/// parameters, and is intended to minimise breakage as we transition users to
+/// the spec design.
+#[derive(Debug, PartialEq)]
+pub enum NewSessionParameters {
+    Spec(SpecNewSessionParameters),
+    Legacy(LegacyNewSessionParameters),
+}
+
+impl Parameters for NewSessionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<NewSessionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+        if data.get("capabilities").is_some() {
+            Ok(NewSessionParameters::Spec(try!(SpecNewSessionParameters::from_json(body))))
+        } else {
+            Ok(NewSessionParameters::Legacy(try!(LegacyNewSessionParameters::from_json(body))))
+        }
+    }
+}
+
+impl ToJson for NewSessionParameters {
+    fn to_json(&self) -> Json {
+        match self {
+            &NewSessionParameters::Spec(ref x) => x.to_json(),
+            &NewSessionParameters::Legacy(ref x) => x.to_json()
+        }
+    }
+}
+
+impl CapabilitiesMatching for NewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        match self {
+            &NewSessionParameters::Spec(ref x) => x.match_browser(browser_capabilities),
+            &NewSessionParameters::Legacy(ref x) => x.match_browser(browser_capabilities)
+        }
+    }
+}
+
+
+#[derive(Debug, PartialEq)]
+pub struct GetParameters {
+    pub url: String
+}
+
+impl Parameters for GetParameters {
+    fn from_json(body: &Json) -> WebDriverResult<GetParameters> {
+        let data = try_opt!(body.as_object(), ErrorStatus::UnknownError,
+                            "Message body was not an object");
+        let url = try_opt!(
+            try_opt!(data.get("url"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'url' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "'url' not a string");
+        Ok(GetParameters {
+            url: url.to_string()
+        })
+    }
+}
+
+impl ToJson for GetParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("url".to_string(), self.url.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct TimeoutsParameters {
+    pub script: Option<u64>,
+    pub page_load: Option<u64>,
+    pub implicit: Option<u64>,
+}
+
+impl Parameters for TimeoutsParameters {
+    fn from_json(body: &Json) -> WebDriverResult<TimeoutsParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let script = match data.get("script") {
+            Some(json) => {
+                Some(try_opt!(json.as_u64(),
+                              ErrorStatus::InvalidArgument,
+                              "Script timeout duration was not a signed integer"))
+            }
+            None => None,
+        };
+
+        let page_load = match data.get("pageLoad") {
+            Some(json) => {
+                Some(try_opt!(json.as_u64(),
+                              ErrorStatus::InvalidArgument,
+                              "Page load timeout duration was not a signed integer"))
+            }
+            None => None,
+        };
+
+        let implicit = match data.get("implicit") {
+            Some(json) => {
+                Some(try_opt!(json.as_u64(),
+                              ErrorStatus::InvalidArgument,
+                              "Implicit timeout duration was not a signed integer"))
+            }
+            None => None,
+        };
+
+        Ok(TimeoutsParameters {
+            script: script,
+            page_load: page_load,
+            implicit: implicit,
+        })
+    }
+}
+
+impl ToJson for TimeoutsParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        if let Some(ms) = self.script {
+            data.insert("script".into(), ms.to_json());
+        }
+        if let Some(ms) = self.page_load {
+            data.insert("pageLoad".into(), ms.to_json());
+        }
+        if let Some(ms) = self.implicit {
+            data.insert("implicit".into(), ms.to_json());
+        }
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct WindowRectParameters {
+    pub x: Nullable<i64>,
+    pub y: Nullable<i64>,
+    pub width: Nullable<u64>,
+    pub height: Nullable<u64>,
+}
+
+impl Parameters for WindowRectParameters {
+    fn from_json(body: &Json) -> WebDriverResult<WindowRectParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let x = match data.get("x") {
+            Some(json) => {
+                try!(Nullable::from_json(json, |n| {
+                    Ok((try_opt!(n.as_i64(),
+                                 ErrorStatus::InvalidArgument,
+                                 "'x' is not an integer")))
+                }))
+            }
+            None => Nullable::Null,
+        };
+        let y = match data.get("y") {
+            Some(json) => {
+                try!(Nullable::from_json(json, |n| {
+                    Ok((try_opt!(n.as_i64(),
+                                 ErrorStatus::InvalidArgument,
+                                 "'y' is not an integer")))
+                }))
+            }
+            None => Nullable::Null,
+        };
+        let width = match data.get("width") {
+            Some(json) => {
+                try!(Nullable::from_json(json, |n| {
+                    Ok((try_opt!(n.as_u64(),
+                                 ErrorStatus::InvalidArgument,
+                                 "'width' is not a positive integer")))
+                }))
+            }
+            None => Nullable::Null,
+        };
+        let height = match data.get("height") {
+            Some(json) => {
+                try!(Nullable::from_json(json, |n| {
+                    Ok((try_opt!(n.as_u64(),
+                                 ErrorStatus::InvalidArgument,
+                                 "'height' is not a positive integer")))
+                }))
+            }
+            None => Nullable::Null,
+        };
+
+        Ok(WindowRectParameters {
+               x: x,
+               y: y,
+               width: width,
+               height: height,
+           })
+    }
+}
+
+impl ToJson for WindowRectParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("x".to_string(), self.x.to_json());
+        data.insert("y".to_string(), self.y.to_json());
+        data.insert("width".to_string(), self.width.to_json());
+        data.insert("height".to_string(), self.height.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct SwitchToWindowParameters {
+    pub handle: String
+}
+
+impl Parameters for SwitchToWindowParameters {
+    fn from_json(body: &Json) -> WebDriverResult<SwitchToWindowParameters> {
+        let data = try_opt!(body.as_object(), ErrorStatus::UnknownError,
+                            "Message body was not an object");
+        let handle = try_opt!(
+            try_opt!(data.get("handle"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'handle' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "'handle' not a string");
+        return Ok(SwitchToWindowParameters {
+            handle: handle.to_string()
+        })
+    }
+}
+
+impl ToJson for SwitchToWindowParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("handle".to_string(), self.handle.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct LocatorParameters {
+    pub using: LocatorStrategy,
+    pub value: String
+}
+
+impl Parameters for LocatorParameters {
+    fn from_json(body: &Json) -> WebDriverResult<LocatorParameters> {
+        let data = try_opt!(body.as_object(), ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let using = try!(LocatorStrategy::from_json(
+            try_opt!(data.get("using"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'using' parameter")));
+
+        let value = try_opt!(
+            try_opt!(data.get("value"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'value' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "Could not convert using to string").to_string();
+
+        return Ok(LocatorParameters {
+            using: using,
+            value: value
+        })
+    }
+}
+
+impl ToJson for LocatorParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("using".to_string(), self.using.to_json());
+        data.insert("value".to_string(), self.value.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct SwitchToFrameParameters {
+    pub id: FrameId
+}
+
+impl Parameters for SwitchToFrameParameters {
+    fn from_json(body: &Json) -> WebDriverResult<SwitchToFrameParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+        let id = try!(FrameId::from_json(try_opt!(data.get("id"),
+                                                  ErrorStatus::UnknownError,
+                                                  "Missing 'id' parameter")));
+
+        Ok(SwitchToFrameParameters {
+            id: id
+        })
+    }
+}
+
+impl ToJson for SwitchToFrameParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("id".to_string(), self.id.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct SendKeysParameters {
+    pub text: String
+}
+
+impl Parameters for SendKeysParameters {
+    fn from_json(body: &Json) -> WebDriverResult<SendKeysParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Message body was not an object");
+        let text = try_opt!(try_opt!(data.get("text"),
+                                     ErrorStatus::InvalidArgument,
+                                     "Missing 'text' parameter").as_string(),
+                            ErrorStatus::InvalidArgument,
+                            "Could not convert 'text' to string");
+
+        Ok(SendKeysParameters {
+            text: text.into()
+        })
+    }
+}
+
+impl ToJson for SendKeysParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("value".to_string(), self.text.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct JavascriptCommandParameters {
+    pub script: String,
+    pub args: Nullable<Vec<Json>>
+}
+
+impl Parameters for JavascriptCommandParameters {
+    fn from_json(body: &Json) -> WebDriverResult<JavascriptCommandParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Message body was not an object");
+
+        let args_json = try_opt!(data.get("args"),
+                                 ErrorStatus::InvalidArgument,
+                                 "Missing args parameter");
+
+        let args = try!(Nullable::from_json(
+            args_json,
+            |x| {
+                Ok((try_opt!(x.as_array(),
+                             ErrorStatus::InvalidArgument,
+                             "Failed to convert args to Array")).clone())
+            }));
+
+         //TODO: Look for WebElements in args?
+        let script = try_opt!(
+            try_opt!(data.get("script"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing script parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "Failed to convert script to String");
+        Ok(JavascriptCommandParameters {
+            script: script.to_string(),
+            args: args.clone()
+        })
+    }
+}
+
+impl ToJson for JavascriptCommandParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        //TODO: Wrap script so that it becomes marionette-compatible
+        data.insert("script".to_string(), self.script.to_json());
+        data.insert("args".to_string(), self.args.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct GetNamedCookieParameters {
+    pub name: Nullable<String>,
+}
+
+impl Parameters for GetNamedCookieParameters {
+    fn from_json(body: &Json) -> WebDriverResult<GetNamedCookieParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Message body was not an object");
+        let name_json = try_opt!(data.get("name"),
+                                 ErrorStatus::InvalidArgument,
+                                 "Missing 'name' parameter");
+        let name = try!(Nullable::from_json(name_json, |x| {
+            Ok(try_opt!(x.as_string(),
+                        ErrorStatus::InvalidArgument,
+                        "Failed to convert name to string")
+                .to_string())
+        }));
+        return Ok(GetNamedCookieParameters { name: name });
+    }
+}
+
+impl ToJson for GetNamedCookieParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("name".to_string(), self.name.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct AddCookieParameters {
+    pub name: String,
+    pub value: String,
+    pub path: Nullable<String>,
+    pub domain: Nullable<String>,
+    pub expiry: Nullable<Date>,
+    pub secure: bool,
+    pub httpOnly: bool
+}
+
+impl Parameters for AddCookieParameters {
+    fn from_json(body: &Json) -> WebDriverResult<AddCookieParameters> {
+        if !body.is_object() {
+            return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                           "Message body was not an object"));
+        }
+
+        let data = try_opt!(body.find("cookie").and_then(|x| x.as_object()),
+                            ErrorStatus::UnableToSetCookie,
+                            "Cookie parameter not found or not an object");
+
+        let name = try_opt!(
+            try_opt!(data.get("name"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'name' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "'name' is not a string").to_string();
+
+        let value = try_opt!(
+            try_opt!(data.get("value"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'value' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "'value' is not a string").to_string();
+
+        let path = match data.get("path") {
+            Some(path_json) => {
+                try!(Nullable::from_json(
+                    path_json,
+                    |x| {
+                        Ok(try_opt!(x.as_string(),
+                                    ErrorStatus::InvalidArgument,
+                                    "Failed to convert path to String").to_string())
+                    }))
+            },
+            None => Nullable::Null
+        };
+
+        let domain = match data.get("domain") {
+            Some(domain_json) => {
+                try!(Nullable::from_json(
+                    domain_json,
+                    |x| {
+                        Ok(try_opt!(x.as_string(),
+                                    ErrorStatus::InvalidArgument,
+                                    "Failed to convert domain to String").to_string())
+                    }))
+            },
+            None => Nullable::Null
+        };
+
+        let expiry = match data.get("expiry") {
+            Some(expiry_json) => {
+                try!(Nullable::from_json(
+                    expiry_json,
+                    |x| {
+                        Ok(Date::new(try_opt!(x.as_u64(),
+                                              ErrorStatus::InvalidArgument,
+                                              "Failed to convert expiry to Date")))
+                    }))
+            },
+            None => Nullable::Null
+        };
+
+        let secure = match data.get("secure") {
+            Some(x) => try_opt!(x.as_boolean(),
+                                ErrorStatus::InvalidArgument,
+                                "Failed to convert secure to boolean"),
+            None => false
+        };
+
+        let http_only = match data.get("httpOnly") {
+            Some(x) => try_opt!(x.as_boolean(),
+                                ErrorStatus::InvalidArgument,
+                                "Failed to convert httpOnly to boolean"),
+            None => false
+        };
+
+        return Ok(AddCookieParameters {
+            name: name,
+            value: value,
+            path: path,
+            domain: domain,
+            expiry: expiry,
+            secure: secure,
+            httpOnly: http_only
+        })
+    }
+}
+
+impl ToJson for AddCookieParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("name".to_string(), self.name.to_json());
+        data.insert("value".to_string(), self.value.to_json());
+        data.insert("path".to_string(), self.path.to_json());
+        data.insert("domain".to_string(), self.domain.to_json());
+        data.insert("expiry".to_string(), self.expiry.to_json());
+        data.insert("secure".to_string(), self.secure.to_json());
+        data.insert("httpOnly".to_string(), self.httpOnly.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct TakeScreenshotParameters {
+    pub element: Nullable<WebElement>
+}
+
+impl Parameters for TakeScreenshotParameters {
+    fn from_json(body: &Json) -> WebDriverResult<TakeScreenshotParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Message body was not an object");
+        let element = match data.get("element") {
+            Some(element_json) => try!(Nullable::from_json(
+                element_json,
+                |x| {
+                    Ok(try!(WebElement::from_json(x)))
+                })),
+            None => Nullable::Null
+        };
+
+        return Ok(TakeScreenshotParameters {
+            element: element
+        })
+    }
+}
+
+impl ToJson for TakeScreenshotParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("element".to_string(), self.element.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ActionsParameters {
+    pub actions: Vec<ActionSequence>
+}
+
+impl Parameters for ActionsParameters {
+    fn from_json(body: &Json) -> WebDriverResult<ActionsParameters> {
+        try_opt!(body.as_object(),
+                 ErrorStatus::InvalidArgument,
+                 "Message body was not an object");
+        let actions = try_opt!(
+            try_opt!(body.find("actions"),
+                     ErrorStatus::InvalidArgument,
+                     "No actions parameter found").as_array(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'actions' was not an array");
+
+        let mut result = Vec::with_capacity(actions.len());
+        for chain in actions.iter() {
+            result.push(try!(ActionSequence::from_json(chain)));
+        }
+        Ok(ActionsParameters {
+            actions: result
+        })
+    }
+}
+
+impl ToJson for ActionsParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("actions".to_owned(),
+                    self.actions.iter().map(|x| x.to_json()).collect::<Vec<Json>>().to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ActionSequence {
+    pub id: Nullable<String>,
+    pub actions: ActionsType
+}
+
+impl Parameters for ActionSequence {
+    fn from_json(body: &Json) -> WebDriverResult<ActionSequence> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Actions chain was not an object");
+
+        let type_name = try_opt!(try_opt!(data.get("type"),
+                                          ErrorStatus::InvalidArgument,
+                                          "Missing type parameter").as_string(),
+                                 ErrorStatus::InvalidArgument,
+                                 "Parameter ;type' was not a string");
+
+        let id = match data.get("id") {
+            Some(x) => Some(try_opt!(x.as_string(),
+                                     ErrorStatus::InvalidArgument,
+                                     "Parameter 'id' was not a string").to_owned()),
+            None => None
+        };
+
+
+        // Note that unlike the spec we get the pointer parameters in ActionsType::from_json
+
+        let actions = match type_name {
+            "none" | "key" | "pointer" => try!(ActionsType::from_json(&body)),
+            _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "Invalid action type"))
+        };
+
+        Ok(ActionSequence {
+            id: id.into(),
+            actions: actions
+        })
+    }
+}
+
+impl ToJson for ActionSequence {
+    fn to_json(&self) -> Json {
+        let mut data: BTreeMap<String, Json> = BTreeMap::new();
+        data.insert("id".into(), self.id.to_json());
+        let (action_type, actions) = match self.actions {
+            ActionsType::Null(ref actions) => {
+                ("none",
+                 actions.iter().map(|x| x.to_json()).collect::<Vec<Json>>())
+            }
+            ActionsType::Key(ref actions) => {
+                ("key",
+                 actions.iter().map(|x| x.to_json()).collect::<Vec<Json>>())
+            }
+            ActionsType::Pointer(ref parameters, ref actions) => {
+                data.insert("parameters".into(), parameters.to_json());
+                ("pointer",
+                 actions.iter().map(|x| x.to_json()).collect::<Vec<Json>>())
+            }
+        };
+        data.insert("type".into(), action_type.to_json());
+        data.insert("actions".into(), actions.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum ActionsType {
+    Null(Vec<NullActionItem>),
+    Key(Vec<KeyActionItem>),
+    Pointer(PointerActionParameters, Vec<PointerActionItem>)
+}
+
+impl Parameters for ActionsType {
+    fn from_json(body: &Json) -> WebDriverResult<ActionsType> {
+        // These unwraps are OK as long as this is only called from ActionSequence::from_json
+        let data = body.as_object().expect("Body should be a JSON Object");
+        let actions_type = body.find("type").and_then(|x| x.as_string()).expect("Type should be a string");
+        let actions_chain = try_opt!(try_opt!(data.get("actions"),
+                                              ErrorStatus::InvalidArgument,
+                                              "Missing actions parameter").as_array(),
+                                     ErrorStatus::InvalidArgument,
+                                     "Parameter 'actions' was not an array");
+        match actions_type {
+            "none" => {
+                let mut actions = Vec::with_capacity(actions_chain.len());
+                for action_body in actions_chain.iter() {
+                    actions.push(try!(NullActionItem::from_json(action_body)));
+                };
+                Ok(ActionsType::Null(actions))
+            },
+            "key" => {
+                let mut actions = Vec::with_capacity(actions_chain.len());
+                for action_body in actions_chain.iter() {
+                    actions.push(try!(KeyActionItem::from_json(action_body)));
+                };
+                Ok(ActionsType::Key(actions))
+            },
+            "pointer" => {
+                let mut actions = Vec::with_capacity(actions_chain.len());
+                let parameters = match data.get("parameters") {
+                    Some(x) => try!(PointerActionParameters::from_json(x)),
+                    None => Default::default()
+                };
+
+                for action_body in actions_chain.iter() {
+                    actions.push(try!(PointerActionItem::from_json(action_body)));
+                }
+                Ok(ActionsType::Pointer(parameters, actions))
+            }
+            _ => panic!("Got unexpected action type after checking type")
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum PointerType {
+    Mouse,
+    Pen,
+    Touch,
+}
+
+impl Parameters for PointerType {
+    fn from_json(body: &Json) -> WebDriverResult<PointerType> {
+        match body.as_string() {
+            Some("mouse") => Ok(PointerType::Mouse),
+            Some("pen") => Ok(PointerType::Pen),
+            Some("touch") => Ok(PointerType::Touch),
+            Some(_) => Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                "Unsupported pointer type"
+            )),
+            None => Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                "Pointer type was not a string"
+            ))
+        }
+    }
+}
+
+impl ToJson for PointerType {
+    fn to_json(&self) -> Json {
+        match *self {
+            PointerType::Mouse => "mouse".to_json(),
+            PointerType::Pen => "pen".to_json(),
+            PointerType::Touch => "touch".to_json(),
+        }.to_json()
+    }
+}
+
+impl Default for PointerType {
+    fn default() -> PointerType {
+        PointerType::Mouse
+    }
+}
+
+#[derive(Debug, Default, PartialEq)]
+pub struct PointerActionParameters {
+    pub pointer_type: PointerType
+}
+
+impl Parameters for PointerActionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<PointerActionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Parameter 'parameters' was not an object");
+        let pointer_type = match data.get("pointerType") {
+            Some(x) => try!(PointerType::from_json(x)),
+            None => PointerType::default()
+        };
+        Ok(PointerActionParameters {
+            pointer_type: pointer_type
+        })
+    }
+}
+
+impl ToJson for PointerActionParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("pointerType".to_owned(),
+                    self.pointer_type.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum NullActionItem {
+    General(GeneralAction)
+}
+
+impl Parameters for NullActionItem {
+    fn from_json(body: &Json) -> WebDriverResult<NullActionItem> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Actions chain was not an object");
+        let type_name = try_opt!(
+            try_opt!(data.get("type"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'type' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'type' was not a string");
+        match type_name {
+            "pause" => Ok(NullActionItem::General(
+                try!(GeneralAction::from_json(body)))),
+            _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "Invalid type attribute"))
+        }
+    }
+}
+
+impl ToJson for NullActionItem {
+    fn to_json(&self) -> Json {
+        match self {
+            &NullActionItem::General(ref x) => x.to_json(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum KeyActionItem {
+    General(GeneralAction),
+    Key(KeyAction)
+}
+
+impl Parameters for KeyActionItem {
+    fn from_json(body: &Json) -> WebDriverResult<KeyActionItem> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Key action item was not an object");
+        let type_name = try_opt!(
+            try_opt!(data.get("type"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'type' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'type' was not a string");
+        match type_name {
+            "pause" => Ok(KeyActionItem::General(
+                try!(GeneralAction::from_json(body)))),
+            _ => Ok(KeyActionItem::Key(
+                try!(KeyAction::from_json(body))))
+        }
+    }
+}
+
+impl ToJson for KeyActionItem {
+    fn to_json(&self) -> Json {
+        match *self {
+            KeyActionItem::General(ref x) => x.to_json(),
+            KeyActionItem::Key(ref x) => x.to_json()
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum PointerActionItem {
+    General(GeneralAction),
+    Pointer(PointerAction)
+}
+
+impl Parameters for PointerActionItem {
+    fn from_json(body: &Json) -> WebDriverResult<PointerActionItem> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::InvalidArgument,
+                            "Pointer action item was not an object");
+        let type_name = try_opt!(
+            try_opt!(data.get("type"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'type' parameter").as_string(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'type' was not a string");
+
+        match type_name {
+            "pause" => Ok(PointerActionItem::General(try!(GeneralAction::from_json(body)))),
+            _ => Ok(PointerActionItem::Pointer(try!(PointerAction::from_json(body))))
+        }
+    }
+}
+
+impl ToJson for PointerActionItem {
+    fn to_json(&self) -> Json {
+        match self {
+            &PointerActionItem::General(ref x) => x.to_json(),
+            &PointerActionItem::Pointer(ref x) => x.to_json()
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum GeneralAction {
+    Pause(PauseAction)
+}
+
+impl Parameters for GeneralAction {
+    fn from_json(body: &Json) -> WebDriverResult<GeneralAction> {
+        match body.find("type").and_then(|x| x.as_string()) {
+            Some("pause") => Ok(GeneralAction::Pause(try!(PauseAction::from_json(body)))),
+            _ => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                         "Invalid or missing type attribute"))
+        }
+    }
+}
+
+impl ToJson for GeneralAction {
+    fn to_json(&self) -> Json {
+        match self {
+            &GeneralAction::Pause(ref x) => x.to_json()
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct PauseAction {
+    pub duration: u64
+}
+
+impl Parameters for PauseAction {
+    fn from_json(body: &Json) -> WebDriverResult<PauseAction> {
+        let default = Json::U64(0);
+        Ok(PauseAction {
+            duration: try_opt!(body.find("duration").unwrap_or(&default).as_u64(),
+                               ErrorStatus::InvalidArgument,
+                               "Parameter 'duration' was not a positive integer")
+        })
+    }
+}
+
+impl ToJson for PauseAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(),
+                    "pause".to_json());
+        data.insert("duration".to_owned(),
+                    self.duration.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum KeyAction {
+    Up(KeyUpAction),
+    Down(KeyDownAction)
+}
+
+impl Parameters for KeyAction {
+    fn from_json(body: &Json) -> WebDriverResult<KeyAction> {
+        match body.find("type").and_then(|x| x.as_string()) {
+            Some("keyDown") => Ok(KeyAction::Down(try!(KeyDownAction::from_json(body)))),
+            Some("keyUp") => Ok(KeyAction::Up(try!(KeyUpAction::from_json(body)))),
+            Some(_) | None => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                      "Invalid type attribute value for key action"))
+        }
+    }
+}
+
+impl ToJson for KeyAction {
+    fn to_json(&self) -> Json {
+        match self {
+            &KeyAction::Down(ref x) => x.to_json(),
+            &KeyAction::Up(ref x) => x.to_json(),
+        }
+    }
+}
+
+fn validate_key_value(value_str: &str) -> WebDriverResult<char> {
+    let mut chars = value_str.chars();
+    let value = if let Some(c) = chars.next() {
+        c
+    } else {
+        return Err(WebDriverError::new(
+            ErrorStatus::InvalidArgument,
+            "Parameter 'value' was an empty string"))
+    };
+    if chars.next().is_some() {
+        return Err(WebDriverError::new(
+            ErrorStatus::InvalidArgument,
+            "Parameter 'value' contained multiple characters"))
+    };
+    Ok(value)
+}
+
+#[derive(Debug, PartialEq)]
+pub struct KeyUpAction {
+    pub value: char
+}
+
+impl Parameters for KeyUpAction {
+    fn from_json(body: &Json) -> WebDriverResult<KeyUpAction> {
+        let value_str = try_opt!(
+                try_opt!(body.find("value"),
+                         ErrorStatus::InvalidArgument,
+                         "Missing value parameter").as_string(),
+                ErrorStatus::InvalidArgument,
+            "Parameter 'value' was not a string");
+
+        let value = try!(validate_key_value(value_str));
+        Ok(KeyUpAction {
+            value: value
+        })
+    }
+}
+
+impl ToJson for KeyUpAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(),
+                    "keyUp".to_json());
+        data.insert("value".to_string(),
+                    self.value.to_string().to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct KeyDownAction {
+    pub value: char
+}
+
+impl Parameters for KeyDownAction {
+    fn from_json(body: &Json) -> WebDriverResult<KeyDownAction> {
+        let value_str = try_opt!(
+                try_opt!(body.find("value"),
+                         ErrorStatus::InvalidArgument,
+                         "Missing value parameter").as_string(),
+                ErrorStatus::InvalidArgument,
+            "Parameter 'value' was not a string");
+        let value = try!(validate_key_value(value_str));
+        Ok(KeyDownAction {
+            value: value
+        })
+    }
+}
+
+impl ToJson for KeyDownAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(),
+                    "keyDown".to_json());
+        data.insert("value".to_owned(),
+                    self.value.to_string().to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum PointerOrigin {
+    Viewport,
+    Pointer,
+    Element(WebElement),
+}
+
+impl Parameters for PointerOrigin {
+    fn from_json(body: &Json) -> WebDriverResult<PointerOrigin> {
+        match *body {
+            Json::String(ref x) => {
+                match &**x {
+                    "viewport" => Ok(PointerOrigin::Viewport),
+                    "pointer" => Ok(PointerOrigin::Pointer),
+                    _ => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                 "Unknown pointer origin"))
+                }
+            },
+            Json::Object(_) => Ok(PointerOrigin::Element(try!(WebElement::from_json(body)))),
+            _ => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                        "Pointer origin was not a string or an object"))
+        }
+    }
+}
+
+impl ToJson for PointerOrigin {
+    fn to_json(&self) -> Json {
+        match *self {
+            PointerOrigin::Viewport => "viewport".to_json(),
+            PointerOrigin::Pointer => "pointer".to_json(),
+            PointerOrigin::Element(ref x) => x.to_json(),
+        }
+    }
+}
+
+impl Default for PointerOrigin {
+    fn default() -> PointerOrigin {
+        PointerOrigin::Viewport
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum PointerAction {
+    Up(PointerUpAction),
+    Down(PointerDownAction),
+    Move(PointerMoveAction),
+    Cancel
+}
+
+impl Parameters for PointerAction {
+    fn from_json(body: &Json) -> WebDriverResult<PointerAction> {
+        match body.find("type").and_then(|x| x.as_string()) {
+            Some("pointerUp") => Ok(PointerAction::Up(try!(PointerUpAction::from_json(body)))),
+            Some("pointerDown") => Ok(PointerAction::Down(try!(PointerDownAction::from_json(body)))),
+            Some("pointerMove") => Ok(PointerAction::Move(try!(PointerMoveAction::from_json(body)))),
+            Some("pointerCancel") => Ok(PointerAction::Cancel),
+            Some(_) | None => Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                "Missing or invalid type argument for pointer action"))
+        }
+    }
+}
+
+impl ToJson for PointerAction {
+    fn to_json(&self) -> Json {
+        match self {
+            &PointerAction::Down(ref x) => x.to_json(),
+            &PointerAction::Up(ref x) => x.to_json(),
+            &PointerAction::Move(ref x) => x.to_json(),
+            &PointerAction::Cancel => {
+                let mut data = BTreeMap::new();
+                data.insert("type".to_owned(),
+                            "pointerCancel".to_json());
+                Json::Object(data)
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct PointerUpAction {
+    pub button: u64,
+}
+
+impl Parameters for PointerUpAction {
+    fn from_json(body: &Json) -> WebDriverResult<PointerUpAction> {
+        let button = try_opt!(
+            try_opt!(body.find("button"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing button parameter").as_u64(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'button' was not a positive integer");
+
+        Ok(PointerUpAction {
+            button: button
+        })
+    }
+}
+
+impl ToJson for PointerUpAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(),
+                    "pointerUp".to_json());
+        data.insert("button".to_owned(), self.button.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct PointerDownAction {
+    pub button: u64,
+}
+
+impl Parameters for PointerDownAction {
+    fn from_json(body: &Json) -> WebDriverResult<PointerDownAction> {
+        let button = try_opt!(
+            try_opt!(body.find("button"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing button parameter").as_u64(),
+            ErrorStatus::InvalidArgument,
+            "Parameter 'button' was not a positive integer");
+
+        Ok(PointerDownAction {
+            button: button
+        })
+    }
+}
+
+impl ToJson for PointerDownAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(),
+                    "pointerDown".to_json());
+        data.insert("button".to_owned(), self.button.to_json());
+        Json::Object(data)
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct PointerMoveAction {
+    pub duration: Nullable<u64>,
+    pub origin: PointerOrigin,
+    pub x: Nullable<i64>,
+    pub y: Nullable<i64>
+}
+
+impl Parameters for PointerMoveAction {
+    fn from_json(body: &Json) -> WebDriverResult<PointerMoveAction> {
+        let duration = match body.find("duration") {
+            Some(duration) => Some(try_opt!(duration.as_u64(),
+                                            ErrorStatus::InvalidArgument,
+                                            "Parameter 'duration' was not a positive integer")),
+            None => None
+
+        };
+
+        let origin = match body.find("origin") {
+            Some(o) => try!(PointerOrigin::from_json(o)),
+            None => PointerOrigin::default()
+        };
+
+        let x = match body.find("x") {
+            Some(x) => {
+                Some(try_opt!(x.as_i64(),
+                              ErrorStatus::InvalidArgument,
+                              "Parameter 'x' was not an integer"))
+            },
+            None => None
+        };
+
+        let y = match body.find("y") {
+            Some(y) => {
+                Some(try_opt!(y.as_i64(),
+                              ErrorStatus::InvalidArgument,
+                              "Parameter 'y' was not an integer"))
+            },
+            None => None
+        };
+
+        Ok(PointerMoveAction {
+            duration: duration.into(),
+            origin: origin.into(),
+            x: x.into(),
+            y: y.into(),
+        })
+    }
+}
+
+impl ToJson for PointerMoveAction {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("type".to_owned(), "pointerMove".to_json());
+        if self.duration.is_value() {
+            data.insert("duration".to_owned(),
+                        self.duration.to_json());
+        }
+
+        data.insert("origin".to_owned(), self.origin.to_json());
+
+        if self.x.is_value() {
+            data.insert("x".to_owned(), self.x.to_json());
+        }
+        if self.y.is_value() {
+            data.insert("y".to_owned(), self.y.to_json());
+        }
+        Json::Object(data)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rustc_serialize::json::Json;
+    use super::{Nullable, Parameters, WindowRectParameters};
+
+    #[test]
+    fn test_window_rect() {
+        let expected = WindowRectParameters {
+            x: Nullable::Value(0i64),
+            y: Nullable::Value(1i64),
+            width: Nullable::Value(2u64),
+            height: Nullable::Value(3u64),
+        };
+        let actual = Json::from_str(r#"{"x": 0, "y": 1, "width": 2, "height": 3}"#).unwrap();
+        assert_eq!(expected, Parameters::from_json(&actual).unwrap());
+    }
+
+    #[test]
+    fn test_window_rect_nullable() {
+        let expected = WindowRectParameters {
+            x: Nullable::Value(0i64),
+            y: Nullable::Null,
+            width: Nullable::Value(2u64),
+            height: Nullable::Null,
+        };
+        let actual = Json::from_str(r#"{"x": 0, "y": null, "width": 2, "height": null}"#).unwrap();
+        assert_eq!(expected, Parameters::from_json(&actual).unwrap());
+    }
+
+    #[test]
+    fn test_window_rect_missing_fields() {
+        let expected = WindowRectParameters {
+            x: Nullable::Value(0i64),
+            y: Nullable::Null,
+            width: Nullable::Value(2u64),
+            height: Nullable::Null,
+        };
+        let actual = Json::from_str(r#"{"x": 0, "width": 2}"#).unwrap();
+        assert_eq!(expected, Parameters::from_json(&actual).unwrap());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/common.rs
@@ -0,0 +1,274 @@
+use rustc_serialize::{Encodable, Encoder};
+use rustc_serialize::json::{Json, ToJson};
+use std::collections::BTreeMap;
+
+use error::{WebDriverResult, WebDriverError, ErrorStatus};
+
+pub static ELEMENT_KEY: &'static str = "element-6066-11e4-a52e-4f735466cecf";
+
+#[derive(Clone, Debug, PartialEq, RustcEncodable)]
+pub struct Date(pub u64);
+
+impl Date {
+    pub fn new(timestamp: u64) -> Date {
+        Date(timestamp)
+    }
+}
+
+impl ToJson for Date {
+    fn to_json(&self) -> Json {
+        let &Date(x) = self;
+        x.to_json()
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Nullable<T: ToJson> {
+    Value(T),
+    Null
+}
+
+impl<T: ToJson> Nullable<T> {
+     pub fn is_null(&self) -> bool {
+        match *self {
+            Nullable::Value(_) => false,
+            Nullable::Null => true
+        }
+    }
+
+     pub fn is_value(&self) -> bool {
+        match *self {
+            Nullable::Value(_) => true,
+            Nullable::Null => false
+        }
+    }
+
+    pub fn map<F, U: ToJson>(self, f: F) -> Nullable<U>
+        where F: FnOnce(T) -> U {
+        match self {
+            Nullable::Value(val) => Nullable::Value(f(val)),
+            Nullable::Null => Nullable::Null
+        }
+    }
+}
+
+impl<T: ToJson> Nullable<T> {
+    //This is not very pretty
+    pub fn from_json<F: FnOnce(&Json) -> WebDriverResult<T>>(value: &Json, f: F) -> WebDriverResult<Nullable<T>> {
+        if value.is_null() {
+            Ok(Nullable::Null)
+        } else {
+            Ok(Nullable::Value(try!(f(value))))
+        }
+    }
+}
+
+impl<T: ToJson> ToJson for Nullable<T> {
+    fn to_json(&self) -> Json {
+        match *self {
+            Nullable::Value(ref x) => x.to_json(),
+            Nullable::Null => Json::Null
+        }
+    }
+}
+
+impl<T: ToJson> Encodable for Nullable<T> {
+    fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
+        match *self {
+            Nullable::Value(ref x) => x.to_json().encode(s),
+            Nullable::Null => s.emit_option_none()
+        }
+    }
+}
+
+impl<T: ToJson> Into<Option<T>> for Nullable<T> {
+    fn into(self) -> Option<T> {
+        match self {
+            Nullable::Value(val) => Some(val),
+            Nullable::Null => None
+        }
+    }
+}
+
+impl<T: ToJson> From<Option<T>> for Nullable<T> {
+    fn from(option: Option<T>) -> Nullable<T> {
+        match option {
+            Some(val) => Nullable::Value(val),
+            None => Nullable::Null,
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct WebElement {
+    pub id: String
+}
+
+impl WebElement {
+    pub fn new(id: String) -> WebElement {
+        WebElement {
+            id: id
+        }
+    }
+
+    pub fn from_json(data: &Json) -> WebDriverResult<WebElement> {
+        let object = try_opt!(data.as_object(),
+                              ErrorStatus::InvalidArgument,
+                              "Could not convert webelement to object");
+        let id_value = try_opt!(object.get(ELEMENT_KEY),
+                                ErrorStatus::InvalidArgument,
+                                "Could not find webelement key");
+
+        let id = try_opt!(id_value.as_string(),
+                          ErrorStatus::InvalidArgument,
+                          "Could not convert web element to string").to_string();
+
+        Ok(WebElement::new(id))
+    }
+}
+
+impl ToJson for WebElement {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert(ELEMENT_KEY.to_string(), self.id.to_json());
+        Json::Object(data)
+    }
+}
+
+impl <T> From<T> for WebElement
+    where T: Into<String> {
+    fn from(data: T) -> WebElement {
+        WebElement::new(data.into())
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum FrameId {
+    Short(u16),
+    Element(WebElement),
+    Null
+}
+
+impl FrameId {
+    pub fn from_json(data: &Json) -> WebDriverResult<FrameId> {
+        match data {
+            &Json::U64(x) => {
+                if x > u16::max_value() as u64 || x < u16::min_value() as u64 {
+                    return Err(WebDriverError::new(ErrorStatus::NoSuchFrame,
+                                                   "frame id out of range"))
+                };
+                Ok(FrameId::Short(x as u16))
+            },
+            &Json::Null => Ok(FrameId::Null),
+            &Json::Object(_) => Ok(FrameId::Element(
+                try!(WebElement::from_json(data)))),
+            _ => Err(WebDriverError::new(ErrorStatus::NoSuchFrame,
+                                         "frame id has unexpected type"))
+        }
+    }
+}
+
+impl ToJson for FrameId {
+    fn to_json(&self) -> Json {
+        match *self {
+            FrameId::Short(x) => {
+                Json::U64(x as u64)
+            },
+            FrameId::Element(ref x) => {
+                Json::String(x.id.clone())
+            },
+            FrameId::Null => {
+                Json::Null
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum LocatorStrategy {
+    CSSSelector,
+    LinkText,
+    PartialLinkText,
+    TagName,
+    XPath,
+}
+
+impl LocatorStrategy {
+    pub fn from_json(body: &Json) -> WebDriverResult<LocatorStrategy> {
+        match try_opt!(body.as_string(),
+                       ErrorStatus::InvalidArgument,
+                       "Expected locator strategy as string") {
+            "css selector" => Ok(LocatorStrategy::CSSSelector),
+            "link text" => Ok(LocatorStrategy::LinkText),
+            "partial link text" => Ok(LocatorStrategy::PartialLinkText),
+            "tag name" => Ok(LocatorStrategy::TagName),
+            "xpath" => Ok(LocatorStrategy::XPath),
+            x => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                         format!("Unknown locator strategy {}", x)))
+        }
+    }
+}
+
+impl ToJson for LocatorStrategy {
+    fn to_json(&self) -> Json {
+        Json::String(match *self {
+            LocatorStrategy::CSSSelector => "css selector",
+            LocatorStrategy::LinkText => "link text",
+            LocatorStrategy::PartialLinkText => "partial link text",
+            LocatorStrategy::TagName => "tag name",
+            LocatorStrategy::XPath => "xpath"
+        }.to_string())
+    }
+}
+
+/// The top-level browsing context has an associated window state which
+/// describes what visibility state its OS widget window is in.
+///
+/// The default state is [`Normal`].
+///
+/// [`normal`]: #variant.Normal
+#[derive(Debug)]
+pub enum WindowState {
+    /// The window is maximized.
+    Maximized,
+    /// The window is iconified.
+    Minimized,
+    /// The window is shown normally.
+    Normal,
+    /// The window is in full screen mode.
+    Fullscreen,
+}
+
+impl WindowState {
+    pub fn from_json(body: &Json) -> WebDriverResult<WindowState> {
+        use self::WindowState::*;
+        let s = try_opt!(
+            body.as_string(),
+            ErrorStatus::InvalidArgument,
+            "Expecetd window state as string"
+        );
+        match s {
+            "maximized" => Ok(Maximized),
+            "minimized" => Ok(Minimized),
+            "normal" => Ok(Normal),
+            "fullscreen" => Ok(Fullscreen),
+            x => Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                format!("Unknown window state {}", x),
+            )),
+        }
+    }
+}
+
+impl ToJson for WindowState {
+    fn to_json(&self) -> Json {
+        use self::WindowState::*;
+        let state = match *self {
+            Maximized => "maximized",
+            Minimized => "minimized",
+            Normal => "normal",
+            Fullscreen => "fullscreen",
+        };
+        Json::String(state.to_string())
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/error.rs
@@ -0,0 +1,318 @@
+use backtrace::Backtrace;
+use hyper::status::StatusCode;
+use rustc_serialize::base64::FromBase64Error;
+use rustc_serialize::json::{DecoderError, Json, ParserError, ToJson};
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+use std::convert::From;
+use std::error::Error;
+use std::fmt;
+use std::io;
+
+#[derive(Debug, PartialEq)]
+pub enum ErrorStatus {
+    /// The [`ElementClick`] command could not be completed because the
+    /// [element] receiving the events is obscuring the element that was
+    /// requested clicked.
+    ///
+    /// [`ElementClick`]:
+    /// ../command/enum.WebDriverCommand.html#variant.ElementClick
+    /// [element]: ../common/struct.WebElement.html
+    ElementClickIntercepted,
+
+    /// A [command] could not be completed because the element is not pointer-
+    /// or keyboard interactable.
+    ///
+    /// [command]: ../command/index.html
+    ElementNotInteractable,
+
+    /// An attempt was made to select an [element] that cannot be selected.
+    ///
+    /// [element]: ../common/struct.WebElement.html
+    ElementNotSelectable,
+
+    /// Navigation caused the user agent to hit a certificate warning, which is
+    /// usually the result of an expired or invalid TLS certificate.
+    InsecureCertificate,
+
+    /// The arguments passed to a [command] are either invalid or malformed.
+    ///
+    /// [command]: ../command/index.html
+    InvalidArgument,
+
+    /// An illegal attempt was made to set a cookie under a different domain
+    /// than the current page.
+    InvalidCookieDomain,
+
+    /// The coordinates provided to an interactions operation are invalid.
+    InvalidCoordinates,
+
+    /// A [command] could not be completed because the element is an invalid
+    /// state, e.g. attempting to click an element that is no longer attached
+    /// to the document.
+    ///
+    /// [command]: ../command/index.html
+    InvalidElementState,
+
+    /// Argument was an invalid selector.
+    InvalidSelector,
+
+    /// Occurs if the given session ID is not in the list of active sessions,
+    /// meaning the session either does not exist or that it’s not active.
+    InvalidSessionId,
+
+    /// An error occurred while executing JavaScript supplied by the user.
+    JavascriptError,
+
+    /// The target for mouse interaction is not in the browser’s viewport and
+    /// cannot be brought into that viewport.
+    MoveTargetOutOfBounds,
+
+    /// An attempt was made to operate on a modal dialogue when one was not
+    /// open.
+    NoSuchAlert,
+
+    /// No cookie matching the given path name was found amongst the associated
+    /// cookies of the current browsing context’s active document.
+    NoSuchCookie,
+
+    /// An [element] could not be located on the page using the given search
+    /// parameters.
+    ///
+    /// [element]: ../common/struct.WebElement.html
+    NoSuchElement,
+
+    /// A [command] to switch to a frame could not be satisfied because the
+    /// frame could not be found.
+    ///
+    /// [command]: ../command/index.html
+    NoSuchFrame,
+
+    /// A [command] to switch to a window could not be satisfied because the
+    /// window could not be found.
+    ///
+    /// [command]: ../command/index.html
+    NoSuchWindow,
+
+    /// A script did not complete before its timeout expired.
+    ScriptTimeout,
+
+    /// A new session could not be created.
+    SessionNotCreated,
+
+    /// A [command] failed because the referenced [element] is no longer
+    /// attached to the DOM.
+    ///
+    /// [command]: ../command/index.html
+    /// [element]: ../common/struct.WebElement.html
+    StaleElementReference,
+
+    /// An operation did not complete before its timeout expired.
+    Timeout,
+
+    /// A screen capture was made impossible.
+    UnableToCaptureScreen,
+
+    /// Setting the cookie’s value could not be done.
+    UnableToSetCookie,
+
+    /// A modal dialogue was open, blocking this operation.
+    UnexpectedAlertOpen,
+
+    /// The requested command could not be executed because it does not exist.
+    UnknownCommand,
+
+    /// An unknown error occurred in the remote end whilst processing the
+    /// [command].
+    ///
+    /// [command]: ../command/index.html
+    UnknownError,
+
+    /// The requested [command] matched a known endpoint, but did not match a
+    /// method for that endpoint.
+    ///
+    /// [command]: ../command/index.html
+    UnknownMethod,
+
+    UnknownPath,
+
+    /// Indicates that a [command] that should have executed properly is not
+    /// currently supported.
+    UnsupportedOperation,
+}
+
+
+impl ErrorStatus {
+    pub fn error_code(&self) -> &'static str {
+        match *self {
+            ErrorStatus::ElementClickIntercepted => "element click intercepted",
+            ErrorStatus::ElementNotInteractable => "element not interactable",
+            ErrorStatus::ElementNotSelectable => "element not selectable",
+            ErrorStatus::InsecureCertificate => "insecure certificate",
+            ErrorStatus::InvalidArgument => "invalid argument",
+            ErrorStatus::InvalidCookieDomain => "invalid cookie domain",
+            ErrorStatus::InvalidCoordinates => "invalid coordinates",
+            ErrorStatus::InvalidElementState => "invalid element state",
+            ErrorStatus::InvalidSelector => "invalid selector",
+            ErrorStatus::InvalidSessionId => "invalid session id",
+            ErrorStatus::JavascriptError => "javascript error",
+            ErrorStatus::MoveTargetOutOfBounds => "move target out of bounds",
+            ErrorStatus::NoSuchAlert => "no such alert",
+            ErrorStatus::NoSuchCookie => "no such cookie",
+            ErrorStatus::NoSuchElement => "no such element",
+            ErrorStatus::NoSuchFrame => "no such frame",
+            ErrorStatus::NoSuchWindow => "no such window",
+            ErrorStatus::ScriptTimeout => "script timeout",
+            ErrorStatus::SessionNotCreated => "session not created",
+            ErrorStatus::StaleElementReference => "stale element reference",
+            ErrorStatus::Timeout => "timeout",
+            ErrorStatus::UnableToCaptureScreen => "unable to capture screen",
+            ErrorStatus::UnableToSetCookie => "unable to set cookie",
+            ErrorStatus::UnexpectedAlertOpen => "unexpected alert open",
+            ErrorStatus::UnknownCommand |
+            ErrorStatus::UnknownError => "unknown error",
+            ErrorStatus::UnknownMethod => "unknown method",
+            ErrorStatus::UnknownPath => "unknown command",
+            ErrorStatus::UnsupportedOperation => "unsupported operation",
+        }
+    }
+
+    pub fn http_status(&self) -> StatusCode {
+        match *self {
+            ErrorStatus::ElementClickIntercepted => StatusCode::BadRequest,
+            ErrorStatus::ElementNotInteractable => StatusCode::BadRequest,
+            ErrorStatus::ElementNotSelectable => StatusCode::BadRequest,
+            ErrorStatus::InsecureCertificate => StatusCode::BadRequest,
+            ErrorStatus::InvalidArgument => StatusCode::BadRequest,
+            ErrorStatus::InvalidCookieDomain => StatusCode::BadRequest,
+            ErrorStatus::InvalidCoordinates => StatusCode::BadRequest,
+            ErrorStatus::InvalidElementState => StatusCode::BadRequest,
+            ErrorStatus::InvalidSelector => StatusCode::BadRequest,
+            ErrorStatus::InvalidSessionId => StatusCode::NotFound,
+            ErrorStatus::JavascriptError => StatusCode::InternalServerError,
+            ErrorStatus::MoveTargetOutOfBounds => StatusCode::InternalServerError,
+            ErrorStatus::NoSuchAlert => StatusCode::BadRequest,
+            ErrorStatus::NoSuchCookie => StatusCode::NotFound,
+            ErrorStatus::NoSuchElement => StatusCode::NotFound,
+            ErrorStatus::NoSuchFrame => StatusCode::BadRequest,
+            ErrorStatus::NoSuchWindow => StatusCode::BadRequest,
+            ErrorStatus::ScriptTimeout => StatusCode::RequestTimeout,
+            ErrorStatus::SessionNotCreated => StatusCode::InternalServerError,
+            ErrorStatus::StaleElementReference => StatusCode::BadRequest,
+            ErrorStatus::Timeout => StatusCode::RequestTimeout,
+            ErrorStatus::UnableToCaptureScreen => StatusCode::BadRequest,
+            ErrorStatus::UnableToSetCookie => StatusCode::InternalServerError,
+            ErrorStatus::UnexpectedAlertOpen => StatusCode::InternalServerError,
+            ErrorStatus::UnknownCommand => StatusCode::NotFound,
+            ErrorStatus::UnknownError => StatusCode::InternalServerError,
+            ErrorStatus::UnknownMethod => StatusCode::MethodNotAllowed,
+            ErrorStatus::UnknownPath => StatusCode::NotFound,
+            ErrorStatus::UnsupportedOperation => StatusCode::InternalServerError,
+        }
+    }
+}
+
+pub type WebDriverResult<T> = Result<T, WebDriverError>;
+
+#[derive(Debug)]
+pub struct WebDriverError {
+    pub error: ErrorStatus,
+    pub message: Cow<'static, str>,
+    pub stack: Cow<'static, str>,
+    pub delete_session: bool,
+}
+
+impl WebDriverError {
+    pub fn new<S>(error: ErrorStatus, message: S) -> WebDriverError
+        where S: Into<Cow<'static, str>>
+    {
+        WebDriverError {
+            error: error,
+            message: message.into(),
+            stack: format!("{:?}", Backtrace::new()).into(),
+            delete_session: false,
+        }
+    }
+
+    pub fn new_with_stack<S>(error: ErrorStatus, message: S, stack: S) -> WebDriverError
+        where S: Into<Cow<'static, str>>
+    {
+        WebDriverError {
+            error: error,
+            message: message.into(),
+            stack: stack.into(),
+            delete_session: false,
+        }
+    }
+
+    pub fn error_code(&self) -> &'static str {
+        self.error.error_code()
+    }
+
+    pub fn http_status(&self) -> StatusCode {
+        self.error.http_status()
+    }
+
+    pub fn to_json_string(&self) -> String {
+        self.to_json().to_string()
+    }
+}
+
+impl ToJson for WebDriverError {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("error".into(), self.error_code().to_json());
+        data.insert("message".into(), self.message.to_json());
+        data.insert("stacktrace".into(), self.stack.to_json());
+
+        let mut wrapper = BTreeMap::new();
+        wrapper.insert("value".into(), Json::Object(data));
+        Json::Object(wrapper)
+    }
+}
+
+impl Error for WebDriverError {
+    fn description(&self) -> &str {
+        self.error_code()
+    }
+
+    fn cause(&self) -> Option<&Error> {
+        None
+    }
+}
+
+impl fmt::Display for WebDriverError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.message.fmt(f)
+    }
+}
+
+impl From<ParserError> for WebDriverError {
+    fn from(err: ParserError) -> WebDriverError {
+        WebDriverError::new(ErrorStatus::UnknownError, err.description().to_string())
+    }
+}
+
+impl From<io::Error> for WebDriverError {
+    fn from(err: io::Error) -> WebDriverError {
+        WebDriverError::new(ErrorStatus::UnknownError, err.description().to_string())
+    }
+}
+
+impl From<DecoderError> for WebDriverError {
+    fn from(err: DecoderError) -> WebDriverError {
+        WebDriverError::new(ErrorStatus::UnknownError, err.description().to_string())
+    }
+}
+
+impl From<FromBase64Error> for WebDriverError {
+    fn from(err: FromBase64Error) -> WebDriverError {
+        WebDriverError::new(ErrorStatus::UnknownError, err.description().to_string())
+    }
+}
+
+impl From<Box<Error>> for WebDriverError {
+    fn from(err: Box<Error>) -> WebDriverError {
+        WebDriverError::new(ErrorStatus::UnknownError, err.description().to_string())
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/httpapi.rs
@@ -0,0 +1,245 @@
+use regex::{Regex, Captures};
+use rustc_serialize::json::Json;
+
+use hyper::method::Method;
+use hyper::method::Method::{Get, Post, Delete};
+
+use command::{WebDriverCommand, WebDriverMessage, WebDriverExtensionCommand,
+              VoidWebDriverExtensionCommand};
+use error::{WebDriverResult, WebDriverError, ErrorStatus};
+
+fn standard_routes<U:WebDriverExtensionRoute>() -> Vec<(Method, &'static str, Route<U>)> {
+    return vec![(Post, "/session", Route::NewSession),
+                (Delete, "/session/{sessionId}", Route::DeleteSession),
+                (Post, "/session/{sessionId}/url", Route::Get),
+                (Get, "/session/{sessionId}/url", Route::GetCurrentUrl),
+                (Post, "/session/{sessionId}/back", Route::GoBack),
+                (Post, "/session/{sessionId}/forward", Route::GoForward),
+                (Post, "/session/{sessionId}/refresh", Route::Refresh),
+                (Get, "/session/{sessionId}/title", Route::GetTitle),
+                (Get, "/session/{sessionId}/source", Route::GetPageSource),
+                (Get, "/session/{sessionId}/window", Route::GetWindowHandle),
+                (Get, "/session/{sessionId}/window/handles", Route::GetWindowHandles),
+                (Delete, "/session/{sessionId}/window", Route::CloseWindow),
+                (Get, "/session/{sessionId}/window/size", Route::GetWindowSize),
+                (Post, "/session/{sessionId}/window/size", Route::SetWindowSize),
+                (Get, "/session/{sessionId}/window/position", Route::GetWindowPosition),
+                (Post, "/session/{sessionId}/window/position", Route::SetWindowPosition),
+                (Get, "/session/{sessionId}/window/rect", Route::GetWindowRect),
+                (Post, "/session/{sessionId}/window/rect", Route::SetWindowRect),
+                (Post, "/session/{sessionId}/window/minimize", Route::MinimizeWindow),
+                (Post, "/session/{sessionId}/window/maximize", Route::MaximizeWindow),
+                (Post, "/session/{sessionId}/window/fullscreen", Route::FullscreenWindow),
+                (Post, "/session/{sessionId}/window", Route::SwitchToWindow),
+                (Post, "/session/{sessionId}/frame", Route::SwitchToFrame),
+                (Post, "/session/{sessionId}/frame/parent", Route::SwitchToParentFrame),
+                (Post, "/session/{sessionId}/element", Route::FindElement),
+                (Post, "/session/{sessionId}/elements", Route::FindElements),
+                (Post, "/session/{sessionId}/element/{elementId}/element", Route::FindElementElement),
+                (Post, "/session/{sessionId}/element/{elementId}/elements", Route::FindElementElements),
+                (Get, "/session/{sessionId}/element/active", Route::GetActiveElement),
+                (Get, "/session/{sessionId}/element/{elementId}/displayed", Route::IsDisplayed),
+                (Get, "/session/{sessionId}/element/{elementId}/selected", Route::IsSelected),
+                (Get, "/session/{sessionId}/element/{elementId}/attribute/{name}", Route::GetElementAttribute),
+                (Get, "/session/{sessionId}/element/{elementId}/property/{name}", Route::GetElementProperty),
+                (Get, "/session/{sessionId}/element/{elementId}/css/{propertyName}", Route::GetCSSValue),
+                (Get, "/session/{sessionId}/element/{elementId}/text", Route::GetElementText),
+                (Get, "/session/{sessionId}/element/{elementId}/name", Route::GetElementTagName),
+                (Get, "/session/{sessionId}/element/{elementId}/rect", Route::GetElementRect),
+                (Get, "/session/{sessionId}/element/{elementId}/enabled", Route::IsEnabled),
+                (Post, "/session/{sessionId}/execute/sync", Route::ExecuteScript),
+                (Post, "/session/{sessionId}/execute/async", Route::ExecuteAsyncScript),
+                (Get, "/session/{sessionId}/cookie", Route::GetCookies),
+                (Get, "/session/{sessionId}/cookie/{name}", Route::GetNamedCookie),
+                (Post, "/session/{sessionId}/cookie", Route::AddCookie),
+                (Delete, "/session/{sessionId}/cookie", Route::DeleteCookies),
+                (Delete, "/session/{sessionId}/cookie/{name}", Route::DeleteCookie),
+                (Get, "/session/{sessionId}/timeouts", Route::GetTimeouts),
+                (Post, "/session/{sessionId}/timeouts", Route::SetTimeouts),
+                (Post, "/session/{sessionId}/element/{elementId}/click", Route::ElementClick),
+                (Post, "/session/{sessionId}/element/{elementId}/tap", Route::ElementTap),
+                (Post, "/session/{sessionId}/element/{elementId}/clear", Route::ElementClear),
+                (Post, "/session/{sessionId}/element/{elementId}/value", Route::ElementSendKeys),
+                (Post, "/session/{sessionId}/alert/dismiss", Route::DismissAlert),
+                (Post, "/session/{sessionId}/alert/accept", Route::AcceptAlert),
+                (Get, "/session/{sessionId}/alert/text", Route::GetAlertText),
+                (Post, "/session/{sessionId}/alert/text", Route::SendAlertText),
+                (Get, "/session/{sessionId}/screenshot", Route::TakeScreenshot),
+                (Get, "/session/{sessionId}/element/{elementId}/screenshot", Route::TakeElementScreenshot),
+                (Post, "/session/{sessionId}/actions", Route::PerformActions),
+                (Delete, "/session/{sessionId}/actions", Route::ReleaseActions),
+                (Get, "/status", Route::Status),]
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum Route<U:WebDriverExtensionRoute> {
+    NewSession,
+    DeleteSession,
+    Get,
+    GetCurrentUrl,
+    GoBack,
+    GoForward,
+    Refresh,
+    GetTitle,
+    GetPageSource,
+    GetWindowHandle,
+    GetWindowHandles,
+    CloseWindow,
+    GetWindowSize,  // deprecated
+    SetWindowSize,  // deprecated
+    GetWindowPosition,  // deprecated
+    SetWindowPosition,  // deprecated
+    GetWindowRect,
+    SetWindowRect,
+    MinimizeWindow,
+    MaximizeWindow,
+    FullscreenWindow,
+    SwitchToWindow,
+    SwitchToFrame,
+    SwitchToParentFrame,
+    FindElement,
+    FindElements,
+    FindElementElement,
+    FindElementElements,
+    GetActiveElement,
+    IsDisplayed,
+    IsSelected,
+    GetElementAttribute,
+    GetElementProperty,
+    GetCSSValue,
+    GetElementText,
+    GetElementTagName,
+    GetElementRect,
+    IsEnabled,
+    ExecuteScript,
+    ExecuteAsyncScript,
+    GetCookies,
+    GetNamedCookie,
+    AddCookie,
+    DeleteCookies,
+    DeleteCookie,
+    GetTimeouts,
+    SetTimeouts,
+    ElementClick,
+    ElementTap,
+    ElementClear,
+    ElementSendKeys,
+    PerformActions,
+    ReleaseActions,
+    DismissAlert,
+    AcceptAlert,
+    GetAlertText,
+    SendAlertText,
+    TakeScreenshot,
+    TakeElementScreenshot,
+    Status,
+    Extension(U),
+}
+
+pub trait WebDriverExtensionRoute : Clone + Send + PartialEq {
+    type Command: WebDriverExtensionCommand + 'static;
+
+    fn command(&self, &Captures, &Json) -> WebDriverResult<WebDriverCommand<Self::Command>>;
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct VoidWebDriverExtensionRoute;
+
+impl WebDriverExtensionRoute for VoidWebDriverExtensionRoute {
+    type Command = VoidWebDriverExtensionCommand;
+
+    fn command(&self, _:&Captures, _:&Json) -> WebDriverResult<WebDriverCommand<VoidWebDriverExtensionCommand>> {
+        panic!("No extensions implemented");
+    }
+}
+
+#[derive(Clone, Debug)]
+struct RequestMatcher<U: WebDriverExtensionRoute> {
+    method: Method,
+    path_regexp: Regex,
+    match_type: Route<U>
+}
+
+impl <U: WebDriverExtensionRoute> RequestMatcher<U> {
+    pub fn new(method: Method, path: &str, match_type: Route<U>) -> RequestMatcher<U> {
+        let path_regexp = RequestMatcher::<U>::compile_path(path);
+        RequestMatcher {
+            method: method,
+            path_regexp: path_regexp,
+            match_type: match_type
+        }
+    }
+
+    pub fn get_match<'t>(&'t self, method: Method, path: &'t str) -> (bool, Option<Captures>) {
+        let captures = self.path_regexp.captures(path);
+        (method == self.method, captures)
+    }
+
+    fn compile_path(path: &str) -> Regex {
+        let mut rv = String::new();
+        rv.push_str("^");
+        let components = path.split('/');
+        for component in components {
+            if component.starts_with("{") {
+                if !component.ends_with("}") {
+                    panic!("Invalid url pattern")
+                }
+                rv.push_str(&format!("(?P<{}>[^/]+)/", &component[1..component.len()-1])[..]);
+            } else {
+                rv.push_str(&format!("{}/", component)[..]);
+            }
+        }
+        //Remove the trailing /
+        rv.pop();
+        rv.push_str("$");
+        //This will fail at runtime if the regexp is invalid
+        Regex::new(&rv[..]).unwrap()
+    }
+}
+
+#[derive(Debug)]
+pub struct WebDriverHttpApi<U: WebDriverExtensionRoute> {
+    routes: Vec<(Method, RequestMatcher<U>)>,
+}
+
+impl <U: WebDriverExtensionRoute> WebDriverHttpApi<U> {
+    pub fn new(extension_routes: &[(Method, &str, U)]) -> WebDriverHttpApi<U> {
+        let mut rv = WebDriverHttpApi::<U> {
+            routes: vec![],
+        };
+        debug!("Creating routes");
+        for &(ref method, ref url, ref match_type) in standard_routes::<U>().iter() {
+            rv.add(method.clone(), *url, (*match_type).clone());
+        };
+        for &(ref method, ref url, ref extension_route) in extension_routes.iter() {
+            rv.add(method.clone(), *url, Route::Extension(extension_route.clone()));
+        };
+        rv
+    }
+
+    fn add(&mut self, method: Method, path: &str, match_type: Route<U>) {
+        let http_matcher = RequestMatcher::new(method.clone(), path, match_type);
+        self.routes.push((method, http_matcher));
+    }
+
+    pub fn decode_request(&self, method: Method, path: &str, body: &str) -> WebDriverResult<WebDriverMessage<U>> {
+        let mut error = ErrorStatus::UnknownPath;
+        for &(ref match_method, ref matcher) in self.routes.iter() {
+            if method == *match_method {
+                let (method_match, captures) = matcher.get_match(method.clone(), path);
+                if captures.is_some() {
+                    if method_match {
+                        return WebDriverMessage::from_http(matcher.match_type.clone(),
+                                                           &captures.unwrap(),
+                                                           body,
+                                                           method == Post)
+                    } else {
+                        error = ErrorStatus::UnknownMethod;
+                    }
+                }
+            }
+        }
+        Err(WebDriverError::new(error,
+                                format!("{} {} did not match a known command", method, path)))
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/lib.rs
@@ -0,0 +1,43 @@
+#![allow(non_snake_case)]
+
+extern crate backtrace;
+#[macro_use]
+extern crate log;
+extern crate rustc_serialize;
+extern crate hyper;
+extern crate regex;
+extern crate cookie;
+extern crate time;
+extern crate url;
+
+#[macro_use] pub mod macros;
+pub mod httpapi;
+pub mod capabilities;
+pub mod command;
+pub mod common;
+pub mod error;
+pub mod server;
+pub mod response;
+
+#[cfg(test)]
+mod nullable_tests {
+    use super::common::Nullable;
+
+    #[test]
+    fn test_nullable_map() {
+        let mut test = Nullable::Value(21);
+
+        assert_eq!(test.map(|x| x << 1), Nullable::Value(42));
+
+        test = Nullable::Null;
+
+        assert_eq!(test.map(|x| x << 1), Nullable::Null);
+    }
+
+    #[test]
+    fn test_nullable_into() {
+        let test: Option<i32> = Nullable::Value(42).into();
+
+        assert_eq!(test, Some(42));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/macros.rs
@@ -0,0 +1,8 @@
+macro_rules! try_opt {
+    ($expr:expr, $err_type:expr, $err_msg:expr) => ({
+        match $expr {
+            Some(x) => x,
+            None => return Err(WebDriverError::new($err_type, $err_msg))
+        }
+    })
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/response.rs
@@ -0,0 +1,336 @@
+use common::{Date, Nullable, WindowState};
+use cookie;
+use rustc_serialize::json::{self, Json, ToJson};
+use std::collections::BTreeMap;
+use time;
+
+#[derive(Debug)]
+pub enum WebDriverResponse {
+    CloseWindow(CloseWindowResponse),
+    Cookie(CookieResponse),
+    Cookies(CookiesResponse),
+    DeleteSession,
+    ElementRect(ElementRectResponse),
+    Generic(ValueResponse),
+    NewSession(NewSessionResponse),
+    Timeouts(TimeoutsResponse),
+    Void,
+    WindowRect(WindowRectResponse),
+}
+
+impl WebDriverResponse {
+    pub fn to_json_string(self) -> String {
+        use response::WebDriverResponse::*;
+
+        let obj = match self {
+            CloseWindow(ref x) => json::encode(&x.to_json()),
+            Cookie(ref x) => json::encode(x),
+            Cookies(ref x) => json::encode(x),
+            DeleteSession => Ok("{}".to_string()),
+            ElementRect(ref x) => json::encode(x),
+            Generic(ref x) => json::encode(x),
+            NewSession(ref x) => json::encode(x),
+            Timeouts(ref x) => json::encode(x),
+            Void => Ok("{}".to_string()),
+            WindowRect(ref x) => json::encode(&x.to_json()),
+        }.unwrap();
+
+        match self {
+            Generic(_) | Cookie(_) | Cookies(_) => obj,
+            _ => {
+                let mut data = String::with_capacity(11 + obj.len());
+                data.push_str("{\"value\": ");
+                data.push_str(&*obj);
+                data.push_str("}");
+                data
+            }
+        }
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct CloseWindowResponse {
+    pub window_handles: Vec<String>,
+}
+
+impl CloseWindowResponse {
+    pub fn new(handles: Vec<String>) -> CloseWindowResponse {
+        CloseWindowResponse { window_handles: handles }
+    }
+}
+
+impl ToJson for CloseWindowResponse {
+    fn to_json(&self) -> Json {
+        Json::Array(self.window_handles
+                    .iter()
+                    .map(|x| Json::String(x.clone()))
+                    .collect::<Vec<Json>>())
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct NewSessionResponse {
+    pub sessionId: String,
+    pub capabilities: json::Json
+}
+
+impl NewSessionResponse {
+    pub fn new(session_id: String, capabilities: json::Json) -> NewSessionResponse {
+        NewSessionResponse {
+            capabilities: capabilities,
+            sessionId: session_id
+        }
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct TimeoutsResponse {
+    pub script: u64,
+    pub pageLoad: u64,
+    pub implicit: u64,
+}
+
+impl TimeoutsResponse {
+    pub fn new(script: u64, page_load: u64, implicit: u64) -> TimeoutsResponse {
+        TimeoutsResponse {
+            script: script,
+            pageLoad: page_load,
+            implicit: implicit,
+        }
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct ValueResponse {
+    pub value: json::Json
+}
+
+impl ValueResponse {
+    pub fn new(value: json::Json) -> ValueResponse {
+        ValueResponse {
+            value: value
+        }
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct ElementRectResponse {
+    /// X axis position of the top-left corner of the element relative
+    // to the current browsing context’s document element in CSS reference
+    // pixels.
+    pub x: f64,
+
+    /// Y axis position of the top-left corner of the element relative
+    // to the current browsing context’s document element in CSS reference
+    // pixels.
+    pub y: f64,
+
+    /// Height of the element’s [bounding rectangle] in CSS reference
+    /// pixels.
+    ///
+    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
+    pub width: f64,
+
+    /// Width of the element’s [bounding rectangle] in CSS reference
+    /// pixels.
+    ///
+    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
+    pub height: f64,
+}
+
+#[derive(Debug)]
+pub struct WindowRectResponse {
+    /// `WindowProxy`’s [screenX] attribute.
+    ///
+    /// [screenX]: https://drafts.csswg.org/cssom-view/#dom-window-screenx
+    pub x: f64,
+
+    /// `WindowProxy`’s [screenY] attribute.
+    ///
+    /// [screenY]: https://drafts.csswg.org/cssom-view/#dom-window-screeny
+    pub y: f64,
+
+    /// Width of the top-level browsing context’s outer dimensions, including
+    /// any browser chrome and externally drawn window decorations in CSS
+    /// reference pixels.
+    pub width: f64,
+
+    /// Height of the top-level browsing context’s outer dimensions, including
+    /// any browser chrome and externally drawn window decorations in CSS
+    /// reference pixels.
+    pub height: f64,
+
+    /// The top-level browsing context’s window state.
+    pub state: WindowState,
+}
+
+impl ToJson for WindowRectResponse {
+    fn to_json(&self) -> Json {
+        let mut body = BTreeMap::new();
+        body.insert("x".to_owned(), self.x.to_json());
+        body.insert("y".to_owned(), self.y.to_json());
+        body.insert("width".to_owned(), self.width.to_json());
+        body.insert("height".to_owned(), self.height.to_json());
+        body.insert("state".to_owned(), self.state.to_json());
+        Json::Object(body)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, RustcEncodable)]
+pub struct Cookie {
+    pub name: String,
+    pub value: String,
+    pub path: Nullable<String>,
+    pub domain: Nullable<String>,
+    pub expiry: Nullable<Date>,
+    pub secure: bool,
+    pub httpOnly: bool,
+}
+
+impl Into<cookie::Cookie<'static>> for Cookie {
+    fn into(self) -> cookie::Cookie<'static> {
+        let cookie = cookie::Cookie::build(self.name, self.value)
+            .secure(self.secure)
+            .http_only(self.httpOnly);
+        let cookie = match self.domain {
+            Nullable::Value(domain) => cookie.domain(domain),
+            Nullable::Null => cookie,
+        };
+        let cookie = match self.path {
+            Nullable::Value(path) => cookie.path(path),
+            Nullable::Null => cookie,
+        };
+        let cookie = match self.expiry {
+            Nullable::Value(Date(expiry)) => {
+                cookie.expires(time::at(time::Timespec::new(expiry as i64, 0)))
+            }
+            Nullable::Null => cookie,
+        };
+        cookie.finish()
+    }
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct CookieResponse {
+    pub value: Cookie,
+}
+
+#[derive(Debug, RustcEncodable)]
+pub struct CookiesResponse {
+    pub value: Vec<Cookie>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{CloseWindowResponse, Cookie, CookieResponse, CookiesResponse, ElementRectResponse,
+                NewSessionResponse, Nullable, TimeoutsResponse, ValueResponse, WebDriverResponse,
+                WindowRectResponse};
+    use common::WindowState;
+    use rustc_serialize::json::Json;
+    use std::collections::BTreeMap;
+
+    fn test(resp: WebDriverResponse, expected_str: &str) {
+        let data = resp.to_json_string();
+        let actual = Json::from_str(&*data).unwrap();
+        let expected = Json::from_str(expected_str).unwrap();
+        assert_eq!(actual, expected);
+    }
+
+    #[test]
+    fn test_close_window() {
+        let resp = WebDriverResponse::CloseWindow(
+            CloseWindowResponse::new(vec!["test".into()]));
+        let expected = r#"{"value": ["test"]}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_cookie() {
+        let cookie = Cookie {
+            name: "name".into(),
+            value: "value".into(),
+            path: Nullable::Value("/".into()),
+            domain: Nullable::Null,
+            expiry: Nullable::Null,
+            secure: true,
+            httpOnly: false,
+        };
+        let resp = WebDriverResponse::Cookie(CookieResponse { value: cookie });
+        let expected = r#"{"value": {"name": "name", "expiry": null, "value": "value",
+"path": "/", "domain": null, "secure": true, "httpOnly": false}}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_cookies() {
+        let resp = WebDriverResponse::Cookies(CookiesResponse {
+            value: vec![
+                Cookie {
+                    name: "name".into(),
+                    value: "value".into(),
+                    path: Nullable::Value("/".into()),
+                    domain: Nullable::Null,
+                    expiry: Nullable::Null,
+                    secure: true,
+                    httpOnly: false,
+                }
+            ]});
+        let expected = r#"{"value": [{"name": "name", "value": "value", "path": "/",
+"domain": null, "expiry": null, "secure": true, "httpOnly": false}]}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_element_rect() {
+        let rect = ElementRectResponse {
+            x: 0f64,
+            y: 1f64,
+            width: 2f64,
+            height: 3f64,
+        };
+        let resp = WebDriverResponse::ElementRect(rect);
+        let expected = r#"{"value": {"x": 0.0, "y": 1.0, "width": 2.0, "height": 3.0}}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_window_rect() {
+        let rect = WindowRectResponse {
+            x: 0f64,
+            y: 1f64,
+            width: 2f64,
+            height: 3f64,
+            state: WindowState::Normal,
+        };
+        let resp = WebDriverResponse::WindowRect(rect);
+        let expected = r#"{"value": {"x": 0.0, "y": 1.0, "width": 2.0, "height": 3.0, "state": "normal"}}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_new_session() {
+        let resp = WebDriverResponse::NewSession(
+            NewSessionResponse::new("test".into(),
+                                    Json::Object(BTreeMap::new())));
+        let expected = r#"{"value": {"sessionId": "test", "capabilities": {}}}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_timeouts() {
+         let resp = WebDriverResponse::Timeouts(TimeoutsResponse::new(
+            1, 2, 3));
+        let expected = r#"{"value": {"script": 1, "pageLoad": 2, "implicit": 3}}"#;
+        test(resp, expected);
+    }
+
+    #[test]
+    fn test_value() {
+        let mut value = BTreeMap::new();
+        value.insert("example".into(), Json::Array(vec![Json::String("test".into())]));
+        let resp = WebDriverResponse::Generic(ValueResponse::new(
+            Json::Object(value)));
+        let expected = r#"{"value": {"example": ["test"]}}"#;
+        test(resp, expected);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/server.rs
@@ -0,0 +1,263 @@
+use std::io::Read;
+use std::marker::PhantomData;
+use std::net::SocketAddr;
+use std::sync::mpsc::{channel, Receiver, Sender};
+use std::sync::Mutex;
+use std::thread;
+
+use hyper::header::{ContentType, CacheControl, CacheDirective};
+use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value};
+use hyper::method::Method;
+use hyper::Result;
+use hyper::server::{Handler, Listening, Request, Response, Server};
+use hyper::status::StatusCode;
+use hyper::uri::RequestUri::AbsolutePath;
+
+use command::{WebDriverMessage, WebDriverCommand};
+use error::{WebDriverResult, WebDriverError, ErrorStatus};
+use httpapi::{WebDriverHttpApi, WebDriverExtensionRoute, VoidWebDriverExtensionRoute};
+use response::{CloseWindowResponse, WebDriverResponse};
+
+enum DispatchMessage<U: WebDriverExtensionRoute> {
+    HandleWebDriver(WebDriverMessage<U>, Sender<WebDriverResult<WebDriverResponse>>),
+    Quit
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Session {
+    id: String
+}
+
+impl Session {
+    fn new(id: String) -> Session {
+        Session {
+            id: id
+        }
+    }
+}
+
+pub trait WebDriverHandler<U: WebDriverExtensionRoute=VoidWebDriverExtensionRoute> : Send {
+    fn handle_command(&mut self, session: &Option<Session>, msg: WebDriverMessage<U>) -> WebDriverResult<WebDriverResponse>;
+    fn delete_session(&mut self, session: &Option<Session>);
+}
+
+#[derive(Debug)]
+struct Dispatcher<T: WebDriverHandler<U>,
+                  U: WebDriverExtensionRoute> {
+    handler: T,
+    session: Option<Session>,
+    extension_type: PhantomData<U>,
+}
+
+impl<T: WebDriverHandler<U>, U: WebDriverExtensionRoute> Dispatcher<T, U> {
+    fn new(handler: T) -> Dispatcher<T, U> {
+        Dispatcher {
+            handler: handler,
+            session: None,
+            extension_type: PhantomData,
+        }
+    }
+
+    fn run(&mut self, msg_chan: Receiver<DispatchMessage<U>>) {
+        loop {
+            match msg_chan.recv() {
+                Ok(DispatchMessage::HandleWebDriver(msg, resp_chan)) => {
+                    let resp = match self.check_session(&msg) {
+                        Ok(_) => self.handler.handle_command(&self.session, msg),
+                        Err(e) => Err(e),
+                    };
+
+                    match resp {
+                        Ok(WebDriverResponse::NewSession(ref new_session)) => {
+                            self.session = Some(Session::new(new_session.sessionId.clone()));
+                        }
+                        Ok(WebDriverResponse::CloseWindow(CloseWindowResponse { ref window_handles })) => {
+                            if window_handles.len() == 0 {
+                                debug!("Last window was closed, deleting session");
+                                self.delete_session();
+                            }
+                        }
+                        Ok(WebDriverResponse::DeleteSession) => self.delete_session(),
+                        Err(ref x) if x.delete_session => self.delete_session(),
+                        _ => {}
+                    }
+
+                    if resp_chan.send(resp).is_err() {
+                        error!("Sending response to the main thread failed");
+                    };
+                }
+                Ok(DispatchMessage::Quit) => break,
+                Err(_) => panic!("Error receiving message in handler"),
+            }
+        }
+    }
+
+    fn delete_session(&mut self) {
+        debug!("Deleting session");
+        self.handler.delete_session(&self.session);
+        self.session = None;
+    }
+
+    fn check_session(&self, msg: &WebDriverMessage<U>) -> WebDriverResult<()> {
+        match msg.session_id {
+            Some(ref msg_session_id) => {
+                match self.session {
+                    Some(ref existing_session) => {
+                        if existing_session.id != *msg_session_id {
+                            Err(WebDriverError::new(
+                                ErrorStatus::InvalidSessionId,
+                                format!("Got unexpected session id {}",
+                                        msg_session_id)))
+                        } else {
+                            Ok(())
+                        }
+                    },
+                    None => Ok(())
+                }
+            },
+            None => {
+                match self.session {
+                    Some(_) => {
+                        match msg.command {
+                            WebDriverCommand::Status => Ok(()),
+                            WebDriverCommand::NewSession(_) => {
+                                Err(WebDriverError::new(
+                                    ErrorStatus::SessionNotCreated,
+                                    "Session is already started"))
+                            },
+                            _ => {
+                                //This should be impossible
+                                error!("Got a message with no session id");
+                                Err(WebDriverError::new(
+                                    ErrorStatus::UnknownError,
+                                    "Got a command with no session?!"))
+                            }
+                        }
+                    },
+                    None => {
+                        match msg.command {
+                            WebDriverCommand::NewSession(_) => Ok(()),
+                            WebDriverCommand::Status => Ok(()),
+                            _ => Err(WebDriverError::new(
+                                ErrorStatus::InvalidSessionId,
+                                "Tried to run a command before creating a session"))
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug)]
+struct HttpHandler<U: WebDriverExtensionRoute> {
+    chan: Mutex<Sender<DispatchMessage<U>>>,
+    api: Mutex<WebDriverHttpApi<U>>
+}
+
+impl <U: WebDriverExtensionRoute> HttpHandler<U> {
+    fn new(api: WebDriverHttpApi<U>, chan: Sender<DispatchMessage<U>>) -> HttpHandler<U> {
+        HttpHandler {
+            chan: Mutex::new(chan),
+            api: Mutex::new(api)
+        }
+    }
+}
+
+impl<U: WebDriverExtensionRoute> Handler for HttpHandler<U> {
+    fn handle(&self, req: Request, res: Response) {
+        let mut req = req;
+        let mut res = res;
+
+        let mut body = String::new();
+        if let Method::Post = req.method {
+            req.read_to_string(&mut body).unwrap();
+        }
+
+        debug!("-> {} {} {}", req.method, req.uri, body);
+
+        match req.uri {
+            AbsolutePath(path) => {
+                let msg_result = {
+                    // The fact that this locks for basically the whole request doesn't
+                    // matter as long as we are only handling one request at a time.
+                    match self.api.lock() {
+                        Ok(ref api) => api.decode_request(req.method, &path[..], &body[..]),
+                        Err(_) => return,
+                    }
+                };
+                let (status, resp_body) = match msg_result {
+                    Ok(message) => {
+                        let (send_res, recv_res) = channel();
+                        match self.chan.lock() {
+                            Ok(ref c) => {
+                                let res =
+                                    c.send(DispatchMessage::HandleWebDriver(message, send_res));
+                                match res {
+                                    Ok(x) => x,
+                                    Err(_) => {
+                                        error!("Something terrible happened");
+                                        return;
+                                    }
+                                }
+                            }
+                            Err(_) => {
+                                error!("Something terrible happened");
+                                return;
+                            }
+                        }
+                        match recv_res.recv() {
+                            Ok(data) => {
+                                match data {
+                                    Ok(response) => (StatusCode::Ok, response.to_json_string()),
+                                    Err(err) => (err.http_status(), err.to_json_string()),
+                                }
+                            }
+                            Err(e) => panic!("Error reading response: {:?}", e),
+                        }
+                    }
+                    Err(err) => (err.http_status(), err.to_json_string()),
+                };
+
+                debug!("<- {} {}", status, resp_body);
+
+                {
+                    let resp_status = res.status_mut();
+                    *resp_status = status;
+                }
+                res.headers_mut()
+                    .set(ContentType(Mime(TopLevel::Application,
+                                          SubLevel::Json,
+                                          vec![(Attr::Charset, Value::Utf8)])));
+                res.headers_mut()
+                    .set(CacheControl(vec![CacheDirective::NoCache]));
+
+                res.send(&resp_body.as_bytes()).unwrap();
+            }
+            _ => {}
+        }
+    }
+}
+
+pub fn start<T, U>(address: SocketAddr,
+                   handler: T,
+                   extension_routes: &[(Method, &str, U)])
+                   -> Result<Listening>
+    where T: 'static + WebDriverHandler<U>,
+          U: 'static + WebDriverExtensionRoute
+{
+    let (msg_send, msg_recv) = channel();
+
+    let api = WebDriverHttpApi::new(extension_routes);
+    let http_handler = HttpHandler::new(api, msg_send);
+    let mut server = try!(Server::http(address));
+    server.keep_alive(None);
+
+    let builder = thread::Builder::new().name("webdriver dispatcher".to_string());
+    try!(builder.spawn(move || {
+        let mut dispatcher = Dispatcher::new(handler);
+        dispatcher.run(msg_recv);
+    }));
+
+    server.handle(http_handler)
+}
--- a/widget/cocoa/nsChildView.h
+++ b/widget/cocoa/nsChildView.h
@@ -219,18 +219,16 @@ class WidgetRenderingContext;
 
 // these are sent to the first responder when the window key status changes
 - (void)viewsWindowDidBecomeKey;
 - (void)viewsWindowDidResignKey;
 
 // Stop NSView hierarchy being changed during [ChildView drawRect:]
 - (void)delayedTearDown;
 
-- (void)sendFocusEvent:(mozilla::EventMessage)eventMessage;
-
 - (void)handleMouseMoved:(NSEvent*)aEvent;
 
 - (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent
                             enter:(BOOL)aEnter
                          exitFrom:(mozilla::WidgetMouseEvent::ExitFrom)aExitFrom;
 
 - (void)updateGLContext;
 - (void)_surfaceNeedsUpdate:(NSNotification*)notification;
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -3352,20 +3352,19 @@ NSEvent* gLastDragMouseDownEvent = nil;
   [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(systemMetricsChanged)
                                                name:NSControlTintDidChangeNotification
                                              object:nil];
   [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(systemMetricsChanged)
                                                name:NSSystemColorsDidChangeNotification
                                              object:nil];
-  // TODO: replace the string with the constant once we build with the 10.7 SDK
   [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(scrollbarSystemMetricChanged)
-                                               name:@"NSPreferredScrollerStyleDidChangeNotification"
+                                               name:NSPreferredScrollerStyleDidChangeNotification
                                              object:nil];
   [[NSDistributedNotificationCenter defaultCenter] addObserver:self
                                                       selector:@selector(systemMetricsChanged)
                                                           name:@"AppleAquaScrollBarVariantChanged"
                                                         object:nil
                                             suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
   [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(_surfaceNeedsUpdate:)
@@ -3626,29 +3625,16 @@ NSEvent* gLastDragMouseDownEvent = nil;
   return YES;
 }
 
 - (BOOL)isOpaque
 {
   return [[self window] isOpaque];
 }
 
-// XXX Is this really used?
-- (void)sendFocusEvent:(EventMessage)eventMessage
-{
-  if (!mGeckoChild)
-    return;
-
-  nsEventStatus status = nsEventStatus_eIgnore;
-  WidgetGUIEvent focusGuiEvent(true, eventMessage, mGeckoChild);
-  focusGuiEvent.mTime = PR_IntervalNow();
-  focusGuiEvent.mTimeStamp = nsCocoaUtils::GetEventTimeStamp(0);
-  mGeckoChild->DispatchEvent(&focusGuiEvent, status);
-}
-
 // We accept key and mouse events, so don't keep passing them up the chain. Allow
 // this to be a 'focused' widget for event dispatch.
 - (BOOL)acceptsFirstResponder
 {
   return YES;
 }
 
 // Accept mouse down events on background windows