mirror of
https://github.com/misenhower/splatoon3.ink.git
synced 2026-03-21 17:54:13 -05:00
Add unit tests for app/common/util
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7084b3c5ae
commit
7257c26869
200
app/common/util.test.mjs
Normal file
200
app/common/util.test.mjs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getTopOfCurrentHour,
|
||||
getDateParts,
|
||||
getGearIcon,
|
||||
normalizeSplatnetResourcePath,
|
||||
deriveId,
|
||||
getFestId,
|
||||
getFestTeamId,
|
||||
getXRankSeasonId,
|
||||
jsonpathQuery,
|
||||
jsonpathApply,
|
||||
calculateCacheExpiry,
|
||||
} from './util.mjs';
|
||||
|
||||
describe('getTopOfCurrentHour', () => {
|
||||
it('zeros out minutes, seconds, and milliseconds', () => {
|
||||
const date = new Date('2024-01-15T14:35:22.123Z');
|
||||
const result = getTopOfCurrentHour(date);
|
||||
expect(result.getUTCMinutes()).toBe(0);
|
||||
expect(result.getUTCSeconds()).toBe(0);
|
||||
expect(result.getUTCMilliseconds()).toBe(0);
|
||||
expect(result.getUTCHours()).toBe(14);
|
||||
});
|
||||
|
||||
it('returns the mutated date object', () => {
|
||||
const date = new Date('2024-01-15T14:35:22.123Z');
|
||||
const result = getTopOfCurrentHour(date);
|
||||
expect(result).toBe(date);
|
||||
});
|
||||
|
||||
it('uses current date when no argument', () => {
|
||||
const before = new Date();
|
||||
const result = getTopOfCurrentHour();
|
||||
expect(result.getUTCMinutes()).toBe(0);
|
||||
expect(result.getUTCSeconds()).toBe(0);
|
||||
expect(result.getUTCMilliseconds()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDateParts', () => {
|
||||
it('returns correct parts with leading zeros', () => {
|
||||
const date = new Date('2024-03-05T08:02:09.000Z');
|
||||
const parts = getDateParts(date);
|
||||
expect(parts).toEqual({
|
||||
year: 2024,
|
||||
month: '03',
|
||||
day: '05',
|
||||
hour: '08',
|
||||
minute: '02',
|
||||
second: '09',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles double-digit values', () => {
|
||||
const date = new Date('2024-12-25T23:59:45.000Z');
|
||||
const parts = getDateParts(date);
|
||||
expect(parts).toEqual({
|
||||
year: 2024,
|
||||
month: '12',
|
||||
day: '25',
|
||||
hour: '23',
|
||||
minute: '59',
|
||||
second: '45',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGearIcon', () => {
|
||||
it('returns cap emoji for HeadGear', () => {
|
||||
expect(getGearIcon({ gear: { __typename: 'HeadGear' } })).toBe('🧢');
|
||||
});
|
||||
|
||||
it('returns shirt emoji for ClothingGear', () => {
|
||||
expect(getGearIcon({ gear: { __typename: 'ClothingGear' } })).toBe('👕');
|
||||
});
|
||||
|
||||
it('returns shoe emoji for ShoesGear', () => {
|
||||
expect(getGearIcon({ gear: { __typename: 'ShoesGear' } })).toBe('👟');
|
||||
});
|
||||
|
||||
it('returns null for unknown type', () => {
|
||||
expect(getGearIcon({ gear: { __typename: 'WeaponGear' } })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeSplatnetResourcePath', () => {
|
||||
it('strips /resources/prod prefix and leading slash', () => {
|
||||
const url = 'https://api.lp1.av5ja.srv.nintendo.net/resources/prod/v2/weapon_illust/12345.png';
|
||||
expect(normalizeSplatnetResourcePath(url)).toBe('v2/weapon_illust/12345.png');
|
||||
});
|
||||
|
||||
it('handles URLs without the prefix', () => {
|
||||
const url = 'https://example.com/images/test.png';
|
||||
expect(normalizeSplatnetResourcePath(url)).toBe('images/test.png');
|
||||
});
|
||||
|
||||
it('strips query strings', () => {
|
||||
const url = 'https://api.lp1.av5ja.srv.nintendo.net/resources/prod/v2/image.png?ver=123';
|
||||
expect(normalizeSplatnetResourcePath(url)).toBe('v2/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveId', () => {
|
||||
it('produces consistent hash for the same URL', () => {
|
||||
const node = { image: { url: 'https://example.com/resources/prod/v2/test.png' }, name: 'Test' };
|
||||
const result1 = deriveId(node);
|
||||
const result2 = deriveId(node);
|
||||
expect(result1.__splatoon3ink_id).toBe(result2.__splatoon3ink_id);
|
||||
});
|
||||
|
||||
it('produces different hashes for different URLs', () => {
|
||||
const node1 = { image: { url: 'https://example.com/resources/prod/v2/a.png' } };
|
||||
const node2 = { image: { url: 'https://example.com/resources/prod/v2/b.png' } };
|
||||
expect(deriveId(node1).__splatoon3ink_id).not.toBe(deriveId(node2).__splatoon3ink_id);
|
||||
});
|
||||
|
||||
it('preserves original node properties', () => {
|
||||
const node = { image: { url: 'https://example.com/test.png' }, name: 'Gear', rarity: 2 };
|
||||
const result = deriveId(node);
|
||||
expect(result.name).toBe('Gear');
|
||||
expect(result.rarity).toBe(2);
|
||||
expect(result.image).toBe(node.image);
|
||||
expect(result.__splatoon3ink_id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFestId', () => {
|
||||
it('extracts ID from base64-encoded Fest string', () => {
|
||||
const encoded = Buffer.from('Fest-US:12345').toString('base64');
|
||||
expect(getFestId(encoded)).toBe('12345');
|
||||
});
|
||||
|
||||
it('handles multi-region prefix', () => {
|
||||
const encoded = Buffer.from('Fest-UJEA:99').toString('base64');
|
||||
expect(getFestId(encoded)).toBe('99');
|
||||
});
|
||||
|
||||
it('returns original value on non-match', () => {
|
||||
expect(getFestId('not-base64-fest')).toBe('not-base64-fest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFestTeamId', () => {
|
||||
it('extracts FESTID:TEAMID from base64-encoded string', () => {
|
||||
const encoded = Buffer.from('FestTeam-US:100:3').toString('base64');
|
||||
expect(getFestTeamId(encoded)).toBe('100:3');
|
||||
});
|
||||
|
||||
it('returns original value on non-match', () => {
|
||||
expect(getFestTeamId('invalid')).toBe('invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getXRankSeasonId', () => {
|
||||
it('extracts REGION-ID from base64-encoded string', () => {
|
||||
const encoded = Buffer.from('XRankingSeason-US:5').toString('base64');
|
||||
expect(getXRankSeasonId(encoded)).toBe('US-5');
|
||||
});
|
||||
|
||||
it('returns original value on non-match', () => {
|
||||
expect(getXRankSeasonId('invalid')).toBe('invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonpathQuery', () => {
|
||||
it('returns matching values from nested objects', () => {
|
||||
const data = { store: { books: [{ title: 'A' }, { title: 'B' }] } };
|
||||
const result = jsonpathQuery(data, '$.store.books[*].title');
|
||||
expect(result).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('returns empty array for no match', () => {
|
||||
const data = { a: 1 };
|
||||
const result = jsonpathQuery(data, '$.b');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonpathApply', () => {
|
||||
it('transforms matching values in-place', () => {
|
||||
const data = { items: [{ price: 10 }, { price: 20 }] };
|
||||
jsonpathApply(data, '$.items[*].price', v => v * 2);
|
||||
expect(data.items[0].price).toBe(20);
|
||||
expect(data.items[1].price).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateCacheExpiry', () => {
|
||||
it('returns timestamp = now + expiresIn - 5 minutes', () => {
|
||||
const before = Date.now();
|
||||
const expiresIn = 3600; // 1 hour in seconds
|
||||
const result = calculateCacheExpiry(expiresIn);
|
||||
const after = Date.now();
|
||||
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
expect(result).toBeGreaterThanOrEqual(before + expiresIn * 1000 - fiveMinutes);
|
||||
expect(result).toBeLessThanOrEqual(after + expiresIn * 1000 - fiveMinutes);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user