diff --git a/resources/js/components/xp/XPProgressBar.jsx b/resources/js/components/xp/XPProgressBar.jsx index f4cb975a..28a29c0f 100644 --- a/resources/js/components/xp/XPProgressBar.jsx +++ b/resources/js/components/xp/XPProgressBar.jsx @@ -1,7 +1,8 @@ import React from 'react' +import { formatProfileNumber } from '../../lib/profileFormatters' function formatXp(value) { - return new Intl.NumberFormat().format(Number(value || 0)) + return formatProfileNumber(value) } function clampPercent(value) { diff --git a/resources/js/components/xp/XPProgressBar.test.jsx b/resources/js/components/xp/XPProgressBar.test.jsx new file mode 100644 index 00000000..699d7cc5 --- /dev/null +++ b/resources/js/components/xp/XPProgressBar.test.jsx @@ -0,0 +1,65 @@ +import React from 'react' +import { hydrateRoot } from 'react-dom/client' +import { act, render, screen } from '@testing-library/react' +import { renderToString } from 'react-dom/server' +import { afterEach, describe, expect, it } from 'vitest' +import XPProgressBar from './XPProgressBar' +import { formatProfileNumber } from '../../lib/profileFormatters' + +const fixture = { + xp: 3024, + currentLevelXp: 2000, + nextLevelXp: 5000, + progressPercent: 34, +} + +describe('XPProgressBar locale-stable formatting', () => { + afterEach(() => { + document.body.innerHTML = '' + }) + + it.each([ + [0, '0'], + [1, '1'], + [999, '999'], + [1000, '1,000'], + [3024, '3,024'], + [1000000, '1,000,000'], + [9529106, '9,529,106'], + ])('formats %s as %s', (value, expected) => { + expect(formatProfileNumber(value)).toBe(expected) + }) + + it('renders the non-zero production-shaped XP value', () => { + render() + + expect(screen.getByText('3,024 XP')).toBeTruthy() + expect(screen.getByText('2,000 / 5,000 XP')).toBeTruthy() + }) + + it('keeps XP zero formatting stable', () => { + render() + + expect(screen.getByText('0 XP')).toBeTruthy() + expect(screen.getByText('0 / 100 XP')).toBeTruthy() + }) + + it('hydrates the same SSR markup without recoverable errors', async () => { + const container = document.createElement('div') + container.innerHTML = renderToString() + document.body.appendChild(container) + const recoverableErrors = [] + const root = hydrateRoot(container, , { + onRecoverableError: (error) => recoverableErrors.push(error), + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + expect(recoverableErrors).toHaveLength(0) + expect(container.textContent).toContain('3,024 XP') + expect(container.textContent).toContain('2,000 / 5,000 XP') + await act(async () => root.unmount()) + }) +})