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 CHANGED
@@ -53,8 +53,11 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
53
53
  "facebook": "bjoern.kimminich",
54
54
  "twitch": "koshiii",
55
55
  "youtube": "@BjörnKimminich",
56
- "mtga_rank": "Platinum 4",
57
- "win rate": "47.37%"
56
+ "mtga_rank": {
57
+ "constructed": "Platinum 3",
58
+ "limited": "Bronze 4"
59
+ },
60
+ "win rate": "47.45%"
58
61
  },
59
62
  "sources": {
60
63
  "Unity League": {
@@ -69,10 +72,10 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
69
72
  "team": "Mull to Five",
70
73
  "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 with its \"Change the target of target spell or ability with a single target\" mode.",
71
74
  "rank germany": "62",
72
- "rank europe": "503",
73
- "rank points": "325",
74
- "record": "50-45-5",
75
- "win rate": "51.7%"
75
+ "rank europe": "506",
76
+ "rank points": "334",
77
+ "record": "52-47-5",
78
+ "win rate": "51.6%"
76
79
  }
77
80
  },
78
81
  "MTG Elo Project": {
@@ -101,6 +104,9 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
101
104
  "data": {
102
105
  "name": "Björn Kimminich",
103
106
  "photo": "https://imagedelivery.net/kN_u_RUfFF6xsGMKYWhO1g/2a7b8d12-5924-4a58-5f9c-c0bf55766800/square",
107
+ "pronouns": "He/Him",
108
+ "twitter": "bkimminich",
109
+ "youtube": "@BjörnKimminich",
104
110
  "tournaments": "2",
105
111
  "record": "4-6-1",
106
112
  "win rate": "36.36%",
@@ -110,7 +116,10 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
110
116
  "Untapped.gg": {
111
117
  "url": "https://mtga.untapped.gg/profile/7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74",
112
118
  "data": {
113
- "mtga_rank": "Platinum 4"
119
+ "mtga_rank": {
120
+ "constructed": "Platinum 3",
121
+ "limited": "Bronze 4"
122
+ }
114
123
  }
115
124
  }
116
125
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "mtg-playerinfo",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A simple NPM module and CLI tool to pull Magic: The Gathering player data from various sources",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "mtg-playerinfo": "./cli.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "node --test",
11
- "test:coverage": "nyc --reporter=lcov --produce-source-map node --test",
10
+ "test": "node --test \"**/*.test.js\"",
11
+ "test:coverage": "nyc --reporter=lcov --produce-source-map node --test \"**/*.test.js\"",
12
12
  "lint": "standard",
13
13
  "lint:fix": "standard --fix"
14
14
  },
@@ -16,10 +16,9 @@
16
16
  "extension": [
17
17
  ".js"
18
18
  ],
19
- "include": [
20
- "src/fetchers/*.js",
21
- "src/index.js",
22
- "cli.js"
19
+ "exclude": [
20
+ "src/utils/httpClient.js",
21
+ "test/"
23
22
  ],
24
23
  "sourceMap": true,
25
24
  "instrument": true
@@ -1,4 +1,4 @@
1
- const { request } = require('../utils/httpClient')
1
+ const httpClient = require('../utils/httpClient')
2
2
  const cheerio = require('cheerio')
3
3
  const { extractHandle, getPlatformName } = require('../utils/socialMediaExtractor')
4
4
 
@@ -6,7 +6,7 @@ class MeleeFetcher {
6
6
  async fetchById (username) {
7
7
  const url = `https://melee.gg/Profile/Index/${username}`
8
8
  try {
9
- const { data } = await request(url)
9
+ const { data } = await httpClient.request(url)
10
10
  return this.parseHtml(data, url, username)
11
11
  } catch (error) {
12
12
  console.error(`Error fetching Melee profile ${username}:`, error.message)
@@ -1,10 +1,10 @@
1
- const { request } = require('../utils/httpClient')
1
+ const httpClient = require('../utils/httpClient')
2
2
 
3
3
  class MtgEloFetcher {
4
4
  async fetchById (id) {
5
5
  const url = `https://mtgeloproject.net/profile/${id}`
6
6
  try {
7
- const { data: html } = await request(url, {
7
+ const { data: html } = await httpClient.request(url, {
8
8
  maxRedirects: 10
9
9
  })
10
10
 
@@ -1,11 +1,11 @@
1
- const { request } = require('../utils/httpClient')
1
+ const httpClient = require('../utils/httpClient')
2
2
  const cheerio = require('cheerio')
3
3
 
4
4
  class UnityLeagueFetcher {
5
5
  async fetchById (id) {
6
6
  const url = `https://unityleague.gg/player/${id}/`
7
7
  try {
8
- const { data } = await request(url)
8
+ const { data } = await httpClient.request(url)
9
9
  return this.parseHtml(data, url)
10
10
  } catch (error) {
11
11
  console.error(`Error fetching Unity League player ${id}:`, error.message)
@@ -1,4 +1,4 @@
1
- const { request } = require('../utils/httpClient')
1
+ const httpClient = require('../utils/httpClient')
2
2
 
3
3
  class UntappedFetcher {
4
4
  async fetchById (id) {
@@ -14,7 +14,7 @@ class UntappedFetcher {
14
14
  const apiUrl = `https://api.mtga.untapped.gg/api/v1/games/users/${userId}/players/${playerCode}/?card_set=ECL`
15
15
 
16
16
  try {
17
- const { data } = await request(apiUrl)
17
+ const { data } = await httpClient.request(apiUrl)
18
18
  const matches = typeof data === 'string' ? JSON.parse(data) : data
19
19
 
20
20
  if (!Array.isArray(matches) || matches.length === 0) {
@@ -47,4 +47,3 @@ module.exports = {
47
47
  extractHandle,
48
48
  getPlatformName
49
49
  }
50
-
@@ -0,0 +1,32 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+
4
+ /**
5
+ * Reads a fixture file from the test/data directory
6
+ * @param {string} name - The name of the fixture file
7
+ * @returns {string} - The content of the fixture file
8
+ */
9
+ function readFixture (name) {
10
+ return fs.readFileSync(path.join(__dirname, '..', 'test', 'data', name), 'utf8')
11
+ }
12
+
13
+ /**
14
+ * Executes a function with console methods muted to avoid cluttering test output
15
+ * @param {Function} fn - The function to execute
16
+ * @returns {Promise<any>|any} - The result of the function
17
+ */
18
+ async function withMutedConsole (fn) {
19
+ const orig = { error: console.error, warn: console.warn, log: console.log }
20
+ try {
21
+ console.error = () => {}
22
+ console.warn = () => {}
23
+ console.log = () => {}
24
+ return await fn()
25
+ } finally {
26
+ console.error = orig.error
27
+ console.warn = orig.warn
28
+ console.log = orig.log
29
+ }
30
+ }
31
+
32
+ module.exports = { readFixture, withMutedConsole }
@@ -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 MeleeFetcher = require('../src/fetchers/melee')
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('MeleeFetcher: parseHtml extracts basic profile info', () => {
7
+ test('MeleeFetcher: parseHtml extracts info from fixture', () => {
13
8
  const html = readFixture('melee.html')
14
9
  const url = 'https://melee.gg/Profile/Index/k0shiii'
15
10
  const username = 'k0shiii'
@@ -17,13 +12,55 @@ test('MeleeFetcher: parseHtml extracts basic profile info', () => {
17
12
 
18
13
  const result = fetcher.parseHtml(html, url, username)
19
14
 
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')
15
+ assert.strictEqual(result.source, 'Melee')
16
+ assert.strictEqual(result.url, url)
17
+ assert.strictEqual(result.name, 'Björn Kimminich')
18
+ assert.strictEqual(result.pronouns, 'He/Him')
19
+ assert.ok(result.bio.includes('Smugly held back'))
20
+ assert.strictEqual(result.facebook, 'bjoern.kimminich')
21
+ assert.strictEqual(result.twitch, 'koshiii')
22
+ assert.strictEqual(result.youtube, '@BjörnKimminich')
23
+ })
24
+
25
+ test('MeleeFetcher: parseHtml handles minimal/missing data', () => {
26
+ const fetcher = new MeleeFetcher()
27
+
28
+ const result = fetcher.parseHtml('<html><body></body></html>', 'url', 'fallback')
29
+ assert.strictEqual(result.name, 'fallback')
30
+ assert.strictEqual(result.pronouns, null)
31
+ assert.strictEqual(result.bio, null)
32
+ })
33
+
34
+ test('MeleeFetcher: parseHtml extracts social links correctly', () => {
35
+ const fetcher = new MeleeFetcher()
36
+ const html = `
37
+ <a class="social-link" href="https://twitter.com/testuser">Twitter</a>
38
+ <a class="social-link" href="not-a-url">Invalid</a>
39
+ `
40
+ const result = fetcher.parseHtml(html, 'url', 'user')
41
+ assert.strictEqual(result.twitter, 'testuser')
42
+ assert.ok(!result.facebook)
43
+ })
44
+
45
+ test('MeleeFetcher: fetchById handles success and error scenarios', async (t) => {
46
+ const fetcher = new MeleeFetcher()
47
+
48
+ await t.test('successful fetch', async () => {
49
+ const mockRequest = t.mock.method(httpClient, 'request', async () => ({
50
+ data: '<html><span style="font-size: xx-large">Fetched Name</span></html>',
51
+ status: 200
52
+ }))
53
+ const result = await fetcher.fetchById('user')
54
+ assert.strictEqual(result.name, 'Fetched Name')
55
+ mockRequest.mock.restore()
56
+ })
57
+
58
+ await t.test('network error', async () => {
59
+ const mockRequest = t.mock.method(httpClient, 'request', () => { throw new Error('Network Error') })
60
+ await withMutedConsole(async () => {
61
+ const result = await fetcher.fetchById('user')
62
+ assert.strictEqual(result, null)
63
+ })
64
+ mockRequest.mock.restore()
65
+ })
29
66
  })
@@ -1,31 +1,70 @@
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 MtgEloFetcher = require('../src/fetchers/mtgElo')
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('MtgEloFetcher: parseHtml extracts name and details including computed Win Rate', () => {
7
+ test('MtgEloFetcher: parseHtml extracts info from fixture', () => {
13
8
  const html = readFixture('mtgElo.html')
14
- const id = 'bjoern-kimminich'
15
- const url = `https://mtgeloproject.net/profile/${id}`
9
+ const url = 'https://mtgeloproject.net/profile/7de50700'
10
+ const fetcher = new MtgEloFetcher()
11
+
12
+ const result = fetcher.parseHtml(html, url, '7de50700')
13
+
14
+ assert.strictEqual(result.source, 'MTG Elo Project')
15
+ assert.strictEqual(result.name, 'Bjoern Kimminich')
16
+ assert.match(result.current_rating, /^\d+$/)
17
+ assert.match(result.record, /^\d+-\d+-\d+$/)
18
+ assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
19
+ })
20
+
21
+ test('MtgEloFetcher: parseHtml handles missing astro-island and uses fallback selectors', () => {
16
22
  const fetcher = new MtgEloFetcher()
23
+ const html = `
24
+ <html>
25
+ <body>
26
+ <div class="text-[22pt]">Kimminich, Björn</div>
27
+ <div class="text-[18pt]">Current rating: <span class="font-bold">1200</span></div>
28
+ <div class="text-[18pt]">Record: 10-5</div>
29
+ </body>
30
+ </html>
31
+ `
32
+ const result = fetcher.parseHtml(html, 'url', 'id')
33
+ assert.strictEqual(result.name, 'Björn Kimminich')
34
+ assert.strictEqual(result.current_rating, '1200')
35
+ assert.strictEqual(result.record, '10-5')
36
+ assert.strictEqual(result['win rate'], '66.67%')
37
+ })
38
+
39
+ test('MtgEloFetcher: parseHtml handles malformed astro-island JSON', async () => {
40
+ const fetcher = new MtgEloFetcher()
41
+ const html = '<astro-island component-url="Profile" props="invalid-json"></astro-island>'
42
+
43
+ await withMutedConsole(() => {
44
+ const result = fetcher.parseHtml(html, 'url', 'id')
45
+ assert.strictEqual(result, null)
46
+ })
47
+ })
48
+
49
+ test('MtgEloFetcher: fetchById handles success and error scenarios', async (t) => {
50
+ const fetcher = new MtgEloFetcher()
51
+
52
+ await t.test('successful fetch', async () => {
53
+ const mockRequest = t.mock.method(httpClient, 'request', async () => ({
54
+ data: '<html><div class="text-[22pt]">Test User</div></html>',
55
+ status: 200
56
+ }))
57
+ const result = await fetcher.fetchById('123')
58
+ assert.strictEqual(result.name, 'Test User')
59
+ mockRequest.mock.restore()
60
+ })
17
61
 
18
- const result = fetcher.parseHtml(html, url, id)
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)
25
- if (result.record) {
26
- assert.match(result.record, /^\d+-\d+(-\d+)?$/)
27
- }
28
- if (result['win rate']) {
29
- assert.match(result['win rate'], /^\d+(\.\d+)?%$/)
30
- }
62
+ await t.test('network error', async () => {
63
+ const mockRequest = t.mock.method(httpClient, 'request', () => { throw new Error('Network Error') })
64
+ await withMutedConsole(async () => {
65
+ const result = await fetcher.fetchById('123')
66
+ assert.strictEqual(result, null)
67
+ })
68
+ mockRequest.mock.restore()
69
+ })
31
70
  })