Make XP progress formatting hydration-safe
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileNumber } from '../../lib/profileFormatters'
|
||||||
|
|
||||||
function formatXp(value) {
|
function formatXp(value) {
|
||||||
return new Intl.NumberFormat().format(Number(value || 0))
|
return formatProfileNumber(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampPercent(value) {
|
function clampPercent(value) {
|
||||||
|
|||||||
@@ -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(<XPProgressBar {...fixture} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('3,024 XP')).toBeTruthy()
|
||||||
|
expect(screen.getByText('2,000 / 5,000 XP')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps XP zero formatting stable', () => {
|
||||||
|
render(<XPProgressBar xp={0} currentLevelXp={0} nextLevelXp={100} progressPercent={0} />)
|
||||||
|
|
||||||
|
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(<XPProgressBar {...fixture} />)
|
||||||
|
document.body.appendChild(container)
|
||||||
|
const recoverableErrors = []
|
||||||
|
const root = hydrateRoot(container, <XPProgressBar {...fixture} />, {
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user