UNPKG

quixote

Version:

CSS unit and integration testing

1,466 lines (1,221 loc) 376 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.quixote = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. var cachedSetTimeout; var cachedClearTimeout; function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } (function () { try { if (typeof setTimeout === 'function') { cachedSetTimeout = setTimeout; } else { cachedSetTimeout = defaultSetTimout; } } catch (e) { cachedSetTimeout = defaultSetTimout; } try { if (typeof clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } else { cachedClearTimeout = defaultClearTimeout; } } catch (e) { cachedClearTimeout = defaultClearTimeout; } } ()) function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.prependListener = noop; process.prependOnceListener = noop; process.listeners = function (name) { return [] } process.binding = function (name) { throw new Error('process.binding is not supported'); }; process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; },{}],2:[function(require,module,exports){ (function (setImmediate,clearImmediate){ var nextTick = require('process/browser.js').nextTick; var apply = Function.prototype.apply; var slice = Array.prototype.slice; var immediateIds = {}; var nextImmediateId = 0; // DOM APIs, for completeness exports.setTimeout = function() { return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout); }; exports.setInterval = function() { return new Timeout(apply.call(setInterval, window, arguments), clearInterval); }; exports.clearTimeout = exports.clearInterval = function(timeout) { timeout.close(); }; function Timeout(id, clearFn) { this._id = id; this._clearFn = clearFn; } Timeout.prototype.unref = Timeout.prototype.ref = function() {}; Timeout.prototype.close = function() { this._clearFn.call(window, this._id); }; // Does not start the time, just sets up the members needed. exports.enroll = function(item, msecs) { clearTimeout(item._idleTimeoutId); item._idleTimeout = msecs; }; exports.unenroll = function(item) { clearTimeout(item._idleTimeoutId); item._idleTimeout = -1; }; exports._unrefActive = exports.active = function(item) { clearTimeout(item._idleTimeoutId); var msecs = item._idleTimeout; if (msecs >= 0) { item._idleTimeoutId = setTimeout(function onTimeout() { if (item._onTimeout) item._onTimeout(); }, msecs); } }; // That's not how node.js implements it but the exposed api is the same. exports.setImmediate = typeof setImmediate === "function" ? setImmediate : function(fn) { var id = nextImmediateId++; var args = arguments.length < 2 ? false : slice.call(arguments, 1); immediateIds[id] = true; nextTick(function onNextTick() { if (immediateIds[id]) { // fn.call() is faster so we optimize for the common use-case // @see http://jsperf.com/call-apply-segu if (args) { fn.apply(null, args); } else { fn.call(null); } // Prevent ids from leaking exports.clearImmediate(id); } }); return id; }; exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : function(id) { delete immediateIds[id]; }; }).call(this,require("timers").setImmediate,require("timers").clearImmediate) },{"process/browser.js":1,"timers":2}],3:[function(require,module,exports){ // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("./util/ensure.js"); var oop = require("./util/oop.js"); var shim = require("./util/shim.js"); var Me = module.exports = function Assertable() { ensure.unreachable("Assertable is abstract and should not be constructed directly."); }; Me.extend = oop.extendFn(Me); oop.makeAbstract(Me, []); Me.prototype.assert = function assert(expected, message) { ensure.signature(arguments, [ Object, [undefined, String] ]); if (message === undefined) message = "Differences found"; var diff = this.diff(expected); if (diff !== "") throw new Error(message + ":\n" + diff + "\n"); }; Me.prototype.diff = function diff(expected) { ensure.signature(arguments, [ Object ]); var result = []; var keys = shim.Object.keys(expected); var key, oneDiff, descriptor; for (var i = 0; i < keys.length; i++) { key = keys[i]; descriptor = this[key]; ensure.that( descriptor !== undefined, this + " doesn't have a property named '" + key + "'. Did you misspell it?" ); oneDiff = descriptor.diff(expected[key]); if (oneDiff !== "") result.push(oneDiff); } return result.join("\n"); }; },{"./util/ensure.js":26,"./util/oop.js":27,"./util/shim.js":28}],4:[function(require,module,exports){ // Copyright Titanium I.T. LLC. "use strict"; var ensure = require("./util/ensure.js"); var QFrame = require("./q_frame.js"); var Size = require("./values/size.js"); var FRAME_WIDTH = 1500; var FRAME_HEIGHT = 200; var features = null; exports.enlargesFrameToPageSize = createDetectionMethod("enlargesFrame"); exports.enlargesFonts = createDetectionMethod("enlargesFonts"); exports.misreportsClipAutoProperty = createDetectionMethod("misreportsClipAuto"); exports.misreportsAutoValuesInClipProperty = createDetectionMethod("misreportsClipValues"); exports.roundsOffPixelCalculations = createDetectionMethod("roundsOffPixelCalculations"); exports.detectBrowserFeatures = function(callback) { var frame = QFrame.create(document.body, { width: FRAME_WIDTH, height: FRAME_HEIGHT }, function(err) { if (err) { return callback(new Error("Error while creating Quixote browser feature detection frame: " + err)); } return detectFeatures(frame, function(err) { frame.remove(); return callback(err); }); }); }; function detectFeatures(frame, callback) { try { features = {}; features.enlargesFrame = detectFrameEnlargement(frame, FRAME_WIDTH); features.misreportsClipAuto = detectReportedClipAuto(frame); features.misreportsClipValues = detectReportedClipPropertyValues(frame); features.roundsOffPixelCalculations = detectRoundsOffPixelCalculations(frame); detectFontEnlargement(frame, FRAME_WIDTH, function(result) { features.enlargesFonts = result; frame.remove(); return callback(null); }); } catch(err) { features = null; return callback(new Error("Error during Quixote browser feature detection: " + err)); } } function createDetectionMethod(propertyName) { return function() { ensure.signature(arguments, []); ensure.that( features !== null, "Must call quixote.createFrame() before using Quixote browser feature detection." ); return features[propertyName]; }; } function detectFrameEnlargement(frame, frameWidth) { frame.reset(); frame.add("<div style='width: " + (frameWidth + 200) + "px'>force scrolling</div>"); return !frame.viewport().width.value().equals(Size.create(frameWidth)); } function detectReportedClipAuto(frame) { frame.reset(); var element = frame.add("<div style='clip: auto;'></div>"); var clip = element.getRawStyle("clip"); return clip !== "auto"; } function detectReportedClipPropertyValues(frame) { frame.reset(); var element = frame.add("<div style='clip: rect(auto, auto, auto, auto);'></div>"); var clip = element.getRawStyle("clip"); // WORKAROUND IE 8: Provides 'clipTop' etc. instead of 'clip' property if (clip === "" && element.getRawStyle("clip-top") === "auto") return false; return clip !== "rect(auto, auto, auto, auto)" && clip !== "rect(auto auto auto auto)"; } function detectRoundsOffPixelCalculations(frame) { var element = frame.add("<div style='font-size: 15px;'></div>"); var size = element.calculatePixelValue("0.5em"); if (size === 7.5) return false; if (size === 8) return true; ensure.unreachable("Failure in roundsOffPixelValues() detection: expected 7.5 or 8, but got " + size); } function detectFontEnlargement(frame, frameWidth, callback) { ensure.that(frameWidth >= 1500, "Detector frame width must be larger than screen to detect font enlargement"); frame.reset(); // WORKAROUND IE 8: we use a <div> because the <style> tag can't be added by frame.add(). At the time of this // writing, I'm not sure if the issue is with frame.add() or if IE just can't programmatically add <style> tags. frame.add("<div><style>p { font-size: 15px; }</style></div>"); var text = frame.add("<p>arbitrary text</p>"); frame.add("<p>must have two p tags to work</p>"); // WORKAROUND IE 8: need to force reflow or getting font-size may fail below // This seems to occur when IE is running in a slow VirtualBox VM. There is no test for this line. frame.forceReflow(); // WORKAROUND Safari 8.0.0: timeout required because font is enlarged asynchronously setTimeout(function() { var fontSize = text.getRawStyle("font-size"); ensure.that(fontSize !== "", "Expected font-size to be a value"); // WORKAROUND IE 8: ignores <style> tag we added above if (fontSize === "12pt") return callback(false); return callback(fontSize !== "15px"); }, 0); } },{"./q_frame.js":22,"./util/ensure.js":26,"./values/size.js":32}],5:[function(require,module,exports){ // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("./util/ensure.js"); var shim = require("./util/shim.js"); var QElement = require("./q_element.js"); var QElementList = require("./q_element_list.js"); var QViewport = require("./q_viewport.js"); var QPage = require("./q_page.js"); var Me = module.exports = function BrowsingContext(contentDocument) { ensure.signature(arguments, [Object]); this.contentWindow = contentDocument.defaultView || contentDocument.parentWindow; this.contentDocument = contentDocument; }; Me.prototype.body = function body() { ensure.signature(arguments, []); return QElement.create(this.contentDocument.body, "<body>"); }; Me.prototype.viewport = function viewport() { ensure.signature(arguments, []); return new QViewport(this); }; Me.prototype.page = function page() { ensure.signature(arguments, []); return new QPage(this); }; Me.prototype.add = function add(html, nickname) { ensure.signature(arguments, [String, [undefined, String]]); return this.body().add(html, nickname); }; Me.prototype.get = function get(selector, nickname) { ensure.signature(arguments, [String, [undefined, String]]); if (nickname === undefined) nickname = selector; var nodes = this.contentDocument.querySelectorAll(selector); ensure.that(nodes.length === 1, "Expected one element to match '" + selector + "', but found " + nodes.length); return QElement.create(nodes[0], nickname); }; Me.prototype.getAll = function getAll(selector, nickname) { ensure.signature(arguments, [String, [undefined, String]]); if (nickname === undefined) nickname = selector; return new QElementList(this.contentDocument.querySelectorAll(selector), nickname); }; Me.prototype.scroll = function scroll(x, y) { ensure.signature(arguments, [Number, Number]); this.contentWindow.scroll(x, y); }; Me.prototype.getRawScrollPosition = function getRawScrollPosition() { ensure.signature(arguments, []); return { x: shim.Window.pageXOffset(this.contentWindow, this.contentDocument), y: shim.Window.pageYOffset(this.contentWindow, this.contentDocument) }; }; // This method is not tested--don't know how. Me.prototype.forceReflow = function forceReflow() { this.body().toDomElement().offsetTop; }; Me.prototype.equals = function equals(that) { ensure.signature(arguments, [Me]); return this.contentWindow === that.contentWindow; }; },{"./q_element.js":20,"./q_element_list.js":21,"./q_page.js":23,"./q_viewport.js":24,"./util/ensure.js":26,"./util/shim.js":28}],6:[function(require,module,exports){ // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var PositionDescriptor = require("./position_descriptor.js"); var Position = require("../values/position.js"); var X_DIMENSION = "x"; var Y_DIMENSION = "y"; var Me = module.exports = function AbsolutePosition(dimension, value) { ensure.signature(arguments, [ String, Number ]); this.should = this.createShould(); switch(dimension) { case X_DIMENSION: PositionDescriptor.x(this); this._value = Position.x(value); break; case Y_DIMENSION: PositionDescriptor.y(this); this._value = Position.y(value); break; default: ensure.unreachable("Unknown dimension: " + dimension); } this._dimension = dimension; }; PositionDescriptor.extend(Me); Me.x = function(value) { ensure.signature(arguments, [ Number ]); return new Me(X_DIMENSION, value); }; Me.y = function(value) { ensure.signature(arguments, [ Number ]); return new Me(Y_DIMENSION, value); }; Me.prototype.value = function() { return this._value; }; Me.prototype.toString = function() { return this._value + " " + this._dimension + "-coordinate"; }; },{"../util/ensure.js":26,"../values/position.js":30,"./position_descriptor.js":13}],7:[function(require,module,exports){ // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var PositionDescriptor = require("./position_descriptor.js"); var X_DIMENSION = "x"; var Y_DIMENSION = "y"; var Me = module.exports = function Center(dimension, position1, position2, description) { ensure.signature(arguments, [ String, PositionDescriptor, PositionDescriptor, String ]); this.should = this.createShould(); if (dimension === X_DIMENSION) PositionDescriptor.x(this); else if (dimension === Y_DIMENSION) PositionDescriptor.y(this); else ensure.unreachable("Unknown dimension: " + dimension); this._dimension = dimension; this._position1 = position1; this._position2 = position2; this._description = description; }; PositionDescriptor.extend(Me); Me.x = factoryFn(X_DIMENSION); Me.y = factoryFn(Y_DIMENSION); Me.prototype.value = function value() { ensure.signature(arguments, []); return this._position1.value().midpoint(this._position2.value()); }; Me.prototype.toString = function toString() { ensure.signature(arguments, []); return this._description; }; function factoryFn(dimension) { return function(position1, position2, description) { return new Me(dimension, position1, position2, description); }; } },{"../util/ensure.js":26,"./position_descriptor.js":13}],8:[function(require,module,exports){ // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var oop = require("../util/oop.js"); var Value = require("../values/value.js"); var Me = module.exports = function Descriptor() { ensure.unreachable("Descriptor is abstract and should not be constructed directly."); }; Me.extend = oop.extendFn(Me); oop.makeAbstract(Me, [ "value", "toString" ]); // WORKAROUND IE 8: Doesn't support Object.defineProperty(), which would allow us to create Me.prototype.should // directly on this class as an accessor method. // WORKAROUND IE 11: Doesn't support ES6 'class' syntax, which would allow us to use getter methods and inheritance. Me.prototype.createShould = function createAssert() { var self = this; return { equal: function(expected, message) { self.doAssertion(expected, message, function(actualValue, expectedValue, expectedDesc, message) { if (!actualValue.equals(expectedValue)) { return message + self + " should be " + expectedValue.diff(actualValue) + ".\n" + " Expected: " + expectedDesc + "\n" + " But was: " + actualValue; } }); }, notEqual: function(expected, message) { self.doAssertion(expected, message, function(actualValue, expectedValue, expectedDesc, message) { if (actualValue.equals(expectedValue)) { return message + self + " shouldn't be " + expectedValue + ".\n" + " Expected: anything but " + expectedDesc + "\n" + " But was: " + actualValue; } }); }, }; }; Me.prototype.doAssertion = function doAssertion(expected, message, assertFn) { message = message === undefined ? "" : message + ": "; expected = convertPrimitiveExpectationToValueObjectIfNeeded(this, expected, message); var actualValue; var expectedValue; try { actualValue = this.value(); expectedValue = expected.value(); } catch (err) { throw new Error( message + "Error in test. Unable to convert descriptors to values.\n" + "Error message: " + err.message + "\n" + " 'actual' descriptor: " + this + " (" + oop.instanceName(this) + ")\n" + " 'expected' descriptor: " + expected + " (" + oop.instanceName(expected) + ")\n" + "If this error is unclear or you think Quixote is at fault, please open\n" + "an issue at https://github.com/jamesshore/quixote/issues. Include this\n" + "error message and a standalone example test that reproduces the error.\n" + "Error stack trace:\n" + err.stack ); } if (!actualValue.isCompatibleWith(expectedValue)) { throwBadExpectation( this, oop.instanceName(expected) + " (" + expected + ")", message, "Attempted to compare two incompatible types:" ); } var expectedDesc = expectedValue.toString(); if (expected instanceof Me) expectedDesc += " (" + expected + ")"; var failure; try { failure = assertFn(actualValue, expectedValue, expectedDesc, message); } catch (err2) { throw new Error( message + "Error in test. Unable to perform assertion.\n" + "Error message: " + err2.message + "\n" + " 'actual' descriptor: " + this + " (" + oop.instanceName(this) + ")\n" + " 'expected' descriptor: " + expected + " (" + oop.instanceName(expected) + ")\n" + " 'actual' value: " + actualValue + " (" + oop.instanceName(actualValue) + ")\n" + " 'expected' value: " + expectedValue + " (" + oop.instanceName(expectedValue) + ")\n" + "If this error is unclear or you think Quixote is at fault, please open\n" + "an issue at https://github.com/jamesshore/quixote/issues. Include this\n" + "error message and a standalone example test that reproduces the error.\n" ); } if (failure !== undefined) throw new Error(failure); }; Me.prototype.diff = function diff(expected) { // Legacy code, strictly for compatibility with deprecated Assertable.equals() and Assertable.diff() methods. // It's weird because we moved to should.equals(), which always throws an exception, but diff returns a string. // To avoid duplicating complex logic, we call should.equals() and then unwrap the exception, but only if it's // the right kind of exception. try { this.should.equal(expected); return ""; } catch (err) { var message = err.message; if (message.indexOf("But was:") === -1) throw err; // it's not an assertion error, it's some other exception return message; } }; Me.prototype.convert = function convert(arg, type) { // This method is meant to be overridden by subclasses. It should return 'undefined' when an argument // can't be converted. In this default implementation, no arguments can be converted, so we always // return 'undefined'. return undefined; }; Me.prototype.equals = function equals(that) { // Descriptors aren't value objects. They're never equal to anything. But sometimes // they're used in the same places value objects are used, and then this method gets called. return false; }; function convertPrimitiveExpectationToValueObjectIfNeeded(self, expected, message) { var expectedType = typeof expected; if (expected === null) expectedType = "null"; if (expectedType === "object" && (expected instanceof Me)) return expected; if (expected === undefined) { throwBadExpectation( self, "undefined", message, "The 'expected' parameter is undefined. Did you misspell a property name?" ); } else if (expectedType === "object") { throwBadExpectation( self, oop.instanceName(expected), message, "The 'expected' parameter should be a descriptor, but it wasn't recognized." ); } else { var converted = self.convert(expected, expectedType); if (converted !== undefined) return converted; throwBadExpectation( self, expectedType, message, "The 'expected' primitive isn't equivalent to the 'actual' descriptor." ); } } function throwBadExpectation(self, expectedType, message, headline) { throw new Error( message + "Error in test. Use a different 'expected' parameter.\n" + headline + "\n" + " 'actual' type: " + oop.instanceName(self) + " (" + self + ")\n" + " 'expected' type: " + expectedType ); } },{"../util/ensure.js":26,"../util/oop.js":27,"../values/value.js":33}],9:[function(require,module,exports){ // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var Position = require("../values/position.js"); var PositionDescriptor = require("./position_descriptor.js"); var TOP = "top"; var RIGHT = "right"; var BOTTOM = "bottom"; var LEFT = "left"; var Me = module.exports = function ElementEdge(element, position) { var QElement = require("../q_element.js"); // break circular dependency ensure.signature(arguments, [QElement, String]); this.should = this.createShould(); if (position === LEFT || position === RIGHT) PositionDescriptor.x(this); else if (position === TOP || position === BOTTOM) PositionDescriptor.y(this); else ensure.unreachable("Unknown position: " + position); this._element = element; this._position = position; }; PositionDescriptor.extend(Me); Me.top = factoryFn(TOP); Me.right = factoryFn(RIGHT); Me.bottom = factoryFn(BOTTOM); Me.left = factoryFn(LEFT); Me.prototype.value = function value() { ensure.signature(arguments, []); var rawPosition = this._element.getRawPosition(); var edge = rawPosition[this._position]; var scroll = this._element.context().getRawScrollPosition(); var rendered = elementRendered(this._element); if (this._position === RIGHT || this._position === LEFT) { if (!rendered) return Position.noX(); return Position.x(edge + scroll.x); } else { if (!rendered) return Position.noY(); return Position.y(edge + scroll.y); } }; Me.prototype.toString = function toString() { ensure.signature(arguments, []); return this._position + " edge of " + this._element; }; function factoryFn(position) { return function factory(element) { return new Me(element, position); }; } function elementRendered(element) { var inDom = element.context().body().contains(element); var displayNone = element.getRawStyle("display") === "none"; return inDom && !displayNone; } },{"../q_element.js":20,"../util/ensure.js":26,"../values/position.js":30,"./position_descriptor.js":13}],10:[function(require,module,exports){ // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var RenderState = require("../values/render_state.js"); var Position = require("../values/position.js"); var Descriptor = require("./descriptor.js"); var ElementRenderEdge = require("./element_render_edge.js"); var Span = require("./span.js"); var Center = require("./center.js"); var Me = module.exports = function ElementRender(element) { var QElement = require("../q_element.js"); // break circular dependency ensure.signature(arguments, [ QElement ]); this.should = this.createShould(); this._element = element; // properties this.top = ElementRenderEdge.top(element); this.right = ElementRenderEdge.right(element); this.bottom = ElementRenderEdge.bottom(element); this.left = ElementRenderEdge.left(element); this.width = Span.create(this.left, this.right, "rendered width of " + element); this.height = Span.create(this.top, this.bottom, "rendered height of " + element); this.center = Center.x(this.left, this.right, "rendered center of " + element); this.middle = Center.y(this.top, this.bottom, "rendered middle of " + element); }; Descriptor.extend(Me); Me.create = function create(element) { return new Me(element); }; Me.prototype.value = function value() { if (this.top.value().equals(Position.noY())) return RenderState.notRendered(); else return RenderState.rendered(); }; Me.prototype.toString = function toString() { return this._element.toString() + " rendering"; }; Me.prototype.convert = function convert(arg, type) { if (type === "boolean") { return arg ? RenderState.rendered() : RenderState.notRendered(); } }; },{"../q_element.js":20,"../util/ensure.js":26,"../values/position.js":30,"../values/render_state.js":31,"./center.js":7,"./descriptor.js":8,"./element_render_edge.js":11,"./span.js":18}],11:[function(require,module,exports){ // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var quixote = require("../quixote.js"); var PositionDescriptor = require("./position_descriptor.js"); var Position = require("../values/position.js"); var QPage = require("../q_page.js"); var Size = require("../values/size.js"); var TOP = "top"; var RIGHT = "right"; var BOTTOM = "bottom"; var LEFT = "left"; var Me = module.exports = function ElementVisibleEdge(element, position) { var QElement = require("../q_element.js"); // break circular dependency ensure.signature(arguments, [ QElement, String ]); this.should = this.createShould(); if (position === LEFT || position === RIGHT) PositionDescriptor.x(this); else if (position === TOP || position === BOTTOM) PositionDescriptor.y(this); else unknownPosition(position); this._element = element; this._position = position; }; PositionDescriptor.extend(Me); Me.top = factoryFn(TOP); Me.right = factoryFn(RIGHT); Me.bottom = factoryFn(BOTTOM); Me.left = factoryFn(LEFT); function factoryFn(position) { return function factory(element) { return new Me(element, position); }; } Me.prototype.toString = function toString() { ensure.signature(arguments, []); return this._position + " rendered edge of " + this._element; }; Me.prototype.value = function() { var position = this._position; var element = this._element; var page = element.context().page(); if (element.top.value().equals(Position.noY())) return notRendered(position); if (element.width.value().equals(Size.create(0))) return notRendered(position); if (element.height.value().equals(Size.create(0))) return notRendered(position); ensure.that( !hasClipPathProperty(element), "Can't determine element rendering because the element is affected by the 'clip-path' property, " + "which Quixote doesn't support." ); var bounds = { top: page.top.value(), right: null, bottom: null, left: page.left.value() }; bounds = intersectionWithOverflow(element, bounds); bounds = intersectionWithClip(element, bounds); var edges = intersection( bounds, element.top.value(), element.right.value(), element.bottom.value(), element.left.value() ); if (isClippedOutOfExistence(bounds, edges)) return notRendered(position); else return edge(edges, position); }; function hasClipPathProperty(element) { var clipPath = element.getRawStyle("clip-path"); return clipPath !== "none" && clipPath !== ""; } function intersectionWithOverflow(element, bounds) { for (var container = element.parent(); container !== null; container = container.parent()) { if (isClippedByAncestorOverflow(element, container)) { bounds = intersection( bounds, container.top.value(), container.right.value(), container.bottom.value(), container.left.value() ); } } return bounds; } function intersectionWithClip(element, bounds) { // WORKAROUND IE 8: Doesn't have any way to detect 'clip: auto' value. ensure.that(!quixote.browser.misreportsClipAutoProperty(), "Can't determine element rendering on this browser because it misreports the value of the" + " `clip: auto` property. You can use `quixote.browser.misreportsClipAutoProperty()` to skip this browser." ); for ( ; element !== null; element = element.parent()) { var clip = element.getRawStyle("clip"); if (clip === "auto" || !canBeClippedByClipProperty(element)) continue; var clipEdges = normalizeClipProperty(element, clip); bounds = intersection( bounds, clipEdges.top, clipEdges.right, clipEdges.bottom, clipEdges.left ); } return bounds; } function normalizeClipProperty(element, clip) { var clipValues = parseClipProperty(element, clip); return { top: clipValues[0] === "auto" ? element.top.value() : element.top.value().plus(Position.y(Number(clipValues[0]))), right: clipValues[1] === "auto" ? element.right.value() : element.left.value().plus(Position.x(Number(clipValues[1]))), bottom: clipValues[2] === "auto" ? element.bottom.value() : element.top.value().plus(Position.y(Number(clipValues[2]))), left: clipValues[3] === "auto" ? element.left.value() : element.left.value().plus(Position.x(Number(clipValues[3]))) }; function parseClipProperty(element, clip) { // WORKAROUND IE 11, Chrome Mobile 44: Reports 0px instead of 'auto' when computing rect() in clip property. ensure.that(!quixote.browser.misreportsAutoValuesInClipProperty(), "Can't determine element rendering on this browser because it misreports the value of the `clip`" + " property. You can use `quixote.browser.misreportsAutoValuesInClipProperty()` to skip this browser." ); var clipRegex = /rect\((.*?),? (.*?),? (.*?),? (.*?)\)/; var matches = clipRegex.exec(clip); ensure.that(matches !== null, "Unable to parse clip property: " + clip); return [ parseLength(matches[1], clip), parseLength(matches[2], clip), parseLength(matches[3], clip), parseLength(matches[4], clip) ]; } function parseLength(pxString, clip) { if (pxString === "auto") return pxString; var pxRegex = /^(.*?)px$/; var matches = pxRegex.exec(pxString); ensure.that(matches !== null, "Unable to parse '" + pxString + "' in clip property: " + clip); return matches[1]; } } function isClippedByAncestorOverflow(element, ancestor) { return canBeClippedByOverflowProperty(element) && hasClippingOverflow(ancestor); } function canBeClippedByOverflowProperty(element) { var position = element.getRawStyle("position"); switch (position) { case "static": case "relative": case "absolute": case "sticky": return true; case "fixed": return false; default: ensure.unreachable("Unknown position property: " + position); } } function hasClippingOverflow(element) { return clips("overflow-x") || clips("overflow-y"); function clips(style) { var overflow = element.getRawStyle(style); switch (overflow) { case "hidden": case "scroll": case "auto": return true; case "visible": return false; default: ensure.unreachable("Unknown " + style + " property: " + overflow); } } } function canBeClippedByClipProperty(element) { var position = element.getRawStyle("position"); switch (position) { case "absolute": case "fixed": return true; case "static": case "relative": case "sticky": return false; default: ensure.unreachable("Unknown position property: " + position); } } function intersection(bounds, top, right, bottom, left) { bounds.top = bounds.top.max(top); bounds.right = (bounds.right === null) ? right : bounds.right.min(right); bounds.bottom = (bounds.bottom === null) ? bottom : bounds.bottom.min(bottom); bounds.left = bounds.left.max(left); return bounds; } function isClippedOutOfExistence(bounds, edges) { return (bounds.top.compare(edges.bottom) >= 0) || (bounds.right !== null && bounds.right.compare(edges.left) <= 0) || (bounds.bottom !== null && bounds.bottom.compare(edges.top) <= 0) || (bounds.left.compare(edges.right) >= 0); } function notRendered(position) { switch(position) { case TOP: case BOTTOM: return Position.noY(); case LEFT: case RIGHT: return Position.noX(); default: unknownPosition(position); } } function edge(edges, position) { switch(position) { case TOP: return edges.top; case RIGHT: return edges.right; case BOTTOM: return edges.bottom; case LEFT: return edges.left; default: unknownPosition(position); } } function unknownPosition(position) { ensure.unreachable("Unknown position: " + position); } },{"../q_element.js":20,"../q_page.js":23,"../quixote.js":25,"../util/ensure.js":26,"../values/position.js":30,"../values/size.js":32,"./position_descriptor.js":13}],12:[function(require,module,exports){ // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. "use strict"; var ensure = require("../util/ensure.js"); var PositionDescriptor = require("./position_descriptor.js"); var Position = require("../values/position.js"); var TOP = "top"; var RIGHT = "right"; var BOTTOM = "bottom"; var LEFT = "left"; var Me = module.exports = function PageEdge(edge, browsingContext) { var BrowsingContext = require("../browsing_context.js"); // break circular dependency ensure.signature(arguments, [ String, BrowsingContext ]); this.should = this.createShould(); if (edge === LEFT || edge === RIGHT) PositionDescriptor.x(this); else if (edge === TOP || edge === BOTTOM) PositionDescriptor.y(this); else ensure.unreachable("Unknown edge: " + edge); this._edge = edge; this._browsingContext = browsingContext; }; PositionDescriptor.extend(Me); Me.top = factoryFn(TOP); Me.right = factoryFn(RIGHT); Me.bottom = factoryFn(BOTTOM); Me.left = factoryFn(LEFT); Me.prototype.value = function value() { ensure.signature(arguments, []); var size = pageSize(this._browsingContext.contentDocument); switch(this._edge) { case TOP: return Position.y(0); case RIGHT: return Position.x(size.width); case BOTTOM: return Position.y(size.height); case LEFT: return Position.x(0); default: ensure.unreachable(); } }; Me.prototype.toString = function toString() { ensure.signature(arguments, []); switch(this._edge) { case TOP: return "top of page"; case RIGHT: return "right side of page"; case BOTTOM: return "bottom of page"; case LEFT: return "left side of page"; default: ensure.unreachable(); } }; function factoryFn(edge) { return function factory(browsingContext) { return new Me(edge, browsingContext); }; } // USEFUL READING: http://www.quirksmode.org/mobile/viewports.html // and http://www.quirksmode.org/mobile/viewports2.html // API SEMANTICS. // Ref https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements // getBoundingClientRect().width: sum of bounding boxes of element (the displayed width of the element, // including padding and border). Fractional. Applies transformations. // clientWidth: visible width of element including padding (but not border). EXCEPT on root element (html), where // it is the width of the viewport. Rounds to an integer. Doesn't apply transformations. // offsetWidth: visible width of element including padding, border, and scrollbars (if any). Rounds to an integer. // Doesn't apply transformations. // scrollWidth: entire width of element, including any part that's not visible due to scrollbars. Rounds to // an integer. Doesn't apply transformations. Not clear if it includes scrollbars, but I think not. Also // not clear if it includes borders or padding. (But from tests, apparently not borders. Except on root // element and body element, which have special results that vary by browser.) // TEST RESULTS: WIDTH // ✔ = correct answer // ✘ = incorrect answer and diverges from spec // ~ = incorrect answer, but matches spec // BROWSERS TESTED: Safari 6.2.0 (Mac OS X 10.8.5); Mobile Safari 7.0.0 (iOS 7.1); Firefox 32.0.0 (Mac OS X 10.8); // Firefox 33.0.0 (Windows 7); Chrome 38.0.2125 (Mac OS X 10.8.5); Chrome 38.0.2125 (Windows 7); IE 8, 9, 10, 11 // html width style smaller than viewport width; body width style smaller than html width style // NOTE: These tests were conducted when correct result was width of border. That has been changed // to "width of viewport." // html.getBoundingClientRect().width // ✘ IE 8, 9, 10: width of viewport // ✔ Safari, Mobile Safari, Chrome, Firefox, IE 11: width of html, including border // html.clientWidth // ~ Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11: width of viewport // html.offsetWidth // ✘ IE 8, 9, 10: width of viewport // ✔ Safari, Mobile Safari, Chrome, Firefox, IE 11: width of html, including border // html.scrollWidth // ✘ IE 8, 9, 10, 11, Firefox: width of viewport // ~ Safari, Mobile Safari, Chrome: width of html, excluding border // body.getBoundingClientRect().width // ~ Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11: width of body, including border // body.clientWidth // ~ Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11: width of body, excluding border // body.offsetWidth // ~ Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11: width of body, including border // body.scrollWidth // ✘ Safari, Mobile Safari, Chrome: width of viewport // ~ Firefox, IE 8, 9, 10, 11: width of body, excluding border // element width style wider than viewport; body and html width styles at default // BROWSER BEHAVIOR: html and body border extend to width of viewport and not beyond (except on Mobile Safari) // Correct result is element width + body border-left + html border-left (except on Mobile Safari) // Mobile Safari uses a layout viewport, so it's expected to include body border-right and html border-right. // html.getBoundingClientRect().width // ✔ Mobile Safari: element width + body border + html border // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width // html.clientWidth // ✔ Mobile Safari: element width + body border + html border // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width // html.offsetWidth // ✔ Mobile Safari: element width + body border + html border // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width // html.scrollWidth // ✔ Mobile Safari: element width + body border + html border // ✘ Safari, Chrome: element width + body border-left (BUT NOT html border-left) // ✔ Firefox, IE 8, 9, 10, 11: element width + body border-left + html border-left // body.getBoundingClientRect().width // ~ Mobile Safari: element width + body border // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width - html border // body.clientWidth // ~ Mobile Safari: element width // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width - html border - body border // body.offsetWidth // ~ Mobile Safari: element width + body border // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: viewport width - html border // body.scrollWidth // ✔ Mobile Safari: element width + body border + html border // ✔ Safari, Chrome: element width + body border-left + html border-left (matches actual browser) // ~ Firefox, IE 8, 9, 10, 11: element width // TEST RESULTS: HEIGHT // ✔ = correct answer // ✘ = incorrect answer and diverges from spec // ~ = incorrect answer, but matches spec // html height style smaller than viewport height; body height style smaller than html height style // NOTE: These tests were conducted when correct result was height of viewport. // html.clientHeight // ✔ Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11: height of viewport // element height style taller than viewport; body and html width styles at default // BROWSER BEHAVIOR: html and body border enclose entire element // Correct result is element width + body border-top + html border-top + body border-bottom + html border-bottom // html.clientHeight // ✔ Mobile Safari: element height + all borders // ~ Safari, Chrome, Firefox, IE 8, 9, 10, 11: height of viewport // html.scrollHeight // ✔ Firefox, IE 8, 9, 10, 11: element height + all borders // ✘ Safari, Mobile Safari, Chrome: element height + html border-bottom // body.scrollHeight // ✔ Safari, Mobile Safari, Chrome: element height + all borders // ~ Firefox, IE 8, 9, 10, 11: element height (body height - body border) function pageSize(document) { var html = document.documentElement; var body = document.body; // BEST WIDTH ANSWER SO FAR (ASSUMING VIEWPORT IS MINIMUM ANSWER): var width = Math.max(body.scrollWidth, html.scrollWidth); // BEST HEIGHT ANSWER SO FAR (ASSUMING VIEWPORT IS MINIMUM ANSWER): var height = Math.max(body.scrollHeight, html.scrollHeight); return { width: width, height: height }; } },{"../browsing_context.js":5,"../util/ensure.js":26,"../values/position.js":30,"./position_descriptor.js":13}],13:[function(require,module,exports){ // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. /*eslint new-cap: "off" */ "use strict"; var ensure = require("../util/ensure.js"); var oop = require("../util/oop.js"); var Descriptor = require("./descriptor.js"); var Position = require("../values/position.js"); // break circular dependencies function RelativePosition() { return require("./relative_position.js"); } function AbsolutePosition() { return require("./absolute_position.js"); } function Span() { return require("./span.js"); } var X_DIMENSION = "X"; var Y_DIMENSION = "Y"; var Me = module.exports = function PositionDescriptor(dimension) { ensure.signature(arguments, [ String ]); ensure.unreachable("PositionDescriptor is abstract and should not be constructed directly."); }; Descriptor.extend(Me); Me.extend = oop.extendFn(Me); function factoryFn(dimension) { return function factory(self) { // _pdbc: "PositionDescriptor base class." An attempt to prevent name conflicts. self._pdbc = { dimension: dimension }; }; } Me.x = factoryFn(X_DIMENSION); Me.y = factoryFn(Y_DIMENSION); Me.prototype.createShould = function() { var self = this; var noX = Position.noX(); var noY = Position.noY(); var should = Descriptor.prototype.createShould.call(this); should.beAbove = assertFn("beAbove", "beLeftOf", Y_DIMENSION, false); should.beBelow = assertFn("beBelow", "beRightOf", Y_DIMENSION, true); should.beLeftOf = assertFn("beLeftOf", "beAbove", X_DIMENSION, false); should.beRightOf = assertFn("beRightOf", "beBelow", X_DIMENSION, true); return should; function assertFn(functionName, otherAxisName, dimension, shouldBeBigger) { return function(expected, message) { self.doAssertion(expected, message, function(actualValue, expectedValue, expectedDesc, message) { if (self._pdbc.dimension !== dimension) { throwCoordinateError(functionName, otherAxisName); } if (expectedValue.isNone()) { throw new Error("'expected' value is not rendered, so relative comparisons aren't possible."); } var expectedMsg = (shouldBeBigger ? "more than" : "less than") + " " + expectedDesc; if (actualValue.isNone()) { return errorMessage(message, "rendered", expectedMsg, actualValue); } var compare = actualValue.compare(expectedValue); if ((shouldBeBigger && compare <= 0) || (!shouldBeBigger && compare >= 0)) { var nudge = shouldBeBigger ? -1 : 1; var shouldBe = "at least " + expectedValue.diff(self.plus(nudge).value()); return errorMessage(message, shouldBe, expectedMsg, actualValue); } }); }; } function throwCoordinateError(functionName, recommendedName) { throw new Error( "Can't use 'should." + functionName + "()' on " + self._pdbc.dimension + " coordinates. Did you mean 'should." + recommendedName + "()'?" ); } function errorMessage(message, shouldBe, expected, actual) { return message + self + " should be " + shouldBe + ".\n" + " Expected: " + expected + "\n" + " But was: " + actual; } }; Me.prototype.plus = function(amount) { if (this._pdbc.dimension === X_DIMENSION) return RelativePosition().right(this, amount); else return RelativePosition().down(this, amount); }; Me.prototype.minus = function(amount) { if (this._pdbc.dimension === X_DIMENSION) return RelativePosition().left(this, amount); else return RelativePosition().up(this, amount); }; Me.prototype.to = function(position, nickname) { ensure.signature(arguments, [[ Me, Number ], [ undefined, String ]]); if (typeof position === "number") { if (this._pdbc.dimension === X_DIMENSION) position = AbsolutePosition().x(position); else position = AbsolutePosition().y(position); } if (this._pdbc.dimension !== position._pdbc