PHP and Javascript implementations of a simple markdown parser
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

testjs.html 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. --color-passed: #090;
  11. --color-failed: #a00;
  12. --color-errored: #a80;
  13. --color-untested: #888;
  14. --color-background: #fff;
  15. --color-text: #000;
  16. --color-secondary: #888;
  17. background-color: var(--color-background);
  18. color: var(--color-text);
  19. }
  20. .testclass {
  21. border: 1px solid var(--color-secondary);
  22. padding: 0.5em 1em;
  23. margin-bottom: 1em;
  24. max-width: 50em;
  25. }
  26. .testclassname {
  27. font-weight: bold;
  28. font-size: 1.25rem;
  29. padding-bottom: 0.25em;
  30. }
  31. .testclassstatus.passed { color: var(--color-passed); }
  32. .testclassstatus.failed { color: var(--color-failed); }
  33. .testclassstatus.errored { color: var(--color-errored); }
  34. .testclassstatus.untested { color: var(--color-untested); }
  35. .testcase {
  36. clear: both;
  37. padding: 0.2em 0;
  38. margin-left: 2em;
  39. }
  40. .testcase {
  41. border-top: 1px solid var(--color-secondary);
  42. }
  43. .testcasename {
  44. font-family: monospace;
  45. }
  46. .testcasestatus {
  47. font-weight: bold;
  48. font-size: 80%;
  49. float: left;
  50. }
  51. .testcasetiming {
  52. float: right;
  53. color: var(--color-secondary);
  54. font-size: 80%;
  55. }
  56. .testcasererun {
  57. float: right;
  58. padding-left: 0.5em;
  59. cursor: pointer;
  60. font-size: 150%;
  61. }
  62. .testcaseresult {
  63. height: 1em;
  64. }
  65. .testcasemessage {
  66. clear: both;
  67. }
  68. .result-untested {
  69. color: var(--color-untested);
  70. }
  71. .result-testing {
  72. color: var(--color-text);
  73. }
  74. .result-passed {
  75. color: var(--color-passed);
  76. }
  77. .result-failed {
  78. color: var(--color-failed);
  79. }
  80. .result-errored {
  81. color: var(--color-errored);
  82. }
  83. @media (prefers-color-scheme: dark) {
  84. :root {
  85. --color-passed: #090;
  86. --color-failed: #a00;
  87. --color-errored: #a80;
  88. --color-untested: #888;
  89. --color-background: #000;
  90. --color-text: #fff;
  91. --color-secondary: #888;
  92. }
  93. }
  94. </style>
  95. <script src="js/markdown.js"></script>
  96. <script src="js/spreadsheet.js"></script>
  97. <script src="jstest/basetest.js"></script>
  98. <!-- Testing infrastructure -->
  99. <script>
  100. /**
  101. * @param {String} text
  102. * @returns {String}
  103. */
  104. function escapeHTML(text) {
  105. return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br/>\n');
  106. }
  107. class ResultType {
  108. static untested = new ResultType('untested');
  109. static testing = new ResultType('testing');
  110. static passed = new ResultType('passed');
  111. static failed = new ResultType('failed');
  112. static errored = new ResultType('errored');
  113. #name;
  114. constructor(name) {
  115. this.#name = name;
  116. }
  117. toString() {
  118. return `${this.constructor.name}.${this.#name}`;
  119. }
  120. }
  121. class FailureError extends Error {
  122. constructor(message=null) {
  123. super(message);
  124. }
  125. }
  126. /**
  127. * Manages the running and results of a single test method on a test
  128. * class.
  129. */
  130. class TestCaseRunner {
  131. /** @var {number} */
  132. static #nextUniqueId = 1;
  133. /** @var {number} */
  134. uniqueId = 0;
  135. /** @var {BaseTest} */
  136. #objectUnderTest;
  137. /** @var {method} */
  138. #method;
  139. /** @var {ResultType} */
  140. result = ResultType.untested;
  141. /** @var {String|null} */
  142. message = null;
  143. expectedError = null;
  144. duration = null;
  145. /** @var {String} */
  146. get className() { return this.#objectUnderTest.constructor.name; }
  147. /** @var {String} */
  148. get methodName() { return this.#method.name; }
  149. /**
  150. * @param {BaseTest} objectUnderTest
  151. * @param {method} method
  152. */
  153. constructor(objectUnderTest, method) {
  154. this.#objectUnderTest = objectUnderTest;
  155. this.#method = method;
  156. this.uniqueId = TestCaseRunner.#nextUniqueId++;
  157. }
  158. run() {
  159. var start;
  160. this.expectedError = null;
  161. this.#objectUnderTest.currentRunner = this;
  162. try {
  163. this.#objectUnderTest.setUp();
  164. } catch (e) {
  165. console.error(`Failed to run ${this.className}.setUp() - ${e.message}`);
  166. this.result = ResultType.errored;
  167. this.message = e.message;
  168. return;
  169. }
  170. try {
  171. start = performance.now();
  172. this.#method.bind(this.#objectUnderTest)();
  173. this.duration = performance.now() - start;
  174. this.result = ResultType.passed;
  175. this.message = null;
  176. } catch (e) {
  177. this.duration = performance.now() - start;
  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. if (e.stack !== undefined) {
  188. this.message += "\n" + e.stack;
  189. }
  190. }
  191. } finally {
  192. this.expectedError = null;
  193. try {
  194. this.#objectUnderTest.tearDown();
  195. this.#objectUnderTest.currentRunner = null;
  196. } catch (e0) {
  197. console.error(`Failed to run ${this.className}.tearDown() - ${e0.message}`);
  198. this.result = ResultType.errored;
  199. this.message = e0.message;
  200. }
  201. }
  202. }
  203. get #cssId() { return `testcase${this.uniqueId}`; }
  204. #isExpectedError(e) {
  205. if (this.expectedError === null) return false;
  206. if (this.expectedError === true) return true;
  207. // TODO: Have a way to specify details about what kind of error is expected. Maybe a prototype instance and/or a testing lambda.
  208. return false;
  209. }
  210. toHTML() {
  211. var html = `<div class="testcase" id="${this.#cssId}">`;
  212. html += `<div class="testcasename"><span class="testcasemethod">${this.methodName}</span></div>`;
  213. if (this.result == ResultType.passed || this.result == ResultType.failed || this.result == ResultType.errored) {
  214. const rerunId = `testcasererun-${this.#cssId}`;
  215. const testId = this.uniqueId;
  216. html += `<div class="testcasererun" id="${rerunId}">🔄</div>`;
  217. setTimeout(() => {
  218. const node = document.getElementById(rerunId);
  219. if (!node) return;
  220. node.addEventListener('click', () => {
  221. TestClassRunner.rerun(testId);
  222. });
  223. }, 0);
  224. }
  225. html += '<div class="testcaseresult">';
  226. switch (this.result) {
  227. case ResultType.untested:
  228. html += '<div class="testcasestatus result-untested">Waiting to test</div>';
  229. break;
  230. case ResultType.testing:
  231. html += '<div class="testcasestatus result-testing">Testing...</div>';
  232. break;
  233. case ResultType.passed:
  234. html += '<div class="testcasestatus result-passed">Passed</div>';
  235. break;
  236. case ResultType.failed:
  237. html += '<div class="testcasestatus result-failed">Failed</div>';
  238. break;
  239. case ResultType.errored:
  240. html += '<div class="testcasestatus result-errored">Errored</div>';
  241. break;
  242. }
  243. if (this.duration !== null) {
  244. html += `<div class="testcasetiming">${Number(this.duration / 1000.0).toFixed(3)}s</div>`;
  245. }
  246. html += '</div>';
  247. if (this.message !== null) {
  248. html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
  249. }
  250. html += `</div>`;
  251. return html;
  252. }
  253. /**
  254. * Updates the HTML node in-place with the current status, or
  255. * adds it if it does not exist yet.
  256. */
  257. updateHTML() {
  258. let existing = document.getElementById(this.#cssId);
  259. if (existing) {
  260. existing.outerHTML = this.toHTML();
  261. } else {
  262. document.getElementById('results').innerHTML += this.toHTML();
  263. }
  264. }
  265. /**
  266. * @param {object} objectUnderTest
  267. * @returns {TestCaseRunner[]}
  268. */
  269. static findTestCases(objectUnderTest) {
  270. if (!(objectUnderTest instanceof BaseTest)) return [];
  271. var members = [];
  272. var obj = objectUnderTest;
  273. do {
  274. members.push(...Object.getOwnPropertyNames(obj));
  275. } while (obj = Object.getPrototypeOf(obj));
  276. return members.sort().filter((e, i, arr) => {
  277. if (e != arr[i + 1] && typeof objectUnderTest[e] == 'function' && e.startsWith('test')) return true;
  278. }).map((name) => {
  279. return new TestCaseRunner(objectUnderTest, objectUnderTest[name]);
  280. });
  281. }
  282. }
  283. class TestClassRunner {
  284. static #nextUniqueId = 1;
  285. #uniqueId = 0;
  286. #theClass;
  287. #instance;
  288. #testCases;
  289. get testCases() { return this.#testCases; }
  290. constructor(theClass) {
  291. this.#theClass = theClass;
  292. this.#instance = new theClass();
  293. this.#testCases = TestCaseRunner.findTestCases(this.#instance);
  294. this.#uniqueId = TestClassRunner.#nextUniqueId++;
  295. }
  296. get #cssId() { return `testclass${this.#uniqueId}`; }
  297. #summaryHTML() {
  298. var anyTesting = false;
  299. var anyFailed = false;
  300. var anyErrored = false;
  301. var anyUntested = false;
  302. var anyPassed = false;
  303. var allPassed = true;
  304. for (const test of this.testCases) {
  305. switch (test.result) {
  306. case ResultType.untested:
  307. anyUntested = true;
  308. allPassed = false;
  309. break;
  310. case ResultType.testing:
  311. anyTesting = true;
  312. allPassed = false;
  313. break;
  314. case ResultType.passed:
  315. anyPassed = true;
  316. break;
  317. case ResultType.failed:
  318. anyFailed = true;
  319. allPassed = false;
  320. break;
  321. case ResultType.errored:
  322. anyErrored = true;
  323. allPassed = false;
  324. break;
  325. }
  326. }
  327. var html = '';
  328. html += `<summary class="testclasssummary" id="${this.#cssId}summary">`;
  329. html += `<span class="testclassname">${this.#theClass.name}</span> `;
  330. if (anyTesting || (anyUntested && (anyPassed || anyFailed || anyErrored))) {
  331. html += '<span class="testclassstatus testing">Testing...</span>';
  332. } else if (anyErrored) {
  333. html += '<span class="testclassstatus errored">Errored</span>';
  334. } else if (anyFailed) {
  335. html += '<span class="testclassstatus failed">Failed</span>';
  336. } else if (allPassed) {
  337. html += '<span class="testclassstatus passed">Passed</span>';
  338. }
  339. html += '</summary>';
  340. return html;
  341. }
  342. toHTML() {
  343. var html = '';
  344. html += `<div class="testclass" id="${this.#cssId}">`;
  345. html += `<details id="${this.#cssId}details">`;
  346. html += this.#summaryHTML();
  347. for (const testCase of this.#testCases) {
  348. html += testCase.toHTML();
  349. }
  350. html += '</details>';
  351. html += '</div>';
  352. return html;
  353. }
  354. updateHTML() {
  355. var existing = document.getElementById(`${this.#cssId}summary`);
  356. if (!existing) {
  357. document.getElementById('results').innerHTML += this.toHTML();
  358. } else {
  359. existing.outerHTML = this.#summaryHTML();
  360. var allPassed = true;
  361. for (const test of this.testCases) {
  362. if (test.result != ResultType.passed) {
  363. allPassed = false;
  364. break;
  365. }
  366. }
  367. if (!TestClassRunner.#isRerunning) {
  368. document.getElementById(`${this.#cssId}details`).open = !allPassed;
  369. }
  370. }
  371. }
  372. static #idToTestCase = {}; // number -> [TestClassRunner, TestCaseRunner]
  373. static runAll(testClasses) {
  374. for (const testClass of testClasses) {
  375. const classRunner = new TestClassRunner(testClass);
  376. classRunner.updateHTML();
  377. const cases = classRunner.testCases.map(function(test) { return [ classRunner, test ] });
  378. for (const testTuple of cases) {
  379. this.#idToTestCase[testTuple[1].uniqueId] = testTuple;
  380. }
  381. this.#testQueue.push(...cases);
  382. }
  383. this.#runTestQueue();
  384. }
  385. static #testQueue = []; // tuples of [TestClassRunner, TestCaseRunner]
  386. static #testInterval = null;
  387. static #isRerunning = false;
  388. static #runTestQueue() {
  389. if (this.#testInterval) return;
  390. this.#testInterval = setInterval(function() {
  391. if (TestClassRunner.#testQueue.length == 0) {
  392. clearInterval(TestClassRunner.#testInterval);
  393. TestClassRunner.#testInterval = null;
  394. return;
  395. }
  396. var classRunner;
  397. var testCase;
  398. [ classRunner, testCase ] = TestClassRunner.#testQueue[0];
  399. if (testCase.result == ResultType.untested) {
  400. testCase.result = ResultType.testing;
  401. testCase.updateHTML();
  402. classRunner.updateHTML();
  403. } else if (testCase.result == ResultType.testing) {
  404. TestClassRunner.#testQueue.splice(0, 1);
  405. testCase.run();
  406. testCase.updateHTML();
  407. classRunner.updateHTML();
  408. }
  409. }, 1);
  410. }
  411. static rerun(testId) {
  412. const tuple = this.#idToTestCase[testId];
  413. if (!tuple) return;
  414. /** @type {TestClassRunner} */
  415. const testClass = tuple[0];
  416. /** @type {TestCaseRunner} */
  417. const testCase = tuple[1];
  418. testCase.result = ResultType.untested;
  419. testCase.updateHTML();
  420. this.#testQueue.push([testClass, testCase]);
  421. this.#isRerunning = true;
  422. this.#runTestQueue();
  423. }
  424. }
  425. </script>
  426. <!-- Tests -->
  427. <script src="jstest/TokenTests.js"></script>
  428. <script src="jstest/UtilsTests.js"></script>
  429. <script src="jstest/InlineTests.js"></script>
  430. <script src="jstest/BlockTests.js"></script>
  431. <script src="jstest/BrokenSyntaxTests.js"></script>
  432. <script src="jstest/spreadsheet/CellValueTests.js"></script>
  433. <script src="jstest/spreadsheet/CellAddressRangeTests.js"></script>
  434. <script src="jstest/spreadsheet/ExpressionSetTests.js"></script>
  435. <script src="jstest/spreadsheet/SpreadsheetMarkdownIntegrationTests.js"></script>
  436. <script>
  437. function onLoad() {
  438. let testClasses = [
  439. TokenTests,
  440. UtilsTests,
  441. InlineTests,
  442. BlockTests,
  443. BrokenSyntaxTests,
  444. CellValueTests,
  445. CellAddressRangeTests,
  446. ExpressionSetTests,
  447. SpreadsheetMarkdownIntegrationTests,
  448. ];
  449. TestClassRunner.runAll(testClasses);
  450. }
  451. document.addEventListener('DOMContentLoaded', onLoad);
  452. function normalizeWhitespace(str) {
  453. return str.replace(/\s+/gs, ' ').replace(/>\s+</gs, '><').trim();
  454. }
  455. </script>
  456. </head>
  457. <body>
  458. <div id="results"></div>
  459. </body>
  460. </html>