PHP and Javascript implementations of a simple markdown parser
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

testjs.html 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Markdown Unit Tests</title>
  6. <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  7. <style type="text/css">
  8. :root {
  9. font-family: sans-serif;
  10. }
  11. .testcase {
  12. border: 1px solid black;
  13. padding: 0.5em 1em;
  14. margin-bottom: 1em;
  15. max-width: 70em;
  16. }
  17. .testcasename {
  18. font-size: 115%;
  19. font-weight: bold;
  20. }
  21. .testcasestatus {
  22. font-weight: bold;
  23. }
  24. .result-untested {
  25. color: #888;
  26. }
  27. .result-testing {
  28. color: black;
  29. }
  30. .result-passed {
  31. color: #090;
  32. }
  33. .result-failed {
  34. color: #a00;
  35. }
  36. .result-errored {
  37. color: #a80;
  38. }
  39. </style>
  40. <script src="js/markdown.js"></script>
  41. <script>
  42. // TODO: Either run tests on a worker or at least on a setTimeout so
  43. // UI can update and not block the thread for the whole duration.
  44. function onLoad() {
  45. let testClasses = [
  46. MarkdownTest,
  47. ];
  48. var tests = [];
  49. for (const testClass of testClasses) {
  50. const instance = new testClass();
  51. if (!(instance instanceof BaseTest)) {
  52. console.error(`${testClass.name} does not extend BaseTest`);
  53. continue;
  54. }
  55. const testCases = TestCaseRun.findTestCases(instance);
  56. tests.push(...testCases);
  57. var classHTML = '<div class="testclass">';
  58. classHTML += `<div class="testclassname">${testClass.name}</div>`;
  59. for (const testCase of testCases) {
  60. classHTML += testCase.toHTML();
  61. }
  62. classHTML += '</div>';
  63. document.getElementById('results').innerHTML += classHTML;
  64. }
  65. var testInterval = setInterval(function() {
  66. if (tests.length == 0) {
  67. clearInterval(testInterval);
  68. testInterval = null;
  69. return;
  70. }
  71. let testCase = tests[0];
  72. if (testCase.result == ResultType.testing) {
  73. tests.splice(0, 1);
  74. testCase.updateHTML();
  75. testCase.run();
  76. testCase.updateHTML();
  77. } else {
  78. testCase.result = ResultType.testing;
  79. testCase.updateHTML();
  80. }
  81. }, 1);
  82. }
  83. /**
  84. * @param {String} text
  85. * @returns {String}
  86. */
  87. function escapeHTML(text) {
  88. return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  89. }
  90. class ResultType {
  91. static untested = new ResultType('untested');
  92. static testing = new ResultType('testing');
  93. static passed = new ResultType('passed');
  94. static failed = new ResultType('failed');
  95. static errored = new ResultType('errored');
  96. #name;
  97. constructor(name) {
  98. this.#name = name;
  99. }
  100. toString() {
  101. return `${this.constructor.name}.${this.#name}`;
  102. }
  103. }
  104. class FailureError extends Error {
  105. constructor(message=null) {
  106. super(message);
  107. }
  108. }
  109. class BaseTest {
  110. setUp() {}
  111. tearDown() {}
  112. /** @var {TestCaseRun|null} */
  113. currentRunner = null;
  114. fail(failMessage=null) {
  115. throw new FailureError(failMessage || 'failed');
  116. }
  117. assertTrue(test, failMessage=null) {
  118. if (!test) this.fail(failMessage || `expected true, got ${test}`);
  119. }
  120. assertFalse(test, failMessage=null) {
  121. if (test) this.fail(failMessage || `expected false, got ${test}`);
  122. }
  123. assertEqual(a, b, failMessage=null) {
  124. if (a != b) this.fail(failMessage || `equality failed: ${a} != ${b}`);
  125. }
  126. expectError(e=true) {
  127. if (this.currentRunner) this.currentRunner.expectedError = e;
  128. }
  129. /**
  130. * @param {number} maxTime maximum time in seconds
  131. * @param {function} timedCode code to run and time
  132. */
  133. profile(maxTime, timedCode) {
  134. const startTime = performance.now();
  135. const result = timedCode();
  136. const endTime = performance.now();
  137. const seconds = (endTime - startTime) / 1000.0;
  138. if (seconds > maxTime) {
  139. this.fail(`Expected <= ${maxTime}s execution time, actual ${seconds}s.`);
  140. }
  141. return result;
  142. }
  143. }
  144. class TestCaseRun {
  145. static #nextUniqueId = 1;
  146. /** @var {BaseTest} */
  147. #objectUnderTest;
  148. /** @var {method} */
  149. #method;
  150. uniqueId = 0;
  151. /** @var {ResultType} */
  152. result = ResultType.untested;
  153. /** @var {String|null} */
  154. message = null;
  155. expectedError = null;
  156. /** @var {String} */
  157. get className() { return this.#objectUnderTest.constructor.name; }
  158. /** @var {String} */
  159. get methodName() { return this.#method.name; }
  160. /**
  161. * @param {BaseTest} objectUnderTest
  162. * @param {method} method
  163. */
  164. constructor(objectUnderTest, method) {
  165. this.#objectUnderTest = objectUnderTest;
  166. this.#method = method;
  167. this.uniqueId = TestCaseRun.#nextUniqueId++;
  168. }
  169. run() {
  170. try {
  171. this.expectedError = null;
  172. this.#objectUnderTest.currentRunner = this;
  173. this.#objectUnderTest.setUp();
  174. this.#method.bind(this.#objectUnderTest)();
  175. this.result = ResultType.passed;
  176. this.message = null;
  177. } catch (e) {
  178. if (e instanceof FailureError) {
  179. this.result = ResultType.failed;
  180. this.message = e.message;
  181. } else if (this.#isExpectedError(e)) {
  182. this.result = ResultType.passed;
  183. this.message = null;
  184. } else {
  185. this.result = ResultType.errored;
  186. this.message = e.message;
  187. }
  188. } finally {
  189. this.expectedError = null;
  190. try {
  191. this.#objectUnderTest.tearDown();
  192. this.#objectUnderTest.currentRunner = null;
  193. } catch (e0) {
  194. console.error(`Failed to run ${this.className}.tearDown() - ${e0.message}`);
  195. this.result = ResultType.errored;
  196. this.message = e0.message;
  197. }
  198. }
  199. }
  200. #isExpectedError(e) {
  201. if (this.expectedError === null) return false;
  202. if (this.expectedError === true) return true;
  203. // TODO: Have a way to specify details about what kind of error is expected. Maybe a prototype instance and/or a testing lambda.
  204. return false;
  205. }
  206. toHTML() {
  207. var html = `<div class="testcase" id="testcase${this.uniqueId}">`;
  208. html += `<div class="testcasename"><span class="testcaseclass">${this.className}</span>.<span class="testcasemethod">${this.methodName}</span></div>`;
  209. switch (this.result) {
  210. case ResultType.untested:
  211. html += '<div class="testcasestatus result-untested">Waiting to test</div>';
  212. break;
  213. case ResultType.testing:
  214. html += '<div class="testcasestatus result-tesitng">Testing...</div>';
  215. break;
  216. case ResultType.passed:
  217. html += '<div class="testcasestatus result-passed">Passed</div>';
  218. break;
  219. case ResultType.failed:
  220. html += '<div class="testcasestatus result-failed">Failed</div>';
  221. html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
  222. break;
  223. case ResultType.errored:
  224. html += '<div class="testcasestatus result-errored">Errored</div>';
  225. html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
  226. break;
  227. }
  228. html += `</div>`;
  229. return html;
  230. }
  231. updateHTML() {
  232. let existing = document.getElementById(`testcase${this.uniqueId}`);
  233. if (existing) {
  234. existing.outerHTML = this.toHTML();
  235. } else {
  236. document.getElementById('results').innerHTML += this.toHTML();
  237. }
  238. }
  239. /**
  240. * @param {object} instance
  241. * @returns {TestCaseRun[]}
  242. */
  243. static findTestCases(instance) {
  244. if (!(instance instanceof BaseTest)) return [];
  245. var members = [];
  246. var obj = instance;
  247. do {
  248. members.push(...Object.getOwnPropertyNames(obj));
  249. } while (obj = Object.getPrototypeOf(obj));
  250. return members.sort().filter((e, i, arr) => {
  251. if (e != arr[i + 1] && typeof instance[e] == 'function' && e.startsWith('test')) return true;
  252. }).map((name) => {
  253. return new TestCaseRun(instance, instance[name]);
  254. });
  255. }
  256. }
  257. class MarkdownTest extends BaseTest {
  258. test_simpleSingleParagraph() {
  259. let markdown = 'Lorem ipsum';
  260. let expected = '<p>Lorem ipsum</p>';
  261. let actual = Markdown.toHTML(markdown).trim();
  262. this.assertEqual(actual, expected);
  263. }
  264. test_strong() {
  265. let markdown = 'Lorem **ipsum** dolor **sit**';
  266. let expected = '<p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong></p>';
  267. let actual = Markdown.toHTML(markdown).trim();
  268. this.assertEqual(actual, expected);
  269. }
  270. test_emphasis() {
  271. let markdown = 'Lorem _ipsum_ dolor _sit_';
  272. let expected = '<p>Lorem <em>ipsum</em> dolor <em>sit</em></p>';
  273. let actual = Markdown.toHTML(markdown).trim();
  274. this.assertEqual(actual, expected);
  275. }
  276. test_strongEmphasis_easy() {
  277. let markdown = 'Lorem **ipsum *dolor* sit** amet';
  278. let expected = '<p>Lorem <strong>ipsum <em>dolor</em> sit</strong> amet</p>';
  279. let actual = Markdown.toHTML(markdown).trim();
  280. this.assertEqual(actual, expected);
  281. }
  282. test_strongEmphasis_medium() {
  283. let markdown = 'Lorem ***ipsum*** dolor';
  284. let expected1 = '<p>Lorem <strong><em>ipsum</em></strong> dolor</p>';
  285. let expected2 = '<p>Lorem <em><strong>ipsum</strong></em> dolor</p>';
  286. let actual = Markdown.toHTML(markdown).trim();
  287. this.assertTrue(actual == expected1 || actual == expected2);
  288. }
  289. test_strongEmphasis_hard1() {
  290. let markdown = 'Lorem ***ipsum* dolor** sit';
  291. let expected = '<p>Lorem <strong><em>ipsum</em> dolor</strong> sit</p>';
  292. let actual = Markdown.toHTML(markdown).trim();
  293. this.assertEqual(actual, expected);
  294. }
  295. test_strongEmphasis_hard2() {
  296. let markdown = 'Lorem ***ipsum** dolor* sit';
  297. let expected = '<p>Lorem <em><strong>ipsum</strong> dolor</em> sit</p>';
  298. let actual = Markdown.toHTML(markdown).trim();
  299. this.assertEqual(actual, expected);
  300. }
  301. test_inlineCode() {
  302. let markdown = 'Lorem `ipsum` dolor';
  303. let expected = '<p>Lorem <code>ipsum</code> dolor</p>';
  304. let actual = Markdown.toHTML(markdown).trim();
  305. this.assertEqual(actual, expected);
  306. }
  307. test_inlineCode_withInnerBacktick() {
  308. let markdown = 'Lorem ``ip`su`m`` dolor';
  309. let expected = '<p>Lorem <code>ip`su`m</code> dolor</p>';
  310. let actual = Markdown.toHTML(markdown).trim();
  311. this.assertEqual(actual, expected);
  312. }
  313. }
  314. document.addEventListener('DOMContentLoaded', onLoad);
  315. </script>
  316. </head>
  317. <body>
  318. <div id="results"></div>
  319. </body>
  320. </html>