content-function.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import * as path from 'node:path';
  2. import {fileURLToPath} from 'node:url';
  3. import {inspect} from 'node:util';
  4. import chroma from 'chroma-js';
  5. import {getColors} from '#colors';
  6. import {quickLoadContentDependencies} from '#content-dependencies';
  7. import {quickEvaluate} from '#content-function';
  8. import * as html from '#html';
  9. import {processLanguageFile} from '#language';
  10. import {empty, showAggregate} from '#sugar';
  11. import {generateURLs, thumb, urlSpec} from '#urls';
  12. import mock from './generic-mock.js';
  13. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  14. export function testContentFunctions(t, message, fn) {
  15. const urls = generateURLs(urlSpec);
  16. t.test(message, async t => {
  17. let loadedContentDependencies;
  18. const language = await processLanguageFile('./src/strings-default.json');
  19. const mocks = [];
  20. const evaluate = ({
  21. from = 'localized.home',
  22. contentDependencies = {},
  23. extraDependencies = {},
  24. ...opts
  25. }) => {
  26. if (!loadedContentDependencies) {
  27. throw new Error(`Await .load() before performing tests`);
  28. }
  29. const {to} = urls.from(from);
  30. return cleanCatchAggregate(() => {
  31. return quickEvaluate({
  32. ...opts,
  33. contentDependencies: {
  34. ...contentDependencies,
  35. ...loadedContentDependencies,
  36. },
  37. extraDependencies: {
  38. html,
  39. language,
  40. thumb,
  41. to,
  42. urls,
  43. appendIndexHTML: false,
  44. getColors: c => getColors(c, {chroma}),
  45. ...extraDependencies,
  46. },
  47. });
  48. });
  49. };
  50. evaluate.load = async (opts) => {
  51. if (loadedContentDependencies) {
  52. throw new Error(`Already loaded!`);
  53. }
  54. loadedContentDependencies = await asyncCleanCatchAggregate(() =>
  55. quickLoadContentDependencies({
  56. logging: false,
  57. ...opts,
  58. }));
  59. };
  60. evaluate.snapshot = (...args) => {
  61. if (!loadedContentDependencies) {
  62. throw new Error(`Await .load() before performing tests`);
  63. }
  64. const [description, opts] =
  65. (typeof args[0] === 'string'
  66. ? args
  67. : ['output', ...args]);
  68. let result = evaluate(opts);
  69. if (opts.multiple) {
  70. result = result.map(item => item.toString()).join('\n');
  71. } else {
  72. result = result.toString();
  73. }
  74. t.matchSnapshot(result, description);
  75. };
  76. evaluate.stubTemplate = name =>
  77. // Creates a particularly permissable template, allowing any slot values
  78. // to be stored and just outputting the contents of those slots as-are.
  79. _stubTemplate(name, false);
  80. evaluate.stubContentFunction = name =>
  81. // Like stubTemplate, but instead of a template directly, returns
  82. // an object describing a content function - suitable for passing
  83. // into evaluate.mock.
  84. _stubTemplate(name, true);
  85. const _stubTemplate = (name, mockContentFunction) => {
  86. const inspectNicely = (value, opts = {}) =>
  87. inspect(value, {
  88. ...opts,
  89. colors: false,
  90. sort: true,
  91. });
  92. const makeTemplate = formatContentFn =>
  93. new (class extends html.Template {
  94. #slotValues = {};
  95. constructor() {
  96. super({
  97. content: () => this.#getContent(formatContentFn),
  98. });
  99. }
  100. setSlots(slotNamesToValues) {
  101. Object.assign(this.#slotValues, slotNamesToValues);
  102. }
  103. setSlot(slotName, slotValue) {
  104. this.#slotValues[slotName] = slotValue;
  105. }
  106. #getContent(formatContentFn) {
  107. const toInspect =
  108. Object.fromEntries(
  109. Object.entries(this.#slotValues)
  110. .filter(([key, value]) => value !== null));
  111. const inspected =
  112. inspectNicely(toInspect, {
  113. breakLength: Infinity,
  114. compact: true,
  115. depth: Infinity,
  116. });
  117. return formatContentFn(inspected); `${name}: ${inspected}`;
  118. }
  119. });
  120. if (mockContentFunction) {
  121. return {
  122. data: (...args) => ({args}),
  123. generate: (data) =>
  124. makeTemplate(slots => {
  125. const argsLines =
  126. (empty(data.args)
  127. ? []
  128. : inspectNicely(data.args, {depth: Infinity})
  129. .split('\n'));
  130. return (`[mocked: ${name}` +
  131. (empty(data.args)
  132. ? ``
  133. : argsLines.length === 1
  134. ? `\n args: ${argsLines[0]}`
  135. : `\n args: ${argsLines[0]}\n` +
  136. argsLines.slice(1).join('\n').replace(/^/gm, ' ')) +
  137. (!empty(data.args)
  138. ? `\n `
  139. : ` - `) +
  140. (slots
  141. ? `slots: ${slots}]`
  142. : `slots: none]`));
  143. }),
  144. };
  145. } else {
  146. return makeTemplate(slots => `${name}: ${slots}`);
  147. }
  148. };
  149. evaluate.mock = (...opts) => {
  150. const {value, close} = mock(...opts);
  151. mocks.push({close});
  152. return value;
  153. };
  154. evaluate.mock.transformContent = {
  155. transformContent: {
  156. extraDependencies: ['html'],
  157. data: content => ({content}),
  158. slots: {mode: {type: 'string'}},
  159. generate: ({content}) => content,
  160. },
  161. };
  162. await fn(t, evaluate);
  163. if (!empty(mocks)) {
  164. cleanCatchAggregate(() => {
  165. const errors = [];
  166. for (const {close} of mocks) {
  167. try {
  168. close();
  169. } catch (error) {
  170. errors.push(error);
  171. }
  172. }
  173. if (!empty(errors)) {
  174. throw new AggregateError(errors, `Errors closing mocks`);
  175. }
  176. });
  177. }
  178. });
  179. }
  180. function printAggregate(error) {
  181. if (error instanceof AggregateError) {
  182. const message = showAggregate(error, {
  183. showTraces: true,
  184. print: false,
  185. pathToFileURL: f => path.relative(path.join(__dirname, '../..'), fileURLToPath(f)),
  186. });
  187. for (const line of message.split('\n')) {
  188. console.error(line);
  189. }
  190. }
  191. }
  192. function cleanCatchAggregate(fn) {
  193. try {
  194. return fn();
  195. } catch (error) {
  196. printAggregate(error);
  197. throw error;
  198. }
  199. }
  200. async function asyncCleanCatchAggregate(fn) {
  201. try {
  202. return await fn();
  203. } catch (error) {
  204. printAggregate(error);
  205. throw error;
  206. }
  207. }