splatoon3.ink/app/common/util.test.mjs
Matt Isenhower 7257c26869 Add unit tests for app/common/util
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 16:45:20 -08:00

201 lines
6.3 KiB
JavaScript

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);
});
});