mtg-playerinfo 1.2.0 → 1.3.0
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/.github/workflows/ci.yml +29 -3
- package/.github/workflows/update-test-data.yml +33 -0
- package/README.md +16 -11
- package/cli.js +30 -13
- package/package.json +21 -2
- package/scripts/update-test-data.js +29 -0
- package/src/fetchers/melee.js +26 -35
- package/src/fetchers/mtgElo.js +40 -40
- package/src/fetchers/topdeck.js +70 -67
- package/src/fetchers/unityLeague.js +45 -45
- package/src/fetchers/untapped.js +54 -0
- package/src/index.js +75 -42
- package/src/utils/httpClient.js +30 -30
- package/test/data/melee.html +77 -62
- package/test/data/mtgElo.html +8 -8
- package/test/data/topdeck.html +778 -662
- package/test/data/topdeck.json +1 -0
- package/test/data/unityLeague.html +1445 -1340
- package/test/data/untapped.json +104 -0
- package/test/edgeCases.test.js +128 -0
- package/test/melee.test.js +23 -23
- package/test/meleeEdgeCases.test.js +53 -0
- package/test/mtgElo.test.js +21 -21
- package/test/mtgEloEdgeCases.test.js +92 -0
- package/test/playerInfoManager.test.js +312 -0
- package/test/topdeck.test.js +42 -29
- package/test/unityLeague.test.js +24 -24
- package/test/unityLeagueEdgeCases.test.js +123 -0
- package/test/untapped.test.js +58 -0
- package/test/verboseLogging.test.js +215 -0
- package/test/winRatePrecision.test.js +25 -0
- package/.github/workflows/pull-player-data.yml +0 -27
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"short_id": "ZcCUi2BJ7g5ubNrREqKS3C",
|
|
4
|
+
"match_start": 1770848945000,
|
|
5
|
+
"season_ordinal": 86,
|
|
6
|
+
"event_name": "Traditional_Ladder",
|
|
7
|
+
"super_format": 2,
|
|
8
|
+
"match_win_condition": 2,
|
|
9
|
+
"games": [
|
|
10
|
+
{
|
|
11
|
+
"game_number": 1,
|
|
12
|
+
"game_duration_seconds": 587,
|
|
13
|
+
"active_player_id": 1,
|
|
14
|
+
"winning_team_id": 2,
|
|
15
|
+
"friendly_deckstring": "AAQAAQqMBQHz1C2kAxkD1vkHqPQChuwHlrgBBbzrAd_rM7j6ApWaCeEEBIdk4JwsCofsFAe16wHuUMuQL5sEr50Qf5k6AAIJ_uIC6_EW1qsTosgL4-4HgQSjsAF2_AIDlxCMwzWO1QwAAAAA",
|
|
16
|
+
"player_opening_hands": [
|
|
17
|
+
"AAQAAQe16wEH51DOxCrMXNn5B736AgAAAAAA"
|
|
18
|
+
],
|
|
19
|
+
"player_mulligan_put_on_bottom": null,
|
|
20
|
+
"opponent_revealed_colors": [
|
|
21
|
+
11
|
|
22
|
+
],
|
|
23
|
+
"opponent_revealed_archetype": {
|
|
24
|
+
"AAQAAQmMBaINvlHE7gbaiSagAZw317sKwNsIB64a_F7QSqKZLL_1B7UE9N8KA6v-LNvSBJP7BgAAAA": null
|
|
25
|
+
},
|
|
26
|
+
"opponent_revealed_deckstrings": [
|
|
27
|
+
"AAQAAQmMBaINvlHE7gbaiSagAZw317sKwNsIB64a_F7QSqKZLL_1B7UE9N8KA6v-LNvSBJP7BgAAAA"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"game_number": 2,
|
|
32
|
+
"game_duration_seconds": 335,
|
|
33
|
+
"active_player_id": 1,
|
|
34
|
+
"winning_team_id": 1,
|
|
35
|
+
"friendly_deckstring": "AAQAAQyMBQHx3QLBnSrBWaQDGQPW-QfL8QLdAqChCQeXEKXbAd_rM7j6AvHlB4W5Aeg3Aodk6pwsB7XrAe5Qy5AvmwSvnRB_mToAAgLp1Bnc5iYCo9M1t5wMA-eALYHrFJABAAAA",
|
|
36
|
+
"player_opening_hands": [
|
|
37
|
+
"AAQAAQWXEJ7bAYqVK6_MBPP7BgGjvAIAAAAA"
|
|
38
|
+
],
|
|
39
|
+
"player_mulligan_put_on_bottom": null,
|
|
40
|
+
"opponent_revealed_colors": [
|
|
41
|
+
11
|
|
42
|
+
],
|
|
43
|
+
"opponent_revealed_archetype": {
|
|
44
|
+
"AAQAAQqMBaIV_F7QSpCYLKABsfUHrgSQ9QK_AQGQ1zUAAAAA": null
|
|
45
|
+
},
|
|
46
|
+
"opponent_revealed_deckstrings": [
|
|
47
|
+
"AAQAAQqMBaIV_F7QSpCYLKABsfUHrgSQ9QK_AQGQ1zUAAAAA"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"game_number": 3,
|
|
52
|
+
"game_duration_seconds": 190,
|
|
53
|
+
"active_player_id": 2,
|
|
54
|
+
"winning_team_id": 1,
|
|
55
|
+
"friendly_deckstring": "AAQAAQyMBQHx3QLBnSrBWaQDGQPW-QfL8QLdAqChCQeXEKXbAd_rM7j6AvHlB4W5Aeg3Aodk6pwsB7XrAe5Qy5AvmwSvnRB_mToAAgLp1Bnc5iYCo9M1t5wMA-eALYHrFJABAAAA",
|
|
56
|
+
"player_opening_hands": [
|
|
57
|
+
"AAQAAQWHZJzYAc7EKqrWCJbRDAG370EAAAAA"
|
|
58
|
+
],
|
|
59
|
+
"player_mulligan_put_on_bottom": null,
|
|
60
|
+
"opponent_revealed_colors": [
|
|
61
|
+
3
|
|
62
|
+
],
|
|
63
|
+
"opponent_revealed_archetype": {
|
|
64
|
+
"AAQAAQeuEoAIvkm-FZBy1Oor-4AIAYS3QAAAAAA": null
|
|
65
|
+
},
|
|
66
|
+
"opponent_revealed_deckstrings": [
|
|
67
|
+
"AAQAAQeuEoAIvkm-FZBy1Oor-4AIAYS3QAAAAAA"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"winning_team_id": 1,
|
|
72
|
+
"active_player_id": 1,
|
|
73
|
+
"friendly_deckstring": "AAQAAQqMBQHz1C2kAxkD1vkHqPQChuwHlrgBBbzrAd_rM7j6ApWaCeEEBIdk4JwsCofsFAe16wHuUMuQL5sEr50Qf5k6AAIJ_uIC6_EW1qsTosgL4-4HgQSjsAF2_AIDlxCMwzWO1QwAAAAA",
|
|
74
|
+
"friendly_deck_name": "Sultai Combo v2",
|
|
75
|
+
"friendly_deck_id": "5e2004f0-e55c-4081-8284-22ca03db47c8",
|
|
76
|
+
"friendly_deck_colors": 22,
|
|
77
|
+
"friendly_deck_tile_id": 98497,
|
|
78
|
+
"friendly_deck_archetype": null,
|
|
79
|
+
"friendly_system_seat_id": 1,
|
|
80
|
+
"friendly_team_id": 1,
|
|
81
|
+
"friendly_ranking_class_before": "Platinum",
|
|
82
|
+
"friendly_ranking_tier_before": 4,
|
|
83
|
+
"friendly_ranking_tier_steps_before": 2,
|
|
84
|
+
"friendly_mythic_percentile_before": 0.0,
|
|
85
|
+
"friendly_mythic_leaderboard_place_before": 0,
|
|
86
|
+
"friendly_rating_before": null,
|
|
87
|
+
"friendly_rating_deviation_before": null,
|
|
88
|
+
"friendly_rating_volatility_before": null,
|
|
89
|
+
"friendly_rating_mythic_before": null,
|
|
90
|
+
"friendly_ranking_class_after": "Platinum",
|
|
91
|
+
"friendly_ranking_tier_after": 4,
|
|
92
|
+
"friendly_ranking_tier_steps_after": 4,
|
|
93
|
+
"friendly_mythic_percentile_after": 0.0,
|
|
94
|
+
"friendly_mythic_leaderboard_place_after": 0,
|
|
95
|
+
"friendly_course_wins_before": null,
|
|
96
|
+
"friendly_course_losses_before": null,
|
|
97
|
+
"opponents": [
|
|
98
|
+
{
|
|
99
|
+
"player_name": "MysticalMachineGun",
|
|
100
|
+
"team_id": 2
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const PlayerInfoManager = require('../src')
|
|
4
|
+
|
|
5
|
+
test('PlayerInfoManager: getPlayerInfo handles all null results gracefully', async () => {
|
|
6
|
+
const manager = new PlayerInfoManager()
|
|
7
|
+
|
|
8
|
+
manager.fetchers.unity.fetchById = async () => null
|
|
9
|
+
manager.fetchers.mtgelo.fetchById = async () => null
|
|
10
|
+
manager.fetchers.melee.fetchById = async () => null
|
|
11
|
+
manager.fetchers.topdeck.fetchById = async () => null
|
|
12
|
+
|
|
13
|
+
const result = await manager.getPlayerInfo({
|
|
14
|
+
unityId: '123',
|
|
15
|
+
mtgeloId: '456',
|
|
16
|
+
meleeUser: 'test',
|
|
17
|
+
topdeckHandle: 'test'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(result.general, {}, 'General should be empty object')
|
|
21
|
+
assert.deepEqual(result.sources, {}, 'Sources should be empty object')
|
|
22
|
+
assert.ok(!result.general['win rate'], 'Should not have win rate with no valid results')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('PlayerInfoManager: getPlayerInfo handles partial null results', async () => {
|
|
26
|
+
const manager = new PlayerInfoManager()
|
|
27
|
+
|
|
28
|
+
manager.fetchers.unity.fetchById = async () => ({
|
|
29
|
+
source: 'Unity League',
|
|
30
|
+
url: 'http://unity.test',
|
|
31
|
+
name: 'Test Player'
|
|
32
|
+
})
|
|
33
|
+
manager.fetchers.mtgelo.fetchById = async () => null
|
|
34
|
+
manager.fetchers.melee.fetchById = async () => null
|
|
35
|
+
manager.fetchers.topdeck.fetchById = async () => null
|
|
36
|
+
|
|
37
|
+
const result = await manager.getPlayerInfo({
|
|
38
|
+
unityId: '123',
|
|
39
|
+
mtgeloId: '456',
|
|
40
|
+
meleeUser: 'test',
|
|
41
|
+
topdeckHandle: 'test'
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
assert.equal(result.general.name, 'Test Player', 'Should have data from Unity League')
|
|
45
|
+
assert.equal(Object.keys(result.sources).length, 1, 'Should have only one source')
|
|
46
|
+
assert.ok(result.sources['Unity League'], 'Should have Unity League in sources')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('PlayerInfoManager: mergeData handles records without draws (W-L format)', async () => {
|
|
50
|
+
const manager = new PlayerInfoManager()
|
|
51
|
+
|
|
52
|
+
const results = [
|
|
53
|
+
{
|
|
54
|
+
source: 'Test Source',
|
|
55
|
+
url: 'http://test.com',
|
|
56
|
+
name: 'Test Player',
|
|
57
|
+
record: '10-5'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const merged = manager.mergeData(results, false)
|
|
62
|
+
|
|
63
|
+
assert.equal(merged.general['win rate'], '66.67%', 'Should calculate win rate correctly for W-L format')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('PlayerInfoManager: mergeData handles invalid win rate strings gracefully', async () => {
|
|
67
|
+
const manager = new PlayerInfoManager()
|
|
68
|
+
|
|
69
|
+
const results = [
|
|
70
|
+
{
|
|
71
|
+
source: 'Test1',
|
|
72
|
+
url: 'http://test1.com',
|
|
73
|
+
name: 'Test',
|
|
74
|
+
winRate: 'invalid%'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
source: 'Test2',
|
|
78
|
+
url: 'http://test2.com',
|
|
79
|
+
name: 'Test',
|
|
80
|
+
'win rate': 'N/A'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
source: 'Test3',
|
|
84
|
+
url: 'http://test3.com',
|
|
85
|
+
name: 'Test',
|
|
86
|
+
record: '10-5-0'
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
const merged = manager.mergeData(results, false)
|
|
91
|
+
|
|
92
|
+
// Should not crash, should calculate from record only
|
|
93
|
+
assert.ok(merged, 'Should not crash on invalid win rate strings')
|
|
94
|
+
assert.equal(merged.general['win rate'], '66.67%', 'Should calculate from valid record')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('PlayerInfoManager: mergeData returns no win rate when insufficient data', async () => {
|
|
98
|
+
const manager = new PlayerInfoManager()
|
|
99
|
+
|
|
100
|
+
const results = [
|
|
101
|
+
{
|
|
102
|
+
source: 'Test',
|
|
103
|
+
url: 'http://test.com',
|
|
104
|
+
name: 'Test Player'
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const merged = manager.mergeData(results, false)
|
|
109
|
+
|
|
110
|
+
assert.ok(!merged.general['win rate'], 'Should not calculate win rate without data')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('PlayerInfoManager: mergeData handles record with all zeros', async () => {
|
|
114
|
+
const manager = new PlayerInfoManager()
|
|
115
|
+
|
|
116
|
+
const results = [
|
|
117
|
+
{
|
|
118
|
+
source: 'Test',
|
|
119
|
+
url: 'http://test.com',
|
|
120
|
+
name: 'Test Player',
|
|
121
|
+
record: '0-0-0'
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const merged = manager.mergeData(results, false)
|
|
126
|
+
|
|
127
|
+
assert.ok(!merged.general['win rate'], 'Should not calculate win rate when no games played')
|
|
128
|
+
})
|
package/test/melee.test.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
const test = require('node:test')
|
|
2
|
-
const assert = require('node:assert/strict')
|
|
3
|
-
const fs = require('node:fs')
|
|
4
|
-
const path = require('node:path')
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
5
|
|
|
6
|
-
const MeleeFetcher = require('../src/fetchers/melee')
|
|
6
|
+
const MeleeFetcher = require('../src/fetchers/melee')
|
|
7
7
|
|
|
8
|
-
function readFixture(name) {
|
|
9
|
-
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
8
|
+
function readFixture (name) {
|
|
9
|
+
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
test('MeleeFetcher
|
|
13
|
-
const html = readFixture('melee.html')
|
|
14
|
-
const url = 'https://melee.gg/Profile/Index/k0shiii'
|
|
15
|
-
const username = 'k0shiii'
|
|
16
|
-
const fetcher = new MeleeFetcher()
|
|
12
|
+
test('MeleeFetcher: parseHtml extracts basic profile info', () => {
|
|
13
|
+
const html = readFixture('melee.html')
|
|
14
|
+
const url = 'https://melee.gg/Profile/Index/k0shiii'
|
|
15
|
+
const username = 'k0shiii'
|
|
16
|
+
const fetcher = new MeleeFetcher()
|
|
17
17
|
|
|
18
|
-
const result = fetcher.parseHtml(html, url, username)
|
|
18
|
+
const result = fetcher.parseHtml(html, url, username)
|
|
19
19
|
|
|
20
|
-
assert.ok(result, 'Should return a result object')
|
|
21
|
-
assert.equal(result.source, 'Melee')
|
|
22
|
-
assert.equal(result.url, url)
|
|
23
|
-
assert.equal(result.name, 'Björn Kimminich')
|
|
24
|
-
assert.equal(result.pronouns, 'He/Him')
|
|
25
|
-
assert.equal(result.bio, 'Smugly held back on an Untimely Malfunction against a Storm player going off, being totally sure that you can redirect the summed-up damage of their Grapeshots back to their face.')
|
|
26
|
-
assert.equal(result.facebook, 'bjoern.kimminich')
|
|
27
|
-
assert.equal(result.twitch, 'koshiii')
|
|
28
|
-
assert.equal(result.youtube, '@BjörnKimminich')
|
|
29
|
-
})
|
|
20
|
+
assert.ok(result, 'Should return a result object')
|
|
21
|
+
assert.equal(result.source, 'Melee')
|
|
22
|
+
assert.equal(result.url, url)
|
|
23
|
+
assert.equal(result.name, 'Björn Kimminich')
|
|
24
|
+
assert.equal(result.pronouns, 'He/Him')
|
|
25
|
+
assert.equal(result.bio, 'Smugly held back on an Untimely Malfunction against a Storm player going off, being totally sure that you can redirect the summed-up damage of their Grapeshots back to their face.')
|
|
26
|
+
assert.equal(result.facebook, 'bjoern.kimminich')
|
|
27
|
+
assert.equal(result.twitch, 'koshiii')
|
|
28
|
+
assert.equal(result.youtube, '@BjörnKimminich')
|
|
29
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const MeleeFetcher = require('../src/fetchers/melee')
|
|
4
|
+
|
|
5
|
+
test('MeleeFetcher: parseHtml uses username when name not found in HTML', () => {
|
|
6
|
+
const fetcher = new MeleeFetcher()
|
|
7
|
+
const html = '<html><body></body></html>'
|
|
8
|
+
|
|
9
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'fallbackname')
|
|
10
|
+
|
|
11
|
+
assert.equal(result.name, 'fallbackname', 'Should use username as fallback')
|
|
12
|
+
assert.equal(result.source, 'Melee')
|
|
13
|
+
assert.equal(result.url, 'http://test.com')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('MeleeFetcher: parseHtml handles invalid social link URLs gracefully', () => {
|
|
17
|
+
const fetcher = new MeleeFetcher()
|
|
18
|
+
const html = `
|
|
19
|
+
<html>
|
|
20
|
+
<body>
|
|
21
|
+
<span style="font-size: xx-large">Test Name</span>
|
|
22
|
+
<a class="social-link" href="not-a-valid-url">Invalid</a>
|
|
23
|
+
<a class="social-link" href="https://twitter.com/validuser">Valid</a>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'testuser')
|
|
29
|
+
|
|
30
|
+
assert.equal(result.name, 'Test Name')
|
|
31
|
+
assert.equal(result.twitter, 'validuser', 'Should parse valid URL')
|
|
32
|
+
// Should not crash on invalid URL
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('MeleeFetcher: parseHtml extracts multiple social links correctly', () => {
|
|
36
|
+
const fetcher = new MeleeFetcher()
|
|
37
|
+
const html = `
|
|
38
|
+
<html>
|
|
39
|
+
<body>
|
|
40
|
+
<span style="font-size: xx-large">Test Player</span>
|
|
41
|
+
<a class="social-link" href="https://twitter.com/testtwitter">Twitter</a>
|
|
42
|
+
<a class="social-link" href="https://www.facebook.com/testfacebook">Facebook</a>
|
|
43
|
+
<a class="social-link" href="https://twitch.tv/testtwitch">Twitch</a>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'testuser')
|
|
49
|
+
|
|
50
|
+
assert.equal(result.twitter, 'testtwitter')
|
|
51
|
+
assert.equal(result.facebook, 'testfacebook')
|
|
52
|
+
assert.equal(result.twitch, 'testtwitch')
|
|
53
|
+
})
|
package/test/mtgElo.test.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
const test = require('node:test')
|
|
2
|
-
const assert = require('node:assert/strict')
|
|
3
|
-
const fs = require('node:fs')
|
|
4
|
-
const path = require('node:path')
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
5
|
|
|
6
|
-
const MtgEloFetcher = require('../src/fetchers/mtgElo')
|
|
6
|
+
const MtgEloFetcher = require('../src/fetchers/mtgElo')
|
|
7
7
|
|
|
8
|
-
function readFixture(name) {
|
|
9
|
-
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
8
|
+
function readFixture (name) {
|
|
9
|
+
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
test('MtgEloFetcher
|
|
13
|
-
const html = readFixture('mtgElo.html')
|
|
14
|
-
const id = 'bjoern-kimminich'
|
|
15
|
-
const url = `https://mtgeloproject.net/profile/${id}
|
|
16
|
-
const fetcher = new MtgEloFetcher()
|
|
12
|
+
test('MtgEloFetcher: parseHtml extracts name and details including computed Win Rate', () => {
|
|
13
|
+
const html = readFixture('mtgElo.html')
|
|
14
|
+
const id = 'bjoern-kimminich'
|
|
15
|
+
const url = `https://mtgeloproject.net/profile/${id}`
|
|
16
|
+
const fetcher = new MtgEloFetcher()
|
|
17
17
|
|
|
18
|
-
const result = fetcher.parseHtml(html, url, id)
|
|
18
|
+
const result = fetcher.parseHtml(html, url, id)
|
|
19
19
|
|
|
20
|
-
assert.ok(result, 'Should return a result object')
|
|
21
|
-
assert.equal(result.source, 'MTG Elo Project')
|
|
22
|
-
assert.equal(result.url, url)
|
|
23
|
-
assert.equal(result.name, 'Bjoern Kimminich')
|
|
24
|
-
assert.equal(result.player_id, id)
|
|
20
|
+
assert.ok(result, 'Should return a result object')
|
|
21
|
+
assert.equal(result.source, 'MTG Elo Project')
|
|
22
|
+
assert.equal(result.url, url)
|
|
23
|
+
assert.equal(result.name, 'Bjoern Kimminich')
|
|
24
|
+
assert.equal(result.player_id, id)
|
|
25
25
|
if (result.record) {
|
|
26
|
-
assert.match(result.record, /^\d+-\d+(-\d+)?$/)
|
|
26
|
+
assert.match(result.record, /^\d+-\d+(-\d+)?$/)
|
|
27
27
|
}
|
|
28
28
|
if (result['win rate']) {
|
|
29
|
-
assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
|
|
29
|
+
assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
|
|
30
30
|
}
|
|
31
|
-
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const MtgEloFetcher = require('../src/fetchers/mtgElo')
|
|
4
|
+
|
|
5
|
+
test('MtgEloFetcher: parseHtml returns null when no name found', () => {
|
|
6
|
+
const fetcher = new MtgEloFetcher()
|
|
7
|
+
const html = '<html><body></body></html>'
|
|
8
|
+
|
|
9
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
10
|
+
|
|
11
|
+
assert.equal(result, null, 'Should return null when name cannot be extracted')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('MtgEloFetcher: parseHtml handles invalid JSON in astro-island gracefully', () => {
|
|
15
|
+
const fetcher = new MtgEloFetcher()
|
|
16
|
+
const html = `
|
|
17
|
+
<html>
|
|
18
|
+
<astro-island component-url="Profile" props='invalid json'></astro-island>
|
|
19
|
+
<div class="text-[22pt]">Fallback, Name</div>
|
|
20
|
+
<div class="text-[18pt]">Current rating: <span class="font-bold">1500</span></div>
|
|
21
|
+
<div class="text-[18pt]">Record: 10-5-2</div>
|
|
22
|
+
</html>
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
26
|
+
|
|
27
|
+
assert.equal(result.name, 'Name Fallback', 'Should use fallback parsing with flipped name')
|
|
28
|
+
assert.equal(result.current_rating, '1500')
|
|
29
|
+
assert.equal(result.record, '10-5-2')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('MtgEloFetcher: parseHtml uses fallback selectors when astro-island missing', () => {
|
|
33
|
+
const fetcher = new MtgEloFetcher()
|
|
34
|
+
const html = `
|
|
35
|
+
<html>
|
|
36
|
+
<div class="text-[22pt]">Smith, John</div>
|
|
37
|
+
<div class="text-[18pt]">Current rating: <span class="font-bold">1600</span></div>
|
|
38
|
+
<div class="text-[18pt]">Record: 20-10-5</div>
|
|
39
|
+
</html>
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
43
|
+
|
|
44
|
+
assert.equal(result.name, 'John Smith', 'Should flip comma-separated name')
|
|
45
|
+
assert.equal(result.current_rating, '1600')
|
|
46
|
+
assert.equal(result.record, '20-10-5')
|
|
47
|
+
assert.equal(result['win rate'], '57.14%', 'Should calculate win rate from record')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('MtgEloFetcher: parseHtml calculates win rate correctly with draws', () => {
|
|
51
|
+
const fetcher = new MtgEloFetcher()
|
|
52
|
+
const html = `
|
|
53
|
+
<html>
|
|
54
|
+
<div class="text-[22pt]">Test Player</div>
|
|
55
|
+
<div class="text-[18pt]">Record: 15-10-5</div>
|
|
56
|
+
</html>
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
60
|
+
|
|
61
|
+
assert.equal(result.record, '15-10-5')
|
|
62
|
+
assert.equal(result['win rate'], '50.00%')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('MtgEloFetcher: parseHtml handles record without draws', () => {
|
|
66
|
+
const fetcher = new MtgEloFetcher()
|
|
67
|
+
const html = `
|
|
68
|
+
<html>
|
|
69
|
+
<div class="text-[22pt]">Test Player</div>
|
|
70
|
+
<div class="text-[18pt]">Record: 12-8</div>
|
|
71
|
+
</html>
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
75
|
+
|
|
76
|
+
assert.equal(result.record, '12-8')
|
|
77
|
+
assert.equal(result['win rate'], '60.00%')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('MtgEloFetcher: parseHtml handles name without comma', () => {
|
|
81
|
+
const fetcher = new MtgEloFetcher()
|
|
82
|
+
const html = `
|
|
83
|
+
<html>
|
|
84
|
+
<div class="text-[22pt]">SingleName</div>
|
|
85
|
+
<div class="text-[18pt]">Record: 5-5-0</div>
|
|
86
|
+
</html>
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
const result = fetcher.parseHtml(html, 'http://test.com', 'test-id')
|
|
90
|
+
|
|
91
|
+
assert.equal(result.name, 'SingleName', 'Should handle single name without comma')
|
|
92
|
+
})
|