From e44b8e893c20a895c8f70d8fdc94f9901e8b414e Mon Sep 17 00:00:00 2001 From: Aria Buckles Date: Thu, 24 Oct 2019 14:46:48 -0700 Subject: [PATCH] Typescript: Add type checking of tests Adds types to simple-markdown-test.js and checks that file with typescript as well \o/ Test plan: `make test` --- __tests__/simple-markdown-test.js | 204 +++++++++++++++++++++++------- package-lock.json | 21 +++ package.json | 3 + tsconfig.json | 3 + 4 files changed, 187 insertions(+), 44 deletions(-) diff --git a/__tests__/simple-markdown-test.js b/__tests__/simple-markdown-test.js index d745b5d..a39a97d 100644 --- a/__tests__/simple-markdown-test.js +++ b/__tests__/simple-markdown-test.js @@ -1,4 +1,5 @@ -// @flow +/* @flow */ +/* @ts-check */ var assert = require("assert"); var _ = require("underscore"); @@ -21,21 +22,31 @@ var FLOW_IGNORE_COVARIANCE = { }; */ -// A pretty-printer that handles `undefined` and functions better -// than JSON.stringify -// Important because some AST node fields can be undefined, and -// if those don't show up in the assert output, it can be -// very confusing to figure out how the actual and expected differ -// Whether node's util.inspect or JSON.stringify is better seems -// context dependent. +/** + * A pretty-printer that handles `undefined` and functions better + * than JSON.stringify + * Important because some AST node fields can be undefined, and + * if those don't show up in the assert output, it can be + * very confusing to figure out how the actual and expected differ + * Whether node's util.inspect or JSON.stringify is better seems + * context dependent. + * + * @param {SimpleMarkdown.ASTNode | Array} ast + */ var prettyPrintAST = function(ast) { return JSON.stringify(ast, null, 4); +// // FIXME(aria): For debugging in more depth? This used to work? // return nodeUtil.inspect(ast, { // depth: null, // colors: false // }); }; +/** + * Asset that two ast parse trees are equal + * @param {SimpleMarkdown.ASTNode | Array} parsed + * @param {SimpleMarkdown.ASTNode | Array} expected + */ var validateParse = function(parsed, expected) { if (!_.isEqual(parsed, expected)) { var parsedStr = prettyPrintAST(parsed); @@ -51,6 +62,10 @@ var validateParse = function(parsed, expected) { } }; +/** + * @param {SimpleMarkdown.ReactElements} reactElements + * @returns {string} + */ var reactToHtml = function(reactElements) { var rawHtml = ReactDOMServer.renderToStaticMarkup( React.createElement('div', {}, reactElements) @@ -65,26 +80,46 @@ var reactToHtml = function(reactElements) { return simplifiedHtml; }; +/** + * @param {SimpleMarkdown.ASTNode} parsed + * @returns {string} + */ var htmlThroughReact = function(parsed) { var output = defaultReactOutput(parsed); return reactToHtml(output); }; +/** + * @param {string} source + * @returns {string} + */ var htmlFromReactMarkdown = function(source) { return htmlThroughReact(implicitParse(source)); }; +/** + * @param {string} source + * @returns {string} + */ var htmlFromMarkdown = function(source) { var html = defaultHtmlOutput(implicitParse(source)); var simplifiedHtml = html.replace(/\s+/g, ' '); return simplifiedHtml; }; +/** + * @param {string} source + * @param {string} html + */ var assertParsesToReact = function(source, html) { var actualHtml = htmlFromReactMarkdown(source); assert.strictEqual(actualHtml, html); }; +/** + * @param {string} source + * @param {string} html + */ var assertParsesToHtml = function(source, html) { var actualHtml = htmlFromMarkdown(source); assert.strictEqual(actualHtml, html); @@ -2492,7 +2527,9 @@ describe("simple markdown", function() { [{type: "text", content: "h2"}], [{type: "text", content: "h3"}] ], - align: [null, null, null], + align: /** @type {Array} */ ( + [null, null, null] + ), cells: [ [ [{type: "text", content: "d1"}], @@ -2534,7 +2571,9 @@ describe("simple markdown", function() { [{type: "em", content: [{type: "text", content: "h2"}]}], [{type: "em", content: [{type: "text", content: "h3"}]}], ], - align: [null, null, null], + align: /** @type {Array} */ ( + [null, null, null] + ), cells: [[ [{type: "em", content: [{type: "text", content: "d1"}]}], [{type: "em", content: [{type: "text", content: "d2"}]}], @@ -2560,6 +2599,10 @@ describe("simple markdown", function() { }); it("should parse table alignments", function() { + /** + * @param {string} tableSrc + * @param {Array} expectedAligns + */ var validateAligns = function(tableSrc, expectedAligns) { var parsed = blockParse(tableSrc + "\n"); assert.strictEqual(parsed[0].type, "table"); @@ -2571,7 +2614,9 @@ describe("simple markdown", function() { "| h1 | h2 | h3 |\n" + "| -- | -- | -- |\n" + "| d1 | d2 | d3 |\n", - [null, null, null] + /** @type {Array} */ ( + [null, null, null] + ) ); validateAligns( @@ -2613,13 +2658,15 @@ describe("simple markdown", function() { it("should parse empty table cells", function() { var expected = [{ type: "table", - header: [ + header: /** @type {any[][]} */ ([ [], [], [] - ], - align: [null, null, null], - cells: [ + ]), + align: /** @type {Array} */ ( + [null, null, null] + ), + cells: /** @type {any[][]} */ ([ [ [], [], @@ -2630,7 +2677,7 @@ describe("simple markdown", function() { [], [] ] - ] + ]), }]; var parsedPiped = blockParse( @@ -2667,7 +2714,9 @@ describe("simple markdown", function() { {type: "text", content: '|'}, ], ], - align: [null, null], + align: /** @type {Array} */ ( + [null, null] + ), cells: [[ [ {type: "text", content: "pos"}, @@ -2709,7 +2758,9 @@ describe("simple markdown", function() { [{type: "text", content: 'Attribute'}], [{type: "text", content: 'Type'}], ], - align: [null, null], + align: /** @type {Array} */ ( + [null, null] + ), cells: [[ [{type: "inlineCode", content: "position"}], [{type: "inlineCode", content: '"left" | "right"'}], @@ -2843,13 +2894,17 @@ describe("simple markdown", function() { describe("parser extension api", function() { it("should parse a simple %variable% extension", function() { var percentVarRule = { - match: function(source) { + match: function(/** @type {string} */ source) { return /^%([\s\S]+?)%/.exec(source); }, order: SimpleMarkdown.defaultRules.em.order + 0.5, - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { return { content: capture[1] }; @@ -2862,6 +2917,7 @@ describe("simple markdown", function() { var rawBuiltParser = SimpleMarkdown.parserFor(rules); + /** @type {SimpleMarkdown.Parser} */ var inlineParse = function(source) { return rawBuiltParser(source, {inline: true}); }; @@ -2878,7 +2934,11 @@ describe("simple markdown", function() { describe("should sort rules by order and name", function() { var emRule = { match: SimpleMarkdown.inlineRegex(/^_([\s\S]+?)_/), - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { return { content: capture[1] }; @@ -2886,7 +2946,11 @@ describe("simple markdown", function() { }; var strongRule = { match: SimpleMarkdown.defaultRules.strong.match, - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { return { content: capture[1] }; @@ -3028,8 +3092,9 @@ describe("simple markdown", function() { it("should output a warning for non-numeric orders", function() { var oldconsolewarn = console.warn; + /** @type {any[]} */ var warnings = []; - /*::FLOW_IGNORE_COVARIANCE.*/ console.warn = function(warning) { + /*::FLOW_IGNORE_COVARIANCE.*/ console.warn = function(/** @type {any} */ warning) { warnings.push(warning); }; var parser1 = SimpleMarkdown.parserFor({ @@ -3130,11 +3195,15 @@ describe("simple markdown", function() { var parser1 = SimpleMarkdown.parserFor({ fancy: { order: SimpleMarkdown.defaultRules.text.order - 1, - match: function(source) { + match: function(/** @type {string} */ source) { return /^.*/.exec(source); }, - parse: function(capture, parse, state) { - return capture[0].split(' ').map(function(word) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { + return capture[0].split(' ').map(function(/** @type {string} */ word) { return { type: "text", content: word }; }); }, @@ -3158,7 +3227,11 @@ describe("simple markdown", function() { // gives a flattened array result of the words. var rules = { Array: { - result: function(arr, output, state) { + result: function( + /** @type {Array} */ arr, + /** @type {SimpleMarkdown.Output} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { return arr.map(function(node) { return output(node, state); }).filter(function(word) { @@ -3168,23 +3241,35 @@ describe("simple markdown", function() { }, word: { order: SimpleMarkdown.defaultRules.text.order - 1, - match: function(source) { + match: function(/** @type {string} */ source) { return /^\w+/.exec(source); }, - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { state.wordCount++; return {content: capture[0]}; }, - result: function(node, output, state) { + result: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.NodeOutput} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { state.wordCount++; return node.content; }, }, delimiter: Object.assign({}, SimpleMarkdown.defaultRules.text, { - match: function(source) { + match: function(/** @type {string} */ source) { return /^\W+/.exec(source); }, - result: function(node, output, state) { + result: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.NodeOutput} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { return null; }, }), @@ -3194,6 +3279,7 @@ describe("simple markdown", function() { var output = SimpleMarkdown.outputFor(rules, 'result', {wordCount: 0}); // test parsing + /** @type {{ wordCount?: number }} */ var parseState = {}; var ast1 = parse('hi here are some words', parseState); assert.strictEqual(parseState.wordCount, 5); @@ -3203,6 +3289,7 @@ describe("simple markdown", function() { assert.deepEqual(ast1, ast2); // test output + /** @type {{ wordCount?: number }} */ var outputState = {}; var result1 = output(ast1, outputState); assert.deepEqual(result1, ['hi', 'here', 'are', 'some', 'words']); @@ -3218,10 +3305,14 @@ describe("simple markdown", function() { { fancy: { order: SimpleMarkdown.defaultRules.text.order - 1, - match: function(source) { + match: function(/** @type {string} */ source) { return /^\w+/.exec(source); }, - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { var word = capture[0]; var translated = state.lookup[word]; if (translated) { @@ -3232,7 +3323,7 @@ describe("simple markdown", function() { }, }, text: Object.assign({}, SimpleMarkdown.defaultRules.text, { - match: function(source) { + match: function(/** @type {string} */ source) { return /^\W+/.exec(source); }, }), @@ -3263,7 +3354,11 @@ describe("simple markdown", function() { { Array: SimpleMarkdown.defaultRules.Array, text: Object.assign({}, SimpleMarkdown.defaultRules.text, { - react: function(node, output, state) { + react: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.ReactNodeOutput} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { return React.createElement( state.TextComponent, {key: state.key}, @@ -3292,10 +3387,14 @@ describe("simple markdown", function() { var parse = SimpleMarkdown.parserFor({ bracketed: { order: SimpleMarkdown.defaultRules.text.order - 1, - match: function(source) { + match: function(/** @type {string} */ source) { return /^\{((?:\\[\S\s]|[^\\\*])+)\}/.exec(source); }, - parse: function(capture, parse, state) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse, + /** @type {SimpleMarkdown.State} */ state + ) { var result = { // note no passing state here: content: parse(capture[1]), @@ -3356,12 +3455,19 @@ describe("simple markdown", function() { var output = SimpleMarkdown.outputFor({ Array: SimpleMarkdown.defaultRules.Array, paragraph: Object.assign({}, SimpleMarkdown.defaultRules.paragraph, { - html: function(node, output) { + html: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.HtmlOutput} */ output + ) { return '

' + output(node.content) + '

'; }, }), text: Object.assign({}, SimpleMarkdown.defaultRules.text, { - html: function(node, output, state) { + html: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.HtmlOutput} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { return '' + @@ -3392,13 +3498,19 @@ describe("simple markdown", function() { Array: SimpleMarkdown.defaultRules.Array, spoiler: { order: SimpleMarkdown.defaultRules.text.order - 1, - match: function(source) { + match: function(/** @type {string} */ source) { return /^\[\[((?:[^\]]|\][^\]])+)\]\]/.exec(source); }, - parse: function(capture, parse) { + parse: function( + /** @type {SimpleMarkdown.Capture} */ capture, + /** @type {SimpleMarkdown.Parser} */ parse + ) { return {content: parse(capture[1])}; }, - html: function(node, output) { + html: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.HtmlOutput} */ output + ) { return '' + output(node.content) + ''; @@ -3800,7 +3912,11 @@ describe("simple markdown", function() { it("should join text nodes before outputting them", function() { var rules = Object.assign({}, SimpleMarkdown.defaultRules, { text: Object.assign({}, SimpleMarkdown.defaultRules.text, { - react: function(node, output, state) { + react: function( + /** @type {SimpleMarkdown.SingleASTNode} */ node, + /** @type {SimpleMarkdown.ReactOutput} */ output, + /** @type {SimpleMarkdown.State} */ state + ) { return React.createElement( 'span', {key: state.key, className: 'text'}, diff --git a/package-lock.json b/package-lock.json index f58627f..117d7fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,6 +200,12 @@ } } }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, "@types/node": { "version": "12.11.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.7.tgz", @@ -225,6 +231,21 @@ "csstype": "^2.2.0" } }, + "@types/react-dom": { + "version": "16.9.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.2.tgz", + "integrity": "sha512-hgPbBoI1aTSTvZwo8HYw35UaTldW6n2ETLvHAcfcg1FaOuBV3olmyCe5eMpx2WybWMBPv0MdU2t5GOcQhP+3zA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/underscore": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.9.3.tgz", + "integrity": "sha512-SwbHKB2DPIDlvYqtK5O+0LFtZAyrUSw4c0q+HWwmH1Ve3KMQ0/5PlV3RX97+3dP7yMrnNQ8/bCWWvQpPl03Mug==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", diff --git a/package.json b/package.json index 3a97e81..d4abe6a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,9 @@ "@types/react": ">=16.0.0" }, "devDependencies": { + "@types/mocha": "^5.2.7", + "@types/react-dom": ">=16.0.0", + "@types/underscore": ">=1.4.4", "express": "^4.16.4", "flow-bin": "^0.94.0", "flow-typed": "^2.5.1", diff --git a/tsconfig.json b/tsconfig.json index 9c477fc..bf5a50e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,9 @@ "simple-markdown.d.ts", "simple-markdown.js" ], + "include": [ + "__tests__/**/*" + ], "exclude": [ "node_modules" ],