Source: plugin/assertivedocs/assertivedocs.js

  1. /**
  2. * @file assertivedocs
  3. * @fileoverview Defines the @assert tag and the supporting logic
  4. */
  5. const fs = require('fs');
  6. const path = require('path');
  7. const logger = require('jsdoc/util/logger');
  8. const env = require('jsdoc/env');
  9. const cwd = process.cwd();
  10. const destination = env.opts.destination || '';
  11. /**
  12. * Current working file.
  13. * @memberof assertivedocs
  14. */
  15. let file;
  16. /**
  17. * @namespace typeMappings
  18. */
  19. const typeMappings = {
  20. /**
  21. * Converts the argument to a string. Arguments are a string by default.
  22. * @param {String} arg - Argument from the unit test specification
  23. * @returns {String}
  24. */
  25. string: function(arg) { return arg },
  26. /**
  27. * Converts the argument to an integer.
  28. * @param {String} arg - Argument from the unit test specification
  29. * @returns {Number}
  30. */
  31. int: function(arg) { return parseInt(arg) },
  32. /**
  33. * Converts the argument to a number.
  34. * @param {String} arg Argument from the unit test specification
  35. * @returns {Number}
  36. */
  37. number: function(arg) { return parseFloat(arg) },
  38. /**
  39. * Converts the argument to a boolean value.
  40. * @param {String} arg - Argument from the unit test specification
  41. * @returns {Boolean}
  42. */
  43. bool: function(arg) { return ['true', 'false', '1', '0'].includes(arg) },
  44. /**
  45. * Converts the argument to an array.
  46. * @param {String} arg - Argument from the unit test specification
  47. * @returns {Array}
  48. */
  49. array: function(arg) { return JSON.parse(arg.replaceAll(';', ',')) },
  50. /**
  51. * Returns undefined.
  52. * @param {String} arg - Argument from the unit test specification
  53. * @returns {undefined}
  54. */
  55. undefined: function(arg) { return undefined },
  56. /**
  57. * Returns null.
  58. * @param {String} arg - Argument from the unit test specification
  59. * @returns {null}
  60. */
  61. null: function(arg) { return null },
  62. /**
  63. * Returns NaN.
  64. * @param {String} arg - Argument from the unit test specification
  65. * @returns {NaN}
  66. */
  67. NaN: function(arg) { return NaN },
  68. /**
  69. * Attempts to convert the argument to the given type
  70. * @param {String} arg - Argument to convert
  71. * @param {String} type - The type to convert to
  72. * @returns {String|Number|Boolean|any[]|undefined|null|NaN}
  73. */
  74. convert: function(arg, type) {
  75. try {
  76. switch (type) {
  77. case 'string':
  78. return typeMappings.string(arg);
  79. case 'int':
  80. return typeMappings.int(arg);
  81. case 'number':
  82. return typeMappings.number(arg);
  83. case 'bool':
  84. return typeMappings.bool(arg);
  85. case 'array':
  86. return typeMappings.array(arg);
  87. case 'undefined':
  88. return typeMappings.undefined(arg);
  89. case 'null':
  90. return typeMappings.null(arg);
  91. case 'NaN':
  92. return typeMappings.NaN(arg);
  93. case 'object':
  94. return typeMappings.object ? typeMappings.object(arg) : {};
  95. default:
  96. return arg;
  97. };
  98. } catch (error) {
  99. logger.error(error);
  100. return arg;
  101. }
  102. },
  103. /**
  104. * Checks that a custom object map has been provided and attempts to add it to
  105. * typeMappings as a mixin.
  106. */
  107. modify: function() {
  108. if (env.opts.assertivedocs.customObjects) {
  109. const { typeMappingsMixin } = require(path.join(cwd, env.opts.assertivedocs.customObjects));
  110. Object.assign(typeMappings, typeMappingsMixin);
  111. }
  112. }
  113. }
  114. // Attempt to load mixin
  115. typeMappings.modify();
  116. /**
  117. * An object for asserting the truth of the
  118. * @namespace Assertion
  119. * @param {Function} func - The function to test
  120. * @param {any[]} args - List of arguments to pass to the function
  121. * @param {any} expected - The expected result of the function
  122. * @example
  123. * function foo(bar) {
  124. * return bar;
  125. * }
  126. *
  127. * const test = Assertion(foo, ['hello'], 'hello');
  128. * // Returns 'true'
  129. * console.log(test.assert());
  130. *
  131. * @assert {Assertion} Test1 - console.log,["hello"]:json=>:undefined
  132. */
  133. function Assertion(func, args, expected) {
  134. /**
  135. * Function to be assessed.
  136. * @memberof Assertion
  137. */
  138. this.func = func ? func : () => {};
  139. /**
  140. * The arguments to be passed to the function.
  141. * @memberof Assertion
  142. */
  143. this.args = args;
  144. /**
  145. * The expected result when the function is passed the arguments.
  146. * @memberof Assertion
  147. */
  148. this.expected = expected;
  149. /**
  150. * Asserts that the stored function produces
  151. * the expected result when passed the given
  152. * arguments.
  153. * @memberof Assertion
  154. * @returns {String}
  155. */
  156. this.assert = function() {
  157. try {
  158. const outcome = this.func(...this.args);
  159. return outcome === this.expected;
  160. } catch (error) {
  161. return error;
  162. }
  163. }
  164. }
  165. /**
  166. * The function to call when an assert tag is found.
  167. * @namespace assertivedocs
  168. * @param {jsdoc.Doclet} doclet - The doclet that the tag is in
  169. * @param {jsdoc.Tag} tag - The found tag
  170. */
  171. function assertOnTagged(doclet, tag) {
  172. // Split tag value description on =>
  173. const parts = tag.value.description.split('=>');
  174. // Get all the arguments to parse
  175. let args = parts[0].split(',');
  176. // Get the type converted arguments
  177. args = args.map((arg) => {
  178. arg = arg.split(':');
  179. return typeMappings.convert(arg[0], arg[1]);
  180. });
  181. // Get the expected result
  182. let expected = parts[1].split(':');
  183. expected = expected.length > 1 ? typeMappings.convert(expected[0], expected[1]) : expected[0];
  184. // Create new Assertion
  185. const test = new Assertion(file[doclet.meta.code.name], args, expected);
  186. // Create formatted unit test result for docs
  187. const result = {
  188. name: tag.value.name ? tag.value.name : "",
  189. arguments: args.map((arg) => typeof arg === 'object' ? JSON.stringify(arg) : arg ).join(', '),
  190. expected: expected,
  191. result: test.assert().toString(),
  192. }
  193. // Assign the result to the doclet
  194. if (!doclet.tests) doclet.tests = [];
  195. doclet.tests.push(result);
  196. }
  197. const defineTags = function(dictionary) {
  198. // Definition of @assert
  199. dictionary.defineTag('assert', {
  200. mustHaveValue: true,
  201. canHaveType: true,
  202. canHaveName: true,
  203. onTagged: assertOnTagged,
  204. });
  205. }
  206. const handlers = {
  207. fileBegin: function(e) {
  208. file = require(e.filename);
  209. },
  210. processingComplete: function(e) {
  211. let out;
  212. fs.readFile(path.join(__dirname, '/assertivehead.html'), 'utf8', (_, data) => {
  213. out = data;
  214. e.doclets.forEach(doclet => {
  215. if (doclet.tests) {
  216. out += `
  217. <h3>${doclet.meta.filename}:${doclet.meta.code.name}</h3>
  218. <table class="params">
  219. <tr>
  220. <thead>
  221. <th>Test</th>
  222. <th>Arguments</th>
  223. <th>Expected</th>
  224. <th class="last">Results</th>
  225. </thead>
  226. </tr>
  227. <tbody>
  228. `;
  229. doclet.tests.forEach((test) => {
  230. out += `
  231. <tr>
  232. <td>${test.name}</td>
  233. <td>${test.arguments}</td>
  234. <td>${test.expected}</td>
  235. <td class="last">${test.result}</td>
  236. </tr>
  237. `;
  238. });
  239. out += `
  240. </tbody>
  241. </table>
  242. </body>
  243. </html>
  244. `;
  245. }
  246. });
  247. if (!fs.existsSync(path.join(cwd, destination, 'unit-tests/'))) {
  248. fs.mkdir(path.join(cwd, destination, 'unit-tests/'), (err) => {
  249. logger.error(err);
  250. fs.writeFile(path.join(cwd, destination, 'unit-tests/index.html'), out, {
  251. encoding: 'utf-8',
  252. }, (error) => {
  253. if (error) logger.error(error);
  254. });
  255. });
  256. } else {
  257. fs.writeFile(path.join(cwd, destination, 'unit-tests/index.html'), out, {
  258. encoding: 'utf-8',
  259. }, (error) => {
  260. if (error) logger.error(error);
  261. });
  262. }
  263. });
  264. }
  265. }
  266. module.exports = {
  267. // Plugin definition
  268. defineTags,
  269. handlers,
  270. // Plugin documnentation; allows assertions to be run on this file
  271. Assertion,
  272. assertOnTagged,
  273. }