quixote
Version:
CSS unit and integration testing
1,466 lines (1,221 loc) • 376 kB
JavaScript
(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