Quixote Hackathon Final Report

What are the three most important things to remember when writing code?

Simplify, simplify, simplify.

That’s what I've been up to with Quixote for the last week. Although the hackathon finished last Thursday, I’ve been working steadily on Quixote ever since. My goal: take the core ideas from the first three releases and refine them into a simple and elegant system.

(In case you missed the announcement, Quixote is a library for unit testing CSS. It’s open source and I developed it live on hitbox.tv every day through Thursday last week. The raw footage from the livestream is now available on my Hitbox channel under the “Videos” tab.)

A Simpler Architecture

At the end of the third day, the core concepts of the Quixote architecture were in place:

  • Descriptor Objects represent some aspect of the page, such as “the position of the top edge of element X”.

  • Value Objects represent a computed value, such as a Y-coordinate or size.

The core concepts were solid, but the implementation was a mess. To create a descriptor in the version 0.3 code, you had to implement a hodgepodge of methods:

  • value(), for computing the value of CSS

  • diff(), for determining how the descriptor differed from an expected value, and also for converting primitives into comparable value objects

  • description(), a generic description of the descriptor

  • toString(), a more specific description of the descriptor

  • describeMatch(), a description of what the descriptor looked like when compared to other descriptors

Duplication abounded. We only had two descriptors (ElementEdge and ElementPosition), and they were essentially a cut-and-paste of each other.

The 0.4 release was planned to include a bunch of new descriptors, and they needed to combine in new and complicated ways. Something had to be done.

Abstraction and Refinement

Inheritance isn’t a popular design concept these days, but this situation seemed like a perfect fit. We had an abstract idea (descriptors) with a common core (diff’ing) and varying implementations (different things to calculate and describe). I decided to create an abstract base class that all the descriptors would extend.

JavaScript doesn’t have a concept of abstract classes—or really, any concept of classes at all—but they can be implemented anyway. I started by factoring out the differences in the two descriptors’ diff() methods. Once they were identical, I introduced a Descriptor base class and moved diff() into it. I also created utility methods to make testing and extension easier.

From there, it was just “simplify, simplify, and simplify.” As I added new descriptors, I discovered new opportunities to refine the design and remove implementation requirements. Descriptors for new concepts like “height” and “width” introduced new value objects, and I saw the opportunity to create an abstract base class for the value objects as well.

By the end, the implementation requirement for a new descriptor was reduced to three methods:

  • value(), for computing the CSS
  • toString(), for describing the descriptor
  • convert(), for converting primitives to comparable value objects

Here’s an example. This is ElementSize, a descriptor that represents the height or width of an element:

"use strict";

var Descriptor = require("./descriptor.js");
var Size = require("../values/size.js");

var X_DIMENSION = "x";
var Y_DIMENSION = "y";

var Me = module.exports = function ElementSize(dimension, element) {
  // ... for clarity, factory methods and parameter checking removed
  this._dimension = dimension;
  this._element = element;
};
Descriptor.extend(Me);

Me.prototype.value = function value() {
  var position = this._element.getRawPosition();
  var result = (this._dimension === X_DIMENSION) ? position.width : position.height;

  return Size.create(result);
};

Me.prototype.toString = function toString() {
  var desc = (this._dimension === X_DIMENSION) ? "width" : "height";
  return desc + " of " + this._element;
};

Me.prototype.convert = function convert(arg, type) {
  if (type === "number") return Size.create(arg);
};

I’m pretty happy with this. It feels like it’s just about width and height, with no extra baggage. Any complexity in this and future descriptors comes from the challenges of cross-browser CSS analysis, not the vagaries of my architecture.

What’s Next

The 0.4 release rounded out Quixote’s ability to understand positions. The 0.5 release followed up with a focus on API stability.

Now my focus is improving Quixote’s breadth. Descriptors for page positioning, colors, stacking order, fonts, and more are on the roadmap. If you’d like to help, we welcome pull requests. Detailed instructions for creating descriptors are available here and other contribution ideas are listed in the README.

Quixote 0.5 is available to download now from npm and GitHub. It’s useful and fast. Give it a try and let us know what you think.

Related Reading

comments powered by Disqus