From 0abb22130f4dca37a310afe9c7a09427ee068f47 Mon Sep 17 00:00:00 2001
From: Ad5001 <mail@ad5001.eu>
Date: Sat, 12 Oct 2024 00:40:46 +0200
Subject: [PATCH 1/2] Disable domain tests, started base tests.

---
 common/package-lock.json       |  25 +++++++
 common/package.json            |   2 +
 common/src/events.mjs          |   3 +-
 common/test/general/events.mjs | 122 +++++++++++++++++++++++++++++++++
 common/test/hooks.mjs          |   2 -
 common/test/math/domain.mjs    |  86 +++++++++++------------
 6 files changed, 194 insertions(+), 46 deletions(-)
 create mode 100644 common/test/general/events.mjs

diff --git a/common/package-lock.json b/common/package-lock.json
index b90e4f5..ee15bf5 100644
--- a/common/package-lock.json
+++ b/common/package-lock.json
@@ -19,9 +19,11 @@
       },
       "devDependencies": {
         "@types/chai": "^5.0.0",
+        "@types/chai-spies": "^1.0.6",
         "@types/mocha": "^10.0.8",
         "chai": "^5.1.1",
         "chai-as-promised": "^8.0.0",
+        "chai-spies": "^1.1.0",
         "esm": "^3.2.25",
         "mocha": "^10.7.3"
       }
@@ -2199,6 +2201,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/chai-spies": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/chai-spies/-/chai-spies-1.0.6.tgz",
+      "integrity": "sha512-xkk4HmhBB9OQeTAifa9MJ+6R5/Rq9+ungDe4JidZD+vqZVeiWZwc2i7/pd1ZKjyGlSBIQePoWdyUyFUGT0rv5w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/chai": "*"
+      }
+    },
     "node_modules/@types/estree": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -2514,6 +2526,19 @@
         "chai": ">= 2.1.2 < 6"
       }
     },
+    "node_modules/chai-spies": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.1.0.tgz",
+      "integrity": "sha512-ikaUhQvQWchRYj2K54itFp3nrcxaFRpSDQxDlRzSn9aWgu9Pi7lD8yFxTso4WnQ39+WZ69oB/qOvqp+isJIIWA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      },
+      "peerDependencies": {
+        "chai": "*"
+      }
+    },
     "node_modules/chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
diff --git a/common/package.json b/common/package.json
index 9ef203c..6940d87 100644
--- a/common/package.json
+++ b/common/package.json
@@ -24,9 +24,11 @@
   },
   "devDependencies": {
     "@types/chai": "^5.0.0",
+    "@types/chai-spies": "^1.0.6",
     "@types/mocha": "^10.0.8",
     "chai": "^5.1.1",
     "chai-as-promised": "^8.0.0",
+    "chai-spies": "^1.1.0",
     "esm": "^3.2.25",
     "mocha": "^10.7.3"
   }
diff --git a/common/src/events.mjs b/common/src/events.mjs
index dbdd00f..702fc7d 100644
--- a/common/src/events.mjs
+++ b/common/src/events.mjs
@@ -79,7 +79,8 @@ export class BaseEventEmitter {
         if(eventType.includes(" ")) { // Unlisten to several different events with the same listener.
             let found = false
             for(const type of eventType.split(" "))
-                found ||= this.off(eventType, eventListener)
+                found ||= this.off(type, eventListener)
+            return found
         } else {
             if(!this.constructor.emits.includes(eventType)) {
                 const className = this.constructor.name
diff --git a/common/test/general/events.mjs b/common/test/general/events.mjs
new file mode 100644
index 0000000..f82875f
--- /dev/null
+++ b/common/test/general/events.mjs
@@ -0,0 +1,122 @@
+/**
+ *  LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ *  Copyright (C) 2021-2024  Ad5001
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import { BaseEventEmitter, BaseEvent } from "../../src/events.mjs"
+
+import { describe, it } from "mocha"
+import { expect, use } from "chai"
+import spies from "chai-spies"
+
+// Setting up modules
+const { spy } = use(spies)
+
+
+class MockEmitter extends BaseEventEmitter {
+    static emits = ["example1", "example2"]
+}
+
+class MockEvent1 extends BaseEvent {
+    constructor() {
+        super("example1")
+    }
+}
+
+class MockEvent2 extends BaseEvent {
+    constructor(parameter) {
+        super("example2")
+        this.parameter = parameter
+    }
+}
+
+const sandbox = spy.sandbox()
+
+describe("Events", function() {
+
+    afterEach(() => {
+        sandbox.restore()
+    })
+
+    it("should forward events to all of its listeners", function() {
+        const emitter = new MockEmitter()
+        const listener1 = spy()
+        const listener2 = spy()
+        emitter.on("example1", listener1)
+        emitter.on("example1", listener2)
+        emitter.emit(new MockEvent1())
+        expect(listener1).to.have.been.called.once
+        expect(listener2).to.have.been.called.once
+    })
+
+    it("should forward multiple events to a singular listener", function() {
+        const emitter = new MockEmitter()
+        const listener = spy()
+        const mockEvent1 = new MockEvent1()
+        const mockEvent2 = new MockEvent2(3)
+        emitter.on("example1 example2", listener)
+        emitter.emit(mockEvent1)
+        emitter.emit(mockEvent2)
+        expect(listener).to.have.been.called.twice
+        expect(listener).to.have.been.called.with.exactly(mockEvent1)
+        expect(listener).to.have.been.called.with.exactly(mockEvent2)
+    })
+
+    it("should be able to remove listeners", function() {
+        const emitter = new MockEmitter()
+        const listener = spy()
+        emitter.on("example1", listener)
+        const removedFromEventItDoesntListenTo = emitter.off("example2", listener)
+        const removedFromEventItListensTo = emitter.off("example1", listener)
+        const removedFromEventASecondTime = emitter.off("example1", listener)
+        expect(removedFromEventItDoesntListenTo).to.be.false
+        expect(removedFromEventItListensTo).to.be.true
+        expect(removedFromEventASecondTime).to.be.false
+        emitter.on("example1 example2", listener)
+        const removedFromBothEvents = emitter.off("example1 example2", listener)
+        expect(removedFromBothEvents).to.be.true
+        emitter.on("example1", listener)
+        const removedFromOneOfTheEvents = emitter.off("example1 example2", listener)
+        expect(removedFromOneOfTheEvents).to.be.true
+    })
+
+    it("should be able to remove listening to one event when listener listens to multiple", function() {
+        const emitter = new MockEmitter()
+        const listener = spy()
+        const mockEvent1 = new MockEvent1()
+        const mockEvent2 = new MockEvent2(3)
+        emitter.on("example1 example2", listener)
+        // Disable listener for example1
+        emitter.off("example1", listener)
+        emitter.emit(mockEvent1)
+        emitter.emit(mockEvent2)
+        expect(listener).to.have.been.called.once
+        expect(listener).to.have.been.called.with.exactly(mockEvent2)
+    })
+
+    it("shouldn't be able to listen/unlisten/emit inexistant events", function() {
+        const emitter = new MockEmitter()
+        const listener = spy()
+        expect(() => emitter.on("inexistant", listener)).to.throw(Error)
+        expect(() => emitter.off("inexistant", listener)).to.throw(Error)
+        expect(() => emitter.emit(new BaseEvent("inexistant"))).to.throw(Error)
+    })
+
+    it("shouldn't be able to emit non-events", function() {
+        const emitter = new MockEmitter()
+        expect(() => emitter.emit("not-an-event")).to.throw(Error)
+    })
+})
\ No newline at end of file
diff --git a/common/test/hooks.mjs b/common/test/hooks.mjs
index e48525e..0f3ebda 100644
--- a/common/test/hooks.mjs
+++ b/common/test/hooks.mjs
@@ -19,12 +19,10 @@ import * as fs from "./mock/fs.mjs";
 import Qt from "./mock/qt.mjs";
 import { MockHelper } from "./mock/helper.mjs";
 import { MockLatex } from "./mock/latex.mjs";
-import Modules from "../src/module/index.mjs";
 
 function setup() {
     globalThis.Helper = new MockHelper()
     globalThis.Latex = new MockLatex()
-    Modules.Latex.initialize({ latex: Latex, helper: Helper })
 }
 
 setup()
diff --git a/common/test/math/domain.mjs b/common/test/math/domain.mjs
index c00384f..e32dbd1 100644
--- a/common/test/math/domain.mjs
+++ b/common/test/math/domain.mjs
@@ -19,46 +19,46 @@
 import { describe, it } from "mocha"
 import { expect } from "chai"
 
-import { Domain, parseDomainSimple } from "../../src/math/domain.mjs"
-
-describe("math.domain", function() {
-    describe("#parseDomainSimple", function() {
-        it("returns predefined domains", function() {
-            const predefinedToCheck = [
-                // Real domains
-                { domain: Domain.R, shortcuts: ["R", "ℝ"] },
-                // Zero exclusive real domains
-                { domain: Domain.RE, shortcuts: ["RE", "R*", "ℝ*"] },
-                // Real positive domains
-                { domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "ℝ+"] },
-                // Zero-exclusive real positive domains
-                { domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "ℝ*⁺", "ℝ⁺*", "ℝ*+", "ℝ+*"] },
-                // Real negative domain
-                { domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "ℝ-"] },
-                // Zero-exclusive real negative domains
-                { domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "ℝ*⁻", "ℝ-*", "ℝ*-"] },
-                // Natural integers domain
-                { domain: Domain.N, shortcuts: ["ℕ", "N", "ZP", "Z+", "ℤ⁺", "ℤ+"] },
-                // Zero-exclusive natural integers domain
-                { domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "ℕ*", "ℕ⁺", "ℕ+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "ℤ*⁺", "ℤ+*", "ℤ*+"] },
-                // Logarithmic natural domains
-                { domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "ℕLOG"] },
-                // All integers domains
-                { domain: Domain.Z, shortcuts: ["Z", "ℤ"] },
-                // Zero-exclusive all integers domain
-                { domain: Domain.ZE, shortcuts: ["ZE", "Z*", "ℤ*"] },
-                // Negative integers domain
-                { domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "ℤ-"] },
-                // Zero-exclusive negative integers domain
-                { domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "ℤ*⁻", "ℤ-*", "ℤ*-"] },
-            ]
-
-            // Real domains
-            for(const { domain, shortcuts } of predefinedToCheck)
-                for(const shortcut of shortcuts)
-                    expect(parseDomainSimple(shortcut)).to.be.equal(domain)
-        })
-
-        it("")
-    })
-})
+// import { Domain, parseDomainSimple } from "../../src/math/domain.mjs"
+//
+// describe("math.domain", function() {
+//     describe("#parseDomainSimple", function() {
+//         it("returns predefined domains", function() {
+//             const predefinedToCheck = [
+//                 // Real domains
+//                 { domain: Domain.R, shortcuts: ["R", "ℝ"] },
+//                 // Zero exclusive real domains
+//                 { domain: Domain.RE, shortcuts: ["RE", "R*", "ℝ*"] },
+//                 // Real positive domains
+//                 { domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "ℝ+"] },
+//                 // Zero-exclusive real positive domains
+//                 { domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "ℝ*⁺", "ℝ⁺*", "ℝ*+", "ℝ+*"] },
+//                 // Real negative domain
+//                 { domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "ℝ-"] },
+//                 // Zero-exclusive real negative domains
+//                 { domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "ℝ*⁻", "ℝ-*", "ℝ*-"] },
+//                 // Natural integers domain
+//                 { domain: Domain.N, shortcuts: ["ℕ", "N", "ZP", "Z+", "ℤ⁺", "ℤ+"] },
+//                 // Zero-exclusive natural integers domain
+//                 { domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "ℕ*", "ℕ⁺", "ℕ+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "ℤ*⁺", "ℤ+*", "ℤ*+"] },
+//                 // Logarithmic natural domains
+//                 { domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "ℕLOG"] },
+//                 // All integers domains
+//                 { domain: Domain.Z, shortcuts: ["Z", "ℤ"] },
+//                 // Zero-exclusive all integers domain
+//                 { domain: Domain.ZE, shortcuts: ["ZE", "Z*", "ℤ*"] },
+//                 // Negative integers domain
+//                 { domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "ℤ-"] },
+//                 // Zero-exclusive negative integers domain
+//                 { domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "ℤ*⁻", "ℤ-*", "ℤ*-"] },
+//             ]
+//
+//             // Real domains
+//             for(const { domain, shortcuts } of predefinedToCheck)
+//                 for(const shortcut of shortcuts)
+//                     expect(parseDomainSimple(shortcut)).to.be.equal(domain)
+//         })
+//
+//         it("")
+//     })
+// })

From 885d1f5dc321f31b0e655617d2f55dd659b96c9c Mon Sep 17 00:00:00 2001
From: Ad5001 <mail@ad5001.eu>
Date: Sat, 12 Oct 2024 03:22:49 +0200
Subject: [PATCH 2/2] Adding test for Utils

---
 common/src/module/latex.mjs    |   4 +-
 common/src/utils.mjs           | 405 +++++++++++++++++----------------
 common/test/general/events.mjs |  14 +-
 common/test/general/utils.mjs  | 183 +++++++++++++++
 4 files changed, 404 insertions(+), 202 deletions(-)
 create mode 100644 common/test/general/utils.mjs

diff --git a/common/src/module/latex.mjs b/common/src/module/latex.mjs
index 8ccbeb5..4ff81cb 100644
--- a/common/src/module/latex.mjs
+++ b/common/src/module/latex.mjs
@@ -137,9 +137,9 @@ class LatexAPI extends Module {
      */
     parif(elem, contents) {
         elem = elem.toString()
-        if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0))
+        if(elem[0] !== "(" && elem.at(-1) !== ")" && contents.some(x => elem.indexOf(x) > 0))
             return this.par(elem)
-        if(elem[0] === "(" && elem[elem.length - 1] === ")")
+        if(elem[0] === "(" && elem.at(-1) === ")")
             return elem.removeEnclosure()
         return elem
     }
diff --git a/common/src/utils.mjs b/common/src/utils.mjs
index 094147d..0e451f4 100644
--- a/common/src/utils.mjs
+++ b/common/src/utils.mjs
@@ -21,14 +21,16 @@
  * Replaces latin characters with their uppercase versions.
  * @return {string}
  */
-String.prototype.toLatinUppercase = String.prototype.toLatinUppercase || function() {
+String.prototype.toLatinUppercase = function() {
     return this.replace(/[a-z]/g, function(match) {
         return match.toUpperCase()
     })
 }
 
 /**
- * Removes the 'enclosers' of a string (e.g. quotes, parentheses, brackets...)
+ * Removes the first and last character of a string
+ * Used to remove enclosing characters like quotes, parentheses, brackets...
+ * @note Does NOT check for their existence ahead of time.
  * @return {string}
  */
 String.prototype.removeEnclosure = function() {
@@ -43,126 +45,126 @@ String.prototype.removeEnclosure = function() {
  * @return {number}
  */
 Number.prototype.toDecimalPrecision = function(decimalPlaces = 0) {
-    const p = Math.pow(10, decimalPlaces);
-    const n = (this * p) * (1 + Number.EPSILON);
-    return Math.round(n) / p;
+    const p = Math.pow(10, decimalPlaces)
+    const n = (this * p) * (1 + Number.EPSILON)
+    return Math.round(n) / p
 }
 
-const powerpos = {
-    "-": "⁻",
-    "+": "⁺",
-    "=": "⁼",
-    " ": " ",
-    "(": "⁽",
-    ")": "⁾",
-    "0": "⁰",
-    "1": "¹",
-    "2": "²",
-    "3": "³",
-    "4": "⁴",
-    "5": "⁵",
-    "6": "⁶",
-    "7": "⁷",
-    "8": "⁸",
-    "9": "⁹",
-    "a": "ᵃ",
-    "b": "ᵇ",
-    "c": "ᶜ",
-    "d": "ᵈ",
-    "e": "ᵉ",
-    "f": "ᶠ",
-    "g": "ᵍ",
-    "h": "ʰ",
-    "i": "ⁱ",
-    "j": "ʲ",
-    "k": "ᵏ",
-    "l": "ˡ",
-    "m": "ᵐ",
-    "n": "ⁿ",
-    "o": "ᵒ",
-    "p": "ᵖ",
-    "r": "ʳ",
-    "s": "ˢ",
-    "t": "ᵗ",
-    "u": "ᵘ",
-    "v": "ᵛ",
-    "w": "ʷ",
-    "x": "ˣ",
-    "y": "ʸ",
-    "z": "ᶻ"
-}
+const CHARACTER_TO_POWER = new Map([
+    ["-", "⁻"],
+    ["+", "⁺"],
+    ["=", "⁼"],
+    [" ", " "],
+    ["(", "⁽"],
+    [")", "⁾"],
+    ["0", "⁰"],
+    ["1", "¹"],
+    ["2", "²"],
+    ["3", "³"],
+    ["4", "⁴"],
+    ["5", "⁵"],
+    ["6", "⁶"],
+    ["7", "⁷"],
+    ["8", "⁸"],
+    ["9", "⁹"],
+    ["a", "ᵃ"],
+    ["b", "ᵇ"],
+    ["c", "ᶜ"],
+    ["d", "ᵈ"],
+    ["e", "ᵉ"],
+    ["f", "ᶠ"],
+    ["g", "ᵍ"],
+    ["h", "ʰ"],
+    ["i", "ⁱ"],
+    ["j", "ʲ"],
+    ["k", "ᵏ"],
+    ["l", "ˡ"],
+    ["m", "ᵐ"],
+    ["n", "ⁿ"],
+    ["o", "ᵒ"],
+    ["p", "ᵖ"],
+    ["r", "ʳ"],
+    ["s", "ˢ"],
+    ["t", "ᵗ"],
+    ["u", "ᵘ"],
+    ["v", "ᵛ"],
+    ["w", "ʷ"],
+    ["x", "ˣ"],
+    ["y", "ʸ"],
+    ["z", "ᶻ"]
+])
 
-const exponents = [
-    "⁰","¹","²","³","⁴","⁵","⁶","⁷","⁸","⁹"
+const CHARACTER_TO_INDICE = new Map([
+    ["-", "₋"],
+    ["+", "₊"],
+    ["=", "₌"],
+    ["(", "₍"],
+    [")", "₎"],
+    [" ", " "],
+    ["0", "₀"],
+    ["1", "₁"],
+    ["2", "₂"],
+    ["3", "₃"],
+    ["4", "₄"],
+    ["5", "₅"],
+    ["6", "₆"],
+    ["7", "₇"],
+    ["8", "₈"],
+    ["9", "₉"],
+    ["a", "ₐ"],
+    ["e", "ₑ"],
+    ["h", "ₕ"],
+    ["i", "ᵢ"],
+    ["j", "ⱼ"],
+    ["k", "ₖ"],
+    ["l", "ₗ"],
+    ["m", "ₘ"],
+    ["n", "ₙ"],
+    ["o", "ₒ"],
+    ["p", "ₚ"],
+    ["r", "ᵣ"],
+    ["s", "ₛ"],
+    ["t", "ₜ"],
+    ["u", "ᵤ"],
+    ["v", "ᵥ"],
+    ["x", "ₓ"]
+])
+
+const EXPONENTS = [
+    "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"
 ]
 
-const exponentReg = new RegExp('(['+exponents.join('')+']+)', 'g')
+const EXPONENTS_REG = new RegExp("([" + EXPONENTS.join("") + "]+)", "g")
 
-const indicepos = {
-    "-": "₋",
-    "+": "₊",
-    "=": "₌",
-    "(": "₍",
-    ")": "₎",
-    " ": " ",
-    "0": "₀",
-    "1": "₁",
-    "2": "₂",
-    "3": "₃",
-    "4": "₄",
-    "5": "₅",
-    "6": "₆",
-    "7": "₇",
-    "8": "₈",
-    "9": "₉",
-    "a": "ₐ",
-    "e": "ₑ",
-    "h": "ₕ",
-    "i": "ᵢ",
-    "j": "ⱼ",
-    "k": "ₖ",
-    "l": "ₗ",
-    "m": "ₘ",
-    "n": "ₙ",
-    "o": "ₒ",
-    "p": "ₚ",
-    "r": "ᵣ",
-    "s": "ₛ",
-    "t": "ₜ",
-    "u": "ᵤ",
-    "v": "ᵥ",
-    "x": "ₓ",
-}
-// Put a text in sup position
+/**
+ * Put a text in sup position
+ * @param {string} text
+ * @return {string}
+ */
 export function textsup(text) {
     let ret = ""
     text = text.toString()
-    for (let i = 0; i < text.length; i++) {
-        if(Object.keys(powerpos).indexOf(text[i]) >= 0) {
-            ret += powerpos[text[i]]
-        } else {
-            ret += text[i]
-        }
-    }
+    for(let letter of text)
+        ret += CHARACTER_TO_POWER.has(letter) ? CHARACTER_TO_POWER.get(letter) : letter
     return ret
 }
 
-// Put a text in sub position
+/**
+ * Put a text in sub position
+ * @param {string} text
+ * @return {string}
+ */
 export function textsub(text) {
     let ret = ""
     text = text.toString()
-    for (let i = 0; i < text.length; i++) {
-        if(Object.keys(indicepos).indexOf(text[i]) >= 0) {
-            ret += indicepos[text[i]]
-        } else {
-            ret += text[i]
-        }
-    }
+    for(let letter of text)
+        ret += CHARACTER_TO_INDICE.has(letter) ? CHARACTER_TO_INDICE.get(letter) : letter
     return ret
 }
 
 /**
  * Simplifies (mathematically) a mathematical expression.
+ * @deprecated
  * @param {string} str - Expression to parse
  * @returns {string}
  */
@@ -185,37 +187,43 @@ export function simplifyExpression(str) {
                 // n1 & n3 are multiplied, opeM is the main operation (- or +).
                 // Putting all n in form of number
                 //n2 = n2 == undefined ? 1 : parseFloat(n)
-                n1 = m1 === undefined ? 1 : eval(m1 + '1')
-                n2 = m2 === undefined ? 1 : eval('1' + m2)
-                n3 = m3 === undefined ? 1 : eval(m3 + '1')
-                n4 = m4 === undefined ? 1 : eval('1' + m4)
-                //let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
-                // Falling back to * in case it does not exist (the corresponding n would be 1)
-                [ope2, ope4] = [ope2, ope4].map(ope => ope === '/' ? '/' : '*')
-                let coeff1 = n1*n2
-                let coeff2 = n3*n4
-                let coefficient = coeff1+coeff2-(opeM === '-' ? 2*coeff2 : 0)
-                
+                n1 = m1 === undefined ? 1 : eval(m1 + "1")
+                n2 = m2 === undefined ? 1 : eval("1" + m2)
+                n3 = m3 === undefined ? 1 : eval(m3 + "1")
+                n4 = m4 === undefined ? 1 : eval("1" + m4)
+                    //let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
+                    // Falling back to * in case it does not exist (the corresponding n would be 1)
+                    [ope2, ope4] = [ope2, ope4].map(ope => ope === "/" ? "/" : "*")
+                let coeff1 = n1 * n2
+                let coeff2 = n3 * n4
+                let coefficient = coeff1 + coeff2 - (opeM === "-" ? 2 * coeff2 : 0)
+
                 return `${coefficient} * π`
             }
         ],
         [ // Removing parenthesis when content is only added from both sides.
             /(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
-            function(match, b4, middle, after) {return `${b4}${middle}${after}`}
+            function(match, b4, middle, after) {
+                return `${b4}${middle}${after}`
+            }
         ],
         [ // Removing parenthesis when content is only multiplied.
             /(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
-            function(match, b4, middle, after) {return `${b4}${middle}${after}`}
+            function(match, b4, middle, after) {
+                return `${b4}${middle}${after}`
+            }
         ],
         [ // Removing parenthesis when content is only multiplied.
             /(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
-            function(match, b4, middle, after) {return `${b4}${middle}${after}`}
+            function(match, b4, middle, after) {
+                return `${b4}${middle}${after}`
+            }
         ],
         [// Simplification additions/subtractions.
             /(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
             function(match, b4, n1, op1, middle, op2, n2, after) {
                 let total
-                if(op2 === '+') {
+                if(op2 === "+") {
                     total = parseFloat(n1) + parseFloat(n2)
                 } else {
                     total = parseFloat(n1) - parseFloat(n2)
@@ -224,14 +232,14 @@ export function simplifyExpression(str) {
             }
         ],
         [// Simplification multiplications/divisions.
-            /([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g, 
+            /([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
             function(match, n1, op1, middle, op2, n2) {
-                if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === '/' &&
-                (parseInt(n1) / parseInt(n2)) % 1 !== 0) {
+                if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === "/" &&
+                    (parseInt(n1) / parseInt(n2)) % 1 !== 0) {
                     // Non int result for int division.
                     return `(${n1} / ${n2}) ${op1} ${middle}`
                 } else {
-                    if(op2 === '*') {
+                    if(op2 === "*") {
                         return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
                     } else {
                         return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
@@ -245,17 +253,17 @@ export function simplifyExpression(str) {
                 let str = middle
                 // Replace all groups
                 while(/\([^)(]+\)/g.test(str))
-                    str = str.replace(/\([^)(]+\)/g, '')
+                    str = str.replace(/\([^)(]+\)/g, "")
                 // There shouldn't be any more parenthesis
                 // If there is, that means the 2 parenthesis are needed.
-                if(!str.includes(')') && !str.includes('(')) {
+                if(!str.includes(")") && !str.includes("(")) {
                     return middle
                 } else {
                     return `(${middle})`
                 }
-                
+
             }
-        ],
+        ]
         // Simple simplifications
         // [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
         // [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
@@ -268,7 +276,7 @@ export function simplifyExpression(str) {
         // [/(^| |\() /g, '$1'],
         // [/ ($|\))/g, '$1'],
     ]
-    
+
     // Replacements
     let found
     do {
@@ -286,24 +294,35 @@ export function simplifyExpression(str) {
 /**
  * Transforms a mathematical expression to make it readable by humans.
  * NOTE: Will break parsing of expression.
+ * @deprecated
  * @param {string} str - Expression to parse.
  * @returns {string}
  */
 export function makeExpressionReadable(str) {
     let replacements = [
         // letiables
-        [/pi/g, 'π'],
-        [/Infinity/g, '∞'],
-        [/inf/g, '∞'],
+        [/pi/g, "π"],
+        [/Infinity/g, "∞"],
+        [/inf/g, "∞"],
         // Other
-        [/ \* /g, '×'],
-        [/ \^ /g, '^'],
-        [/\^\(([\d\w+-]+)\)/g, function(match, p1) { return textsup(p1) }],
-        [/\^([\d\w+-]+)/g, function(match, p1) { return textsup(p1) }],
-        [/_\(([\d\w+-]+)\)/g, function(match, p1) { return textsub(p1) }],
-        [/_([\d\w+-]+)/g, function(match, p1) { return textsub(p1) }],
-        [/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }],
-        [/(\d|\))×/g, '$1'],
+        [/ \* /g, "×"],
+        [/ \^ /g, "^"],
+        [/\^\(([\d\w+-]+)\)/g, function(match, p1) {
+            return textsup(p1)
+        }],
+        [/\^([\d\w+-]+)/g, function(match, p1) {
+            return textsup(p1)
+        }],
+        [/_\(([\d\w+-]+)\)/g, function(match, p1) {
+            return textsub(p1)
+        }],
+        [/_([\d\w+-]+)/g, function(match, p1) {
+            return textsub(p1)
+        }],
+        [/\[([^\[\]]+)\]/g, function(match, p1) {
+            return textsub(p1)
+        }],
+        [/(\d|\))×/g, "$1"],
         [/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
             if(a.length < b.length) {
                 return `∫${textsub(a)}${textsup(b)} ${body} d${by}`
@@ -312,10 +331,10 @@ export function makeExpressionReadable(str) {
             }
         }],
         [/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
-            return `d(${body.replace(new RegExp(by, 'g'), 'x')})/dx`
+            return `d(${body.replace(new RegExp(by, "g"), "x")})/dx`
         }]
     ]
-    
+
     // str = simplifyExpression(str)
     // Replacements
     for(let replacement of replacements)
@@ -324,6 +343,48 @@ export function makeExpressionReadable(str) {
     return str
 }
 
+/** @type {[RegExp, string][]} */
+const replacements = [
+    // Greek letters
+    [/(\W|^)al(pha)?(\W|$)/g, "$1α$3"],
+    [/(\W|^)be(ta)?(\W|$)/g, "$1β$3"],
+    [/(\W|^)ga(mma)?(\W|$)/g, "$1γ$3"],
+    [/(\W|^)de(lta)?(\W|$)/g, "$1δ$3"],
+    [/(\W|^)ep(silon)?(\W|$)/g, "$1ε$3"],
+    [/(\W|^)ze(ta)?(\W|$)/g, "$1ζ$3"],
+    [/(\W|^)et(a)?(\W|$)/g, "$1η$3"],
+    [/(\W|^)th(eta)?(\W|$)/g, "$1θ$3"],
+    [/(\W|^)io(ta)?(\W|$)/g, "$1ι$3"],
+    [/(\W|^)ka(ppa)?(\W|$)/g, "$1κ$3"],
+    [/(\W|^)la(mbda)?(\W|$)/g, "$1λ$3"],
+    [/(\W|^)mu(\W|$)/g, "$1μ$2"],
+    [/(\W|^)nu(\W|$)/g, "$1ν$2"],
+    [/(\W|^)xi(\W|$)/g, "$1ξ$2"],
+    [/(\W|^)rh(o)?(\W|$)/g, "$1ρ$3"],
+    [/(\W|^)si(gma)?(\W|$)/g, "$1σ$3"],
+    [/(\W|^)ta(u)?(\W|$)/g, "$1τ$3"],
+    [/(\W|^)up(silon)?(\W|$)/g, "$1υ$3"],
+    [/(\W|^)ph(i)?(\W|$)/g, "$1φ$3"],
+    [/(\W|^)ch(i)?(\W|$)/g, "$1χ$3"],
+    [/(\W|^)ps(i)?(\W|$)/g, "$1ψ$3"],
+    [/(\W|^)om(ega)?(\W|$)/g, "$1ω$3"],
+    // Capital greek letters
+    [/(\W|^)gga(mma)?(\W|$)/g, "$1Γ$3"],
+    [/(\W|^)gde(lta)?(\W|$)/g, "$1Δ$3"],
+    [/(\W|^)gth(eta)?(\W|$)/g, "$1Θ$3"],
+    [/(\W|^)gla(mbda)?(\W|$)/g, "$1Λ$3"],
+    [/(\W|^)gxi(\W|$)/g, "$1Ξ$2"],
+    [/(\W|^)gpi(\W|$)/g, "$1Π$2"],
+    [/(\W|^)gsi(gma)?(\W|$)/g, "$1Σ$3"],
+    [/(\W|^)gph(i)?(\W|$)/g, "$1Φ$3"],
+    [/(\W|^)gps(i)?(\W|$)/g, "$1Ψ$3"],
+    [/(\W|^)gom(ega)?(\W|$)/g, "$1Ω$3"],
+    // Array elements
+    [/\[([^\]\[]+)\]/g, function(match, p1) {
+        return textsub(p1)
+    }]
+]
+
 /**
  * Parses a variable name to make it readable by humans.
  *
@@ -332,65 +393,24 @@ export function makeExpressionReadable(str) {
  * @returns {string} - The parsed name
  */
 export function parseName(str, removeUnallowed = true) {
-    let replacements = [
-        // Greek letters
-        [/([^a-z]|^)al(pha)?([^a-z]|$)/g, '$1α$3'],
-        [/([^a-z]|^)be(ta)?([^a-z]|$)/g, '$1β$3'],
-        [/([^a-z]|^)ga(mma)?([^a-z]|$)/g, '$1γ$3'],
-        [/([^a-z]|^)de(lta)?([^a-z]|$)/g, '$1δ$3'],
-        [/([^a-z]|^)ep(silon)?([^a-z]|$)/g, '$1ε$3'],
-        [/([^a-z]|^)ze(ta)?([^a-z]|$)/g, '$1ζ$3'],
-        [/([^a-z]|^)et(a)?([^a-z]|$)/g, '$1η$3'],
-        [/([^a-z]|^)th(eta)?([^a-z]|$)/g, '$1θ$3'],
-        [/([^a-z]|^)io(ta)?([^a-z]|$)/g, '$1ι$3'],
-        [/([^a-z]|^)ka(ppa)([^a-z]|$)?/g, '$1κ$3'],
-        [/([^a-z]|^)la(mbda)?([^a-z]|$)/g, '$1λ$3'],
-        [/([^a-z]|^)mu([^a-z]|$)/g, '$1μ$2'],
-        [/([^a-z]|^)nu([^a-z]|$)/g, '$1ν$2'],
-        [/([^a-z]|^)xi([^a-z]|$)/g, '$1ξ$2'],
-        [/([^a-z]|^)rh(o)?([^a-z]|$)/g, '$1ρ$3'],
-        [/([^a-z]|^)si(gma)?([^a-z]|$)/g, '$1σ$3'],
-        [/([^a-z]|^)ta(u)?([^a-z]|$)/g, '$1τ$3'],
-        [/([^a-z]|^)up(silon)?([^a-z]|$)/g, '$1υ$3'],
-        [/([^a-z]|^)ph(i)?([^a-z]|$)/g, '$1φ$3'],
-        [/([^a-z]|^)ch(i)?([^a-z]|$)/g, '$1χ$3'],
-        [/([^a-z]|^)ps(i)?([^a-z]|$)/g, '$1ψ$3'],
-        [/([^a-z]|^)om(ega)?([^a-z]|$)/g, '$1ω$3'],
-        // Capital greek letters
-        [/([^a-z]|^)gga(mma)?([^a-z]|$)/g, '$1Γ$3'],
-        [/([^a-z]|^)gde(lta)?([^a-z]|$)/g, '$1Δ$3'],
-        [/([^a-z]|^)gth(eta)?([^a-z]|$)/g, '$1Θ$3'],
-        [/([^a-z]|^)gla(mbda)?([^a-z]|$)/g, '$1Λ$3'],
-        [/([^a-z]|^)gxi([^a-z]|$)/g, '$1Ξ$2'],
-        [/([^a-z]|^)gpi([^a-z]|$)/g, '$1Π$2'],
-        [/([^a-z]|^)gsi(gma)([^a-z]|$)?/g, '$1Σ$3'],
-        [/([^a-z]|^)gph(i)?([^a-z]|$)/g, '$1Φ$3'],
-        [/([^a-z]|^)gps(i)?([^a-z]|$)/g, '$1Ψ$3'],
-        [/([^a-z]|^)gom(ega)?([^a-z]|$)/g, '$1Ω$3'],
-        // Underscores
-        // [/_\(([^_]+)\)/g, function(match, p1) { return textsub(p1) }],
-        // [/_([^" ]+)/g, function(match, p1) { return textsub(p1) }],
-        // Array elements
-        [/\[([^\]\[]+)\]/g, function(match, p1) { return textsub(p1) }],
-        // Removing
-        [/[xπℝℕ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
-    ]
-    if(!removeUnallowed) replacements.pop()
-    // Replacements
-    for(let replacement of replacements)
+    for(const replacement of replacements)
         str = str.replace(replacement[0], replacement[1])
+    if(removeUnallowed)
+        str = str.replace(/[xnπℝℕ\\∪∩\]\[ ()^/÷*×+=\d¹²³⁴⁵⁶⁷⁸⁹⁰-]/g, "")
+
     return str
 }
 
 /**
  * Transforms camel case strings to a space separated one.
  *
+ * @deprecated
  * @param {string} label - Camel case to parse
  * @returns {string} Parsed label.
  */
 export function camelCase2readable(label) {
     let parsed = parseName(label, false)
-    return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g," $1")
+    return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g, " $1")
 }
 
 /**
@@ -398,12 +418,12 @@ export function camelCase2readable(label) {
  * @returns {string}
  */
 export function getRandomColor() {
-    let clrs = '0123456789ABCDEF';
-    let color = '#';
+    let clrs = "0123456789ABCDEF"
+    let color = "#"
     for(let i = 0; i < 6; i++) {
-        color += clrs[Math.floor(Math.random() * (16-5*(i%2===0)))];
+        color += clrs[Math.floor(Math.random() * (16 - 5 * (i % 2 === 0)))]
     }
-    return color;
+    return color
 }
 
 /**
@@ -412,16 +432,15 @@ export function getRandomColor() {
  * @returns {string}
  */
 export function escapeHTML(str) {
-    return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
+    return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
 }
 
 
-
 /**
  * Parses exponents and replaces them with expression values
  * @param {string} expression - The expression to replace in.
  * @return {string} The parsed expression
  */
 export function exponentsToExpression(expression) {
-    return expression.replace(exponentReg, (m, exp) => '^' + exp.split('').map((x) => exponents.indexOf(x)).join(''))
+    return expression.replace(EXPONENTS_REG, (m, exp) => "^" + exp.split("").map((x) => EXPONENTS.indexOf(x)).join(""))
 }
diff --git a/common/test/general/events.mjs b/common/test/general/events.mjs
index f82875f..15d44aa 100644
--- a/common/test/general/events.mjs
+++ b/common/test/general/events.mjs
@@ -45,7 +45,7 @@ class MockEvent2 extends BaseEvent {
 
 const sandbox = spy.sandbox()
 
-describe("Events", function() {
+describe("EventsEmitters", function() {
 
     afterEach(() => {
         sandbox.restore()
@@ -71,11 +71,11 @@ describe("Events", function() {
         emitter.emit(mockEvent1)
         emitter.emit(mockEvent2)
         expect(listener).to.have.been.called.twice
-        expect(listener).to.have.been.called.with.exactly(mockEvent1)
-        expect(listener).to.have.been.called.with.exactly(mockEvent2)
+        expect(listener).to.also.have.been.called.with.exactly(mockEvent1)
+        expect(listener).to.also.have.been.called.with.exactly(mockEvent2)
     })
 
-    it("should be able to remove listeners", function() {
+    it("should be able to have listeners remvoed", function() {
         const emitter = new MockEmitter()
         const listener = spy()
         emitter.on("example1", listener)
@@ -93,7 +93,7 @@ describe("Events", function() {
         expect(removedFromOneOfTheEvents).to.be.true
     })
 
-    it("should be able to remove listening to one event when listener listens to multiple", function() {
+    it("should be able to remove one listener listening to one event when said listener listens to multiple", function() {
         const emitter = new MockEmitter()
         const listener = spy()
         const mockEvent1 = new MockEvent1()
@@ -104,10 +104,10 @@ describe("Events", function() {
         emitter.emit(mockEvent1)
         emitter.emit(mockEvent2)
         expect(listener).to.have.been.called.once
-        expect(listener).to.have.been.called.with.exactly(mockEvent2)
+        expect(listener).to.also.have.been.called.with.exactly(mockEvent2)
     })
 
-    it("shouldn't be able to listen/unlisten/emit inexistant events", function() {
+    it("shouldn't be able to add listen/unlisten to, or emit inexistant events", function() {
         const emitter = new MockEmitter()
         const listener = spy()
         expect(() => emitter.on("inexistant", listener)).to.throw(Error)
diff --git a/common/test/general/utils.mjs b/common/test/general/utils.mjs
new file mode 100644
index 0000000..8078645
--- /dev/null
+++ b/common/test/general/utils.mjs
@@ -0,0 +1,183 @@
+/**
+ *  LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ *  Copyright (C) 2021-2024  Ad5001
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import { textsup, textsub, parseName, getRandomColor, escapeHTML, exponentsToExpression } from "../../src/utils.mjs"
+
+
+import { describe, it } from "mocha"
+import { expect } from "chai"
+
+describe("Extensions", function() {
+    describe("#String.toLatinUppercase", function() {
+        it("should be able to transform latin characters from strings to their uppercase version", function() {
+            expect("abc".toLatinUppercase()).to.equal("ABC")
+            expect("abCd".toLatinUppercase()).to.equal("ABCD")
+            expect("ab123cd456".toLatinUppercase()).to.equal("AB123CD456")
+            expect("ABC".toLatinUppercase()).to.equal("ABC")
+        })
+
+        it("shouldn't transform non latin characters to their uppercase version", function() {
+            expect("abαπ".toLatinUppercase()).to.equal("ABαπ")
+            expect("abαπ".toLatinUppercase()).to.not.equal("abαπ".toUpperCase())
+        })
+    })
+
+    describe("#String.removeEnclosure", function() {
+        it("should be able to remove the first and last characters", function() {
+            expect("[1+t]".removeEnclosure()).to.equal("1+t")
+            expect('"a+b+c*d"'.removeEnclosure()).to.equal("a+b+c*d")
+            expect("(pi/2)".removeEnclosure()).to.equal("pi/2")
+        })
+    })
+
+    describe("#Number.toDecimalPrecision", function() {
+        it("should be able to round a number to a fixed decimal precision", function() {
+            expect(123.456789.toDecimalPrecision()).to.equal(123)
+            expect(123.456789.toDecimalPrecision(1)).to.equal(123.5)
+            expect(123.456789.toDecimalPrecision(2)).to.equal(123.46)
+            expect(123.456789.toDecimalPrecision(3)).to.equal(123.457)
+            expect(123.456789.toDecimalPrecision(4)).to.equal(123.4568)
+            expect(123.456789.toDecimalPrecision(5)).to.equal(123.45679)
+            expect(123.456789.toDecimalPrecision(6)).to.equal(123.456789)
+            expect(123.111111.toDecimalPrecision(5)).to.equal(123.11111)
+        })
+    })
+})
+
+describe("Utils", function() {
+    describe("#textsup", function() {
+        it("should transform characters which have a sup unicode equivalent", function()  {
+            expect(textsup("-+=()")).to.equal("⁻⁺⁼⁽⁾")
+            expect(textsup("0123456789")).to.equal("⁰¹²³⁴⁵⁶⁷⁸⁹")
+            expect(textsup("abcdefghijklmnoprstuvwxyz")).to.equal("ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ")
+        })
+
+        it("shouldn't transform characters without a sup equivalent", function() {
+            expect(textsup("ABCDEFGHIJKLMNOPQRSTUVWXYZq")).to.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZq")
+        })
+
+        it("should partially transform strings which only have a few characters with a sup equivalent", function() {
+            expect(textsup("ABCabcABC")).to.equal("ABCᵃᵇᶜABC")
+        })
+    })
+
+    describe("#textsub", function() {
+        it("should transform characters which have a sub unicode equivalent", function()  {
+            expect(textsub("-+=()")).to.equal("₋₊₌₍₎")
+            expect(textsub("0123456789")).to.equal("₀₁₂₃₄₅₆₇₈₉")
+            expect(textsub("aehijklmnoprstuvx")).to.equal("ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ")
+        })
+
+        it("shouldn't transform characters without a sub equivalent", function() {
+            expect(textsub("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).to.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+            expect(textsub("bcdfgqyz")).to.equal("bcdfgqyz")
+        })
+
+        it("should partially transform strings which only have a few characters with a sub equivalent", function() {
+            expect(textsub("ABC123ABC")).to.equal("ABC₁₂₃ABC")
+        })
+    })
+
+    describe("#parseName", function() {
+        it("should parse greek letter names", function() {
+            const shorthands = {
+                "α": ["al", "alpha"],
+                "β": ["be", "beta"],
+                "γ": ["ga", "gamma"],
+                "δ": ["de", "delta"],
+                "ε": ["ep", "epsilon"],
+                "ζ": ["ze", "zeta"],
+                "η": ["et", "eta"],
+                "θ": ["th", "theta"],
+                "κ": ["ka", "kappa"],
+                "λ": ["la", "lambda"],
+                "μ": ["mu"],
+                "ν": ["nu"],
+                "ξ": ["xi"],
+                "ρ": ["rh", "rho"],
+                "σ": ["si", "sigma"],
+                "τ": ["ta", "tau"],
+                "υ": ["up", "upsilon"],
+                "φ": ["ph", "phi"],
+                "χ": ["ch", "chi"],
+                "ψ": ["ps", "psi"],
+                "ω": ["om", "omega"],
+                "Γ": ["gga", "ggamma"],
+                "Δ": ["gde", "gdelta"],
+                "Θ": ["gth", "gtheta"],
+                "Λ": ["gla", "glambda"],
+                "Ξ": ["gxi"],
+                "Π": ["gpi"],
+                "Σ": ["gsi", "gsigma"],
+                "Φ": ["gph", "gphi"],
+                "Ψ": ["gps", "gpsi"],
+                "Ω": ["gom", "gomega"],
+            }
+            for(const [char, shorts] of Object.entries(shorthands)) {
+                expect(parseName(char)).to.equal(char)
+                for(const short of shorts)
+                    expect(parseName(short)).to.equal(char)
+            }
+        })
+
+        it("should parse array elements into sub", function() {
+            expect(parseName("u[n+1]")).to.equal("uₙ₊₁")
+            expect(parseName("u[(n+x)]")).to.equal("u₍ₙ₊ₓ₎")
+            expect(parseName("u[A]")).to.equal("uA")
+        })
+
+        it("should remove disallowed characters when indicated", function() {
+            const disallowed = "xnπℝℕ\\∪∩[] ()^/^/÷*×+=1234567890¹²³⁴⁵⁶⁷⁸⁹⁰-"
+            expect(parseName(disallowed)).to.equal("")
+            expect(parseName("AA" + disallowed)).to.equal("AA")
+            expect(parseName(disallowed, false)).to.equal(disallowed)
+        })
+
+        it("should be able to do all three at once", function() {
+            expect(parseName("al[n+1]+n")).to.equal("αₙ₊₁")
+            expect(parseName("al[n+1]+n", false)).to.equal("αₙ₊₁+n")
+        })
+    })
+
+    describe("#getRandomColor", function() {
+        it("should provide a valid color", function() {
+            const colorReg = /^#[A-F\d]{6}$/
+            for(let i = 0; i < 50; i++)
+                expect(getRandomColor()).to.match(colorReg)
+        })
+    })
+
+    describe("#escapeHTML", function() {
+        it("should should escape ampersands", function() {
+            expect(escapeHTML("&")).to.equal("&amp;")
+            expect(escapeHTML("High & Mighty")).to.equal("High &amp; Mighty")
+        })
+
+        it("should escape injected HTML tags", function() {
+            expect(escapeHTML("<script>alert('Injected!')</script>")).to.equal("&lt;script&gt;alert('Injected!')&lt;/script&gt;")
+            expect(escapeHTML('<a href="javascript:alert()">Link</a>')).to.equal('&lt;a href="javascript:alert()"&gt;Link&lt;/a&gt;')
+        })
+    })
+
+    describe("#exponentsToExpression", function() {
+        it("should transform exponents to power expression", function() {
+            expect(exponentsToExpression("x¹²³⁴⁵⁶⁷⁸⁹⁰")).to.equal("x^1234567890")
+            expect(exponentsToExpression("x¹²+y³⁴")).to.equal("x^12+y^34")
+        })
+    })
+})