| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>Markdown Unit Tests</title>
- <link rel="icon" href="data:;base64,iVBORw0KGgo=">
- <style type="text/css">
- :root {
- font-family: sans-serif;
- --color-passed: #090;
- --color-failed: #a00;
- --color-errored: #a80;
- --color-untested: #888;
- --color-background: #fff;
- --color-text: #000;
- --color-secondary: #888;
- background-color: var(--color-background);
- color: var(--color-text);
- }
- .testclass {
- border: 1px solid var(--color-secondary);
- padding: 0.5em 1em;
- margin-bottom: 1em;
- max-width: 50em;
- }
- .testclassname {
- font-weight: bold;
- font-size: 1.25rem;
- padding-bottom: 0.25em;
- }
- .testclassstatus.passed { color: var(--color-passed); }
- .testclassstatus.failed { color: var(--color-failed); }
- .testclassstatus.errored { color: var(--color-errored); }
- .testclassstatus.untested { color: var(--color-untested); }
- .testcase {
- clear: both;
- padding: 0.2em 0;
- margin-left: 2em;
- }
- .testcase {
- border-top: 1px solid var(--color-secondary);
- }
- .testcasename {
- font-family: monospace;
- }
- .testcasestatus {
- font-weight: bold;
- font-size: 80%;
- float: left;
- }
- .testcasetiming {
- float: right;
- color: var(--color-secondary);
- font-size: 80%;
- }
- .testcasererun {
- float: right;
- padding-left: 0.5em;
- cursor: pointer;
- font-size: 150%;
- }
- .testcaseresult {
- height: 1em;
- }
- .testcasemessage {
- clear: both;
- }
- .result-untested {
- color: var(--color-untested);
- }
- .result-testing {
- color: var(--color-text);
- }
- .result-passed {
- color: var(--color-passed);
- }
- .result-failed {
- color: var(--color-failed);
- }
- .result-errored {
- color: var(--color-errored);
- }
- @media (prefers-color-scheme: dark) {
- :root {
- --color-passed: #090;
- --color-failed: #a00;
- --color-errored: #a80;
- --color-untested: #888;
- --color-background: #000;
- --color-text: #fff;
- --color-secondary: #888;
- }
- }
- </style>
- <script src="js/markdown.js"></script>
- <script src="js/spreadsheet.js"></script>
- <script src="jstest/basetest.js"></script>
- <!-- Testing infrastructure -->
- <script>
- /**
- * @param {String} text
- * @returns {String}
- */
- function escapeHTML(text) {
- return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>\n');
- }
-
- class ResultType {
- static untested = new ResultType('untested');
- static testing = new ResultType('testing');
- static passed = new ResultType('passed');
- static failed = new ResultType('failed');
- static errored = new ResultType('errored');
-
- #name;
-
- constructor(name) {
- this.#name = name;
- }
-
- toString() {
- return `${this.constructor.name}.${this.#name}`;
- }
- }
-
- class FailureError extends Error {
- constructor(message=null) {
- super(message);
- }
- }
-
- /**
- * Manages the running and results of a single test method on a test
- * class.
- */
- class TestCaseRunner {
- /** @var {number} */
- static #nextUniqueId = 1;
- /** @var {number} */
- uniqueId = 0;
- /** @var {BaseTest} */
- #objectUnderTest;
- /** @var {method} */
- #method;
- /** @var {ResultType} */
- result = ResultType.untested;
- /** @var {String|null} */
- message = null;
- expectedError = null;
- duration = null;
- /** @var {String} */
- get className() { return this.#objectUnderTest.constructor.name; }
- /** @var {String} */
- get methodName() { return this.#method.name; }
- /**
- * @param {BaseTest} objectUnderTest
- * @param {method} method
- */
- constructor(objectUnderTest, method) {
- this.#objectUnderTest = objectUnderTest;
- this.#method = method;
- this.uniqueId = TestCaseRunner.#nextUniqueId++;
- }
- run() {
- var start;
- this.expectedError = null;
- this.#objectUnderTest.currentRunner = this;
- try {
- this.#objectUnderTest.setUp();
- } catch (e) {
- console.error(`Failed to run ${this.className}.setUp() - ${e.message}`);
- this.result = ResultType.errored;
- this.message = e.message;
- return;
- }
- try {
- start = performance.now();
- this.#method.bind(this.#objectUnderTest)();
- this.duration = performance.now() - start;
- this.result = ResultType.passed;
- this.message = null;
- } catch (e) {
- this.duration = performance.now() - start;
- if (e instanceof FailureError) {
- this.result = ResultType.failed;
- this.message = e.message;
- } else if (this.#isExpectedError(e)) {
- this.result = ResultType.passed;
- this.message = null;
- } else {
- this.result = ResultType.errored;
- this.message = e.message;
- if (e.stack !== undefined) {
- this.message += "\n" + e.stack;
- }
- }
- } finally {
- this.expectedError = null;
- try {
- this.#objectUnderTest.tearDown();
- this.#objectUnderTest.currentRunner = null;
- } catch (e0) {
- console.error(`Failed to run ${this.className}.tearDown() - ${e0.message}`);
- this.result = ResultType.errored;
- this.message = e0.message;
- }
- }
- }
- get #cssId() { return `testcase${this.uniqueId}`; }
- #isExpectedError(e) {
- if (this.expectedError === null) return false;
- if (this.expectedError === true) return true;
- // TODO: Have a way to specify details about what kind of error is expected. Maybe a prototype instance and/or a testing lambda.
- return false;
- }
- toHTML() {
- var html = `<div class="testcase" id="${this.#cssId}">`;
- html += `<div class="testcasename"><span class="testcasemethod">${this.methodName}</span></div>`;
- if (this.result == ResultType.passed || this.result == ResultType.failed || this.result == ResultType.errored) {
- const rerunId = `testcasererun-${this.#cssId}`;
- const testId = this.uniqueId;
- html += `<div class="testcasererun" id="${rerunId}">🔄</div>`;
- setTimeout(() => {
- const node = document.getElementById(rerunId);
- if (!node) return;
- node.addEventListener('click', () => {
- TestClassRunner.rerun(testId);
- });
- }, 0);
- }
- html += '<div class="testcaseresult">';
- switch (this.result) {
- case ResultType.untested:
- html += '<div class="testcasestatus result-untested">Waiting to test</div>';
- break;
- case ResultType.testing:
- html += '<div class="testcasestatus result-testing">Testing...</div>';
- break;
- case ResultType.passed:
- html += '<div class="testcasestatus result-passed">Passed</div>';
- break;
- case ResultType.failed:
- html += '<div class="testcasestatus result-failed">Failed</div>';
- break;
- case ResultType.errored:
- html += '<div class="testcasestatus result-errored">Errored</div>';
- break;
- }
- if (this.duration !== null) {
- html += `<div class="testcasetiming">${Number(this.duration / 1000.0).toFixed(3)}s</div>`;
- }
- html += '</div>';
- if (this.message !== null) {
- html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
- }
- html += `</div>`;
- return html;
- }
- /**
- * Updates the HTML node in-place with the current status, or
- * adds it if it does not exist yet.
- */
- updateHTML() {
- let existing = document.getElementById(this.#cssId);
- if (existing) {
- existing.outerHTML = this.toHTML();
- } else {
- document.getElementById('results').innerHTML += this.toHTML();
- }
- }
-
- /**
- * @param {object} objectUnderTest
- * @returns {TestCaseRunner[]}
- */
- static findTestCases(objectUnderTest) {
- if (!(objectUnderTest instanceof BaseTest)) return [];
- var members = [];
- var obj = objectUnderTest;
- do {
- members.push(...Object.getOwnPropertyNames(obj));
- } while (obj = Object.getPrototypeOf(obj));
- return members.sort().filter((e, i, arr) => {
- if (e != arr[i + 1] && typeof objectUnderTest[e] == 'function' && e.startsWith('test')) return true;
- }).map((name) => {
- return new TestCaseRunner(objectUnderTest, objectUnderTest[name]);
- });
- }
- }
-
- class TestClassRunner {
- static #nextUniqueId = 1;
-
- #uniqueId = 0;
- #theClass;
- #instance;
- #testCases;
-
- get testCases() { return this.#testCases; }
-
- constructor(theClass) {
- this.#theClass = theClass;
- this.#instance = new theClass();
- this.#testCases = TestCaseRunner.findTestCases(this.#instance);
- this.#uniqueId = TestClassRunner.#nextUniqueId++;
- }
- get #cssId() { return `testclass${this.#uniqueId}`; }
- #summaryHTML() {
- var anyTesting = false;
- var anyFailed = false;
- var anyErrored = false;
- var anyUntested = false;
- var anyPassed = false;
- var allPassed = true;
- for (const test of this.testCases) {
- switch (test.result) {
- case ResultType.untested:
- anyUntested = true;
- allPassed = false;
- break;
- case ResultType.testing:
- anyTesting = true;
- allPassed = false;
- break;
- case ResultType.passed:
- anyPassed = true;
- break;
- case ResultType.failed:
- anyFailed = true;
- allPassed = false;
- break;
- case ResultType.errored:
- anyErrored = true;
- allPassed = false;
- break;
- }
- }
- var html = '';
- html += `<summary class="testclasssummary" id="${this.#cssId}summary">`;
- html += `<span class="testclassname">${this.#theClass.name}</span> `;
- if (anyTesting || (anyUntested && (anyPassed || anyFailed || anyErrored))) {
- html += '<span class="testclassstatus testing">Testing...</span>';
- } else if (anyErrored) {
- html += '<span class="testclassstatus errored">Errored</span>';
- } else if (anyFailed) {
- html += '<span class="testclassstatus failed">Failed</span>';
- } else if (allPassed) {
- html += '<span class="testclassstatus passed">Passed</span>';
- }
- html += '</summary>';
- return html;
- }
- toHTML() {
- var html = '';
- html += `<div class="testclass" id="${this.#cssId}">`;
- html += `<details id="${this.#cssId}details">`;
- html += this.#summaryHTML();
- for (const testCase of this.#testCases) {
- html += testCase.toHTML();
- }
- html += '</details>';
- html += '</div>';
- return html;
- }
- updateHTML() {
- var existing = document.getElementById(`${this.#cssId}summary`);
- if (!existing) {
- document.getElementById('results').innerHTML += this.toHTML();
- } else {
- existing.outerHTML = this.#summaryHTML();
- var allPassed = true;
- for (const test of this.testCases) {
- if (test.result != ResultType.passed) {
- allPassed = false;
- break;
- }
- }
- if (!TestClassRunner.#isRerunning) {
- document.getElementById(`${this.#cssId}details`).open = !allPassed;
- }
- }
- }
-
- static #idToTestCase = {}; // number -> [TestClassRunner, TestCaseRunner]
- static runAll(testClasses) {
- for (const testClass of testClasses) {
- const classRunner = new TestClassRunner(testClass);
- classRunner.updateHTML();
- const cases = classRunner.testCases.map(function(test) { return [ classRunner, test ] });
- for (const testTuple of cases) {
- this.#idToTestCase[testTuple[1].uniqueId] = testTuple;
- }
- this.#testQueue.push(...cases);
- }
- this.#runTestQueue();
- }
-
- static #testQueue = []; // tuples of [TestClassRunner, TestCaseRunner]
- static #testInterval = null;
- static #isRerunning = false;
- static #runTestQueue() {
- if (this.#testInterval) return;
- this.#testInterval = setInterval(function() {
- if (TestClassRunner.#testQueue.length == 0) {
- clearInterval(TestClassRunner.#testInterval);
- TestClassRunner.#testInterval = null;
- return;
- }
- var classRunner;
- var testCase;
- [ classRunner, testCase ] = TestClassRunner.#testQueue[0];
- if (testCase.result == ResultType.untested) {
- testCase.result = ResultType.testing;
- testCase.updateHTML();
- classRunner.updateHTML();
- } else if (testCase.result == ResultType.testing) {
- TestClassRunner.#testQueue.splice(0, 1);
- testCase.run();
- testCase.updateHTML();
- classRunner.updateHTML();
- }
- }, 1);
- }
-
- static rerun(testId) {
- const tuple = this.#idToTestCase[testId];
- if (!tuple) return;
- /** @type {TestClassRunner} */
- const testClass = tuple[0];
- /** @type {TestCaseRunner} */
- const testCase = tuple[1];
- testCase.result = ResultType.untested;
- testCase.updateHTML();
- this.#testQueue.push([testClass, testCase]);
- this.#isRerunning = true;
- this.#runTestQueue();
- }
- }
- </script>
- <!-- Tests -->
- <script src="jstest/TokenTests.js"></script>
- <script src="jstest/UtilsTests.js"></script>
- <script src="jstest/InlineTests.js"></script>
- <script src="jstest/BlockTests.js"></script>
- <script src="jstest/BrokenSyntaxTests.js"></script>
- <script src="jstest/spreadsheet/CellValueTests.js"></script>
- <script src="jstest/spreadsheet/CellAddressRangeTests.js"></script>
- <script src="jstest/spreadsheet/ExpressionSetTests.js"></script>
- <script src="jstest/spreadsheet/SpreadsheetMarkdownIntegrationTests.js"></script>
- <script>
- function onLoad() {
- let testClasses = [
- TokenTests,
- UtilsTests,
- InlineTests,
- BlockTests,
- BrokenSyntaxTests,
- CellValueTests,
- CellAddressRangeTests,
- ExpressionSetTests,
- SpreadsheetMarkdownIntegrationTests,
- ];
- TestClassRunner.runAll(testClasses);
- }
- document.addEventListener('DOMContentLoaded', onLoad);
-
- function normalizeWhitespace(str) {
- return str.replace(/\s+/gs, ' ').replace(/>\s+</gs, '><').trim();
- }
- </script>
- </head>
- <body>
- <div id="results"></div>
- </body>
- </html>
|