create-debugger.spec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import sinon from 'sinon';
  2. import {expect} from 'chai';
  3. import {inspect} from 'util';
  4. import {createDebugger} from './create-debugger.js';
  5. // вспомогательная функция для удаления ANSI escape-кодов (цветов)
  6. // eslint-disable-next-line no-control-regex
  7. const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, '');
  8. describe('createDebugger', function () {
  9. let consoleLogSpy;
  10. let originalDebugEnv;
  11. let originalDebuggerNamespaceEnv;
  12. let originalLocalStorage;
  13. beforeEach(function () {
  14. // шпионим за console.log перед каждым тестом
  15. consoleLogSpy = sinon.spy(console, 'log');
  16. // сохраняем исходные переменные окружения
  17. originalDebugEnv = process.env.DEBUG;
  18. originalDebuggerNamespaceEnv = process.env.DEBUGGER_NAMESPACE;
  19. // базовая симуляция localStorage для тестов
  20. originalLocalStorage = global.localStorage;
  21. global.localStorage = {
  22. _store: {},
  23. getItem(key) {
  24. return this._store[key] || null;
  25. },
  26. setItem(key, value) {
  27. this._store[key] = String(value);
  28. },
  29. removeItem(key) {
  30. delete this._store[key];
  31. },
  32. clear() {
  33. this._store = {};
  34. },
  35. };
  36. // сбрасываем переменные перед тестом
  37. delete process.env.DEBUG;
  38. delete process.env.DEBUGGER_NAMESPACE;
  39. global.localStorage.clear();
  40. });
  41. afterEach(function () {
  42. // восстанавливаем console.log
  43. consoleLogSpy.restore();
  44. // восстанавливаем переменные окружения
  45. if (originalDebugEnv === undefined) {
  46. delete process.env.DEBUG;
  47. } else {
  48. process.env.DEBUG = originalDebugEnv;
  49. }
  50. if (originalDebuggerNamespaceEnv === undefined) {
  51. delete process.env.DEBUGGER_NAMESPACE;
  52. } else {
  53. process.env.DEBUGGER_NAMESPACE = originalDebuggerNamespaceEnv;
  54. }
  55. // восстанавливаем localStorage
  56. global.localStorage = originalLocalStorage;
  57. });
  58. describe('general', function () {
  59. it('should create a debugger function', function () {
  60. const debug = createDebugger('test');
  61. expect(debug).to.be.a('function');
  62. expect(debug.withNs).to.be.a('function');
  63. expect(debug.withHash).to.be.a('function');
  64. expect(debug.withOffset).to.be.a('function');
  65. });
  66. it('should output a simple string message when enabled', function () {
  67. process.env.DEBUG = 'test';
  68. const debug = createDebugger('test');
  69. debug('hello world');
  70. expect(consoleLogSpy.calledOnce).to.be.true;
  71. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  72. 'test hello world',
  73. );
  74. });
  75. it('should not output if not enabled via DEBUG', function () {
  76. process.env.DEBUG = 'other';
  77. const debug = createDebugger('test');
  78. debug('hello world');
  79. expect(consoleLogSpy.called).to.be.false;
  80. });
  81. it('should output formatted string messages using %s, %v, %l', function () {
  82. process.env.DEBUG = 'format';
  83. const debug = createDebugger('format');
  84. debug('hello %s', 'world');
  85. debug('value is %v', 123);
  86. debug('list: %l', ['a', 1, true]);
  87. expect(consoleLogSpy.calledThrice).to.be.true;
  88. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.equal(
  89. 'format hello world',
  90. );
  91. expect(stripAnsi(consoleLogSpy.getCall(1).args[0])).to.equal(
  92. 'format value is 123',
  93. );
  94. expect(stripAnsi(consoleLogSpy.getCall(2).args[0])).to.equal(
  95. 'format list: "a", 1, true',
  96. );
  97. });
  98. it('should output object inspection', function () {
  99. process.env.DEBUG = 'obj';
  100. const debug = createDebugger('obj');
  101. const data = {a: 1, b: {c: 'deep'}};
  102. debug(data);
  103. // ожидаем, что inspect будет вызван и его результат
  104. // будет выведен построчно
  105. const expectedInspect = inspect(data, {
  106. colors: true,
  107. depth: null,
  108. compact: false,
  109. });
  110. const expectedLines = expectedInspect.split('\n');
  111. expect(consoleLogSpy.callCount).to.equal(expectedLines.length);
  112. expectedLines.forEach((line, index) => {
  113. // проверяем каждую строку с префиксом
  114. // замечание: точное сравнение с inspect может быть хрупким из-за версий node/util
  115. // здесь мы проверяем, что префикс есть и остальная часть строки соответствует inspect
  116. expect(stripAnsi(consoleLogSpy.getCall(index).args[0])).to.contain(
  117. 'obj ',
  118. );
  119. // ожидаем, что строка вывода содержит соответствующую строку из inspect (без цвета)
  120. expect(stripAnsi(consoleLogSpy.getCall(index).args[0])).to.have.string(
  121. stripAnsi(line),
  122. );
  123. });
  124. });
  125. it('should output object inspection with a description', function () {
  126. process.env.DEBUG = 'objdesc';
  127. const debug = createDebugger('objdesc');
  128. const data = {email: 'test@example.com'};
  129. const description = 'User data:';
  130. debug(data, description);
  131. const expectedInspect = inspect(data, {
  132. colors: true,
  133. depth: null,
  134. compact: false,
  135. });
  136. const expectedLines = expectedInspect.split('\n');
  137. // 1 для описания + строки объекта
  138. const totalExpectedCalls = 1 + expectedLines.length;
  139. expect(consoleLogSpy.callCount).to.equal(totalExpectedCalls);
  140. // первая строка - описание
  141. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.equal(
  142. `objdesc ${description}`,
  143. );
  144. // последующие строки - объект
  145. expectedLines.forEach((line, index) => {
  146. const callIndex = index + 1;
  147. expect(stripAnsi(consoleLogSpy.getCall(callIndex).args[0])).to.contain(
  148. 'objdesc ',
  149. );
  150. expect(
  151. stripAnsi(consoleLogSpy.getCall(callIndex).args[0]),
  152. ).to.have.string(stripAnsi(line));
  153. });
  154. });
  155. });
  156. describe('namespaces', function () {
  157. it('should use namespace provided in createDebugger', function () {
  158. process.env.DEBUG = 'app';
  159. const debug = createDebugger('app');
  160. debug('message');
  161. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  162. 'app message',
  163. );
  164. });
  165. it('should use namespace from DEBUGGER_NAMESPACE env variable', function () {
  166. process.env.DEBUGGER_NAMESPACE = 'base';
  167. // должен быть включен для вывода
  168. process.env.DEBUG = 'base';
  169. // без явного namespace
  170. const debug = createDebugger();
  171. debug('message');
  172. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  173. 'base message',
  174. );
  175. });
  176. it('should combine DEBUGGER_NAMESPACE and createDebugger namespace', function () {
  177. process.env.DEBUGGER_NAMESPACE = 'base';
  178. process.env.DEBUG = 'base:app';
  179. const debug = createDebugger('app');
  180. debug('message');
  181. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  182. 'base:app message',
  183. );
  184. });
  185. it('should extend namespace with withNs()', function () {
  186. process.env.DEBUG = 'app:service';
  187. const debugApp = createDebugger('app');
  188. const debugService = debugApp.withNs('service');
  189. debugService('message');
  190. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  191. 'app:service message',
  192. );
  193. });
  194. it('should extend namespace with multiple args in withNs()', function () {
  195. process.env.DEBUG = 'app:service:module';
  196. const debugApp = createDebugger('app');
  197. const debugService = debugApp.withNs('service', 'module');
  198. debugService('message');
  199. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  200. 'app:service:module message',
  201. );
  202. });
  203. it('should allow chaining withNs()', function () {
  204. process.env.DEBUG = 'app:service:module';
  205. const debug = createDebugger('app').withNs('service').withNs('module');
  206. debug('message');
  207. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  208. 'app:service:module message',
  209. );
  210. });
  211. it('should throw error if withNs is called with non-string', function () {
  212. const debug = createDebugger('app');
  213. expect(() => debug.withNs(123)).to.throw(/must be a non-empty String/);
  214. expect(() => debug.withNs(null)).to.throw(/must be a non-empty String/);
  215. expect(() => debug.withNs('')).to.throw(/must be a non-empty String/);
  216. });
  217. });
  218. describe('DEBUG / localStorage', function () {
  219. it('should enable debugger based on exact match in DEBUG', function () {
  220. process.env.DEBUG = 'app:service';
  221. const debug = createDebugger('app:service');
  222. debug('message');
  223. expect(consoleLogSpy.called).to.be.true;
  224. });
  225. it('should disable debugger if no match in DEBUG', function () {
  226. process.env.DEBUG = 'app:other';
  227. const debug = createDebugger('app:service');
  228. debug('message');
  229. expect(consoleLogSpy.called).to.be.false;
  230. });
  231. it('should enable debugger based on wildcard match in DEBUG (*)', function () {
  232. process.env.DEBUG = 'app:*';
  233. const debugService = createDebugger('app:service');
  234. const debugDb = createDebugger('app:db');
  235. const debugOther = createDebugger('other:app');
  236. debugService('message svc');
  237. debugDb('message db');
  238. debugOther('message other');
  239. expect(consoleLogSpy.calledTwice).to.be.true;
  240. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  241. 'app:service message svc',
  242. );
  243. expect(stripAnsi(consoleLogSpy.secondCall.args[0])).to.equal(
  244. 'app:db message db',
  245. );
  246. });
  247. it('should enable all debuggers if DEBUG=*', function () {
  248. process.env.DEBUG = '*';
  249. const debug1 = createDebugger('app:service');
  250. const debug2 = createDebugger('other');
  251. debug1('msg 1');
  252. debug2('msg 2');
  253. expect(consoleLogSpy.calledTwice).to.be.true;
  254. });
  255. it('should handle multiple patterns in DEBUG (comma)', function () {
  256. process.env.DEBUG = 'app:*,svc:auth';
  257. const debugAppSvc = createDebugger('app:service');
  258. const debugAppDb = createDebugger('app:db');
  259. const debugSvcAuth = createDebugger('svc:auth');
  260. const debugSvcOther = createDebugger('svc:other');
  261. const debugOther = createDebugger('other');
  262. debugAppSvc('1');
  263. debugAppDb('2');
  264. debugSvcAuth('3');
  265. // не должен выводиться
  266. debugSvcOther('4');
  267. debugOther('5');
  268. expect(consoleLogSpy.calledThrice).to.be.true;
  269. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.contain(
  270. 'app:service 1',
  271. );
  272. expect(stripAnsi(consoleLogSpy.getCall(1).args[0])).to.contain(
  273. 'app:db 2',
  274. );
  275. expect(stripAnsi(consoleLogSpy.getCall(2).args[0])).to.contain(
  276. 'svc:auth 3',
  277. );
  278. });
  279. it('should handle multiple patterns in DEBUG (space)', function () {
  280. // используем пробел
  281. process.env.DEBUG = 'app:* svc:auth';
  282. const debugAppSvc = createDebugger('app:service');
  283. const debugSvcAuth = createDebugger('svc:auth');
  284. const debugOther = createDebugger('other');
  285. debugAppSvc('1');
  286. debugSvcAuth('3');
  287. // не должен выводиться
  288. debugOther('5');
  289. expect(consoleLogSpy.calledTwice).to.be.true;
  290. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.contain(
  291. 'app:service 1',
  292. );
  293. expect(stripAnsi(consoleLogSpy.getCall(1).args[0])).to.contain(
  294. 'svc:auth 3',
  295. );
  296. });
  297. it('should use localStorage pattern if DEBUG env is not set', function () {
  298. // process.env.debug не установлен (по умолчанию в beforeEach)
  299. global.localStorage.setItem('debug', 'local:*');
  300. const debugLocal = createDebugger('local:test');
  301. const debugOther = createDebugger('other:test');
  302. debugLocal('message local');
  303. debugOther('message other');
  304. expect(consoleLogSpy.calledOnce).to.be.true;
  305. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  306. 'local:test message local',
  307. );
  308. });
  309. it('should prioritize DEBUG env over localStorage', function () {
  310. process.env.DEBUG = 'env:*';
  311. global.localStorage.setItem('debug', 'local:*');
  312. const debugEnv = createDebugger('env:test');
  313. const debugLocal = createDebugger('local:test');
  314. debugEnv('message env');
  315. // не должен выводиться
  316. debugLocal('message local');
  317. expect(consoleLogSpy.calledOnce).to.be.true;
  318. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.equal(
  319. 'env:test message env',
  320. );
  321. });
  322. });
  323. describe('hashing', function () {
  324. it('should add a hash prefix with withHash()', function () {
  325. process.env.DEBUG = 'hash';
  326. // default length 4
  327. const debug = createDebugger('hash').withHash();
  328. debug('message');
  329. expect(consoleLogSpy.calledOnce).to.be.true;
  330. // проверяем формат: namespace:hash message
  331. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.match(
  332. /^hash:[a-f0-9]{4} message$/,
  333. );
  334. });
  335. it('should use the same hash for multiple calls on the same instance', function () {
  336. process.env.DEBUG = 'hash';
  337. const debug = createDebugger('hash').withHash();
  338. debug('message1');
  339. debug('message2');
  340. expect(consoleLogSpy.calledTwice).to.be.true;
  341. const hash1 = stripAnsi(consoleLogSpy.getCall(0).args[0]).match(
  342. /hash:([a-f0-9]{4})/,
  343. )[1];
  344. const hash2 = stripAnsi(consoleLogSpy.getCall(1).args[0]).match(
  345. /hash:([a-f0-9]{4})/,
  346. )[1];
  347. expect(hash1).to.equal(hash2);
  348. });
  349. it('should generate different hashes for different instances', function () {
  350. process.env.DEBUG = 'hash';
  351. const debug1 = createDebugger('hash').withHash();
  352. const debug2 = createDebugger('hash').withHash();
  353. debug1('m1');
  354. debug2('m2');
  355. expect(consoleLogSpy.calledTwice).to.be.true;
  356. const hash1 = stripAnsi(consoleLogSpy.getCall(0).args[0]).match(
  357. /hash:([a-f0-9]{4})/,
  358. )[1];
  359. const hash2 = stripAnsi(consoleLogSpy.getCall(1).args[0]).match(
  360. /hash:([a-f0-9]{4})/,
  361. )[1];
  362. // вероятность коллизии крайне мала для 4 символов
  363. expect(hash1).to.not.equal(hash2);
  364. });
  365. it('should allow specifying hash length in withHash()', function () {
  366. process.env.DEBUG = 'hash';
  367. const debug = createDebugger('hash').withHash(8);
  368. debug('message');
  369. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.match(
  370. /^hash:[a-f0-9]{8} message$/,
  371. );
  372. });
  373. it('should throw error if withHash is called with invalid length', function () {
  374. const debug = createDebugger('app');
  375. expect(() => debug.withHash(0)).to.throw(/must be a positive Number/);
  376. expect(() => debug.withHash(-1)).to.throw(/must be a positive Number/);
  377. expect(() => debug.withHash(null)).to.throw(/must be a positive Number/);
  378. expect(() => debug.withHash('abc')).to.throw(/must be a positive Number/);
  379. });
  380. });
  381. describe('offset', function () {
  382. // предупреждение: ожидания в этом тесте (`offset message1`) могут не соответствовать
  383. // предполагаемой логике (2 пробела на уровень), проверьте реализацию `getPrefix` и `offsetStep`.
  384. it('should add offset spaces with withOffset()', function () {
  385. process.env.DEBUG = 'offset';
  386. const debug1 = createDebugger('offset').withOffset(1);
  387. const debug2 = createDebugger('offset').withOffset(2);
  388. debug1('message1');
  389. debug2('message2');
  390. expect(consoleLogSpy.calledTwice).to.be.true;
  391. // проверяем отступы (этот комментарий может быть неточен, см. предупреждение выше)
  392. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.equal(
  393. 'offset message1',
  394. );
  395. expect(stripAnsi(consoleLogSpy.getCall(1).args[0])).to.equal(
  396. 'offset message2',
  397. );
  398. });
  399. it('should apply offset to all lines of object inspection', function () {
  400. process.env.DEBUG = 'offsetobj';
  401. const debug = createDebugger('offsetobj').withOffset(1);
  402. const data = {a: 1, b: 2};
  403. debug(data);
  404. const expectedInspect = inspect(data, {
  405. colors: true,
  406. depth: null,
  407. compact: false,
  408. });
  409. const expectedLines = expectedInspect.split('\n');
  410. expect(consoleLogSpy.callCount).to.equal(expectedLines.length);
  411. expectedLines.forEach((line, index) => {
  412. // ожидаем префикс + отступ + текст строки
  413. // предупреждение: \s{2} здесь может не соответствовать ожиданиям в других тестах смещения.
  414. expect(stripAnsi(consoleLogSpy.getCall(index).args[0])).to.match(
  415. /^offsetobj\s{2}/,
  416. );
  417. expect(stripAnsi(consoleLogSpy.getCall(index).args[0])).to.contain(
  418. stripAnsi(line),
  419. );
  420. });
  421. });
  422. it('should throw error if withOffset is called with invalid size', function () {
  423. const debug = createDebugger('app');
  424. expect(() => debug.withOffset(0)).to.throw(/must be a positive Number/);
  425. expect(() => debug.withOffset(-1)).to.throw(/must be a positive Number/);
  426. expect(() => debug.withOffset(null)).to.throw(
  427. /must be a positive Number/,
  428. );
  429. expect(() => debug.withOffset('abc')).to.throw(
  430. /must be a positive Number/,
  431. );
  432. });
  433. });
  434. describe('combine', function () {
  435. it('should combine namespace, hash, and offset', function () {
  436. process.env.DEBUG = 'app:svc';
  437. const debug = createDebugger('app')
  438. .withNs('svc')
  439. .withHash(5)
  440. .withOffset(1);
  441. debug('combined message');
  442. expect(consoleLogSpy.calledOnce).to.be.true;
  443. // ожидаемый формат: namespace:hash<offset>message
  444. // предупреждение: \s{4} здесь может не соответствовать ожиданиям в других тестах смещения.
  445. expect(stripAnsi(consoleLogSpy.firstCall.args[0])).to.match(
  446. /^app:svc:[a-f0-9]{5}\s{4}combined message$/,
  447. );
  448. });
  449. it('should combine features and output object correctly', function () {
  450. process.env.DEBUG = 'app:svc';
  451. const debug = createDebugger('app')
  452. .withNs('svc')
  453. .withHash(3)
  454. .withOffset(1);
  455. const data = {id: 123};
  456. const description = 'Data:';
  457. debug(data, description);
  458. const expectedInspect = inspect(data, {
  459. colors: true,
  460. depth: null,
  461. compact: false,
  462. });
  463. const expectedLines = expectedInspect.split('\n');
  464. const totalExpectedCalls = 1 + expectedLines.length;
  465. expect(consoleLogSpy.callCount).to.equal(totalExpectedCalls);
  466. // проверяем строку описания
  467. // предупреждение: \s{4} здесь может не соответствовать ожиданиям в других тестах смещения.
  468. expect(stripAnsi(consoleLogSpy.getCall(0).args[0])).to.match(
  469. /^app:svc:[a-f0-9]{3}\s{4}Data:$/,
  470. );
  471. // проверяем строки объекта
  472. expectedLines.forEach((line, index) => {
  473. const callIndex = index + 1;
  474. const logLine = stripAnsi(consoleLogSpy.getCall(callIndex).args[0]);
  475. // префикс, хэш, отступ
  476. // предупреждение: \s{2} здесь может не соответствовать ожиданиям в других тестах смещения.
  477. expect(logLine).to.match(/^app:svc:[a-f0-9]{3}\s{2}/);
  478. // содержимое строки inspect
  479. expect(logLine).to.contain(stripAnsi(line));
  480. });
  481. });
  482. });
  483. describe('creation error', function () {
  484. it('should throw error if createDebugger is called with invalid type', function () {
  485. expect(() => createDebugger(123)).to.throw(
  486. /must be a String or an Object/,
  487. );
  488. expect(() => createDebugger(true)).to.throw(
  489. /must be a String or an Object/,
  490. );
  491. // массив - не простой объект
  492. expect(() => createDebugger([])).to.throw(
  493. /must be a String or an Object/,
  494. );
  495. // null должен вызывать ошибку (проверяется отдельно или убедитесь, что isNonArrayObject(null) === false)
  496. // expect(() => createDebugger(null)).to.throw(/must be a String or an Object/);
  497. });
  498. });
  499. });