diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a069fd..3b949a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Documentation for Triangle class.
- Tags in changelog link to github.
- Get centroid and midpoints methods for Triangle class.
+- Point class that can be drawn as a tiny circle or tiny line.
### Fixed
diff --git a/source/Plot.js b/source/Plot.js
index 7959379..9c19f11 100644
--- a/source/Plot.js
+++ b/source/Plot.js
@@ -5,7 +5,7 @@ import { Line } from "./Line.js";
import { Circle } from "./Circle.js";
import { Path } from "./Path.js";
import { SVGBuilder } from "./SVGBuilder.js";
-
+import { Point } from "./Point.js";
/**
* @class
* Plot is an object that is able to create, display and download SVG documents.
@@ -48,6 +48,8 @@ export class Plot {
this.lines = [];
this.paths = [];
this.circles = [];
+ this.points = [];
+
this.seedHistory = [];
this.svgBuilder = new SVGBuilder();
@@ -121,9 +123,11 @@ plot.draw();
this.paths.push(shape);
} else if (shape instanceof Circle) {
this.circles.push(shape);
+ } else if (shape instanceof Point){
+ this.points.push(shape);
} else {
throw new TypeError(
- "Unsupported shape type. Shape must be a Line, Path or Circle.",
+ "Unsupported shape type. Shape must be a Line, Path, Point or Circle.",
);
}
}
@@ -146,6 +150,8 @@ plot.draw();
this.addCirclesToSVG();
+ this.addPointsToSVG();
+
this.svg = this.svgBuilder.build();
const timeTaken = +(
@@ -317,6 +323,12 @@ plot.draw();
}
}
+ addPointsToSVG() {
+ for (const point of this.points) {
+ this.svgBuilder.addShape(point.toSVGElement());
+ }
+ }
+
/**
* Appends this plot's SVG element to the document body.
*/
diff --git a/source/Point.js b/source/Point.js
new file mode 100644
index 0000000..6b7ca60
--- /dev/null
+++ b/source/Point.js
@@ -0,0 +1,35 @@
+import { Vector } from "./Vector.js";
+import { Circle } from "./Circle.js";
+import { Line } from "./Line.js";
+/**
+ * Class representing a Point.
+ */
+export class Point {
+ /**
+ * Create a Point.
+ * @param {Vector} position
+ * @param {string} [style = "circle"]
+ */
+ constructor(position, length = 0.25, style = "circle") {
+ this.position = position;
+ this.style = style;
+ this.length = length;
+ }
+
+ /**
+ * @returns {SVGElement}
+ */
+ toSVGElement() {
+ let shape;
+ switch (this.style) {
+ case "circle":
+ shape = new Circle(this.position.x, this.position.y, this.length);
+ return shape.toSVGElement();
+ case "line":
+ let length = new Vector(this.length, 0);
+ let direction = 0;
+ shape = new Line(this.position, Vector.add(this.position, length));
+ return shape.toSVGElement();
+ }
+ }
+}
diff --git a/source/index.js b/source/index.js
index d0c4828..eab8932 100644
--- a/source/index.js
+++ b/source/index.js
@@ -1,4 +1,5 @@
export { Vector } from "./Vector.js";
+export { Point } from "./Point.js";
export { Line } from "./Line.js";
export { Path } from "./Path.js";
export { Circle } from "./Circle.js";
diff --git a/source/tests/Plot.spec.js b/source/tests/Plot.spec.js
index 5e129d9..d356076 100644
--- a/source/tests/Plot.spec.js
+++ b/source/tests/Plot.spec.js
@@ -2,455 +2,473 @@ import { Plot } from "../Plot";
import { Line } from "../Line";
import { Path } from "../Path";
import { Circle } from "../Circle";
+import { Point } from "../Point";
import { Vector } from "../Vector";
describe("Plot", () => {
- describe("constructor", () => {
- it("Creates a seed object literal with hex and decimal properties if Plot is instantiated with a seed property.", () => {
- let plot = new Plot({
- seed: 123,
- });
-
- expect(plot.seed).toHaveProperty("hex");
- expect(plot.seed).toHaveProperty("decimal");
- });
- });
-
- describe("addSingleShape", () => {
- let plot;
- beforeEach(() => {
- plot = new Plot();
- });
-
- it("Adds Lines to the Plot.lines array.", () => {
- const line = Line.fromArray(0, 0, 1, 1);
- plot.addSingleShape(line);
- expect(plot.lines[0]).toEqual(line);
- });
-
- it("Adds Paths to the Plot.paths array.", () => {
- const path = new Path([new Vector(), new Vector(1, 1), new Vector(2, 2)]);
- plot.addSingleShape(path);
- expect(plot.paths[0]).toEqual(path);
- });
-
- it("Adds Circles to the Plot.circles array.", () => {
- const circle = new Circle(0, 0, 5);
- plot.addSingleShape(circle);
- expect(plot.circles[0]).toEqual(circle);
- });
-
- it("Throws a TypeError if an unsupported shape is added.", () => {
- const unsupportedShape = { foo: 15, bar: 20 };
- expect(() => plot.addSingleShape(unsupportedShape)).toThrow(TypeError);
- });
- });
-
- describe("addPathsToSVG", () => {
- it("Adds a path to the SVG via the SVGBuilder.", () => {
- const plot = new Plot();
- plot.add(
- new Path([new Vector(0, 0), new Vector(3, 0), new Vector(2, 1)]),
- );
- plot.draw();
- expect(plot.svg.outerHTML).toMatch("path");
- });
- });
-
- describe("addCirclesToSVG", () => {
- it("Adds a circle to the SVG via the SVGBuilder.", () => {
- const plot = new Plot();
- plot.add(new Circle(0, 0, 100));
- plot.draw();
- expect(plot.svg.outerHTML).toMatch("circle");
- });
- });
-
- describe("addLinesToSVG", () => {
- it("Adds a line to the SVG via the SVGBuilder.", () => {
- const plot = new Plot();
- plot.add(new Line(new Vector(0, 0), new Vector(4, 4)));
- plot.draw();
- expect(plot.svg.outerHTML).toMatch("line");
- });
- });
-
- describe("draw", () => {
- it("Adds the seed to seedHistory if it is not already present.", () => {
- let plot = new Plot();
- plot.setSeed("ffffff");
- plot.draw();
- plot.setSeed("abcdef");
- plot.draw();
- plot.setSeed("ffffff");
- plot.draw();
- expect(plot.seedHistory.length).toBe(2);
- });
- });
-
- describe("setSeed", () => {
- it("Sets the Plot seed.", () => {
- const plot = new Plot();
- plot.draw();
- plot.setSeed("2");
- expect(plot.seed.decimal).toEqual(2);
- });
- });
-
- describe("randomizeSeed", () => {
- it("Sets the Plot seed to a new and different psuedo-random 8-bit hex string.", () => {
- const plot = new Plot();
- plot.draw();
- const oldSeed = plot.seed;
- plot.randomiseSeed();
- expect(plot.seed).not.toEqual(oldSeed);
- });
- });
-
- describe("clear", () => {
- it("Clears the document body.", () => {
- let plot = new Plot();
-
- let line = new Line(new Vector(1, 1), new Vector(5, 5));
-
- plot.add(line);
-
- plot.draw();
-
- expect(plot.lines).toHaveLength(1);
-
- plot.clear();
-
- expect(plot.lines).toHaveLength(0);
- expect(document.body.innerHTML).toBe("");
- });
- });
-
- describe("removeShortLines", () => {
- it("Returns an array of lines that are longer than the minimum length.", () => {
- const plot = new Plot();
- plot.minimumLineLength = 100;
- const lines = [
- Line.fromArray([0, 0, 1, 1]),
- Line.fromArray([0, 0, 0.01, 0]),
- Line.fromArray([0, 0, 100, 100]),
- ];
-
- plot.add(lines);
- plot.draw();
- expect(plot.lines.length).toBe(1);
- });
-
- it.skip("Doesn't remove lines that are longer than the minimum length.", () => {
- const plot = new Plot();
- plot.minimumLineLength = 10;
- const lines = [
- Line.fromArray(0, 0, 100, 100),
- Line.fromArray(0, 0, 1000, 10),
- ];
- plot.add(lines);
- plot.removeShortLines(plot.minimumLineLength);
- // plot.draw();
- expect(plot.lines.length).toBe(2);
- });
- });
-
- describe("handleKeydown", () => {
- let plot;
-
- beforeEach(() => {
- plot = new Plot();
- jest.spyOn(plot, "randomiseSeed").mockImplementation(() => {});
- jest.spyOn(plot, "downloadSVG").mockImplementation(() => {});
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it("Calls randomiseSeed when 'r' is pressed.", () => {
- const event = new KeyboardEvent("keydown", { key: "r" });
- plot.handleKeydown(event);
- expect(plot.randomiseSeed).toHaveBeenCalled();
- });
-
- it("Does not call randomiseSeed when 'r' is not pressed.", () => {
- const event = new KeyboardEvent("keydown", { key: "p" });
- plot.handleKeydown(event);
- expect(plot.randomiseSeed).not.toHaveBeenCalled();
- });
-
- it("Calls downloadSVG when 'd' is pressed.", () => {
- const event = new KeyboardEvent("keydown", { key: "d" });
- plot.handleKeydown(event);
- expect(plot.downloadSVG).toHaveBeenCalled();
- });
-
- it("Does not handle keydown events if an element is focused.", () => {
- const input = document.createElement("input");
- document.body.appendChild(input);
- input.focus();
-
- const event = new KeyboardEvent("keydown", { key: "d" });
- plot.handleKeydown(event);
- expect(plot.downloadSVG).not.toHaveBeenCalled();
- expect(plot.randomiseSeed).not.toHaveBeenCalled();
- });
-
- it("Does nothing if an unmapped key is pressed.", () => {
- const event = new KeyboardEvent("keydown", { key: "z" });
- plot.handleKeydown(event);
- expect(plot.randomiseSeed).not.toHaveBeenCalled();
- expect(plot.downloadSVG).not.toHaveBeenCalled();
- });
- it("Does nothing if an unmapped key is pressed.", () => {
- const event = new KeyboardEvent("keydown", { key: "f" });
- plot.handleKeydown(event);
- expect(plot.randomiseSeed).not.toHaveBeenCalled();
- expect(plot.downloadSVG).not.toHaveBeenCalled();
- });
- });
-
- describe("createUI", () => {
- it("Creates necessary HTML elements", () => {
- const plot = new Plot();
- plot.createUI(0.05);
- expect(document.querySelector("header")).not.toBeNull();
- });
- });
-
- describe("deduplicateLines", () => {
- it("Checks the lines array and removes lines that are duplicated.", () => {
- const plot = new Plot();
-
- plot.add([
- Line.fromArray([0, 0, 5, 5]),
- Line.fromArray([0, 0, 1, 1]),
- Line.fromArray([0, 0, 1, 1]),
- Line.fromArray([0, 0, 1, 1]),
- Line.fromArray([0, 0, 1, 2]),
- ]);
- plot.deduplicateLines();
- expect(plot.lines.length).toBe(3);
- });
- });
-
- describe("downloadSVG", () => {
- test("Downloads the SVG file. (Bad test)", () => {
- // Instantiate the Plot class
- const plot = new Plot(100, 100, {
- seed: 1,
- });
-
- // Mock methods
- const createObjectURLMock = jest.fn(() => "mock-url");
- const revokeObjectURLMock = jest.fn();
- const appendChildMock = jest
- .spyOn(document.body, "appendChild")
- .mockImplementation(() => {});
- const removeChildMock = jest
- .spyOn(document.body, "removeChild")
- .mockImplementation(() => {});
-
- global.URL.createObjectURL = createObjectURLMock;
- global.URL.revokeObjectURL = revokeObjectURLMock;
-
- // Call the downloadSVG method
- plot.downloadSVG("test.svg");
-
- // Check that the serializer and Blob were called correctly
- expect(createObjectURLMock).toHaveBeenCalledTimes(1);
- expect(revokeObjectURLMock).toHaveBeenCalledTimes(1);
- expect(appendChildMock).toHaveBeenCalledTimes(1);
- expect(removeChildMock).toHaveBeenCalledTimes(1);
-
- // Check that the element was created and clicked
- const aElement = document.body.appendChild.mock.calls[0][0];
- expect(aElement.tagName).toBe("A");
-
- // Use URL.createObjectURL to generate the expected absolute URL
- const expectedHref = new URL("mock-url", document.location).href;
- expect(aElement.href).toBe(expectedHref);
- // expect(aElement.download).toBe(plot.filename());
-
- // Clean up mocks
- appendChildMock.mockRestore();
- removeChildMock.mockRestore();
- global.URL.createObjectURL = createObjectURLMock.mockRestore();
- global.URL.revokeObjectURL = revokeObjectURLMock.mockRestore();
- });
- });
-
- describe("removeOverlappingLines", () => {
- it("Checks the lines array and removes sub-lines.", () => {
- const plot = new Plot();
- plot.add([
- Line.fromArray([0, 0, 5, 5]),
- // These lines are sub-lines of the above.
- Line.fromArray([1, 1, 4, 4]),
- Line.fromArray([2, 2, 3, 3]),
-
- // These lines are not sub-lines of the first line.
- Line.fromArray([1, 5, 5, 1]),
- Line.fromArray([3, 2, 2, 3]),
- ]);
- plot.removeOverlappingLines();
-
- expect(plot.lines.length).toBe(3);
- });
- });
-
- describe("createHistoryForm", () => {
- it("Creates a form with options that call setSeed when clicked.", () => {
- let plot = new Plot();
- let parent = document.createElement("div");
- plot.createHistoryForm(parent);
-
- plot.randomiseSeed();
- plot.randomiseSeed();
-
- plot.setSeed = jest.fn();
-
- const options = document
- .getElementById("history")
- .querySelectorAll("option");
- options[1].dispatchEvent(new Event("click"));
- expect(plot.setSeed).toHaveBeenCalled();
- });
- });
-
- describe("createNavigation", () => {
- let plot;
- let parent;
-
- beforeEach(() => {
- plot = new Plot();
- parent = document.createElement("div");
- document.body.appendChild(parent);
- });
-
- afterEach(() => {
- document.body.innerHTML = ""; // Clean up the DOM after each test
- });
-
- it("creates a nav element with a ul as its child", () => {
- plot.createNavigation(parent);
-
- const nav = parent.querySelector("nav");
- const ul = nav.querySelector("ul");
-
- expect(nav).not.toBeNull();
- expect(ul).not.toBeNull();
- expect(parent.contains(nav)).toBe(true);
- expect(nav.contains(ul)).toBe(true);
- });
-
- it("creates two nav items with the correct labels", () => {
- plot.createNavigation(parent);
-
- const ul = parent.querySelector("ul");
- const navItems = ul.querySelectorAll("li");
-
- expect(navItems.length).toBe(2);
- expect(navItems[0].textContent).toBe("⬇️");
- expect(navItems[1].textContent).toBe("🔄");
- });
-
- it("Creates a download nav item that calls downloadSVG when clicked.", () => {
- plot.createNavigation(parent);
- plot.downloadSVG = jest.fn();
- plot.draw();
-
- let button = document.querySelector("nav ul li a");
- button.dispatchEvent(new Event("click"));
- expect(plot.downloadSVG).toHaveBeenCalled();
- });
-
- it("Creates a randomiseSeed nav item that calls randomiseSeed when clicked.", () => {
- plot.createNavigation(parent);
- plot.randomiseSeed = jest.fn();
- plot.draw();
-
- let button = document.querySelector("nav ul li:nth-child(2) a");
- button.dispatchEvent(new Event("click"));
- expect(plot.randomiseSeed).toHaveBeenCalled();
- });
- });
-
- describe("createSeedInput", () => {
- let plot;
- let parentElement;
- beforeEach(() => {
- plot = new Plot();
- parentElement = document.createElement("div");
- });
- afterEach(() => {
- document.body.innerHTML = "";
- });
-
- it("Creates an input element with the correct initial value.", () => {
- plot.createSeedInput(parentElement);
- const seedInput = parentElement.querySelector("input");
- expect(seedInput.value).toBe(plot.seed.hex);
- });
-
- it("Creates an input element that selects input text on focus.", () => {
- plot.createSeedInput(parentElement);
- const seedInput = parentElement.querySelector("input");
- seedInput.select = jest.fn();
-
- seedInput.dispatchEvent(new Event("focus"));
- expect(seedInput.select).toHaveBeenCalled();
- });
-
- it("Creates an input element that sets the Plot seed to the selected option.", () => {
- plot.createSeedInput(parentElement);
- const seedInput = parentElement.querySelector("input");
- plot.setSeed = jest.fn();
-
- seedInput.dispatchEvent(new Event("change"));
- expect(plot.setSeed).toHaveBeenCalled();
- });
- });
-
- describe("add", () => {
- test("Adds a line object to a plot.", () => {
- const plot = new Plot();
-
- const line = new Line(new Vector(0, 0), new Vector(5, 5));
-
- plot.add(line);
-
- expect(plot.lines).toContain(line);
- expect(plot.lines.length).toBe(1);
- });
-
- it.skip("Adds nothing to the plot if given an empty array to add.", () => {});
-
- test("Adds an array of lines to a plot.", () => {
- const plot = new Plot();
-
- const lines = [
- new Line(new Vector(0, 0), new Vector(5, 5)),
- new Line(new Vector(3, 1), new Vector(4, 4)),
- ];
-
- plot.add(lines);
-
- expect(plot.lines).toEqual(lines);
- });
-
- test("Adds a single line and an inner array of lines to a plot.", () => {
- const plot = new Plot();
-
- const array = [
- new Line(new Vector(0, 0), new Vector(5, 5)),
- new Line(new Vector(3, 1), new Vector(4, 4)),
- ];
-
- const line = new Line(new Vector(10, 10), new Vector(11, 17));
- plot.add([array, line]);
- expect(plot.lines.length).toBe(3);
- });
- });
+ describe("constructor", () => {
+ it("Creates a seed object literal with hex and decimal properties if Plot is instantiated with a seed property.", () => {
+ let plot = new Plot({
+ seed: 123,
+ });
+
+ expect(plot.seed).toHaveProperty("hex");
+ expect(plot.seed).toHaveProperty("decimal");
+ });
+ });
+
+ describe("addSingleShape", () => {
+ let plot;
+ beforeEach(() => {
+ plot = new Plot();
+ });
+
+ it("Adds Lines to the Plot.lines array.", () => {
+ const line = Line.fromArray(0, 0, 1, 1);
+ plot.addSingleShape(line);
+ expect(plot.lines[0]).toEqual(line);
+ });
+
+ it("Adds Paths to the Plot.paths array.", () => {
+ const path = new Path([new Vector(), new Vector(1, 1), new Vector(2, 2)]);
+ plot.addSingleShape(path);
+ expect(plot.paths[0]).toEqual(path);
+ });
+
+ it("Adds Circles to the Plot.circles array.", () => {
+ const circle = new Circle(0, 0, 5);
+ plot.addSingleShape(circle);
+ expect(plot.circles[0]).toEqual(circle);
+ });
+
+ it("Adds points to the Plot.points array.", () => {
+ const point = new Point(new Vector());
+ plot.addSingleShape(point);
+ expect(plot.points[0]).toEqual(point);
+ });
+
+ it("Throws a TypeError if an unsupported shape is added.", () => {
+ const unsupportedShape = { foo: 15, bar: 20 };
+ expect(() => plot.addSingleShape(unsupportedShape)).toThrow(TypeError);
+ });
+ });
+
+ describe("addPathsToSVG", () => {
+ it("Adds a path to the SVG via the SVGBuilder.", () => {
+ const plot = new Plot();
+ plot.add(
+ new Path([new Vector(0, 0), new Vector(3, 0), new Vector(2, 1)])
+ );
+ plot.draw();
+ expect(plot.svg.outerHTML).toMatch("path");
+ });
+ });
+
+ describe("addCirclesToSVG", () => {
+ it("Adds a circle to the SVG via the SVGBuilder.", () => {
+ const plot = new Plot();
+ plot.add(new Circle(0, 0, 100));
+ plot.draw();
+ expect(plot.svg.outerHTML).toMatch("circle");
+ });
+ });
+
+ describe("addLinesToSVG", () => {
+ it("Adds a line to the SVG via the SVGBuilder.", () => {
+ const plot = new Plot();
+ plot.add(new Line(new Vector(0, 0), new Vector(4, 4)));
+ plot.draw();
+ expect(plot.svg.outerHTML).toMatch("line");
+ });
+ });
+
+ describe("addPointsToSVG", () => {
+ it("Adds points to the SVG via the SVGBuilder.", () => {
+ const plot = new Plot();
+ plot.add(new Point(new Vector()));
+ plot.draw();
+
+ // Points are circles by default
+ expect(plot.svg.outerHTML).toMatch("circle");
+ })
+ })
+
+ describe("draw", () => {
+ it("Adds the seed to seedHistory if it is not already present.", () => {
+ let plot = new Plot();
+ plot.setSeed("ffffff");
+ plot.draw();
+ plot.setSeed("abcdef");
+ plot.draw();
+ plot.setSeed("ffffff");
+ plot.draw();
+ expect(plot.seedHistory.length).toBe(2);
+ });
+ });
+
+ describe("setSeed", () => {
+ it("Sets the Plot seed.", () => {
+ const plot = new Plot();
+ plot.draw();
+ plot.setSeed("2");
+ expect(plot.seed.decimal).toEqual(2);
+ });
+ });
+
+ describe("randomizeSeed", () => {
+ it("Sets the Plot seed to a new and different psuedo-random 8-bit hex string.", () => {
+ const plot = new Plot();
+ plot.draw();
+ const oldSeed = plot.seed;
+ plot.randomiseSeed();
+ expect(plot.seed).not.toEqual(oldSeed);
+ });
+ });
+
+ describe("clear", () => {
+ it("Clears the document body.", () => {
+ let plot = new Plot();
+
+ let line = new Line(new Vector(1, 1), new Vector(5, 5));
+
+ plot.add(line);
+
+ plot.draw();
+
+ expect(plot.lines).toHaveLength(1);
+
+ plot.clear();
+
+ expect(plot.lines).toHaveLength(0);
+ expect(document.body.innerHTML).toBe("");
+ });
+ });
+
+ describe("removeShortLines", () => {
+ it("Returns an array of lines that are longer than the minimum length.", () => {
+ const plot = new Plot();
+ plot.minimumLineLength = 100;
+ const lines = [
+ Line.fromArray([0, 0, 1, 1]),
+ Line.fromArray([0, 0, 0.01, 0]),
+ Line.fromArray([0, 0, 100, 100]),
+ ];
+
+ plot.add(lines);
+ plot.draw();
+ expect(plot.lines.length).toBe(1);
+ });
+
+ it.skip("Doesn't remove lines that are longer than the minimum length.", () => {
+ const plot = new Plot();
+ plot.minimumLineLength = 10;
+ const lines = [
+ Line.fromArray(0, 0, 100, 100),
+ Line.fromArray(0, 0, 1000, 10),
+ ];
+ plot.add(lines);
+ plot.removeShortLines(plot.minimumLineLength);
+ // plot.draw();
+ expect(plot.lines.length).toBe(2);
+ });
+ });
+
+ describe("handleKeydown", () => {
+ let plot;
+
+ beforeEach(() => {
+ plot = new Plot();
+ jest.spyOn(plot, "randomiseSeed").mockImplementation(() => {});
+ jest.spyOn(plot, "downloadSVG").mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("Calls randomiseSeed when 'r' is pressed.", () => {
+ const event = new KeyboardEvent("keydown", { key: "r" });
+ plot.handleKeydown(event);
+ expect(plot.randomiseSeed).toHaveBeenCalled();
+ });
+
+ it("Does not call randomiseSeed when 'r' is not pressed.", () => {
+ const event = new KeyboardEvent("keydown", { key: "p" });
+ plot.handleKeydown(event);
+ expect(plot.randomiseSeed).not.toHaveBeenCalled();
+ });
+
+ it("Calls downloadSVG when 'd' is pressed.", () => {
+ const event = new KeyboardEvent("keydown", { key: "d" });
+ plot.handleKeydown(event);
+ expect(plot.downloadSVG).toHaveBeenCalled();
+ });
+
+ it("Does not handle keydown events if an element is focused.", () => {
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+ input.focus();
+
+ const event = new KeyboardEvent("keydown", { key: "d" });
+ plot.handleKeydown(event);
+ expect(plot.downloadSVG).not.toHaveBeenCalled();
+ expect(plot.randomiseSeed).not.toHaveBeenCalled();
+ });
+
+ it("Does nothing if an unmapped key is pressed.", () => {
+ const event = new KeyboardEvent("keydown", { key: "z" });
+ plot.handleKeydown(event);
+ expect(plot.randomiseSeed).not.toHaveBeenCalled();
+ expect(plot.downloadSVG).not.toHaveBeenCalled();
+ });
+ it("Does nothing if an unmapped key is pressed.", () => {
+ const event = new KeyboardEvent("keydown", { key: "f" });
+ plot.handleKeydown(event);
+ expect(plot.randomiseSeed).not.toHaveBeenCalled();
+ expect(plot.downloadSVG).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("createUI", () => {
+ it("Creates necessary HTML elements", () => {
+ const plot = new Plot();
+ plot.createUI(0.05);
+ expect(document.querySelector("header")).not.toBeNull();
+ });
+ });
+
+ describe("deduplicateLines", () => {
+ it("Checks the lines array and removes lines that are duplicated.", () => {
+ const plot = new Plot();
+
+ plot.add([
+ Line.fromArray([0, 0, 5, 5]),
+ Line.fromArray([0, 0, 1, 1]),
+ Line.fromArray([0, 0, 1, 1]),
+ Line.fromArray([0, 0, 1, 1]),
+ Line.fromArray([0, 0, 1, 2]),
+ ]);
+ plot.deduplicateLines();
+ expect(plot.lines.length).toBe(3);
+ });
+ });
+
+ describe("downloadSVG", () => {
+ test("Downloads the SVG file. (Bad test)", () => {
+ // Instantiate the Plot class
+ const plot = new Plot(100, 100, {
+ seed: 1,
+ });
+
+ // Mock methods
+ const createObjectURLMock = jest.fn(() => "mock-url");
+ const revokeObjectURLMock = jest.fn();
+ const appendChildMock = jest
+ .spyOn(document.body, "appendChild")
+ .mockImplementation(() => {});
+ const removeChildMock = jest
+ .spyOn(document.body, "removeChild")
+ .mockImplementation(() => {});
+
+ global.URL.createObjectURL = createObjectURLMock;
+ global.URL.revokeObjectURL = revokeObjectURLMock;
+
+ // Call the downloadSVG method
+ plot.downloadSVG("test.svg");
+
+ // Check that the serializer and Blob were called correctly
+ expect(createObjectURLMock).toHaveBeenCalledTimes(1);
+ expect(revokeObjectURLMock).toHaveBeenCalledTimes(1);
+ expect(appendChildMock).toHaveBeenCalledTimes(1);
+ expect(removeChildMock).toHaveBeenCalledTimes(1);
+
+ // Check that the element was created and clicked
+ const aElement = document.body.appendChild.mock.calls[0][0];
+ expect(aElement.tagName).toBe("A");
+
+ // Use URL.createObjectURL to generate the expected absolute URL
+ const expectedHref = new URL("mock-url", document.location).href;
+ expect(aElement.href).toBe(expectedHref);
+ // expect(aElement.download).toBe(plot.filename());
+
+ // Clean up mocks
+ appendChildMock.mockRestore();
+ removeChildMock.mockRestore();
+ global.URL.createObjectURL = createObjectURLMock.mockRestore();
+ global.URL.revokeObjectURL = revokeObjectURLMock.mockRestore();
+ });
+ });
+
+ describe("removeOverlappingLines", () => {
+ it("Checks the lines array and removes sub-lines.", () => {
+ const plot = new Plot();
+ plot.add([
+ Line.fromArray([0, 0, 5, 5]),
+ // These lines are sub-lines of the above.
+ Line.fromArray([1, 1, 4, 4]),
+ Line.fromArray([2, 2, 3, 3]),
+
+ // These lines are not sub-lines of the first line.
+ Line.fromArray([1, 5, 5, 1]),
+ Line.fromArray([3, 2, 2, 3]),
+ ]);
+ plot.removeOverlappingLines();
+
+ expect(plot.lines.length).toBe(3);
+ });
+ });
+
+ describe("createHistoryForm", () => {
+ it("Creates a form with options that call setSeed when clicked.", () => {
+ let plot = new Plot();
+ let parent = document.createElement("div");
+ plot.createHistoryForm(parent);
+
+ plot.randomiseSeed();
+ plot.randomiseSeed();
+
+ plot.setSeed = jest.fn();
+
+ const options = document
+ .getElementById("history")
+ .querySelectorAll("option");
+ options[1].dispatchEvent(new Event("click"));
+ expect(plot.setSeed).toHaveBeenCalled();
+ });
+ });
+
+ describe("createNavigation", () => {
+ let plot;
+ let parent;
+
+ beforeEach(() => {
+ plot = new Plot();
+ parent = document.createElement("div");
+ document.body.appendChild(parent);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = ""; // Clean up the DOM after each test
+ });
+
+ it("creates a nav element with a ul as its child", () => {
+ plot.createNavigation(parent);
+
+ const nav = parent.querySelector("nav");
+ const ul = nav.querySelector("ul");
+
+ expect(nav).not.toBeNull();
+ expect(ul).not.toBeNull();
+ expect(parent.contains(nav)).toBe(true);
+ expect(nav.contains(ul)).toBe(true);
+ });
+
+ it("creates two nav items with the correct labels", () => {
+ plot.createNavigation(parent);
+
+ const ul = parent.querySelector("ul");
+ const navItems = ul.querySelectorAll("li");
+
+ expect(navItems.length).toBe(2);
+ expect(navItems[0].textContent).toBe("⬇️");
+ expect(navItems[1].textContent).toBe("🔄");
+ });
+
+ it("Creates a download nav item that calls downloadSVG when clicked.", () => {
+ plot.createNavigation(parent);
+ plot.downloadSVG = jest.fn();
+ plot.draw();
+
+ let button = document.querySelector("nav ul li a");
+ button.dispatchEvent(new Event("click"));
+ expect(plot.downloadSVG).toHaveBeenCalled();
+ });
+
+ it("Creates a randomiseSeed nav item that calls randomiseSeed when clicked.", () => {
+ plot.createNavigation(parent);
+ plot.randomiseSeed = jest.fn();
+ plot.draw();
+
+ let button = document.querySelector("nav ul li:nth-child(2) a");
+ button.dispatchEvent(new Event("click"));
+ expect(plot.randomiseSeed).toHaveBeenCalled();
+ });
+ });
+
+ describe("createSeedInput", () => {
+ let plot;
+ let parentElement;
+ beforeEach(() => {
+ plot = new Plot();
+ parentElement = document.createElement("div");
+ });
+ afterEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ it("Creates an input element with the correct initial value.", () => {
+ plot.createSeedInput(parentElement);
+ const seedInput = parentElement.querySelector("input");
+ expect(seedInput.value).toBe(plot.seed.hex);
+ });
+
+ it("Creates an input element that selects input text on focus.", () => {
+ plot.createSeedInput(parentElement);
+ const seedInput = parentElement.querySelector("input");
+ seedInput.select = jest.fn();
+
+ seedInput.dispatchEvent(new Event("focus"));
+ expect(seedInput.select).toHaveBeenCalled();
+ });
+
+ it("Creates an input element that sets the Plot seed to the selected option.", () => {
+ plot.createSeedInput(parentElement);
+ const seedInput = parentElement.querySelector("input");
+ plot.setSeed = jest.fn();
+
+ seedInput.dispatchEvent(new Event("change"));
+ expect(plot.setSeed).toHaveBeenCalled();
+ });
+ });
+
+ describe("add", () => {
+ test("Adds a line object to a plot.", () => {
+ const plot = new Plot();
+
+ const line = new Line(new Vector(0, 0), new Vector(5, 5));
+
+ plot.add(line);
+
+ expect(plot.lines).toContain(line);
+ expect(plot.lines.length).toBe(1);
+ });
+
+ it.skip("Adds nothing to the plot if given an empty array to add.", () => {});
+
+ test("Adds an array of lines to a plot.", () => {
+ const plot = new Plot();
+
+ const lines = [
+ new Line(new Vector(0, 0), new Vector(5, 5)),
+ new Line(new Vector(3, 1), new Vector(4, 4)),
+ ];
+
+ plot.add(lines);
+
+ expect(plot.lines).toEqual(lines);
+ });
+
+ test("Adds a single line and an inner array of lines to a plot.", () => {
+ const plot = new Plot();
+
+ const array = [
+ new Line(new Vector(0, 0), new Vector(5, 5)),
+ new Line(new Vector(3, 1), new Vector(4, 4)),
+ ];
+
+ const line = new Line(new Vector(10, 10), new Vector(11, 17));
+ plot.add([array, line]);
+ expect(plot.lines.length).toBe(3);
+ });
+ });
});
diff --git a/source/tests/Point.spec.js b/source/tests/Point.spec.js
new file mode 100644
index 0000000..7750a9f
--- /dev/null
+++ b/source/tests/Point.spec.js
@@ -0,0 +1,32 @@
+import { Point } from "../Point.js";
+import { Vector } from "../Vector.js";
+
+describe("Point", () => {
+ describe("constructor", () => {
+ it("Returns a valid Point object if created from a Vector.", () => {
+ const vector = new Vector(2, 2);
+ const point = new Point(vector);
+
+ expect(point instanceof Point).toBeTruthy();
+ });
+ });
+
+ describe("toSVGElement", () => {
+ it("Returns a valid Circle SVGElement if Point style is 'circle'.", () => {
+ const point = new Point(new Vector(0, 0), "circle");
+ const s = new XMLSerializer();
+ const string = s.serializeToString(point.toSVGElement());
+
+ expect(point.toSVGElement() instanceof SVGElement).toBeTruthy();
+ expect(string).toMatch(` {
+ const point = new Point(new Vector(0, 0), 0.1, "line");
+ const s = new XMLSerializer();
+ const string = s.serializeToString(point.toSVGElement());
+
+ expect(point.toSVGElement() instanceof SVGElement).toBeTruthy();
+ expect(string).toMatch(`