NH_base

Base library usable any time.

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

  1. // ==UserScript==
  2. // ==UserLibrary==
  3. // @name NH_base
  4. // @description Base library usable any time.
  5. // @version 52
  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 = 52;
  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. * @type {number} - Constant useful for testing length of an array.
  29. */
  30. const ONE_ITEM = 1;
  31.  
  32. /**
  33. * @typedef {object} NexusHoratioVersion
  34. * @property {string} name - Library name.
  35. * @property {number} [minVersion=0] - Minimal version needed.
  36. */
  37.  
  38. /**
  39. * Ensures appropriate versions of NexusHoratio libraries are loaded.
  40. * @param {NexusHoratioVersion[]} versions - Versions required.
  41. * @returns {object} - Namespace with only ensured libraries present.
  42. * @throws {Error} - When requirements not met.
  43. */
  44. function ensure(versions) {
  45. let msg = 'Forgot to set a message';
  46. const namespace = {};
  47. for (const ver of versions) {
  48. const {
  49. name,
  50. minVersion = 0,
  51. } = ver;
  52. const lib = window.NexusHoratio[name];
  53. if (!lib) {
  54. msg = `Library "${name}" is not loaded`;
  55. throw new Error(`Not Loaded: ${msg}`);
  56. }
  57. if (minVersion > lib.version) {
  58. msg = `At least version ${minVersion} of library "${name}" ` +
  59. `required; version ${lib.version} present.`;
  60. throw new Error(`Min Version: ${msg}`);
  61. }
  62. namespace[name] = lib;
  63. }
  64. return namespace;
  65. }
  66.  
  67. const NH = ensure([{name: 'xunit', minVersion: 49}]);
  68.  
  69. /* eslint-disable require-jsdoc */
  70. class EnsureTestCase extends NH.xunit.TestCase {
  71.  
  72. testEmpty() {
  73. const actual = ensure([]);
  74. const expected = {};
  75. this.assertEqual(actual, expected);
  76. }
  77.  
  78. testNameOnly() {
  79. const ns = ensure([{name: 'base'}]);
  80. this.assertTrue(ns.base);
  81. }
  82.  
  83. testMinVersion() {
  84. this.assertRaisesRegExp(
  85. Error, /^Min Version:.*required.*present.$/u, () => {
  86. ensure([{name: 'base', minVersion: Number.MAX_VALUE}]);
  87. }
  88. );
  89. }
  90.  
  91. testMissing() {
  92. this.assertRaisesRegExp(
  93. Error, /^Not Loaded: /u, () => {
  94. ensure([{name: 'missing'}]);
  95. }
  96. );
  97. }
  98.  
  99. }
  100. /* eslint-enable */
  101.  
  102. NH.xunit.testing.testCases.push(EnsureTestCase);
  103.  
  104. /** Base exception that uses the name of the class. */
  105. class Exception extends Error {
  106.  
  107. /** @type {string} */
  108. get name() {
  109. return this.constructor.name;
  110. }
  111.  
  112. }
  113.  
  114. /* eslint-disable require-jsdoc */
  115. class ExceptionTestCase extends NH.xunit.TestCase {
  116.  
  117. testBase() {
  118. // Assemble/Act
  119. const e = new Exception(this.id);
  120.  
  121. // Assert
  122. this.assertEqual(e.name, 'Exception', 'name');
  123. this.assertEqual(
  124. e.toString(), 'Exception: ExceptionTestCase.testBase', 'toString'
  125. );
  126. this.assertTrue(e instanceof Exception, 'is exception');
  127. this.assertTrue(e instanceof Error, 'is error');
  128. this.assertFalse(e instanceof TypeError, 'is NOT type-error');
  129. }
  130.  
  131. testInheritance() {
  132. // Assemble
  133. class TestException extends Exception {}
  134. class DifferentException extends Exception {}
  135.  
  136. // Act
  137. const te = new TestException('silly message');
  138.  
  139. // Assert
  140. this.assertEqual(te.name, 'TestException', 'name');
  141. this.assertEqual(
  142. te.toString(), 'TestException: silly message', 'toString'
  143. );
  144. this.assertTrue(te instanceof TestException, 'is test-exception');
  145. this.assertTrue(te instanceof Exception, 'is exception');
  146. this.assertTrue(te instanceof Error, 'is error');
  147. this.assertFalse(te instanceof TypeError, 'is NOT type-error');
  148. this.assertFalse(te instanceof DifferentException,
  149. 'is NOT different-exception');
  150. }
  151.  
  152. }
  153. /* eslint-enable */
  154.  
  155. NH.xunit.testing.testCases.push(ExceptionTestCase);
  156.  
  157. /**
  158. * Simple dispatcher (event bus).
  159. *
  160. * It takes a fixed list of event types upon construction and attempts to
  161. * use an unknown event will throw an error.
  162. */
  163. class Dispatcher {
  164.  
  165. /**
  166. * @callback Handler
  167. * @param {string} eventType - Event type.
  168. * @param {*} data - Event data.
  169. */
  170.  
  171. /**
  172. * @param {...string} eventTypes - Event types this instance can handle.
  173. */
  174. constructor(...eventTypes) {
  175. for (const eventType of eventTypes) {
  176. this.#handlers.set(eventType, []);
  177. }
  178. }
  179.  
  180. /**
  181. * Attach a function to an eventType.
  182. * @param {string} eventType - Event type to connect with.
  183. * @param {Handler} func - Single argument function to call.
  184. * @returns {Dispatcher} - This instance, for chaining.
  185. */
  186. on(eventType, func) {
  187. const handlers = this.#getHandlers(eventType);
  188. handlers.push(func);
  189. return this;
  190. }
  191.  
  192. /**
  193. * Remove all instances of a function registered to an eventType.
  194. * @param {string} eventType - Event type to disconnect from.
  195. * @param {Handler} func - Function to remove.
  196. * @returns {Dispatcher} - This instance, for chaining.
  197. */
  198. off(eventType, func) {
  199. const handlers = this.#getHandlers(eventType);
  200. let index = 0;
  201. while ((index = handlers.indexOf(func)) !== NOT_FOUND) {
  202. handlers.splice(index, 1);
  203. }
  204. return this;
  205. }
  206.  
  207. /**
  208. * Calls all registered functions for the given eventType.
  209. * @param {string} eventType - Event type to use.
  210. * @param {object} data - Data to pass to each function.
  211. * @returns {Dispatcher} - This instance, for chaining.
  212. */
  213. fire(eventType, data) {
  214. const handlers = this.#getHandlers(eventType);
  215. for (const handler of handlers) {
  216. handler(eventType, data);
  217. }
  218. return this;
  219. }
  220.  
  221. #handlers = new Map();
  222.  
  223. /**
  224. * Look up array of handlers by event type.
  225. * @param {string} eventType - Event type to look up.
  226. * @throws {Error} - When eventType was not registered during
  227. * instantiation.
  228. * @returns {Handler[]} - Handlers currently registered for this
  229. * eventType.
  230. */
  231. #getHandlers = (eventType) => {
  232. const handlers = this.#handlers.get(eventType);
  233. if (!handlers) {
  234. const eventTypes = Array.from(this.#handlers.keys())
  235. .join(', ');
  236. throw new Error(
  237. `Unknown event type: ${eventType}, must be one of: ${eventTypes}`
  238. );
  239. }
  240. return handlers;
  241. }
  242.  
  243. }
  244.  
  245. /* eslint-disable max-lines-per-function */
  246. /* eslint-disable max-statements */
  247. /* eslint-disable no-new */
  248. /* eslint-disable no-empty-function */
  249. /* eslint-disable require-jsdoc */
  250. class DispatcherTestCase extends NH.xunit.TestCase {
  251.  
  252. testConstruction() {
  253. this.assertNoRaises(() => {
  254. new Dispatcher();
  255. }, 'empty');
  256.  
  257. this.assertNoRaises(() => {
  258. new Dispatcher('one');
  259. }, 'single');
  260.  
  261. this.assertNoRaises(() => {
  262. new Dispatcher('one', 'two', 'three');
  263. }, 'multiple');
  264. }
  265.  
  266. testOnOff() {
  267. const dispatcher = new Dispatcher('boo');
  268. const handler = () => {};
  269.  
  270. this.assertNoRaises(() => {
  271. dispatcher.on('boo', handler);
  272. dispatcher.on('boo', handler);
  273. }, 'on twice');
  274.  
  275. this.assertNoRaises(() => {
  276. dispatcher.off('boo', handler);
  277. dispatcher.off('boo', handler);
  278. }, 'off twice');
  279.  
  280. this.assertNoRaises(() => {
  281. dispatcher.on('boo', handler)
  282. .off('boo', handler)
  283. .on('boo', handler)
  284. .off('boo', handler);
  285. }, 'chaining works');
  286.  
  287. this.assertRaisesRegExp(
  288. Error,
  289. /Unknown event type: hoo, must be one of: boo/u,
  290. () => {
  291. dispatcher.on('hoo', handler);
  292. },
  293. 'on, bad event type'
  294. );
  295.  
  296. this.assertRaisesRegExp(
  297. Error,
  298. /Unknown event type: hoo, must be one of: boo/u,
  299. () => {
  300. dispatcher.off('hoo', handler);
  301. },
  302. 'on, bad event type'
  303. );
  304. }
  305.  
  306. testFire() {
  307. const dispatcher = new Dispatcher('boo', 'ya');
  308. const calls1 = [];
  309. const calls2 = new Map();
  310. const handler1 = (...args) => {
  311. calls1.push(args);
  312. };
  313. const handler2 = (type, data) => {
  314. calls2.set(type, data);
  315. };
  316.  
  317. this.assertNoRaises(() => {
  318. dispatcher.fire('boo', 'random data')
  319. .fire('ya', 'other stuff');
  320. });
  321. this.assertEqual(calls1, [], 'calls1 empty');
  322. this.assertEqual(calls2, new Map(), 'calls2 empty');
  323.  
  324. dispatcher.on('boo', handler1)
  325. .on('ya', handler2)
  326. .fire('boo', 'more random data');
  327. this.assertEqual(
  328. calls1, [['boo', 'more random data']], 'single handler1 registered'
  329. );
  330. this.assertEqual(calls2, new Map(), 'calls2 still empty');
  331.  
  332. calls1.length = 0;
  333. calls2.clear();
  334. dispatcher.on('boo', handler1)
  335. .on('boo', handler2)
  336. .on('ya', handler2)
  337. .fire('boo', {an: 'object'})
  338. .fire('ya', 'ya stuff');
  339. this.assertEqual(
  340. calls1,
  341. [['boo', {an: 'object'}], ['boo', {an: 'object'}]],
  342. 'handler1 registered twice'
  343. );
  344. this.assertEqual(
  345. calls2,
  346. new Map([['boo', {an: 'object'}], ['ya', 'ya stuff']]),
  347. 'calls2 registered once'
  348. );
  349.  
  350. calls1.length = 0;
  351. calls2.clear();
  352. dispatcher.off('boo', handler1)
  353. .fire('boo', {a: 'different object'});
  354. this.assertEqual(calls1, [], 'single off got rid of all handler1');
  355. this.assertEqual(
  356. calls2,
  357. new Map([['boo', {a: 'different object'}]]),
  358. 'handler2 still there'
  359. );
  360.  
  361. calls1.length = 0;
  362. calls2.clear();
  363. this.assertRaisesRegExp(
  364. Error,
  365. /Unknown event type: hoo, must be one of: boo, ya/u,
  366. () => {
  367. dispatcher.fire('hoo', 'oops');
  368. },
  369. 'bad eventType'
  370. );
  371. this.assertEqual(calls1, [], 'calls1 should be empty');
  372. this.assertEqual(calls2, new Map(), 'calls2 should be empty');
  373. }
  374.  
  375. testBadHandler() {
  376. const dispatcher = new Dispatcher('oops');
  377.  
  378. this.assertNoRaises(() => {
  379. dispatcher.on('oops', null);
  380. }, 'happily sets silly handler');
  381.  
  382. this.assertRaises(
  383. TypeError,
  384. () => {
  385. dispatcher.fire('oops', 'this will not end well');
  386. },
  387. 'and then it crashes'
  388. );
  389. }
  390.  
  391. }
  392. /* eslint-enable */
  393.  
  394. NH.xunit.testing.testCases.push(DispatcherTestCase);
  395.  
  396. /**
  397. * A simple message system that will queue messages to be delivered.
  398. *
  399. * This is similar to the WEB API's `MessageChannel`.
  400. */
  401. class MessageQueue {
  402.  
  403. /** @type {number} - Number of messages currently queued. */
  404. get count() {
  405. return this.#messages.length;
  406. }
  407.  
  408. /**
  409. * @param {...*} items - Whatever to add to the queue.
  410. * @returns {MessageQueue} - This instance, for chaining.
  411. */
  412. post(...items) {
  413. this.#messages.push(items);
  414. this.#dispatcher.fire('post');
  415. return this;
  416. }
  417.  
  418. /**
  419. * @param {?function(...*)} func - Function that receives the messages.
  420. * If falsy, listener is removed.
  421. * @returns {MessageQueue} - This instance, for chaining.
  422. */
  423. listen(func) {
  424. if (func) {
  425. this.#listener = func;
  426. this.#dispatcher.on('post', this.#handler);
  427. this.#handler();
  428. } else {
  429. this.#listener = null;
  430. this.#dispatcher.off('post', this.#handler);
  431. }
  432. return this;
  433. }
  434.  
  435. #dispatcher = new Dispatcher('post');
  436. #listener
  437. #messages = [];
  438.  
  439. #handler = () => {
  440. while (this.#messages.length && this.#listener) {
  441. this.#listener(...this.#messages.shift());
  442. }
  443. }
  444.  
  445. }
  446.  
  447. /* eslint-disable no-magic-numbers */
  448. /* eslint-disable require-jsdoc */
  449. class MessageQueueTestCase extends NH.xunit.TestCase {
  450.  
  451. testCount() {
  452. // Assemble
  453. const mq = new MessageQueue();
  454.  
  455. // Act
  456. for (let i = 0; i < 20; i += 1) {
  457. mq.post(i);
  458. }
  459.  
  460. // Assert
  461. this.assertEqual(mq.count, 20);
  462. }
  463.  
  464. testListener() {
  465. // Assemble
  466. const mq = new MessageQueue();
  467. const messages = [];
  468. const cb = (message) => {
  469. messages.push(message);
  470. };
  471. mq.post('a')
  472. .post('b')
  473. .post('c');
  474.  
  475. // Act
  476. mq.listen(cb)
  477. .post(1);
  478. mq.post(2)
  479. .post(3);
  480.  
  481. // Assert
  482. this.assertEqual(messages, ['a', 'b', 'c', 1, 2, 3], 'received');
  483. this.assertEqual(mq.count, 0, 'final count');
  484. }
  485.  
  486. testDisconnect() {
  487. // Assemble
  488. const mq = new MessageQueue();
  489. const messages = [];
  490. const cb = (message) => {
  491. messages.push(message);
  492. mq.listen();
  493. };
  494. mq.post('a')
  495. .post('b')
  496. .post('c');
  497.  
  498. // Act
  499. mq.listen(cb);
  500. mq.post(1)
  501. .post(2);
  502.  
  503. // Assert
  504. this.assertEqual(messages, ['a'], 'received');
  505. this.assertEqual(mq.count, 4, 'final count');
  506. }
  507.  
  508. testListenerChange() {
  509. // Assemble
  510. const mq = new MessageQueue();
  511. const newMessages = [];
  512. const origMessages = [];
  513. const newCallback = (message) => {
  514. newMessages.push(message);
  515. };
  516. const origCallback = (message) => {
  517. origMessages.push(message);
  518. mq.listen(newCallback);
  519. };
  520. mq.post('a')
  521. .post('b')
  522. .post('c');
  523.  
  524. // Act
  525. mq.listen(origCallback)
  526. .post(1)
  527. .post(2);
  528.  
  529. // Assert
  530. this.assertEqual(origMessages, ['a'], 'orig messages');
  531. this.assertEqual(newMessages, ['b', 'c', 1, 2], 'new messages');
  532. this.assertEqual(mq.count, 0, 'final count');
  533. }
  534.  
  535. testFancyMessages() {
  536. // Assemble
  537. const mq = new MessageQueue();
  538. const messages = [];
  539. const cb = (...items) => {
  540. messages.push(...items);
  541. messages.push('---');
  542. };
  543. mq.listen(cb);
  544. const obj = {z: 26};
  545.  
  546. mq.post('line 1', 'line 2', 'line 3');
  547. mq.post(1)
  548. .post(obj, [4, 'd']);
  549.  
  550. this.assertEqual(
  551. messages,
  552. ['line 1', 'line 2', 'line 3', '---', 1, '---', obj, [4, 'd'], '---']
  553. );
  554. }
  555.  
  556. }
  557. /* eslint-enable */
  558.  
  559. NH.xunit.testing.testCases.push(MessageQueueTestCase);
  560.  
  561. /**
  562. * NexusHoratio libraries and apps should log issues here.
  563. *
  564. * They should be logged in the form of multiple strings:
  565. * NH.base.issues.post('Something bad', 'detail 1', 'detail 2');
  566. *
  567. * An eventual listener should do something like:
  568. * listen(...issues) {
  569. * for (const issue of issues) {
  570. * displayIssueMessage(issue);
  571. * }
  572. * displayIssueSeparator();
  573. * }
  574. */
  575. const issues = new MessageQueue();
  576.  
  577. /**
  578. * A Number like class that supports operations.
  579. *
  580. * For lack of any other standard, methods will be named like those in
  581. * Python's operator module.
  582. *
  583. * All operations should return `this` to allow chaining.
  584. *
  585. * The existence of the valueOf(), toString() and toJSON() methods will
  586. * probably allow this class to work in many situations through type
  587. * coercion.
  588. */
  589. class NumberOp {
  590.  
  591. /** @param {number} value - Initial value, parsed by Number(). */
  592. constructor(value) {
  593. this.assign(value);
  594. }
  595.  
  596. /** @returns {number} - Current value. */
  597. valueOf() {
  598. return this.#value;
  599. }
  600.  
  601. /** @returns {string} - Current value. */
  602. toString() {
  603. return `${this.valueOf()}`;
  604. }
  605.  
  606. /** @returns {number} - Current value. */
  607. toJSON() {
  608. return this.valueOf();
  609. }
  610.  
  611. /**
  612. * @param {number} value - Number to assign.
  613. * @returns {NumberOp} - This instance.
  614. */
  615. assign(value = 0) {
  616. this.#value = Number(value);
  617. return this;
  618. }
  619.  
  620. /**
  621. * @param {number} value - Number to add.
  622. * @returns {NumberOp} - This instance.
  623. */
  624. add(value) {
  625. this.#value += Number(value);
  626. return this;
  627. }
  628.  
  629. #value
  630.  
  631. }
  632.  
  633. /* eslint-disable newline-per-chained-call */
  634. /* eslint-disable no-magic-numbers */
  635. /* eslint-disable no-undefined */
  636. /* eslint-disable require-jsdoc */
  637. class NumberOpTestCase extends NH.xunit.TestCase {
  638.  
  639. testValueOf() {
  640. this.assertEqual(new NumberOp().valueOf(), 0, 'default');
  641. this.assertEqual(new NumberOp(null).valueOf(), 0, 'null');
  642. this.assertEqual(new NumberOp(undefined).valueOf(), 0, 'undefined');
  643. this.assertEqual(new NumberOp(42).valueOf(), 42, 'number');
  644. this.assertEqual(new NumberOp('52').valueOf(), 52, 'string');
  645. }
  646.  
  647. testToString() {
  648. this.assertEqual(new NumberOp(123).toString(), '123', 'number');
  649. this.assertEqual(new NumberOp(null).toString(), '0', 'null');
  650. this.assertEqual(new NumberOp(undefined).toString(), '0', 'undefined');
  651. }
  652.  
  653. testTemplateLiteral() {
  654. const val = new NumberOp(456);
  655. this.assertEqual(`abc${val}xyz`, 'abc456xyz');
  656. }
  657.  
  658. testBasicMath() {
  659. this.assertEqual(new NumberOp(124) + 6, 130, 'NO + x');
  660. this.assertEqual(3 + new NumberOp(5), 8, 'x + NO');
  661. }
  662.  
  663. testStringManipulation() {
  664. const a = 'abc';
  665. const x = 'xyz';
  666. const n = new NumberOp('654');
  667.  
  668. this.assertEqual(a + n, 'abc654', 's + NO');
  669. this.assertEqual(n + x, '654xyz', 'NO + s');
  670. }
  671.  
  672. testAssignOp() {
  673. const n = new NumberOp(123);
  674. n.assign(42);
  675. this.assertEqual(n.valueOf(), 42, 'number');
  676.  
  677. n.assign(null);
  678. this.assertEqual(n.valueOf(), 0, 'null');
  679.  
  680. n.assign(789);
  681. this.assertEqual(n.valueOf(), 789, 'number, reset');
  682.  
  683. n.assign(undefined);
  684. this.assertEqual(n.valueOf(), 0, 'undefined');
  685. }
  686.  
  687. testAddOp() {
  688. this.assertEqual(new NumberOp(3).add(1)
  689. .valueOf(), 4,
  690. 'number');
  691. this.assertEqual(new NumberOp(1).add('5')
  692. .valueOf(), 6,
  693. 'string');
  694. this.assertEqual(new NumberOp(3).add(new NumberOp(8))
  695. .valueOf(), 11,
  696. 'NO.add(NO)');
  697. this.assertEqual(new NumberOp(9).add(-16)
  698. .valueOf(), -7,
  699. 'negative');
  700. }
  701.  
  702. testChaining() {
  703. this.assertEqual(new NumberOp().add(1)
  704. .add(2)
  705. .add('3')
  706. .valueOf(), 6,
  707. 'adds');
  708. this.assertEqual(new NumberOp(3).assign(40)
  709. .add(2)
  710. .valueOf(), 42,
  711. 'mixed');
  712. }
  713.  
  714. }
  715. /* eslint-enable */
  716.  
  717. NH.xunit.testing.testCases.push(NumberOpTestCase);
  718.  
  719. /**
  720. * Subclass of {Map} similar to Python's defaultdict.
  721. *
  722. * First argument is a factory function that will create a new default value
  723. * for the key if not already present in the container.
  724. *
  725. * The factory function may take arguments. If `.get()` is called with
  726. * extra arguments, those will be passed to the factory if it needed.
  727. */
  728. class DefaultMap extends Map {
  729.  
  730. /**
  731. * @param {function(...args) : *} factory - Function that creates a new
  732. * default value if a requested key is not present.
  733. * @param {Iterable} [iterable] - Passed to {Map} super().
  734. */
  735. constructor(factory, iterable) {
  736. if (!(factory instanceof Function)) {
  737. throw new TypeError('The factory argument MUST be of ' +
  738. `type Function, not ${typeof factory}.`);
  739. }
  740. super(iterable);
  741.  
  742. this.#factory = factory;
  743. }
  744.  
  745. /**
  746. * Enhanced version of `Map.prototype.get()`.
  747. * @param {*} key - The key of the element to return from this instance.
  748. * @param {...*} args - Extra arguments passed tot he factory function if
  749. * it is called.
  750. * @returns {*} - The value associated with the key, perhaps newly
  751. * created.
  752. */
  753. get(key, ...args) {
  754. if (!this.has(key)) {
  755. this.set(key, this.#factory(...args));
  756. }
  757.  
  758. return super.get(key);
  759. }
  760.  
  761. #factory
  762.  
  763. }
  764.  
  765. /* eslint-disable newline-per-chained-call */
  766. /* eslint-disable no-magic-numbers */
  767. /* eslint-disable no-new */
  768. /* eslint-disable require-jsdoc */
  769. class DefaultMapTestCase extends NH.xunit.TestCase {
  770.  
  771. testNoFactory() {
  772. this.assertRaisesRegExp(TypeError, /MUST.*not undefined/u, () => {
  773. new DefaultMap();
  774. });
  775. }
  776.  
  777. testBadFactory() {
  778. this.assertRaisesRegExp(TypeError, /MUST.*not string/u, () => {
  779. new DefaultMap('a');
  780. });
  781. }
  782.  
  783. testFactorWithArgs() {
  784. // Assemble
  785. const dummy = new DefaultMap(x => new NumberOp(x));
  786. this.defaultEqual = this.equalValueOf;
  787.  
  788. // Act
  789. dummy.get('a');
  790. dummy.get('b', 5);
  791.  
  792. // Assert
  793. this.assertEqual(Array.from(dummy.entries()),
  794. [['a', 0], ['b', 5]]);
  795. }
  796.  
  797. testWithIterable() {
  798. // Assemble
  799. const dummy = new DefaultMap(Number, [[1, 'one'], [2, 'two']]);
  800.  
  801. // Act
  802. dummy.set(3, ['a', 'b']);
  803. dummy.get(4);
  804.  
  805. // Assert
  806. this.assertEqual(Array.from(dummy.entries()),
  807. [[1, 'one'], [2, 'two'], [3, ['a', 'b']], [4, 0]]);
  808. }
  809.  
  810. testCounter() {
  811. // Assemble
  812. const dummy = new DefaultMap(() => new NumberOp());
  813. this.defaultEqual = this.equalValueOf;
  814.  
  815. // Act
  816. dummy.get('a');
  817. dummy.get('b').add(1);
  818. dummy.get('b').add(1);
  819. dummy.get('c');
  820. dummy.get(4).add(1);
  821.  
  822. // Assert
  823. this.assertEqual(Array.from(dummy.entries()),
  824. [['a', 0], ['b', 2], ['c', 0], [4, 1]]);
  825. }
  826.  
  827. testArray() {
  828. // Assemble
  829. const dummy = new DefaultMap(Array);
  830.  
  831. // Act
  832. dummy.get('a').push(1, 2, 3);
  833. dummy.get('b').push(4, 5, 6);
  834. dummy.get('a').push('one', 'two', 'three');
  835.  
  836. // Assert
  837. this.assertEqual(Array.from(dummy.entries()),
  838. [['a', [1, 2, 3, 'one', 'two', 'three']], ['b', [4, 5, 6]]]);
  839. }
  840.  
  841. }
  842. /* eslint-enable */
  843.  
  844. NH.xunit.testing.testCases.push(DefaultMapTestCase);
  845.  
  846. /**
  847. * Fancy-ish log messages (likely over engineered).
  848. *
  849. * Console nested message groups can be started and ended using the special
  850. * method pairs, {@link Logger#entered}/{@link Logger#leaving} and {@link
  851. * Logger#starting}/{@link Logger#finished}. By default, the former are
  852. * opened and the latter collapsed (documented here as closed).
  853. *
  854. * Individual Loggers can be enabled/disabled by setting the {@link
  855. * Logger##Config.enabled} boolean property.
  856. *
  857. * Each Logger will have also have a collection of {@link Logger##Group}s
  858. * associated with it. These groups can have one of three modes: "opened",
  859. * "closed", "silenced". The first two correspond to the browser console
  860. * nested message groups. The intro and outro type of methods will handle
  861. * the nesting. If a group is set as "silenced", no messages will be sent
  862. * to the console.
  863. *
  864. * All Logger instances register a configuration with a singleton Map keyed
  865. * by the instance name. If more than one instance is created with the same
  866. * name, they all share the same configuration.
  867. *
  868. * Configurations can be exported as a plain object and reimported using the
  869. * {@link Logger.configs} property. The object could be saved via the
  870. * userscript script manager. Depending on which one, it may have to be
  871. * processed with the JSON.{stringify,parse} functions. Once exported, the
  872. * object may be modified. This could be used to provide a UI to edit the
  873. * object, though no schema is provided.
  874. *
  875. * Some values may be of interest to users for help in debugging a script.
  876. *
  877. * The {callCount} value is how many times a logger would have been used for
  878. * messages, even if the logger is disabled. Similarly, each group
  879. * associated with a logger also has a {callCount}. These values can be
  880. * used to determine which loggers and groups generate a lot of messages and
  881. * could be disabled or silenced.
  882. *
  883. * The {sequence} value is a rough indicator of how recently a logger or
  884. * group was actually used. It is purposely not a timestamp, but rather,
  885. * more closely associated with how often configurations are restored,
  886. * e.g. during web page reloads. A low sequence number, relative to the
  887. * others, may indicate a logger was renamed, groups removed, or simply
  888. * parts of an application that have not been visited recently. Depending
  889. * on the situation, the could clean up old configs, or explore other parts
  890. * of the script.
  891. *
  892. * @example
  893. * const log = new Logger('Bob');
  894. * foo(x) {
  895. * const me = 'foo';
  896. * log.entered(me, x);
  897. * ... do stuff ...
  898. * log.starting('loop');
  899. * for (const item in items) {
  900. * log.log(`Processing ${item}`);
  901. * ...
  902. * }
  903. * log.finished('loop');
  904. * log.leaving(me, y);
  905. * return y;
  906. * }
  907. *
  908. * Logger.config('Bob').enabled = true;
  909. * Logger.config('Bob').group('foo').mode = 'silenced');
  910. *
  911. * GM.setValue('Logger', Logger.configs);
  912. * ... restart browser ...
  913. * Logger.configs = GM.getValue('Logger');
  914. */
  915. class Logger {
  916.  
  917. /** @param {string} name - Name for this logger. */
  918. constructor(name) {
  919. this.#mq.listen(this.#errMsgListener);
  920. this.#name = name;
  921. this.#config = Logger.config(name);
  922. Logger.#loggers.get(this.#name)
  923. .push(new WeakRef(this));
  924. }
  925.  
  926. static sequence = 1;
  927.  
  928. /** @type {object} - Logger configurations. */
  929. static get configs() {
  930. return Logger.#toPojo();
  931. }
  932.  
  933. /** @param {object} val - Logger configurations. */
  934. static set configs(val) {
  935. Logger.#fromPojo(val);
  936. Logger.#resetLoggerConfigs();
  937. }
  938.  
  939. /** @type {string[]} - Names of known loggers. */
  940. static get loggers() {
  941. return Array.from(this.#loggers.keys());
  942. }
  943.  
  944. /**
  945. * Get configuration of a specific Logger.
  946. * @param {string} name - Logger configuration to get.
  947. * @returns {Logger.Config} - Current config for that Logger.
  948. */
  949. static config(name) {
  950. return this.#configs.get(name);
  951. }
  952.  
  953. /** Reset all configs to an empty state. */
  954. static resetConfigs() {
  955. this.#configs.clear();
  956. this.sequence = 1;
  957. }
  958.  
  959. /** Clear the console. */
  960. static clear() {
  961. this.#clear();
  962. }
  963.  
  964. /** @type {boolean} - Whether logging is currently enabled. */
  965. get enabled() {
  966. return this.#config.enabled;
  967. }
  968.  
  969. /** @type {boolean} - Indicates whether messages include a stack trace. */
  970. get includeStackTrace() {
  971. return this.#config.includeStackTrace;
  972. }
  973.  
  974. /** @type {MessageQueue} */
  975. get mq() {
  976. return this.#mq;
  977. }
  978.  
  979. /** @type {string} - Name for this logger. */
  980. get name() {
  981. return this.#name;
  982. }
  983.  
  984. /** @type {boolean} - Indicates whether current group is silenced. */
  985. get silenced() {
  986. let ret = false;
  987. const group = this.#groupStack.at(-1);
  988. if (group) {
  989. const mode = this.#config.group(group).mode;
  990. ret = mode === Logger.#GroupMode.Silenced;
  991. }
  992. return ret;
  993. }
  994.  
  995. /**
  996. * Log a specific message.
  997. * @param {string} msg - Message to send to console.debug.
  998. * @param {...*} rest - Arbitrary items to pass to console.debug.
  999. */
  1000. log(msg, ...rest) {
  1001. this.#log(msg, ...rest);
  1002. }
  1003.  
  1004. /**
  1005. * Indicate entered a specific group.
  1006. * @param {string} group - Group that was entered.
  1007. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1008. */
  1009. entered(group, ...rest) {
  1010. this.#intro(group, Logger.#GroupMode.Opened, ...rest);
  1011. }
  1012.  
  1013. /**
  1014. * Indicate leaving a specific group.
  1015. * @param {string} group - Group leaving.
  1016. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1017. */
  1018. leaving(group, ...rest) {
  1019. this.#outro(group, ...rest);
  1020. }
  1021.  
  1022. /**
  1023. * Indicate starting a specific collapsed group.
  1024. * @param {string} group - Group that is being started.
  1025. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1026. */
  1027. starting(group, ...rest) {
  1028. this.#intro(group, Logger.#GroupMode.Closed, ...rest);
  1029. }
  1030.  
  1031. /**
  1032. * Indicate finishe a specific collapsed group.
  1033. * @param {string} group - Group that was entered.
  1034. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1035. */
  1036. finished(group, ...rest) {
  1037. this.#outro(group, ...rest);
  1038. }
  1039.  
  1040. static #Config = class {
  1041.  
  1042. sequence = 0;
  1043.  
  1044. /** @type {NumberOp} */
  1045. get callCount() {
  1046. return this.#callCount;
  1047. }
  1048.  
  1049. /** @type {boolean} - Whether logging is currently enabled. */
  1050. get enabled() {
  1051. return this.#enabled;
  1052. }
  1053.  
  1054. /** @param {boolean} val - Set whether logging is currently enabled. */
  1055. set enabled(val) {
  1056. this.#enabled = Boolean(val);
  1057. }
  1058.  
  1059. /** @type {Map<string,Logger.#Group>} - Per group settings. */
  1060. get groups() {
  1061. return this.#groups;
  1062. }
  1063.  
  1064. /** @type {boolean} - Whether messages include a stack trace. */
  1065. get includeStackTrace() {
  1066. return this.#includeStackTrace;
  1067. }
  1068.  
  1069. /** @param {boolean} val - Set inclusion of stack traces. */
  1070. set includeStackTrace(val) {
  1071. this.#includeStackTrace = Boolean(val);
  1072. }
  1073.  
  1074. /**
  1075. * @param {string} name - Name of the group to get.
  1076. * @param {Logger.#GroupMode} mode - Default mode if not seen before.
  1077. * @returns {Logger.#Group} - Requested group, perhaps newly made.
  1078. */
  1079. group(name, mode) {
  1080. const sanitizedName = name ?? 'null';
  1081. const defaultMode = mode ?? 'opened';
  1082. return this.#groups.get(sanitizedName, defaultMode);
  1083. }
  1084.  
  1085. /**
  1086. * Capture that the associated Logger was used.
  1087. * @param {string} name - Which group was used.
  1088. */
  1089. used(name) {
  1090. const grp = this.group(name);
  1091.  
  1092. this.callCount.add(1);
  1093. this.sequence = Logger.sequence;
  1094.  
  1095. grp.callCount.add(1);
  1096. grp.sequence = Logger.sequence;
  1097. }
  1098.  
  1099. /** @returns {object} - Config as a plain object. */
  1100. toPojo() {
  1101. const pojo = {
  1102. callCount: this.callCount.valueOf(),
  1103. sequence: this.sequence,
  1104. enabled: this.enabled,
  1105. includeStackTrace: this.includeStackTrace,
  1106. groups: {},
  1107. };
  1108.  
  1109. for (const [k, v] of this.groups) {
  1110. pojo.groups[k] = v.toPojo();
  1111. }
  1112.  
  1113. return pojo;
  1114. }
  1115.  
  1116. /** @param {object} pojo - Config as a plain object. */
  1117. fromPojo(pojo) {
  1118. if (Object.hasOwn(pojo, 'callCount')) {
  1119. this.callCount.assign(pojo.callCount);
  1120. }
  1121. if (Object.hasOwn(pojo, 'sequence')) {
  1122. this.sequence = pojo.sequence;
  1123. Logger.sequence = Math.max(Logger.sequence, this.sequence);
  1124. }
  1125. if (Object.hasOwn(pojo, 'enabled')) {
  1126. this.enabled = pojo.enabled;
  1127. }
  1128. if (Object.hasOwn(pojo, 'includeStackTrace')) {
  1129. this.includeStackTrace = pojo.includeStackTrace;
  1130. }
  1131. if (Object.hasOwn(pojo, 'groups')) {
  1132. for (const [k, v] of Object.entries(pojo.groups)) {
  1133. const gm = Logger.#GroupMode.byName(v.mode);
  1134. if (gm) {
  1135. this.group(k)
  1136. .fromPojo(v);
  1137. }
  1138. }
  1139. }
  1140. }
  1141.  
  1142. #callCount = new NumberOp();
  1143. #enabled = false;
  1144. #groups = new DefaultMap(x => new Logger.#Group(x));
  1145. #includeStackTrace = false;
  1146.  
  1147. }
  1148.  
  1149. static #Group = class {
  1150.  
  1151. /** @param {Logger.#GroupMode} mode - Initial mode for this group. */
  1152. constructor(mode) {
  1153. this.mode = mode;
  1154. this.sequence = 0;
  1155. }
  1156.  
  1157. /** @type {NumberOp} */
  1158. get callCount() {
  1159. return this.#callCount;
  1160. }
  1161.  
  1162. /** @type {Logger.#GroupMode} */
  1163. get mode() {
  1164. return this.#mode;
  1165. }
  1166.  
  1167. /** @param {Logger.#GroupMode} val - Mode to set this group. */
  1168. set mode(val) {
  1169. let newVal = val;
  1170. if (!(newVal instanceof Logger.#GroupMode)) {
  1171. newVal = Logger.#GroupMode.byName(newVal);
  1172. }
  1173. if (newVal) {
  1174. this.#mode = newVal;
  1175. }
  1176. }
  1177.  
  1178. /** @returns {object} - Group as a plain object. */
  1179. toPojo() {
  1180. const pojo = {
  1181. mode: this.mode.name,
  1182. callCount: this.callCount.valueOf(),
  1183. sequence: this.sequence,
  1184. };
  1185.  
  1186. return pojo;
  1187. }
  1188.  
  1189. /** @param {object} pojo - Group as a plain object. */
  1190. fromPojo(pojo) {
  1191. this.mode = pojo.mode;
  1192. this.callCount.assign(pojo.callCount);
  1193. this.sequence = pojo.sequence ?? 0;
  1194. Logger.sequence = Math.max(Logger.sequence, this.sequence);
  1195. }
  1196.  
  1197. #callCount = new NumberOp();
  1198. #mode
  1199.  
  1200. }
  1201.  
  1202. /** Enum/helper for Logger groups. */
  1203. static #GroupMode = class {
  1204.  
  1205. /**
  1206. * @param {string} name - Mode name.
  1207. * @param {string} [greeting] - Greeting when opening group.
  1208. * @param {string} [farewell] - Salutation when closing group.
  1209. * @param {string} [func] - console.func to use for opening group.
  1210. */
  1211. constructor(name, greeting, farewell, func) { // eslint-disable-line max-params
  1212. this.#farewell = farewell;
  1213. this.#func = func;
  1214. this.#greeting = greeting;
  1215. this.#name = name;
  1216.  
  1217. Logger.#GroupMode.#known.set(name, this);
  1218.  
  1219. Object.freeze(this);
  1220. }
  1221.  
  1222. /**
  1223. * Find GroupMode by name.
  1224. * @param {string} name - Mode name.
  1225. * @returns {GroupMode} - Mode, if found.
  1226. */
  1227. static byName(name) {
  1228. return this.#known.get(name);
  1229. }
  1230.  
  1231. /** @type {string} - Farewell when closing group. */
  1232. get farewell() {
  1233. return this.#farewell;
  1234. }
  1235.  
  1236. /** @type {string} - console.func to use for opening group. */
  1237. get func() {
  1238. return this.#func;
  1239. }
  1240.  
  1241. /** @type {string} - Greeting when opening group. */
  1242. get greeting() {
  1243. return this.#greeting;
  1244. }
  1245.  
  1246. /** @type {string} - Mode name. */
  1247. get name() {
  1248. return this.#name;
  1249. }
  1250.  
  1251. static #known = new Map();
  1252.  
  1253. #farewell
  1254. #func
  1255. #greeting
  1256. #name
  1257.  
  1258. }
  1259.  
  1260. static {
  1261. Logger.#GroupMode.Silenced = new Logger.#GroupMode('silenced');
  1262. Logger.#GroupMode.Opened = new Logger.#GroupMode(
  1263. 'opened', 'Entered', 'Leaving', 'group'
  1264. );
  1265. Logger.#GroupMode.Closed = new Logger.#GroupMode(
  1266. 'closed', 'Starting', 'Finished', 'groupCollapsed'
  1267. );
  1268.  
  1269. Object.freeze(Logger.#GroupMode);
  1270. }
  1271.  
  1272. static #configs = new DefaultMap(() => new Logger.#Config());
  1273. static #loggers = new DefaultMap(Array);
  1274.  
  1275. /**
  1276. * Set Logger configs from a plain object.
  1277. * @param {object} pojo - Created by {Logger.#toPojo}.
  1278. */
  1279. static #fromPojo = (pojo) => {
  1280. if (pojo && pojo.type === 'LoggerConfigs') {
  1281. this.resetConfigs();
  1282. for (const [k, v] of Object.entries(pojo.entries)) {
  1283. this.#configs.get(k)
  1284. .fromPojo(v);
  1285. }
  1286. Logger.sequence += 1;
  1287. }
  1288. }
  1289.  
  1290. /** @returns {object} - Logger.#configs as a plain object. */
  1291. static #toPojo = () => {
  1292. const pojo = {
  1293. type: 'LoggerConfigs',
  1294. entries: {},
  1295. };
  1296. for (const [k, v] of this.#configs.entries()) {
  1297. pojo.entries[k] = v.toPojo();
  1298. }
  1299. return pojo;
  1300. }
  1301.  
  1302. /**
  1303. * This only resets Logger instances that have know configs.
  1304. *
  1305. * That way, Loggers created during tests wrapped with a save/restore
  1306. * sequence, will not have their configs regenerated.
  1307. */
  1308. static #resetLoggerConfigs = () => {
  1309. for (const key of this.#configs.keys()) {
  1310. // We do not want to accidentally create a key in this DefaultMap.
  1311. if (this.#loggers.has(key)) {
  1312. const loggerArrays = this.#loggers.get(key);
  1313. for (const loggerRef of loggerArrays) {
  1314. const logger = loggerRef.deref();
  1315. if (logger) {
  1316. logger.#config = Logger.config(key);
  1317. }
  1318. }
  1319. }
  1320. }
  1321. }
  1322.  
  1323. /* eslint-disable no-console */
  1324. static #clear = () => {
  1325. console.clear();
  1326. }
  1327.  
  1328. #config
  1329. #groupStack = [];
  1330. #mq = new MessageQueue();
  1331. #name
  1332.  
  1333. #errMsgListener = (...msgs) => {
  1334. console.error(...msgs);
  1335. }
  1336.  
  1337. /**
  1338. * Log a specific message.
  1339. * @param {string} msg - Message to send to console.debug.
  1340. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1341. */
  1342. #log = (msg, ...rest) => {
  1343. const group = this.#groupStack.at(-1);
  1344. this.#config.used(group);
  1345. if (this.enabled && !this.silenced) {
  1346. if (this.includeStackTrace) {
  1347. console.groupCollapsed(`${this.name} call stack`);
  1348. console.trace();
  1349. console.groupEnd();
  1350. }
  1351. console.debug(`${this.name}: ${msg}`, ...rest);
  1352. }
  1353. }
  1354.  
  1355. /**
  1356. * Introduces a specific group.
  1357. * @param {string} group - Group being created.
  1358. * @param {Logger.#GroupMode} defaultMode - Mode to use if new.
  1359. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1360. */
  1361. #intro = (group, defaultMode, ...rest) => {
  1362. this.#groupStack.push(group);
  1363. const mode = this.#config.group(group, defaultMode).mode;
  1364.  
  1365. if (this.enabled && mode !== Logger.#GroupMode.Silenced) {
  1366. console[mode.func](`${this.name}: ${group}`);
  1367. }
  1368.  
  1369. if (rest.length) {
  1370. const msg = `${mode.greeting} ${group} with`;
  1371. this.log(msg, ...rest);
  1372. }
  1373. }
  1374.  
  1375. /**
  1376. * Concludes a specific group.
  1377. * @param {string} group - Group leaving.
  1378. * @param {...*} rest - Arbitrary items to pass to console.debug.
  1379. */
  1380. #outro = (group, ...rest) => {
  1381. const mode = this.#config.group(group).mode;
  1382.  
  1383. let msg = `${mode.farewell} ${group}`;
  1384. if (rest.length) {
  1385. msg += ' with:';
  1386. }
  1387. this.log(msg, ...rest);
  1388.  
  1389. const lastGroup = this.#groupStack.pop();
  1390. if (group !== lastGroup) {
  1391. this.#mq.post(`${this.name}: Logging group mismatch! Received ` +
  1392. `"${group}", expected to see "${lastGroup}"`);
  1393. }
  1394.  
  1395. if (this.enabled && mode !== Logger.#GroupMode.Silenced) {
  1396. console.groupEnd();
  1397. }
  1398. }
  1399. /* eslint-enable */
  1400.  
  1401. /* eslint-disable require-jsdoc */
  1402. /* eslint-disable no-undefined */
  1403. /** This must be nested due to accessing #private fields. */
  1404. static GroupModeTestCase = class extends NH.xunit.TestCase {
  1405.  
  1406. testClassIsFrozen() {
  1407. this.assertRaisesRegExp(TypeError, /is not extensible/u, () => {
  1408. Logger.#GroupMode.Bob = {};
  1409. });
  1410. }
  1411.  
  1412. testInstanceIsFrozen() {
  1413. this.assertRaisesRegExp(TypeError, /is not extensible/u, () => {
  1414. Logger.#GroupMode.Silenced.newProp = 'data';
  1415. });
  1416. }
  1417.  
  1418. testLookupByValidName() {
  1419. // Act
  1420. const gm = Logger.#GroupMode.byName('closed');
  1421.  
  1422. // Assert
  1423. this.assertEqual(gm, Logger.#GroupMode.Closed);
  1424. }
  1425.  
  1426. testLookupByInvalidName() {
  1427. // Act
  1428. const gm = Logger.#GroupMode.byName('nope');
  1429.  
  1430. // Assert
  1431. this.assertEqual(gm, undefined);
  1432. }
  1433.  
  1434. }
  1435. /* eslint-enable */
  1436.  
  1437. }
  1438.  
  1439. NH.xunit.testing.testCases.push(Logger.GroupModeTestCase);
  1440.  
  1441. /* eslint-disable class-methods-use-this */
  1442. /* eslint-disable newline-per-chained-call */
  1443. /* eslint-disable no-magic-numbers */
  1444. /* eslint-disable require-jsdoc */
  1445. class LoggerTestCase extends NH.xunit.TestCase {
  1446.  
  1447. setUp() {
  1448. this.addCleanup(this.restoreConfigs, Logger.configs);
  1449. Logger.resetConfigs();
  1450. }
  1451.  
  1452. restoreConfigs(saved) {
  1453. Logger.configs = saved;
  1454. }
  1455.  
  1456. testReset() {
  1457. // Assemble
  1458. Logger.config(this.id).enabled = true;
  1459.  
  1460. // Act
  1461. Logger.resetConfigs();
  1462.  
  1463. // Assert
  1464. this.assertEqual(Logger.configs.entries, {});
  1465. }
  1466.  
  1467. testInitialValues() {
  1468. // Assemble
  1469. const logger = new Logger(this.id);
  1470.  
  1471. // Assert
  1472. this.assertFalse(logger.enabled, 'enabled');
  1473. this.assertFalse(logger.includeStackTrace, 'stack trace');
  1474. this.assertEqual(Logger.config(this.id).groups.size, 0, 'no groups');
  1475. }
  1476.  
  1477. testGroupDefaults() {
  1478. // Assemble
  1479. const logger = new Logger(this.id);
  1480.  
  1481. // Act
  1482. logger.entered('func');
  1483. logger.starting('loop');
  1484.  
  1485. // Assert
  1486. const groups = Logger.config(this.id).groups;
  1487. this.assertEqual(groups.size, 2, 'we saw two groups');
  1488. this.assertEqual(groups.get('func').mode.name, 'opened', 'func');
  1489. this.assertEqual(groups.get('loop').mode.name, 'closed', 'loop');
  1490. }
  1491.  
  1492. testCountsCollected() {
  1493. // Assemble
  1494. Logger.sequence = 10;
  1495. const logger = new Logger(this.id);
  1496.  
  1497. // Act
  1498. // Results in counts
  1499. logger.log('one');
  1500. logger.log('two');
  1501.  
  1502. // Basic intros do not log a message
  1503. logger.entered('ent1');
  1504.  
  1505. // Intros with extra stuff do log
  1506. logger.entered('ent2', 'extra');
  1507.  
  1508. // Count is in a group
  1509. logger.log('three');
  1510.  
  1511. // Outros cause logs
  1512. logger.leaving('ent2');
  1513. logger.leaving('ent1', 'extra');
  1514.  
  1515. // Assert
  1516.  
  1517. // Some of these are {@link NumberOp}
  1518. this.defaultEqual = this.equalValueOf;
  1519.  
  1520. const config = Logger.config(this.id);
  1521. this.assertEqual(config.callCount, 6, 'call count');
  1522. this.assertEqual(config.sequence, 10, 'sequence');
  1523. this.assertEqual(config.groups.get('null').callCount, 2, 'null count');
  1524. this.assertEqual(config.groups.get('null').sequence, 10, 'null seq');
  1525. this.assertEqual(config.groups.get('ent1').callCount, 1, '1 count');
  1526. this.assertEqual(config.groups.get('ent1').sequence, 10, '1 seq');
  1527. this.assertEqual(config.groups.get('ent2').callCount, 3, '2 count');
  1528. this.assertEqual(config.groups.get('ent2').sequence, 10, '2 seq');
  1529. }
  1530.  
  1531. testExpectMismatchedGroup() {
  1532. // Assemble
  1533. const messages = [];
  1534. const listener = (...msgs) => {
  1535. messages.push(...msgs);
  1536. };
  1537. const logger = new Logger(this.id);
  1538. logger.mq.listen(listener);
  1539.  
  1540. // Act
  1541. logger.entered('one');
  1542. logger.leaving('two');
  1543.  
  1544. // Assert
  1545. this.assertEqual(messages, [
  1546. 'LoggerTestCase.testExpectMismatchedGroup: Logging group mismatch!' +
  1547. ' Received "two", expected to see "one"',
  1548. ]);
  1549. }
  1550.  
  1551. testUpdateGroupByString() {
  1552. // Assemble
  1553. const logger = new Logger(this.id);
  1554. logger.entered('one');
  1555.  
  1556. // Act
  1557. Logger.config('updateGroupByString').group('one').mode = 'silenced';
  1558. this.assertEqual(
  1559. Logger.config('updateGroupByString').group('one').mode.name,
  1560. 'silenced'
  1561. );
  1562. }
  1563.  
  1564. testSaveRestoreConfigsTopLevel() {
  1565. // This test does not strictly follow Assemble/Act/Assert as it has
  1566. // extra verifications during state changes.
  1567.  
  1568. // Some of these are {@link NumberOp}
  1569. this.defaultEqual = this.equalValueOf;
  1570.  
  1571. // Initial
  1572. Logger.config(this.id).includeStackTrace = true;
  1573. const logger = new Logger(this.id);
  1574. logger.log('bumping the call count');
  1575.  
  1576. const savedConfigs = Logger.configs;
  1577.  
  1578. this.assertTrue(Logger.config(this.id).includeStackTrace, 'init trace');
  1579. this.assertEqual(Logger.config(this.id).callCount, 1, 'init count');
  1580.  
  1581. // Reset
  1582. Logger.resetConfigs();
  1583.  
  1584. this.assertFalse(Logger.config(this.id).includeStackTrace,
  1585. 'reset trace');
  1586. this.assertEqual(Logger.config(this.id).callCount, 0, 'reset count');
  1587.  
  1588. // Bob was not present before saving the configs. So, the following
  1589. // tweak away from defaults should reset after restoration.
  1590. Logger.config('Bob').enabled = true;
  1591.  
  1592. // Restore
  1593. Logger.configs = savedConfigs;
  1594.  
  1595. this.assertTrue(Logger.config(this.id).includeStackTrace,
  1596. 'restore trace');
  1597. this.assertEqual(Logger.config(this.id).callCount, 1, 'restore count');
  1598. this.assertFalse(Logger.config('Bob').enabled, 'restore Bob');
  1599. }
  1600.  
  1601. testSaveRestoreConfigsGroups() {
  1602. // This test does not strictly follow Assemble/Act/Assert as it has
  1603. // extra verifications during state changes.
  1604.  
  1605. // Some of these are {@link NumberOp}
  1606. this.defaultEqual = this.equalValueOf;
  1607.  
  1608. const grp = 'a-loop';
  1609.  
  1610. // Initial
  1611. const logger = new Logger(this.id);
  1612. logger.starting(grp);
  1613. logger.finished(grp, 'bumping the call count');
  1614.  
  1615. this.assertEqual(Logger.config(this.id).group(grp).mode.name,
  1616. 'closed',
  1617. 'init mode');
  1618. this.assertEqual(Logger.config(this.id).group(grp).callCount,
  1619. 1,
  1620. 'init count');
  1621.  
  1622. const savedConfigs = Logger.configs;
  1623.  
  1624. // Reset
  1625. Logger.resetConfigs();
  1626.  
  1627. this.assertEqual(Logger.config(this.id).group(grp).mode.name,
  1628. 'opened',
  1629. 'reset mode');
  1630. this.assertEqual(Logger.config(this.id).group(grp).callCount,
  1631. 0,
  1632. 'reset count');
  1633.  
  1634. // Restore
  1635. Logger.configs = savedConfigs;
  1636.  
  1637. this.assertEqual(Logger.config(this.id).group(grp).mode.name,
  1638. 'closed',
  1639. 'restore mode');
  1640. this.assertEqual(Logger.config(this.id).group(grp).callCount,
  1641. 1,
  1642. 'restore count');
  1643. }
  1644.  
  1645. testSaveRestoreBumpsSequenceAboveHighest() {
  1646. const grp = 'some-group';
  1647. Logger.sequence = 23;
  1648. const logger = new Logger(this.id);
  1649.  
  1650. // Just generating a group so it can have a sequence
  1651. logger.starting(grp);
  1652. logger.finished(grp);
  1653.  
  1654. const savedConfigs = Logger.configs;
  1655.  
  1656. this.assertEqual(savedConfigs.entries[this.id].groups[grp].sequence,
  1657. 23,
  1658. 'just checking....');
  1659.  
  1660. savedConfigs.entries[this.id].sequence = 34;
  1661. savedConfigs.entries[this.id].groups[grp].sequence = 42;
  1662.  
  1663. // Restore - sequence should be > max(34, 42) from above
  1664. Logger.configs = savedConfigs;
  1665. this.assertTrue(Logger.sequence > 42, 'better be bumped');
  1666. }
  1667.  
  1668. }
  1669. /* eslint-enable */
  1670.  
  1671. NH.xunit.testing.testCases.push(LoggerTestCase);
  1672.  
  1673. /**
  1674. * Execute TestCase tests.
  1675. * @param {Logger} logger - Logger to use.
  1676. * @returns {boolean} - Success status.
  1677. */
  1678. function doTestCases(logger) {
  1679. const me = 'Running TestCases';
  1680. logger.entered(me);
  1681.  
  1682. const savedConfigs = Logger.configs;
  1683. const result = NH.xunit.runTests();
  1684. Logger.configs = savedConfigs;
  1685.  
  1686. const summary = result.summary(true)
  1687. .join('\n');
  1688. logger.log(`summary:\n${summary}`);
  1689. if (result.errors.length) {
  1690. logger.starting('Errors');
  1691.  
  1692. for (const error of result.errors) {
  1693. logger.log('error:', error);
  1694. }
  1695.  
  1696. logger.finished('Errors');
  1697. }
  1698.  
  1699. if (result.failures.length) {
  1700. logger.starting('Failures');
  1701.  
  1702. for (const failure of result.failures) {
  1703. logger.log('failure:', failure.name, failure.message);
  1704. }
  1705.  
  1706. logger.finished('Failures');
  1707. }
  1708.  
  1709. logger.leaving(me, result.wasSuccessful());
  1710. return result.wasSuccessful();
  1711. }
  1712.  
  1713. /**
  1714. * Basic test runner.
  1715. *
  1716. * This depends on {Logger}, hence the location in this file.
  1717. */
  1718. function runTests() {
  1719. if (NH.xunit.testing.enabled) {
  1720. const logger = new Logger('Testing');
  1721. if (doTestCases(logger)) {
  1722. logger.log('All TestCases passed.');
  1723. } else {
  1724. logger.log('At least one TestCase failed.');
  1725. }
  1726. }
  1727. }
  1728.  
  1729. NH.xunit.testing.run = runTests;
  1730.  
  1731. /**
  1732. * Create a UUID-like string with a base.
  1733. * @param {string} strBase - Base value for the string.
  1734. * @returns {string} - A unique string.
  1735. */
  1736. function uuId(strBase) {
  1737. return `${strBase}-${crypto.randomUUID()}`;
  1738. }
  1739.  
  1740. /**
  1741. * Normalizes a string to be safe to use as an HTML element id.
  1742. * @param {string} input - The string to normalize.
  1743. * @returns {string} - Normlized string.
  1744. */
  1745. function safeId(input) {
  1746. let result = input
  1747. .replaceAll(' ', '-')
  1748. .replaceAll('.', '_')
  1749. .replaceAll(',', '__comma__')
  1750. .replaceAll(':', '__colon__');
  1751. if (!(/^[a-z_]/iu).test(result)) {
  1752. result = `a${result}`;
  1753. }
  1754. return result;
  1755. }
  1756.  
  1757. /* eslint-disable no-undefined */
  1758. /* eslint-disable require-jsdoc */
  1759. class SafeIdTestCase extends NH.xunit.TestCase {
  1760.  
  1761. testNormalInputs() {
  1762. const tests = [
  1763. {text: 'Tabby Cat', expected: 'Tabby-Cat'},
  1764. {text: '_', expected: '_'},
  1765. {text: '', expected: 'a'},
  1766. {text: '0', expected: 'a0'},
  1767. {text: 'a.b.c', expected: 'a_b_c'},
  1768. {text: 'a,b,c', expected: 'a__comma__b__comma__c'},
  1769. {text: 'a:b::c', expected: 'a__colon__b__colon____colon__c'},
  1770. ];
  1771. for (const {text, expected} of tests) {
  1772. this.assertEqual(safeId(text), expected, text);
  1773. }
  1774. }
  1775.  
  1776. testBadInputs() {
  1777. this.assertRaises(
  1778. TypeError,
  1779. () => {
  1780. safeId(undefined);
  1781. },
  1782. 'undefined'
  1783. );
  1784.  
  1785. this.assertRaises(
  1786. TypeError,
  1787. () => {
  1788. safeId(null);
  1789. },
  1790. 'null'
  1791. );
  1792. }
  1793.  
  1794. }
  1795. /* eslint-enable */
  1796.  
  1797. NH.xunit.testing.testCases.push(SafeIdTestCase);
  1798.  
  1799. /**
  1800. * Equivalent (for now) Java's hashCode (do not store externally).
  1801. *
  1802. * Do not expect it to be stable across releases.
  1803. *
  1804. * Implements: s[0]*31(n-1) + s[1]*31(n-2) + ... + s[n-1]
  1805. * @param {string} s - String to hash.
  1806. * @returns {string} - Hash value.
  1807. */
  1808. function strHash(s) {
  1809. let hash = 0;
  1810. for (let i = 0; i < s.length; i += 1) {
  1811. // eslint-disable-next-line no-magic-numbers
  1812. hash = (hash * 31) + s.charCodeAt(i) | 0;
  1813. }
  1814. return `${hash}`;
  1815. }
  1816.  
  1817. /**
  1818. * Separate a string of concatenated words along transitions.
  1819. *
  1820. * Transitions are:
  1821. * lower to upper (lowerUpper -> lower Upper)
  1822. * grouped upper to lower (ABCd -> AB Cd)
  1823. * underscores (snake_case -> snake case)
  1824. * spaces
  1825. * character/numbers (lower2Upper -> lower 2 Upper)
  1826. * Likely only works with ASCII.
  1827. * Empty strings return an empty array.
  1828. * Extra separators are consolidated.
  1829. * @param {string} text - Text to parse.
  1830. * @returns {string[]} - Parsed text.
  1831. */
  1832. function simpleParseWords(text) {
  1833. const results = [];
  1834.  
  1835. const working = [text];
  1836. const moreWork = [];
  1837.  
  1838. while (working.length || moreWork.length) {
  1839. if (working.length === 0) {
  1840. working.push(...moreWork);
  1841. moreWork.length = 0;
  1842. }
  1843.  
  1844. // Unicode categories used below:
  1845. // L - Letter
  1846. // Ll - Letter, lower
  1847. // Lu - Letter, upper
  1848. // N - Number
  1849. let word = working.shift();
  1850. if (word) {
  1851. word = word.replace(
  1852. /(?<lower>\p{Ll})(?<upper>\p{Lu})/u,
  1853. '$<lower> $<upper>'
  1854. );
  1855.  
  1856. word = word.replace(
  1857. /(?<upper>\p{Lu}+)(?<lower>\p{Lu}\p{Ll})/u,
  1858. '$<upper> $<lower>'
  1859. );
  1860.  
  1861. word = word.replace(
  1862. /(?<letter>\p{L})(?<number>\p{N})/u,
  1863. '$<letter> $<number>'
  1864. );
  1865.  
  1866. word = word.replace(
  1867. /(?<number>\p{N})(?<letter>\p{L})/u,
  1868. '$<number> $<letter>'
  1869. );
  1870.  
  1871. const split = word.split(/[ _]/u);
  1872. if (split.length > 1 || moreWork.length) {
  1873. moreWork.push(...split);
  1874. } else {
  1875. results.push(word);
  1876. }
  1877. }
  1878. }
  1879.  
  1880. return results;
  1881. }
  1882.  
  1883. /* eslint-disable require-jsdoc */
  1884. class SimpleParseWordsTestCase extends NH.xunit.TestCase {
  1885.  
  1886. testEmpty() {
  1887. // Act
  1888. const actual = simpleParseWords('');
  1889.  
  1890. // Assert
  1891. this.assertEqual(actual, []);
  1892. }
  1893.  
  1894. testSeparatorsOnly() {
  1895. // Act
  1896. const actual = simpleParseWords(' _ __ _');
  1897.  
  1898. // Assert
  1899. this.assertEqual(actual, []);
  1900. }
  1901.  
  1902. testAllLower() {
  1903. // Act
  1904. const actual = simpleParseWords('lower');
  1905.  
  1906. // Assert
  1907. const expected = ['lower'];
  1908. this.assertEqual(actual, expected);
  1909. }
  1910.  
  1911. testAllUpper() {
  1912. // Act
  1913. const actual = simpleParseWords('UPPER');
  1914.  
  1915. // Assert
  1916. const expected = ['UPPER'];
  1917. this.assertEqual(actual, expected);
  1918. }
  1919.  
  1920. testMixed() {
  1921. // Act
  1922. const actual = simpleParseWords('Mixed');
  1923.  
  1924. // Assert
  1925. const expected = ['Mixed'];
  1926. this.assertEqual(actual, expected);
  1927. }
  1928.  
  1929. testSimpleCamelCase() {
  1930. // Act
  1931. const actual = simpleParseWords('SimpleCamelCase');
  1932.  
  1933. // Assert
  1934. const expected = ['Simple', 'Camel', 'Case'];
  1935. this.assertEqual(actual, expected);
  1936. }
  1937.  
  1938. testLongCamelCase() {
  1939. // Act
  1940. const actual = simpleParseWords('AnUPPERWord');
  1941.  
  1942. // Assert
  1943. const expected = ['An', 'UPPER', 'Word'];
  1944. this.assertEqual(actual, expected);
  1945. }
  1946.  
  1947. testLowerCamelCase() {
  1948. // Act
  1949. const actual = simpleParseWords('lowerCamelCase');
  1950.  
  1951. // Assert
  1952. const expected = ['lower', 'Camel', 'Case'];
  1953. this.assertEqual(actual, expected);
  1954. }
  1955.  
  1956. testSnakeCase() {
  1957. // Act
  1958. const actual = simpleParseWords('snake_case_Example');
  1959.  
  1960. // Assert
  1961. const expected = ['snake', 'case', 'Example'];
  1962. this.assertEqual(actual, expected);
  1963. }
  1964.  
  1965. testDoubleSnakeCase() {
  1966. // Act
  1967. const actual = simpleParseWords('double__snake_Case_example');
  1968.  
  1969. // Assert
  1970. const expected = ['double', 'snake', 'Case', 'example'];
  1971. this.assertEqual(actual, expected);
  1972. }
  1973.  
  1974. testWithNumbers() {
  1975. // Act
  1976. const actual = simpleParseWords('One23fourFive');
  1977.  
  1978. // Assert
  1979. const expected = ['One', '23', 'four', 'Five'];
  1980. this.assertEqual(actual, expected);
  1981. }
  1982.  
  1983. testWithSpaces() {
  1984. // Act
  1985. const actual = simpleParseWords('ABCd EF ghIj');
  1986.  
  1987. // Assert
  1988. const expected = ['AB', 'Cd', 'EF', 'gh', 'Ij'];
  1989. this.assertEqual(actual, expected);
  1990. }
  1991.  
  1992. testComplicated() {
  1993. // Act
  1994. const actual = simpleParseWords(
  1995. 'A_VERYComplicated_Wordy __ _ Example'
  1996. );
  1997.  
  1998. // Assert
  1999. const expected = ['A', 'VERY', 'Complicated', 'Wordy', 'Example'];
  2000. this.assertEqual(actual, expected);
  2001. }
  2002.  
  2003. }
  2004. /* eslint-enable */
  2005.  
  2006. NH.xunit.testing.testCases.push(SimpleParseWordsTestCase);
  2007.  
  2008. /**
  2009. * Base class for building services that can be turned on and off.
  2010. *
  2011. * Subclasses should NOT override methods here, except for constructor().
  2012. * Instead they should register listeners for appropriate events.
  2013. *
  2014. * Generally, methods will fire two event verbs. The first, in present
  2015. * tense, will instruct what should happen (activate, deactivate). The
  2016. * second, in past tense, will describe what should have happened
  2017. * (activated, deactivated). Typically, subclasses will act upon the
  2018. * present tense, and users of the class may act upon the past tense.
  2019. *
  2020. * @example
  2021. * class DummyService extends Service {
  2022. *
  2023. * constructor(name, dummyArgs) {
  2024. * super(`The ${name}`);
  2025. * this.#args = dummyArgs
  2026. * this.on('activate', this.#onActivate)
  2027. * .on('deactivate', this.#onDeactivate);
  2028. * }
  2029. *
  2030. * #onActivate = (event) => {
  2031. * ... do activate stuff with this.#args ...
  2032. * }
  2033. *
  2034. * #onDeactivate = (event) => {
  2035. * ... do deactivate stuff with this.#args ...
  2036. * }
  2037. *
  2038. * }
  2039. *
  2040. * ... else where ...
  2041. * function dummyEventCallback(event, svc) {
  2042. * console.info(`${svc.name}` was ${event}`);
  2043. * }
  2044. *
  2045. * const service = new DummyService('Bob', bobInfo)
  2046. * .on('activated', dummyEventCallback)
  2047. * .on('deactivated', dummyEventCallback);
  2048. * service.activate();
  2049. * service.deactivate();
  2050. *
  2051. */
  2052. class Service {
  2053.  
  2054. /** @param {string} name - Custom portion of this instance. */
  2055. constructor(name) {
  2056. if (new.target === Service) {
  2057. throw new TypeError('Abstract class; do not instantiate directly.');
  2058. }
  2059. this.#name = `${this.constructor.name}: ${name}`;
  2060. this.#shortName = name;
  2061. this.#dispatcher = new Dispatcher(...Service.#knownEvents);
  2062. this.#logger = new Logger(this.#name);
  2063. }
  2064.  
  2065. /** @type {Logger} - Logger instance. */
  2066. get logger() {
  2067. return this.#logger;
  2068. }
  2069.  
  2070. /** @type {string} - Instance name. */
  2071. get name() {
  2072. return this.#name;
  2073. }
  2074.  
  2075. /** @type {string} - Shorter instance name. */
  2076. get shortName() {
  2077. return this.#shortName;
  2078. }
  2079.  
  2080. /**
  2081. * Called each time service is activated.
  2082. *
  2083. * @fires 'activate' 'activated'
  2084. */
  2085. activate() {
  2086. if (!this.#activated || this.#allowReactivation) {
  2087. this.#dispatcher.fire('activate', this);
  2088. this.#dispatcher.fire('activated', this);
  2089. }
  2090. this.#activated = true;
  2091. }
  2092.  
  2093. /**
  2094. * Called each time service is deactivated.
  2095. *
  2096. * @fires 'deactivate' 'deactivated'
  2097. */
  2098. deactivate() {
  2099. this.#dispatcher.fire('deactivate', this);
  2100. this.#dispatcher.fire('deactivated', this);
  2101. this.#activated = false;
  2102. }
  2103.  
  2104. /**
  2105. * Attach a function to an eventType.
  2106. * @param {string} eventType - Event type to connect with.
  2107. * @param {Dispatcher~Handler} func - Single argument function to
  2108. * call.
  2109. * @returns {Service} - This instance, for chaining.
  2110. */
  2111. on(eventType, func) {
  2112. this.#dispatcher.on(eventType, func);
  2113. return this;
  2114. }
  2115.  
  2116. /**
  2117. * Remove all instances of a function registered to an eventType.
  2118. * @param {string} eventType - Event type to disconnect from.
  2119. * @param {Dispatcher~Handler} func - Function to remove.
  2120. * @returns {Service} - This instance, for chaining.
  2121. */
  2122. off(eventType, func) {
  2123. this.#dispatcher.off(eventType, func);
  2124. return this;
  2125. }
  2126.  
  2127. /**
  2128. * @param {boolean} allow - Whether to allow this service to be activated
  2129. * when already active.
  2130. * @returns {ScrollerService} - This instance, for chaining.
  2131. */
  2132. allowReactivation(allow) {
  2133. this.#allowReactivation = allow;
  2134. return this;
  2135. }
  2136.  
  2137. static #knownEvents = [
  2138. 'activate',
  2139. 'activated',
  2140. 'deactivate',
  2141. 'deactivated',
  2142. ];
  2143.  
  2144. #activated = false
  2145. #allowReactivation = true
  2146. #dispatcher
  2147. #logger
  2148. #name
  2149. #shortName
  2150.  
  2151. }
  2152.  
  2153. /* eslint-disable max-lines-per-function */
  2154. /* eslint-disable max-statements */
  2155. /* eslint-disable no-new */
  2156. /* eslint-disable require-jsdoc */
  2157. class ServiceTestCase extends NH.xunit.TestCase {
  2158.  
  2159. static Test = class extends Service {
  2160.  
  2161. constructor(name) {
  2162. super(`The ${name}`);
  2163. this.on('activate', this.#onEvent)
  2164. .on('deactivated', this.#onEvent);
  2165. }
  2166.  
  2167. set mq(val) {
  2168. this.#mq = val;
  2169. }
  2170.  
  2171. #mq
  2172.  
  2173. #onEvent = (evt, data) => {
  2174. this.#mq.post('via Service', evt, data.shortName);
  2175. }
  2176.  
  2177. }
  2178.  
  2179. testAbstract() {
  2180. this.assertRaises(TypeError, () => {
  2181. new Service();
  2182. });
  2183. }
  2184.  
  2185. testProperties() {
  2186. // Assemble
  2187. const s = new ServiceTestCase.Test(this.id);
  2188.  
  2189. // Assert
  2190. this.assertEqual(
  2191. s.name, 'Test: The ServiceTestCase.testProperties', 'name'
  2192. );
  2193. this.assertEqual(
  2194. s.shortName, 'The ServiceTestCase.testProperties', 'short name'
  2195. );
  2196. }
  2197.  
  2198. testSimpleEvents() {
  2199. // Assemble
  2200. const s = new ServiceTestCase.Test(this.id);
  2201. const mq = new MessageQueue();
  2202. s.mq = mq;
  2203.  
  2204. const messages = [];
  2205. const capture = (...message) => {
  2206. messages.push(message);
  2207. };
  2208. const cb = (evt, service) => {
  2209. mq.post('via cb', evt, service.name);
  2210. };
  2211.  
  2212. const shortName = 'The ServiceTestCase.testSimpleEvents';
  2213. const longName = 'Test: The ServiceTestCase.testSimpleEvents';
  2214.  
  2215. // Act I - Basic captures
  2216. s.on('activated', cb)
  2217. .on('deactivate', cb);
  2218. s.activate();
  2219. s.deactivate();
  2220.  
  2221. mq.listen(capture);
  2222.  
  2223. // Assert
  2224. this.assertEqual(
  2225. messages,
  2226. [
  2227. ['via Service', 'activate', shortName],
  2228. ['via cb', 'activated', longName],
  2229. ['via cb', 'deactivate', longName],
  2230. ['via Service', 'deactivated', shortName],
  2231. ],
  2232. 'first run through'
  2233. );
  2234.  
  2235. messages.length = 0;
  2236. // Act II - Make sure *off()* is wired in.
  2237. s.off('deactivate', cb);
  2238.  
  2239. s.activate();
  2240. s.deactivate();
  2241.  
  2242. // Assert
  2243. this.assertEqual(
  2244. messages,
  2245. [
  2246. ['via Service', 'activate', shortName],
  2247. ['via cb', 'activated', longName],
  2248. // No deactivate in this spot this time
  2249. ['via Service', 'deactivated', shortName],
  2250. ],
  2251. 'second run through'
  2252. );
  2253. }
  2254.  
  2255. testReactivation() {
  2256. // Assemble
  2257. const messages = [];
  2258. const capture = (...message) => {
  2259. messages.push(message);
  2260. };
  2261.  
  2262. const s = new ServiceTestCase.Test(this.id);
  2263. s.mq = new MessageQueue()
  2264. .listen(capture);
  2265.  
  2266. const shortName = `The ${this.id}`;
  2267.  
  2268. // Act I - Allowed by default
  2269. s.activate();
  2270. s.activate();
  2271. s.deactivate();
  2272.  
  2273. // Assert
  2274. this.assertEqual(
  2275. messages,
  2276. [
  2277. ['via Service', 'activate', shortName],
  2278. // Activation while active worked
  2279. ['via Service', 'activate', shortName],
  2280. ['via Service', 'deactivated', shortName],
  2281. ],
  2282. 'allowed by default'
  2283. );
  2284.  
  2285. // Act II - Turning off works
  2286. messages.length = 0;
  2287. s.allowReactivation(false);
  2288.  
  2289. s.activate();
  2290. s.activate();
  2291. s.deactivate();
  2292.  
  2293. // Assert
  2294. this.assertEqual(
  2295. messages,
  2296. [
  2297. ['via Service', 'activate', shortName],
  2298. // No reactivation here
  2299. ['via Service', 'deactivated', shortName],
  2300. ],
  2301. 'turning off works'
  2302. );
  2303.  
  2304. // Act III - Turning back on works
  2305. messages.length = 0;
  2306. s.allowReactivation(true);
  2307.  
  2308. s.activate();
  2309. s.activate();
  2310. s.deactivate();
  2311.  
  2312. // Assert
  2313. this.assertEqual(
  2314. messages,
  2315. [
  2316. ['via Service', 'activate', shortName],
  2317. // Activation while active worked
  2318. ['via Service', 'activate', shortName],
  2319. ['via Service', 'deactivated', shortName],
  2320. ],
  2321. 'turning back on works'
  2322. );
  2323. }
  2324.  
  2325. }
  2326. /* eslint-enable */
  2327.  
  2328. NH.xunit.testing.testCases.push(ServiceTestCase);
  2329.  
  2330. return {
  2331. version: version,
  2332. NOT_FOUND: NOT_FOUND,
  2333. ONE_ITEM: ONE_ITEM,
  2334. ensure: ensure,
  2335. Exception: Exception,
  2336. Dispatcher: Dispatcher,
  2337. MessageQueue: MessageQueue,
  2338. issues: issues,
  2339. DefaultMap: DefaultMap,
  2340. Logger: Logger,
  2341. uuId: uuId,
  2342. safeId: safeId,
  2343. strHash: strHash,
  2344. simpleParseWords: simpleParseWords,
  2345. Service: Service,
  2346. };
  2347.  
  2348. }());