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.
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 |
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("
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 |
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 |
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
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 |
");
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 | [](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