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.
- package/README.md +16 -7
- package/package.json +6 -7
- package/src/fetchers/melee.js +2 -2
- package/src/fetchers/mtgElo.js +2 -2
- package/src/fetchers/unityLeague.js +2 -2
- package/src/fetchers/untapped.js +2 -2
- package/src/utils/socialMediaExtractor.js +0 -1
- package/test/helpers.js +32 -0
- package/test/melee.test.js +54 -17
- package/test/mtgElo.test.js +62 -23
- package/test/playerInfoManager.test.js +365 -0
- package/test/socialMediaExtractor.test.js +31 -0
- package/test/topdeck.test.js +69 -33
- package/test/unityLeague.test.js +59 -18
- package/test/untapped.test.js +75 -64
- package/test/edgeCases.test.js +0 -128
- package/test/meleeEdgeCases.test.js +0 -53
- package/test/mtgEloEdgeCases.test.js +0 -92
- package/test/unityLeagueEdgeCases.test.js +0 -123
- package/test/verboseLogging.test.js +0 -213
- package/test/winRatePrecision.test.js +0 -25
|
@@ -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
|
+
})
|
package/test/topdeck.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
12
|
+
const result = fetcher.parseHtml(html, url, '@k0shiii')
|
|
21
13
|
|
|
22
|
-
assert.
|
|
23
|
-
assert.
|
|
24
|
-
assert.
|
|
25
|
-
assert.
|
|
26
|
-
assert.
|
|
27
|
-
assert.
|
|
28
|
-
assert.
|
|
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:
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
})
|
package/test/unityLeague.test.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
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.
|
|
20
|
-
assert.
|
|
21
|
-
assert.
|
|
22
|
-
assert.
|
|
23
|
-
assert.
|
|
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
|
+
})
|