Inking Sites
Table of Contents
Pen and Ink Advice
- Longstride Illustration: Lots of information about inking (found when searching for Master Studies)
class ConstantRadius {
constructor(length) {
this.length = length;
}; // constructor
}; // Constant Radius
class Circulator {
_to_radians;
constructor(angle_increment, center_x, center_y, radius, p5) {
this.angle = 0;
this.angle_increment = angle_increment;
this.center_x = center_x;
this.center_y = center_y;
this.radius = radius;
this.p5 = p5;
}; // constructor
get to_radians() {
if (!this._to_radians) {
this._to_radians = Math.PI/180;
};
return this._to_radians;
}; // to-radians
get theta() {
return this.angle * this.to_radians;
}; // theta
get theta_opposite() {
return this.theta + Math.PI;
}; // theta-opposite
get x_start() {
return this.center_x + this.radius.length * Math.cos(this.theta);
}; // x-start
get y_start() {
return this.center_y + this.radius.length * Math.sin(this.theta);
}; // y-start
get x_end() {
return this.center_x + this.radius.length * Math.cos(this.theta_opposite);
}; // x-end
get y_end() {
return this.center_y + this.radius.length * Math.sin(this.theta_opposite);
}; // y-end
draw() {
this.p5.line(this.x_start, this.y_start, this.x_end, this.y_end);
this.angle += this.angle_increment;
}; // draw
}; // Circulator
const WIDTH = 500;
const HEIGHT = WIDTH;
const POINT_COLOR = "RoyalBlue";
const CENTER_X = WIDTH/2;
const CENTER_Y = HEIGHT/2;
const RADIUS = WIDTH/2;
function circulator_sketch(p5) {
let circulator;
p5.setup = function() {
p5.createCanvas(WIDTH, HEIGHT);
p5.background("white");
p5.stroke(POINT_COLOR);
p5.fill(POINT_COLOR);
const radius = new ConstantRadius(RADIUS);
circulator = new Circulator(1, CENTER_X, CENTER_Y, radius, p5);
}; // setup
p5.draw = function() {
circulator.draw();
}; // draw
}// circulator_sketch
new p5(circulator_sketch, CIRCULATOR_DIV);
class HSLFader {
_increment = 1;
_stroke_color;
_next_color
constructor(circulator, p5, hue=225, saturation=72.7, lightness=56.9) {
this.circulator = circulator;
this.p5 = p5;
this.hue = hue;
this.saturation = saturation;
this.lightness = lightness;
}; // constructor
get increment() {
let lightness = this.p5.lightness(this.stroke_color);
if (lightness >= 100) {
this._increment = -1;
} else if (lightness <= 50) {
this._increment = 1;
}; // if-else-if
return this._increment;
}; // increment
get stroke_color() {
if (!this._stroke_color) {
this.p5.colorMode(this.p5.HSL);
this._stroke_color = this.p5.color(this.hue,
this.saturation,
this.lightness);
}; // if
return this._stroke_color;
}; // stroke_color
get next_color() {
this._stroke_color = this.p5.color(
this.hue, this.saturation,
this.p5.lightness(this.stroke_color) + this.increment
)
return this.stroke_color;
}; // next-color
draw() {
this.p5.stroke(this.next_color);
this.circulator.draw();
}; // draw
}; // CirculaterFader
function hsl_fader_sketch(p5) {
const WIDTH = 500;
const HEIGHT = WIDTH;
const POINT_COLOR = "RoyalBlue";
const CENTER_X = WIDTH/2;
const CENTER_Y = HEIGHT/2;
const RADIUS = WIDTH/2;
let fader;
p5.setup = function() {
p5.createCanvas(WIDTH, HEIGHT);
p5.background("white");
p5.stroke(POINT_COLOR);
p5.fill(POINT_COLOR);
const radius = new ConstantRadius(RADIUS);
const circulator = new Circulator(1, CENTER_X, CENTER_Y, radius, p5);
fader = new HSLFader(circulator, p5);
}; // setup
p5.draw = function() {
fader.draw();
}; // draw
}; // hsl-fader-sketch
new p5(hsl_fader_sketch, HSL_FADER_DIV);
class NoisyRadius {
_noise_coordinate = 0;
constructor(scale, p5, noise_step=0.005) {
this.scale = scale;
this.noise_step = noise_step;
this.p5 = p5;
}; // constructor
get noise_coordinate() {
this._noise_coordinate += this.noise_step;
return this._noise_coordinate;
}; // noise_coordinate
get length() {
this._length = (this.p5.noise(this.noise_coordinate)
* this.scale) + 1;
return this._length;
}; // length
}; // NoisyRadius
function noisy_fader_sketch(p5) {
const WIDTH = 500;
const HEIGHT = WIDTH;
const POINT_COLOR = "RoyalBlue";
const CENTER_X = WIDTH/2;
const CENTER_Y = HEIGHT/2;
const RADIUS = WIDTH/2;
let fader;
p5.setup = function() {
p5.createCanvas(WIDTH, HEIGHT);
p5.background("white");
p5.stroke(POINT_COLOR);
p5.fill(POINT_COLOR);
const radius = new NoisyRadius(RADIUS, p5);
const circulator = new Circulator(1, CENTER_X, CENTER_Y, radius, p5);
fader = new HSLFader(circulator, p5);
}; // setup
p5.draw = function() {
fader.draw();
}; // draw
}; // noisy-fader-sketch
new p5(noisy_fader_sketch, NOISY_FADER_DIV);
The Validator
class checks the type of a given value and throws an Error if it's not correct. It's meant to validate settings, in particular the SliderSettings.
The constructor takes the document
as an argument to make it testable and also to make explicit where it came from. The class also defines an array emptiness
to hold the values that I'll use to check if a variable was set.
This is the document that I'm passing to the Validator
for testing.
const VALID_ID = "validator-id";
const document = new JSDOM(`
<html>
<head></head>
<body>
<div id=${VALID_ID}></div>
</body>
</html>
`).window.document;
Feature: Validator
I don't have a "Given" statement in this part of the post even though I'm implementing the Given
javascript here because each of the Scenarios after this re-use the same Given
but I thought it made sense to go here since it sort of tests the existence of the Validator
.
Given("a Validator", function() {
this.validate = new Validator(document);
});
And here's the class definition that the Given
is using.
class Validator {
emptiness = [null, undefined, NaN];
constructor(document) {
this.document = document;
}
These blocks are the pattern that I'm going to follow for most of the rest of the code:
Our first method checks that a variable holds a number of some kind.
Scenario: The expected number is a number.
Given a Validator
When is_a_number is given a number
Then nothing happens.
// Given a Validator
When("is_a_number is given a number", function() {
this.validate.is_a_number("good-number", faker.number.float());
this.validate.is_a_number("good-number", 0);
});
Then("nothing happens.", function() {});
This is the case where we get what we wanted.
Note: I added a second check for 0
because I was originally using the falsy
check (!(actual)
) but it turns out that 0 would be considered false if you do that so I added an explicit check to make sure I wasn't disallowing 0.
Scenario: The expected number isn't a number.
Given a Validator
When an expected number isn't actually a number
Then it throws an Error.
// Given a Validator
When("an expected number isn't actually a number", function() {
this.bad_call = function() {
this.validate.is_a_number("bad-number", faker.lorem.word());
};
});
Then("it throws an Error.", function() {
expect(this.bad_call.bind(this)).to.throw(Error);
});
I'm just checking for a string. I suppose there are other checks to be made, but since the Validator
is only intended to validate my own code for mistakes, I don't suppose it really needs to be exhaustive.
Scenario: The expected number wasn't assigned.
Given a Validator
When an expected number isn't assigned
Then it throws an Error.
// Given a Validator
When("an expected number isn't assigned", function() {
this.bad_call = function() {
this.validate.is_a_number("no-number", null);
};
});
// Then it throws an error
This isn't explicitly needed, I think, since it falls within "non-number" but I wrote the tests as I made the SliderSettings
and sometimes I would get the parameters out of order (I wish javascript had named variables) so I added null
checks for the arguments to make it more obvious.
And here's the implementation.
is_a_number(identifier, actual) {
if ((!actual && actual !== 0) || isNaN(actual)) {
throw Error(`"${identifier}" must be a number not "${actual}"`);
};
}; // is_a_number
The first condition checks that the number isn't 'falsy', but in javascript 0
is considered falsy so to allow zeros I added the check that it's not 0
if it's falsy. The conditional also checks if it is javascript's idea of a NaN using the global isNaN. This function coerces values to numbers (e.g. the string "120" is not Nan) so I originally used Number.isNaN, since the documentation says that it doesn't coerce values, but that turns out to mean that it just returns false
without coercing the string… I suppose there's a reason for this, particularly since NaN
is meant for numeric data types, so a string is "not a number" but it can't be NaN, but whatever the reasion, it's something to remember, although it seems odd that, in being more strict, Number.isNaN
ends up returning the same value as the global version.
This is for the cases where I have no particular type in the mind but a variable does need to be set to something.
Scenario: The variable has a value set.
Given a Validator
When is_set is given a variable that's set
Then nothing happens.
// Given a Validator
When("is_set is given a variable that's set", function() {
this.validate.is_set("set-variable", faker.lorem.word());
this.validate.is_set("set-variable", 0);
this.validate.is_set("set-variable", false);
});
// Then nothing happens.
Given the broad view of what I'm saying is_set
should check for it'd be hard to check all the possibilities so this mostly checks that I didn't use a falsy
check or something like that which would create false negatives.
Scenario: The variable is empty.
Given a Validator
When is_set is given an empty variable
Then it throws an Error.
// Given a Validator
When("is_set is given an empty variable", function() {
this.bad_call = function() {
this.validate.is_set(null);
};
});
// Then it throws an Error.
Checking for null
should be the most common case, since I'm going to use this to validate an object and make sure it's attributes were all set.
Given a Validator
When is_set is given an undefined variable
Then it throws an Error.
// Given a Validator
When("is_set is given an undefined variable", function() {
this.bad_call = function() {
this.validate.is_set(undefined);
};
});
// Then it throws an Error.
I wouldn't think this would be something that needs to be checked, but since javascript just returns undefined
instead or raising an error if you misspell a variable name, I guess it's useful.
This checks if the value is in whatever is in the emptiness
array, which as of now has:
null
undefined
NaN
I'm not sure about that last one. I think I was trying to use all the falsy
values that weren't likely to be actual values (like 0, false
), but now you can't use infinity either. Not that I can think of a case that I would, but maybe that'll have to be taken out later.
is_set(identifier, actual) {
if (this.emptiness.includes(actual)) {
throw Error(`"${identifier} must be set, not "${actual}"`);
};
}; //is_set
Scenario: The variable has an integer
Given a Validator
When is_an_integer is given a variable with an integer
Then nothing happens.
// Given a Validator
When("is_an_integer is given a variable with an integer", function() {
this.validate.is_an_integer("is-integer", faker.number.int());
this.validate.is_an_integer("is-integer", 1.0);
});
// Then nothing happens
Our happy-path case. The second check in the When
is there to make it clearer that even though 1.0
smells like a float, Number.isInteger
treats it like an integer.
Scenario: The variable has a string
Given a Validator
When is_an_integer is given a string
Then it throws an Error.
// Given a Validator
When("is_an_integer is given a string", function() {
this.bad_call = function() {
this.validate.is_an_integer("not-integer", `${faker.number.int()}`);
};
});
// Then it throws an Error.
I think this is the most likely error - it was passed a string. Interestingly, like the Number.isNaN
function, the Number.isInteger function that I'm using also doesn't coerce strings so while "5" isn't not NaN, it also isn't an integer.
Scenario: "is_an_integer" is given a float.
Given a Validator
When is_an_integer is given a float
Then it throws an Error.
// Given a Validator
When("is_an_integer is given a float", function() {
this.bad_call = function() {
this.validator.is_an_integer("float-not-integer", 5.5);
};
});
// Then it throws an Error.
Since I showed above that 5.0 is considered an integer I felt obliged to make sure that other floats aren't considered integers.
Scenario: The integer variable wasn't set.
Given a Validator
When an expected integer wasn't set
Then it throws an Error.
// Given a Validator
When("an expected integer wasn't set", function() {
this.bad_call = function() {
this.validate.is_an_integer("no-integer", null);
};
});
// Then it throws an Error.
This is, oddly, the only built-in that I could find that does type checks (but I didn't look that hard, and I was using DuckDuckGo so I might have found something using a different search engine).
is_an_integer(identifier, actual) {
if (!Number.isInteger(actual)) {
throw Error(`"${identifier}" must be an integer, not ${actual}`);
};
}; // is_an_integer
This is what really started it all. I had some mysterious errors drawing a spiral which turned out to be because I had changed a div ID in the HTML but not in the javascript. So this checks to see if there really an element with the ID. It doesn't check if it's the right ID, but I don't know that there's a simple way to do that anyway.
Scenario: A valid ID is given.
Given a Validator
When is_an_element_id is given a valid element ID
Then nothing happens.
// Given a Validator
When("is_an_element_id is given a valid element ID", function() {
this.validate.is_an_element_id("good-id", VALID_ID);
});
// Then nothing happens.
Since I'm using JSDOM I needed to use a real ID to check if it was valid, not a random string.
Scenario: An invalid ID is given.
Given a Validator
When is_an_element is given an invalid element ID
Then it throws an Error.
// Given a Validator
When("is_an_element is given an invalid element ID", function() {
this.bad_call = function() {
this.validate.is_an_element_id("bad-id", VALID_ID + "invalid");
};
});
// Then it throws an Error.
Although I suppose the odds of a random string matching my div
ID is pretty low, I thought that mangling the ID would be a better guaranty that it won't match than using faker
to generate a string.
This relies on the built-in document.getElementById
method (well, built-in when there's a browser).
is_an_element_id(identifier, actual) {
if (this.document.getElementById(actual) === null) {
throw Error(`"${identifier}" isn't a valid ID - "${actual}"`);
};
}; // is_an_id
The Slidini class is going to create two HTML elements - a Slider and a label (what I call a caption) for the slider.
class Slidini {
_slider = null;
_caption = null;
Feature: A Slidini class to hold a slider and its caption.
constructor(slider_settings, caption_settings, p5) {
this.slider_settings = slider_settings;
this.caption_settings = caption_settings;
this.p5 = p5;
} // constructor
The slider_settings
and caption_settings
should be instances of the SliderSettings
and CaptionSettings
classes (see the Slider and Caption Settings post) and the p5
object should be a p5.js instance.
This creates the slider and the caption. I decided to muddy them together like this in order to create the callback to update the caption whenever the slider is updated, and to add the initial caption once the slider is created.
get slider() {
if (this._slider === null) {
// create the slider
this._slider = this.p5.createSlider(
this.slider_settings.min,
this.slider_settings.max,
this.slider_settings.default_value,
this.slider_settings.step_size,
);
// attach it to the div tag
this._slider.parent(this.slider_settings.slider_div);
// set the callback to change label on update
this._slider.input(() => this.update_caption());
// add the label to the slider
this.update_caption();
}
return this._slider;
}
This is the caption for the slider. It expects there to be a div
that it can stick the caption into.
get caption() {
if (this._caption === null) {
this._caption = this.p5.select(
this.caption_settings.div_selector);
}
return this._caption;
}
This sets the caption to the current value of the slider. It's used both to initialize the caption and as a callback to update the caption whenever the slider's value changes.
update_caption() {
this.caption.html(
`${this.caption_settings.label}: ` +
`${this.slider.value().toFixed(this.caption_settings.precision)}`);
} // update_caption
The SliderSettings
class holds the values for the Slidini class and optionally validates the values it's been given.
Since there's only one method to call and it defers everything to the Validator I'm going to have one Scenario to test, but to try and make it easier to read I'm going to break up the Then-And statements within it, but I'm not going to break up the check_rep
method itself so I'm not going to show the implementation under each test it satisfies, but just show the class definition in entirety after all the tests.
First we need to import some javascript. Even though I'm faking all the methods I'm going to use on the Validator
class I used the real definition because I was hoping to figure out how to get sinon
to copy all the methods automatically, but I didn't see anything indicating it can, so maybe next time I'll just make a fake object instead.
import { expect } from "chai";
import { faker } from "@faker-js/faker";
import { Given, When, Then } from "@cucumber/cucumber";
import { fake, replace } from "sinon";
import { SliderSettings } from "../../../../files/javascript/slider.js";
import { Validator } from "../../../../files/javascript/validator.js";
Since the Validator's methods get called more than once I need to be able to know what (zero-based) index each call is - e.g. checking default_value
is the third Validator.is_a_number
call that the SliderSettings
makes, so to retrieve the object to check that the call went as expected I need to get the sinon
call object at index 2. I'm making the IS
object below to hold the indices to get the calls for each property… it'll make more sense later.
const IS = {
NUMBER: { min: 0,
max: 1,
default_value : 2,
step_size: 3,
},
ELEMENT: {
slider_div: 0,
}
};
const METHODS = ["is_a_number", "is_set", "is_an_integer", "is_an_element_id"];
The METHODS
array holds the names of all of the Validator's methods that check_rep
uses so that I can replace the Validator's methods in a loop instead of doing it separately for each one.
Now I'll build the SliderSettings
with the faked Validator
methods in the cucumber Given
function.
Feature: Slider Settings
Scenario: check_rep is called.
Given a Slider Settings
Since all the methods are going to be faked, I don't need a mock document
the way I did for the Validator
tests.
Given("a Slider Settings", function() {
this.validator = new Validator({});
Now that I have a Validator
instance, I can replace all the methods to test with fakes
.
for (const method of METHODS) {
replace(this.validator, method,
fake.returns(null));
}
Next, I'll fake the arguments passed to the SliderSettings
object and store them in the World
this
so that I can check that they were passed to the validator as expected in the tests.
this.min = faker.number.float();
this.max = faker.number.float();
this.default_value = faker.number.float();
this.step_size = faker.number.float();
this.slider_div = faker.lorem.word();
Finally, I can create the SliderSettings
to test.
this.settings = new SliderSettings(this.min,
this.max,
this.default_value,
this.step_size,
this.slider_div,
this.validator);
});
This is the only call to SliderSettings
I make.
When check_rep is called
When("check_rep is called", function() {
this.settings.check_rep();
});
Now the rest of the tests check all the calls to the Validator
that the check_rep
method made.
The first property that check_rep
validates is the min
.
Then it checked the min
// Given a Slider Settings
// When check_rep is called
Then("it checked the min", function() {
expect(this.validator.is_a_number.getCall(IS.NUMBER.min).calledWith(
"min", this.min
)).to.be.true;
});
this.validator.is_a_number
is a faked method which allows us to check the arguments passed to it by getting the call object using getCall
and checking the arguments with calledWith
. In this case checking min
is the first call to is_a_number
so I'm passing 0
to getCall
, retrieving it from the IS
object I created earlier (using IS.NUMBER.min
).
I'm not crazy about the need to pass in strings, but since they always match the variable name I guess it's easy enough to see any typos.
The rest of the checks are pretty much the same thing but with different variables so I'll stop the commentary for a while.
And it checked the max
Then("it checked the max", function() {
expect(this.validator.is_a_number.getCall(IS.NUMBER.max).calledWith(
"max", this.max
)).to.be.true;
});
And it checked the default_value
Then("it checked the default_value", function() {
expect(this.validator.is_a_number.getCall(IS.NUMBER.default_value).calledWith(
"default_value", this.default_value
)).to.be.true;
});
And it checked the step_size
Then("it checked the step_size", function() {
expect(this.validator.is_a_number.getCall(IS.NUMBER.step_size).calledWith(
"step_size", this.step_size
)).to.be.true;
});
And it checked the slider_div
Then("it checked the slider_div", function() {
expect(this.validator.is_an_element_id.getCall(IS.ELEMENT.slider_div).calledWith(
"slider_div", this.slider_div
)).to.be.true;
});
Now that we have the tests, I'll implement the slider settings.
The SliderSettings
holds the settings to build Slidini, the Slider and Caption holder. It really could be done with a plain object (which is what it was) but I decided to add a validator to make sure that I was getting all the parameters right.
class SliderSettings {
constructor(min, max, default_value, step_size,
slider_div,
validator) {
this.min = min;
this.max = max;
this.default_value = default_value;
this.step_size = step_size;
this.slider_div = slider_div;
this.confirm = validator;
}; // constructor
check_rep(){
this.confirm.is_a_number("min", this.min);
this.confirm.is_a_number("max", this.max);
this.confirm.is_a_number("default_value", this.default_value);
this.confirm.is_a_number("step_size", this.step_size);
this.confirm.is_an_element_id("slider_div", this.slider_div);
}; // check_rep
}; // SliderSettings
The Caption Settings are pretty much just like the Slider Settings except that they are meant for the label that lets the user know what the current slider's value is. I used to have everything in the SliderSettings but it wasn't obvious what belonged to which so I broke them apart.
This is pretty much exactly the same as the testing for the SliderSettings
so I won't have a whole lot to add to it.
import { expect } from "chai";
import { faker } from "@faker-js/faker";
import { Given, When, Then } from "@cucumber/cucumber";
import { fake, replace } from "sinon";
import { CaptionSettings } from "../../../../files/javascript/slider.js";
import { Validator } from "../../../../files/javascript/validator.js";
const CAPTION_IS = {
SET: {
label: 0
},
INTEGER: {
precision: 0
},
ELEMENT: {
caption_div: 0
}
};
const METHODS = ["is_a_number", "is_set", "is_an_integer", "is_an_element_id"];
Feature: Settings for a caption/label.
Scenario: The CaptionSettings is built.
Given a CaptionSettings
When the properties are checked
Then they are the expected properties.
Given("a CaptionSettings", function() {
These are the three properties that the Slidini class is going to need to set up the label.
this.label = faker.lorem.words();
this.precision = faker.number.int();
this.caption_div = faker.lorem.word();
this.div_id_selector = "#" + this.caption_div;
Once again I'm replacing the Validator methods so I can check the calls and as a side-effect the document won't get used so I don't need JSDOM
.
this.validator = new Validator({});
for (const method of METHODS) {
replace(this.validator, method,
fake.returns(null));
}
And finally I'll build our software to test.
this.caption_settings = new CaptionSettings(this.label,
this.precision,
this.caption_div,
this.validator);
Putting the values to check into variables seems like an unnecessary step, since you could test and retrieve the properties at the same time, but I like the use of When
and it makes the lines in the Then
block a little shorter.
When("the properties are checked", function() {
this.actual_label = this.caption_settings.label;
this.actual_precision = this.caption_settings.precision;
this.actual_caption_div = this.caption_settings.caption_div;
});
Then("they are the expected properties.", function() {
expect(this.actual_label).to.equal(this.label);
expect(this.actual_precision).to.equal(this.precision);
expect(this.actual_caption_div).to.equal(this.caption_div);
});
Scenario: check_rep is called.
Given a CaptionSettings
When CaptionSettings.check_rep is called
Oddly, when I just said "When check_rep is called" instead of "When CaptionSettings.check_rep is called" cucumber ended up using the function I made for the SliderSettings
tests that had the same When
string. For some reason it lets them pollute each other's tests, even though they have separate feature and step files. I suppose this could make it easier to re-use test-functions, but it makes it kind of dangerous since you have to make sure that everything has a unique title.
Or maybe there's something I'm missing…
When("CaptionSettings.check_rep is called", function() {
this.caption_settings.check_rep();
});
Then it checks the label
Then("it checks the label", function() {
expect(this.validator.is_set.getCall(CAPTION_IS.SET.label).calledWith(
"label", this.label
)).to.be.true;
});
And it checks the precision
Then("it checks the precision", function() {
expect(this.validator.is_an_integer.getCall(CAPTION_IS.INTEGER.precision).calledWith(
"precision", this.precision
)).to.be.true;
});
And it checks the caption div ID.
Then("it checks the caption div ID.", function() {
expect(this.validator.is_an_element_id.getCall(
CAPTION_IS.ELEMENT.caption_div).calledWith(
"caption_div", this.caption_div
)).to.be.true;
});
The p5.select
method uses CSS selectors so it needs you to put a #
sign in front of the DIV ID to tell it that it's an ID.
Scenario: The caption DIV selector is set up
Given a CaptionSettings
When the caption DIV ID selector is retrieved
Then the caption DIV selector has the pound sign.
When("the caption DIV ID selector is retrieved", function() {
this.actual_div_id_selector = this.caption_settings.div_selector;
});
Then("the caption DIV selector has the pound sign.", function() {
expect(this.actual_div_id_selector).to.equal(this.div_id_selector);
});
So here's where I implemennt the class. The label property is a string that gets inserted into the string that's displayed on the Label element. Maybe I should have called it something else… The precision property is used to decide how many decimal places to show in the Label, and the caption_div is the ID of the element where I'm going to stick the Label.
class CaptionSettings {
_div_selector = null;
constructor(label, precision, caption_div, validator) {
this.label = label;
this.precision = precision;
this.caption_div = caption_div;
this.validator = validator;
};
get div_selector(){
if (this._div_selector === null) {
this._div_selector = "#" + this.caption_div;
}
return this._div_selector;
};
check_rep() {
this.validator.is_set("label", this.label);
this.validator.is_an_integer("precision", this.precision);
this.validator.is_an_element_id("caption_div", this.caption_div);
};
}; // CaptionSettings
In my code from my spiral post I used document.getElementById as a check to make sure that I was passing in a valid div
ID to p5.js
and I decided to add some testing to make sure my validator was working as I expected. The problem was that when you run tests in node.js
the document
object doesn't exist. One way to get around this might be to use Selenium
or some other browser-tester but that seemed like overkill.
My first idea was that I would just mock the document
object using sinon since this is a fairly easy thing to do in python (maybe not mocking document
, specifically, since that is a javascript thing, but mocking objects in a module is fairly easy) but I simply couldn't find anything that seemed to indicate that this was possible, only posts saying that sinon
doesn't work that way.
So then I stumbled upon jsdom, which appeared to be what I wanted to mock the document. The problem was that it's more of a browser simulator not a means to replace objects so I still had to figure out a way to replace the document
that my class was expecting with the jsdom
document. In the other post where I first used jsdom
I followed the pattern shown in this github repository where you stick the jsdom document
into the global
variable which makes it appear to be a global variable just like it is when you run the code in a browser.
global.document = dom.window.document;
Where dom
is an instance of jsdom
setup with some HTML to use for the testing. Amazingly this did seem to work…
Then I got to this post where I wanted to document getting jsdom
to work and, even though I was using a simpler version of the previous post, I found that the tests were failing mysteriously (pretty much everything with javascript is mysterious to me). With a lot of troubleshooting, I managed to find out that instead of using the dom
I was creating here it was still using the same one from the other testing. Except sometimes it was using the dom
I defined here and these tests passed but then the other tests failed. I guess global
means global in the truer sense, not just for the scope of a single set of tests, and there's some kind of race going on between the different pieces of code that are trying to set and use this global document
. Oy.
So, then I went looking at the jsdom
documentation (which is pretty much just a readme and all the "issues" people have posted), but there was one page on their wiki titled Don't Stuff jsdom Globals Onto the Node Global, which, I guess, means that I shouldn't have done what I did and all those Stack Overflow answers say to do. The page scolding all us silly people for using global
did have a few examples of how they thought you should do it instead, so then I tried their solution of adding the javascript for my class to the jsdom
object instead of using it in the cucumber.js
test code. That way my class would have access to the document
in their global space.
Easy-peasy. Well… this produced more mysterious failures except now it was in a different place. After running their examples in the Node REPL unsuccessfully I found this "issue" where it's explained that they don't support the <script type="module">
parameter (so you can't use ES6 imports like I do). Okay, so then I dumped the class definition to a string and added it directly, but no matter what I couldn't get jsdom
to interpret any class I put in, although functions did work, so I came to the conclusion that they don't support javascript classes either.
I couldn't find anything specific about not supporting classes, but trying to search using terms like class
and document
brings up so much irrelevant stuff that it's maddening to even look for anything so there might have been some skimming fatigue that blinded me to any documents about it, if they existed.
But then, while fiddling with the Node REPL I found that defining the document
before instantiating my class made it work, so I thought, okay, why am I trying so hard to do all this patching when I can just create the document
object in my tests and then create the object under test? Well, the answer to that is - "because it doesn't work". For some reason the document
object existed before and after creating my test object but it was always undefined within the class method I was testing. Mysterious.
In the end I ended up doing an ugly workaround which didn't really even require using jsdom
, although I guess using it at least validates that I'm using the right method name… Anyway, here it is.
This is a Cucumber.js test so I'll create a simple feature file for a class that retrieves a document element and test the case where the ID is correct and the one where it is incorrect.
Feature: An Element Getter
Scenario: A Valid ID
Given an ElementGetter with the correct ID
When the element is retrieved
Then it is the expected element.
Scenario: The wrong ID.
Given an ElementGetter with the wrong ID
When the element is retrieved using the wrong ID
Then it throws an error.
Now the implementation. This is kind of a useless class, but this was supposed to be about how to get jsdom
working so I can test code expecting a document
.
This is the class I'm going to test.
class ElementGetter {
#element;
constructor(element_id, document) {
this.element_id = element_id;
this.document = document;
};
The constructor shows the main change I made to get it working - instead of using a global document
I added it as an argument. I went through all that rigamarole trying to avoid this since it seemed like I was changing code just to test it, but now that I think about it, it's what I'd've done in python anyway, since I kind of don't like these "magic" objects that show up without being created or imported like they do in javascript.
I made a getter for the retrieved element. It probably would have been easier in this case if I didn't store it, since I could test both when the ID is correct and when it isn't with the same object, just by changing the this.element_id
value, but it's a pattern I often use and it gave me the chance to test out javascript's Private Class Fields syntax. To be quite honest, I think using the pound sign (#
) is kind of ugly - I prefer the underscore, and Pygments draws red boxes around the #
- but at least I know about it.
The main value in using a getter here is that it can check that the element exists, since an invalid ID passed to getElementById
will just return a null
object rather thhan throw in error.
get element() {
if (!this.#element) {
this.#element = this.document.getElementById(this.element_id);
if (!this.#element) {
throw Error(`Unable to pull the element with ID '${this.element_id}'`);
};
};
return this.#element;
};
And now the test code.
import { expect } from "chai";
import { Given, When, Then } from "@cucumber/cucumber";
import { JSDOM } from "jsdom";
These are the libraries that I installed to support testing. Using jsdom
instead of creating a mock was convenient, but I might have to watch what the overhead is if I make a lot of tests that use it.
Now I'll import the ElementGetter
class that I defined above. It occurs to me that for a case like this where I don't actually use the code for anything other than testing I could have put it next to the tests, but I guess this is a better dress rehearsal for really using code in a post.
Note the extra step up the path (../
) because this time I followed the cucumber
example and put the steps in a folder named steps
instead of in a file named steps.js
, which might make it easier to organize in the future if I have more to test, but makes relative paths that much more painful.
import { ElementGetter } from "../../../../files/posts/cucumber-chai-and-jsdom/puller.js";
Here's where I create the jsdom
object with a div
that the ElementGetter
can get. I'm passing in the whole HTML string but in the documentation they sometimes just pass in the body.
const EXPECTED_ID = "expected-div";
const document = (new JSDOM(`<html>
<head></head>
<body>
<div id='${EXPECTED_ID}'></div>
</body></html>`)).window.document;
This is the first scenario where I expect the ElementGetter
to successfully find the element. There's not a lot to test here, other than it doesn't crash.
Given("an ElementGetter with the correct ID", function() {
this.puller = new ElementGetter(EXPECTED_ID, document);
});
When("the element is retrieved", function() {
this.actual_element = this.puller.element;
});
Then("it is the expected element.", function() {
expect(this.actual_element.id).to.equal(EXPECTED_ID);
});
This is the more interesting case where we give the ElementGetter
an ID that doesn't match any element in the page.
Given("an ElementGetter with the wrong ID", function() {
this.puller = new ElementGetter(EXPECTED_ID + "abc", document);
});
Since I made a getter to retrieve the element, you can't pass it directly to chai
to test - trying to pass this.puller.element
to chai will trigger the error before chai gets it - so instead I'm using something I learned working with pytest. I'm creating a function that will retrieve the element and then passing the function to chai
to test that it raises an error.
When("the element is retrieved using the wrong ID", function() {
this.bad_call = function(){
this.puller.element
}
});
Then("it throws an error.", function() {
expect(this.bad_call).to.throw(Error);
});
I suppose the biggest lesson is that I shouldn't have tried so hard to fake the document
object as a magic global object the way it normally is used and instead just gone for an explicit argument that gets passed to the class (or function) that needs it (which sort of follows the Dependency Injection Pattern). I also learned that jsdom
is interesting but behind the ECMA standard, as were some of the other libraries I ran into in trying to solve different parts of this problem, so I have to either decide to not use ECMA 6 or not rely so much on these other libraries that don't use the current standards.
This is a sketch that extends the Generative Art Circles post (slightly) to make concentric circles. I was going to make a spiral but realized after I wrote out the code that I had actually made concentric circles - so it's sort of a half-step from the circle to the spiral.
The ConcentricCircles
class keeps track of the parameters for the circles and draws them. Here I'm declaring the class and some fields to store the parameters.
class ConcentricCircles {
// geometry
degrees_in_a_circle = 360;
to_radians = (2 * Math.PI)/ this.degrees_in_a_circle;
// the starting values for the circles
radius = 5;
_step = 5;
// the center of our sketch (and the circles)
center_x;
center_y;
// the size of the circle to draw the circles
point_diameter = 1;
constructor(p5, center_x, center_y, maximum_radius){
this.p5 = p5;
this.center_x = center_x;
this.center_y = center_y;
this.maximum_radius = maximum_radius;
} // constructor
The constructor takes the p5.js
object and the coordinates for the center of the circles (center_x
and center_y
) as well as the maximum_radius
- the value at which the circles have hit their limit and should turn around. This is presumably half the width of the canvas, but since the ConcentricCircles
class isn't creating the canvas I thought it should be the code that creates the sketch that decides what the limit is.
The step
is the amount the radius increases between each circle. I put it in a getter so that it can check if the circles are at the limits of the expected maximum (or minimum) size and thus should change direction (shrink instead of grow or vice-versa).
get step() {
if (this.radius > (this.maximum_radius - this._step) || this.radius <= 0) {
this._step *= -1;
}
return this._step
}
The draw
method is what the sketch function calls to tell the ConcentricCircles
to draw a circle. This is similar to the sketch that drew a single circle except that the radius gets incremented at after the circle is drawn.
draw() {
let radians, x, y;
for (let angle = 0; angle < this.degrees_in_a_circle; angle += 1){
radians = angle * this.to_radians;
x = this.center_x + this.radius * Math.cos(radians);
y = this.center_y + this.radius * Math.sin(radians);
this.p5.circle(x, y, this.point_diameter);
}
this.radius += this.step;
}
This is the function that gets passed to p5
to execute the setup
and draw
methods.
After declaring the function and some constants for the canvas, it creates an instance of the ConcentricCircles
class.
function concentric_circles(p5){
// the size of the canvas and the color of the circles
const WIDTH = 500;
const HEIGHT = WIDTH;
const POINT_COLOR = "RoyalBlue";
const circles = new ConcentricCircles(p5, WIDTH/2, HEIGHT/2, WIDTH/2);
The setup
method doesn't do anything fancy, although I did have to set the frameRate
to a slower speed otherwise I couldn't see the circles being animated.
p5.setup = function(){
p5.createCanvas(WIDTH, HEIGHT);
p5.background("white");
p5.stroke(POINT_COLOR);
p5.fill(POINT_COLOR);
p5.frameRate(10);
} // end setup
The draw
method defers to the ConcentricCircles.draw
method to do the actual drawing of the circles, but I added a light white overlay so that the circles fade out and you can see the animation.
p5.draw = function() {
circles.draw();
p5.background(255, 75);
}// end draw
That's pretty much it for the sketch, the last thing to do is just pass the concentric_circles
function to p5
along with the id
for the div
where the sketch should go (which I defined but don't show in the post).
new p5(concentric_circles, CONCENTRIC_CIRCLES_DIV);
And that's it for drawing concentric circles, now on to spirals.
To specify where the parts of the sketch go I added some div
tags to the HTML so I'm going to create some javascript constants with the div IDs to make it easier to keep track of them.
This is the ID for the div
where the main sketch will go, it gets passed to the p5 constructor, along with the sketch definition, to create the the P5 instance.
const SPIRAL_DIV = "spiral-0a168ba9";
I'm going to add some sliders to make it easier to adjust some of the parameters and see how that affects the sketch. These are the IDs of the div
tags where I'm going to put the sliders to change some of the sketch values. The angle and radius sliders will set how much the angle and radius will change as the circle is drawn. If, for example, the angle slider is set to 5, then every point that's added will be rotated five degrees from the previous point, and if the radius is set to 5, then the radius will grow by 5 every time a point is added.
The circle slider is a little different in that it sets the diameter for the circles that I'm drawing to create the spiral, so it's just an aesthetic setting.
const SPIRAL_ANGLE_SLIDER = "angle-slider-0a168ba9";
const SPIRAL_RADIUS_SLIDER = "radius-slider-0a168ba9";
const SPIRAL_CIRCLE_SLIDER = "circle-slider-0a168ba9";
I also created some div
tags that I'll put some text into to show the current value of each of the sliders.
const SPIRAL_ANGLE_TEXT = "angle-text-0a168ba9";
const SPIRAL_RADIUS_TEXT = "radius-text-0a168ba9";
const SPIRAL_CIRCLE_TEXT = "circle-text-0a168ba9";
This is used by the settings classes to try and see if I'm passing in valid arguments.
const VALIDATOR = new Validator(document);
The values used to create the angle-increment slider.
const ANGLE_SLIDER = new SliderSettings(
0,
40,
5,
0,
SPIRAL_ANGLE_SLIDER,
VALIDATOR
);
const ANGLE_CAPTION = new CaptionSettings(
"Angle Increment",
2,
SPIRAL_ANGLE_TEXT,
VALIDATOR
);
ANGLE_SLIDER.check_rep();
ANGLE_CAPTION.check_rep();
The values used to create the radius increment slider.
const RADIUS_SLIDER = new SliderSettings(
0,
20,
1,
0,
SPIRAL_RADIUS_SLIDER,
VALIDATOR
);
const RADIUS_CAPTION = new CaptionSettings(
"Radius Increment",
2,
SPIRAL_RADIUS_TEXT,
VALIDATOR
);
RADIUS_SLIDER.check_rep();
RADIUS_CAPTION.check_rep();
The values used to create the circle diameter slider.
const CIRCLE_SLIDER = new SliderSettings(
1,
100,
1,
0,
SPIRAL_CIRCLE_SLIDER,
VALIDATOR
);
const CIRCLE_CAPTION = new CaptionSettings(
"Point Diameter",
2,
SPIRAL_CIRCLE_TEXT,
VALIDATOR
);
CIRCLE_SLIDER.check_rep();
CIRCLE_CAPTION.check_rep();
class Spiralizer {
// geometry
degrees_in_a_circle = 360;
to_radians = (2 * Math.PI)/ this.degrees_in_a_circle;
// the starting values for the circles
radius = 1;
angle = 0;
// the center of our sketch (and the circles)
center_x;
center_y;
constructor(p5, center_x, center_y, maximum_radius,
angle_slider, radius_slider, circle_slider){
this.p5 = p5;
this.center_x = center_x;
this.center_y = center_y;
this.maximum_radius = maximum_radius;
// the amount to move the points on the circle as they're drawn
this.angle_increment = angle_slider;
this.radius_increment = radius_slider;
// the size of the circle to draw the circles
this.point_diameter = circle_slider;
} // constructor
draw() {
let radians, x, y;
radians = this.angle * this.to_radians;
x = this.center_x + this.radius * Math.cos(radians);
y = this.center_y + this.radius * Math.sin(radians);
this.p5.circle(x, y, this.point_diameter.value());
this.radius += this.radius_increment.value();
this.angle += this.angle_increment.value();
if (this.radius >= this.maximum_radius) {
this.radius = this.radius_increment.value();
}
} // end draw
reset() {
this.radius = this.radius_increment.value();
this.angle = 0;
} // end reset
This is going to be the sketch that we pass to the P5 constructor to create the animation.
function spiral_sketch(p5) {
// the size of the canvas and the color of the circles
const WIDTH = 500;
const HEIGHT = WIDTH;
const POINT_COLOR = "RoyalBlue";
let spiralizer;
let angle_slider;
let radius_slider;
let circle_slider;
p5.setup = function(){
p5.createCanvas(WIDTH, HEIGHT);
p5.background("white");
p5.stroke(POINT_COLOR);
p5.fill(POINT_COLOR);
angle_slider = new Slidini(ANGLE_SLIDER, ANGLE_CAPTION, p5);
radius_slider = new Slidini(RADIUS_SLIDER, RADIUS_CAPTION, p5);
circle_slider = new Slidini(CIRCLE_SLIDER, CIRCLE_CAPTION, p5);
spiralizer = new Spiralizer(p5, WIDTH/2, HEIGHT/2, WIDTH/2,
angle_slider.slider,
radius_slider.slider,
circle_slider.slider);
} // end setup
p5.draw = function() {
spiralizer.draw();
p5.background(255, 5);
}// end draw
p5.doubleClicked = function() {
spiralizer.reset();
p5.background("white");
} // end doubleClicked
To create the animation I'll create a p5 object by passing in the function from the previous section and the div ID to identify where in the page the sketch should go.
new p5(spiral_sketch, SPIRAL_DIV);