Quixote is a library for testing CSS. It's fast—over 100 tests/second—and has a powerful API. You can use it for unit testing (test your CSS files directly) or integration testing (test against a real server). Either way, your tests check how HTML elements are actually rendered by the browser.
Quixote runs in the browser and works with any test framework. You can even test multiple browsers simultaneously by using a tool such as Karma or Test'em. It works in modern desktop browsers, mobile browsers, and IE 8+.
Example test:
// 'frame' is the Quixote test frame. See below for complete examples.
var header = frame.get("#header");
var navbar = frame.get(".navbar");
navbar.assert({
top: header.bottom.plus(10), // The navbar is 10px below the header,
left: frame.page().left, // it's flush against the left side of the screen,
width: frame.viewport().width // and it's exactly as wide as the viewport.
});
Example output:
top edge of '.navbar' was 13px lower than expected.
Expected: 50px (10px below bottom edge of '#header')
But was: 63px
$ npm install quixote
Or download dist/quixote.js.
Quixote is a UMD module, which means it will work with CommonJS and AMD module loaders. If you just load the file using a <script>
tag, Quixote will be available via the global variable quixote
.
Quixote runs in the browser and works with any test framework. The following examples use Karma, Mocha, and Chai.
Quixote works by rendering elements in an iframe, then checking them using getComputedStyle() and getBoundingClientRect. These two APIs allow Quixote to check how elements are actually rendered in the browser.
See the example directory for a seed project that has Karma, Mocha, and Chai set up with the following examples. Read the readme in that directory to learn how to use it.
Quixote can be used in a unit-testing style or an integration-testing style (or both).
Use the unit test style when you want to test-drive individual CSS rules. In the unit test style, you'll use Quixote to:
- Load your CSS file
- Create elements that are styled by your CSS
- Confirm that the elements are displayed correctly.
A Quixote unit test might be used to test-drive a "button" class:
.button {
display: block;
width: 100%;
text-align: center;
text-transform: uppercase;
text-decoration: none;
}
The test code would look like this:
var assert = require("chai").assert;
var quixote = require("quixote");
describe("Button", function() {
var frame;
var container;
var button;
before(function(done) {
frame = quixote.createFrame({
stylesheet: "/base/src/client/screen.css"
}, done);
});
after(function() {
frame.remove();
});
beforeEach(function() {
frame.reset();
container = frame.add(
"<div>" +
" <a id='button' class='button' href='#anything'>foo</a>" +
"</div>"
);
button = frame.get("#button");
});
it("fills its container", function() {
button.assert({
width: container.width
});
});
it("has styled text", function() {
assert.equal(button.getRawStyle("text-align"), "center", "should be centered");
assert.equal(button.getRawStyle("text-decoration"), "underline", "should be underlined");
assert.equal(button.getRawStyle("text-transform"), "uppercase", "should be uppercase");
});
});
Use the integration test style when you want to test a complete web page. In the integration test style, you'll use Quixote to:
- Load your URL
- Get elements from the page
- Confirm that the elements are displayed correctly.
Imagine a site with a home page that contained a logo at the top and a nav bar below it. The logo is centered and the nav bar stretches the width of the window. The integration test for that page would look like this:
var assert = require("chai").assert;
var quixote = require("quixote");
describe("Home page", function() {
var BACKGROUND_BLUE = "rgb(65, 169, 204)";
var WHITE = "rgb(255, 255, 255)";
var MEDIUM_BLUE = "rgb(0, 121, 156)";
var frame;
var logo;
var navbar;
before(function(done) {
frame = quixote.createFrame({
src: "/", // the URL must be proxied to localhost
width: 800
}, done);
});
after(function(done) {
frame.remove();
});
beforeEach(function() {
frame.reset();
logo = frame.get("#logo");
navbar = frame.get("#navbar");
});
it("has an overall layout", function() {
logo.assert({
top: 12,
center: frame.page().center
}, "logo should be centered at top of page");
assert.equal(logo.getRawStyle("text-align"), "center", "logo alt text should be centered");
navbar.assert({
top: logo.bottom.plus(10),
left: frame.page().left,
width: frame.viewport().width
}, "navbar should stretch the width of the window");
});
it("has a color scheme", function() {
assert.equal(frame.body().getRawStyle("background-color", BACKGROUND_BLUE, "page background");
assert.equal(logo.getRawStyle("color", WHITE, "logo text");
assert.equal(navbar.getRawStyle("background-color", MEDIUM_BLUE, "navbar background color");
assert.equal(navbar.getRawStyle("color", WHITE, "navbar text");
});
it("has a typographic scheme", function() {
// etc
});
});
To begin, you'll need a way of running tests in the browser. You can use any test framework you like.
If you don't already have a preferred test framework:
- Install Karma. Karma runs your test suite in multiple browsers simultaneously.
- Install Mocha. Mocha is a test framework. It organizes and runs your tests.
- Install Chai. Chai is an assertion library. It allows you to check results.
Quixote gets your CSS and HTML files by loading URLs. You'll need to configure your test tool to make sure they're available. For integration tests, you'll need to proxy your server to localhost to avoid browser security errors.
In the unit test style, you load your CSS files directly. If you're using Karma, you can do it by modifying the files
parameter in karma.conf.js
. By default, they'll be available off of the /base/
directory.
files: [
// ...
{ pattern: 'src/client/screen.css', included: false }
// The 'included' parameter prevents Karma from automatically loading your CSS.
],
In the integration test style, you load an HTML page that's styled by your CSS. Because of browsers' security rules, the file must be served from the same server as your test, which means your test server will have to proxy the real server.
If you're using Karma, you can do it by setting the proxies
parameter in karma.conf.js
. In most cases, you'll want to proxy your server under test to the root directory, which means you'll also want to move the Karma runner from the root to a different directory. You can do that with the urlRoot
parameter.
urlRoot: '/karma/'
proxies: {
'/': 'http://server_under_test/'
},
Normally, to capture a browser for Karma, you visit http://localhost:9876
. With this configuration, you would visit http://localhost:9876/karma
instead. Visiting http://localhost:9876
will show you the proxied server under test.
Now you can write your tests. Quixote uses a special test frame for its tests, so you'll need to create and destroy it using quixote.createFrame() and frame.remove(). This is a relatively slow operation, so try to do it just once for each file you test.
If you modify the contents of the test frame, you can reset it to a pristine state by calling frame.reset(). This is faster than recreating the test frame.
In the unit test style, you create a frame that loads your CSS file:
var quixote = require("quixote");
var frame;
before(function(done) {
frame = quixote.createFrame({
stylesheet: "/base/src/client/screen.css"
}, done);
});
after(function() {
frame.remove();
});
beforeEach(function() {
frame.reset();
});
In the integration test style, you do the same thing, but your frame will load your proxied server under test:
var quixote = require("quixote");
var frame;
before(function(done) {
frame = quixote.createFrame({
src: "/"
}, done);
});
after(function() {
frame.remove();
});
beforeEach(function() {
frame.reset();
});
The Quixote test frame will give you access to everything you need to test your code. You can add elements to the frame using frame.add() and get elements from the frame using frame.get(). Once you have an element, you can use Quixote's custom assertions by calling element.assert(). You can also pull style information out of an element, for use with another assertion library, by calling element.getRawStyle().
In the unit test style, you add elements to the frame so they'll be styled by your CSS file. By adding the elements in your test, you make it easier to understand how your test works and you document how your CSS is meant to be used.
For example, if you were planning to test-drive this CSS:
.button {
display: block;
width: 100%;
text-align: center;
text-transform: uppercase;
text-decoration: none;
}
You would use this code:
describe("Button") {
beforeEach(function() {
frame.reset();
container = frame.add(
"<div>" +
" <a id='button' class='button' href='#anything'>foo</a>" +
"</div>"
);
button = frame.get("#button");
});
it("fills its container", function() {
button.assert({
width: container.width
});
});
it("has styled text", function() {
assert.equal(button.getRawStyle("text-align"), "center", "should be centered");
assert.equal(button.getRawStyle("text-decoration"), "underline", "should be underlined");
assert.equal(button.getRawStyle("text-transform"), "uppercase", "should be uppercase");
});
});
In the integration test style, you load a complete page, so rather than adding elements to the frame, you'll just pull out the ones you want to test.
describe("Home page", function() {
var BACKGROUND_BLUE = "rgb(65, 169, 204)";
var WHITE = "rgb(255, 255, 255)";
var MEDIUM_BLUE = "rgb(0, 121, 156)";
var logo;
var navbar;
beforeEach(function() {
frame.reset();
logo = frame.get("#logo");
navbar = frame.get("#navbar");
});
it("has an overall layout", function() {
logo.assert({
top: 12,
center: frame.page().center
}, "logo should be centered at top of page");
assert.equal(logo.getRawStyle("text-align"), "center", "logo alt text should be centered");
navbar.assert({
top: logo.bottom.plus(10),
left: frame.page().left,
width: frame.viewport().width
}, "navbar should stretch the width of the window");
});
it("has a color scheme", function() {
assert.equal(frame.body().getRawStyle("background-color", BACKGROUND_BLUE, "page background");
assert.equal(logo.getRawStyle("color", WHITE, "logo text");
assert.equal(navbar.getRawStyle("background-color", MEDIUM_BLUE, "navbar background color");
assert.equal(navbar.getRawStyle("color", WHITE, "navbar text");
});
it("has a typographic scheme", function() {
// etc
});
});
The site CSS Test has a great rundown of CSS testing tools and libraries. To summarize, there are two main approaches to CSS testing:
One of the most popular approaches, exemplified by Wraith, is to programmatically take a screenshot of a page and compare it to a known-good screenshot. If there's a difference, the test fails and the user gets a composite view with the differences highlighted. This is great for checking if things have changed. However, it's slow and prone to false positives.
The other popular approach, and the one used by Quixote, is to ask the DOM how the browser has rendered it elements. This is 100x faster than the screenshot approach and it allows users to write more targeted tests. However, the DOM calls are cumbersome and suffer from bugs and inconsistencies. Quixote is the only tool we are aware of that attempts to solve these problems.
Created by James Shore as part of the Let's Code: Test-Driven JavaScript screencast. Let's Code JavaScript is a screencast series on rigorous, professional web development.
Many thanks to our contributors!
- Jay Bazuzi (@JayBazuzi): Travis CI integration (v0.1)
- @bjornicus: Fail fast if HTML or stylesheet URL is invalid (v0.3)
MIT License. See LICENSE.txt.