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())
+ })
+})