NH_base

Base library usable any time.

当前为 2023-11-12 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/477290/1279237/NH_base.js

  1. // ==UserScript==
  2. // ==UserLibrary==
  3. // @name NH_base
  4. // @description Base library usable any time.
  5. // @version 29
  6. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html
  7. // @homepageURL https://github.com/nexushoratio/userscripts
  8. // @supportURL https://github.com/nexushoratio/userscripts/issues
  9. // @match https://www.example.com/*
  10. // ==/UserLibrary==
  11. // ==/UserScript==
  12.  
  13. window.NexusHoratio ??= {};
  14.  
  15. window.NexusHoratio.base = (function base() {
  16. 'use strict';
  17.  
  18. /** @type {number} - Bumped per release. */
  19. const version = 29;
  20.  
  21. /**
  22. * @type {number} - Constant (to make eslint's `no-magic-numbers` setting
  23. * happy).
  24. */
  25. const NOT_FOUND = -1;
  26.  
  27. /**
  28. * @typedef {NexusHoratioVersion}
  29. * @property {string} name - Library name.
  30. * @property {number} [minVersion=0] - Minimal version needed.
  31. */
  32.  
  33. /**
  34. * Ensures appropriate versions of NexusHoratio libraries are loaded.
  35. * @param {NexusHoratioVersion[]} versions - Versions required.
  36. * @returns {object} - Namespace with only ensured libraries present.
  37. * @throws {Error} - When requirements not met.
  38. */
  39. function ensure(versions) {
  40. let msg = 'Forgot to set a message';
  41. const namespace = {};
  42. for (const ver of versions) {
  43. const {
  44. name,
  45. minVersion = 0,
  46. } = ver;
  47. const lib = window.NexusHoratio[name];
  48. if (!lib) {
  49. msg = `Library "${name}" is not loaded`;
  50. throw new Error(`Not Loaded: ${msg}`);
  51. }
  52. if (minVersion > lib.version) {
  53. msg = `At least version ${minVersion} of library "${name}" ` +
  54. `required; version ${lib.version} present.`;
  55. throw new Error(`Min Version: ${msg}`);
  56. }
  57. namespace[name] = lib;
  58. }
  59. return namespace;
  60. }
  61.  
  62. const NH = ensure([{name: 'xunit', minVersion: 20}]);
  63.  
  64. /* eslint-disable require-jsdoc */
  65. class EnsureTestCase extends NH.xunit.TestCase {
  66.  
  67. testEmpty() {
  68. const actual = ensure([]);
  69. const expected = {};
  70. // TODO(#183): Better assertEqual()
  71. this.assertEqual(JSON.stringify(actual), JSON.stringify(expected));
  72. }
  73.  
  74. testNameOnly() {
  75. const ns = ensure([{name: 'base'}]);
  76. this.assertTrue(ns.base);
  77. }
  78.  
  79. testMinVersion() {
  80. this.assertRaisesRegExp(
  81. Error, /^Min Version:.*required.*present.$/u, () => {
  82. ensure([{name: 'base', minVersion: Number.MAX_VALUE}]);
  83. }
  84. );
  85. }
  86.  
  87. testMissing() {
  88. this.assertRaisesRegExp(
  89. Error, /^Not Loaded: /u, () => {
  90. ensure([{name: 'missing'}]);
  91. }
  92. );
  93. }
  94.  
  95. }
  96. /* eslint-enable */
  97.  
  98. NH.xunit.testing.testCases.push(EnsureTestCase);
  99.  
  100. /**
  101. * A Number like class that supports operations.
  102. *
  103. * For lack of any other standard, methods will be named like those in
  104. * Python's operator module.
  105. *
  106. * All operations should return `this` to allow chaining.
  107. *
  108. * The existence of the valueOf(), toString() and toJSON() methods will
  109. * probably allow this class to work in many situations through type
  110. * coercion.
  111. */
  112. class NumberOp {
  113.  
  114. /** @param {number} value - Initial value, parsed by Number(). */
  115. constructor(value) {
  116. this.assign(value);
  117. }
  118.  
  119. /** @returns {number} - Current value. */
  120. valueOf() {
  121. return this.#value;
  122. }
  123.  
  124. /** @returns {string} - Current value. */
  125. toString() {
  126. return `${this.valueOf()}`;
  127. }
  128.  
  129. /** @returns {number} - Current value. */
  130. toJSON() {
  131. return this.valueOf();
  132. }
  133.  
  134. /**
  135. * @param {number} value - Number to assign.
  136. * @returns {NumberOp} - This instance.
  137. */
  138. assign(value = 0) {
  139. this.#value = Number(value);
  140. return this;
  141. }
  142.  
  143. /**
  144. * @param {number} value - Number to add.
  145. * @returns {NumberOp} - This instance.
  146. */
  147. add(value) {
  148. this.#value += Number(value);
  149. return this;
  150. }
  151.  
  152. #value
  153.  
  154. }
  155.  
  156. /* eslint-disable no-magic-numbers */
  157. /* eslint-disable no-undefined */
  158. /* eslint-disable require-jsdoc */
  159. class NumberOpTestCase extends NH.xunit.TestCase {
  160.  
  161. testValueOf() {
  162. this.assertEqual(new NumberOp().valueOf(), 0, 'default');
  163. this.assertEqual(new NumberOp(null).valueOf(), 0, 'null');
  164. this.assertEqual(new NumberOp(undefined).valueOf(), 0, 'undefined');
  165. this.assertEqual(new NumberOp(42).valueOf(), 42, 'number');
  166. this.assertEqual(new NumberOp('52').valueOf(), 52, 'string');
  167. }
  168.  
  169. testToString() {
  170. this.assertEqual(new NumberOp(123).toString(), '123', 'number');
  171. this.assertEqual(new NumberOp(null).toString(), '0', 'null');
  172. this.assertEqual(new NumberOp(undefined).toString(), '0', 'undefined');
  173. }
  174.  
  175. testTemplateLiteral() {
  176. const val = new NumberOp(456);
  177. this.assertEqual(`abc${val}xyz`, 'abc456xyz');
  178. }
  179.  
  180. testBasicMath() {
  181. this.assertEqual(new NumberOp(124) + 6, 130, 'NO + x');
  182. this.assertEqual(3 + new NumberOp(5), 8, 'x + NO');
  183. }
  184.  
  185. testStringManipulation() {
  186. const a = 'abc';
  187. const x = 'xyz';
  188. const n = new NumberOp('654');
  189.  
  190. this.assertEqual(a + n, 'abc654', 's + NO');
  191. this.assertEqual(n + x, '654xyz', 'NO + s');
  192. }
  193.  
  194. testAssignOp() {
  195. const n = new NumberOp(123);
  196. n.assign(42);
  197. this.assertEqual(n.valueOf(), 42, 'number');
  198.  
  199. n.assign(null);
  200. this.assertEqual(n.valueOf(), 0, 'null');
  201.  
  202. n.assign(789);
  203. this.assertEqual(n.valueOf(), 789, 'number, reset');
  204.  
  205. n.assign(undefined);
  206. this.assertEqual(n.valueOf(), 0, 'undefined');
  207. }
  208.  
  209. testAddOp() {
  210. this.assertEqual(new NumberOp(3).add(1)
  211. .valueOf(), 4,
  212. 'number');
  213. this.assertEqual(new NumberOp(1).add('5')
  214. .valueOf(), 6,
  215. 'string');
  216. this.assertEqual(new NumberOp(3).add(new NumberOp(8))
  217. .valueOf(), 11,
  218. 'NO.add(NO)');
  219. this.assertEqual(new NumberOp(9).add(-16)
  220. .valueOf(), -7,
  221. 'negative');
  222. }
  223.  
  224. testChaining() {
  225. this.assertEqual(new NumberOp().add(1)
  226. .add(2)
  227. .add('3')
  228. .valueOf(), 6,
  229. 'adds');
  230. this.assertEqual(new NumberOp(3).assign(40)
  231. .add(2)
  232. .valueOf(), 42,
  233. 'mixed');
  234. }
  235.  
  236. }
  237. /* eslint-enable */
  238.  
  239. NH.xunit.testing.testCases.push(NumberOpTestCase);
  240.  
  241. /**
  242. * Subclass of {Map} similar to Python's defaultdict.
  243. *
  244. * First argument is a factory function that will create a new default value
  245. * for the key if not already present in the container.
  246. *
  247. * The factory function may take arguments. If `.get()` is called with
  248. * extra arguments, those will be passed to the factory if it needed.
  249. */
  250. class DefaultMap extends Map {
  251.  
  252. /**
  253. * @param {function(...args) : *} factory - Function that creates a new
  254. * default value if a requested key is not present.
  255. * @param {Iterable} [iterable] - Passed to {Map} super().
  256. */
  257. constructor(factory, iterable) {
  258. if (!(factory instanceof Function)) {
  259. throw new TypeError('The factory argument MUST be of ' +
  260. `type Function, not ${typeof factory}.`);
  261. }
  262. super(iterable);
  263.  
  264. this.#factory = factory;
  265. }
  266.  
  267. /**
  268. * Enhanced version of `Map.prototype.get()`.
  269. * @param {*} key - The key of the element to return from this instance.
  270. * @param {...*} args - Extra arguments passed tot he factory function if
  271. * it is called.
  272. * @returns {*} - The value associated with the key, perhaps newly
  273. * created.
  274. */
  275. get(key, ...args) {
  276. if (!this.has(key)) {
  277. this.set(key, this.#factory(...args));
  278. }
  279.  
  280. return super.get(key);
  281. }
  282.  
  283. #factory
  284.  
  285. }
  286.  
  287. /* eslint-disable require-jsdoc */
  288. /* eslint-disable no-new */
  289. /* eslint-disable no-magic-numbers */
  290. class DefaultMapTestCase extends NH.xunit.TestCase {
  291.  
  292. testNoFactory() {
  293. this.assertRaisesRegExp(TypeError, /MUST.*not undefined/u, () => {
  294. new DefaultMap();
  295. });
  296. }
  297.  
  298. testBadFactory() {
  299. this.assertRaisesRegExp(TypeError, /MUST.*not string/u, () => {
  300. new DefaultMap('a');
  301. });
  302. }
  303.  
  304. testFactorWithArgs() {
  305. // Assemble
  306. const dummy = new DefaultMap(x => new NumberOp(x));
  307. this.defaultEqual = this.equalValueOf;
  308.  
  309. // Act
  310. dummy.get('a');
  311. dummy.get('b', 5);
  312.  
  313. // Assert
  314. this.assertEqual(Array.from(dummy.entries()),
  315. [['a', 0], ['b', 5]]);
  316. }
  317.  
  318. testWithIterable() {
  319. // Assemble
  320. const dummy = new DefaultMap(Number, [[1, 'one'], [2, 'two']]);
  321.  
  322. // Act
  323. dummy.set(3, ['a', 'b']);
  324. dummy.get(4);
  325.  
  326. // Assert
  327. this.assertEqual(Array.from(dummy.entries()),
  328. [[1, 'one'], [2, 'two'], [3, ['a', 'b']], [4, 0]]);
  329. }
  330.  
  331. testCounter() {
  332. // Assemble
  333. const dummy = new DefaultMap(() => new NumberOp());
  334. this.defaultEqual = this.equalValueOf;
  335.  
  336. // Act
  337. dummy.get('a');
  338. dummy.get('b').add(1);
  339. dummy.get('b').add(1);
  340. dummy.get('c');
  341. dummy.get(4).add(1);
  342.  
  343. // Assert
  344. this.assertEqual(Array.from(dummy.entries()),
  345. [['a', 0], ['b', 2], ['c', 0], [4, 1]]);
  346. }
  347.  
  348. testArray() {
  349. // Assemble
  350. const dummy = new DefaultMap(Array);
  351.  
  352. // Act
  353. dummy.get('a').push(1, 2, 3);
  354. dummy.get('b').push(4, 5, 6);
  355. dummy.get('a').push('one', 'two', 'three');
  356.  
  357. // Assert
  358. this.assertEqual(Array.from(dummy.entries()),
  359. [['a', [1, 2, 3, 'one', 'two', 'three']], ['b', [4, 5, 6]]]);
  360. }
  361.  
  362. }
  363. /* eslint-enable */
  364.  
  365. NH.xunit.testing.testCases.push(DefaultMapTestCase);
  366.  
  367. /**
  368. * Fancy-ish log messages (likely over engineered).
  369. *
  370. * Console nested message groups can be started and ended using the special
  371. * method pairs, {@link Logger#entered}/{@link Logger#leaving} and {@link
  372. * Logger#starting}/{@link Logger#finished}. By default, the former are
  373. * opened and the latter collapsed (documented here as closed).
  374. *
  375. * Individual Loggers can be enabled/disabled by setting the {@link
  376. * Logger##Config.enabled} boolean property.
  377. *
  378. * Each Logger will have also have a collection of {@link Logger##Group}s
  379. * associated with it. These groups can have one of three modes: "opened",
  380. * "closed", "silenced". The first two correspond to the browser console
  381. * nested message groups. The intro and outro type of methods will handle
  382. * the nesting. If a group is set as "silenced", no messages will be sent
  383. * to the console.
  384. *
  385. * All Logger instances register a configuration with a singleton Map keyed
  386. * by the instance name. If more than one instance is created with the same
  387. * name, they all share the same configuration.
  388. *
  389. * Configurations can be exported as a plain object and reimported using the
  390. * {@link Logger.configs} property. The object could be saved via the
  391. * userscript script manager. Depending on which one, it may have to be
  392. * processed with the JSON.{stringify,parse} functions. Once exported, the
  393. * object may be modified. This could be used to provide a UI to edit the
  394. * object, though no schema is provided.
  395. *
  396. * Some values may be of interest to users for help in debugging a script.
  397. *
  398. * The {callCount} value is how many times a logger would have been used for
  399. * messages, even if the logger is disabled. Similarly, each group
  400. * associated with a logger also has a {callCount}. These values can be
  401. * used to determine which loggers and groups generate a lot of messages and
  402. * could be disabled or silenced.
  403. *
  404. * The {sequence} value is a rough indicator of how recently a logger or
  405. * group was actually used. It is purposely not a timestamp, but rather,
  406. * more closely associated with how often configurations are restored,
  407. * e.g. during web page reloads. A low sequence number, relative to the
  408. * others, may indicate a logger was renamed, groups removed, or simply
  409. * parts of an application that have not been visited recently. Depending
  410. * on the situation, the could clean up old configs, or explore other parts
  411. * of the script.
  412. *
  413. * @example
  414. * const log = new Logger('Bob');
  415. * foo(x) {
  416. * const me = 'foo';
  417. * log.entered(me, x);
  418. * ... do stuff ...
  419. * log.starting('loop');
  420. * for (const item in items) {
  421. * log.log(`Processing ${item}`);
  422. * ...
  423. * }
  424. * log.finished('loop');
  425. * log.leaving(me, y);
  426. * return y;
  427. * }
  428. *
  429. * Logger.config('Bob').enabled = true;
  430. * Logger.config('Bob').group('foo').mode = 'silenced');
  431. *
  432. * GM.setValue('Logger', Logger.configs);
  433. * ... restart browser ...
  434. * Logger.configs = GM.getValue('Logger');
  435. */
  436. class Logger {
  437.  
  438. /** @param {string} name - Name for this logger. */
  439. constructor(name) {
  440. this.#name = name;
  441. this.#config = Logger.config(name);
  442. Logger.#loggers.get(this.#name).push(new WeakRef(this));
  443. }
  444.  
  445. static sequence = 1;
  446.  
  447. /** @type {object} - Logger configurations. */
  448. static get configs() {
  449. return Logger.#toPojo();
  450. }
  451.  
  452. /** @param {object} val - Logger configurations. */
  453. static set configs(val) {
  454. Logger.#fromPojo(val);
  455. Logger.#resetLoggerConfigs();
  456. }
  457.  
  458. /** @type {string[]} - Names of known loggers. */
  459. static get loggers() {
  460. return Array.from(this.#loggers.keys());
  461. }
  462.  
  463. /**
  464. * Get configuration of a specific Logger.
  465. * @param {string} name - Logger configuration to get.
  466. * @returns {Logger.Config} - Current config for that Logger.
  467. */
  468. static config(name) {
  469. return this.#configs.get(name);
  470. }
  471.  
  472. /** Reset all configs to an empty state. */
  473. static resetConfigs() {
  474. this.#configs.clear();
  475. this.sequence = 1;
  476. }
  477.  
  478. /** Clear the console. */
  479. static clear() {
  480. this.#clear();
  481. }
  482.  
  483. /** @type {boolean} - Whether logging is currently enabled. */
  484. get enabled() {
  485. return this.#config.enabled;
  486. }
  487.  
  488. /** @type {boolean} - Indicates whether messages include a stack trace. */
  489. get includeStackTrace() {
  490. return this.#config.includeStackTrace;
  491. }
  492.  
  493. /** @type {string} - Name for this logger. */
  494. get name() {
  495. return this.#name;
  496. }
  497.  
  498. /** @type {boolean} - Indicates whether current group is silenced. */
  499. get silenced() {
  500. let ret = false;
  501. const group = this.#groupStack.at(-1);
  502. if (group) {
  503. const mode = this.#config.group(group).mode;
  504. ret = mode === Logger.#GroupMode.Silenced;
  505. }
  506. return ret;
  507. }
  508.  
  509. /**
  510. * Log a specific message.
  511. * @param {string} msg - Message to send to console.debug.
  512. * @param {...*} rest - Arbitrary items to pass to console.debug.
  513. */
  514. log(msg, ...rest) {
  515. this.#log(msg, ...rest);
  516. }
  517.  
  518. /**
  519. * Indicate entered a specific group.
  520. * @param {string} group - Group that was entered.
  521. * @param {...*} rest - Arbitrary items to pass to console.debug.
  522. */
  523. entered(group, ...rest) {
  524. this.#intro(group, Logger.#GroupMode.Opened, ...rest);
  525. }
  526.  
  527. /**
  528. * Indicate leaving a specific group.
  529. * @param {string} group - Group leaving.
  530. * @param {...*} rest - Arbitrary items to pass to console.debug.
  531. */
  532. leaving(group, ...rest) {
  533. this.#outro(group, ...rest);
  534. }
  535.  
  536. /**
  537. * Indicate starting a specific collapsed group.
  538. * @param {string} group - Group that is being started.
  539. * @param {...*} rest - Arbitrary items to pass to console.debug.
  540. */
  541. starting(group, ...rest) {
  542. this.#intro(group, Logger.#GroupMode.Closed, ...rest);
  543. }
  544.  
  545. /**
  546. * Indicate finishe a specific collapsed group.
  547. * @param {string} group - Group that was entered.
  548. * @param {...*} rest - Arbitrary items to pass to console.debug.
  549. */
  550. finished(group, ...rest) {
  551. this.#outro(group, ...rest);
  552. }
  553.  
  554. static #configs = new DefaultMap(() => new Logger.#Config());
  555. static #loggers = new DefaultMap(Array);
  556.  
  557. /**
  558. * Set Logger configs from a plain object.
  559. * @param {object} pojo - Created by {Logger.#toPojo}.
  560. */
  561. static #fromPojo = (pojo) => {
  562. if (pojo && pojo.type === 'LoggerConfigs') {
  563. this.resetConfigs();
  564. for (const [k, v] of Object.entries(pojo.entries)) {
  565. this.#configs.get(k).fromPojo(v);
  566. }
  567. Logger.sequence += 1;
  568. }
  569. }
  570.  
  571. /** @returns {object} - Logger.#configs as a plain object. */
  572. static #toPojo = () => {
  573. const pojo = {
  574. type: 'LoggerConfigs',
  575. entries: {},
  576. };
  577. for (const [k, v] of this.#configs.entries()) {
  578. pojo.entries[k] = v.toPojo();
  579. }
  580. return pojo;
  581. }
  582.  
  583. static #resetLoggerConfigs = () => {
  584. for (const [key, loggerArrays] of this.#loggers) {
  585. for (const loggerRef of loggerArrays) {
  586. const logger = loggerRef.deref();
  587. if (logger) {
  588. logger.#config = Logger.config(key);
  589. }
  590. }
  591. }
  592. }
  593.  
  594. /* eslint-disable no-console */
  595. static #clear = () => {
  596. console.clear();
  597. }
  598.  
  599. #config
  600. #groupStack = [];
  601. #name
  602.  
  603. /**
  604. * Log a specific message.
  605. * @param {string} msg - Message to send to console.debug.
  606. * @param {...*} rest - Arbitrary items to pass to console.debug.
  607. */
  608. #log = (msg, ...rest) => {
  609. const group = this.#groupStack.at(-1);
  610. this.#config.used(group);
  611. if (this.enabled && !this.silenced) {
  612. if (this.includeStackTrace) {
  613. console.groupCollapsed(`${this.name} call stack`);
  614. console.includeStackTrace();
  615. console.groupEnd();
  616. }
  617. console.debug(`${this.name}: ${msg}`, ...rest);
  618. }
  619. }
  620.  
  621. /**
  622. * Introduces a specific group.
  623. * @param {string} group - Group being created.
  624. * @param {Logger.#GroupMode} defaultMode - Mode to use if new.
  625. * @param {...*} rest - Arbitrary items to pass to console.debug.
  626. */
  627. #intro = (group, defaultMode, ...rest) => {
  628. this.#groupStack.push(group);
  629. const mode = this.#config.group(group, defaultMode).mode;
  630.  
  631. if (this.enabled && mode !== Logger.#GroupMode.Silenced) {
  632. console[mode.func](`${this.name}: ${group}`);
  633. }
  634.  
  635. if (rest.length) {
  636. const msg = `${mode.greeting} ${group} with`;
  637. this.log(msg, ...rest);
  638. }
  639. }
  640.  
  641. /**
  642. * Concludes a specific group.
  643. * @param {string} group - Group leaving.
  644. * @param {...*} rest - Arbitrary items to pass to console.debug.
  645. */
  646. #outro = (group, ...rest) => {
  647. const mode = this.#config.group(group).mode;
  648.  
  649. let msg = `${mode.farewell} ${group}`;
  650. if (rest.length) {
  651. msg += ' with:';
  652. }
  653. this.log(msg, ...rest);
  654.  
  655. const lastGroup = this.#groupStack.pop();
  656. if (group !== lastGroup) {
  657. console.error(`${this.name}: Group mismatch! Passed ` +
  658. `"${group}", expected to see "${lastGroup}"`);
  659. }
  660.  
  661. if (this.enabled && mode !== Logger.#GroupMode.Silenced) {
  662. console.groupEnd();
  663. }
  664. }
  665. /* eslint-enable */
  666.  
  667. static #Config = class {
  668.  
  669. sequence = 0;
  670.  
  671. /** @type {NumberOp} */
  672. get callCount() {
  673. return this.#callCount;
  674. }
  675.  
  676. /** @type {boolean} - Whether logging is currently enabled. */
  677. get enabled() {
  678. return this.#enabled;
  679. }
  680.  
  681. /** @param {boolean} val - Set whether logging is currently enabled. */
  682. set enabled(val) {
  683. this.#enabled = Boolean(val);
  684. }
  685.  
  686. /** @type {Map<string,Logger.#Group>} - Per group settings. */
  687. get groups() {
  688. return this.#groups;
  689. }
  690.  
  691. /** @type {boolean} - Whether messages include a stack trace. */
  692. get includeStackTrace() {
  693. return this.#includeStackTrace;
  694. }
  695.  
  696. /** @param {boolean} val - Set inclusion of stack traces. */
  697. set includeStackTrace(val) {
  698. this.#includeStackTrace = Boolean(val);
  699. }
  700.  
  701. /**
  702. * @param {string} name - Name of the group to get.
  703. * @param {Logger.#GroupMode} mode - Default mode if not seen before.
  704. * @returns {Logger.#Group} - Requested group, perhaps newly made.
  705. */
  706. group(name, mode) {
  707. const sanitizedName = name ?? 'null';
  708. const defaultMode = mode ?? 'opened';
  709. return this.#groups.get(sanitizedName, defaultMode);
  710. }
  711.  
  712. /**
  713. * Capture that the associated Logger was used.
  714. * @param {string} name - Which group was used.
  715. */
  716. used(name) {
  717. const grp = this.group(name);
  718.  
  719. this.callCount.add(1);
  720. this.sequence = Logger.sequence;
  721.  
  722. grp.callCount.add(1);
  723. grp.sequence = Logger.sequence;
  724. }
  725.  
  726. /** @returns {object} - Config as a plain object. */
  727. toPojo() {
  728. const pojo = {
  729. callCount: this.callCount.valueOf(),
  730. sequence: this.sequence,
  731. enabled: this.enabled,
  732. includeStackTrace: this.includeStackTrace,
  733. groups: {},
  734. };
  735.  
  736. for (const [k, v] of this.groups) {
  737. pojo.groups[k] = v.toPojo();
  738. }
  739.  
  740. return pojo;
  741. }
  742.  
  743. /** @param {object} pojo - Config as a plain object. */
  744. fromPojo(pojo) {
  745. if (Object.hasOwn(pojo, 'callCount')) {
  746. this.callCount.assign(pojo.callCount);
  747. }
  748. if (Object.hasOwn(pojo, 'sequence')) {
  749. this.sequence = pojo.sequence;
  750. Logger.sequence = Math.max(Logger.sequence, this.sequence);
  751. }
  752. if (Object.hasOwn(pojo, 'enabled')) {
  753. this.enabled = pojo.enabled;
  754. }
  755. if (Object.hasOwn(pojo, 'includeStackTrace')) {
  756. this.includeStackTrace = pojo.includeStackTrace;
  757. }
  758. if (Object.hasOwn(pojo, 'groups')) {
  759. for (const [k, v] of Object.entries(pojo.groups)) {
  760. const gm = Logger.#GroupMode.byName(v.mode);
  761. if (gm) {
  762. this.group(k).fromPojo(v);
  763. }
  764. }
  765. }
  766. }
  767.  
  768. #callCount = new NumberOp();
  769. #enabled = false;
  770. #groups = new DefaultMap(x => new Logger.#Group(x));
  771. #includeStackTrace = false;
  772.  
  773. }
  774.  
  775. static #Group = class {
  776.  
  777. /** @param {Logger.#GroupMode} mode - Initial mode for this group. */
  778. constructor(mode) {
  779. this.mode = mode;
  780. this.sequence = 0;
  781. }
  782.  
  783. /** @type {NumberOp} */
  784. get callCount() {
  785. return this.#callCount;
  786. }
  787.  
  788. /** @type {Logger.#GroupMode} */
  789. get mode() {
  790. return this.#mode;
  791. }
  792.  
  793. /** @param {Logger.#GroupMode} val - Mode to set this group. */
  794. set mode(val) {
  795. let newVal = val;
  796. if (!(newVal instanceof Logger.#GroupMode)) {
  797. newVal = Logger.#GroupMode.byName(newVal);
  798. }
  799. if (newVal) {
  800. this.#mode = newVal;
  801. }
  802. }
  803.  
  804. /** @returns {object} - Group as a plain object. */
  805. toPojo() {
  806. const pojo = {
  807. mode: this.mode.name,
  808. callCount: this.callCount.valueOf(),
  809. sequence: this.sequence,
  810. };
  811.  
  812. return pojo;
  813. }
  814.  
  815. /** @param {object} pojo - Group as a plain object. */
  816. fromPojo(pojo) {
  817. this.mode = pojo.mode;
  818. this.callCount.assign(pojo.callCount);
  819. this.sequence = pojo.sequence ?? 0;
  820. Logger.sequence = Math.max(Logger.sequence, this.sequence);
  821. }
  822.  
  823. #callCount = new NumberOp();
  824. #mode
  825.  
  826. }
  827.  
  828. /** Enum/helper for Logger groups. */
  829. static #GroupMode = class {
  830.  
  831. /**
  832. * @param {string} name - Mode name.
  833. * @param {string} [greeting] - Greeting when opening group.
  834. * @param {string} [farewell] - Salutation when closing group.
  835. * @param {string} [func] - console.func to use for opening group.
  836. */
  837. constructor(name, greeting, farewell, func) { // eslint-disable-line max-params
  838. this.#farewell = farewell;
  839. this.#func = func;
  840. this.#greeting = greeting;
  841. this.#name = name;
  842.  
  843. Logger.#GroupMode.#known.set(name, this);
  844.  
  845. Object.freeze(this);
  846. }
  847.  
  848. /**
  849. * Find GroupMode by name.
  850. * @param {string} name - Mode name.
  851. * @returns {GroupMode} - Mode, if found.
  852. */
  853. static byName(name) {
  854. return this.#known.get(name);
  855. }
  856.  
  857. /** @type {string} - Farewell when closing group. */
  858. get farewell() {
  859. return this.#farewell;
  860. }
  861.  
  862. /** @type {string} - console.func to use for opening group. */
  863. get func() {
  864. return this.#func;
  865. }
  866.  
  867. /** @type {string} - Greeting when opening group. */
  868. get greeting() {
  869. return this.#greeting;
  870. }
  871.  
  872. /** @type {string} - Mode name. */
  873. get name() {
  874. return this.#name;
  875. }
  876.  
  877. static #known = new Map();
  878.  
  879. #farewell
  880. #func
  881. #greeting
  882. #name
  883.  
  884. }
  885.  
  886. static {
  887. Logger.#GroupMode.Silenced = new Logger.#GroupMode('silenced');
  888. Logger.#GroupMode.Opened = new Logger.#GroupMode(
  889. 'opened', 'Entered', 'Leaving', 'group'
  890. );
  891. Logger.#GroupMode.Closed = new Logger.#GroupMode(
  892. 'closed', 'Starting', 'Finished', 'groupCollapsed'
  893. );
  894.  
  895. Object.freeze(Logger.#GroupMode);
  896. }
  897.  
  898. // JavaScript does not support friend type access, so embedded these
  899. // tests.
  900. static #testGroupMode
  901.  
  902. static {
  903.  
  904. /* eslint-disable max-lines-per-function */
  905. /** Test case. */
  906. Logger.#testGroupMode = () => {
  907. const tests = new Map();
  908.  
  909. tests.set('classIsFrozen', {test: () => {
  910. try {
  911. Logger.#GroupMode.Bob = {};
  912. } catch (e) {
  913. if (e instanceof TypeError) {
  914. return 'cold';
  915. }
  916. }
  917. return 'hot';
  918. },
  919. expected: 'cold'});
  920.  
  921. tests.set('instanceIsFrozen', {test: () => {
  922. try {
  923. Logger.#GroupMode.Silenced.newProp = 'data';
  924. } catch (e) {
  925. if (e.message.includes('newProp')) {
  926. return 'cold';
  927. }
  928. return 'exception message missing newProp';
  929. }
  930. return 'hot';
  931. },
  932. expected: 'cold'});
  933.  
  934. tests.set('byName', {test: () => {
  935. const gm = Logger.#GroupMode.byName('closed');
  936. return gm;
  937. },
  938. expected: Logger.#GroupMode.Closed});
  939.  
  940. tests.set('byNameBad', {test: () => {
  941. const gm = Logger.#GroupMode.byName('bob');
  942. if (!gm) {
  943. return 'expected-missing-bob';
  944. }
  945. return 'confused-bob';
  946. },
  947. expected: 'expected-missing-bob'});
  948.  
  949. for (const [name, {test, expected}] of tests) {
  950. const actual = test();
  951. const passed = actual === expected;
  952. const msg = `t:${name} e:${expected} a:${actual} p:${passed}`;
  953. NH.xunit.testing.log.log(msg);
  954. if (!passed) {
  955. throw new Error(msg);
  956. }
  957. }
  958.  
  959. };
  960. /* eslint-enable */
  961.  
  962. Logger.#testGroupMode.testName = 'testLoggerGroupMode';
  963.  
  964. NH.xunit.testing.funcs.push(Logger.#testGroupMode);
  965. }
  966.  
  967. }
  968.  
  969. /* eslint-disable max-lines-per-function */
  970. /* eslint-disable max-statements */
  971. /* eslint-disable no-magic-numbers */
  972. /** Test case. */
  973. function testLogger() {
  974. const tests = new Map();
  975.  
  976. tests.set('testReset', {test: () => {
  977. Logger.config('testReset').enabled = true;
  978. Logger.resetConfigs();
  979. return JSON.stringify(Logger.configs.entries);
  980. },
  981. expected: '{}'});
  982.  
  983. tests.set('defaultDisabled', {test: () => {
  984. const config = Logger.config('defaultDisabled');
  985. return config.enabled;
  986. },
  987. expected: false});
  988.  
  989. tests.set('defaultNoStackTraces', {test: () => {
  990. const config = Logger.config('defaultNoStackTraces');
  991. return config.includeStackTrace;
  992. },
  993. expected: false});
  994.  
  995. tests.set('defaultNoGroups', {test: () => {
  996. const config = Logger.config('defaultNoGroups');
  997. return config.groups.size;
  998. },
  999. expected: 0});
  1000.  
  1001. tests.set('openedGroup', {test: () => {
  1002. const logger = new Logger('openedGroup');
  1003. logger.entered('ent');
  1004. return Logger.config('openedGroup').groups.get('ent').mode.name;
  1005. },
  1006. expected: 'opened'});
  1007.  
  1008. tests.set('closedGroup', {test: () => {
  1009. const logger = new Logger('closedGroup');
  1010. logger.starting('start');
  1011. return Logger.config('closedGroup').groups.get('start').mode.name;
  1012. },
  1013. expected: 'closed'});
  1014.  
  1015. tests.set('countsCollected', {test: () => {
  1016. const me = 'countsCollected';
  1017. Logger.sequence = 10;
  1018. const logger = new Logger(me);
  1019. const results = [];
  1020.  
  1021. // Results in counts
  1022. logger.log('one');
  1023. logger.log('two');
  1024.  
  1025. // No count because no message logged
  1026. logger.entered('ent1');
  1027.  
  1028. // The extra causes a log message
  1029. logger.entered('ent2', 'extra');
  1030.  
  1031. // Count in group
  1032. logger.log('three');
  1033.  
  1034. // Outros cause logs
  1035. logger.leaving('ent2');
  1036. logger.leaving('ent1', 'extra');
  1037.  
  1038. results.push(Logger.config(me).callCount, Logger.config(me).sequence);
  1039. for (const [name, group] of Logger.config(me).groups) {
  1040. results.push(name, group.callCount, group.sequence);
  1041. }
  1042.  
  1043. return JSON.stringify(results);
  1044. },
  1045. expected: '[6,10,"null",2,10,"ent1",1,10,"ent2",3,10]'});
  1046.  
  1047. tests.set('expectMismatchedGroup', {test: () => {
  1048. // This test requires manual verification that an error message was
  1049. // logged:
  1050. // <name>: Group mismatch! Passed "two", expected to see "one"
  1051. const logger = new Logger('expectMismatchedGroup');
  1052. logger.entered('one');
  1053. logger.leaving('two');
  1054. return 'x';
  1055. },
  1056. expected: 'x'});
  1057.  
  1058. tests.set('updateGroupByString', {test: () => {
  1059. const logger = new Logger('updateGroupByString');
  1060. logger.entered('one');
  1061. Logger.config('updateGroupByString').group('one').mode = 'silenced';
  1062. return Logger.config('updateGroupByString').group('one').mode.name;
  1063. },
  1064. expected: 'silenced'});
  1065.  
  1066. tests.set('restoreConfigsTopLevel', {test: () => {
  1067. const me = 'restoreConfigsTopLevel';
  1068. const results = [];
  1069.  
  1070. Logger.config(me).includeStackTrace = true;
  1071. const logger = new Logger(me);
  1072. logger.log('once');
  1073. results.push(Logger.config(me).includeStackTrace);
  1074. results.push(Logger.config(me).callCount);
  1075. const oldConfigs = Logger.configs;
  1076.  
  1077. Logger.resetConfigs();
  1078. results.push(Logger.config(me).includeStackTrace);
  1079. results.push(Logger.config(me).callCount);
  1080.  
  1081. // Bob is not in oldConfigs, so should go back to the default (false)
  1082. // after restoring the configs.
  1083. Logger.config('Bob').enabled = true;
  1084. Logger.configs = oldConfigs;
  1085. results.push(Logger.config(me).includeStackTrace);
  1086. results.push(Logger.config(me).callCount);
  1087. results.push(Logger.config('Bob').enabled);
  1088.  
  1089. return JSON.stringify(results);
  1090. },
  1091. expected: '[true,1,false,0,true,1,false]'});
  1092.  
  1093. tests.set('restoreConfigsGroups', {test: () => {
  1094. const me = 'restoreConfigsGroups';
  1095. const results = [];
  1096.  
  1097. const logger = new Logger(me);
  1098. logger.starting('ent');
  1099. logger.finished('ent');
  1100. results.push(Logger.config(me).group('ent').mode.name);
  1101. results.push(Logger.config(me).group('ent').callCount);
  1102.  
  1103. const saved = Logger.configs;
  1104. Logger.resetConfigs();
  1105. results.push(Logger.config(me).group('ent').mode.name);
  1106. results.push(Logger.config(me).group('ent').callCount);
  1107.  
  1108. Logger.configs = saved;
  1109. results.push(Logger.config(me).group('ent').mode.name);
  1110. results.push(Logger.config(me).group('ent').callCount);
  1111.  
  1112. return JSON.stringify(results);
  1113. },
  1114. expected: '["closed",1,"opened",0,"closed",1]'});
  1115.  
  1116. tests.set('sequenceIncreases', {test: () => {
  1117. const me = 'sequenceIncreases';
  1118. const groupName = 'ent';
  1119. Logger.sequence = 23;
  1120.  
  1121. const logger = new Logger(me);
  1122. logger.starting(groupName);
  1123. logger.finished(groupName);
  1124.  
  1125. const saved = Logger.configs;
  1126. saved.entries[me].sequence = 34;
  1127. saved.entries[me].groups[groupName].sequence = 42;
  1128.  
  1129. Logger.configs = saved;
  1130. return Logger.sequence > 42;
  1131. },
  1132. expected: true});
  1133.  
  1134. const savedConfigs = Logger.configs;
  1135. for (const [name, {test, expected}] of tests) {
  1136. Logger.resetConfigs();
  1137. const actual = test();
  1138. const passed = actual === expected;
  1139. const msg = `t:${name} e:${expected} a:${actual} p:${passed}`;
  1140. NH.xunit.testing.log.log(msg);
  1141. if (!passed) {
  1142. throw new Error(msg);
  1143. }
  1144. }
  1145. Logger.configs = savedConfigs;
  1146.  
  1147. }
  1148. /* eslint-enable */
  1149.  
  1150. NH.xunit.testing.funcs.push(testLogger);
  1151.  
  1152. /**
  1153. * Execute function tests.
  1154. * @returns {boolean} - Success status.
  1155. */
  1156. function doFunctionTests() {
  1157. const me = 'Running function tests';
  1158. NH.xunit.testing.log.entered(me);
  1159. let savedConfigs = null;
  1160. let passed = true;
  1161.  
  1162. for (const test of NH.xunit.testing.funcs) {
  1163. const name = test.name || test.testName;
  1164. NH.xunit.testing.log.starting(name);
  1165. savedConfigs = Logger.configs;
  1166. try {
  1167. test();
  1168. } catch (e) {
  1169. NH.xunit.testing.log.log('caught exception:', e);
  1170. passed = false;
  1171. }
  1172. Logger.configs = savedConfigs;
  1173. NH.xunit.testing.log.finished(name);
  1174. // Bail after the first failure.
  1175. if (!passed) {
  1176. break;
  1177. }
  1178. }
  1179.  
  1180. NH.xunit.testing.log.leaving(me, passed);
  1181. return passed;
  1182. }
  1183.  
  1184. /**
  1185. * Execute TestCase tests.
  1186. * @returns {boolean} - Success status.
  1187. */
  1188. function doTestCases() {
  1189. const me = 'Running TestCases';
  1190. NH.xunit.testing.log.entered(me);
  1191.  
  1192. const savedConfigs = Logger.configs;
  1193. const result = NH.xunit.runTests();
  1194. Logger.configs = savedConfigs;
  1195.  
  1196. NH.xunit.testing.log.log('result:', result);
  1197. if (result.errors.length) {
  1198. NH.xunit.testing.log.starting('Errors');
  1199. for (const error of result.errors) {
  1200. NH.xunit.testing.log.log('error:', error);
  1201. }
  1202. NH.xunit.testing.log.finished('Errors');
  1203. }
  1204.  
  1205. if (result.failures.length) {
  1206. NH.xunit.testing.log.starting('Failures');
  1207. for (const failure of result.failures) {
  1208. NH.xunit.testing.log.log('failure:', failure.name, failure.message);
  1209. }
  1210. NH.xunit.testing.log.finished('Failures');
  1211. }
  1212.  
  1213. NH.xunit.testing.log.leaving(me, result.wasSuccessful());
  1214. return result.wasSuccessful();
  1215. }
  1216.  
  1217. /**
  1218. * Basic test runner.
  1219. *
  1220. * This depends on {Logger}, hence the location in this file.
  1221. */
  1222. function runTests() {
  1223. NH.xunit.testing.log = new Logger('Testing');
  1224.  
  1225. if (NH.xunit.testing.enabled) {
  1226. if (doFunctionTests()) {
  1227. NH.xunit.testing.log.log('All function tests passed.');
  1228. } else {
  1229. NH.xunit.testing.log.log('A function test failed.');
  1230. }
  1231. if (doTestCases()) {
  1232. NH.xunit.testing.log.log('All TestCases passed.');
  1233. } else {
  1234. NH.xunit.testing.log.log('At least one TestCase failed.');
  1235. }
  1236. }
  1237.  
  1238. }
  1239.  
  1240. NH.xunit.testing.run = runTests;
  1241.  
  1242. /**
  1243. * Create a UUID-like string with a base.
  1244. * @param {string} strBase - Base value for the string.
  1245. * @returns {string} - A unique string.
  1246. */
  1247. function uuId(strBase) {
  1248. return `${strBase}-${crypto.randomUUID()}`;
  1249. }
  1250.  
  1251. /**
  1252. * Normalizes a string to be safe to use as an HTML element id.
  1253. * @param {string} input - The string to normalize.
  1254. * @returns {string} - Normlized string.
  1255. */
  1256. function safeId(input) {
  1257. let result = input
  1258. .replaceAll(' ', '-')
  1259. .replaceAll('.', '_')
  1260. .replaceAll(',', '__comma__')
  1261. .replaceAll(':', '__colon__');
  1262. if (!(/^[a-z_]/iu).test(result)) {
  1263. result = `a${result}`;
  1264. }
  1265. return result;
  1266. }
  1267.  
  1268. /** Test case. */
  1269. function testSafeId() {
  1270. const tests = [
  1271. {test: 'Tabby Cat', expected: 'Tabby-Cat'},
  1272. {test: '_', expected: '_'},
  1273. {test: '', expected: 'a'},
  1274. {test: '0', expected: 'a0'},
  1275. {test: 'a.b.c', expected: 'a_b_c'},
  1276. {test: 'a,b,c', expected: 'a__comma__b__comma__c'},
  1277. {test: 'a:b::c', expected: 'a__colon__b__colon____colon__c'},
  1278. ];
  1279.  
  1280. for (const {test, expected} of tests) {
  1281. const actual = safeId(test);
  1282. const passed = actual === expected;
  1283. const msg = `${test} ${expected} ${actual}, ${passed}`;
  1284. NH.xunit.testing.log.log(msg);
  1285. if (!passed) {
  1286. throw new Error(msg);
  1287. }
  1288. }
  1289. }
  1290.  
  1291. NH.xunit.testing.funcs.push(testSafeId);
  1292.  
  1293. /**
  1294. * Equivalent (for now) Java's hashCode (do not store externally).
  1295. *
  1296. * Do not expect it to be stable across releases.
  1297. *
  1298. * Implements: s[0]*31(n-1) + s[1]*31(n-2) + ... + s[n-1]
  1299. * @param {string} s - String to hash.
  1300. * @returns {string} - Hash value.
  1301. */
  1302. function strHash(s) {
  1303. let hash = 0;
  1304. for (let i = 0; i < s.length; i += 1) {
  1305. // eslint-disable-next-line no-magic-numbers
  1306. hash = (hash * 31) + s.charCodeAt(i) | 0;
  1307. }
  1308. return `${hash}`;
  1309. }
  1310.  
  1311. /**
  1312. * Simple dispatcher (event bus).
  1313. *
  1314. * It takes a fixed list of event types upon construction and attempts to
  1315. * use an unknown event will throw an error.
  1316. */
  1317. class Dispatcher {
  1318.  
  1319. /**
  1320. * @callback Handler
  1321. * @param {string} eventType - Event type.
  1322. * @param {*} data - Event data.
  1323. */
  1324.  
  1325. /**
  1326. * @param {...string} eventTypes - Event types this instance can handle.
  1327. */
  1328. constructor(...eventTypes) {
  1329. for (const eventType of eventTypes) {
  1330. this.#handlers.set(eventType, []);
  1331. }
  1332. }
  1333.  
  1334. /**
  1335. * Attach a function to an eventType.
  1336. * @param {string} eventType - Event type to connect with.
  1337. * @param {Handler} func - Single argument function to call.
  1338. */
  1339. on(eventType, func) {
  1340. const handlers = this.#getHandlers(eventType);
  1341. handlers.push(func);
  1342. }
  1343.  
  1344. /**
  1345. * Remove all instances of a function registered to an eventType.
  1346. * @param {string} eventType - Event type to disconnect from.
  1347. * @param {Handler} func - Function to remove.
  1348. */
  1349. off(eventType, func) {
  1350. const handlers = this.#getHandlers(eventType);
  1351. let index = 0;
  1352. while ((index = handlers.indexOf(func)) !== NOT_FOUND) {
  1353. handlers.splice(index, 1);
  1354. }
  1355. }
  1356.  
  1357. /**
  1358. * Calls all registered functions for the given eventType.
  1359. * @param {string} eventType - Event type to use.
  1360. * @param {object} data - Data to pass to each function.
  1361. */
  1362. fire(eventType, data) {
  1363. const handlers = this.#getHandlers(eventType);
  1364. for (const handler of handlers) {
  1365. handler(eventType, data);
  1366. }
  1367. }
  1368.  
  1369. #handlers = new Map();
  1370.  
  1371. /**
  1372. * Look up array of handlers by event type.
  1373. * @param {string} eventType - Event type to look up.
  1374. * @throws {Error} - When eventType was not registered during
  1375. * instantiation.
  1376. * @returns {Handler[]} - Handlers currently registered for this
  1377. * eventType.
  1378. */
  1379. #getHandlers = (eventType) => {
  1380. const handlers = this.#handlers.get(eventType);
  1381. if (!handlers) {
  1382. const eventTypes = Array.from(this.#handlers.keys()).join(', ');
  1383. throw new Error(
  1384. `Unknown event type: ${eventType}, must be one of: ${eventTypes}`
  1385. );
  1386. }
  1387. return handlers;
  1388. }
  1389.  
  1390. }
  1391.  
  1392. /**
  1393. * Separate a string of concatenated words along transitions.
  1394. *
  1395. * Transitions are:
  1396. * lower to upper (lowerUpper -> lower Upper)
  1397. * grouped upper to lower (ABCd -> AB Cd)
  1398. * underscores (snake_case -> snake case)
  1399. * spaces
  1400. * character/numbers (lower2Upper -> lower 2 Upper)
  1401. * Likely only works with ASCII.
  1402. * Empty strings return an empty array.
  1403. * Extra separators are consolidated.
  1404. * @param {string} text - Text to parse.
  1405. * @returns {string[]} - Parsed text.
  1406. */
  1407. function simpleParseWords(text) {
  1408. const results = [];
  1409.  
  1410. const working = [text];
  1411. const moreWork = [];
  1412.  
  1413. while (working.length || moreWork.length) {
  1414. if (working.length === 0) {
  1415. working.push(...moreWork);
  1416. moreWork.length = 0;
  1417. }
  1418.  
  1419. // Unicode categories used below:
  1420. // L - Letter
  1421. // Ll - Letter, lower
  1422. // Lu - Letter, upper
  1423. // N - Number
  1424. let word = working.shift();
  1425. if (word) {
  1426. word = word.replace(
  1427. /(?<lower>\p{Ll})(?<upper>\p{Lu})/u,
  1428. '$<lower> $<upper>'
  1429. );
  1430.  
  1431. word = word.replace(
  1432. /(?<upper>\p{Lu}+)(?<lower>\p{Lu}\p{Ll})/u,
  1433. '$<upper> $<lower>'
  1434. );
  1435.  
  1436. word = word.replace(
  1437. /(?<letter>\p{L})(?<number>\p{N})/u,
  1438. '$<letter> $<number>'
  1439. );
  1440.  
  1441. word = word.replace(
  1442. /(?<number>\p{N})(?<letter>\p{L})/u,
  1443. '$<number> $<letter>'
  1444. );
  1445.  
  1446. const split = word.split(/[ _]/u);
  1447. if (split.length > 1 || moreWork.length) {
  1448. moreWork.push(...split);
  1449. } else {
  1450. results.push(word);
  1451. }
  1452. }
  1453. }
  1454.  
  1455. return results;
  1456. }
  1457.  
  1458. /* eslint-disable require-jsdoc */
  1459. class SimpleParseWordsTestCase extends NH.xunit.TestCase {
  1460.  
  1461. // TODO(#183): Stop doing joins once assertEqual() is better.
  1462. testEmpty() {
  1463. // Act
  1464. const actual = simpleParseWords('');
  1465.  
  1466. // Assert
  1467. this.assertEqual(actual.length, 0);
  1468. }
  1469.  
  1470. testSeparatorsOnly() {
  1471. // Act
  1472. const actual = simpleParseWords(' _ __ _');
  1473.  
  1474. // Assert
  1475. this.assertEqual(actual.length, 0);
  1476. }
  1477.  
  1478. testAllLower() {
  1479. // Act
  1480. const actual = simpleParseWords('lower');
  1481.  
  1482. // Assert
  1483. const expected = 'lower';
  1484. this.assertEqual(actual.join(','), expected);
  1485. }
  1486.  
  1487. testAllUpper() {
  1488. // Act
  1489. const actual = simpleParseWords('UPPER');
  1490.  
  1491. // Assert
  1492. const expected = 'UPPER';
  1493. this.assertEqual(actual.join(','), expected);
  1494. }
  1495.  
  1496. testMixed() {
  1497. // Act
  1498. const actual = simpleParseWords('Mixed');
  1499.  
  1500. // Assert
  1501. const expected = 'Mixed';
  1502. this.assertEqual(actual.join(','), expected);
  1503. }
  1504.  
  1505. testSimpleCamelCase() {
  1506. // Act
  1507. const actual = simpleParseWords('SimpleCamelCase');
  1508.  
  1509. // Assert
  1510. const expected = 'Simple,Camel,Case';
  1511. this.assertEqual(actual.join(','), expected);
  1512. }
  1513.  
  1514. testLongCamelCase() {
  1515. // Act
  1516. const actual = simpleParseWords('AnUPPERWord');
  1517.  
  1518. // Assert
  1519. const expected = 'An,UPPER,Word';
  1520. this.assertEqual(actual.join(','), expected);
  1521. }
  1522.  
  1523. testLowerCamelCase() {
  1524. // Act
  1525. const actual = simpleParseWords('lowerCamelCase');
  1526.  
  1527. // Assert
  1528. const expected = 'lower,Camel,Case';
  1529. this.assertEqual(actual.join(','), expected);
  1530. }
  1531.  
  1532. testSnakeCase() {
  1533. // Act
  1534. const actual = simpleParseWords('snake_case_Example');
  1535.  
  1536. // Assert
  1537. const expected = 'snake,case,Example';
  1538. this.assertEqual(actual.join(','), expected);
  1539. }
  1540.  
  1541. testDoubleSnakeCase() {
  1542. // Act
  1543. const actual = simpleParseWords('double__snake_Case_example');
  1544.  
  1545. // Assert
  1546. const expected = 'double,snake,Case,example';
  1547. this.assertEqual(actual.join(','), expected);
  1548. }
  1549.  
  1550. testWithNumbers() {
  1551. // Act
  1552. const actual = simpleParseWords('One23fourFive');
  1553.  
  1554. // Assert
  1555. const expected = 'One,23,four,Five';
  1556. this.assertEqual(actual.join(','), expected);
  1557. }
  1558.  
  1559. testWithSpaces() {
  1560. // Act
  1561. const actual = simpleParseWords('ABCd EF ghIj');
  1562.  
  1563. // Assert
  1564. const expected = 'AB,Cd,EF,gh,Ij';
  1565. this.assertEqual(actual.join(','), expected);
  1566. }
  1567.  
  1568. testComplicated() {
  1569. // Act
  1570. const actual = simpleParseWords(
  1571. 'A_VERYComplicated_Wordy __ _ Example'
  1572. );
  1573.  
  1574. // Assert
  1575. const expected = 'A,VERY,Complicated,Wordy,Example';
  1576. this.assertEqual(actual.join(','), expected);
  1577. }
  1578.  
  1579. }
  1580. /* eslint-enable */
  1581.  
  1582. NH.xunit.testing.testCases.push(SimpleParseWordsTestCase);
  1583.  
  1584. return {
  1585. version: version,
  1586. NOT_FOUND: NOT_FOUND,
  1587. ensure: ensure,
  1588. DefaultMap: DefaultMap,
  1589. Logger: Logger,
  1590. uuId: uuId,
  1591. safeId: safeId,
  1592. strHash: strHash,
  1593. Dispatcher: Dispatcher,
  1594. simpleParseWords: simpleParseWords,
  1595. };
  1596.  
  1597. }());