├── example ├── watch.bat ├── .gitignore ├── watch.sh ├── jake.bat ├── jake.sh ├── video_poster.jpg ├── build │ ├── scripts │ │ ├── run_jake.bat │ │ ├── run_jake.sh │ │ ├── watch.js │ │ └── build.jakefile.js │ ├── config │ │ ├── build_command.js │ │ ├── paths.js │ │ ├── tested_browsers.js │ │ ├── jshint.conf.js │ │ └── karma.conf.js │ └── util │ │ ├── browserify_runner.js │ │ ├── version_checker.js │ │ ├── sh.js │ │ ├── mocha_runner.js │ │ └── karma_runner.js ├── src │ ├── toggle.js │ ├── screen.css │ ├── assert.js │ ├── icon.svg │ ├── index.html │ └── _toggle_test.js ├── package.json ├── LICENSE.txt └── readme.md ├── .gitignore ├── src ├── _q_frame_test2.css ├── __reset.css ├── _q_frame_test.css ├── _q_frame_test.html ├── quixote.js ├── __reset.js ├── _quixote_test.js ├── values │ ├── render_state.js │ ├── _render_state_test.js │ ├── value.js │ ├── size.js │ ├── _size_test.js │ ├── _value_test.js │ ├── pixels.js │ ├── position.js │ └── _position_test.js ├── q_page.js ├── descriptors │ ├── span.js │ ├── _absolute_position_test.js │ ├── absolute_position.js │ ├── center.js │ ├── element_render.js │ ├── relative_size.js │ ├── _center_test.js │ ├── size_multiple.js │ ├── element_edge.js │ ├── relative_position.js │ ├── _size_multiple_test.js │ ├── _relative_size_test.js │ ├── size_descriptor.js │ ├── _span_test.js │ ├── viewport_edge.js │ ├── _relative_position_test.js │ ├── _element_edge_test.js │ ├── _element_render_test.js │ ├── position_descriptor.js │ └── _size_descriptor_test.js ├── q_viewport.js ├── q_element_list.js ├── assertable.js ├── util │ ├── oop.js │ ├── _oop_test.js │ └── ensure.js ├── _q_viewport_test.js ├── _q_page_test.js ├── _browser_test.js ├── _q_element_list_test.js ├── _assertable_test.js ├── browsing_context.js ├── browser.js ├── _browsing_context_test.js └── q_element.js ├── jake.sh ├── release.sh ├── integrate.sh ├── docs ├── README.md ├── index.html ├── QElementList.md ├── ElementRender.md ├── QPage.md ├── QViewport.md ├── api.md ├── Span.md ├── SizeDescriptor.md ├── PositionDescriptor.md └── quixote.md ├── test ├── README.md └── _assertion_test.js ├── spikes ├── ie8_iframe_quirks │ ├── index.html │ ├── inner.html │ └── README.md ├── ios_frame_scrolling │ ├── README.md │ ├── inner.html │ └── index.html ├── ios_text_sizing │ ├── index.html │ ├── inner.html │ └── README.md └── ios_frame_sizing │ ├── index.html │ ├── inner.html │ └── README.md ├── vendor └── camelcase-1.0.1-modified.js ├── package.json ├── ROADMAP.md ├── LICENSE.txt └── watch.js /example/watch.bat: -------------------------------------------------------------------------------- 1 | @node build/scripts/watch.js %* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | generated/ -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | generated/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /src/_q_frame_test2.css: -------------------------------------------------------------------------------- 1 | .style-me { 2 | height: 123px; 3 | } -------------------------------------------------------------------------------- /example/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node build/scripts/watch.js $* -------------------------------------------------------------------------------- /example/jake.bat: -------------------------------------------------------------------------------- 1 | @call build\scripts\run_jake -f build\scripts\build.jakefile.js %* 2 | -------------------------------------------------------------------------------- /src/__reset.css: -------------------------------------------------------------------------------- 1 | body, p, div { 2 | margin: 0; 3 | border: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /jake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . build/scripts/run_jake.sh -f build/scripts/build.jakefile.js $* 3 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . build/scripts/run_jake.sh -f build/scripts/release.jakefile.js $* 3 | -------------------------------------------------------------------------------- /src/_q_frame_test.css: -------------------------------------------------------------------------------- 1 | .style-me { 2 | font-size: 42px; 3 | -webkit-text-size-adjust: 100%; 4 | } -------------------------------------------------------------------------------- /example/jake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . build/scripts/run_jake.sh -f build/scripts/build.jakefile.js $* 3 | -------------------------------------------------------------------------------- /example/video_poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/quixote/HEAD/example/video_poster.jpg -------------------------------------------------------------------------------- /integrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . build/scripts/run_jake.sh -f build/scripts/integrate.jakefile.js $* 3 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Quixote Documentation 2 | 3 | * [Quixote Overview](../README.md) 4 | 5 | * [API Documentation](api.md) -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | This directory only contains end-to-end tests. Most testing is done via unit tests, which are in the `src` directory. -------------------------------------------------------------------------------- /spikes/ie8_iframe_quirks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /vendor/camelcase-1.0.1-modified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (str) { 3 | if (str.length === 1) { 4 | return str; 5 | } 6 | 7 | return str 8 | .replace(/^[_.\- ]+/, '') 9 | .toLowerCase() 10 | .replace(/[_.\- ]+(\w|$)/g, function (m, p1) { 11 | return p1.toUpperCase(); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /spikes/ie8_iframe_quirks/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
full width
10 | 11 | 12 | -------------------------------------------------------------------------------- /example/build/scripts/run_jake.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Runs Jake from node_modules directory, preventing it from needing to be installed globally 3 | REM Also ensures node modules have been installed 4 | REM There's no Quixote-specific configuration in this file. 5 | 6 | if not exist node_modules\.bin\jake.cmd call npm install 7 | node_modules\.bin\jake %* -------------------------------------------------------------------------------- /example/build/scripts/run_jake.sh: -------------------------------------------------------------------------------- 1 | # Runs Jake from node_modules directory, preventing it from needing to be installed globally 2 | # Also ensures node modules have been installed 3 | # There's no Quixote-specific configuration in this file. 4 | 5 | [ ! -f node_modules/.bin/jake ] && echo "Installing npm modules:" && npm install 6 | node_modules/.bin/jake $* -------------------------------------------------------------------------------- /src/_q_frame_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 |

If this element exists, then this file was loaded into the frame.

12 | 13 |

If this element is styled, then the stylesheet was loaded using a link tag.

14 | 15 | -------------------------------------------------------------------------------- /example/src/toggle.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // A simple JavaScript library to toggle the visibility of an element. It's tested by `_toggle_test.js`. 4 | // There's nothing related to Quixote in this file. 5 | 6 | 7 | (function() { 8 | "use strict"; 9 | 10 | exports.init = function init(clickMe, element, className) { 11 | clickMe.addEventListener("click", function() { 12 | element.classList.toggle(className); 13 | }); 14 | }; 15 | 16 | }()); -------------------------------------------------------------------------------- /spikes/ios_frame_scrolling/README.md: -------------------------------------------------------------------------------- 1 | These files demonstrate how to make an iframe scrollable on Mobile Safari. 2 | 3 | To use, serve the files from a web server (I like npm's `http-server` for simplicity and convenience), then 4 | visit index.html in Mobile Safari or any other browser. 5 | 6 | Observe: 7 | * The frame has scroll bars. This is not the default for Mobile Safari. (Thanks to David Walsh, http://davidwalsh.name/scroll-iframes-ios, for this part of the solution.) 8 | * Clicking the button scrolls the frame. This requires special code on Mobile Safari. 9 | -------------------------------------------------------------------------------- /spikes/ios_text_sizing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

In this frame, the text is made larger than specified by its stylesheet.

9 | 10 | 11 |

This is the same frame content in a narrower frame. In this one, the text is exactly the size specified by its stylesheet.

12 | 13 | 14 |

Link to inner file (may be needed for Mobile Safari cache-busting)

15 | 16 | 17 | -------------------------------------------------------------------------------- /spikes/ie8_iframe_quirks/README.md: -------------------------------------------------------------------------------- 1 | These files demonstrate an issue with iframes when IE 8 is in "quirks" mode. 2 | 3 | To use, serve the files from a web server (I like npm's `http-server` for simplicity and convenience), then 4 | visit index.html in IE 8 or any other browser. 5 | 6 | Observe: 7 | * The red 'full width' element does not extend the entire width of the frame in IE 8. 8 | 9 | Then uncomment the DOCTYPE at the top of inner.html. This turns off "quirks" mode. 10 | 11 | Observe: 12 | * The red 'full width' element now extends the entire width of the frame. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quixote: CSS Unit Testing 6 | 7 | 8 |

Quixote: CSS Unit Testing

9 | 10 |

You can find Quixote documentation on GitHub:

11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/build/config/build_command.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // A cross-platform mechanism for determining how to run the build. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var UNIX_BUILD_COMMAND = "./jake.sh"; 10 | var WINDOWS_BUILD_COMMAND = "jake.bat"; 11 | 12 | var os = require("os"); 13 | 14 | exports.get = function() { 15 | return os.platform() === "win32" ? WINDOWS_BUILD_COMMAND : UNIX_BUILD_COMMAND; 16 | }; 17 | 18 | }()); -------------------------------------------------------------------------------- /spikes/ios_frame_sizing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spikes/ios_frame_scrolling/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 |
Top left
18 |
Under
19 |
Scroll
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/src/screen.css: -------------------------------------------------------------------------------- 1 | /* Our CSS file. 2 | 3 | 4 | /* The media object. We use Quixote to test it in `_media_css_test.js`. */ 5 | 6 | .media:after { 7 | content: ""; 8 | display: table; 9 | clear: both; 10 | } 11 | 12 | .media__figure { 13 | float: left; 14 | margin-right: 10px; 15 | } 16 | 17 | .media__content { 18 | overflow: hidden; 19 | } 20 | 21 | 22 | /* Our example code also includes a simple JavaScript library for toggling the visibility of an element. */ 23 | /* The .invisible class is used in that example. It's not related to the Quixote tests. */ 24 | 25 | .invisible { 26 | visibility: hidden; 27 | } 28 | -------------------------------------------------------------------------------- /example/build/util/browserify_runner.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014 Titanium I.T. LLC - See LICENSE.txt for license */ 2 | 3 | // Helper function for running Browserify 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | "use strict"; 7 | 8 | var fs = require("fs"); 9 | var path = require("path"); 10 | var browserify = require("browserify"); 11 | 12 | exports.bundle = function(config, success, failure) { 13 | var b = browserify(config.options); 14 | 15 | b.add(path.resolve(config.entry)); 16 | b.bundle(function(err, bundle) { 17 | if (err) return failure(err); 18 | fs.writeFileSync(config.outfile, bundle); 19 | return success(); 20 | }); 21 | }; -------------------------------------------------------------------------------- /example/build/config/paths.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // Lists commonly-used directories. They're all relative to the project root. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | module.exports = { 10 | generatedDir: "generated", 11 | testDir: "generated/test", 12 | distDir: "generated/dist", 13 | clientDistDir: "generated/dist/client", 14 | 15 | buildDir: "build", 16 | clientDir: "src", 17 | clientEntryPoint: "src/toggle.js", 18 | clientDistBundle: "generated/dist/client/bundle.js" 19 | }; 20 | 21 | }()); -------------------------------------------------------------------------------- /example/build/config/tested_browsers.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | // Uncomment and modify the following list to cause the build to fail unless these browsers are tested. 6 | // There's no Quixote-specific configuration in this file. 7 | 8 | module.exports = [ 9 | //"IE 10.0.0 (Windows 7 0.0.0)", 10 | //"Firefox 41.0.0 (Mac OS X 10.10.0)", 11 | //"Chrome 46.0.2490 (Mac OS X 10.10.5)", 12 | //"Safari 9.0.1 (Mac OS X 10.10.5)", 13 | //"Mobile Safari 8.0.0 (iOS 8.4.0)", 14 | //"Chrome Mobile 44.0.2403 (Android 6.0.0)" 15 | ]; 16 | 17 | }()); -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quixote-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "http-server": "^0.9.0" 7 | }, 8 | "devDependencies": { 9 | "browserify": "^14.1.0", 10 | "jake": "^8.0.15", 11 | "jshint": "^2.9.4", 12 | "karma": "^1.4.1", 13 | "karma-commonjs": "1.0.0", 14 | "karma-firefox-launcher": "^1.0.0", 15 | "karma-mocha": "^1.3.0", 16 | "mocha": "^3.2.0", 17 | "nodemon": "^1.11.0", 18 | "object-merge": "^2.5.1", 19 | "procfile": "^0.1.1", 20 | "semver": "^5.3.0", 21 | "shelljs": "^0.7.6", 22 | "simplebuild-jshint": "^1.3.0", 23 | "simplebuild-karma": "^1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/quixote.js: -------------------------------------------------------------------------------- 1 | // Copyright Titanium I.T. LLC. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var QElement = require('./q_element.js'); 6 | var QFrame = require("./q_frame.js"); 7 | var browser = require("./browser.js"); 8 | 9 | exports.browser = browser; 10 | 11 | exports.createFrame = function(options, callback) { 12 | return QFrame.create(document.body, options, function(err, callbackFrame) { 13 | if (err) return callback(err); 14 | browser.detectBrowserFeatures(function(err) { 15 | callback(err, callbackFrame); 16 | }); 17 | }); 18 | }; 19 | 20 | exports.elementFromDom = function(domElement, nickname) { 21 | return QElement.create(domElement, nickname); 22 | }; 23 | -------------------------------------------------------------------------------- /spikes/ios_text_sizing/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 |

This is a line of text inside a frame.

17 |

This is another line of text inside a frame.

18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /spikes/ios_text_sizing/README.md: -------------------------------------------------------------------------------- 1 | These files demonstrate how Mobile Safari sizes an iframe. 2 | 3 | To use, serve the files from a web server (I like npm's `http-server` for simplicity and convenience), then 4 | visit index.html in Mobile Safari or any other browser. 5 | 6 | On Mobile Safari, observe: 7 | * The top frame, which extends past the edge of the window, has larger text than the bottom frame, which doesn't. 8 | 9 | 10 | CONCLUSION: 11 | 12 | Mobile Safari will increase the size of text in a frame when the frame extends past the iPhone edge. This behavior can be changed by setting `-webkit-text-size-adjust: 100%` in CSS. Using `-webkit-text-size-adjust: none` will also work, but there are reports that it prevents the text size from being changed in some browsers. 13 | -------------------------------------------------------------------------------- /example/src/assert.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // A small modification to Chai. Why? Just to demonstrate how you can customize an assertion library 4 | // without writing it all yourself. 5 | // There's nothing related to Quixote in this file. 6 | 7 | (function() { 8 | "use strict"; 9 | 10 | var assert = require("../vendor/chai-2.1.0").assert; 11 | 12 | // 'module.exports = assert' doesn't work because it's a shallow copy. Any changes (such as when we 13 | // overwrite exports.fail) changes Chai's functions. In the case of export.fail, it causes an infinite 14 | // loop. Oops. 15 | Object.keys(assert).forEach(function(property) { 16 | exports[property] = assert[property]; 17 | }); 18 | 19 | exports.fail = function(message) { 20 | assert.fail(null, null, message); 21 | }; 22 | 23 | }()); -------------------------------------------------------------------------------- /src/__reset.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | // Set up a simple 'reset stylesheet' test frame for all tests to use. 5 | // Because there's no "describe" block in this file, the 'before' and 'after' run before and after 6 | // the entire test suite, and the 'beforeEach' runs before every test. 7 | // 8 | // This reduces the number of times frames are created and destroyed, which speeds up the tests. 9 | 10 | var quixote = require("./quixote.js"); 11 | 12 | exports.WIDTH = 500; 13 | exports.HEIGHT = 400; 14 | 15 | mocha.timeout(20000); 16 | 17 | before(function(done) { 18 | var options = { 19 | width: exports.WIDTH, 20 | height: exports.HEIGHT, 21 | stylesheet: "/base/src/__reset.css" 22 | }; 23 | 24 | exports.frame = quixote.createFrame(options, done); 25 | }); 26 | 27 | beforeEach(function() { 28 | exports.frame.reset(); 29 | }); -------------------------------------------------------------------------------- /src/_quixote_test.js: -------------------------------------------------------------------------------- 1 | // Copyright Titanium I.T. LLC. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var quixote = require("./quixote.js"); 6 | var QFrame = require("./q_frame.js"); 7 | var reset = require("./__reset.js"); 8 | 9 | describe("FOUNDATION: Quixote", function() { 10 | 11 | it("creates frame", function(done) { 12 | var frame = quixote.createFrame({ src: "/base/src/_q_frame_test.html" }, function(err, callbackFrame) { 13 | assert.noException(function() { 14 | callbackFrame.get("#exists"); 15 | }); 16 | frame.remove(); 17 | done(err); 18 | }); 19 | assert.type(frame, QFrame, "createFrame() returns frame object immediately"); 20 | }); 21 | 22 | it("creates QElement from DOM element", function() { 23 | var domElement = reset.frame.add("
my element
").toDomElement(); 24 | 25 | var element = quixote.elementFromDom(domElement); 26 | assert.equal(element.toDomElement(), domElement); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /spikes/ios_frame_sizing/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | 33 | 34 | 35 |
100% width
36 |
Scroll creator
37 |
@media successful
38 | 39 | 40 | -------------------------------------------------------------------------------- /example/build/scripts/watch.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // Automatically runs build when files change. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var nodemon = require("nodemon"); 10 | var buildCommand = require("../config/build_command.js"); 11 | var paths = require("../config/paths.js"); 12 | 13 | console.log("*** Using nodemon to run " + buildCommand.get() + ". Type 'rs' to force restart."); 14 | nodemon({ 15 | ext: "sh bat json js html css", 16 | ignore: paths.generatedDir, 17 | exec: buildCommand.get() + " " + process.argv.slice(2).join(" "), 18 | execMap: { 19 | sh: "/bin/sh", 20 | bat: "cmd.exe /c", 21 | cmd: "cmd.exe /c" 22 | } 23 | }).on("restart", function(files) { 24 | if (files) console.log("*** Restarting due to", files); 25 | else console.log("*** Restarting"); 26 | }); 27 | 28 | }()); 29 | -------------------------------------------------------------------------------- /example/src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/values/render_state.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Value = require("./value.js"); 6 | 7 | var RENDERED = "rendered"; 8 | var NOT_RENDERED = "not rendered"; 9 | 10 | var Me = module.exports = function RenderState(state) { 11 | ensure.signature(arguments, [ String ]); 12 | 13 | this._state = state; 14 | }; 15 | Value.extend(Me); 16 | 17 | Me.rendered = function rendered() { 18 | return new Me(RENDERED); 19 | }; 20 | 21 | Me.notRendered = function notRendered() { 22 | return new Me(NOT_RENDERED); 23 | }; 24 | 25 | Me.prototype.compatibility = function compatibility() { 26 | return [ Me ]; 27 | }; 28 | 29 | Me.prototype.diff = Value.safe(function diff(expected) { 30 | var thisState = this._state; 31 | var expectedState = expected._state; 32 | 33 | if (thisState === expectedState) return ""; 34 | else return this.toString(); 35 | }); 36 | 37 | Me.prototype.toString = function toString() { 38 | return this._state; 39 | }; 40 | -------------------------------------------------------------------------------- /example/build/util/version_checker.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | 3 | // Helper function for checking version numbers. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | 7 | (function() { 8 | "use strict"; 9 | 10 | var semver = require("semver"); 11 | 12 | exports.check = function(options, success, fail) { 13 | if (options.strict) { 14 | if (semver.neq(options.actual, options.expected)) return failWithQualifier("exactly"); 15 | } 16 | else { 17 | if (semver.lt(options.actual, options.expected)) return failWithQualifier("at least"); 18 | if (semver.neq(options.actual, options.expected)) console.log("Warning: Newer " + options.name + 19 | " version than expected. Expected " + options.expected + ", but was " + options.actual + "."); 20 | } 21 | return success(); 22 | 23 | function failWithQualifier(qualifier) { 24 | return fail("Incorrect " + options.name + " version. Expected " + qualifier + 25 | " " + options.expected + ", but was " + options.actual + "."); 26 | } 27 | }; 28 | 29 | }()); -------------------------------------------------------------------------------- /src/q_page.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var PageEdge = require("./descriptors/page_edge.js"); 6 | var Center = require("./descriptors/center.js"); 7 | var Assertable = require("./assertable.js"); 8 | var Span = require("./descriptors/span.js"); 9 | 10 | var Me = module.exports = function QPage(browsingContext) { 11 | var BrowsingContext = require("./browsing_context.js"); // break circular dependency 12 | ensure.signature(arguments, [ BrowsingContext ]); 13 | 14 | // properties 15 | this.top = PageEdge.top(browsingContext); 16 | this.right = PageEdge.right(browsingContext); 17 | this.bottom = PageEdge.bottom(browsingContext); 18 | this.left = PageEdge.left(browsingContext); 19 | 20 | this.width = Span.create(this.left, this.right, "width of page"); 21 | this.height = Span.create(this.top, this.bottom, "height of page"); 22 | 23 | this.center = Center.x(this.left, this.right, "center of page"); 24 | this.middle = Center.y(this.top, this.bottom, "middle of page"); 25 | }; 26 | Assertable.extend(Me); 27 | -------------------------------------------------------------------------------- /src/descriptors/span.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var PositionDescriptor = require("./position_descriptor.js"); 6 | var SizeDescriptor = require("./size_descriptor.js"); 7 | var Center = require("./center.js"); 8 | 9 | var Me = module.exports = function Span(from, to, description) { 10 | ensure.signature(arguments, [ PositionDescriptor, PositionDescriptor, String ]); 11 | 12 | this.should = this.createShould(); 13 | 14 | this.center = Center.x(from, to, "center of " + description); 15 | this.middle = Center.y(from, to, "middle of " + description); 16 | 17 | this._from = from; 18 | this._to = to; 19 | this._description = description; 20 | }; 21 | SizeDescriptor.extend(Me); 22 | 23 | Me.create = function(from, to, description) { 24 | return new Me(from, to, description); 25 | }; 26 | 27 | Me.prototype.value = function() { 28 | ensure.signature(arguments, []); 29 | return this._from.value().distanceTo(this._to.value()); 30 | }; 31 | 32 | Me.prototype.toString = function() { 33 | return this._description; 34 | }; 35 | -------------------------------------------------------------------------------- /example/LICENSE.txt: -------------------------------------------------------------------------------- 1 | License (The MIT License) 2 | ------- 3 | Copyright (c) 2015 Titanium I.T. LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quixote", 3 | "version": "1.0.1", 4 | "description": "CSS unit and integration testing", 5 | "main": "src/quixote.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "browserify": "^16.5.1", 9 | "eslint": "^6.8.0", 10 | "gaze": "^1.1.3", 11 | "glob": "^7.1.6", 12 | "jake": "^8.1.1", 13 | "karma": "^1.3.0", 14 | "karma-commonjs": "1.0.0", 15 | "karma-firefox-launcher": "^1.3.0", 16 | "karma-mocha": "^1.3.0", 17 | "mocha": "^3.5.3", 18 | "semver": "^7.2.1", 19 | "shelljs": "^0.8.3", 20 | "simplebuild-karma": "^1.0.0" 21 | }, 22 | "scripts": { 23 | "test": "echo '*** Read CONTRIBUTING.md for a better way to test quixote ***' && ./jake.sh loose=true capture=Firefox" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/jamesshore/quixote.git" 28 | }, 29 | "keywords": [ 30 | "css", 31 | "test", 32 | "tdd" 33 | ], 34 | "author": "James Shore ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/jamesshore/quixote/issues" 38 | }, 39 | "homepage": "https://github.com/jamesshore/quixote" 40 | } 41 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Agile Engineering for the Web 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 | 19 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/q_viewport.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var ViewportEdge = require("./descriptors/viewport_edge.js"); 6 | var Center = require("./descriptors/center.js"); 7 | var Assertable = require("./assertable.js"); 8 | var Span = require("./descriptors/span.js"); 9 | 10 | var Me = module.exports = function QViewport(browsingContext) { 11 | var BrowsingContext = require("./browsing_context"); // break circular dependency 12 | ensure.signature(arguments, [ BrowsingContext ]); 13 | 14 | // properties 15 | this.top = ViewportEdge.top(browsingContext); 16 | this.right = ViewportEdge.right(browsingContext); 17 | this.bottom = ViewportEdge.bottom(browsingContext); 18 | this.left = ViewportEdge.left(browsingContext); 19 | 20 | this.width = Span.create(this.left, this.right, "width of viewport"); 21 | this.height = Span.create(this.top, this.bottom, "height of viewport"); 22 | 23 | this.center = Center.x(this.left, this.right, "center of viewport"); 24 | this.middle = Center.y(this.top, this.bottom, "middle of viewport"); 25 | }; 26 | Assertable.extend(Me); 27 | -------------------------------------------------------------------------------- /docs/QElementList.md: -------------------------------------------------------------------------------- 1 | # Quixote API: `QElementList` 2 | 3 | * [Back to overview README](../README.md) 4 | * [Back to API overview](api.md) 5 | 6 | `QElementList` instances contain a list of [`QElement`](QElement.md) objects. It's provided by [`QFrame.getAll()`.](QFrame.md#framegetall) 7 | 8 | 9 | ## Methods 10 | 11 | ### list.length() 12 | 13 | ``` 14 | Stability: 3 - Stable 15 | ``` 16 | 17 | Determine the number of elements in the list. 18 | 19 | `length = list.length()` 20 | 21 | * `length (number)` The number of elements in the list. 22 | 23 | 24 | ### list.at() 25 | 26 | ``` 27 | Stability: 3 - Stable 28 | ``` 29 | 30 | Retrieve an element from the list. Positive and negative indices are allowed (see below). Throws an exception if the index is out of bounds. 31 | 32 | `element = list.at(index, nickname)` 33 | 34 | * `element (`[`QElement`](QElement.md)`)` The element retrieved. 35 | 36 | * `index (number)` Zero-based index of the element to retrieve. If the index is negative, it counts from the end of the list. 37 | 38 | * `nickname (optional string)` The name to use when describing `element` in error messages. Uses the list's nickname with a subscript (e.g., `myList[0]`) by default. 39 | 40 | Example: Retrieve the last element: `var element = list.index(-1);` 41 | -------------------------------------------------------------------------------- /src/values/_render_state_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var RenderState = require("./render_state.js"); 6 | var Value = require("./value.js"); 7 | 8 | describe("VALUE: RenderState", function() { 9 | 10 | var rendered = RenderState.rendered(); 11 | var notRendered = RenderState.notRendered(); 12 | 13 | it("is a value object", function() { 14 | assert.implements(rendered, Value); 15 | }); 16 | 17 | it("is a boolean reflecting whether element is rendered or not", function() { 18 | assert.objEqual(RenderState.rendered(), rendered, "rendered"); 19 | assert.objEqual(RenderState.notRendered(), notRendered, "not rendered"); 20 | }); 21 | 22 | it("describes difference", function() { 23 | assert.equal(rendered.diff(rendered), ""); 24 | assert.equal(rendered.diff(notRendered), "rendered"); 25 | 26 | assert.equal(notRendered.diff(rendered), "not rendered"); 27 | assert.equal(notRendered.diff(notRendered), ""); 28 | }); 29 | 30 | it("converts to string", function() { 31 | assert.equal(rendered.toString(), "rendered"); 32 | assert.equal(notRendered.toString(), "not rendered"); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/q_element_list.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var QElement = require("./q_element.js"); 6 | 7 | var Me = module.exports = function QElementList(nodeList, nickname) { 8 | ensure.signature(arguments, [ Object, String ]); 9 | 10 | this._nodeList = nodeList; 11 | this._nickname = nickname; 12 | }; 13 | 14 | Me.prototype.length = function length() { 15 | ensure.signature(arguments, []); 16 | 17 | return this._nodeList.length; 18 | }; 19 | 20 | Me.prototype.at = function at(requestedIndex, nickname) { 21 | ensure.signature(arguments, [ Number, [undefined, String] ]); 22 | 23 | var index = requestedIndex; 24 | var length = this.length(); 25 | if (index < 0) index = length + index; 26 | 27 | ensure.that( 28 | index >= 0 && index < length, 29 | "'" + this._nickname + "'[" + requestedIndex + "] is out of bounds; list length is " + length 30 | ); 31 | var element = this._nodeList[index]; 32 | 33 | if (nickname === undefined) nickname = this._nickname + "[" + index + "]"; 34 | return QElement.create(element, nickname); 35 | }; 36 | 37 | Me.prototype.toString = function toString() { 38 | ensure.signature(arguments, []); 39 | 40 | return "'" + this._nickname + "' list"; 41 | }; -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Quixote Roadmap 2 | 3 | ## Release Ideas 4 | 5 | * **✅ v0.1** Basic DOM manipulation; raw position and style information 6 | * **✅ v0.2** "Cooked" absolute position info; initial assertion API 7 | * **✅ v0.3** Positioning relative to other elements 8 | * **✅ v0.4** Advanced positioning (middle, center, height, width, arithmetic, fractions) 9 | * **✅ v0.5** API hardening 10 | * **✅ v0.6** Responsive design 11 | * **✅ v0.7** Page and viewport assertions 12 | * **✅ v0.8 - v0.11** Dogfooding and real-world usage (more dogfooding releases to come) 13 | * **✅ v0.12** Element display status descriptors 14 | * **✅ v0.13** Element rendering boundaries 15 | * **✅ v0.14** QFrame quality of life improvements 16 | * **✅ v0.15** Support for third-party test runners 17 | * **✅ v1.0.0** New assertions 18 | * See our [work-in-progress roadmap](https://github.com/jamesshore/quixote/blob/master/ROADMAP.md) for the latest release plans. 19 | 20 | 21 | ## Current Feature: TBD 22 | 23 | 24 | ## To Do: TBD 25 | 26 | 27 | ## Dogfooding Notes 28 | 29 | * Provide a better way of integrating with standard assertion libraries? Use `valueOf()`? 30 | * Provide better error message when cross-origin 'src' provided to quixote.createFrame 31 | 32 | 33 | ## Future Features 34 | 35 | * Z-ordering 36 | * Colors? Contrast (fg color vs. bg color?)) 37 | * Plugin API 38 | -------------------------------------------------------------------------------- /example/build/util/sh.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2015 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // Helper functions for running processes. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var jake = require("jake"); 10 | 11 | exports.runMany = function(commands, successCallback, failureCallback) { 12 | var stdout = []; 13 | function serializedSh(command) { 14 | if (command) { 15 | run(command, function(oneStdout) { 16 | stdout.push(oneStdout); 17 | serializedSh(commands.shift()); 18 | }, failureCallback); 19 | } 20 | else { 21 | successCallback(stdout); 22 | } 23 | } 24 | serializedSh(commands.shift()); 25 | }; 26 | 27 | var run = exports.run = function(oneCommand, successCallback, failureCallback) { 28 | var stdout = ""; 29 | var child = jake.createExec(oneCommand); 30 | child.on("stdout", function(data) { 31 | process.stdout.write(data); 32 | stdout += data; 33 | }); 34 | child.on("stderr", function(data) { 35 | process.stderr.write(data); 36 | }); 37 | child.on("cmdEnd", function() { 38 | successCallback(stdout); 39 | }); 40 | child.on("error", function() { 41 | failureCallback(stdout); 42 | }); 43 | 44 | console.log("> " + oneCommand); 45 | child.run(); 46 | }; 47 | 48 | }()); -------------------------------------------------------------------------------- /src/assertable.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var oop = require("./util/oop.js"); 6 | var shim = require("./util/shim.js"); 7 | 8 | var Me = module.exports = function Assertable() { 9 | ensure.unreachable("Assertable is abstract and should not be constructed directly."); 10 | }; 11 | Me.extend = oop.extendFn(Me); 12 | oop.makeAbstract(Me, []); 13 | 14 | Me.prototype.assert = function assert(expected, message) { 15 | ensure.signature(arguments, [ Object, [undefined, String] ]); 16 | if (message === undefined) message = "Differences found"; 17 | 18 | var diff = this.diff(expected); 19 | if (diff !== "") throw new Error(message + ":\n" + diff + "\n"); 20 | }; 21 | 22 | Me.prototype.diff = function diff(expected) { 23 | ensure.signature(arguments, [ Object ]); 24 | 25 | var result = []; 26 | var keys = shim.Object.keys(expected); 27 | var key, oneDiff, descriptor; 28 | for (var i = 0; i < keys.length; i++) { 29 | key = keys[i]; 30 | descriptor = this[key]; 31 | ensure.that( 32 | descriptor !== undefined, 33 | this + " doesn't have a property named '" + key + "'. Did you misspell it?" 34 | ); 35 | oneDiff = descriptor.diff(expected[key]); 36 | if (oneDiff !== "") result.push(oneDiff); 37 | } 38 | 39 | return result.join("\n"); 40 | }; 41 | -------------------------------------------------------------------------------- /spikes/ios_frame_scrolling/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 | 34 |
Status: 35 |
    36 |
    37 | 38 | -------------------------------------------------------------------------------- /src/descriptors/_absolute_position_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | 7 | var PositionDescriptor = require("./position_descriptor"); 8 | var AbsolutePosition = require("./absolute_position.js"); 9 | var Position = require("../values/position.js"); 10 | 11 | describe("DESCRIPTOR: AbsolutePosition", function() { 12 | 13 | var IRRELEVANT = 42; 14 | 15 | it("is a position descriptor", function() { 16 | assert.implements(AbsolutePosition.x(IRRELEVANT), PositionDescriptor); 17 | }); 18 | 19 | it("regurgitates its value", function() { 20 | assert.objEqual(AbsolutePosition.x(10).value(), Position.x(10), "x"); 21 | assert.objEqual(AbsolutePosition.y(20).value(), Position.y(20), "y"); 22 | }); 23 | 24 | it("renders to string", function() { 25 | assert.equal(AbsolutePosition.x(10).toString(), "10px x-coordinate", "x"); 26 | assert.equal(AbsolutePosition.y(20).toString(), "20px y-coordinate", "y"); 27 | }); 28 | 29 | it("has assertions", function() { 30 | assert.exception( 31 | function() { AbsolutePosition.x(10).should.equal(AbsolutePosition.x(30)); }, 32 | "10px x-coordinate should be 20px to right.\n" + 33 | " Expected: 30px (30px x-coordinate)\n" + 34 | " But was: 10px" 35 | ); 36 | }); 37 | 38 | }); -------------------------------------------------------------------------------- /src/descriptors/absolute_position.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var PositionDescriptor = require("./position_descriptor.js"); 6 | var Position = require("../values/position.js"); 7 | 8 | var X_DIMENSION = "x"; 9 | var Y_DIMENSION = "y"; 10 | 11 | var Me = module.exports = function AbsolutePosition(dimension, value) { 12 | ensure.signature(arguments, [ String, Number ]); 13 | 14 | this.should = this.createShould(); 15 | 16 | switch(dimension) { 17 | case X_DIMENSION: 18 | PositionDescriptor.x(this); 19 | this._value = Position.x(value); 20 | break; 21 | case Y_DIMENSION: 22 | PositionDescriptor.y(this); 23 | this._value = Position.y(value); 24 | break; 25 | default: ensure.unreachable("Unknown dimension: " + dimension); 26 | } 27 | this._dimension = dimension; 28 | }; 29 | PositionDescriptor.extend(Me); 30 | 31 | Me.x = function(value) { 32 | ensure.signature(arguments, [ Number ]); 33 | return new Me(X_DIMENSION, value); 34 | }; 35 | 36 | Me.y = function(value) { 37 | ensure.signature(arguments, [ Number ]); 38 | return new Me(Y_DIMENSION, value); 39 | }; 40 | 41 | Me.prototype.value = function() { 42 | return this._value; 43 | }; 44 | 45 | Me.prototype.toString = function() { 46 | return this._value + " " + this._dimension + "-coordinate"; 47 | }; 48 | -------------------------------------------------------------------------------- /src/descriptors/center.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var PositionDescriptor = require("./position_descriptor.js"); 6 | 7 | var X_DIMENSION = "x"; 8 | var Y_DIMENSION = "y"; 9 | 10 | var Me = module.exports = function Center(dimension, position1, position2, description) { 11 | ensure.signature(arguments, [ String, PositionDescriptor, PositionDescriptor, String ]); 12 | 13 | this.should = this.createShould(); 14 | 15 | if (dimension === X_DIMENSION) PositionDescriptor.x(this); 16 | else if (dimension === Y_DIMENSION) PositionDescriptor.y(this); 17 | else ensure.unreachable("Unknown dimension: " + dimension); 18 | 19 | this._dimension = dimension; 20 | this._position1 = position1; 21 | this._position2 = position2; 22 | this._description = description; 23 | }; 24 | PositionDescriptor.extend(Me); 25 | 26 | Me.x = factoryFn(X_DIMENSION); 27 | Me.y = factoryFn(Y_DIMENSION); 28 | 29 | Me.prototype.value = function value() { 30 | ensure.signature(arguments, []); 31 | return this._position1.value().midpoint(this._position2.value()); 32 | }; 33 | 34 | Me.prototype.toString = function toString() { 35 | ensure.signature(arguments, []); 36 | return this._description; 37 | }; 38 | 39 | function factoryFn(dimension) { 40 | return function(position1, position2, description) { 41 | return new Me(dimension, position1, position2, description); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | License (The MIT License) 2 | ------- 3 | Copyright (c) 2014-2015 Titanium I.T. LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | 25 | Third Party Code: 26 | ----------------- 27 | 28 | * 'proclaim' licensed under MIT license (https://github.com/rowanmanning/proclaim) 29 | * 'camelcase' © Sindre Sorhus, licensed under MIT license (https://github.com/sindresorhus/camelcase) 30 | * 'async' Copyright 2010-2014 Caolan McMahon, licensed under MIT license (https://github.com/caolan/async) -------------------------------------------------------------------------------- /example/build/config/jshint.conf.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // Configuration options for JSHint. Change this to match your preferences. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var merge = require("object-merge"); 10 | 11 | var universalOptions = { 12 | bitwise: true, 13 | curly: false, 14 | eqeqeq: true, 15 | forin: true, 16 | immed: true, 17 | latedef: false, 18 | newcap: true, 19 | noarg: true, 20 | noempty: true, 21 | nonew: true, 22 | regexp: true, 23 | undef: true, 24 | strict: true, 25 | globalstrict: true, // "global" stricts are okay when using CommonJS modules 26 | trailing: true 27 | }; 28 | 29 | exports.nodeOptions = merge(universalOptions, { 30 | node: true 31 | }); 32 | 33 | exports.clientOptions = merge(universalOptions, { 34 | browser: true 35 | }); 36 | 37 | var universalGlobals = { 38 | // Mocha 39 | before: false, 40 | after: false, 41 | beforeEach: false, 42 | afterEach: false, 43 | describe: false, 44 | it: false 45 | }; 46 | 47 | exports.nodeGlobals = merge(universalGlobals, { 48 | // Jake 49 | jake: false, 50 | desc: false, 51 | task: false, 52 | directory: false, 53 | complete: false, 54 | fail: false 55 | }); 56 | 57 | exports.clientGlobals = merge(universalGlobals, { 58 | // CommonJS 59 | exports: false, 60 | require: false, 61 | module: false, 62 | 63 | // Browser 64 | console: false 65 | }); 66 | 67 | }()); -------------------------------------------------------------------------------- /spikes/ios_frame_sizing/README.md: -------------------------------------------------------------------------------- 1 | These files demonstrate how Mobile Safari sizes an iframe. 2 | 3 | To use, serve the files from a web server (I like npm's `http-server` for simplicity and convenience), then 4 | visit index.html in Mobile Safari or any other browser. 5 | 6 | On Mobile Safari, observe: 7 | * The frame ignores the `height` and `width` attributes. 8 | * The red full-width element extends all the way to the scroll-creator box. On a desktop browser, the full-width element only extends the width of the viewport. 9 | * The blue `@media successful` box does not appear. (It's styled to appear when the width of the viewport is <= 1000px.) 10 | 11 | Now load inner.html in Mobile Safari. Observe that the page behaves similarly to a desktop browser: 12 | * The red full-width element does *not* extend the entire with of the page. It only extends the width of the viewport (980px on iOS, unless otherwise configured with a `` tag). 13 | * The blue `@media successful` box *does* appear. 14 | 15 | Now comment out the "scroll creator" line in inner.html. Load inner.html (to flush the cache) and then index.html. On index.html, observe that the page matches the iframe's width and height. 16 | * The frame obeys the `width` and `height` attributes on Mobile Safari. 17 | * The red full-width element is only 400px wide 18 | * The blue `@media successful` box does appear. 19 | 20 | 21 | CONCLUSION: 22 | 23 | Mobile Safari sizes a frame to the frame size attributes *or* the actual page size, whichever is *larger*. 24 | 25 | This is in contrast to a desktop browser, which always sizes a frame its attributes, regardless of the page size. -------------------------------------------------------------------------------- /example/src/_toggle_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | 3 | // Example JavaScript tests. We're using Mocha as our test framework and Chai for assertions. 4 | // There's nothing related to Quixote in this file. It's just an example of testing JavaScript. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var assert = require("./assert.js"); 10 | var toggle = require("./toggle.js"); 11 | 12 | describe("Toggle", function() { 13 | 14 | var container; 15 | 16 | beforeEach(function() { 17 | container = document.createElement("div"); 18 | document.body.appendChild(container); 19 | }); 20 | 21 | afterEach(function() { 22 | container.parentNode.removeChild(container); 23 | }); 24 | 25 | it("toggle CSS class on an element when another element is clicked", function() { 26 | var clickMe = addDiv(); 27 | var target = addDiv(); 28 | var cssClass = "someClassName"; 29 | 30 | toggle.init(clickMe, target, cssClass); 31 | assert.isFalse(hasClass(target, cssClass), "should not contain class before click"); 32 | 33 | clickMe.click(); 34 | assert.isTrue(hasClass(target, cssClass), "should contain class after click"); 35 | 36 | clickMe.click(); 37 | assert.isFalse(hasClass(target, cssClass), "should not contain class after another click"); 38 | }); 39 | 40 | function hasClass(target, cssClass) { 41 | return target.classList.contains(cssClass); 42 | } 43 | 44 | function addDiv() { 45 | var element = document.createElement("div"); 46 | container.appendChild(element); 47 | return element; 48 | } 49 | 50 | }); 51 | 52 | }()); -------------------------------------------------------------------------------- /src/util/oop.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | // can't use ensure.js due to circular dependency 5 | var shim = require("./shim.js"); 6 | 7 | exports.className = function(constructor) { 8 | if (typeof constructor !== "function") throw new Error("Not a constructor"); 9 | return shim.Function.name(constructor); 10 | }; 11 | 12 | exports.instanceName = function(obj) { 13 | var prototype = shim.Object.getPrototypeOf(obj); 14 | if (prototype === null) return ""; 15 | 16 | var constructor = prototype.constructor; 17 | if (constructor === undefined || constructor === null) return ""; 18 | 19 | return shim.Function.name(constructor); 20 | }; 21 | 22 | exports.extendFn = function extendFn(parentConstructor) { 23 | return function(childConstructor) { 24 | childConstructor.prototype = shim.Object.create(parentConstructor.prototype); 25 | childConstructor.prototype.constructor = childConstructor; 26 | }; 27 | }; 28 | 29 | exports.makeAbstract = function makeAbstract(constructor, methods) { 30 | var name = shim.Function.name(constructor); 31 | shim.Array.forEach(methods, function(method) { 32 | constructor.prototype[method] = function() { 33 | throw new Error(name + " subclasses must implement " + method + "() method"); 34 | }; 35 | }); 36 | 37 | constructor.prototype.checkAbstractMethods = function checkAbstractMethods() { 38 | var unimplemented = []; 39 | var self = this; 40 | shim.Array.forEach(methods, function(name) { 41 | if (self[name] === constructor.prototype[name]) unimplemented.push(name + "()"); 42 | }); 43 | return unimplemented; 44 | }; 45 | }; -------------------------------------------------------------------------------- /src/_q_viewport_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var reset = require("./__reset.js"); 6 | var QViewport = require("./q_viewport.js"); 7 | var Assertable = require("./assertable.js"); 8 | 9 | describe("FOUNDATION: QViewport", function() { 10 | 11 | var frame; 12 | var viewport; 13 | 14 | beforeEach(function() { 15 | frame = reset.frame; 16 | viewport = new QViewport(frame.toBrowsingContext()); 17 | }); 18 | 19 | it("is Assertable", function() { 20 | assert.implements(viewport, Assertable); 21 | }); 22 | 23 | it("has size properties", function() { 24 | assert.equal(viewport.width.diff(reset.WIDTH), "", "width"); 25 | assert.equal(viewport.height.diff(reset.HEIGHT), "", "height"); 26 | assert.equal(viewport.width.toString(), "width of viewport", "width description"); 27 | assert.equal(viewport.height.toString(), "height of viewport", "height description"); 28 | }); 29 | 30 | it("has edge properties", function() { 31 | assert.equal(viewport.top.diff(0), "", "top"); 32 | assert.equal(viewport.right.diff(reset.WIDTH), "", "right"); 33 | assert.equal(viewport.bottom.diff(reset.HEIGHT), "", "bottom"); 34 | assert.equal(viewport.left.diff(0), "", "left"); 35 | }); 36 | 37 | it("has center properties", function() { 38 | assert.equal(viewport.center.diff(reset.WIDTH / 2), "", "center"); 39 | assert.equal(viewport.middle.diff(reset.HEIGHT / 2), "", "middle"); 40 | assert.equal(viewport.center.toString(), "center of viewport", "center description"); 41 | assert.equal(viewport.middle.toString(), "middle of viewport", "middle description"); 42 | }); 43 | 44 | }); -------------------------------------------------------------------------------- /src/values/value.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var oop = require("../util/oop.js"); 6 | var shim = require("../util/shim.js"); 7 | 8 | var Me = module.exports = function Value() {}; 9 | Me.extend = oop.extendFn(Me); 10 | oop.makeAbstract(Me, [ 11 | "compatibility", 12 | "diff", 13 | "toString" 14 | ]); 15 | 16 | Me.safe = function safe(fn) { 17 | return function() { 18 | ensureCompatibility(this, this.compatibility(), arguments); 19 | return fn.apply(this, arguments); 20 | }; 21 | }; 22 | 23 | Me.prototype.value = function value() { 24 | ensure.signature(arguments, []); 25 | return this; 26 | }; 27 | 28 | Me.prototype.equals = function equals(that) { 29 | return this.diff(that) === ""; 30 | }; 31 | 32 | Me.prototype.isCompatibleWith = function isCompatibleWith(that) { 33 | if (that === null || typeof that !== "object") return false; 34 | 35 | var compatibleTypes = this.compatibility(); 36 | for (var i = 0; i < compatibleTypes.length; i++) { 37 | if (that instanceof compatibleTypes[i]) return true; 38 | } 39 | return false; 40 | }; 41 | 42 | function ensureCompatibility(self, compatible, args) { 43 | var arg; 44 | for (var i = 0; i < args.length; i++) { // args is not an Array, can't use forEach 45 | arg = args[i]; 46 | if (!self.isCompatibleWith(arg)) { 47 | var type = typeof arg; 48 | if (arg === null) type = "null"; 49 | if (type === "object") type = oop.instanceName(arg); 50 | 51 | throw new Error( 52 | "A descriptor doesn't make sense. (" + oop.instanceName(self) + " can't combine with " + type + ")" 53 | ); 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/_q_page_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var reset = require("./__reset.js"); 6 | var QPage = require("./q_page.js"); 7 | var Assertable = require("./assertable.js"); 8 | 9 | describe("FOUNDATION: QPage", function() { 10 | 11 | var WIDTH = reset.WIDTH + 200; 12 | var HEIGHT = reset.HEIGHT + 200; 13 | 14 | var frame; 15 | var page; 16 | 17 | beforeEach(function() { 18 | frame = reset.frame; 19 | page = new QPage(frame.toBrowsingContext()); 20 | 21 | frame.add( 22 | "
    element
    " 24 | ); 25 | }); 26 | 27 | it("is Assertable", function() { 28 | assert.implements(page, Assertable); 29 | }); 30 | 31 | it("has size properties", function() { 32 | assert.equal(page.width.diff(WIDTH), "", "width"); 33 | assert.equal(page.height.diff(HEIGHT), "", "height"); 34 | assert.equal(page.width.toString(), "width of page", "width description"); 35 | assert.equal(page.height.toString(), "height of page", "height description"); 36 | }); 37 | 38 | it("has edge properties", function() { 39 | assert.equal(page.top.diff(0), "", "top"); 40 | assert.equal(page.right.diff(WIDTH), "", "right"); 41 | assert.equal(page.bottom.diff(HEIGHT), "", "bottom"); 42 | assert.equal(page.left.diff(0), "", "left"); 43 | }); 44 | 45 | it("has center properties", function() { 46 | assert.equal(page.center.diff(WIDTH / 2), "", "center"); 47 | assert.equal(page.middle.diff(HEIGHT / 2), "", "middle"); 48 | assert.equal(page.center.toString(), "center of page", "center description"); 49 | assert.equal(page.middle.toString(), "middle of page", "middle description"); 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /src/descriptors/element_render.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var RenderState = require("../values/render_state.js"); 6 | var Position = require("../values/position.js"); 7 | var Descriptor = require("./descriptor.js"); 8 | var ElementRenderEdge = require("./element_render_edge.js"); 9 | var Span = require("./span.js"); 10 | var Center = require("./center.js"); 11 | 12 | var Me = module.exports = function ElementRender(element) { 13 | var QElement = require("../q_element.js"); // break circular dependency 14 | ensure.signature(arguments, [ QElement ]); 15 | 16 | this.should = this.createShould(); 17 | this._element = element; 18 | 19 | // properties 20 | this.top = ElementRenderEdge.top(element); 21 | this.right = ElementRenderEdge.right(element); 22 | this.bottom = ElementRenderEdge.bottom(element); 23 | this.left = ElementRenderEdge.left(element); 24 | 25 | this.width = Span.create(this.left, this.right, "rendered width of " + element); 26 | this.height = Span.create(this.top, this.bottom, "rendered height of " + element); 27 | 28 | this.center = Center.x(this.left, this.right, "rendered center of " + element); 29 | this.middle = Center.y(this.top, this.bottom, "rendered middle of " + element); 30 | }; 31 | Descriptor.extend(Me); 32 | 33 | Me.create = function create(element) { 34 | return new Me(element); 35 | }; 36 | 37 | Me.prototype.value = function value() { 38 | if (this.top.value().equals(Position.noY())) return RenderState.notRendered(); 39 | else return RenderState.rendered(); 40 | }; 41 | 42 | Me.prototype.toString = function toString() { 43 | return this._element.toString() + " rendering"; 44 | }; 45 | 46 | Me.prototype.convert = function convert(arg, type) { 47 | if (type === "boolean") { 48 | return arg ? RenderState.rendered() : RenderState.notRendered(); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/descriptors/relative_size.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Size = require("../values/size.js"); 6 | var Descriptor = require("./descriptor.js"); 7 | var SizeDescriptor = require("./size_descriptor.js"); 8 | var Value = require("../values/value.js"); 9 | var SizeMultiple = require("./size_multiple.js"); 10 | 11 | var PLUS = 1; 12 | var MINUS = -1; 13 | 14 | var Me = module.exports = function RelativeSize(direction, relativeTo, amount) { 15 | ensure.signature(arguments, [ Number, Descriptor, [Number, Descriptor, Value] ]); 16 | 17 | this.should = this.createShould(); 18 | 19 | this._direction = direction; 20 | this._relativeTo = relativeTo; 21 | 22 | if (typeof amount === "number") { 23 | this._amount = Size.create(Math.abs(amount)); 24 | if (amount < 0) this._direction *= -1; 25 | } 26 | else { 27 | this._amount = amount; 28 | } 29 | }; 30 | SizeDescriptor.extend(Me); 31 | 32 | Me.larger = factoryFn(PLUS); 33 | Me.smaller = factoryFn(MINUS); 34 | 35 | Me.prototype.value = function value() { 36 | ensure.signature(arguments, []); 37 | 38 | var baseValue = this._relativeTo.value(); 39 | var relativeValue = this._amount.value(); 40 | 41 | if (this._direction === PLUS) return baseValue.plus(relativeValue); 42 | else return baseValue.minus(relativeValue); 43 | }; 44 | 45 | Me.prototype.toString = function toString() { 46 | ensure.signature(arguments, []); 47 | 48 | var base = this._relativeTo.toString(); 49 | if (this._amount.equals(Size.create(0))) return base; 50 | 51 | var relation = this._amount.toString(); 52 | if (this._direction === PLUS) relation += " larger than "; 53 | else relation += " smaller than "; 54 | 55 | return relation + base; 56 | }; 57 | 58 | function factoryFn(direction) { 59 | return function factory(relativeTo, amount) { 60 | return new Me(direction, relativeTo, amount); 61 | }; 62 | } -------------------------------------------------------------------------------- /src/descriptors/_center_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var PositionDescriptor = require("./position_descriptor.js"); 7 | var Center = require("./center.js"); 8 | var Position = require("../values/position.js"); 9 | 10 | describe("DESCRIPTOR: Center", function() { 11 | 12 | var element; 13 | var center; 14 | var middle; 15 | 16 | var CENTER = 85; 17 | var MIDDLE = 90; 18 | 19 | beforeEach(function() { 20 | var frame = reset.frame; 21 | 22 | frame.add( 23 | "

    one

    " 24 | ); 25 | element = frame.get("#one"); 26 | 27 | center = Center.x(element.left, element.right, "horizontal description"); 28 | middle = Center.y(element.top, element.bottom, "vertical description"); 29 | }); 30 | 31 | it("is a position descriptor", function() { 32 | assert.implements(center, PositionDescriptor); 33 | }); 34 | 35 | it("resolves to value", function() { 36 | assert.objEqual(center.value(), Position.x(CENTER), "center"); 37 | assert.objEqual(middle.value(), Position.y(MIDDLE), "middle"); 38 | }); 39 | 40 | it("converts comparison arguments", function() { 41 | assert.objEqual(center.convert(13, "number"), Position.x(13), "should convert numbers to x-positions"); 42 | assert.objEqual(middle.convert(13, "number"), Position.y(13), "should convert numbers to y-positions"); 43 | }); 44 | 45 | it("converts to string", function() { 46 | assert.equal(center.toString(), "horizontal description", "center"); 47 | assert.equal(middle.toString(), "vertical description", "middle"); 48 | }); 49 | 50 | it("has assertions", function() { 51 | assert.exception( 52 | function() { center.should.equal(10); }, 53 | "horizontal description should be 75px to left.\n" + 54 | " Expected: 10px\n" + 55 | " But was: 85px" 56 | ); 57 | }); 58 | 59 | }); -------------------------------------------------------------------------------- /src/descriptors/size_multiple.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Descriptor = require("./descriptor.js"); 6 | var SizeDescriptor = require("./size_descriptor.js"); 7 | var Size = require("../values/size.js"); 8 | 9 | var Me = module.exports = function SizeMultiple(relativeTo, multiple) { 10 | ensure.signature(arguments, [ Descriptor, Number ]); 11 | 12 | this.should = this.createShould(); 13 | 14 | this._relativeTo = relativeTo; 15 | this._multiple = multiple; 16 | }; 17 | SizeDescriptor.extend(Me); 18 | 19 | Me.create = function create(relativeTo, multiple) { 20 | return new Me(relativeTo, multiple); 21 | }; 22 | 23 | Me.prototype.value = function value() { 24 | ensure.signature(arguments, []); 25 | 26 | return this._relativeTo.value().times(this._multiple); 27 | }; 28 | 29 | Me.prototype.toString = function toString() { 30 | ensure.signature(arguments, []); 31 | 32 | var multiple = this._multiple; 33 | var base = this._relativeTo.toString(); 34 | if (multiple === 1) return base; 35 | 36 | var desc; 37 | switch(multiple) { 38 | case 1/2: desc = "half of "; break; 39 | case 1/3: desc = "one-third of "; break; 40 | case 2/3: desc = "two-thirds of "; break; 41 | case 1/4: desc = "one-quarter of "; break; 42 | case 3/4: desc = "three-quarters of "; break; 43 | case 1/5: desc = "one-fifth of "; break; 44 | case 2/5: desc = "two-fifths of "; break; 45 | case 3/5: desc = "three-fifths of "; break; 46 | case 4/5: desc = "four-fifths of "; break; 47 | case 1/6: desc = "one-sixth of "; break; 48 | case 5/6: desc = "five-sixths of "; break; 49 | case 1/8: desc = "one-eighth of "; break; 50 | case 3/8: desc = "three-eighths of "; break; 51 | case 5/8: desc = "five-eighths of "; break; 52 | case 7/8: desc = "seven-eighths of "; break; 53 | default: 54 | if (multiple > 1) desc = multiple + " times "; 55 | else desc = (multiple * 100) + "% of "; 56 | } 57 | 58 | return desc + base; 59 | }; -------------------------------------------------------------------------------- /src/_browser_test.js: -------------------------------------------------------------------------------- 1 | // Copyright Titanium I.T. LLC. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var quixote = require("./quixote.js"); 6 | 7 | describe("FOUNDATION: Browser capability", function() { 8 | 9 | var mobileSafari; 10 | var chromeMobile; 11 | var ie8; 12 | var ie11; 13 | 14 | beforeEach(function() { 15 | var userAgent = navigator.userAgent; 16 | 17 | // These user agent strings may be brittle. It's okay because we only use them in the tests. Modify them 18 | // as needed to make sure tests match real-world behavior. 19 | mobileSafari = userAgent.match(/(iPad|iPhone|iPod touch);/i) !== null; 20 | chromeMobile = userAgent.match(/Android/) !== null; 21 | ie8 = userAgent.match(/MSIE 8\.0/) !== null; 22 | ie11 = userAgent.match(/rv:11\.0/) !== null; 23 | }); 24 | 25 | it("detects whether browser expands frame to fit size of page", function() { 26 | assert.equal( 27 | quixote.browser.enlargesFrameToPageSize(), 28 | false, 29 | "everything should respect frame size" 30 | ); 31 | }); 32 | 33 | it("detects whether browser expands size of font when frame is large", function() { 34 | assert.equal( 35 | quixote.browser.enlargesFonts(), 36 | mobileSafari, 37 | "everything but Mobile Safari should respect frame size" 38 | ); 39 | }); 40 | 41 | it("detects whether browser can detect `clip: auto` value", function() { 42 | assert.equal( 43 | quixote.browser.misreportsClipAutoProperty(), 44 | ie8, 45 | "everything but IE 8 should calculate 'clip: auto' properly" 46 | ); 47 | }); 48 | 49 | it("detects whether browser computes `clip: rect(auto, ...)` value correctly", function() { 50 | assert.equal( 51 | quixote.browser.misreportsAutoValuesInClipProperty(), 52 | ie11, 53 | "everything but IE 11 should calculate clip values properly" 54 | ); 55 | }); 56 | 57 | it("detects whether browser rounds off floating-point pixel values", function() { 58 | assert.equal( 59 | quixote.browser.roundsOffPixelCalculations(), 60 | ie8 || ie11, 61 | "only IE 8 and IE 11 should round off pixel values" 62 | ); 63 | }); 64 | 65 | }); -------------------------------------------------------------------------------- /watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | // Automatically runs Jake when files change. 4 | // 5 | // Thanks to Davide Alberto Molin for contributing this code. 6 | // See http://www.letscodejavascript.com/v3/comments/live/7 for details. 7 | 8 | (function() { 9 | "use strict"; 10 | 11 | var gaze = require("gaze"); 12 | var spawn = require("child_process").spawn; 13 | var path = require("path"); 14 | 15 | var WATCH = [ 16 | "build/**/*.js", 17 | "src/**/*.js", "src/**/*.html", "src/**/*.css", 18 | "vendor/**/*.js", 19 | "test/**/*.js" 20 | ]; 21 | 22 | var COMMAND = require("./build/config/build_command.js"); 23 | 24 | var args = process.argv.slice(2); 25 | var child = null; 26 | var buildQueued = false; 27 | var buildStartedAt; 28 | 29 | gaze(WATCH, function(err, watcher) { 30 | if (err) { 31 | console.log("WATCH ERROR:", err); 32 | return; 33 | } 34 | 35 | console.log("Will run " + COMMAND + " when " + WATCH.join(" or ") + " changes."); 36 | watcher.on("all", triggerBuild); 37 | triggerBuild(); // Always run after startup 38 | }); 39 | 40 | function triggerBuild(event, filepath) { 41 | logEvent(event, filepath); 42 | if (child === null) runJake(); 43 | else queueAnotherBuild(); 44 | } 45 | 46 | function runJake() { 47 | buildStartedAt = Date.now(); 48 | console.log("\n*** RUN> " + COMMAND + " " + args.join(" ")); 49 | child = spawn(COMMAND, args, { stdio: "inherit" }); 50 | 51 | child.once("exit", function(code) { 52 | child = null; 53 | }); 54 | } 55 | 56 | function queueAnotherBuild() { 57 | if (buildQueued) return; 58 | if (debounce()) return; 59 | 60 | console.log("*** Build queued"); 61 | buildQueued = true; 62 | child.once("exit", function(code) { 63 | buildQueued = false; 64 | triggerBuild(); 65 | }); 66 | 67 | function debounce() { 68 | var msSinceLastBuild = Date.now() - buildStartedAt; 69 | return msSinceLastBuild < 1000; 70 | } 71 | } 72 | 73 | function logEvent(event, filepath) { 74 | if (filepath === undefined) return; 75 | 76 | var truncatedPath = path.basename(path.dirname(filepath)) + "/" + path.basename(filepath); 77 | console.log("*** " + event.toUpperCase() + ": .../" + truncatedPath); 78 | } 79 | 80 | }()); 81 | -------------------------------------------------------------------------------- /example/build/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Quixote-specific configuration starts with "QUIXOTE:" 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | var paths = require("./paths.js"); 8 | 9 | module.exports = function(config) { 10 | config.set({ 11 | 12 | // base path, that will be used to resolve files and exclude 13 | basePath: '../..', 14 | 15 | // frameworks to use 16 | frameworks: ['mocha', 'commonjs'], 17 | 18 | // list of files / patterns to load in the browser 19 | files: [ 20 | 'src/**/*.js', 21 | 'vendor/**/*.js', 22 | 23 | // QUIXOTE: Serve the CSS file so we can load it in our tests 24 | // Mark it `included: false` so Karma doesn't load it automatically 25 | { pattern: 'src/screen.css', included: false } 26 | ], 27 | 28 | // list of files to exclude 29 | exclude: [], 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: { 34 | 'src/**/*.js': ['commonjs'], 35 | 'vendor/**/*.js': ['commonjs'] 36 | }, 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 40 | reporters: ['dots'], 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: false, 54 | 55 | // Start these browsers, currently available: 56 | // - Chrome 57 | // - ChromeCanary 58 | // - Firefox 59 | // - Opera 60 | // - Safari (only Mac) 61 | // - PhantomJS 62 | // - IE (only Windows) 63 | browsers: [], 64 | 65 | // If browser does not capture in given timeout [ms], kill it 66 | captureTimeout: 60000, 67 | 68 | // Continuous Integration mode 69 | // if true, it capture browsers, run tests and exit 70 | singleRun: false 71 | }); 72 | }; 73 | 74 | }()); 75 | -------------------------------------------------------------------------------- /src/descriptors/element_edge.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Position = require("../values/position.js"); 6 | var PositionDescriptor = require("./position_descriptor.js"); 7 | 8 | var TOP = "top"; 9 | var RIGHT = "right"; 10 | var BOTTOM = "bottom"; 11 | var LEFT = "left"; 12 | 13 | var Me = module.exports = function ElementEdge(element, position) { 14 | var QElement = require("../q_element.js"); // break circular dependency 15 | ensure.signature(arguments, [QElement, String]); 16 | 17 | this.should = this.createShould(); 18 | 19 | if (position === LEFT || position === RIGHT) PositionDescriptor.x(this); 20 | else if (position === TOP || position === BOTTOM) PositionDescriptor.y(this); 21 | else ensure.unreachable("Unknown position: " + position); 22 | 23 | this._element = element; 24 | this._position = position; 25 | }; 26 | PositionDescriptor.extend(Me); 27 | 28 | Me.top = factoryFn(TOP); 29 | Me.right = factoryFn(RIGHT); 30 | Me.bottom = factoryFn(BOTTOM); 31 | Me.left = factoryFn(LEFT); 32 | 33 | Me.prototype.value = function value() { 34 | ensure.signature(arguments, []); 35 | 36 | var rawPosition = this._element.getRawPosition(); 37 | var edge = rawPosition[this._position]; 38 | 39 | var scroll = this._element.context().getRawScrollPosition(); 40 | var rendered = elementRendered(this._element); 41 | 42 | if (this._position === RIGHT || this._position === LEFT) { 43 | if (!rendered) return Position.noX(); 44 | return Position.x(edge + scroll.x); 45 | } 46 | else { 47 | if (!rendered) return Position.noY(); 48 | return Position.y(edge + scroll.y); 49 | } 50 | }; 51 | 52 | Me.prototype.toString = function toString() { 53 | ensure.signature(arguments, []); 54 | return this._position + " edge of " + this._element; 55 | }; 56 | 57 | function factoryFn(position) { 58 | return function factory(element) { 59 | return new Me(element, position); 60 | }; 61 | } 62 | 63 | function elementRendered(element) { 64 | var inDom = element.context().body().contains(element); 65 | var displayNone = element.getRawStyle("display") === "none"; 66 | 67 | return inDom && !displayNone; 68 | } 69 | -------------------------------------------------------------------------------- /src/values/size.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Value = require("./value.js"); 6 | var Pixels = require("./pixels.js"); 7 | 8 | var Me = module.exports = function Size(value) { 9 | ensure.signature(arguments, [ [Number, Pixels] ]); 10 | 11 | this._value = (typeof value === "number") ? Pixels.create(value) : value; 12 | }; 13 | Value.extend(Me); 14 | 15 | Me.create = function create(value) { 16 | return new Me(value); 17 | }; 18 | 19 | Me.createNone = function createNone() { 20 | ensure.signature(arguments, []); 21 | 22 | return new Me(Pixels.NONE); 23 | }; 24 | 25 | Me.prototype.compatibility = function compatibility() { 26 | return [ Me ]; 27 | }; 28 | 29 | Me.prototype.isNone = function isNone() { 30 | return this._value.isNone(); 31 | }; 32 | 33 | Me.prototype.plus = Value.safe(function plus(operand) { 34 | return new Me(this._value.plus(operand._value)); 35 | }); 36 | 37 | Me.prototype.minus = Value.safe(function minus(operand) { 38 | return new Me(this._value.minus(operand._value)); 39 | }); 40 | 41 | Me.prototype.times = function times(operand) { 42 | return new Me(this._value.times(operand)); 43 | }; 44 | 45 | Me.prototype.compare = Value.safe(function compare(that) { 46 | return this._value.compare(that._value); 47 | }); 48 | 49 | Me.prototype.diff = Value.safe(function diff(expected) { 50 | var actualValue = this._value; 51 | var expectedValue = expected._value; 52 | 53 | if (actualValue.equals(expectedValue)) return ""; 54 | if (isNone(expected) && !isNone(this)) return "rendered"; 55 | if (!isNone(expected) && isNone(this)) return "not rendered"; 56 | 57 | var desc = actualValue.compare(expectedValue) > 0 ? " bigger" : " smaller"; 58 | return actualValue.diff(expectedValue) + desc; 59 | }); 60 | 61 | Me.prototype.toString = function toString() { 62 | ensure.signature(arguments, []); 63 | 64 | if (isNone(this)) return "not rendered"; 65 | else return this._value.toString(); 66 | }; 67 | 68 | Me.prototype.toPixels = function toPixels() { 69 | ensure.signature(arguments, []); 70 | return this._value; 71 | }; 72 | 73 | function isNone(size) { 74 | return size._value.equals(Pixels.NONE); 75 | } -------------------------------------------------------------------------------- /test/_assertion_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var reset = require("../src/__reset.js"); 5 | var assert = require("../src/util/assert.js"); 6 | 7 | describe("END-TO-END: Assertion rendering", function() { 8 | 9 | var frame; 10 | var element; 11 | 12 | beforeEach(function() { 13 | frame = reset.frame; 14 | frame.add( 15 | "

    one

    " 16 | ); 17 | element = frame.get("#element"); 18 | }); 19 | 20 | it("handles fractions well", function() { 21 | frame.add( 22 | "

    golden

    " 23 | ); 24 | var goldenRect = frame.get("#golden"); 25 | var goldenRatio = 1.6180339887; 26 | 27 | goldenRect.assert({ 28 | width: goldenRect.height.times(goldenRatio) 29 | }); 30 | }); 31 | 32 | it("provides nice explanation when descriptor doesn't match a hand-coded value", function() { 33 | assert.equal( 34 | element.width.diff(60), 35 | "width of '#element' should be 70px smaller.\n" + 36 | " Expected: 60px\n" + 37 | " But was: 130px" 38 | ); 39 | }); 40 | 41 | it("provides nice explanation when relative difference between elements", function() { 42 | assert.equal( 43 | element.width.diff(element.height), 44 | "width of '#element' should be 70px smaller.\n" + 45 | " Expected: 60px (height of '#element')\n" + 46 | " But was: 130px" 47 | ); 48 | }); 49 | 50 | it("renders multiple differences nicely", function() { 51 | assert.equal( 52 | element.diff({ 53 | width: element.height, 54 | top: 13 55 | }), 56 | "width of '#element' should be 70px smaller.\n" + 57 | " Expected: 60px (height of '#element')\n" + 58 | " But was: 130px\n" + 59 | "top edge of '#element' should be 3px lower.\n" + 60 | " Expected: 13px\n" + 61 | " But was: 10px" 62 | ); 63 | }); 64 | 65 | it("fails nicely when invalid property is diff'd", function() { 66 | assert.exception(function() { 67 | element.diff({ XXX: "non-existant" }); 68 | }, "'#element' doesn't have a property named 'XXX'. Did you misspell it?"); 69 | }); 70 | 71 | }); -------------------------------------------------------------------------------- /docs/ElementRender.md: -------------------------------------------------------------------------------- 1 | # Quixote API: `ElementRender` 2 | 3 | * [Back to overview README.](../README.md) 4 | * [Back to API overview.](api.md) 5 | 6 | `ElementRender` instances represent whether an element is rendered on the page or not. For a complete explanation, see the [`QElement.render`](QElement.md#element-rendering) property. 7 | 8 | 9 | ## Equivalents 10 | 11 | ``` 12 | Stability: 3 - Stable 13 | ``` 14 | 15 | Methods with a `ElementRender equivalent` parameter can take any of the following: 16 | 17 | * An `ElementRender` instance, such as `QElement.render`. 18 | * A boolean, where `true` means the element is rendered and `false` means it isn't. 19 | 20 | 21 | #### Example: `ElementRender` 22 | 23 | ```javascript 24 | // "The image render status should match the lightbox's." 25 | image.render.should.equal(lightbox.render); 26 | ``` 27 | 28 | #### Example: `boolean` 29 | 30 | ```javascript 31 | // "The lightbox should not be rendered." 32 | lightbox.render.should.equal(false); 33 | ``` 34 | 35 | 36 | ## Assertions 37 | 38 | Use these methods to make assertions about the element's render status. In all cases, if the assertion is true, nothing happens. Otherwise, the assertion throws an exception explaining why it failed. 39 | 40 | 41 | ### elementRender.should.equal() 42 | 43 | ``` 44 | Stability: 3 - Stable 45 | ``` 46 | 47 | Check whether the element is rendered. 48 | 49 | `elementRender.should.equal(expectation, message)` 50 | 51 | * `expectation (ElementRender equivalent)` The expected render status. 52 | 53 | * `message (optional string)` A message to include when the assertion fails. 54 | 55 | Example: 56 | 57 | ```javascript 58 | // "The disclaimer banner should be rendered." 59 | disclaimer.render.should.equal(true); 60 | ``` 61 | 62 | 63 | ### elementRender.should.notEqual() 64 | 65 | ``` 66 | Stability: 3 - Stable 67 | ``` 68 | 69 | Check whether the element is rendered opposite to another element. 70 | 71 | `elementRender.should.notEqual(expectation, message)` 72 | 73 | * `expectation (ElementRender equivalent)` The render status to not match. 74 | 75 | * `message (optional string)` A message to include when the assertion fails. 76 | 77 | Example: 78 | 79 | ```javascript 80 | // "The disclaimer banner should not be rendered when the cookie banner is rendered." 81 | disclaimer.render.should.notEqual(cookieBanner.render); 82 | ``` 83 | -------------------------------------------------------------------------------- /src/_q_element_list_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var reset = require("./__reset.js"); 6 | var QElementList = require("./q_element_list.js"); 7 | 8 | describe("FOUNDATION: QElementList", function() { 9 | 10 | var none; 11 | var one; 12 | var some; 13 | var item1; 14 | var item2; 15 | var item3; 16 | 17 | before(function() { 18 | var frame = reset.frame; 19 | var document = frame.toDomElement().contentDocument; 20 | 21 | var list = frame.add("" + 22 | "
      " + 23 | "
    • Item 1
    • " + 24 | "
    • Item 2
    • " + 25 | "
    • Item 3
    • " + 26 | "
    ", "list" 27 | ); 28 | 29 | var noneDom = document.querySelectorAll(".no-such-class"); 30 | var oneDom = document.querySelectorAll("ul"); 31 | var someDom = document.querySelectorAll("li"); 32 | 33 | none = new QElementList(noneDom, "none"); 34 | one = new QElementList(oneDom, "one"); 35 | some = new QElementList(someDom, "some"); 36 | 37 | item1 = frame.get("#item1"); 38 | item2 = frame.get("#item2"); 39 | item3 = frame.get("#item3"); 40 | }); 41 | 42 | it("reports length", function() { 43 | assert.equal(none.length(), 0, "none"); 44 | assert.equal(one.length(), 1, "one"); 45 | assert.equal(some.length(), 3, "some"); 46 | }); 47 | 48 | it("describes itself", function() { 49 | assert.equal(none.toString(), "'none' list"); 50 | }); 51 | 52 | it("retrieves elements by index", function() { 53 | assert.objEqual(some.at(0), item1, "zero-based indexing"); 54 | assert.objEqual(some.at(1), item2, "forward index"); 55 | assert.objEqual(some.at(-1), item3, "backward index"); 56 | }); 57 | 58 | it("describes retrieved elements", function() { 59 | assert.equal(some.at(1).toString(), "'some[1]'", "forward index"); 60 | assert.equal(some.at(-3).toString(), "'some[0]'", "backward index"); 61 | assert.equal(some.at(1, "my name").toString(), "'my name'", "nickname provided"); 62 | }); 63 | 64 | it("fails fast when element out of bounds", function() { 65 | assert.exception(function() { 66 | none.at(0); 67 | }, "'none'[0] is out of bounds; list length is 0", "forward index"); 68 | assert.exception(function() { 69 | some.at(-5); 70 | }, "'some'[-5] is out of bounds; list length is 3", "backward index"); 71 | }); 72 | 73 | }); -------------------------------------------------------------------------------- /example/build/util/mocha_runner.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // Helper function for running Mocha 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var Mocha = require("mocha"); 10 | var jake = require("jake"); 11 | 12 | exports.runTests = function runTests(options, success, failure) { 13 | var mocha = new Mocha(options.options); 14 | var files = deglob(options.files); 15 | files.forEach(mocha.addFile.bind(mocha)); 16 | 17 | // This is a bit of a hack. The issue is this: during test execution, if an exception is thrown inside 18 | // of a callback (and keep in mind that assertions throw exceptions), there's no way for Mocha to catch 19 | // that exception. 20 | // So Mocha registers an 'uncaughtException' handler on Node's process object. That way any unhandled 21 | // exception is passed to Mocha. 22 | // The problem is that Jake ALSO listens for 'uncaughtException'. Its handler and Mocha's handler don't 23 | // get along. Somehow the Jake handler seems to terminate Mocha's test run... not sure why. We need to 24 | // disable Jake's handler while Mocha is running. 25 | // This code disables ALL uncaughtException handlers and then restores them after Mocha is done. It's 26 | // very hacky and likely to cause problems in certain edge cases (for example, '.once' listeners aren't 27 | // restored properly), but it seems to be working for now. 28 | // It might be possible to create a better solution by using Node's 'domain' module. Something to look 29 | // into if you're reading this. Another solution is to just spawn Mocha in a separate process, but I 30 | // didn't want the time penalty involved. Besides, this seems to be working okay. 31 | var savedListeners = disableExceptionListeners(); 32 | 33 | var runner = mocha.run(function(failures) { 34 | restoreExceptionListeners(savedListeners); 35 | if (failures) return failure("Tests failed"); 36 | else return success(); 37 | }); 38 | }; 39 | 40 | function deglob(globs) { 41 | return new jake.FileList(globs).toArray(); 42 | } 43 | 44 | function disableExceptionListeners() { 45 | var listeners = process.listeners("uncaughtException"); 46 | process.removeAllListeners("uncaughtException"); 47 | return listeners; 48 | } 49 | 50 | function restoreExceptionListeners(listeners) { 51 | listeners.forEach(process.addListener.bind(process, "uncaughtException")); 52 | } 53 | 54 | }()); 55 | -------------------------------------------------------------------------------- /src/descriptors/relative_position.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Descriptor = require("./descriptor.js"); 6 | var PositionDescriptor = require("./position_descriptor.js"); 7 | var Value = require("../values/value.js"); 8 | var Size = require("../values/size.js"); 9 | 10 | var X_DIMENSION = "x"; 11 | var Y_DIMENSION = "y"; 12 | var PLUS = 1; 13 | var MINUS = -1; 14 | 15 | var Me = module.exports = function RelativePosition(dimension, direction, relativeTo, relativeAmount) { 16 | ensure.signature(arguments, [ String, Number, Descriptor, [Number, Descriptor, Value] ]); 17 | 18 | this.should = this.createShould(); 19 | 20 | if (dimension === X_DIMENSION) PositionDescriptor.x(this); 21 | else if (dimension === Y_DIMENSION) PositionDescriptor.y(this); 22 | else ensure.unreachable("Unknown dimension: " + dimension); 23 | 24 | this._dimension = dimension; 25 | this._direction = direction; 26 | this._relativeTo = relativeTo; 27 | 28 | if (typeof relativeAmount === "number") { 29 | if (relativeAmount < 0) this._direction *= -1; 30 | this._amount = Size.create(Math.abs(relativeAmount)); 31 | } 32 | else { 33 | this._amount = relativeAmount; 34 | } 35 | }; 36 | PositionDescriptor.extend(Me); 37 | 38 | Me.right = createFn(X_DIMENSION, PLUS); 39 | Me.down = createFn(Y_DIMENSION, PLUS); 40 | Me.left = createFn(X_DIMENSION, MINUS); 41 | Me.up = createFn(Y_DIMENSION, MINUS); 42 | 43 | function createFn(dimension, direction) { 44 | return function create(relativeTo, relativeAmount) { 45 | return new Me(dimension, direction, relativeTo, relativeAmount); 46 | }; 47 | } 48 | 49 | Me.prototype.value = function value() { 50 | ensure.signature(arguments, []); 51 | 52 | var baseValue = this._relativeTo.value(); 53 | var relativeValue = this._amount.value(); 54 | 55 | if (this._direction === PLUS) return baseValue.plus(relativeValue); 56 | else return baseValue.minus(relativeValue); 57 | }; 58 | 59 | Me.prototype.toString = function toString() { 60 | ensure.signature(arguments, []); 61 | 62 | var base = this._relativeTo.toString(); 63 | if (this._amount.equals(Size.create(0))) return base; 64 | 65 | var relation = this._amount.toString(); 66 | if (this._dimension === X_DIMENSION) relation += (this._direction === PLUS) ? " to right of " : " to left of "; 67 | else relation += (this._direction === PLUS) ? " below " : " above "; 68 | 69 | return relation + base; 70 | }; 71 | -------------------------------------------------------------------------------- /src/values/_size_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var Size = require("./size.js"); 6 | var Value = require("./value.js"); 7 | var Pixels = require("./pixels.js"); 8 | 9 | describe("VALUE: Size", function() { 10 | 11 | var a1 = Size.create(52); 12 | var a2 = Size.create(52); 13 | var b = Size.create(7); 14 | var none = Size.createNone(); 15 | 16 | it("is a value object", function() { 17 | assert.implements(a1, Value); 18 | }); 19 | 20 | it("can be constructed from pixels", function() { 21 | assert.objEqual(Size.create(Pixels.create(52)), a1); 22 | }); 23 | 24 | it("knows if it is 'none' or not", function() { 25 | assert.equal(none.isNone(), true, "none"); 26 | assert.equal(a1.isNone(), false, "not none"); 27 | }); 28 | 29 | it("can be be non-rendered", function() { 30 | assert.objEqual(Size.createNone(), none); 31 | }); 32 | 33 | it("performs arithmetic", function() { 34 | assert.objEqual(a1.plus(b), Size.create(59), "plus"); 35 | assert.objEqual(a1.minus(b), Size.create(45), "minus"); 36 | assert.objEqual(b.times(3), Size.create(21), "multiply"); 37 | }); 38 | 39 | it("performs arithmetic on non-rendered values (but result is always non-rendered)", function() { 40 | assert.objEqual(none.plus(none), none, "non-rendered + non-rendered"); 41 | assert.objEqual(a1.plus(none), none, "on-screen + non-rendered"); 42 | assert.objEqual(none.plus(a1), none, "non-rendered + on-screen"); 43 | }); 44 | 45 | it("converts to pixels", function() { 46 | assert.objEqual(a1.toPixels(), Pixels.create(52)); 47 | }); 48 | 49 | it("compares", function() { 50 | assert.equal(a1.compare(b) > 0, true, "bigger"); 51 | assert.equal(b.compare(a1) < 0, true, "smaller"); 52 | assert.equal(b.compare(b) === 0, true, "same"); 53 | }); 54 | 55 | it("describes difference", function() { 56 | assert.equal(a1.diff(a2), "", "same"); 57 | assert.equal(a1.diff(b), "45px bigger", "bigger"); 58 | assert.equal(b.diff(a1), "45px smaller", "smaller"); 59 | 60 | assert.equal(none.diff(none), "", "both non-rendered"); 61 | assert.equal(a1.diff(none), "rendered", "expected non-rendered, but was rendered"); 62 | assert.equal(none.diff(a1), "not rendered", "expected rendered, but was non-rendered"); 63 | }); 64 | 65 | it("converts to string", function() { 66 | assert.equal(a1.toString(), "52px"); 67 | assert.equal(none.toString(), "not rendered"); 68 | }); 69 | 70 | }); -------------------------------------------------------------------------------- /example/build/util/karma_runner.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2015 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // Helper functions for running Karma 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var path = require("path"); 10 | var sh = require("./sh.js"); 11 | var runner = require("karma/lib/runner"); 12 | var server = require("karma/lib/server"); 13 | 14 | var KARMA = "node node_modules/karma/bin/karma"; 15 | 16 | exports.serve = function(configFile, success, fail) { 17 | var command = KARMA + " start " + configFile; 18 | sh.run(command, success, function () { 19 | fail("Could not start Karma server"); 20 | }); 21 | }; 22 | 23 | exports.runTests = function(options, success, fail) { 24 | options.capture = options.capture || []; 25 | var config = { 26 | configFile: path.resolve(options.configFile), 27 | browsers: options.capture, 28 | singleRun: options.capture.length > 0 29 | }; 30 | 31 | var runKarma = runner.run.bind(runner); 32 | if (config.singleRun) runKarma = server.start.bind(server); 33 | 34 | var stdout = new CapturedStdout(); 35 | runKarma(config, function(exitCode) { 36 | stdout.restore(); 37 | 38 | if (exitCode) return fail("Client tests failed (did you start the Karma server?)"); 39 | var browserMissing = checkRequiredBrowsers(options.browsers, stdout); 40 | if (browserMissing && options.strict) return fail("Did not test all browsers"); 41 | if (stdout.capturedOutput.indexOf("TOTAL: 0 SUCCESS") !== -1) return fail("No tests were run!"); 42 | 43 | return success(); 44 | }); 45 | }; 46 | 47 | function checkRequiredBrowsers(requiredBrowsers, stdout) { 48 | var browserMissing = false; 49 | requiredBrowsers.forEach(function(browser) { 50 | browserMissing = lookForBrowser(browser, stdout.capturedOutput) || browserMissing; 51 | }); 52 | return browserMissing; 53 | } 54 | 55 | function lookForBrowser(browser, output) { 56 | var missing = output.indexOf(browser + ": Executed") === -1; 57 | if (missing) console.log("Warning: " + browser + " was not tested!"); 58 | return missing; 59 | } 60 | 61 | function CapturedStdout() { 62 | var self = this; 63 | self.oldStdout = process.stdout.write; 64 | self.capturedOutput = ""; 65 | 66 | process.stdout.write = function(data) { 67 | self.capturedOutput += data; 68 | self.oldStdout.apply(this, arguments); 69 | }; 70 | } 71 | 72 | CapturedStdout.prototype.restore = function() { 73 | process.stdout.write = this.oldStdout; 74 | }; 75 | 76 | }()); -------------------------------------------------------------------------------- /src/descriptors/_size_multiple_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var SizeMultiple = require("./size_multiple.js"); 7 | var Size = require("../values/size.js"); 8 | var SizeDescriptor = require("./size_descriptor.js"); 9 | 10 | describe("DESCRIPTOR: SizeMultiple", function() { 11 | 12 | var WIDTH = 130; 13 | 14 | var element; 15 | var twice; 16 | 17 | beforeEach(function() { 18 | var frame = reset.frame; 19 | frame.add( 20 | "

    element

    " 21 | ); 22 | element = frame.get("#element"); 23 | twice = SizeMultiple.create(element.width, 2); 24 | }); 25 | 26 | it("is a size descriptor", function() { 27 | assert.implements(twice, SizeDescriptor); 28 | }); 29 | 30 | it("resolves to value", function() { 31 | assert.objEqual(twice.value(), Size.create(WIDTH * 2)); 32 | }); 33 | 34 | it("renders to string", function() { 35 | // multiple 36 | check(2, "2 times ", "twice"); 37 | check(52.1838, "52.1838 times ", "any multiple"); 38 | check(1, "", "same"); 39 | 40 | // vulgar fractions 41 | check(1/2, "half of ", "1/2"); 42 | check(1/3, "one-third of ", "1/3"); 43 | check(2/3, "two-thirds of ", "2/3"); 44 | check(1/4, "one-quarter of ", "1/4"); 45 | check(3/4, "three-quarters of ", "3/4"); 46 | check(1/5, "one-fifth of ", "1/5"); 47 | check(2/5, "two-fifths of ", "2/5"); 48 | check(3/5, "three-fifths of ", "3/5"); 49 | check(4/5, "four-fifths of ", "4/5"); 50 | check(1/6, "one-sixth of ", "1/6"); 51 | check(5/6, "five-sixths of ", "5/6"); 52 | check(1/8, "one-eighth of ", "1/8"); 53 | check(3/8, "three-eighths of ", "3/8"); 54 | check(5/8, "five-eighths of ", "5/8"); 55 | check(7/8, "seven-eighths of ", "7/8"); 56 | 57 | // percentages 58 | check(0.1, "10% of ", "10%"); 59 | check(0.42, "42% of ", "arbitrary percentage"); 60 | check(0.12345, "12.345% of ", "decimal percentage"); 61 | 62 | function check(multiple, expected, message) { 63 | var descriptor = SizeMultiple.create(element.width, multiple); 64 | assert.equal(descriptor.toString(), expected + element.width, message); 65 | } 66 | }); 67 | 68 | it("has assertions", function() { 69 | assert.exception( 70 | function() { twice.should.equal(30); }, 71 | "2 times width of '#element' should be 230px smaller.\n" + 72 | " Expected: 30px\n" + 73 | " But was: 260px" 74 | ); 75 | }); 76 | 77 | }); -------------------------------------------------------------------------------- /src/_assertable_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("./util/assert.js"); 5 | var Assertable = require("./assertable.js"); 6 | var reset = require("./__reset.js"); 7 | 8 | describe("FOUNDATION: Assertable abstract base class", function() { 9 | 10 | var TOP = 10; 11 | var RIGHT = 150; 12 | var BOTTOM = 70; 13 | var LEFT = 20; 14 | 15 | var frame; 16 | var element; 17 | 18 | beforeEach(function() { 19 | frame = reset.frame; 20 | frame.add( 21 | "

    one

    " 22 | ); 23 | element = frame.get("#element"); 24 | }); 25 | 26 | it("can be extended", function() { 27 | function Subclass() {} 28 | 29 | Assertable.extend(Subclass); 30 | assert.type(new Subclass(), Assertable); 31 | }); 32 | 33 | it("diffs one property", function() { 34 | var expected = element.top.diff(600); 35 | assert.equal(element.diff({ top: 600 }), expected, "difference"); 36 | assert.equal(element.diff({ top: TOP }), "", "no difference"); 37 | }); 38 | 39 | it("diffs multiple properties", function() { 40 | var topDiff = element.top.diff(600); 41 | var rightDiff = element.right.diff(400); 42 | var bottomDiff = element.bottom.diff(200); 43 | 44 | assert.equal( 45 | element.diff({ top: 600, right: 400, bottom: 200 }), 46 | topDiff + "\n" + rightDiff + "\n" + bottomDiff, 47 | "three differences" 48 | ); 49 | assert.equal(element.diff({ top: TOP, right: RIGHT, bottom: BOTTOM }), "", "no differences"); 50 | assert.equal( 51 | element.diff({ top: 600, right: RIGHT, bottom: 200}), 52 | topDiff + "\n" + bottomDiff, 53 | "two differences, with middle one okay" 54 | ); 55 | assert.equal(element.diff({ top: TOP, right: RIGHT, bottom: 200}), bottomDiff, "one difference"); 56 | }); 57 | 58 | it("supports relative comparisons", function() { 59 | var two = frame.add("
    two
    "); 60 | assert.equal(element.diff({ top: two.top }), "", "relative diff"); 61 | }); 62 | 63 | it("has variant that throws an exception when differences found", function() { 64 | var diff = element.diff({ top: 600 }); 65 | 66 | assert.noException(function() { 67 | element.assert({ top: TOP }); 68 | }, "same"); 69 | 70 | assert.exception(function() { 71 | element.assert({ top: 600 }); 72 | }, "Differences found:\n" + diff + "\n", "different"); 73 | 74 | assert.exception(function() { 75 | element.assert({ top: 600 }, "a message"); 76 | }, "a message:\n" + diff + "\n", "different, with a message"); 77 | }); 78 | 79 | }); -------------------------------------------------------------------------------- /src/browsing_context.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var shim = require("./util/shim.js"); 6 | var QElement = require("./q_element.js"); 7 | var QElementList = require("./q_element_list.js"); 8 | var QViewport = require("./q_viewport.js"); 9 | var QPage = require("./q_page.js"); 10 | 11 | var Me = module.exports = function BrowsingContext(contentDocument) { 12 | ensure.signature(arguments, [Object]); 13 | 14 | this.contentWindow = contentDocument.defaultView || contentDocument.parentWindow; 15 | this.contentDocument = contentDocument; 16 | }; 17 | 18 | Me.prototype.body = function body() { 19 | ensure.signature(arguments, []); 20 | 21 | return QElement.create(this.contentDocument.body, ""); 22 | }; 23 | 24 | Me.prototype.viewport = function viewport() { 25 | ensure.signature(arguments, []); 26 | 27 | return new QViewport(this); 28 | }; 29 | 30 | Me.prototype.page = function page() { 31 | ensure.signature(arguments, []); 32 | 33 | return new QPage(this); 34 | }; 35 | 36 | Me.prototype.add = function add(html, nickname) { 37 | ensure.signature(arguments, [String, [undefined, String]]); 38 | return this.body().add(html, nickname); 39 | }; 40 | 41 | Me.prototype.get = function get(selector, nickname) { 42 | ensure.signature(arguments, [String, [undefined, String]]); 43 | if (nickname === undefined) nickname = selector; 44 | 45 | var nodes = this.contentDocument.querySelectorAll(selector); 46 | ensure.that(nodes.length === 1, "Expected one element to match '" + selector + "', but found " + nodes.length); 47 | return QElement.create(nodes[0], nickname); 48 | }; 49 | 50 | Me.prototype.getAll = function getAll(selector, nickname) { 51 | ensure.signature(arguments, [String, [undefined, String]]); 52 | if (nickname === undefined) nickname = selector; 53 | 54 | return new QElementList(this.contentDocument.querySelectorAll(selector), nickname); 55 | }; 56 | 57 | Me.prototype.scroll = function scroll(x, y) { 58 | ensure.signature(arguments, [Number, Number]); 59 | 60 | this.contentWindow.scroll(x, y); 61 | }; 62 | 63 | Me.prototype.getRawScrollPosition = function getRawScrollPosition() { 64 | ensure.signature(arguments, []); 65 | 66 | return { 67 | x: shim.Window.pageXOffset(this.contentWindow, this.contentDocument), 68 | y: shim.Window.pageYOffset(this.contentWindow, this.contentDocument) 69 | }; 70 | }; 71 | 72 | // This method is not tested--don't know how. 73 | Me.prototype.forceReflow = function forceReflow() { 74 | this.body().toDomElement().offsetTop; 75 | }; 76 | 77 | Me.prototype.equals = function equals(that) { 78 | ensure.signature(arguments, [Me]); 79 | return this.contentWindow === that.contentWindow; 80 | }; 81 | -------------------------------------------------------------------------------- /src/descriptors/_relative_size_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var RelativeSize = require("./relative_size.js"); 7 | var Size = require("../values/size.js"); 8 | var SizeDescriptor = require("./size_descriptor.js"); 9 | 10 | describe("DESCRIPTOR: RelativeSize", function() { 11 | 12 | var element; 13 | var smaller; 14 | var larger; 15 | 16 | var WIDTH = 130; 17 | var HEIGHT = 60; 18 | 19 | beforeEach(function() { 20 | var frame = reset.frame; 21 | frame.add( 22 | "

    element

    " 23 | ); 24 | element = frame.get("#element"); 25 | larger = RelativeSize.larger(element.height, 10); 26 | smaller = RelativeSize.smaller(element.width, 5); 27 | }); 28 | 29 | it("is a size descriptor", function() { 30 | assert.implements(smaller, SizeDescriptor); 31 | }); 32 | 33 | it("resolves to value", function() { 34 | assert.objEqual(larger.value(), Size.create(70), "y"); 35 | assert.objEqual(smaller.value(), Size.create(125), "x"); 36 | }); 37 | 38 | it("computes value relative to a size descriptor", function() { 39 | var rel = RelativeSize.larger(element.height, element.width); 40 | assert.objEqual(rel.value(), Size.create(HEIGHT + WIDTH)); 41 | }); 42 | 43 | it("computes value relative to a relative size descriptor", function() { 44 | var rel = RelativeSize.larger(element.height, element.width.plus(10)); 45 | assert.objEqual(rel.value(), Size.create(HEIGHT + WIDTH + 10)); 46 | }); 47 | 48 | it("converts to string", function() { 49 | assertLarger(element.width, 10, "10px larger than ", "larger +"); 50 | assertLarger(element.width, -15, "15px smaller than ", "larger -"); 51 | assertLarger(element.width, 0, "", "larger 0"); 52 | 53 | assertSmaller(element.width, 10, "10px smaller than ", "smaller +"); 54 | assertSmaller(element.width, -10, "10px larger than ", "smaller -"); 55 | assertSmaller(element.width, 0, "", "smaller 0"); 56 | 57 | function assertLarger(relativeTo, amount, expected, message) { 58 | assert.equal(RelativeSize.larger(relativeTo, amount).toString(), expected + relativeTo, message); 59 | } 60 | 61 | function assertSmaller(relativeTo, amount, expected, message) { 62 | assert.equal(RelativeSize.smaller(relativeTo, amount).toString(), expected + relativeTo, message); 63 | } 64 | }); 65 | 66 | it("has assertions", function() { 67 | assert.exception( 68 | function() { larger.should.equal(30); }, 69 | "10px larger than height of '#element' should be 40px smaller.\n" + 70 | " Expected: 30px\n" + 71 | " But was: 70px" 72 | ); 73 | }); 74 | 75 | }); -------------------------------------------------------------------------------- /docs/QPage.md: -------------------------------------------------------------------------------- 1 | # Quixote API: `QPage` 2 | 3 | * [Back to overview README](../README.md) 4 | * [Back to API overview](api.md) 5 | 6 | `QPage` instances represent the overall browser page. You can get an instance by calling [`QFrame.page()`](QFrame.md#framepage). You'll use its properties in your assertions. 7 | 8 | 9 | ## Properties 10 | 11 | Use these properties in your assertions. 12 | 13 | **Compatibility Notes:** 14 | 15 | * We aren't aware of a standard way to get the dimensions of the page. We have implemented a solution that works on our [tested browsers](../build/config/tested_browsers.js), but it may not work on all browsers. If you use these properties, perform a visual check to make sure they're working as expected. If they aren't, please file an issue. 16 | 17 | * In particular, the current solution for page dimensions only works on pages in standards mode. Specifically, they have been tested on pages using ``. They do *not* work on pages without a doctype. If support for another doctype is important to you, please let us know by opening an issue. 18 | 19 | **Pixel Rounding Note:** Browsers handle pixel rounding in different ways. We consider pixel values to be the same if they're within 0.5px of each other. If you have rounding errors that are *greater* than 0.5px, make sure your test browsers are set to a zoom level of 100%. Zooming can exaggerate rounding errors. 20 | 21 | 22 | ### Positions and Sizes 23 | 24 | ``` 25 | Stability: 3 - Stable 26 | ``` 27 | 28 | These properties describe the dimensions of the entire page, regardless of how much is scrolled out of view. The page is always at least as big as the viewport. By using page properties in your element assertions, you can assert where elements are positioned relative to the overall page. 29 | 30 | * `page.top (`[`PositionDescriptor`](PositionDescriptor.md)`)` The top of the page. 31 | * `page.right (`[`PositionDescriptor`](PositionDescriptor.md)`)` The right side of the page. 32 | * `page.bottom (`[`PositionDescriptor`](PositionDescriptor.md)`)` The bottom of the page. 33 | * `page.left (`[`PositionDescriptor`](PositionDescriptor.md)`)` The left side of the page. 34 | * `page.center (`[`PositionDescriptor`](PositionDescriptor.md)`)` Horizontal center: midway between right and left. 35 | * `page.middle (`[`PositionDescriptor`](PositionDescriptor.md)`)` Vertical middle: midway between top and bottom. 36 | * `page.width (`[`SizeDescriptor`](SizeDescriptor.md)`)` Width of the page. 37 | * `page.height (`[`SizeDescriptor`](SizeDescriptor.md)`)` Height of the page. 38 | 39 | Example: 40 | 41 | ```javascript 42 | var page = frame.page(); 43 | sidebar.right.should.equal(page.right); // The sidebar should be flush to the right side of the page 44 | sidebar.height.should.equal(page.height); // The sidebar height should equal the page height 45 | ``` -------------------------------------------------------------------------------- /src/values/_value_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var Value = require("./value.js"); 6 | 7 | describe("VALUE: abstract base class", function() { 8 | 9 | var a1; 10 | var a2; 11 | var b; 12 | 13 | beforeEach(function() { 14 | a1 = new Example("a"); 15 | a2 = new Example("a"); 16 | b = new Example("b"); 17 | }); 18 | 19 | it("can be extended", function() { 20 | function Subclass() {} 21 | 22 | Value.extend(Subclass); 23 | assert.type(new Subclass(), Value); 24 | }); 25 | 26 | it("responds to value() with itself", function() { 27 | assert.equal(a1.value(), a1); // note identity comparison, not objEqual() 28 | }); 29 | 30 | it("determines equality (relies on `diff()`)", function() { 31 | assert.objEqual(a1, a2, "same"); 32 | assert.objNotEqual(a1, b, "different"); 33 | }); 34 | 35 | it("determines if two instances are compatible", function() { 36 | var example = new Example("example"); 37 | 38 | assert.equal(example.isCompatibleWith(new Example("")), true, "same type"); 39 | assert.equal(example.isCompatibleWith(new CompatibleExample()), true, "compatible"); 40 | assert.equal(example.isCompatibleWith(new IncompatibleExample()), false, "incompatible"); 41 | assert.equal(example.isCompatibleWith("primitive"), false, "primitives always incompatible"); 42 | }); 43 | 44 | describe("safety check", function() { 45 | 46 | it("does nothing when object is compatible", function() { 47 | assert.noException(function() { 48 | a1.diff(new CompatibleExample()); 49 | }, "in compatibility list"); 50 | }); 51 | 52 | it("fails fast when operating on incompatible types", function() { 53 | check(undefined, "undefined"); 54 | check(null, "null"); 55 | check(true, "boolean"); 56 | check("foo", "string"); 57 | check(function() {}, "function"); 58 | check({}, "Object"); 59 | check(new IncompatibleExample(), "IncompatibleExample"); 60 | 61 | function check(arg, expected) { 62 | assert.exception(function() { 63 | a1.diff(arg); 64 | }, "A descriptor doesn't make sense. (Example can't combine with " + expected + ")", expected); 65 | } 66 | }); 67 | 68 | }); 69 | 70 | function Example(value) { 71 | this._value = value; 72 | } 73 | Value.extend(Example); 74 | 75 | Example.prototype.compatibility = function compatibility() { 76 | return [ Example, CompatibleExample ]; 77 | }; 78 | 79 | Example.prototype.diff = Value.safe(function diff(expected) { 80 | return (this._value === expected._value) ? "" : "different"; 81 | }); 82 | 83 | Example.prototype.toString = function toString() { 84 | return "" + this._value; 85 | }; 86 | 87 | function CompatibleExample() {} 88 | function IncompatibleExample() {} 89 | 90 | }); -------------------------------------------------------------------------------- /src/descriptors/size_descriptor.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | /*eslint new-cap: "off" */ 3 | "use strict"; 4 | 5 | var ensure = require("../util/ensure.js"); 6 | var oop = require("../util/oop.js"); 7 | var Descriptor = require("./descriptor.js"); 8 | var Size = require("../values/size.js"); 9 | 10 | function RelativeSize() { 11 | return require("./relative_size.js"); // break circular dependency 12 | } 13 | 14 | function SizeMultiple() { 15 | return require("./size_multiple.js"); // break circular dependency 16 | } 17 | 18 | var Me = module.exports = function SizeDescriptor() { 19 | ensure.unreachable("SizeDescriptor is abstract and should not be constructed directly."); 20 | }; 21 | Descriptor.extend(Me); 22 | Me.extend = oop.extendFn(Me); 23 | 24 | Me.prototype.createShould = function() { 25 | var self = this; 26 | 27 | var should = Descriptor.prototype.createShould.call(this); 28 | should.beBiggerThan = assertFn(-1, true); 29 | should.beSmallerThan = assertFn(1, false); 30 | return should; 31 | 32 | function assertFn(direction, shouldBeBigger) { 33 | return function(expected, message) { 34 | self.doAssertion(expected, message, function(actualValue, expectedValue, expectedDesc, message) { 35 | if (expectedValue.isNone()) { 36 | throw new Error("'expected' value is not rendered, so relative comparisons aren't possible."); 37 | } 38 | 39 | var expectedMsg = (shouldBeBigger ? "more than" : "less than") + " " + expectedDesc; 40 | 41 | if (actualValue.isNone()) { 42 | return errorMessage(message, "rendered", expectedMsg, actualValue); 43 | } 44 | 45 | var compare = actualValue.compare(expectedValue); 46 | if ((shouldBeBigger && compare <= 0) || (!shouldBeBigger && compare >= 0)) { 47 | var nudge = shouldBeBigger ? -1 : 1; 48 | var shouldBe = "at least " + expectedValue.diff(self.plus(nudge).value()); 49 | return errorMessage(message, shouldBe, expectedMsg, actualValue); 50 | } 51 | }); 52 | }; 53 | } 54 | 55 | function errorMessage(message, shouldBe, expected, actual) { 56 | return message + self + " should be " + shouldBe + ".\n" + 57 | " Expected: " + expected + "\n" + 58 | " But was: " + actual; 59 | } 60 | 61 | }; 62 | 63 | Me.prototype.plus = function plus(amount) { 64 | return RelativeSize().larger(this, amount); 65 | }; 66 | 67 | Me.prototype.minus = function minus(amount) { 68 | return RelativeSize().smaller(this, amount); 69 | }; 70 | 71 | Me.prototype.times = function times(amount) { 72 | return SizeMultiple().create(this, amount); 73 | }; 74 | 75 | Me.prototype.convert = function convert(arg, type) { 76 | switch(type) { 77 | case "number": return Size.create(arg); 78 | case "string": return arg === "none" ? Size.createNone() : undefined; 79 | default: return undefined; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /docs/QViewport.md: -------------------------------------------------------------------------------- 1 | # Quixote API: `QViewport` 2 | 3 | * [Back to overview README](../README.md) 4 | * [Back to API overview](api.md) 5 | 6 | `QViewport` instances represent the part of the page that's visible in the test frame, not including scrollbars. You can get an instance by calling [`QFrame.viewport()`](QFrame.md#frameviewport). You'll use its properties in your assertions. 7 | 8 | 9 | ## Properties 10 | 11 | Use these properties in your assertions. 12 | 13 | **Compatibility Notes:** 14 | 15 | * Although there *is* a standard way to get the dimensions of the viewport, and we've confirmed that it works on our [tested browsers](../build/config/tested_browsers.js), it may not be supported properly by all browsers. If you use these properties, perform a visual check to make sure they're working as expected. If they aren't, please file an issue. 16 | 17 | * In particular, the current solution for viewport dimensions only works on pages in standards mode. Specifically, they have been tested on pages using ``. They do *not* work on pages without a doctype. If support for another doctype is important to you, please let us know by opening an issue. 18 | 19 | * Older versions of Mobile Safari ignored the `width` and `height` attributes on an iframe, as described in the compatibility note for [`quixote.createFrame()`](quixote.md#quixotecreateframe). This can result in viewport properties returning larger-than-expected values. 20 | 21 | **Pixel Rounding Note:** Browsers handle pixel rounding in different ways. We consider pixel values to be the same if they're within 0.5px of each other. If you have rounding errors that are *greater* than 0.5px, make sure your test browsers are set to a zoom level of 100%. Zooming can exaggerate rounding errors. 22 | 23 | 24 | ### Positions and Sizes 25 | 26 | ``` 27 | Stability: 3 - Stable 28 | ``` 29 | 30 | These properties describe the dimension of the viewport. By them in your element assertions, you can assert what's visible to the user. 31 | 32 | * `viewport.top (`[`PositionDescriptor`](PositionDescriptor.md)`)` The highest visible part of the page. 33 | * `viewport.right (`[`PositionDescriptor`](PositionDescriptor.md)`)` The rightmost visible part of the page. 34 | * `viewport.bottom (`[`PositionDescriptor`](PositionDescriptor.md)`)` The lowest visible part of the page. 35 | * `viewport.left (`[`PositionDescriptor`](PositionDescriptor.md)`)` The leftmost visible part of the page. 36 | * `viewport.center (`[`PositionDescriptor`](PositionDescriptor.md)`)` Horizontal center: midway between right and left. 37 | * `viewport.middle (`[`PositionDescriptor`](PositionDescriptor.md)`)` Vertical middle: midway between top and bottom. 38 | * `viewport.width (`[`SizeDescriptor`](SizeDescriptor.md)`)` Width of the viewport. 39 | * `viewport.height (`[`SizeDescriptor`](SizeDescriptor.md)`)` Height of the viewport. 40 | 41 | Example: 42 | 43 | ```javascript 44 | var viewport = frame.viewport(); 45 | disclaimer.bottom.should.equal(viewport.bottom); // The disclaimer should be flush to the bottom of the viewport 46 | disclaimer.width.should.equal(viewport.width); // The disclaimer width should equal the viewport width 47 | ``` 48 | -------------------------------------------------------------------------------- /src/descriptors/_span_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var SizeDescriptor = require("./size_descriptor.js"); 7 | var PositionDescriptor = require("./position_descriptor.js"); 8 | var Span = require("./span.js"); 9 | var Position = require("../values/position.js"); 10 | var Size = require("../values/size.js"); 11 | 12 | var IRRELEVANT_POSITION = 42; 13 | var IRRELEVANT_DESCRIPTION = "irrelevant"; 14 | 15 | describe("DESCRIPTOR: Span", function() { 16 | 17 | it("is a descriptor", function() { 18 | assert.implements(xSpan(IRRELEVANT_POSITION, IRRELEVANT_POSITION), SizeDescriptor); 19 | }); 20 | 21 | it("resolves to value", function() { 22 | assert.objEqual(xSpan(10, 30).value(), Size.create(20), "forward"); 23 | assert.objEqual(xSpan(30, 10).value(), Size.create(20), "backward"); 24 | }); 25 | 26 | it("renders to a string", function() { 27 | assert.equal(xSpan(IRRELEVANT_POSITION, IRRELEVANT_POSITION, "my description").toString(), "my description"); 28 | }); 29 | 30 | it("has assertions", function() { 31 | assert.exception( 32 | function() { xSpan(10, 30, "size").should.equal(30); }, 33 | "size should be 10px bigger.\n" + 34 | " Expected: 30px\n" + 35 | " But was: 20px" 36 | ); 37 | }); 38 | 39 | it("has horizontal center", function() { 40 | var center = xSpan(10, 30, "my description").center; 41 | 42 | assert.objEqual(center.value(), Position.x(20), "value"); 43 | assert.equal(center.toString(), "center of my description", "description"); 44 | }); 45 | 46 | it("has vertical middle", function() { 47 | var middle = ySpan(10, 30, "my description").middle; 48 | 49 | assert.objEqual(middle.value(), Position.y(20), "value"); 50 | assert.equal(middle.toString(), "middle of my description", "description"); 51 | }); 52 | 53 | it("fails fast when asking for horizontal center of vertical span", function() { 54 | assert.exception( 55 | function() { ySpan(10, 30).center.should.equal(20); }, 56 | /Can't compare X coordinate to Y coordinate/ 57 | ); 58 | }); 59 | 60 | it("fails fast when asking vertical middle of horizontal span", function() { 61 | assert.exception( 62 | function() { xSpan(10, 30).middle.should.equal(20); }, 63 | /Can't compare X coordinate to Y coordinate/ 64 | ); 65 | }); 66 | 67 | }); 68 | 69 | function xSpan(from, to, description) { 70 | if (description === undefined) description = IRRELEVANT_DESCRIPTION; 71 | return Span.create(new TestPosition(Position.x(from)), new TestPosition(Position.x(to)), description); 72 | } 73 | 74 | function ySpan(from, to, description) { 75 | if (description === undefined) description = IRRELEVANT_DESCRIPTION; 76 | return Span.create(new TestPosition(Position.y(from)), new TestPosition(Position.y(to)), description); 77 | } 78 | 79 | function TestPosition(position) { 80 | this._position = position; 81 | } 82 | PositionDescriptor.extend(TestPosition); 83 | TestPosition.prototype.value = function value() { return this._position; }; 84 | TestPosition.prototype.toString = function toString() { return "test position: " + this._position; }; 85 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Quixote API 2 | 3 | For an overview, installation notes, and an example, see [the readme](../README.md). 4 | 5 | 6 | ## Quick Reference 7 | 8 | * Create the Quixote test frame with [`quixote.createFrame()`](quixote.md#quixotecreateframe). 9 | * Add test elements to the frame with [`QFrame.add()`](QFrame.md#frameadd). 10 | * Get elements from the frame with [`QFrame.get()`](QFrame.md#frameget). 11 | * Reset the frame with [`QFrame.reset()`](QFrame.md#framereset) or [`QFrame.reload()`](QFrame.md#framereload). 12 | * Make assertions with [`QElement`](QElement.md) properties. 13 | * When the property you want doesn't exist, use [`QElement.getRawStyle()`](QElement.md#elementgetrawstyle). 14 | 15 | 16 | ## Classes and Modules 17 | 18 | * [`quixote`](quixote.md) Create the Quixote test frame, wrap DOM elements, and check browser compatibility. 19 | * [`QFrame`](QFrame.md) Manipulate the DOM inside your test frame. 20 | * [`QElement`](QElement.md) Manipulate, make assertions about, and get styling information for a specific element. 21 | * [`QElementList`](QElementList.md) Multiple QElements. 22 | * [`QPage`](QPage.md) Information about the overall page. 23 | * [`QViewport`](QViewport.md) Information about the viewport (the part of the page you can see). 24 | 25 | ### Descriptor classes 26 | 27 | * [`PositionDescriptor`](PositionDescriptor.md) X and Y coordinates. 28 | * [`SizeDescriptor`](SizeDescriptor.md) Widths, heights, and distances. 29 | * [`ElementRender`](ElementRender.md) Render boundaries. 30 | * [`Span`](Span.md) Imaginary lines between two X or Y coordinates. 31 | 32 | 33 | ## Backwards Compatibility 34 | 35 | We strive to maintain backwards compatibility. Breaking changes to the API will be described in the [change log](../CHANGELOG.md). 36 | 37 | That said, **any class, property, or method that isn't described in the API documentation is not for public use and may change at any time.** Class names may change at any time. Don't construct classes manually or refer to them by name. Any object you need can be obtained from a property or method call. 38 | 39 | Each section of the API is marked with a *stability index* inspired by Node.js. They have the following meaning: 40 | 41 | ``` 42 | Stability: 0 - Deprecated 43 | ``` 44 | 45 | This feature is known to be problematic, and changes are planned. Do not rely on it. Use of the feature may cause warnings. Backwards compatibility should not be expected. 46 | 47 | ``` 48 | Stability: 1 - Experimental 49 | ``` 50 | 51 | This feature was introduced recently, and may change or be removed in future versions. Please try it out and provide feedback. If it addresses a use-case that is important to you, tell the core team. 52 | 53 | ``` 54 | Stability: 2 - Unstable 55 | ``` 56 | 57 | The API is in the process of settling, but has not yet had sufficient real-world testing to be considered stable. 58 | 59 | ``` 60 | Stability: 3 - Stable 61 | ``` 62 | 63 | The API has proven satisfactory, but cleanup in the underlying code may cause minor changes. Backwards-compatibility will be maintained as much as possible. 64 | 65 | ``` 66 | Stability: 4 - API Frozen 67 | ``` 68 | 69 | This API has been tested extensively in production and is unlikely to ever have to change. 70 | 71 | ``` 72 | Stability: 5 - Locked 73 | ``` 74 | 75 | Unless serious bugs are found, this code will not ever change. Please do not suggest changes in this area; they will be refused. 76 | -------------------------------------------------------------------------------- /src/values/pixels.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Value = require("./value.js"); 6 | 7 | var Me = module.exports = function Pixels(amount) { 8 | ensure.signature(arguments, [ [ Number, null ] ]); 9 | this._none = (amount === null); 10 | this._amount = amount; 11 | }; 12 | Value.extend(Me); 13 | 14 | Me.create = function create(amount) { 15 | return new Me(amount); 16 | }; 17 | 18 | Me.createNone = function createNone() { 19 | return new Me(null); 20 | }; 21 | 22 | Me.ZERO = Me.create(0); 23 | Me.NONE = Me.createNone(); 24 | 25 | Me.prototype.compatibility = function compatibility() { 26 | return [ Me ]; 27 | }; 28 | 29 | Me.prototype.isNone = function() { 30 | ensure.signature(arguments, []); 31 | return this._none; 32 | }; 33 | 34 | Me.prototype.plus = Value.safe(function plus(operand) { 35 | if (this._none || operand._none) return Me.createNone(); 36 | return new Me(this._amount + operand._amount); 37 | }); 38 | 39 | Me.prototype.minus = Value.safe(function minus(operand) { 40 | if (this._none || operand._none) return Me.createNone(); 41 | return new Me(this._amount - operand._amount); 42 | }); 43 | 44 | Me.prototype.difference = Value.safe(function difference(operand) { 45 | if (this._none || operand._none) return Me.createNone(); 46 | return new Me(Math.abs(this._amount - operand._amount)); 47 | }); 48 | 49 | Me.prototype.times = function times(operand) { 50 | ensure.signature(arguments, [ Number ]); 51 | 52 | if (this._none) return Me.createNone(); 53 | return new Me(this._amount * operand); 54 | }; 55 | 56 | Me.prototype.average = Value.safe(function average(operand) { 57 | if (this._none || operand._none) return Me.createNone(); 58 | return new Me((this._amount + operand._amount) / 2); 59 | }); 60 | 61 | Me.prototype.compare = Value.safe(function compare(operand) { 62 | var bothHavePixels = !this._none && !operand._none; 63 | var neitherHavePixels = this._none && operand._none; 64 | var onlyLeftHasPixels = !this._none && operand._none; 65 | 66 | if (bothHavePixels) { 67 | var difference = this._amount - operand._amount; 68 | if (Math.abs(difference) <= 0.5) return 0; 69 | else return difference; 70 | } 71 | else if (neitherHavePixels) { 72 | return 0; 73 | } 74 | else if (onlyLeftHasPixels) { 75 | return 1; 76 | } 77 | else { 78 | return -1; 79 | } 80 | }); 81 | 82 | Me.min = function(l, r) { 83 | ensure.signature(arguments, [ Me, Me ]); 84 | 85 | if (l._none || r._none) return Me.createNone(); 86 | return l.compare(r) <= 0 ? l : r; 87 | }; 88 | 89 | Me.max = function(l, r) { 90 | ensure.signature(arguments, [ Me, Me ]); 91 | 92 | if (l._none || r._none) return Me.createNone(); 93 | return l.compare(r) >= 0 ? l : r; 94 | }; 95 | 96 | Me.prototype.diff = Value.safe(function diff(expected) { 97 | if (this.compare(expected) === 0) return ""; 98 | if (this._none || expected._none) return "non-measurable"; 99 | 100 | var difference = Math.abs(this._amount - expected._amount); 101 | 102 | var desc = difference; 103 | if (difference * 100 !== Math.floor(difference * 100)) desc = "about " + difference.toFixed(2); 104 | return desc + "px"; 105 | }); 106 | 107 | Me.prototype.toString = function toString() { 108 | ensure.signature(arguments, []); 109 | return this._none ? "no pixels" : this._amount + "px"; 110 | }; 111 | -------------------------------------------------------------------------------- /src/util/_oop_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("./assert.js"); 5 | var oop = require("./oop.js"); 6 | var shim = require("./shim.js"); 7 | 8 | describe("UTIL: OOP module", function() { 9 | 10 | it("determines name of class", function() { 11 | function Me() {} 12 | var Anon = function() {}; 13 | 14 | assert.equal(oop.className(Me), "Me", "named class"); 15 | // WORKAROUND Chrome 51: Chrome automatically names the function after the variable ("Anon" in this case), 16 | // so it doesn't have unnamed classes. We use the 'if' clause to skip this assertion on Chrome. 17 | if (Anon.name !== "Anon") assert.equal(oop.className(Anon), "", "unnamed class"); 18 | 19 | assert.exception(function() { 20 | console.log(oop.className({})); 21 | }, /Not a constructor/, "not a class"); 22 | }); 23 | 24 | it("determines name of object's class", function() { 25 | // WORKAROUND IE 8: The IE 8 getPrototypeOf shim is incomplete, so we skip this test 26 | if (!Object.getPrototypeOf) return; 27 | 28 | function Me() {} 29 | assert.equal(oop.instanceName(new Me()), "Me", "named class"); 30 | 31 | var Anon = function() {}; 32 | // WORKAROUND Chrome 51: Chrome automatically names the function after the variable ("Anon" in this case), 33 | // so it doesn't have unnamed classes. We use the 'if' clause to skip this assertion on Chrome. 34 | if (Anon.name !== "Anon") assert.equal(oop.instanceName(new Anon()), "", "unnamed class"); 35 | 36 | function BadConstructor() {} 37 | BadConstructor.prototype.constructor = undefined; 38 | assert.equal(oop.instanceName(new BadConstructor()), "", "undefined constructor"); 39 | BadConstructor.prototype.constructor = null; 40 | assert.equal(oop.instanceName(new BadConstructor()), "", "null constructor"); 41 | BadConstructor.prototype.constructor = "foo"; 42 | assert.equal(oop.instanceName(new BadConstructor()), "", "non-function constructor"); 43 | 44 | var noPrototype = shim.Object.create(null); 45 | assert.equal(oop.instanceName(noPrototype), "", "no prototype"); 46 | }); 47 | 48 | it("creates extend function", function() { 49 | // WORKAROUND IE 8: The IE 8 getPrototypeOf shim is incomplete, so we skip this test 50 | // The production code works fine on IE 8, but the test relies on getPrototypeOf(). 51 | if (!Object.getPrototypeOf) return; 52 | 53 | function Parent() {} 54 | Parent.extend = oop.extendFn(Parent); 55 | 56 | function Child() {} 57 | Parent.extend(Child); 58 | 59 | assert.equal(shim.Object.getPrototypeOf(Child.prototype).constructor, Parent, "prototype chain"); 60 | assert.equal(Child.prototype.constructor, Child, "constructor property"); 61 | }); 62 | 63 | it("turns a class into an abstract base class", function() { 64 | // WORKAROUND IE 8: IE 8 doesn't have function.bind and I'm too lazy to implement a shim for it right now. 65 | if (!Function.prototype.bind) return; 66 | 67 | function Parent() {} 68 | Parent.extend = oop.extendFn(Parent); 69 | 70 | oop.makeAbstract(Parent, [ "foo", "bar", "baz" ]); 71 | var obj = new Parent(); 72 | 73 | assert.exception(obj.foo.bind(obj), "Parent subclasses must implement foo() method", "foo()"); 74 | assert.exception(obj.bar.bind(obj), "Parent subclasses must implement bar() method", "bar()"); 75 | assert.exception(obj.baz.bind(obj), "Parent subclasses must implement baz() method", "baz()"); 76 | 77 | function Child() {} 78 | Parent.extend(Child); 79 | Child.prototype.baz = function() {}; 80 | 81 | assert.deepEqual( 82 | new Child().checkAbstractMethods(), 83 | [ "foo()", "bar()" ], 84 | "should know which methods are unimplemented" 85 | ); 86 | }); 87 | 88 | }); -------------------------------------------------------------------------------- /example/build/scripts/build.jakefile.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // Main build file. Contains all tasks needed for normal development. 4 | // There's no Quixote-specific configuration in this file. 5 | 6 | (function() { 7 | "use strict"; 8 | 9 | var startTime = Date.now(); 10 | 11 | var shell = require("shelljs"); 12 | var jshint = require("simplebuild-jshint"); 13 | var karma = require("simplebuild-karma"); 14 | var browserify = require("../util/browserify_runner.js"); 15 | 16 | var browsers = require("../config/tested_browsers.js"); 17 | var jshintConfig = require("../config/jshint.conf.js"); 18 | var paths = require("../config/paths.js"); 19 | 20 | var KARMA_CONFIG = "./build/config/karma.conf.js"; 21 | 22 | var strict = !process.env.loose; 23 | 24 | 25 | //*** GENERAL 26 | 27 | desc("Lint and test"); 28 | task("default", [ "lint", "test" ], function() { 29 | var elapsedSeconds = (Date.now() - startTime) / 1000; 30 | console.log("\n\nBUILD OK (" + elapsedSeconds.toFixed(2) + "s)"); 31 | }); 32 | 33 | desc("Start server (for manual testing)"); 34 | task("run", [ "build" ], function() { 35 | jake.exec("node ./node_modules/http-server/bin/http-server " + paths.clientDistDir, { interactive: true }, complete); 36 | }, { async: true }); 37 | 38 | desc("Delete generated files"); 39 | task("clean", function() { 40 | shell.rm("-rf", paths.generatedDir); 41 | }); 42 | 43 | 44 | //*** LINT 45 | 46 | desc("Lint everything"); 47 | task("lint", ["lintNode", "lintClient"]); 48 | 49 | task("lintNode", function() { 50 | process.stdout.write("Linting Node.js code: "); 51 | jshint.checkFiles({ 52 | files: [ paths.buildDir + "/**/*.js" ], 53 | options: jshintConfig.nodeOptions, 54 | globals: jshintConfig.nodeGlobals 55 | }, complete, fail); 56 | }, { async: true }); 57 | 58 | task("lintClient", function() { 59 | process.stdout.write("Linting browser code: "); 60 | jshint.checkFiles({ 61 | files: [ paths.clientDir + "/**/*.js" ], 62 | options: jshintConfig.clientOptions, 63 | globals: jshintConfig.clientGlobals 64 | }, complete, fail); 65 | }, { async: true }); 66 | 67 | 68 | //*** TEST 69 | 70 | desc("Start Karma server -- run this first"); 71 | task("karma", function() { 72 | karma.start({ 73 | configFile: KARMA_CONFIG 74 | }, complete, fail); 75 | }, { async: true }); 76 | 77 | desc("Run tests"); 78 | task("test", function() { 79 | console.log("Testing browser code: "); 80 | 81 | var browsersToCapture = process.env.capture ? process.env.capture.split(",") : []; 82 | karma.run({ 83 | configFile: KARMA_CONFIG, 84 | expectedBrowsers: browsers, 85 | strict: strict, 86 | capture: browsersToCapture 87 | }, complete, fail); 88 | }, { async: true }); 89 | 90 | 91 | //*** BUILD 92 | 93 | desc("Build distribution package"); 94 | task("build", [ "prepDistDir", "buildClient" ]); 95 | 96 | task("prepDistDir", function() { 97 | shell.rm("-rf", paths.distDir); 98 | }); 99 | 100 | task("buildClient", [ paths.clientDistDir, "bundleClientJs" ], function() { 101 | console.log("Copying client code: ."); 102 | shell.cp( 103 | paths.clientDir + "/*.html", 104 | paths.clientDir + "/*.css", 105 | paths.clientDir + "/*.svg", 106 | paths.clientDistDir 107 | ); 108 | }); 109 | 110 | task("bundleClientJs", [ paths.clientDistDir ], function() { 111 | console.log("Bundling browser code with Browserify: ."); 112 | browserify.bundle({ 113 | entry: paths.clientEntryPoint, 114 | outfile: paths.clientDistBundle, 115 | options: { 116 | standalone: "toggle", 117 | debug: true 118 | } 119 | }, complete, fail); 120 | }, { async: true }); 121 | 122 | 123 | //*** CREATE DIRECTORIES 124 | 125 | directory(paths.testDir); 126 | directory(paths.clientDistDir); 127 | 128 | }()); -------------------------------------------------------------------------------- /src/values/position.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var Value = require("./value.js"); 6 | var Pixels = require("./pixels.js"); 7 | var Size = require("./size.js"); 8 | 9 | var X_DIMENSION = "x"; 10 | var Y_DIMENSION = "y"; 11 | 12 | var Me = module.exports = function Position(dimension, value) { 13 | ensure.signature(arguments, [ String, [ Number, Pixels ] ]); 14 | 15 | this._dimension = dimension; 16 | this._value = (typeof value === "number") ? Pixels.create(value) : value; 17 | }; 18 | Value.extend(Me); 19 | 20 | Me.x = function x(value) { 21 | ensure.signature(arguments, [ [ Number, Pixels ] ]); 22 | 23 | return new Me(X_DIMENSION, value); 24 | }; 25 | 26 | Me.y = function y(value) { 27 | ensure.signature(arguments, [ [ Number, Pixels ] ]); 28 | 29 | return new Me(Y_DIMENSION, value); 30 | }; 31 | 32 | Me.noX = function noX() { 33 | ensure.signature(arguments, []); 34 | 35 | return new Me(X_DIMENSION, Pixels.NONE); 36 | }; 37 | 38 | Me.noY = function noY() { 39 | ensure.signature(arguments, []); 40 | 41 | return new Me(Y_DIMENSION, Pixels.NONE); 42 | }; 43 | 44 | Me.prototype.compatibility = function compatibility() { 45 | return [ Me, Size ]; 46 | }; 47 | 48 | Me.prototype.isNone = function isNone() { 49 | return this._value.isNone(); 50 | }; 51 | 52 | Me.prototype.distanceTo = function(operand) { 53 | ensure.signature(arguments, [ Me ]); 54 | checkAxis(this, operand); 55 | return Size.create(this._value.difference(operand.toPixels())); 56 | }; 57 | 58 | Me.prototype.plus = Value.safe(function plus(operand) { 59 | checkAxis(this, operand); 60 | return new Me(this._dimension, this._value.plus(operand.toPixels())); 61 | }); 62 | 63 | Me.prototype.minus = Value.safe(function minus(operand) { 64 | checkAxis(this, operand); 65 | return new Me(this._dimension, this._value.minus(operand.toPixels())); 66 | }); 67 | 68 | Me.prototype.midpoint = Value.safe(function midpoint(operand) { 69 | checkAxis(this, operand); 70 | return new Me(this._dimension, this._value.average(operand.toPixels())); 71 | }); 72 | 73 | Me.prototype.compare = Value.safe(function compare(operand) { 74 | checkAxis(this, operand); 75 | return this._value.compare(operand.toPixels()); 76 | }); 77 | 78 | Me.prototype.min = Value.safe(function min(operand) { 79 | checkAxis(this, operand); 80 | return new Me(this._dimension, Pixels.min(this._value, operand.toPixels())); 81 | }); 82 | 83 | Me.prototype.max = Value.safe(function max(operand) { 84 | checkAxis(this, operand); 85 | return new Me(this._dimension, Pixels.max(this._value, operand.toPixels())); 86 | }); 87 | 88 | Me.prototype.diff = Value.safe(function diff(expected) { 89 | ensure.signature(arguments, [ Me ]); 90 | checkAxis(this, expected); 91 | 92 | var actualValue = this._value; 93 | var expectedValue = expected._value; 94 | 95 | if (actualValue.equals(expectedValue)) return ""; 96 | else if (isNone(expected) && !isNone(this)) return "rendered"; 97 | else if (!isNone(expected) && isNone(this)) return "not rendered"; 98 | 99 | var direction; 100 | var comparison = actualValue.compare(expectedValue); 101 | if (this._dimension === X_DIMENSION) direction = comparison < 0 ? "to left" : "to right"; 102 | else direction = comparison < 0 ? "higher" : "lower"; 103 | 104 | return actualValue.diff(expectedValue) + " " + direction; 105 | }); 106 | 107 | Me.prototype.toString = function toString() { 108 | ensure.signature(arguments, []); 109 | 110 | if (isNone(this)) return "not rendered"; 111 | else return this._value.toString(); 112 | }; 113 | 114 | Me.prototype.toPixels = function toPixels() { 115 | ensure.signature(arguments, []); 116 | return this._value; 117 | }; 118 | 119 | function checkAxis(self, other) { 120 | if (other instanceof Me) { 121 | ensure.that(self._dimension === other._dimension, "Can't compare X coordinate to Y coordinate"); 122 | } 123 | } 124 | 125 | function isNone(position) { 126 | return position._value.equals(Pixels.NONE); 127 | } -------------------------------------------------------------------------------- /docs/Span.md: -------------------------------------------------------------------------------- 1 | # Quixote API: `Span` 2 | 3 | * [Back to overview README.](../README.md) 4 | * [Back to API overview.](api.md) 5 | 6 | `Span` instances represent an imaginary line between two X or Y coordinates. They can be horizontal or vertical, but not diagonal. They are created by [`PositionDescriptor.to()`](PositionDescriptor.md#positionto). 7 | 8 | If either end of the span is not rendered, the whole span is considered to be not rendered. 9 | 10 | 11 | ## Assertions 12 | 13 | Use these methods to make assertions about the size of the span. In all cases, if the assertion is true, nothing happens. Otherwise, the assertion throws an exception explaining why it failed. 14 | 15 | Span sizes are always positive. 16 | 17 | 18 | ### Equality 19 | 20 | ``` 21 | Stability: 3 - Stable 22 | ``` 23 | 24 | Check whether the length of a span matches a size. 25 | 26 | * `span.should.equal(expectation, message)` Assert that the span length matches the expectation. 27 | * `span.should.notEqual(expectation, message)` Assert that the span length does not match the expectation. 28 | 29 | Parameters: 30 | 31 | * `expectation (SizeDescriptor equivalent)` The size to compare against. 32 | 33 | * `message (optional string)` A message to include when the assertion fails. 34 | 35 | Example: 36 | 37 | ```javascript 38 | // "The whitespace between the columns should be 20 pixels wide." 39 | leftColumn.right.to(rightColumn.left).should.equal(20); 40 | ``` 41 | 42 | 43 | ### Relative Positioning 44 | 45 | ``` 46 | Stability: 3 - Stable 47 | ``` 48 | 49 | Check whether the length of the span is bigger or smaller than a size. 50 | 51 | * `size.should.beBiggerThan(expectation, message)` Assert that the span is bigger than the expectation. 52 | * `size.should.beSmallerThan(expectation, message)` Assert that the span is smaller than the expectation. 53 | 54 | Parameters: 55 | 56 | * `expectation (PositionDescriptor equivalent)` The size to compare against. Must be be rendered. 57 | 58 | * `message (optional string)` A message to include when the assertion fails. 59 | 60 | Example: 61 | 62 | ```javascript 63 | // "The testimonials should be shorter than the viewport." 64 | firstTestimonial.top.to(lastTestimonial.bottom).should.beSmallerThan(frame.viewport().height); 65 | ``` 66 | 67 | 68 | ## Properties 69 | 70 | Use these properties to make additional assertions about the span. 71 | 72 | ``` 73 | Stability: 3 - Stable 74 | ``` 75 | 76 | * `span.center (`[`PositionDescriptor`](PositionDescriptor.md)`)` The horizontal center of the span. For use with horizontal spans only. 77 | * `span.middle (`[`PositionDescriptor`](PositionDescriptor.md)`)` The vertical middle of the span. For use with vertical spans only. 78 | 79 | Example: 80 | 81 | ```javascript 82 | // "The thumbnail should be centered to the left of the description." 83 | thumbnailImage.center.should.equal(thumbnailComponent.left.to(description.left)); 84 | ``` 85 | 86 | 87 | ## Methods 88 | 89 | These methods are useful when you want to compare spans that aren't exactly the same. 90 | 91 | 92 | ### span.plus() 93 | 94 | ``` 95 | Stability: 3 - Stable 96 | ``` 97 | 98 | Create a `SizeDescriptor` that's bigger than this span. 99 | 100 | `size = span.plus(amount)` 101 | 102 | * `size (`[`SizeDescriptor`](SizeDescriptor.md)`)` The size. 103 | 104 | * `amount (`[`SizeDescriptor equivalent`](SizeDescriptor.md)`)` The number of pixels to increase. 105 | 106 | 107 | #### span.minus() 108 | 109 | ``` 110 | Stability: 3 - Stable 111 | ``` 112 | 113 | Create a `SizeDescriptor` that's smaller than this one. 114 | 115 | `size = span.minus(amount)` 116 | 117 | * `size (`[`SizeDescriptor`](SizeDescriptor.md)`)` The size. 118 | 119 | * `amount (`[`SizeDescriptor equivalent`](SizeDescriptor.md)`)` The number of pixels to decrease. 120 | 121 | 122 | #### span.times() 123 | 124 | ``` 125 | Stability: 3 - Stable 126 | ``` 127 | 128 | Create a `SizeDescriptor` that's a multiple or fraction of the size of this one. 129 | 130 | `size = span.times(multiple)` 131 | 132 | * `size (`[`SizeDescriptor`](SizeDescriptor.md)`)` The size. 133 | 134 | * `multiple (number)` The number to multiply. 135 | -------------------------------------------------------------------------------- /src/descriptors/viewport_edge.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var ensure = require("../util/ensure.js"); 5 | var PositionDescriptor = require("./position_descriptor.js"); 6 | var Position = require("../values/position.js"); 7 | 8 | var TOP = "top"; 9 | var RIGHT = "right"; 10 | var BOTTOM = "bottom"; 11 | var LEFT = "left"; 12 | 13 | var Me = module.exports = function ViewportEdge(position, browsingContext) { 14 | var BrowsingContext = require("../browsing_context.js"); // break circular dependency 15 | ensure.signature(arguments, [ String, BrowsingContext ]); 16 | 17 | this.should = this.createShould(); 18 | 19 | if (position === LEFT || position === RIGHT) PositionDescriptor.x(this); 20 | else if (position === TOP || position === BOTTOM) PositionDescriptor.y(this); 21 | else ensure.unreachable("Unknown position: " + position); 22 | 23 | this._position = position; 24 | this._browsingContext = browsingContext; 25 | }; 26 | PositionDescriptor.extend(Me); 27 | 28 | Me.top = factoryFn(TOP); 29 | Me.right = factoryFn(RIGHT); 30 | Me.bottom = factoryFn(BOTTOM); 31 | Me.left = factoryFn(LEFT); 32 | 33 | Me.prototype.value = function() { 34 | ensure.signature(arguments, []); 35 | 36 | var scroll = this._browsingContext.getRawScrollPosition(); 37 | var x = Position.x(scroll.x); 38 | var y = Position.y(scroll.y); 39 | 40 | var size = viewportSize(this._browsingContext.contentDocument.documentElement); 41 | 42 | switch(this._position) { 43 | case TOP: return y; 44 | case RIGHT: return x.plus(Position.x(size.width)); 45 | case BOTTOM: return y.plus(Position.y(size.height)); 46 | case LEFT: return x; 47 | 48 | default: ensure.unreachable(); 49 | } 50 | }; 51 | 52 | Me.prototype.toString = function() { 53 | ensure.signature(arguments, []); 54 | return this._position + " edge of viewport"; 55 | }; 56 | 57 | function factoryFn(position) { 58 | return function factory(content) { 59 | return new Me(position, content); 60 | }; 61 | } 62 | 63 | 64 | 65 | // USEFUL READING: http://www.quirksmode.org/mobile/viewports.html 66 | // and http://www.quirksmode.org/mobile/viewports2.html 67 | 68 | // 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); 69 | // 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 70 | 71 | // Width techniques I've tried: (Note: results are different in quirks mode) 72 | // body.clientWidth 73 | // body.offsetWidth 74 | // body.getBoundingClientRect().width 75 | // fails on all browsers: doesn't include margin 76 | // body.scrollWidth 77 | // works on Safari, Mobile Safari, Chrome 78 | // fails on Firefox, IE 8, 9, 10, 11: doesn't include margin 79 | // html.getBoundingClientRect().width 80 | // html.offsetWidth 81 | // works on Safari, Mobile Safari, Chrome, Firefox 82 | // fails on IE 8, 9, 10: includes scrollbar 83 | // html.scrollWidth 84 | // html.clientWidth 85 | // WORKS! Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11 86 | 87 | // Height techniques I've tried: (Note that results are different in quirks mode) 88 | // body.clientHeight 89 | // body.offsetHeight 90 | // body.getBoundingClientRect().height 91 | // fails on all browsers: only includes height of content 92 | // body getComputedStyle("height") 93 | // fails on all browsers: IE8 returns "auto"; others only include height of content 94 | // body.scrollHeight 95 | // works on Safari, Mobile Safari, Chrome; 96 | // fails on Firefox, IE 8, 9, 10, 11: only includes height of content 97 | // html.getBoundingClientRect().height 98 | // html.offsetHeight 99 | // works on IE 8, 9, 10 100 | // fails on IE 11, Safari, Mobile Safari, Chrome: only includes height of content 101 | // html.scrollHeight 102 | // works on Firefox, IE 8, 9, 10, 11 103 | // fails on Safari, Mobile Safari, Chrome: only includes height of content 104 | // html.clientHeight 105 | // WORKS! Safari, Mobile Safari, Chrome, Firefox, IE 8, 9, 10, 11 106 | function viewportSize(htmlElement) { 107 | return { 108 | width: htmlElement.clientWidth, 109 | height: htmlElement.clientHeight 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/util/ensure.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2014 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | "use strict"; 3 | 4 | // Runtime assertions for production code. (Contrast to assert.js, which is for test code.) 5 | 6 | var shim = require("./shim.js"); 7 | var oop = require("./oop.js"); 8 | 9 | exports.that = function(variable, message) { 10 | if (message === undefined) message = "Expected condition to be true"; 11 | 12 | if (variable === false) throw new EnsureException(exports.that, message); 13 | if (variable !== true) throw new EnsureException(exports.that, "Expected condition to be true or false"); 14 | }; 15 | 16 | exports.unreachable = function(message) { 17 | if (!message) message = "Unreachable code executed"; 18 | 19 | throw new EnsureException(exports.unreachable, message); 20 | }; 21 | 22 | exports.signature = function(args, signature) { 23 | signature = signature || []; 24 | var expectedArgCount = signature.length; 25 | var actualArgCount = args.length; 26 | 27 | if (actualArgCount > expectedArgCount) { 28 | throw new EnsureException( 29 | exports.signature, 30 | "Function called with too many arguments: expected " + expectedArgCount + " but got " + actualArgCount 31 | ); 32 | } 33 | 34 | var arg, types, name; 35 | for (var i = 0; i < signature.length; i++) { 36 | arg = args[i]; 37 | types = signature[i]; 38 | name = "Argument #" + (i + 1); 39 | 40 | if (!shim.Array.isArray(types)) types = [ types ]; 41 | if (!argMatchesAnyPossibleType(arg, types)) { 42 | var message = name + " expected " + explainPossibleTypes(types) + ", but was " + explainArg(arg); 43 | throw new EnsureException(exports.signature, message); 44 | } 45 | } 46 | }; 47 | 48 | function argMatchesAnyPossibleType(arg, type) { 49 | for (var i = 0; i < type.length; i++) { 50 | if (argMatchesType(arg, type[i])) return true; 51 | } 52 | return false; 53 | 54 | function argMatchesType(arg, type) { 55 | switch (getArgType(arg)) { 56 | case "boolean": return type === Boolean; 57 | case "string": return type === String; 58 | case "number": return type === Number; 59 | case "array": return type === Array; 60 | case "function": return type === Function; 61 | case "object": return type === Object || arg instanceof type; 62 | case "undefined": return type === undefined; 63 | case "null": return type === null; 64 | case "NaN": return typeof(type) === "number" && isNaN(type); 65 | 66 | default: exports.unreachable(); 67 | } 68 | } 69 | } 70 | 71 | function explainPossibleTypes(type) { 72 | var joiner = ""; 73 | var result = ""; 74 | for (var i = 0; i < type.length; i++) { 75 | result += joiner + explainOneType(type[i]); 76 | joiner = (i === type.length - 2) ? ", or " : ", "; 77 | } 78 | return result; 79 | 80 | function explainOneType(type) { 81 | switch (type) { 82 | case Boolean: return "boolean"; 83 | case String: return "string"; 84 | case Number: return "number"; 85 | case Array: return "array"; 86 | case Function: return "function"; 87 | case null: return "null"; 88 | case undefined: return "undefined"; 89 | default: 90 | if (typeof type === "number" && isNaN(type)) return "NaN"; 91 | else { 92 | return oop.className(type) + " instance"; 93 | } 94 | } 95 | } 96 | } 97 | 98 | function explainArg(arg) { 99 | var type = getArgType(arg); 100 | if (type !== "object") return type; 101 | 102 | return oop.instanceName(arg) + " instance"; 103 | } 104 | 105 | function getArgType(variable) { 106 | var type = typeof variable; 107 | if (variable === null) type = "null"; 108 | if (shim.Array.isArray(variable)) type = "array"; 109 | if (type === "number" && isNaN(variable)) type = "NaN"; 110 | return type; 111 | } 112 | 113 | 114 | /*****/ 115 | 116 | var EnsureException = exports.EnsureException = function EnsureException(fnToRemoveFromStackTrace, message) { 117 | if (Error.captureStackTrace) Error.captureStackTrace(this, fnToRemoveFromStackTrace); 118 | else this.stack = (new Error()).stack; 119 | this.message = message; 120 | }; 121 | EnsureException.prototype = shim.Object.create(Error.prototype); 122 | EnsureException.prototype.constructor = EnsureException; 123 | EnsureException.prototype.name = "EnsureException"; 124 | -------------------------------------------------------------------------------- /src/descriptors/_relative_position_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var quixote = require("../quixote.js"); 7 | var RelativePosition = require("./relative_position.js"); 8 | var Position = require("../values/position.js"); 9 | var PositionDescriptor = require("./position_descriptor.js"); 10 | 11 | describe("DESCRIPTOR: RelativePosition", function() { 12 | 13 | var element; 14 | var right; 15 | var down; 16 | var left; 17 | var up; 18 | 19 | var TOP = 300; 20 | var RIGHT = 150; 21 | var BOTTOM = 70; 22 | var LEFT = 20; 23 | 24 | var WIDTH = 130; 25 | var HEIGHT = 60; 26 | 27 | var RIGHT_ADJ = 5; 28 | var DOWN_ADJ = 10; 29 | var LEFT_ADJ = 3; 30 | var UP_ADJ = 4; 31 | 32 | beforeEach(function() { 33 | var frame = reset.frame; 34 | frame.add( 35 | "

    element

    " 36 | ); 37 | element = frame.get("#element"); 38 | right = RelativePosition.right(element.left, RIGHT_ADJ); 39 | down = RelativePosition.down(element.top, DOWN_ADJ); 40 | left = RelativePosition.left(element.left, LEFT_ADJ); 41 | up = RelativePosition.up(element.top, UP_ADJ); 42 | }); 43 | 44 | it("is a position descriptor", function() { 45 | assert.implements(right, PositionDescriptor); 46 | }); 47 | 48 | it("resolves to value", function() { 49 | assert.objEqual(right.value(), Position.x(LEFT + RIGHT_ADJ), "right"); 50 | assert.objEqual(down.value(), Position.y(TOP + DOWN_ADJ), "down"); 51 | assert.objEqual(left.value(), Position.x(LEFT - LEFT_ADJ), "left"); 52 | assert.objEqual(up.value(), Position.y(TOP - UP_ADJ), "up"); 53 | }); 54 | 55 | it("computes value relative to a size descriptor", function() { 56 | right = RelativePosition.right(element.left, element.width); 57 | assert.objEqual(right.value(), Position.x(LEFT + WIDTH)); 58 | }); 59 | 60 | it("computes value relative to a relative size descriptor", function() { 61 | right = RelativePosition.right(element.left, element.width.plus(10)); 62 | assert.objEqual(right.value(), Position.x(LEFT + WIDTH + 10)); 63 | }); 64 | 65 | it("converts arguments to comparable values", function() { 66 | assert.objEqual(right.convert(13, "number"), Position.x(13), "right"); 67 | assert.objEqual(down.convert(13, "number"), Position.y(13), "down"); 68 | assert.objEqual(left.convert(13, "number"), Position.x(13), "left"); 69 | assert.objEqual(up.convert(13, "number"), Position.y(13), "up"); 70 | }); 71 | 72 | it("converts to string", function() { 73 | assertRight(element.left, 10, "10px to right of ", "right +"); 74 | assertRight(element.left, -15, "15px to left of ", "right -"); 75 | assertRight(element.left, 0, "", "right 0"); 76 | 77 | assertDown(element.top, 20, "20px below ", "down +"); 78 | assertDown(element.top, -20, "20px above ", "down -"); 79 | assertDown(element.top, 0, "", "down 0"); 80 | 81 | assertLeft(element.left, 10, "10px to left of ", "left +"); 82 | assertLeft(element.left, -10, "10px to right of ", "left -"); 83 | assertLeft(element.left, 0, "", "left 0"); 84 | 85 | assertUp(element.top, 20, "20px above ", "up +"); 86 | assertUp(element.top, -20, "20px below ", "up -"); 87 | assertUp(element.top, 0, "", "up 0"); 88 | 89 | function assertRight(edge, amount, expected, message) { 90 | assert.equal(RelativePosition.right(edge, amount).toString(), expected + edge.toString(), message); 91 | } 92 | 93 | function assertDown(edge, amount, expected, message) { 94 | assert.equal(RelativePosition.down(edge, amount).toString(), expected + edge.toString(), message); 95 | } 96 | 97 | function assertLeft(edge, amount, expected, message) { 98 | assert.equal(RelativePosition.left(edge, amount).toString(), expected + edge.toString(), message); 99 | } 100 | 101 | function assertUp(edge, amount, expected, message) { 102 | assert.equal(RelativePosition.up(edge, amount).toString(), expected + edge.toString(), message); 103 | } 104 | }); 105 | 106 | it("has assertions", function() { 107 | assert.exception( 108 | function() { left.should.equal(30); }, 109 | "3px to left of left edge of '#element' should be 13px to right.\n" + 110 | " Expected: 30px\n" + 111 | " But was: 17px" 112 | ); 113 | }); 114 | 115 | }); -------------------------------------------------------------------------------- /src/descriptors/_element_edge_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var assert = require("../util/assert.js"); 5 | var reset = require("../__reset.js"); 6 | var quixote = require("../quixote.js"); 7 | var ElementEdge = require("./element_edge.js"); 8 | var Position = require("../values/position.js"); 9 | var PositionDescriptor = require("./position_descriptor.js"); 10 | 11 | describe("DESCRIPTOR: ElementEdge", function() { 12 | 13 | var frame; 14 | var element; 15 | var top; 16 | var right; 17 | var bottom; 18 | var left; 19 | 20 | var TOP = 10; 21 | var RIGHT = 150; 22 | var BOTTOM = 70; 23 | var LEFT = 20; 24 | 25 | beforeEach(function() { 26 | frame = reset.frame; 27 | element = frame.add( 28 | "

    element

    ", 29 | "element" 30 | ); 31 | top = ElementEdge.top(element); 32 | right = ElementEdge.right(element); 33 | bottom = ElementEdge.bottom(element); 34 | left = ElementEdge.left(element); 35 | }); 36 | 37 | it("is a position descriptor", function() { 38 | assert.implements(top, PositionDescriptor); 39 | }); 40 | 41 | it("resolves to value by looking at bounding box", function() { 42 | assert.objEqual(top.value(), Position.y(TOP), "top"); 43 | assert.objEqual(right.value(), Position.x(RIGHT), "right"); 44 | assert.objEqual(bottom.value(), Position.y(BOTTOM), "bottom"); 45 | assert.objEqual(left.value(), Position.x(LEFT), "left"); 46 | }); 47 | 48 | it("accounts for scrolling", function() { 49 | if (quixote.browser.enlargesFrameToPageSize()) return; 50 | 51 | frame.add("
    scroll enabler
    "); 52 | 53 | frame.scroll(50, 60); 54 | 55 | assert.objEqual(top.value(), Position.y(TOP), "top"); 56 | assert.objEqual(right.value(), Position.x(RIGHT), "right"); 57 | assert.objEqual(bottom.value(), Position.y(BOTTOM), "bottom"); 58 | assert.objEqual(left.value(), Position.x(LEFT), "left"); 59 | }); 60 | 61 | it("knows elements with display:none are not rendered", function() { 62 | element.toDomElement().style.display = "none"; 63 | 64 | assert.objEqual(top.value(), Position.noY(), "top"); 65 | assert.objEqual(right.value(), Position.noX(), "right"); 66 | assert.objEqual(bottom.value(), Position.noY(), "bottom"); 67 | assert.objEqual(left.value(), Position.noX(), "left"); 68 | }); 69 | 70 | it("knows elements not in the DOM are not rendered", function() { 71 | var domElement = element.toDomElement(); 72 | domElement.parentNode.removeChild(domElement); 73 | 74 | assert.objEqual(top.value(), Position.noY(), "top"); 75 | assert.objEqual(right.value(), Position.noX(), "right"); 76 | assert.objEqual(bottom.value(), Position.noY(), "bottom"); 77 | assert.objEqual(left.value(), Position.noX(), "left"); 78 | }); 79 | 80 | it("considers elements with zero width ARE rendered", function() { 81 | element.toDomElement().style.width = "0px"; 82 | 83 | assert.objEqual(top.value(), Position.y(TOP), "top"); 84 | assert.objEqual(right.value(), Position.x(LEFT + 0), "right"); 85 | assert.objEqual(bottom.value(), Position.y(BOTTOM), "bottom"); 86 | assert.objEqual(left.value(), Position.x(LEFT), "left"); 87 | }); 88 | 89 | it("considers elements with zero height ARE rendered", function() { 90 | element.toDomElement().style.height = "0px"; 91 | 92 | assert.objEqual(top.value(), Position.y(TOP), "top"); 93 | assert.objEqual(right.value(), Position.x(RIGHT), "right"); 94 | assert.objEqual(bottom.value(), Position.y(TOP + 0), "bottom"); 95 | assert.objEqual(left.value(), Position.x(LEFT), "left"); 96 | }); 97 | 98 | it("converts to string", function() { 99 | assertDesc(element, top, "top edge of ", "top"); 100 | assertDesc(element, right, "right edge of ", "right"); 101 | assertDesc(element, bottom, "bottom edge of ", "bottom"); 102 | assertDesc(element, left, "left edge of ", "left"); 103 | 104 | function assertDesc(element, edge, expected, message) { 105 | assert.equal(edge.toString(), expected + element, message); 106 | } 107 | }); 108 | 109 | it("has assertions", function() { 110 | assert.exception( 111 | function() { left.should.equal(30); }, 112 | "left edge of 'element' should be 10px to right.\n" + 113 | " Expected: 30px\n" + 114 | " But was: 20px" 115 | ); 116 | }); 117 | 118 | }); -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | Quixote Example 2 | =========== 3 | 4 | This example code is based on my "Agile Engineering for the Web" talk, first presented at Øredev in Malmö Sweden on 4 Nov 2015. The talk demonstrates test-driven development of front-end JavaScript and CSS. You can see it online here: 5 | 6 | [![Video link](video_poster.jpg)](https://vimeo.com/144642399) 7 | 8 | The Quixote portion starts at 21:50. 9 | 10 | There have been some changes to the example since the video was recorded. The biggest change is to Quixote's assertion API. The assertions in the video look like this (see 27:18): 11 | 12 | ```javascript 13 | figure.assert({ 14 | left: frame.body().left 15 | }); 16 | ``` 17 | 18 | This assertion checks that the left edge of the 'figure' element is the same as the left edge of the page's body element. 19 | 20 | The current version of Quixote provides a more natural API. The same assertion now looks like this: 21 | 22 | ```javascript 23 | figure.left.should.equal(frame.body().left); 24 | ``` 25 | 26 | The code in this example uses this style. 27 | 28 | 29 | About the Example 30 | ----------------- 31 | 32 | This code demonstrates CSS and JavaScript tests. It uses: 33 | 34 | * [Karma](http://karma-runner.github.io) for cross-browser testing. 35 | * [Mocha](https://mochajs.org/) for running tests. 36 | * [Chai](http://chaijs.com/) for assertions. 37 | * [Quixote](https://github.com/jamesshore/quixote) for testing CSS. 38 | 39 | The sample application uses Nicole Sullivan's [media object](http://www.stubbornella.org/content/2010/06/25/the-media-object-saves-hundreds-of-lines-of-code/) to display an icon with some text. Clicking the icon causes the text to appear and disappear. 40 | 41 | Important files: 42 | 43 | * [`src/_media_css_test.js`](src/_media_css_test.js): CSS tests 44 | 45 | * [`src/screen.css`](src/screen.css): CSS code 46 | 47 | * [`build/config/karma.conf.js`](build/config/karma.conf.js): Karma configuration. Look for the `// QUIXOTE` comment to see how to make Karma serve CSS files. 48 | 49 | 50 | Running the Tests 51 | ----------------- 52 | 53 | Before running the tests: 54 | 55 | 1. Install [Node.js](http://nodejs.org/download/). 56 | 2. Install Quixote: `npm install quixote` 57 | 3. Change to the example directory: `cd node_modules/quixote/example` 58 | 59 | To run the tests: 60 | 61 | 1. Start the Karma server: `./jake.sh karma` (Unix/Mac) or `jake karma` (Windows) 62 | 2. Open `http://localhost:9876` in one or more browsers. 63 | 3. Run `./jake.sh loose=true` (Unix/Mac) or `jake loose=true` (Windows) every time you want to build and test. Alternatively, use `./watch.sh loose=true` (Unix/Mac) or `watch loose=true` (Windows) to automatically run `jake` whenever files change. 64 | 65 | Remove the `loose=true` parameter for strict Node and browser version checking. 66 | 67 | To run the app: 68 | 69 | 1. Run `./jake.sh run` (Unix/Mac) or `jake run` (Windows). 70 | 2. Open `http://localhost:8080` in a browser. 71 | 3. Click the coffee cup icon to see the text appear and disappear. 72 | 73 | 74 | Contents 75 | -------- 76 | 77 | This repository consists of the following directories: 78 | 79 | * `build`: Build automation. 80 | * `build/config`: Build configuration. 81 | * `build/scripts`: Build scripts. Don't run them directly. 82 | * `build/util`: Modules used by the build scripts. 83 | * `node_modules`: npm dependencies (used by the build). 84 | * `src`: Front-end code. 85 | * `vendor`: Client code dependencies. 86 | 87 | In the repository root, you'll find the following scripts. For each script, there's a `.sh` version for Unix and Mac and a `.bat` version for Windows: 88 | 89 | * `jake`: Build and test automation. 90 | * `watch`: Automatically runs `jake` when any files change. Any arguments are passed through to jake. 91 | 92 | For all these scripts, use `-T` to see the available build targets and their documentation. If no target is provided, the script will run `default`. Use `--help` for additional options. 93 | 94 | The scripts have these additional options: 95 | 96 | * `loose=true`: Disable strict browser and version checks. 97 | * `capture=Firefox,Safari,etc`: Automatically launch, use, and quit the requested browsers. You can use this instead of running `./jake.sh karma` and manually starting the browsers yourself. Note that the browser name is case-sensitive. The Firefox launcher is included; if you need additional launchers, you'll need to install them; e.g., `npm install karma-safari-launcher`. 98 | 99 | 100 | 101 | License 102 | ------- 103 | 104 | MIT License. See `LICENSE.TXT`. -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | // Copyright Titanium I.T. LLC. 2 | "use strict"; 3 | 4 | var ensure = require("./util/ensure.js"); 5 | var QFrame = require("./q_frame.js"); 6 | var Size = require("./values/size.js"); 7 | 8 | var FRAME_WIDTH = 1500; 9 | var FRAME_HEIGHT = 200; 10 | 11 | var features = null; 12 | 13 | exports.enlargesFrameToPageSize = createDetectionMethod("enlargesFrame"); 14 | exports.enlargesFonts = createDetectionMethod("enlargesFonts"); 15 | exports.misreportsClipAutoProperty = createDetectionMethod("misreportsClipAuto"); 16 | exports.misreportsAutoValuesInClipProperty = createDetectionMethod("misreportsClipValues"); 17 | exports.roundsOffPixelCalculations = createDetectionMethod("roundsOffPixelCalculations"); 18 | 19 | exports.detectBrowserFeatures = function(callback) { 20 | var frame = QFrame.create(document.body, { width: FRAME_WIDTH, height: FRAME_HEIGHT }, function(err) { 21 | if (err) { 22 | return callback(new Error("Error while creating Quixote browser feature detection frame: " + err)); 23 | } 24 | return detectFeatures(frame, function(err) { 25 | frame.remove(); 26 | return callback(err); 27 | }); 28 | }); 29 | }; 30 | 31 | function detectFeatures(frame, callback) { 32 | try { 33 | features = {}; 34 | features.enlargesFrame = detectFrameEnlargement(frame, FRAME_WIDTH); 35 | features.misreportsClipAuto = detectReportedClipAuto(frame); 36 | features.misreportsClipValues = detectReportedClipPropertyValues(frame); 37 | features.roundsOffPixelCalculations = detectRoundsOffPixelCalculations(frame); 38 | 39 | detectFontEnlargement(frame, FRAME_WIDTH, function(result) { 40 | features.enlargesFonts = result; 41 | frame.remove(); 42 | return callback(null); 43 | }); 44 | 45 | } 46 | catch(err) { 47 | features = null; 48 | return callback(new Error("Error during Quixote browser feature detection: " + err)); 49 | } 50 | } 51 | 52 | function createDetectionMethod(propertyName) { 53 | return function() { 54 | ensure.signature(arguments, []); 55 | ensure.that( 56 | features !== null, 57 | "Must call quixote.createFrame() before using Quixote browser feature detection." 58 | ); 59 | 60 | return features[propertyName]; 61 | }; 62 | } 63 | 64 | function detectFrameEnlargement(frame, frameWidth) { 65 | frame.reset(); 66 | 67 | frame.add("
    force scrolling
    "); 68 | return !frame.viewport().width.value().equals(Size.create(frameWidth)); 69 | } 70 | 71 | function detectReportedClipAuto(frame) { 72 | frame.reset(); 73 | 74 | var element = frame.add("
    "); 75 | var clip = element.getRawStyle("clip"); 76 | 77 | return clip !== "auto"; 78 | } 79 | 80 | function detectReportedClipPropertyValues(frame) { 81 | frame.reset(); 82 | 83 | var element = frame.add("
    "); 84 | var clip = element.getRawStyle("clip"); 85 | 86 | // WORKAROUND IE 8: Provides 'clipTop' etc. instead of 'clip' property 87 | if (clip === "" && element.getRawStyle("clip-top") === "auto") return false; 88 | 89 | return clip !== "rect(auto, auto, auto, auto)" && clip !== "rect(auto auto auto auto)"; 90 | } 91 | 92 | function detectRoundsOffPixelCalculations(frame) { 93 | var element = frame.add("
    "); 94 | var size = element.calculatePixelValue("0.5em"); 95 | 96 | if (size === 7.5) return false; 97 | if (size === 8) return true; 98 | ensure.unreachable("Failure in roundsOffPixelValues() detection: expected 7.5 or 8, but got " + size); 99 | } 100 | 101 | function detectFontEnlargement(frame, frameWidth, callback) { 102 | ensure.that(frameWidth >= 1500, "Detector frame width must be larger than screen to detect font enlargement"); 103 | frame.reset(); 104 | 105 | // WORKAROUND IE 8: we use a
    because the
    "); 108 | 109 | var text = frame.add("

    arbitrary text

    "); 110 | frame.add("

    must have two p tags to work

    "); 111 | 112 | // WORKAROUND IE 8: need to force reflow or getting font-size may fail below 113 | // This seems to occur when IE is running in a slow VirtualBox VM. There is no test for this line. 114 | frame.forceReflow(); 115 | 116 | // WORKAROUND Safari 8.0.0: timeout required because font is enlarged asynchronously 117 | setTimeout(function() { 118 | var fontSize = text.getRawStyle("font-size"); 119 | ensure.that(fontSize !== "", "Expected font-size to be a value"); 120 | 121 | // WORKAROUND IE 8: ignores