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,312 @@
|
|
|
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
|
+
|
|
6
|
+
const PlayerInfoManager = require('../src')
|
|
7
|
+
|
|
8
|
+
function readFixture (name) {
|
|
9
|
+
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fixtures = {
|
|
13
|
+
unityLeague: readFixture('unityLeague.html'),
|
|
14
|
+
mtgElo: readFixture('mtgElo.html'),
|
|
15
|
+
melee: readFixture('melee.html'),
|
|
16
|
+
topdeck: readFixture('topdeck.html')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createMockedManager () {
|
|
20
|
+
const manager = new PlayerInfoManager()
|
|
21
|
+
|
|
22
|
+
manager.fetchers.unity.fetchById = async function (id) {
|
|
23
|
+
const url = `https://unityleague.gg/player/${id}/`
|
|
24
|
+
return this.parseHtml(fixtures.unityLeague, url)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
manager.fetchers.mtgelo.fetchById = async function (id) {
|
|
28
|
+
const url = `https://mtgeloproject.net/profile/${id}`
|
|
29
|
+
return this.parseHtml(fixtures.mtgElo, url, id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
manager.fetchers.melee.fetchById = async function (username) {
|
|
33
|
+
const url = `https://melee.gg/Profile/Index/${username}`
|
|
34
|
+
return this.parseHtml(fixtures.melee, url, username)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
manager.fetchers.topdeck.fetchById = async function (handle) {
|
|
38
|
+
const cleanHandle = handle.startsWith('@') ? handle : `@${handle}`
|
|
39
|
+
const url = `https://topdeck.gg/profile/${cleanHandle}`
|
|
40
|
+
return this.parseHtml(fixtures.topdeck, url, cleanHandle)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return manager
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('PlayerInfoManager: getPlayerInfo with all four sources merges data correctly', async () => {
|
|
47
|
+
const manager = createMockedManager()
|
|
48
|
+
|
|
49
|
+
const result = await manager.getPlayerInfo({
|
|
50
|
+
unityId: '16215',
|
|
51
|
+
mtgeloId: '3irvwtmk',
|
|
52
|
+
meleeUser: 'k0shiii',
|
|
53
|
+
topdeckHandle: 'k0shiii'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
assert.ok(result.general, 'Result should have general property')
|
|
57
|
+
assert.ok(result.sources, 'Result should have sources property')
|
|
58
|
+
assert.ok(result.sources['Unity League'], 'Should have Unity League source')
|
|
59
|
+
assert.ok(result.sources['MTG Elo Project'], 'Should have MTG Elo Project source')
|
|
60
|
+
assert.ok(result.sources.Melee, 'Should have Melee source')
|
|
61
|
+
assert.ok(result.sources.Topdeck, 'Should have Topdeck source')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('PlayerInfoManager: name property takes priority from Unity League over MTG Elo', async () => {
|
|
65
|
+
const manager = createMockedManager()
|
|
66
|
+
|
|
67
|
+
const result = await manager.getPlayerInfo({
|
|
68
|
+
unityId: '16215',
|
|
69
|
+
mtgeloId: '3irvwtmk',
|
|
70
|
+
meleeUser: 'k0shiii',
|
|
71
|
+
topdeckHandle: 'k0shiii'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
assert.equal(result.general.name, 'Björn Kimminich', 'Name should come from Unity League')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('PlayerInfoManager: photo property takes priority from Unity League over Topdeck', async () => {
|
|
78
|
+
const manager = createMockedManager()
|
|
79
|
+
|
|
80
|
+
const result = await manager.getPlayerInfo({
|
|
81
|
+
unityId: '16215',
|
|
82
|
+
mtgeloId: '3irvwtmk',
|
|
83
|
+
meleeUser: 'k0shiii',
|
|
84
|
+
topdeckHandle: 'k0shiii'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
assert.equal(result.general.photo, 'https://unityleague.gg/media/player_profile/1000023225.jpg',
|
|
88
|
+
'Photo should come from Unity League')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('PlayerInfoManager: country property comes from Unity League', async () => {
|
|
92
|
+
const manager = createMockedManager()
|
|
93
|
+
|
|
94
|
+
const result = await manager.getPlayerInfo({
|
|
95
|
+
unityId: '16215',
|
|
96
|
+
mtgeloId: '3irvwtmk',
|
|
97
|
+
meleeUser: 'k0shiii',
|
|
98
|
+
topdeckHandle: 'k0shiii'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Only Unity League provides country
|
|
102
|
+
assert.equal(result.general.country, 'de', 'Country should come from Unity League')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('PlayerInfoManager: bio property takes priority from Unity League over Melee', async () => {
|
|
106
|
+
const manager = createMockedManager()
|
|
107
|
+
|
|
108
|
+
const result = await manager.getPlayerInfo({
|
|
109
|
+
unityId: '16215',
|
|
110
|
+
mtgeloId: '3irvwtmk',
|
|
111
|
+
meleeUser: 'k0shiii',
|
|
112
|
+
topdeckHandle: 'k0shiii'
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
assert.ok(result.general.bio, 'Bio should be present')
|
|
116
|
+
assert.ok(result.general.bio.includes('Untimely Malfunction'), 'Bio should contain text from Unity League and Melee profile')
|
|
117
|
+
assert.ok(result.general.bio.includes('Change the target of target spell or ability with a single target'),
|
|
118
|
+
'Bio should contain text from only Unity League profile')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('PlayerInfoManager: pronouns property comes from Melee', async () => {
|
|
122
|
+
const manager = createMockedManager()
|
|
123
|
+
|
|
124
|
+
const result = await manager.getPlayerInfo({
|
|
125
|
+
unityId: '16215',
|
|
126
|
+
mtgeloId: '3irvwtmk',
|
|
127
|
+
meleeUser: 'k0shiii',
|
|
128
|
+
topdeckHandle: 'k0shiii'
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
assert.equal(result.general.pronouns, 'He/Him', 'Pronouns should come from Melee')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('PlayerInfoManager: social links (facebook, twitch, youtube) come from Melee', async () => {
|
|
135
|
+
const manager = createMockedManager()
|
|
136
|
+
|
|
137
|
+
const result = await manager.getPlayerInfo({
|
|
138
|
+
unityId: '16215',
|
|
139
|
+
mtgeloId: '3irvwtmk',
|
|
140
|
+
meleeUser: 'k0shiii',
|
|
141
|
+
topdeckHandle: 'k0shiii'
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
assert.equal(result.general.facebook, 'bjoern.kimminich', 'Facebook should come from Melee')
|
|
145
|
+
assert.equal(result.general.twitch, 'koshiii', 'Twitch should come from Melee')
|
|
146
|
+
assert.equal(result.general.youtube, '@BjörnKimminich', 'YouTube should come from Melee')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('PlayerInfoManager: win rate is averaged across all sources', async () => {
|
|
150
|
+
const manager = createMockedManager()
|
|
151
|
+
|
|
152
|
+
const result = await manager.getPlayerInfo({
|
|
153
|
+
unityId: '16215',
|
|
154
|
+
mtgeloId: '3irvwtmk',
|
|
155
|
+
meleeUser: 'k0shiii',
|
|
156
|
+
topdeckHandle: 'k0shiii'
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
assert.ok(result.general['win rate'], 'Win rate should be present')
|
|
160
|
+
assert.match(result.general['win rate'], /^\d+(\.\d+)?%$/, 'Win rate should be a percentage')
|
|
161
|
+
// TODO Calculate that the win rate is actually the average of all available source win rates
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('PlayerInfoManager: team property comes from Unity League', async () => {
|
|
165
|
+
const manager = createMockedManager()
|
|
166
|
+
|
|
167
|
+
const result = await manager.getPlayerInfo({
|
|
168
|
+
unityId: '16215',
|
|
169
|
+
mtgeloId: '3irvwtmk',
|
|
170
|
+
meleeUser: 'k0shiii',
|
|
171
|
+
topdeckHandle: 'k0shiii'
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
assert.equal(result.general.team, 'Mull to Five', 'Team should come from Unity League')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('PlayerInfoManager: hometown property comes from Unity League', async () => {
|
|
178
|
+
const manager = createMockedManager()
|
|
179
|
+
|
|
180
|
+
const result = await manager.getPlayerInfo({
|
|
181
|
+
unityId: '16215',
|
|
182
|
+
mtgeloId: '3irvwtmk',
|
|
183
|
+
meleeUser: 'k0shiii',
|
|
184
|
+
topdeckHandle: 'k0shiii'
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
assert.equal(result.general.hometown, 'Hamburg', 'Hometown should come from Unity League')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('PlayerInfoManager: age property comes from Unity League', async () => {
|
|
191
|
+
const manager = createMockedManager()
|
|
192
|
+
|
|
193
|
+
const result = await manager.getPlayerInfo({
|
|
194
|
+
unityId: '16215',
|
|
195
|
+
mtgeloId: '3irvwtmk',
|
|
196
|
+
meleeUser: 'k0shiii',
|
|
197
|
+
topdeckHandle: 'k0shiii'
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
assert.equal(result.general.age, '45', 'Age should come from Unity League')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('PlayerInfoManager: each source contains its own player profile URL', async () => {
|
|
204
|
+
const manager = createMockedManager()
|
|
205
|
+
|
|
206
|
+
const result = await manager.getPlayerInfo({
|
|
207
|
+
unityId: '16215',
|
|
208
|
+
mtgeloId: '3irvwtmk',
|
|
209
|
+
meleeUser: 'k0shiii',
|
|
210
|
+
topdeckHandle: 'k0shiii'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
assert.equal(result.sources['Unity League'].url, 'https://unityleague.gg/player/16215/',
|
|
214
|
+
'Unity League URL should be correct')
|
|
215
|
+
assert.equal(result.sources['MTG Elo Project'].url, 'https://mtgeloproject.net/profile/3irvwtmk',
|
|
216
|
+
'MTG Elo Project URL should be correct')
|
|
217
|
+
assert.equal(result.sources.Melee.url, 'https://melee.gg/Profile/Index/k0shiii',
|
|
218
|
+
'Melee URL should be correct')
|
|
219
|
+
assert.equal(result.sources.Topdeck.url, 'https://topdeck.gg/profile/@k0shiii',
|
|
220
|
+
'Topdeck URL should be correct')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('PlayerInfoManager: each source contains its specific data in the sources object', async () => {
|
|
224
|
+
const manager = createMockedManager()
|
|
225
|
+
|
|
226
|
+
const result = await manager.getPlayerInfo({
|
|
227
|
+
unityId: '16215',
|
|
228
|
+
mtgeloId: '3irvwtmk',
|
|
229
|
+
meleeUser: 'k0shiii',
|
|
230
|
+
topdeckHandle: 'k0shiii'
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
assert.match(result.sources['Unity League'].data['rank germany'], /^\d+$/,
|
|
234
|
+
'Unity League should have rank germany')
|
|
235
|
+
assert.match(result.sources['Unity League'].data['rank europe'], /^\d+$/,
|
|
236
|
+
'Unity League should have rank europe')
|
|
237
|
+
assert.match(result.sources['Unity League'].data['rank points'], /^\d+$/,
|
|
238
|
+
'Unity League should have rank points')
|
|
239
|
+
|
|
240
|
+
assert.ok(result.sources['MTG Elo Project'].data.player_id,
|
|
241
|
+
'MTG Elo should have player_id')
|
|
242
|
+
assert.ok(result.sources['MTG Elo Project'].data.current_rating,
|
|
243
|
+
'MTG Elo should have current_rating')
|
|
244
|
+
|
|
245
|
+
assert.equal(result.sources.Melee.data.name, 'Björn Kimminich',
|
|
246
|
+
'Melee should have name')
|
|
247
|
+
|
|
248
|
+
assert.equal(result.sources.Topdeck.data.name, 'Björn Kimminich',
|
|
249
|
+
'Topdeck should have name')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('PlayerInfoManager: priority order verification - Unity League > MTG Elo > Melee > Topdeck', async () => {
|
|
253
|
+
const manager = createMockedManager()
|
|
254
|
+
|
|
255
|
+
const result = await manager.getPlayerInfo({
|
|
256
|
+
unityId: '16215',
|
|
257
|
+
mtgeloId: '3irvwtmk',
|
|
258
|
+
meleeUser: 'k0shiii',
|
|
259
|
+
topdeckHandle: 'k0shiii'
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
assert.equal(result.general.name, 'Björn Kimminich',
|
|
263
|
+
'Name should use Unity League spelling "Björn Kimminich", not MTG Elo spelling "Bjoern Kimminich"')
|
|
264
|
+
|
|
265
|
+
assert.ok(result.general.photo.includes('unityleague.gg'),
|
|
266
|
+
'Photo should come from Unity League, not Topdeck')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('PlayerInfoManager: with subset of sources - only Unity and Melee', async () => {
|
|
270
|
+
const manager = createMockedManager()
|
|
271
|
+
|
|
272
|
+
const result = await manager.getPlayerInfo({
|
|
273
|
+
unityId: '16215',
|
|
274
|
+
meleeUser: 'k0shiii'
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
assert.equal(Object.keys(result.sources).length, 2, 'Should have exactly 2 sources')
|
|
278
|
+
assert.ok(result.sources['Unity League'], 'Should have Unity League')
|
|
279
|
+
assert.ok(result.sources.Melee, 'Should have Melee')
|
|
280
|
+
assert.ok(!result.sources['MTG Elo Project'], 'Should not have MTG Elo Project')
|
|
281
|
+
assert.ok(!result.sources.Topdeck, 'Should not have Topdeck')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('PlayerInfoManager: with only Topdeck source', async () => {
|
|
285
|
+
const manager = createMockedManager()
|
|
286
|
+
|
|
287
|
+
const result = await manager.getPlayerInfo({
|
|
288
|
+
topdeckHandle: 'k0shiii'
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
assert.equal(Object.keys(result.sources).length, 1, 'Should have exactly 1 source')
|
|
292
|
+
assert.ok(result.sources.Topdeck, 'Should have Topdeck')
|
|
293
|
+
|
|
294
|
+
assert.equal(result.general.name, 'Björn Kimminich', 'Name should come from Topdeck')
|
|
295
|
+
assert.ok(result.general.photo, 'Photo should be present from Topdeck')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('PlayerInfoManager: with sources in different order (Melee first, then Unity)', async () => {
|
|
299
|
+
const manager = createMockedManager()
|
|
300
|
+
|
|
301
|
+
const result = await manager.getPlayerInfo({
|
|
302
|
+
meleeUser: 'k0shiii',
|
|
303
|
+
unityId: '16215'
|
|
304
|
+
}, ['melee', 'unity'])
|
|
305
|
+
|
|
306
|
+
assert.equal(result.general.name, 'Björn Kimminich', 'Name should use Melee version')
|
|
307
|
+
assert.ok(result.general.photo.includes('unityleague.gg'),
|
|
308
|
+
'Photo should come from Unity League as Melee does not return one')
|
|
309
|
+
assert.ok(result.general.bio.includes('Untimely Malfunction'), 'Bio should contain text from Unity League and Melee profile')
|
|
310
|
+
assert.ok(!result.general.bio.includes('Change the target of target spell or ability with a single target'),
|
|
311
|
+
'Bio should not contain text from only Unity League profile')
|
|
312
|
+
})
|
package/test/topdeck.test.js
CHANGED
|
@@ -1,33 +1,46 @@
|
|
|
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
|
+
const { mock } = require('node:test')
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const httpClient = require('../src/utils/httpClient')
|
|
8
|
+
const TopdeckFetcher = require('../src/fetchers/topdeck')
|
|
7
9
|
|
|
8
|
-
function readFixture(name) {
|
|
9
|
-
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
10
|
+
function readFixture (name) {
|
|
11
|
+
return fs.readFileSync(path.join(__dirname, 'data', name), 'utf8')
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
test('TopdeckFetcher
|
|
13
|
-
const html = readFixture('topdeck.html')
|
|
14
|
-
const handle = '@k0shiii'
|
|
15
|
-
const url = `https://topdeck.gg/profile/${handle}
|
|
16
|
-
const fetcher = new TopdeckFetcher()
|
|
17
|
-
|
|
18
|
-
const result = fetcher.parseHtml(html, url, handle)
|
|
19
|
-
|
|
20
|
-
assert.ok(result, 'Should return a result object')
|
|
21
|
-
assert.equal(result.source, 'Topdeck')
|
|
22
|
-
assert.equal(result.url, url)
|
|
23
|
-
assert.equal(result.name, 'Björn Kimminich')
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
14
|
+
test('TopdeckFetcher: parseHtml extracts stats from DOM when available', () => {
|
|
15
|
+
const html = readFixture('topdeck.html')
|
|
16
|
+
const handle = '@k0shiii'
|
|
17
|
+
const url = `https://topdeck.gg/profile/${handle}`
|
|
18
|
+
const fetcher = new TopdeckFetcher()
|
|
19
|
+
|
|
20
|
+
const result = fetcher.parseHtml(html, url, handle)
|
|
21
|
+
|
|
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
|
+
})
|
|
27
|
+
|
|
28
|
+
test('TopdeckFetcher: fetchStats updates playerInfo with data from stats JSON', async () => {
|
|
29
|
+
const statsJson = readFixture('topdeck.json')
|
|
30
|
+
const internalId = 'm4VSTJShiXR1PCSCWaM9TBY0rcg1'
|
|
31
|
+
const fetcher = new TopdeckFetcher()
|
|
32
|
+
const playerInfo = { source: 'Topdeck' }
|
|
33
|
+
|
|
34
|
+
mock.method(httpClient, 'request', async (url) => {
|
|
35
|
+
if (url.endsWith(`/profile/${internalId}/stats`)) {
|
|
36
|
+
return { data: statsJson }
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unexpected URL: ${url}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await fetcher.fetchStats(internalId, playerInfo)
|
|
42
|
+
|
|
43
|
+
assert.match(playerInfo.tournaments, /^\d+$/)
|
|
44
|
+
assert.match(playerInfo.record, /^\d+-\d+-\d+$/)
|
|
45
|
+
assert.match(playerInfo['win rate'], /^\d+(\.\d+)?%$/)
|
|
46
|
+
})
|
package/test/unityLeague.test.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
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 UnityLeagueFetcher = require('../src/fetchers/unityLeague')
|
|
6
|
+
const UnityLeagueFetcher = require('../src/fetchers/unityLeague')
|
|
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('UnityLeagueFetcher
|
|
13
|
-
const html = readFixture('unityLeague.html')
|
|
14
|
-
const url = 'https://unityleague.gg/player/16215/'
|
|
15
|
-
const fetcher = new UnityLeagueFetcher()
|
|
12
|
+
test('UnityLeagueFetcher: parseHtml extracts profile, photo, ranks and stats', () => {
|
|
13
|
+
const html = readFixture('unityLeague.html')
|
|
14
|
+
const url = 'https://unityleague.gg/player/16215/'
|
|
15
|
+
const fetcher = new UnityLeagueFetcher()
|
|
16
16
|
|
|
17
|
-
const result = fetcher.parseHtml(html, url)
|
|
17
|
+
const result = fetcher.parseHtml(html, url)
|
|
18
18
|
|
|
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.
|
|
26
|
-
assert.
|
|
27
|
-
assert.
|
|
28
|
-
assert.
|
|
29
|
-
assert.
|
|
30
|
-
})
|
|
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+$/)
|
|
28
|
+
assert.match(result.record, /^\d+-\d+-\d+$/)
|
|
29
|
+
assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
|
|
30
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
|
|
6
|
+
const UntappedFetcher = require('../src/fetchers/untapped')
|
|
7
|
+
|
|
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', () => {
|
|
13
|
+
const fetcher = new UntappedFetcher()
|
|
14
|
+
const fixtureJson = readFixture('untapped.json')
|
|
15
|
+
const matches = JSON.parse(fixtureJson)
|
|
16
|
+
|
|
17
|
+
const url = 'https://mtga.untapped.gg/profile/7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74'
|
|
18
|
+
const result = fetcher.parseMatch(matches[0], url)
|
|
19
|
+
|
|
20
|
+
assert.strictEqual(result.source, 'Untapped.gg')
|
|
21
|
+
assert.strictEqual(result.url, url)
|
|
22
|
+
|
|
23
|
+
assert.match(result.mtga_rank, /^(Bronze|Silver|Gold|Platinum|Diamond|Mythic)\s\d+$/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('UntappedFetcher: handles missing rank data', () => {
|
|
27
|
+
const fetcher = new UntappedFetcher()
|
|
28
|
+
const testMatch = {
|
|
29
|
+
friendly_ranking_class_after: null,
|
|
30
|
+
friendly_ranking_tier_after: null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const url = 'https://mtga.untapped.gg/profile/test-user/test-code'
|
|
34
|
+
const result = fetcher.parseMatch(testMatch, url)
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(result.mtga_rank, null)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('UntappedFetcher: constructs correct API URL from two-part ID', () => {
|
|
40
|
+
const fetcher = new UntappedFetcher()
|
|
41
|
+
const userId = '7de50700-c3f6-48e4-a38d-2add5b0d9b71'
|
|
42
|
+
const playerCode = '76DCDWCZS5FX5PIEEMUVY6GV74'
|
|
43
|
+
const id = `${userId}/${playerCode}`
|
|
44
|
+
|
|
45
|
+
const parts = id.split('/')
|
|
46
|
+
assert.strictEqual(parts.length, 2)
|
|
47
|
+
assert.strictEqual(parts[0], userId)
|
|
48
|
+
assert.strictEqual(parts[1], playerCode)
|
|
49
|
+
|
|
50
|
+
const apiUrl = `https://api.mtga.untapped.gg/api/v1/games/users/${parts[0]}/players/${parts[1]}/?card_set=ECL`
|
|
51
|
+
assert.strictEqual(apiUrl, 'https://api.mtga.untapped.gg/api/v1/games/users/7de50700-c3f6-48e4-a38d-2add5b0d9b71/players/76DCDWCZS5FX5PIEEMUVY6GV74/?card_set=ECL')
|
|
52
|
+
|
|
53
|
+
const profileUrl = `https://mtga.untapped.gg/profile/${parts[0]}/${parts[1]}`
|
|
54
|
+
assert.strictEqual(profileUrl, 'https://mtga.untapped.gg/profile/7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|