diff --git a/src/components/import/ImportDetailsForm.test.tsx b/src/components/import/ImportDetailsForm.test.tsx
index 3447000e..1dd18e8b 100644
--- a/src/components/import/ImportDetailsForm.test.tsx
+++ b/src/components/import/ImportDetailsForm.test.tsx
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DUMMY_SEED_PHRASE, pseudoRandomInteger } from '@/lib/utils'
import { flushActUpdates } from '@/test/flushActUpdates'
+import { withRuntimeLocale } from '@/test/withRuntimeLocale'
import type { BlockHeight } from '@/types/global'
import { ImportDetailsForm } from './ImportDetailsForm'
@@ -64,24 +65,26 @@ describe('ImportDetailsForm', () => {
})
it('warns and does not submit when the blockheight is larger than current blockheight', async () => {
- const onSubmit = vi.fn()
- const currentBlockHeight = pseudoRandomInteger(1, Number.MAX_SAFE_INTEGER - 1)
- render(
- ,
- )
- typeBlockheight(currentBlockHeight + 1)
- fireEvent.submit(document.querySelector('form')!)
- await waitFor(() =>
- expect(
- screen.getByText(
- `import_wallet.import_details.feedback_invalid_blockheight {"min":"0","max":"${currentBlockHeight.toLocaleString()}"}`,
- ),
- ).toBeInTheDocument(),
- )
- expect(onSubmit).not.toHaveBeenCalled()
+ await withRuntimeLocale('en-US', async () => {
+ const onSubmit = vi.fn()
+ const currentBlockHeight = pseudoRandomInteger(1, Number.MAX_SAFE_INTEGER - 1)
+ render(
+ ,
+ )
+ typeBlockheight(currentBlockHeight + 1)
+ fireEvent.submit(document.querySelector('form')!)
+ await waitFor(() =>
+ expect(
+ screen.getByText(
+ `import_wallet.import_details.feedback_invalid_blockheight {"min":"0","max":"${currentBlockHeight.toLocaleString()}"}`,
+ ),
+ ).toBeInTheDocument(),
+ )
+ expect(onSubmit).not.toHaveBeenCalled()
+ })
})
it('submits when all values are valid', async () => {
diff --git a/src/components/settings/RescanChainPage.test.tsx b/src/components/settings/RescanChainPage.test.tsx
index 0958d730..1e4c52df 100644
--- a/src/components/settings/RescanChainPage.test.tsx
+++ b/src/components/settings/RescanChainPage.test.tsx
@@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { type RescanInfo } from '@/context/JamSessionInfoContext'
import { SEGWIT_ACTIVATION_BLOCK, type WalletFileName } from '@/lib/utils'
+import { withRuntimeLocale } from '@/test/withRuntimeLocale'
import type { BlockHeight } from '@/types/global'
import { RescanChainPage } from './RescanChainPage'
@@ -85,18 +86,20 @@ describe('RescanChainPage', () => {
})
it('renders the form', async () => {
- await renderPage()
+ await withRuntimeLocale('en-US', async () => {
+ await renderPage()
- expect(screen.getByText('rescan_chain.title')).toBeInTheDocument()
+ expect(screen.getByText('rescan_chain.title')).toBeInTheDocument()
- expect(screen.getByRole('spinbutton', { name: 'rescan_chain.label_blockheight' })).toBeInTheDocument()
- expect(screen.getByPlaceholderText('rescan_chain.placeholder_blockheight')).toBeInTheDocument()
+ expect(screen.getByRole('spinbutton', { name: 'rescan_chain.label_blockheight' })).toBeInTheDocument()
+ expect(screen.getByPlaceholderText('rescan_chain.placeholder_blockheight')).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Last 144 blocks (~24 hours)' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Last 52,560 blocks (~1 year)' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'From block #481,824' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Last 144 blocks (~24 hours)' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Last 52,560 blocks (~1 year)' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'From block #481,824' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'rescan_chain.text_button_submit' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'rescan_chain.text_button_submit' })).toBeInTheDocument()
+ })
})
it('renders the form and navigates back', async () => {
diff --git a/src/components/ui/jam/Balance.test.tsx b/src/components/ui/jam/Balance.test.tsx
index b5c8ce40..0915d358 100644
--- a/src/components/ui/jam/Balance.test.tsx
+++ b/src/components/ui/jam/Balance.test.tsx
@@ -5,6 +5,7 @@ import user from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { Balance } from '@/components/ui/jam/Balance'
import { JamDisplayContextProvider } from '@/context/JamDisplayContextProvider'
+import { withRuntimeLocale } from '@/test/withRuntimeLocale'
const render = (ui: React.ReactNode, options?: Omit) => {
const providers = ({ children }: { children: React.ReactNode }) => {
@@ -35,11 +36,13 @@ describe('', () => {
})
it('should render balance in SATS', () => {
- render()
- expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
- expect(screen.getByTestId('sats-symbol')).toBeVisible()
- expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
- expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
+ expect(screen.getByTestId('sats-symbol')).toBeVisible()
+ expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
+ })
})
it('should render a string BTC value correctly as BTC', () => {
@@ -49,9 +52,11 @@ describe('', () => {
})
it('should render a string BTC value correctly as SATS', () => {
- render()
- expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`12,303,224,961`)
- expect(screen.getByTestId('sats-symbol')).toBeVisible()
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`12,303,224,961`)
+ expect(screen.getByTestId('sats-symbol')).toBeVisible()
+ })
})
it('should render a zero string BTC value correctly as BTC', () => {
@@ -65,23 +70,31 @@ describe('', () => {
})
it('should render a large string BTC value correctly as BTC', () => {
- render()
- expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`)
+ })
})
it('should render a large string BTC value correctly as SATS', () => {
- render()
- expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`)
+ })
})
it('should render a max string BTC value correctly as BTC', () => {
- render()
- expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`)
+ })
})
it('should render a max string BTC value correctly as SATS', () => {
- render()
- expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
+ })
})
it('should render a string SATS value correctly as SATS', () => {
@@ -105,23 +118,31 @@ describe('', () => {
})
it('should render a large string SATS value correctly as BTC', () => {
- render()
- expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`)
+ })
})
it('should render a large string SATS value correctly as SATS', () => {
- render()
- expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`)
+ })
})
it('should render a max string SATS value correctly as BTC', () => {
- render()
- expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`)
+ })
})
it('should render a max string SATS value correctly as SATS', () => {
- render()
- expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
+ })
})
it('should render frozen balance in BTC', () => {
@@ -132,10 +153,12 @@ describe('', () => {
})
it('should render frozen balance in SATS', () => {
- render()
- expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
- expect(screen.getByTestId('sats-symbol')).toBeVisible()
- expect(screen.getByTestId('frozen-symbol')).toBeVisible()
+ withRuntimeLocale('en-US', () => {
+ render()
+ expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
+ expect(screen.getByTestId('sats-symbol')).toBeVisible()
+ expect(screen.getByTestId('frozen-symbol')).toBeVisible()
+ })
})
it('should render balance without symbol', () => {
diff --git a/src/context/JamDisplayContextProvider.test.tsx b/src/context/JamDisplayContextProvider.test.tsx
index acba726c..e2c2d2a4 100644
--- a/src/context/JamDisplayContextProvider.test.tsx
+++ b/src/context/JamDisplayContextProvider.test.tsx
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it } from 'vitest'
import { jamSettingsStore } from '@/store/jamSettingsStore'
+import { withRuntimeLocale } from '@/test/withRuntimeLocale'
import { useJamDisplayContext } from './JamDisplayContext'
import { JamDisplayContextProvider } from './JamDisplayContextProvider'
@@ -24,16 +25,18 @@ describe('JamDisplayContextProvider', () => {
})
it('should format amounts with the current display settings', () => {
- render(
-
-
- ,
- )
+ withRuntimeLocale('en-US', () => {
+ render(
+
+
+ ,
+ )
- expect(screen.getByTestId('default-amount')).toHaveTextContent('123,456,789')
- expect(screen.getByTestId('btc-amount')).toHaveTextContent('1.23456789')
- expect(screen.getByTestId('hidden-amount')).toHaveTextContent('*****')
- expect(screen.getByTestId('sats-symbol')).toBeInTheDocument()
+ expect(screen.getByTestId('default-amount')).toHaveTextContent('123,456,789')
+ expect(screen.getByTestId('btc-amount')).toHaveTextContent('1.23456789')
+ expect(screen.getByTestId('hidden-amount')).toHaveTextContent('*****')
+ expect(screen.getByTestId('sats-symbol')).toBeInTheDocument()
+ })
})
it('should hide default amounts when privacy mode is enabled', () => {
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index 63e14d6a..f63bcad5 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { JM_WALLET_FILE_EXTENSION } from '@/constants/jm'
+import { withRuntimeLocale } from '@/test/withRuntimeLocale'
import {
cn,
debounce,
@@ -37,24 +38,6 @@ import {
} from './utils'
import type { WalletFileName } from './utils'
-const withRuntimeLocale = (locale: string, callback: () => void) => {
- /* eslint-disable unicorn/no-this-outside-of-class -- mocking Number.prototype.toLocaleString requires `this` */
- const numberToLocaleStringMock = vi.spyOn(Number.prototype, 'toLocaleString').mockImplementation(function (
- this: number,
- locales,
- options,
- ) {
- return Intl.NumberFormat(locales ?? locale, options).format(this)
- })
- /* eslint-enable unicorn/no-this-outside-of-class */
-
- try {
- callback()
- } finally {
- numberToLocaleStringMock.mockRestore()
- }
-}
-
describe('cn', () => {
it('should merge class names correctly', () => {
expect(cn('text-red-500', 'bg-blue-500')).toBe('text-red-500 bg-blue-500')
@@ -502,8 +485,10 @@ describe('formatBtc', () => {
})
it('should handle large BTC values', () => {
- expect(formatBtc(21000000)).toBe('21,000,000.00000000')
- expect(formatBtc(100.99999999)).toBe('100.99999999')
+ withRuntimeLocale('en-US', () => {
+ expect(formatBtc(21000000)).toBe('21,000,000.00000000')
+ expect(formatBtc(100.99999999)).toBe('100.99999999')
+ })
})
it('should handle very small BTC values', () => {
@@ -555,9 +540,11 @@ describe('getBtcParts', () => {
describe('formatSats', () => {
it('should format satoshi values with locale-specific thousands separators', () => {
- expect(formatSats(1000)).toBe('1,000')
- expect(formatSats(1000000)).toBe('1,000,000')
- expect(formatSats(100000000)).toBe('100,000,000') // 1 BTC in sats
+ withRuntimeLocale('en-US', () => {
+ expect(formatSats(1000)).toBe('1,000')
+ expect(formatSats(1000000)).toBe('1,000,000')
+ expect(formatSats(100000000)).toBe('100,000,000') // 1 BTC in sats
+ })
})
it('should handle small satoshi values', () => {
@@ -568,13 +555,17 @@ describe('formatSats', () => {
})
it('should handle large satoshi values', () => {
- expect(formatSats(2100000000000000)).toBe('2,100,000,000,000,000') // 21M BTC in sats
- expect(formatSats(12345678901)).toBe('12,345,678,901')
+ withRuntimeLocale('en-US', () => {
+ expect(formatSats(2100000000000000)).toBe('2,100,000,000,000,000') // 21M BTC in sats
+ expect(formatSats(12345678901)).toBe('12,345,678,901')
+ })
})
it('should handle negative satoshi values', () => {
- expect(formatSats(-1000)).toBe('-1,000')
- expect(formatSats(-1234567)).toBe('-1,234,567')
+ withRuntimeLocale('en-US', () => {
+ expect(formatSats(-1000)).toBe('-1,000')
+ expect(formatSats(-1234567)).toBe('-1,234,567')
+ })
})
it('should follow runtime locale when no explicit locale is provided', () => {
diff --git a/src/test/withRuntimeLocale.ts b/src/test/withRuntimeLocale.ts
new file mode 100644
index 00000000..d8d41354
--- /dev/null
+++ b/src/test/withRuntimeLocale.ts
@@ -0,0 +1,52 @@
+import { vi } from 'vitest'
+
+const OriginalIntlNumberFormat = Intl.NumberFormat
+
+/**
+ * Runs `callback` with `Number.prototype.toLocaleString` and `Intl.NumberFormat`
+ * mocked so that calls without an explicit locale argument format using
+ * `locale` instead of the host machine's default locale. Calls that already
+ * pass an explicit locale are left untouched. This keeps locale-dependent
+ * assertions deterministic across contributors' machines and CI without
+ * pinning a locale in app code.
+ */
+export function withRuntimeLocale(locale: string, callback: () => Promise): Promise
+export function withRuntimeLocale(locale: string, callback: () => void): void
+export function withRuntimeLocale(locale: string, callback: () => void | Promise): void | Promise {
+ /* eslint-disable unicorn/no-this-outside-of-class -- mocking Number.prototype.toLocaleString requires `this` */
+ const numberToLocaleStringMock = vi.spyOn(Number.prototype, 'toLocaleString').mockImplementation(function (
+ this: number,
+ locales,
+ options,
+ ) {
+ return Intl.NumberFormat(locales ?? locale, options).format(this)
+ })
+ /* eslint-enable unicorn/no-this-outside-of-class */
+
+ const intlNumberFormatMock = vi.spyOn(Intl, 'NumberFormat').mockImplementation(function (
+ locales?: string | string[],
+ options?: Intl.NumberFormatOptions,
+ ) {
+ return new OriginalIntlNumberFormat(locales ?? locale, options)
+ } as unknown as typeof Intl.NumberFormat)
+
+ const restore = () => {
+ numberToLocaleStringMock.mockRestore()
+ intlNumberFormatMock.mockRestore()
+ }
+
+ let result: void | Promise
+ try {
+ result = callback()
+ } catch (error) {
+ restore()
+ throw error
+ }
+
+ if (result instanceof Promise) {
+ return result.finally(restore)
+ }
+
+ restore()
+ return undefined
+}