Make profile SSR formatting deterministic

This commit is contained in:
2026-08-29 20:24:38 +02:00
parent 872f420190
commit 99d94c9333
5 changed files with 86 additions and 4 deletions
@@ -7,10 +7,10 @@ import FollowButton from '../social/FollowButton'
import FollowersPreview from '../social/FollowersPreview'
import MutualFollowersBadge from '../social/MutualFollowersBadge'
import { shinyFlagUrl } from '../../utils/flagUrl'
import { formatProfileDate, formatProfileNumber } from '../../lib/profileFormatters'
function formatCompactNumber(value) {
const numeric = Number(value ?? 0)
return numeric.toLocaleString('en-US')
return formatProfileNumber(value)
}
export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing, followerCount, recentFollowers = [], followContext = null, heroBgUrl, countryName, leaderboardRank, extraActions = null }) {
@@ -25,7 +25,7 @@ export default function ProfileHero({ user, profile, isOwner, viewerIsFollowing,
const uname = user.username || user.name || 'Unknown'
const displayName = user.name || uname
const joinDate = user.created_at
? new Date(user.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
? formatProfileDate(user.created_at, { month: 'long', year: 'numeric' })
: null
const progressPercent = Math.round(Number(user?.progress_percent ?? 0))
const heroStats = [
@@ -0,0 +1,46 @@
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../artwork/ArtworkGallery', () => ({
default: () => <div data-testid="mock-artwork-gallery" />,
}))
import TabArtworks from './TabArtworks'
describe('TabArtworks SSR hydration formatting', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('hydrates formatted artwork statistics without recoverable mismatches', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false })))
const props = {
artworks: [],
featuredArtworks: [{
id: 1,
name: 'Featured artwork',
views: 9529106,
likes: 1216,
downloads: 12,
}],
username: 'alice',
galleryUrl: '/@alice/artworks',
}
const container = document.createElement('div')
container.innerHTML = renderToString(<TabArtworks {...props} />)
document.body.appendChild(container)
const recoverableErrors = []
hydrateRoot(container, <TabArtworks {...props} />, {
onRecoverableError: (error) => recoverableErrors.push(error),
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(recoverableErrors).toHaveLength(0)
expect(container.textContent).toContain('9,529,106')
expect(container.textContent).toContain('1,216')
})
})
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'
import ArtworkGallery from '../../artwork/ArtworkGallery'
import { formatProfileNumber } from '../../../lib/profileFormatters'
function slugify(value) {
return String(value ?? '')
@@ -9,7 +10,7 @@ function slugify(value) {
}
function formatNumber(value) {
return Number(value ?? 0).toLocaleString()
return formatProfileNumber(value)
}
function sortByPublishedAt(items) {