mtg-playerinfo 1.4.0 → 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.
@@ -1,15 +1,10 @@
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 UntappedFetcher = require('../src/fetchers/untapped')
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('UntappedFetcher: parses most recent match and extracts MTGA rank', () => {
7
+ test('UntappedFetcher: parseMatches extracts MTGA rank from fixture', () => {
13
8
  const fetcher = new UntappedFetcher()
14
9
  const fixtureJson = readFixture('untapped.json')
15
10
  const matches = JSON.parse(fixtureJson)
@@ -19,83 +14,99 @@ test('UntappedFetcher: parses most recent match and extracts MTGA rank', () => {
19
14
 
20
15
  assert.strictEqual(result.source, 'Untapped.gg')
21
16
  assert.strictEqual(result.url, url)
22
-
23
17
  assert.strictEqual(typeof result.mtga_rank, 'object')
24
18
 
25
- // Format is either "Rank Tier" (e.g. "Diamond 2") or "Mythic #Place" or "Mythic Percentile%" (e.g. "Mythic #123" or "Mythic 98.67%")
26
- if (result.mtga_rank.constructed !== undefined) {
27
- assert.match(result.mtga_rank.constructed, /^(Bronze|Silver|Gold|Platinum|Diamond|Mythic)(\s\d+|\s#\d+|\s\d+(\.\d+)?%)$/)
19
+ const rankRegex = /^(Bronze|Silver|Gold|Platinum|Diamond|Mythic)(\s\d+|\s#\d+|\s\d+(\.\d+)?%)$/
20
+ if (result.mtga_rank.constructed) {
21
+ assert.match(result.mtga_rank.constructed, rankRegex)
28
22
  }
29
- if (result.mtga_rank.limited !== undefined) {
30
- assert.match(result.mtga_rank.limited, /^(Bronze|Silver|Gold|Platinum|Diamond|Mythic)(\s\d+|\s#\d+|\s\d+(\.\d+)?%)$/)
23
+ if (result.mtga_rank.limited) {
24
+ assert.match(result.mtga_rank.limited, rankRegex)
31
25
  }
32
26
  })
33
27
 
34
- test('UntappedFetcher: handles missing rank data', () => {
28
+ test('UntappedFetcher: formats Mythic rank correctly', () => {
35
29
  const fetcher = new UntappedFetcher()
36
- const testMatches = [
30
+ const testCases = [
37
31
  {
38
- super_format: 2,
39
- friendly_ranking_class_after: null,
40
- friendly_ranking_tier_after: null,
41
- match_start: 1000
32
+ label: 'leaderboard place',
33
+ match: { super_format: 2, friendly_ranking_class_after: 'Mythic', friendly_mythic_leaderboard_place_after: 123, match_start: 1000 },
34
+ expected: 'Mythic #123'
42
35
  },
43
36
  {
44
- super_format: 1,
45
- friendly_ranking_class_after: null,
46
- friendly_ranking_tier_after: null,
47
- match_start: 2000
37
+ label: 'percentile',
38
+ match: { super_format: 1, friendly_ranking_class_after: 'Mythic', friendly_mythic_percentile_after: 98.789, match_start: 2000 },
39
+ expected: 'Mythic 98%'
48
40
  }
49
41
  ]
50
42
 
51
- const url = 'https://mtga.untapped.gg/profile/test-user/test-code'
52
- const result = fetcher.parseMatches(testMatches, url)
53
-
54
- assert.strictEqual('constructed' in result.mtga_rank, false)
55
- assert.strictEqual('limited' in result.mtga_rank, false)
43
+ for (const { label, match, expected } of testCases) {
44
+ const result = fetcher.parseMatches([match], 'url')
45
+ const rank = match.super_format === 2 ? result.mtga_rank.constructed : result.mtga_rank.limited
46
+ assert.strictEqual(rank, expected, `Failed for ${label}`)
47
+ }
56
48
  })
57
49
 
58
- test('UntappedFetcher: formats Mythic rank with leaderboard place or percentile', () => {
50
+ test('UntappedFetcher: handles missing or incomplete rank data', () => {
59
51
  const fetcher = new UntappedFetcher()
60
- const testMatches = [
61
- {
62
- super_format: 2,
63
- friendly_ranking_class_after: 'Mythic',
64
- friendly_ranking_tier_after: null,
65
- friendly_mythic_leaderboard_place_after: 123,
66
- friendly_mythic_percentile_after: 101.5,
67
- match_start: 1000
68
- },
69
- {
70
- super_format: 1,
71
- friendly_ranking_class_after: 'Mythic',
72
- friendly_ranking_tier_after: null,
73
- friendly_mythic_leaderboard_place_after: null,
74
- friendly_mythic_percentile_after: 98.789,
75
- match_start: 2000
76
- }
52
+ const cases = [
53
+ { label: 'null class', match: { super_format: 2, friendly_ranking_class_after: null } },
54
+ { label: 'Mythic no data', match: { super_format: 2, friendly_ranking_class_after: 'Mythic', friendly_mythic_leaderboard_place_after: null, friendly_mythic_percentile_after: null } },
55
+ { label: 'missing tier', match: { super_format: 2, friendly_ranking_class_after: 'Gold', friendly_ranking_tier_after: null } }
77
56
  ]
78
57
 
79
- const url = 'https://mtga.untapped.gg/profile/test-user/test-code'
80
- const result = fetcher.parseMatches(testMatches, url)
81
-
82
- assert.strictEqual(result.mtga_rank.constructed, 'Mythic #123')
83
- assert.strictEqual(result.mtga_rank.limited, 'Mythic 98%')
58
+ for (const { label, match } of cases) {
59
+ const result = fetcher.parseMatches([match], 'url')
60
+ assert.ok(!result.mtga_rank.constructed, `Should not have rank for ${label}`)
61
+ }
84
62
  })
85
63
 
86
- test('UntappedFetcher: constructs correct API URL from two-part ID', () => {
87
- const userId = '7de50700-c3f6-48e4-a38d-2add5b0d9b71'
88
- const playerCode = '76DCDWCZS5FX5PIEEMUVY6GV74'
89
- const id = `${userId}/${playerCode}`
64
+ test('UntappedFetcher: findMostRecentByFormat picks latest match', () => {
65
+ const fetcher = new UntappedFetcher()
66
+ const testMatches = [
67
+ { super_format: 2, friendly_ranking_class_after: 'Gold', friendly_ranking_tier_after: 4, match_start: 1000 },
68
+ { super_format: 2, friendly_ranking_class_after: 'Gold', friendly_ranking_tier_after: 1, match_start: 5000 },
69
+ { super_format: 2, friendly_ranking_class_after: 'Gold', friendly_ranking_tier_after: 2, match_start: 3000 }
70
+ ]
90
71
 
91
- const parts = id.split('/')
92
- assert.strictEqual(parts.length, 2)
93
- assert.strictEqual(parts[0], userId)
94
- assert.strictEqual(parts[1], playerCode)
72
+ const result = fetcher.parseMatches(testMatches, 'url')
73
+ assert.strictEqual(result.mtga_rank.constructed, 'Gold 1')
74
+ })
95
75
 
96
- const apiUrl = `https://api.mtga.untapped.gg/api/v1/games/users/${parts[0]}/players/${parts[1]}/?card_set=ECL`
97
- assert.strictEqual(apiUrl, 'https://api.mtga.untapped.gg/api/v1/games/users/7de50700-c3f6-48e4-a38d-2add5b0d9b71/players/76DCDWCZS5FX5PIEEMUVY6GV74/?card_set=ECL')
76
+ test('UntappedFetcher: fetchById handles various ID and response scenarios', async (t) => {
77
+ const fetcher = new UntappedFetcher()
98
78
 
99
- const profileUrl = `https://mtga.untapped.gg/profile/${parts[0]}/${parts[1]}`
100
- assert.strictEqual(profileUrl, 'https://mtga.untapped.gg/profile/7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74')
79
+ await t.test('invalid ID format', async () => {
80
+ await withMutedConsole(async () => {
81
+ const result = await fetcher.fetchById('invalid-id')
82
+ assert.strictEqual(result, null)
83
+ })
84
+ })
85
+
86
+ await t.test('network error', async () => {
87
+ const mockRequest = t.mock.method(httpClient, 'request', () => { throw new Error('Network Error') })
88
+ await withMutedConsole(async () => {
89
+ const result = await fetcher.fetchById('user/code')
90
+ assert.strictEqual(result, null)
91
+ })
92
+ mockRequest.mock.restore()
93
+ })
94
+
95
+ await t.test('empty matches array', async () => {
96
+ const mockRequest = t.mock.method(httpClient, 'request', async () => ({ data: [] }))
97
+ await withMutedConsole(async () => {
98
+ const result = await fetcher.fetchById('user/code')
99
+ assert.strictEqual(result, null)
100
+ })
101
+ mockRequest.mock.restore()
102
+ })
103
+
104
+ await t.test('non-array data', async () => {
105
+ const mockRequest = t.mock.method(httpClient, 'request', async () => ({ data: {} }))
106
+ await withMutedConsole(async () => {
107
+ const result = await fetcher.fetchById('user/code')
108
+ assert.strictEqual(result, null)
109
+ })
110
+ mockRequest.mock.restore()
111
+ })
101
112
  })
@@ -1,128 +0,0 @@
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
- })
@@ -1,53 +0,0 @@
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
- })
@@ -1,92 +0,0 @@
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
- })
@@ -1,123 +0,0 @@
1
- const test = require('node:test')
2
- const assert = require('node:assert/strict')
3
- const UnityLeagueFetcher = require('../src/fetchers/unityLeague')
4
-
5
- test('UnityLeagueFetcher: parseHtml ignores non-player_profile images', () => {
6
- const fetcher = new UnityLeagueFetcher()
7
- const html = `
8
- <html>
9
- <h1 class="d-inline">Test Player</h1>
10
- <div class="card-body">
11
- <img class="img-fluid" src="https://unityleague.gg/media/default_avatar.jpg" />
12
- </div>
13
- </html>
14
- `
15
-
16
- const result = fetcher.parseHtml(html, 'http://test.com')
17
-
18
- assert.equal(result.photo, null, 'Should not use non-player_profile images')
19
- assert.equal(result.name, 'Test Player')
20
- })
21
-
22
- test('UnityLeagueFetcher: parseHtml handles missing country data gracefully', () => {
23
- const fetcher = new UnityLeagueFetcher()
24
- const html = `
25
- <html>
26
- <h1 class="d-inline">Test Player</h1>
27
- <dt class="small text-muted">Country:</dt>
28
- <dd></dd>
29
- </html>
30
- `
31
-
32
- const result = fetcher.parseHtml(html, 'http://test.com')
33
-
34
- assert.ok(result, 'Should return result even with missing country')
35
- assert.equal(result.name, 'Test Player')
36
- assert.equal(result.country, '', 'Country should be empty string')
37
- })
38
-
39
- test('UnityLeagueFetcher: parseHtml handles country text without flag icon', () => {
40
- const fetcher = new UnityLeagueFetcher()
41
- const html = `
42
- <html>
43
- <h1 class="d-inline">Test Player</h1>
44
- <dt class="small text-muted">Country:</dt>
45
- <dd>Germany</dd>
46
- </html>
47
- `
48
-
49
- const result = fetcher.parseHtml(html, 'http://test.com')
50
-
51
- assert.equal(result.country, 'Germany', 'Should use text when no flag icon')
52
- })
53
-
54
- test('UnityLeagueFetcher: parseHtml handles missing bio element', () => {
55
- const fetcher = new UnityLeagueFetcher()
56
- const html = `
57
- <html>
58
- <h1 class="d-inline">Test Player</h1>
59
- </html>
60
- `
61
-
62
- const result = fetcher.parseHtml(html, 'http://test.com')
63
-
64
- assert.ok(!result.bio, 'Bio should be undefined when element missing')
65
- })
66
-
67
- test('UnityLeagueFetcher: parseHtml extracts country from header flag', () => {
68
- const fetcher = new UnityLeagueFetcher()
69
- const html = `
70
- <html>
71
- <h1 class="d-inline">Test Player</h1>
72
- <div class="card-body">
73
- <i class="fi fi-us"></i>
74
- </div>
75
- </html>
76
- `
77
-
78
- const result = fetcher.parseHtml(html, 'http://test.com')
79
-
80
- assert.equal(result.country, 'us', 'Should extract country code from header flag')
81
- })
82
-
83
- test('UnityLeagueFetcher: parseHtml handles missing ranking table', () => {
84
- const fetcher = new UnityLeagueFetcher()
85
- const html = `
86
- <html>
87
- <h1 class="d-inline">Test Player</h1>
88
- </html>
89
- `
90
-
91
- const result = fetcher.parseHtml(html, 'http://test.com')
92
-
93
- assert.ok(result, 'Should handle missing ranking table')
94
- assert.ok(!result['rank germany'], 'Should not have rank data')
95
- })
96
-
97
- test('UnityLeagueFetcher: parseHtml extracts ranking data with special characters', () => {
98
- const fetcher = new UnityLeagueFetcher()
99
- const html = `
100
- <html>
101
- <h1 class="d-inline">Test Player</h1>
102
- <table class="table-sm">
103
- <thead>
104
- <tr>
105
- <th>Germany</th>
106
- <th>Europe</th>
107
- </tr>
108
- </thead>
109
- <tbody>
110
- <tr>
111
- <td>#42</td>
112
- <td>1st</td>
113
- </tr>
114
- </tbody>
115
- </table>
116
- </html>
117
- `
118
-
119
- const result = fetcher.parseHtml(html, 'http://test.com')
120
-
121
- assert.equal(result['rank germany'], '42', 'Should extract numeric value from #42')
122
- assert.equal(result['rank europe'], '1', 'Should extract numeric value from 1st')
123
- })