// Allows browsers to perform doctests. // Uses the code highlight package from http://highlightjs.readthedocs.io // if available var docTestLogMessages = [] Array.prototype.equals = function(array) { return ( this.length == array.length && this.every(function(this_i, i) { return this_i == array[i] }) ) } export default class Doctest { static assert(value) { if (!value) { throw new Error('Assertion violated') } } static pprint(obj) { if (obj === null) return 'null' let stringified = obj.toString() if (stringified == '[object Object]') return JSON.stringify(obj) return stringified } /** * When dealing with floating point numbers, * floating point imprecision may occur. Therefore a simple * equality check is not sufficient. * * The expectPointPrecision method compares two points by calculating their difference * and accepting the result as far as it is lower than the defined acceptable error. * * @static * @param {number} a - Number a to test. * @param {number} b - Number b to test against. * @param {number} accetableError - Defines the value of how much the two numbers may differ, that the test still passes. * @memberof Doctest */ static expectPointPrecision(pointA, pointB, accetableError = 0.000001) { if (Math.abs(pointA.x - pointB.x) > accetableError || Math.abs(pointA.y - pointB.y) > accetableError) { throw new Error( `Testing difference of Points ${this.pprint(pointA)} and ${this.pprint( pointB )} exceeded the acceptable error of ${accetableError} (x:${Math.abs( pointB.x - pointA.x )}, y: ${Math.abs(pointB.y - pointA.y)}).` ) } } /** * When dealing with floating point numbers, * floating point imprecision may occur. Therefore a simple * equality check is not sufficient. * * The expectPrecise method compares two numbers by calculating their difference * and accepting the result as far as it is lower than the defined acceptable error. * * @static * @param {number} a - Number a to test. * @param {number} b - Number b to test against. * @param {number} accetableError - Defines the value of how much the two numbers may differ, that the test still passes. * @memberof Doctest */ static expectPrecision(a, b, accetableError = 0.000001) { let aFloat = parseFloat(a) let bFloat = parseFloat(b) console.log(aFloat) if (isNaN(aFloat) || isNaN(bFloat) || Math.abs(bFloat - aFloat) > accetableError) { throw new Error( `Testing difference of ${this.pprint(a)} and ${this.pprint( b )} exceeded the acceptable error of ${accetableError} (${Math.abs(bFloat - aFloat)}).` ) } } static expect(expr, value) { if (this.pprint(expr) != this.pprint(value)) { //throw new Error("got `" + expr + "` but expected `" + value + "`.") throw new Error('got `' + this.pprint(expr) + '` but expected `' + this.pprint(value) + '`.') } } static expectError(error, message) { let index = error.toString().indexOf(message) if (index < 0) { throw new Error('got `' + message + '` but expected `' + error + '`.') } } static expectLog(...messages) { // if (!docTestLogMessages.equals(messages)) { docTestLogMessages.forEach((msg, i) => { if (msg != messages[i]) throw new Error('Unexpected log message: `' + messages[i] + '`.') }) // throw new Error('Uups') //} } static log(message) { docTestLogMessages.push(message) } static highlight(code) { if (typeof hljs == 'undefined') return code return hljs.highlight('javascript', code) } static stripLeadingLines(code) { let result = [] let informative = false for (let line of code.split('\n')) { if (line.trim().length > 0) { informative = true } if (informative) result.push(line) } return result.join('\n') } static event(type = 'mouse', { clientX = 0, clientY = 0 } = {}) { if (type.startsWith('mouse')) { return new MouseEvent(type, { clientX, clientY }) } return { type, clientX, clientY } } static run(replaceExpect = false) { if (typeof hljs != 'undefined') { hljs.initHighlighting() } let doctests = document.querySelectorAll('.doctest') for (let i = 0; i < doctests.length; i++) { let doctest = doctests[i] let code = this.stripLeadingLines(doctest.innerHTML) let text = this.highlight(code) let container = document.createElement('div') container.className = 'doctest-wrapper' if (doctest.hasAttribute('data-title') || doctest.hasAttribute('data-collapsible')) { let titlebar = document.createElement('div') titlebar.className = 'doctest-titlebar' titlebar.style = 'min-height: 10px;' container.appendChild(titlebar) if (doctest.hasAttribute('data-title')) { let title = document.createElement('h6') title.innerText = doctest.getAttribute('data-title') title.className = 'doctest-section-title' titlebar.appendChild(title) } if (doctest.hasAttribute('data-collapsible')) { let icon = document.createElement('i') icon.className = 'material-icons' titlebar.classList.add('doctest-collapsible-toggle') if (titlebar.childNodes.length > 0) { titlebar.insertBefore(icon, titlebar.childNodes[0]) } else { titlebar.appendChild(icon) } const collapsedClass = 'collapsed' function setToggleMode(collapse) { if (collapse) { container.classList.add(collapsedClass) icon.innerText = 'arrow_drop_down' } else { container.classList.remove(collapsedClass) icon.innerText = 'arrow_drop_up' } } function determineToggleMode() { setToggleMode(!container.classList.contains(collapsedClass)) } setToggleMode(doctest.hasAttribute('data-collapsed')) titlebar.addEventListener('click', determineToggleMode) } } let pre = document.createElement('pre') pre.className = 'hljs doctest' // See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work // let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g let lines = text.value ? text.value.split('\n') : text.split('\n') let better = [] for (let line of lines) { if (replaceExpect && line.trim().startsWith('Doctest.expect(')) { line = line.replace(/Doctest\.expect\(/, '>>> ').trim() if (line.endsWith(')') || line.endsWith(',')) { line = line.slice(0, -1) } } better.push(line) } pre.innerHTML = better.join('\n') // text.value.replace(re, ">>> $1\n$2") container.appendChild(pre) doctest.parentNode.replaceChild(container, doctest) } } } // Needed to make Doctest visible in modules //window.Doctest = Doctest