| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- <!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;
- }
- .testcase {
- border: 1px solid black;
- padding: 0.5em 1em;
- margin-bottom: 1em;
- max-width: 70em;
- }
- .testcasename {
- font-size: 115%;
- font-weight: bold;
- }
- .testcasestatus {
- font-weight: bold;
- }
- .result-untested {
- color: #888;
- }
- .result-testing {
- color: black;
- }
- .result-passed {
- color: #090;
- }
- .result-failed {
- color: #a00;
- }
- .result-errored {
- color: #a80;
- }
- </style>
- <script src="js/markdown.js"></script>
- <script>
- // TODO: Either run tests on a worker or at least on a setTimeout so
- // UI can update and not block the thread for the whole duration.
- function onLoad() {
- let testClasses = [
- MarkdownTest,
- ];
- var tests = [];
- for (const testClass of testClasses) {
- const instance = new testClass();
- if (!(instance instanceof BaseTest)) {
- console.error(`${testClass.name} does not extend BaseTest`);
- continue;
- }
- const testCases = TestCaseRun.findTestCases(instance);
- tests.push(...testCases);
- var classHTML = '<div class="testclass">';
- classHTML += `<div class="testclassname">${testClass.name}</div>`;
- for (const testCase of testCases) {
- classHTML += testCase.toHTML();
- }
- classHTML += '</div>';
- document.getElementById('results').innerHTML += classHTML;
- }
- var testInterval = setInterval(function() {
- if (tests.length == 0) {
- clearInterval(testInterval);
- testInterval = null;
- return;
- }
- let testCase = tests[0];
- if (testCase.result == ResultType.testing) {
- tests.splice(0, 1);
- testCase.updateHTML();
- testCase.run();
- testCase.updateHTML();
- } else {
- testCase.result = ResultType.testing;
- testCase.updateHTML();
- }
- }, 1);
- }
-
- /**
- * @param {String} text
- * @returns {String}
- */
- function escapeHTML(text) {
- return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
- }
-
- 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);
- }
- }
-
- class BaseTest {
- setUp() {}
- tearDown() {}
- /** @var {TestCaseRun|null} */
- currentRunner = null;
- fail(failMessage=null) {
- throw new FailureError(failMessage || 'failed');
- }
- assertTrue(test, failMessage=null) {
- if (!test) this.fail(failMessage || `expected true, got ${test}`);
- }
- assertFalse(test, failMessage=null) {
- if (test) this.fail(failMessage || `expected false, got ${test}`);
- }
- assertEqual(a, b, failMessage=null) {
- if (a != b) this.fail(failMessage || `equality failed: ${a} != ${b}`);
- }
- expectError(e=true) {
- if (this.currentRunner) this.currentRunner.expectedError = e;
- }
- /**
- * @param {number} maxTime maximum time in seconds
- * @param {function} timedCode code to run and time
- */
- profile(maxTime, timedCode) {
- const startTime = performance.now();
- const result = timedCode();
- const endTime = performance.now();
- const seconds = (endTime - startTime) / 1000.0;
- if (seconds > maxTime) {
- this.fail(`Expected <= ${maxTime}s execution time, actual ${seconds}s.`);
- }
- return result;
- }
- }
-
- class TestCaseRun {
- static #nextUniqueId = 1;
- /** @var {BaseTest} */
- #objectUnderTest;
- /** @var {method} */
- #method;
- uniqueId = 0;
- /** @var {ResultType} */
- result = ResultType.untested;
- /** @var {String|null} */
- message = null;
- expectedError = 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 = TestCaseRun.#nextUniqueId++;
- }
- run() {
- try {
- this.expectedError = null;
- this.#objectUnderTest.currentRunner = this;
- this.#objectUnderTest.setUp();
- this.#method.bind(this.#objectUnderTest)();
- this.result = ResultType.passed;
- this.message = null;
- } catch (e) {
- 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;
- }
- } 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;
- }
- }
- }
- #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="testcase${this.uniqueId}">`;
- html += `<div class="testcasename"><span class="testcaseclass">${this.className}</span>.<span class="testcasemethod">${this.methodName}</span></div>`;
- 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-tesitng">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>';
- html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
- break;
- case ResultType.errored:
- html += '<div class="testcasestatus result-errored">Errored</div>';
- html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
- break;
- }
- html += `</div>`;
- return html;
- }
- updateHTML() {
- let existing = document.getElementById(`testcase${this.uniqueId}`);
- if (existing) {
- existing.outerHTML = this.toHTML();
- } else {
- document.getElementById('results').innerHTML += this.toHTML();
- }
- }
-
- /**
- * @param {object} instance
- * @returns {TestCaseRun[]}
- */
- static findTestCases(instance) {
- if (!(instance instanceof BaseTest)) return [];
- var members = [];
- var obj = instance;
- do {
- members.push(...Object.getOwnPropertyNames(obj));
- } while (obj = Object.getPrototypeOf(obj));
- return members.sort().filter((e, i, arr) => {
- if (e != arr[i + 1] && typeof instance[e] == 'function' && e.startsWith('test')) return true;
- }).map((name) => {
- return new TestCaseRun(instance, instance[name]);
- });
- }
- }
-
- class MarkdownTest extends BaseTest {
- test_simpleSingleParagraph() {
- let markdown = 'Lorem ipsum';
- let expected = '<p>Lorem ipsum</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_strong() {
- let markdown = 'Lorem **ipsum** dolor **sit**';
- let expected = '<p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong></p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_emphasis() {
- let markdown = 'Lorem _ipsum_ dolor _sit_';
- let expected = '<p>Lorem <em>ipsum</em> dolor <em>sit</em></p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_strongEmphasis_easy() {
- let markdown = 'Lorem **ipsum *dolor* sit** amet';
- let expected = '<p>Lorem <strong>ipsum <em>dolor</em> sit</strong> amet</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_strongEmphasis_medium() {
- let markdown = 'Lorem ***ipsum*** dolor';
- let expected1 = '<p>Lorem <strong><em>ipsum</em></strong> dolor</p>';
- let expected2 = '<p>Lorem <em><strong>ipsum</strong></em> dolor</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertTrue(actual == expected1 || actual == expected2);
- }
-
- test_strongEmphasis_hard1() {
- let markdown = 'Lorem ***ipsum* dolor** sit';
- let expected = '<p>Lorem <strong><em>ipsum</em> dolor</strong> sit</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_strongEmphasis_hard2() {
- let markdown = 'Lorem ***ipsum** dolor* sit';
- let expected = '<p>Lorem <em><strong>ipsum</strong> dolor</em> sit</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_inlineCode() {
- let markdown = 'Lorem `ipsum` dolor';
- let expected = '<p>Lorem <code>ipsum</code> dolor</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
-
- test_inlineCode_withInnerBacktick() {
- let markdown = 'Lorem ``ip`su`m`` dolor';
- let expected = '<p>Lorem <code>ip`su`m</code> dolor</p>';
- let actual = Markdown.toHTML(markdown).trim();
- this.assertEqual(actual, expected);
- }
- }
-
- document.addEventListener('DOMContentLoaded', onLoad);
- </script>
- </head>
- <body>
- <div id="results"></div>
- </body>
- </html>
|