Files
OpenStock/__tests__/utils.test.ts
Bortlesboat 02a3a68267 fix: add international exchange mappings for TradingView widgets
Expand formatSymbolForTradingView to map 40+ Finnhub exchange suffixes
to their TradingView prefix equivalents. Previously only .SS, .SZ, and
.HK were handled, causing international symbols like 2330.TW to fail
on the stock details page.

Add comprehensive test suite for utility functions including
formatSymbolForTradingView, formatMarketCapValue, formatChangePercent,
getChangeColorClass, and calculateNewsDistribution.

Closes #24
2026-03-28 15:28:12 -04:00

205 lines
6.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
formatSymbolForTradingView,
formatMarketCapValue,
formatChangePercent,
getChangeColorClass,
calculateNewsDistribution,
} from '@/lib/utils';
describe('formatSymbolForTradingView', () => {
it('returns empty string for empty input', () => {
expect(formatSymbolForTradingView('')).toBe('');
});
it('returns uppercase US symbol as-is', () => {
expect(formatSymbolForTradingView('AAPL')).toBe('AAPL');
expect(formatSymbolForTradingView('aapl')).toBe('AAPL');
});
// Asia-Pacific exchanges
it('maps Taiwan (.TW) to TWSE prefix', () => {
expect(formatSymbolForTradingView('2330.TW')).toBe('TWSE:2330');
});
it('maps Taiwan OTC (.TWO) to TPEX prefix', () => {
expect(formatSymbolForTradingView('6488.TWO')).toBe('TPEX:6488');
});
it('maps Tokyo (.T) to TSE prefix', () => {
expect(formatSymbolForTradingView('7203.T')).toBe('TSE:7203');
});
it('maps Hong Kong (.HK) to HKEX prefix', () => {
expect(formatSymbolForTradingView('0700.HK')).toBe('HKEX:0700');
});
it('maps Shanghai (.SS) to SSE prefix', () => {
expect(formatSymbolForTradingView('600519.SS')).toBe('SSE:600519');
});
it('maps Shenzhen (.SZ) to SZSE prefix', () => {
expect(formatSymbolForTradingView('000858.SZ')).toBe('SZSE:000858');
});
it('maps Korea (.KS) to KRX prefix', () => {
expect(formatSymbolForTradingView('005930.KS')).toBe('KRX:005930');
});
it('maps India NSE (.NS) to NSE prefix', () => {
expect(formatSymbolForTradingView('RELIANCE.NS')).toBe('NSE:RELIANCE');
});
it('maps India BSE (.BO) to BSE prefix', () => {
expect(formatSymbolForTradingView('RELIANCE.BO')).toBe('BSE:RELIANCE');
});
it('maps Australia (.AX) to ASX prefix', () => {
expect(formatSymbolForTradingView('BHP.AX')).toBe('ASX:BHP');
});
it('maps Singapore (.SI) to SGX prefix', () => {
expect(formatSymbolForTradingView('D05.SI')).toBe('SGX:D05');
});
// European exchanges
it('maps London (.L) to LSE prefix', () => {
expect(formatSymbolForTradingView('SHEL.L')).toBe('LSE:SHEL');
});
it('maps Germany/Xetra (.DE) to XETR prefix', () => {
expect(formatSymbolForTradingView('SAP.DE')).toBe('XETR:SAP');
});
it('maps Paris (.PA) to EURONEXT prefix', () => {
expect(formatSymbolForTradingView('MC.PA')).toBe('EURONEXT:MC');
});
it('maps Amsterdam (.AS) to EURONEXT prefix', () => {
expect(formatSymbolForTradingView('ASML.AS')).toBe('EURONEXT:ASML');
});
it('maps Milan (.MI) to MIL prefix', () => {
expect(formatSymbolForTradingView('ENI.MI')).toBe('MIL:ENI');
});
it('maps Madrid (.MC) to BME prefix', () => {
expect(formatSymbolForTradingView('SAN.MC')).toBe('BME:SAN');
});
it('maps Swiss (.SW) to SIX prefix', () => {
expect(formatSymbolForTradingView('NESN.SW')).toBe('SIX:NESN');
});
it('maps Stockholm (.ST) to OMXSTO prefix', () => {
expect(formatSymbolForTradingView('ERIC-B.ST')).toBe('OMXSTO:ERIC-B');
});
// Americas
it('maps Toronto (.TO) to TSX prefix', () => {
expect(formatSymbolForTradingView('RY.TO')).toBe('TSX:RY');
});
it('maps Brazil (.SA) to BMFBOVESPA prefix', () => {
expect(formatSymbolForTradingView('VALE3.SA')).toBe('BMFBOVESPA:VALE3');
});
// Middle East & Africa
it('maps Tel Aviv (.TA) to TASE prefix', () => {
expect(formatSymbolForTradingView('TEVA.TA')).toBe('TASE:TEVA');
});
it('maps Johannesburg (.JO) to JSE prefix', () => {
expect(formatSymbolForTradingView('NPN.JO')).toBe('JSE:NPN');
});
// Case insensitivity
it('handles lowercase input', () => {
expect(formatSymbolForTradingView('2330.tw')).toBe('TWSE:2330');
expect(formatSymbolForTradingView('shel.l')).toBe('LSE:SHEL');
});
// Longer suffix matched before shorter (.TWO before .TW)
it('prioritizes longer suffixes over shorter ones', () => {
// .TWO should match before .TW
expect(formatSymbolForTradingView('6488.TWO')).toBe('TPEX:6488');
// But .TW still works for regular TW symbols
expect(formatSymbolForTradingView('2330.TW')).toBe('TWSE:2330');
});
});
describe('formatMarketCapValue', () => {
it('returns N/A for invalid inputs', () => {
expect(formatMarketCapValue(0)).toBe('N/A');
expect(formatMarketCapValue(-100)).toBe('N/A');
expect(formatMarketCapValue(NaN)).toBe('N/A');
expect(formatMarketCapValue(Infinity)).toBe('N/A');
});
it('formats trillions', () => {
expect(formatMarketCapValue(3.1e12)).toBe('$3.10T');
});
it('formats billions', () => {
expect(formatMarketCapValue(900e9)).toBe('$900.00B');
});
it('formats millions', () => {
expect(formatMarketCapValue(25e6)).toBe('$25.00M');
});
it('formats sub-million values', () => {
expect(formatMarketCapValue(999999.99)).toBe('$999999.99');
});
});
describe('formatChangePercent', () => {
it('returns empty string for undefined/null', () => {
expect(formatChangePercent(undefined)).toBe('');
expect(formatChangePercent(null as any)).toBe('');
});
it('formats positive change with + sign', () => {
expect(formatChangePercent(2.5)).toBe('+2.50%');
});
it('formats negative change', () => {
expect(formatChangePercent(-1.23)).toBe('-1.23%');
});
it('formats zero change', () => {
expect(formatChangePercent(0)).toBe('0.00%');
});
});
describe('getChangeColorClass', () => {
it('returns gray for zero or undefined', () => {
expect(getChangeColorClass(0)).toBe('text-gray-400');
expect(getChangeColorClass(undefined)).toBe('text-gray-400');
});
it('returns green for positive', () => {
expect(getChangeColorClass(1.5)).toBe('text-green-500');
});
it('returns red for negative', () => {
expect(getChangeColorClass(-0.5)).toBe('text-red-500');
});
});
describe('calculateNewsDistribution', () => {
it('returns 3 items per symbol for 1-2 symbols', () => {
expect(calculateNewsDistribution(1).itemsPerSymbol).toBe(3);
expect(calculateNewsDistribution(2).itemsPerSymbol).toBe(3);
});
it('returns 2 items per symbol for exactly 3', () => {
expect(calculateNewsDistribution(3).itemsPerSymbol).toBe(2);
});
it('returns 1 item per symbol for 4+', () => {
expect(calculateNewsDistribution(5).itemsPerSymbol).toBe(1);
expect(calculateNewsDistribution(10).itemsPerSymbol).toBe(1);
});
});