From 54c11635e03c9d704562d225f76d34be02bd11da Mon Sep 17 00:00:00 2001 From: maxswa Date: Sun, 19 Mar 2023 23:14:10 -0400 Subject: [PATCH] Add `GetStatsOptions` for `getStats` --- __tests__/hiscores.test.ts | 47 +++++++++++++++++++++ src/hiscores.ts | 84 +++++++++++++++++++++----------------- src/types.ts | 13 ++++++ 3 files changed, 106 insertions(+), 38 deletions(-) diff --git a/__tests__/hiscores.test.ts b/__tests__/hiscores.test.ts index e70bc63..a482da7 100644 --- a/__tests__/hiscores.test.ts +++ b/__tests__/hiscores.test.ts @@ -505,3 +505,50 @@ test('Get stats by gamemode', async () => { expect.assertions(2); }); + +describe('Get stats options', () => { + const rsn = 'player'; + let axiosMock: jest.Mock; + beforeEach(() => { + axios.get = jest.fn( + (url) => + new Promise((resolve) => + resolve( + url === getPlayerTableURL('main', rsn) + ? { data: lynxTitanNamePage } + : { status: 200, data: lynxTitanStats } + ) + ) + ); + axiosMock = axios.get as any; + axiosMock.mockClear(); + }); + it('fetches all gamemodes and formatted RSN when no options provided', async () => { + await getStats(rsn); + expect(axiosMock.mock.calls.map((val) => val[0])).toEqual([ + getStatsURL('main', rsn), + getPlayerTableURL('main', rsn), + getStatsURL('ironman', rsn), + getStatsURL('hardcore', rsn), + getStatsURL('ultimate', rsn) + ]); + }); + it('skips fetching formatted RSN when option is provided', async () => { + await getStats(rsn, { shouldGetFormattedRsn: false }); + expect( + axiosMock.mock.calls.some( + (val) => val[0] === getPlayerTableURL('main', rsn) + ) + ).toBeFalsy(); + }); + it('skips fetching game mode when option is provided', async () => { + await getStats(rsn, { + otherGamemodes: ['ironman', 'ultimate'] + }); + expect( + axiosMock.mock.calls.some( + (val) => val[0] === getStatsURL('hardcore', rsn) + ) + ).toBeFalsy(); + }); +}); diff --git a/src/hiscores.ts b/src/hiscores.ts index 6edadf2..d7e012d 100644 --- a/src/hiscores.ts +++ b/src/hiscores.ts @@ -1,4 +1,5 @@ -import { JSDOM } from 'jsdom'; +import { AxiosResponse } from 'axios'; +import { BinaryData, JSDOM } from 'jsdom'; import { Player, Activity, @@ -12,7 +13,8 @@ import { PlayerSkillRow, ActivityName, PlayerActivityRow, - Bosses + Bosses, + GetStatsOptions } from './types'; import { getStatsURL, @@ -28,7 +30,8 @@ import { getActivityPageURL, httpGet, BOSSES, - INVALID_FORMAT_ERROR + INVALID_FORMAT_ERROR, + validateRSN } from './utils'; /** @@ -38,17 +41,13 @@ import { * @returns Formatted version of the rsn. */ export async function getRSNFormat(rsn: string): Promise { - if (typeof rsn !== 'string') { - throw Error('RSN must be a string'); - } else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) { - throw Error('RSN contains invalid character'); - } else if (rsn.length > 12 || rsn.length < 1) { - throw Error('RSN must be between 1 and 12 characters'); - } + validateRSN(rsn); const url = getPlayerTableURL('main', rsn); try { - const response = await httpGet(url); + const response = await httpGet( + url + ); const dom = new JSDOM(response.data); const anchor = dom.window.document.querySelector( '.personal-hiscores__row.personal-hiscores__row--type-highlight a' @@ -163,25 +162,36 @@ export function parseStats(csv: string): Stats { * @param rsn Username of the player. * @returns Player object. */ -export async function getStats(rsn: string): Promise { - if (typeof rsn !== 'string') { - throw Error('RSN must be a string'); - } else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) { - throw Error('RSN contains invalid character'); - } else if (rsn.length > 12 || rsn.length < 1) { - throw Error('RSN must be between 1 and 12 characters'); - } +export async function getStats( + rsn: string, + options?: GetStatsOptions +): Promise { + validateRSN(rsn); + const otherGamemodes = options?.otherGamemodes ?? [ + 'ironman', + 'hardcore', + 'ultimate' + ]; + const shouldGetFormattedRsn = options?.shouldGetFormattedRsn ?? true; - const mainRes = await httpGet(getStatsURL('main', rsn)); + const mainRes = await httpGet(getStatsURL('main', rsn)); if (mainRes.status === 200) { - const otherResponses = await Promise.all([ - httpGet(getStatsURL('ironman', rsn)).catch((err) => err), - httpGet(getStatsURL('hardcore', rsn)).catch((err) => err), - httpGet(getStatsURL('ultimate', rsn)).catch((err) => err), - getRSNFormat(rsn).catch(() => undefined) - ]); - - const [ironRes, hcRes, ultRes, formattedName] = otherResponses; + const emptyResponse: AxiosResponse = { + status: 404, + data: '', + statusText: '', + headers: {}, + config: {} + }; + const getModeStats = async ( + mode: Extract + ): Promise> => + otherGamemodes.includes(mode) + ? httpGet(getStatsURL(mode, rsn)).catch((err) => err) + : emptyResponse; + const formattedName = shouldGetFormattedRsn + ? await getRSNFormat(rsn).catch(() => undefined) + : undefined; const player: Player = { name: formattedName ?? rsn, @@ -192,8 +202,11 @@ export async function getStats(rsn: string): Promise { }; player.main = parseStats(mainRes.data); + const ironRes = await getModeStats('ironman'); if (ironRes.status === 200) { player.ironman = parseStats(ironRes.data); + const hcRes = await getModeStats('hardcore'); + const ultRes = await getModeStats('ultimate'); if (hcRes.status === 200) { player.mode = 'hardcore'; player.hardcore = parseStats(hcRes.data); @@ -251,16 +264,11 @@ export async function getStatsByGamemode( rsn: string, mode: Gamemode = 'main' ): Promise { - if (typeof rsn !== 'string') { - throw Error('RSN must be a string'); - } else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) { - throw Error('RSN contains invalid character'); - } else if (rsn.length > 12 || rsn.length < 1) { - throw Error('RSN must be between 1 and 12 characters'); - } else if (!GAMEMODES.includes(mode)) { + validateRSN(rsn); + if (!GAMEMODES.includes(mode)) { throw Error('Invalid game mode'); } - const response = await httpGet(getStatsURL(mode, rsn)); + const response = await httpGet(getStatsURL(mode, rsn)); if (response.status !== 200) { throw Error('Player not found'); } @@ -283,7 +291,7 @@ export async function getSkillPage( } const url = getSkillPageURL(mode, skill, page); - const response = await httpGet(url); + const response = await httpGet(url); const dom = new JSDOM(response.data); const playersHTML = dom.window.document.querySelectorAll( '.personal-hiscores__row' @@ -331,7 +339,7 @@ export async function getActivityPage( } const url = getActivityPageURL(mode, activity, page); - const response = await httpGet(url); + const response = await httpGet(url); const dom = new JSDOM(response.data); const playersHTML = dom.window.document.querySelectorAll( '.personal-hiscores__row' diff --git a/src/types.ts b/src/types.ts index be007ec..cd41995 100644 --- a/src/types.ts +++ b/src/types.ts @@ -166,3 +166,16 @@ export interface PlayerActivityRow extends Activity { name: string; dead: boolean; } + +export interface GetStatsOptions { + /** + * Other game modes to fetch ranks for. + * @defaultvalue `['ironman', 'hardcore', 'ultimate']` + */ + otherGamemodes?: Extract[]; + /** + * If true, the formatted RSN will be fetched. Otherwise it will return the provided, unformatted RSN. + * @defaultvalue `true` + */ + shouldGetFormattedRsn?: boolean; +}