mtg-playerinfo 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -310,3 +310,368 @@ test('PlayerInfoManager: with sources in different order (Melee first, then Unit
310
310
  assert.ok(!result.general.bio.includes('Change the target of target spell or ability with a single target'),
311
311
  'Bio should not contain text from only Unity League profile')
312
312
  })
313
+
314
+ // --- Consolidated subtest clusters from edgeCases.test.js, verboseLogging.test.js, winRatePrecision.test.js ---
315
+
316
+ test.describe('PlayerInfoManager: Edge Cases', () => {
317
+ test('PlayerInfoManager: getPlayerInfo handles all null results gracefully', async () => {
318
+ const manager = new PlayerInfoManager()
319
+
320
+ manager.fetchers.unity.fetchById = async () => null
321
+ manager.fetchers.mtgelo.fetchById = async () => null
322
+ manager.fetchers.melee.fetchById = async () => null
323
+ manager.fetchers.topdeck.fetchById = async () => null
324
+
325
+ const result = await manager.getPlayerInfo({
326
+ unityId: '123',
327
+ mtgeloId: '456',
328
+ meleeUser: 'test',
329
+ topdeckHandle: 'test'
330
+ })
331
+
332
+ assert.deepEqual(result.general, {}, 'General should be empty object')
333
+ assert.deepEqual(result.sources, {}, 'Sources should be empty object')
334
+ assert.ok(!result.general['win rate'], 'Should not have win rate with no valid results')
335
+ })
336
+
337
+ test('PlayerInfoManager: getPlayerInfo handles partial null results', async () => {
338
+ const manager = new PlayerInfoManager()
339
+
340
+ manager.fetchers.unity.fetchById = async () => ({
341
+ source: 'Unity League',
342
+ url: 'http://unity.test',
343
+ name: 'Test Player'
344
+ })
345
+ manager.fetchers.mtgelo.fetchById = async () => null
346
+ manager.fetchers.melee.fetchById = async () => null
347
+ manager.fetchers.topdeck.fetchById = async () => null
348
+
349
+ const result = await manager.getPlayerInfo({
350
+ unityId: '123',
351
+ mtgeloId: '456',
352
+ meleeUser: 'test',
353
+ topdeckHandle: 'test'
354
+ })
355
+
356
+ assert.equal(result.general.name, 'Test Player', 'Should have data from Unity League')
357
+ assert.equal(Object.keys(result.sources).length, 1, 'Should have only one source')
358
+ assert.ok(result.sources['Unity League'], 'Should have Unity League in sources')
359
+ })
360
+
361
+ test('PlayerInfoManager: mergeData handles records without draws (W-L format)', async () => {
362
+ const manager = new PlayerInfoManager()
363
+
364
+ const results = [
365
+ {
366
+ source: 'Test Source',
367
+ url: 'http://test.com',
368
+ name: 'Test Player',
369
+ record: '10-5'
370
+ }
371
+ ]
372
+
373
+ const merged = manager.mergeData(results, false)
374
+
375
+ assert.equal(merged.general['win rate'], '66.67%', 'Should calculate win rate correctly for W-L format')
376
+ })
377
+
378
+ test('PlayerInfoManager: mergeData handles invalid win rate strings gracefully', async () => {
379
+ const manager = new PlayerInfoManager()
380
+
381
+ const results = [
382
+ {
383
+ source: 'Test1',
384
+ url: 'http://test1.com',
385
+ name: 'Test',
386
+ winRate: 'invalid%'
387
+ },
388
+ {
389
+ source: 'Test2',
390
+ url: 'http://test2.com',
391
+ name: 'Test',
392
+ 'win rate': 'N/A'
393
+ },
394
+ {
395
+ source: 'Test3',
396
+ url: 'http://test3.com',
397
+ name: 'Test',
398
+ record: '10-5-0'
399
+ }
400
+ ]
401
+
402
+ const merged = manager.mergeData(results, false)
403
+
404
+ // Should not crash, should calculate from record only
405
+ assert.ok(merged, 'Should not crash on invalid win rate strings')
406
+ assert.equal(merged.general['win rate'], '66.67%', 'Should calculate from valid record')
407
+ })
408
+
409
+ test('PlayerInfoManager: mergeData returns no win rate when insufficient data', async () => {
410
+ const manager = new PlayerInfoManager()
411
+
412
+ const results = [
413
+ {
414
+ source: 'Test',
415
+ url: 'http://test.com',
416
+ name: 'Test Player'
417
+ }
418
+ ]
419
+
420
+ const merged = manager.mergeData(results, false)
421
+
422
+ assert.ok(!merged.general['win rate'], 'Should not calculate win rate without data')
423
+ })
424
+
425
+ test('PlayerInfoManager: mergeData handles record with all zeros', async () => {
426
+ const manager = new PlayerInfoManager()
427
+
428
+ const results = [
429
+ {
430
+ source: 'Test',
431
+ url: 'http://test.com',
432
+ name: 'Test Player',
433
+ record: '0-0-0'
434
+ }
435
+ ]
436
+
437
+ const merged = manager.mergeData(results, false)
438
+
439
+ assert.ok(!merged.general['win rate'], 'Should not calculate win rate when no games played')
440
+ })
441
+ })
442
+
443
+ test.describe('PlayerInfoManager: Verbose Logging', () => {
444
+ test('PlayerInfoManager: verbose mode logs promoted properties', async () => {
445
+ const manager = new PlayerInfoManager()
446
+
447
+ const results = [
448
+ {
449
+ source: 'Source1',
450
+ url: 'http://source1.com',
451
+ name: 'Player Name'
452
+ }
453
+ ]
454
+
455
+ const capturedLogs = []
456
+ const originalLog = console.log
457
+ console.log = (msg) => { capturedLogs.push(msg) }
458
+
459
+ try {
460
+ const merged = manager.mergeData(results, true)
461
+ assert.equal(merged.general.name, 'Player Name')
462
+ assert.ok(capturedLogs.some(log => log.includes('⬆️') && log.includes('name') && log.includes('Source1')),
463
+ 'Should log property promotion with ⬆️ emoji')
464
+ } finally {
465
+ console.log = originalLog
466
+ }
467
+ })
468
+
469
+ test('PlayerInfoManager: verbose mode logs matching property values', async () => {
470
+ const manager = new PlayerInfoManager()
471
+
472
+ const results = [
473
+ {
474
+ source: 'Source1',
475
+ url: 'http://source1.com',
476
+ name: 'Player Name',
477
+ team: 'Team A'
478
+ },
479
+ {
480
+ source: 'Source2',
481
+ url: 'http://source2.com',
482
+ name: 'Player Name',
483
+ team: 'Team A'
484
+ }
485
+ ]
486
+
487
+ const capturedLogs = []
488
+ const originalLog = console.log
489
+ console.log = (msg) => { capturedLogs.push(msg) }
490
+
491
+ try {
492
+ manager.mergeData(results, true)
493
+ assert.ok(capturedLogs.some(log => log.includes('🆗') && log.includes('name') && log.includes('Source2')),
494
+ 'Should log matching property with 🆗 emoji')
495
+ assert.ok(capturedLogs.some(log => log.includes('🆗') && log.includes('team') && log.includes('Source2')),
496
+ 'Should log matching team property with 🆗 emoji')
497
+ } finally {
498
+ console.log = originalLog
499
+ }
500
+ })
501
+
502
+ test('PlayerInfoManager: verbose mode logs conflicting property values', async () => {
503
+ const manager = new PlayerInfoManager()
504
+
505
+ const results = [
506
+ {
507
+ source: 'Source1',
508
+ url: 'http://source1.com',
509
+ name: 'Alice'
510
+ },
511
+ {
512
+ source: 'Source2',
513
+ url: 'http://source2.com',
514
+ name: 'Bob'
515
+ }
516
+ ]
517
+
518
+ const capturedLogs = []
519
+ const originalLog = console.log
520
+ console.log = (msg) => { capturedLogs.push(msg) }
521
+
522
+ try {
523
+ const merged = manager.mergeData(results, true)
524
+ assert.equal(merged.general.name, 'Alice', 'Should keep first value')
525
+ assert.ok(capturedLogs.some(log => log.includes('🆚') && log.includes('name') && log.includes('Source2') && log.includes('Bob') && log.includes('Alice')),
526
+ 'Should log conflicting property with 🆚 emoji')
527
+ } finally {
528
+ console.log = originalLog
529
+ }
530
+ })
531
+
532
+ test('PlayerInfoManager: verbose mode uses photo emoji for photo conflicts', async () => {
533
+ const manager = new PlayerInfoManager()
534
+
535
+ const results = [
536
+ {
537
+ source: 'Source1',
538
+ url: 'http://source1.com',
539
+ name: 'Player',
540
+ photo: 'http://photo1.jpg'
541
+ },
542
+ {
543
+ source: 'Source2',
544
+ url: 'http://source2.com',
545
+ name: 'Player',
546
+ photo: 'http://photo2.jpg'
547
+ }
548
+ ]
549
+
550
+ const capturedLogs = []
551
+ const originalLog = console.log
552
+ console.log = (msg) => { capturedLogs.push(msg) }
553
+
554
+ try {
555
+ manager.mergeData(results, true)
556
+ assert.ok(capturedLogs.some(log => log.includes('🆕') && log.includes('photo')),
557
+ 'Should log photo conflict with 🆕 emoji instead of 🆚')
558
+ } finally {
559
+ console.log = originalLog
560
+ }
561
+ })
562
+
563
+ test('PlayerInfoManager: verbose mode logs with null/undefined properties', async () => {
564
+ const manager = new PlayerInfoManager()
565
+
566
+ const results = [
567
+ {
568
+ source: 'Source1',
569
+ url: 'http://source1.com',
570
+ name: 'Player',
571
+ bio: null,
572
+ team: undefined
573
+ },
574
+ {
575
+ source: 'Source2',
576
+ url: 'http://source2.com',
577
+ name: 'Player',
578
+ bio: 'Test bio',
579
+ team: 'Team A'
580
+ }
581
+ ]
582
+
583
+ const capturedLogs = []
584
+ const originalLog = console.log
585
+ console.log = (msg) => { capturedLogs.push(msg) }
586
+
587
+ try {
588
+ manager.mergeData(results, true)
589
+ assert.ok(capturedLogs.some(log => log.includes('⬆️') && log.includes('bio') && log.includes('Source2')),
590
+ 'Should promote bio from Source2 after Source1 had null')
591
+ assert.ok(capturedLogs.some(log => log.includes('⬆️') && log.includes('team') && log.includes('Source2')),
592
+ 'Should promote team from Source2 after Source1 had undefined')
593
+ } finally {
594
+ console.log = originalLog
595
+ }
596
+ })
597
+
598
+ test('PlayerInfoManager: verbose mode is false by default and logs nothing', async () => {
599
+ const manager = new PlayerInfoManager()
600
+
601
+ const results = [
602
+ {
603
+ source: 'Source1',
604
+ url: 'http://source1.com',
605
+ name: 'Player Name'
606
+ }
607
+ ]
608
+
609
+ const capturedLogs = []
610
+ const originalLog = console.log
611
+ console.log = (msg) => { capturedLogs.push(msg) }
612
+
613
+ try {
614
+ manager.mergeData(results, false)
615
+ assert.equal(capturedLogs.length, 0, 'Should not log when verbose is false')
616
+ } finally {
617
+ console.log = originalLog
618
+ }
619
+ })
620
+
621
+ test('PlayerInfoManager: verbose mode logs each property separately', async () => {
622
+ const manager = new PlayerInfoManager()
623
+
624
+ const results = [
625
+ {
626
+ source: 'Source1',
627
+ url: 'http://source1.com',
628
+ name: 'Alice',
629
+ team: 'Team A',
630
+ bio: 'Bio 1',
631
+ photo: 'photo1.jpg',
632
+ pronouns: 'they/them'
633
+ }
634
+ ]
635
+
636
+ const capturedLogs = []
637
+ const originalLog = console.log
638
+ console.log = (msg) => { capturedLogs.push(msg) }
639
+
640
+ try {
641
+ manager.mergeData(results, true)
642
+ const promotedLogs = capturedLogs.filter(log => log.includes('⬆️'))
643
+ assert.ok(promotedLogs.length >= 5, `Should log 5+ promoted properties, got ${promotedLogs.length}`)
644
+ assert.ok(promotedLogs.some(log => log.includes('name')))
645
+ assert.ok(promotedLogs.some(log => log.includes('team')))
646
+ assert.ok(promotedLogs.some(log => log.includes('bio')))
647
+ assert.ok(promotedLogs.some(log => log.includes('photo')))
648
+ assert.ok(promotedLogs.some(log => log.includes('pronouns')))
649
+ } finally {
650
+ console.log = originalLog
651
+ }
652
+ })
653
+ })
654
+
655
+ test.describe('PlayerInfoManager: Win Rate Calculation', () => {
656
+ test('PlayerInfoManager: Win rate should be calculated from total of source W-L-D records', async () => {
657
+ const manager = new PlayerInfoManager()
658
+
659
+ const results = [
660
+ {
661
+ source: 'Source1',
662
+ url: 'url1',
663
+ record: '10-0-0',
664
+ 'win rate': '100.00%'
665
+ },
666
+ {
667
+ source: 'Source2',
668
+ url: 'url2',
669
+ record: '0-90-10',
670
+ 'win rate': '0.00%'
671
+ }
672
+ ]
673
+
674
+ const merged = manager.mergeData(results)
675
+ assert.strictEqual(merged.general['win rate'], '9.09%', 'Win rate should be calculated from total records')
676
+ })
677
+ })
@@ -0,0 +1,31 @@
1
+ const test = require('node:test')
2
+ const assert = require('node:assert/strict')
3
+ const { extractHandle, getPlatformName } = require('../src/utils/socialMediaExtractor')
4
+
5
+ test('socialMediaExtractor: extractHandle extracts handle correctly', () => {
6
+ assert.strictEqual(extractHandle('https://twitter.com/user'), 'user')
7
+ assert.strictEqual(extractHandle('https://www.facebook.com/user/'), 'user')
8
+ assert.strictEqual(extractHandle('https://twitch.tv/user'), 'user')
9
+ assert.strictEqual(extractHandle('https://youtube.com/@user'), '@user')
10
+ assert.strictEqual(extractHandle('https://instagram.com/user?query=1'), 'user')
11
+ })
12
+
13
+ test('socialMediaExtractor: extractHandle handles null/empty/invalid input', () => {
14
+ assert.strictEqual(extractHandle(null), null)
15
+ assert.strictEqual(extractHandle(''), null)
16
+ assert.strictEqual(extractHandle('not-a-url'), null)
17
+ })
18
+
19
+ test('socialMediaExtractor: getPlatformName extracts platform name correctly', () => {
20
+ assert.strictEqual(getPlatformName('https://twitter.com/user'), 'twitter')
21
+ assert.strictEqual(getPlatformName('https://www.facebook.com/user'), 'facebook')
22
+ assert.strictEqual(getPlatformName('https://twitch.tv/user'), 'twitch')
23
+ assert.strictEqual(getPlatformName('https://youtube.com/@user'), 'youtube')
24
+ assert.strictEqual(extractHandle('https://twitter.com/'), null)
25
+ })
26
+
27
+ test('socialMediaExtractor: getPlatformName handles null/empty/invalid input', () => {
28
+ assert.strictEqual(getPlatformName(null), null)
29
+ assert.strictEqual(getPlatformName(''), null)
30
+ assert.strictEqual(getPlatformName('not-a-url'), null)
31
+ })
@@ -1,49 +1,85 @@
1
1
  const test = require('node:test')
2
2
  const assert = require('node:assert/strict')
3
- const fs = require('node:fs')
4
- const path = require('node:path')
5
- const { mock } = require('node:test')
6
-
7
- const httpClient = require('../src/utils/httpClient')
8
3
  const TopdeckFetcher = require('../src/fetchers/topdeck')
4
+ const httpClient = require('../src/utils/httpClient')
5
+ const { readFixture, withMutedConsole } = require('./helpers')
9
6
 
10
- function readFixture (name) {
11
- return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
12
- }
13
-
14
- test('TopdeckFetcher: parseHtml extracts stats from DOM when available', () => {
7
+ test('TopdeckFetcher: parseHtml extracts info from fixture', () => {
15
8
  const html = readFixture('topdeck.html')
16
- const handle = '@k0shiii'
17
- const url = `https://topdeck.gg/profile/${handle}`
9
+ const url = 'https://topdeck.gg/profile/@k0shiii'
18
10
  const fetcher = new TopdeckFetcher()
19
11
 
20
- const result = fetcher.parseHtml(html, url, handle)
12
+ const result = fetcher.parseHtml(html, url, '@k0shiii')
21
13
 
22
- assert.ok(result, 'Should return a result object')
23
- assert.equal(result.source, 'Topdeck')
24
- assert.equal(result.url, url)
25
- assert.equal(result.name, 'Björn Kimminich')
26
- assert.equal(result.pronouns, 'He/Him', 'Should extract pronouns from badge')
27
- assert.equal(result.twitter, 'bkimminich', 'Should extract Twitter handle')
28
- assert.equal(result.youtube, '@BjörnKimminich', 'Should extract YouTube handle')
14
+ assert.strictEqual(result.source, 'Topdeck')
15
+ assert.strictEqual(result.name, 'Björn Kimminich')
16
+ assert.strictEqual(result.photo, 'https://imagedelivery.net/kN_u_RUfFF6xsGMKYWhO1g/2a7b8d12-5924-4a58-5f9c-c0bf55766800/square')
17
+ assert.strictEqual(result.twitter, 'bkimminich')
18
+ assert.strictEqual(result.youtube, '@BjörnKimminich')
19
+ assert.match(result.tournaments, /^\d+$/)
20
+ assert.match(result.record, /^\d+-\d+-\d+$/)
29
21
  })
30
22
 
31
- test('TopdeckFetcher: fetchStats updates playerInfo with data from stats JSON', async () => {
32
- const statsJson = readFixture('topdeck.json')
33
- const internalId = 'm4VSTJShiXR1PCSCWaM9TBY0rcg1'
23
+ test('TopdeckFetcher: parseHtml handles missing stats and different DOM shapes', () => {
34
24
  const fetcher = new TopdeckFetcher()
35
- const playerInfo = { source: 'Topdeck' }
25
+ const html = `
26
+ <html>
27
+ <body>
28
+ <h1>Simple Name</h1>
29
+ <div class="stats-container">
30
+ <div class="stat">
31
+ <div class="label">Win Rate</div>
32
+ <div class="value">55%</div>
33
+ </div>
34
+ </div>
35
+ </body>
36
+ </html>
37
+ `
38
+ const result = fetcher.parseHtml(html, 'url', 'handle')
39
+ assert.strictEqual(result.name, 'Simple Name')
40
+ assert.strictEqual(result['win rate'], '55%')
41
+ })
36
42
 
37
- mock.method(httpClient, 'request', async (url) => {
38
- if (url.endsWith(`/profile/${internalId}/stats`)) {
39
- return { data: statsJson }
40
- }
41
- throw new Error(`Unexpected URL: ${url}`)
43
+ test('TopdeckFetcher: fetchById and fetchStats integration', async (t) => {
44
+ const fetcher = new TopdeckFetcher()
45
+
46
+ await t.test('successful fetch with stats', async () => {
47
+ const mockRequest = t.mock.method(httpClient, 'request')
48
+ mockRequest.mock.mockImplementation(async (url) => {
49
+ if (url.includes('/stats')) {
50
+ return { data: { yearlyStats: { 2024: { overall: { totalTournaments: 5, wins: 3, losses: 1, draws: 1 } } } }, status: 200 }
51
+ }
52
+ return { data: '<html><h2>Test User</h2>const playerId = "abc123";</html>', status: 200 }
53
+ })
54
+
55
+ const result = await fetcher.fetchById('testuser')
56
+ assert.strictEqual(result.name, '@testuser')
57
+ assert.strictEqual(result.record, '3-1-1')
58
+ assert.strictEqual(result['win rate'], '60.00%')
59
+ mockRequest.mock.restore()
42
60
  })
43
61
 
44
- await fetcher.fetchStats(internalId, playerInfo)
62
+ await t.test('fetch error handling', async () => {
63
+ const mockRequest = t.mock.method(httpClient, 'request', () => { throw new Error('Boom') })
64
+ await withMutedConsole(async () => {
65
+ const result = await fetcher.fetchById('testuser')
66
+ assert.strictEqual(result, null)
67
+ })
68
+ mockRequest.mock.restore()
69
+ })
45
70
 
46
- assert.match(playerInfo.tournaments, /^\d+$/)
47
- assert.match(playerInfo.record, /^\d+-\d+-\d+$/)
48
- assert.match(playerInfo['win rate'], /^\d+(\.\d+)?%$/)
71
+ await t.test('stats fetch error handling', async () => {
72
+ const mockRequest = t.mock.method(httpClient, 'request')
73
+ mockRequest.mock.mockImplementation(async (url) => {
74
+ if (url.includes('/stats')) throw new Error('Stats Error')
75
+ return { data: '<html>const playerId = "abc123";</html>', status: 200 }
76
+ })
77
+
78
+ await withMutedConsole(async () => {
79
+ const result = await fetcher.fetchById('testuser')
80
+ assert.ok(result)
81
+ assert.ok(!result.record)
82
+ })
83
+ mockRequest.mock.restore()
84
+ })
49
85
  })
@@ -1,30 +1,71 @@
1
1
  const test = require('node:test')
2
2
  const assert = require('node:assert/strict')
3
- const fs = require('node:fs')
4
- const path = require('node:path')
5
-
6
3
  const UnityLeagueFetcher = require('../src/fetchers/unityLeague')
4
+ const httpClient = require('../src/utils/httpClient')
5
+ const { readFixture, withMutedConsole } = require('./helpers')
7
6
 
8
- function readFixture (name) {
9
- return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
10
- }
11
-
12
- test('UnityLeagueFetcher: parseHtml extracts profile, photo, ranks and stats', () => {
7
+ test('UnityLeagueFetcher: parseHtml extracts info from fixture', () => {
13
8
  const html = readFixture('unityLeague.html')
14
- const url = 'https://unityleague.gg/player/16215/'
9
+ const url = 'https://unityleague.gg/player/koshiii/'
15
10
  const fetcher = new UnityLeagueFetcher()
16
11
 
17
12
  const result = fetcher.parseHtml(html, url)
18
13
 
19
- assert.ok(result, 'Should return a result object')
20
- assert.equal(result.source, 'Unity League')
21
- assert.equal(result.url, url)
22
- assert.equal(result.name, 'Björn Kimminich')
23
- assert.equal(result.photo, 'https://unityleague.gg/media/player_profile/1000023225.jpg')
24
- assert.equal(result.country, 'de')
25
- assert.match(result['rank germany'], /^\d+$/)
26
- assert.match(result['rank europe'], /^\d+$/)
27
- assert.match(result['rank points'], /^\d+$/)
14
+ assert.strictEqual(result.source, 'Unity League')
15
+ assert.strictEqual(result.name, 'Björn Kimminich')
16
+ assert.strictEqual(result.photo, 'https://unityleague.gg/media/player_profile/1000023225.jpg')
17
+ assert.strictEqual(result.country, 'de')
18
+ assert.ok(result.bio.includes('Smugly held back'))
28
19
  assert.match(result.record, /^\d+-\d+-\d+$/)
29
20
  assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
30
21
  })
22
+
23
+ test('UnityLeagueFetcher: parseHtml handles edge cases in DOM', () => {
24
+ const fetcher = new UnityLeagueFetcher()
25
+
26
+ const cases = [
27
+ {
28
+ label: 'non-profile image',
29
+ html: '<html><body><div class="card-body"><img class="img-fluid" src="/other.jpg"></div></body></html>',
30
+ check: (res) => assert.strictEqual(res.photo, null)
31
+ },
32
+ {
33
+ label: 'country from dd list',
34
+ html: '<html><body><dt class="small text-muted">Country:</dt><dd><i class="fi fi-us"></i> United States</dd></body></html>',
35
+ check: (res) => assert.strictEqual(res.country, 'us')
36
+ },
37
+ {
38
+ label: 'no account header',
39
+ html: '<html><body></body></html>',
40
+ check: (res) => assert.strictEqual(res.name, '')
41
+ }
42
+ ]
43
+
44
+ for (const { html, check } of cases) {
45
+ const res = fetcher.parseHtml(html, 'url')
46
+ check(res)
47
+ }
48
+ })
49
+
50
+ test('UnityLeagueFetcher: fetchById handles success and error scenarios', async (t) => {
51
+ const fetcher = new UnityLeagueFetcher()
52
+
53
+ await t.test('successful fetch', async () => {
54
+ const mockRequest = t.mock.method(httpClient, 'request', async () => ({
55
+ data: '<html><h1 class="d-inline">Fetched User</h1></html>',
56
+ status: 200
57
+ }))
58
+ const result = await fetcher.fetchById('koshiii')
59
+ assert.strictEqual(result.name, 'Fetched User')
60
+ mockRequest.mock.restore()
61
+ })
62
+
63
+ await t.test('network error', async () => {
64
+ const mockRequest = t.mock.method(httpClient, 'request', () => { throw new Error('Network Error') })
65
+ await withMutedConsole(async () => {
66
+ const result = await fetcher.fetchById('koshiii')
67
+ assert.strictEqual(result, null)
68
+ })
69
+ mockRequest.mock.restore()
70
+ })
71
+ })