Skip to content

Commit 98d4fc9

Browse files
committed
Add tests for cent-react components and hooks
1 parent b15f285 commit 98d4fc9

7 files changed

Lines changed: 835 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { render, screen } from '@testing-library/react'
2+
import { Money } from '@thesis-co/cent'
3+
import { MoneyDiff } from '../../src/components/MoneyDiff'
4+
5+
describe('MoneyDiff', () => {
6+
describe('basic rendering', () => {
7+
it('renders positive difference with plus sign', () => {
8+
render(<MoneyDiff value={Money('$120')} compareTo={Money('$100')} />)
9+
expect(screen.getByText('+$20.00')).toBeInTheDocument()
10+
})
11+
12+
it('renders negative difference with minus sign', () => {
13+
render(<MoneyDiff value={Money('$80')} compareTo={Money('$100')} />)
14+
expect(screen.getByText('-$20.00')).toBeInTheDocument()
15+
})
16+
17+
it('renders zero difference without sign', () => {
18+
render(<MoneyDiff value={Money('$100')} compareTo={Money('$100')} />)
19+
expect(screen.getByText('$0.00')).toBeInTheDocument()
20+
})
21+
22+
it('accepts string values', () => {
23+
render(<MoneyDiff value="$150" compareTo="$100" />)
24+
expect(screen.getByText('+$50.00')).toBeInTheDocument()
25+
})
26+
})
27+
28+
describe('percentage display', () => {
29+
it('shows percentage when showPercentage is true', () => {
30+
render(<MoneyDiff value={Money('$120')} compareTo={Money('$100')} showPercentage />)
31+
expect(screen.getByText('+$20.00 (+20.00%)')).toBeInTheDocument()
32+
})
33+
34+
it('shows negative percentage for decrease', () => {
35+
render(<MoneyDiff value={Money('$80')} compareTo={Money('$100')} showPercentage />)
36+
expect(screen.getByText('-$20.00 (-20.00%)')).toBeInTheDocument()
37+
})
38+
39+
it('respects percentageDecimals option', () => {
40+
render(
41+
<MoneyDiff
42+
value={Money('$133.33')}
43+
compareTo={Money('$100')}
44+
showPercentage
45+
percentageDecimals={1}
46+
/>
47+
)
48+
expect(screen.getByText(/33\.3%/)).toBeInTheDocument()
49+
})
50+
})
51+
52+
describe('data-direction attribute', () => {
53+
it('sets data-direction to increase for positive diff', () => {
54+
render(<MoneyDiff value={Money('$120')} compareTo={Money('$100')} data-testid="diff" />)
55+
expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'increase')
56+
})
57+
58+
it('sets data-direction to decrease for negative diff', () => {
59+
render(<MoneyDiff value={Money('$80')} compareTo={Money('$100')} data-testid="diff" />)
60+
expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'decrease')
61+
})
62+
63+
it('sets data-direction to unchanged for zero diff', () => {
64+
render(<MoneyDiff value={Money('$100')} compareTo={Money('$100')} data-testid="diff" />)
65+
expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'unchanged')
66+
})
67+
})
68+
69+
describe('custom rendering', () => {
70+
it('passes render props to children function', () => {
71+
render(
72+
<MoneyDiff value={Money('$150')} compareTo={Money('$100')}>
73+
{({ direction, formatted, percentageChange }) => (
74+
<span data-testid="custom">
75+
{direction}: {formatted.difference} ({percentageChange}%)
76+
</span>
77+
)}
78+
</MoneyDiff>
79+
)
80+
81+
expect(screen.getByTestId('custom')).toHaveTextContent('increase: +$50.00 (50.00%)')
82+
})
83+
84+
it('provides Money instances in render props', () => {
85+
let receivedCurrent: Money | null = null
86+
let receivedDiff: Money | null = null
87+
88+
render(
89+
<MoneyDiff value={Money('$120')} compareTo={Money('$100')}>
90+
{({ current, difference }) => {
91+
receivedCurrent = current
92+
receivedDiff = difference
93+
return <span>test</span>
94+
}}
95+
</MoneyDiff>
96+
)
97+
98+
expect(receivedCurrent?.toString()).toBe('$120.00')
99+
expect(receivedDiff?.toString()).toBe('$20.00')
100+
})
101+
})
102+
103+
describe('styling', () => {
104+
it('applies className', () => {
105+
render(
106+
<MoneyDiff
107+
value={Money('$120')}
108+
compareTo={Money('$100')}
109+
className="my-diff"
110+
data-testid="diff"
111+
/>
112+
)
113+
expect(screen.getByTestId('diff')).toHaveClass('my-diff')
114+
})
115+
116+
it('renders as different element type', () => {
117+
render(
118+
<MoneyDiff value={Money('$120')} compareTo={Money('$100')} as="div" data-testid="diff" />
119+
)
120+
expect(screen.getByTestId('diff').tagName).toBe('DIV')
121+
})
122+
})
123+
})
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { render, screen } from '@testing-library/react'
2+
import { Money } from '@thesis-co/cent'
3+
import { MoneyDisplay } from '../../src/components/MoneyDisplay'
4+
5+
describe('MoneyDisplay', () => {
6+
describe('basic rendering', () => {
7+
it('renders formatted money value', () => {
8+
render(<MoneyDisplay value={Money('$100.50')} />)
9+
expect(screen.getByText('$100.50')).toBeInTheDocument()
10+
})
11+
12+
it('renders with string value', () => {
13+
render(<MoneyDisplay value="$200.00" />)
14+
expect(screen.getByText('$200.00')).toBeInTheDocument()
15+
})
16+
17+
it('renders nothing for null value without placeholder', () => {
18+
const { container } = render(<MoneyDisplay value={null} />)
19+
expect(container).toBeEmptyDOMElement()
20+
})
21+
22+
it('renders placeholder for null value', () => {
23+
render(<MoneyDisplay value={null} placeholder="—" />)
24+
expect(screen.getByText('—')).toBeInTheDocument()
25+
})
26+
27+
it('renders placeholder for undefined value', () => {
28+
render(<MoneyDisplay value={undefined} placeholder="N/A" />)
29+
expect(screen.getByText('N/A')).toBeInTheDocument()
30+
})
31+
})
32+
33+
describe('formatting options', () => {
34+
it('applies compact notation', () => {
35+
render(<MoneyDisplay value={Money('$1500000')} compact />)
36+
// Compact notation varies by browser, just check it renders
37+
expect(screen.getByText(/\$1\.5M|\$1,500K/)).toBeInTheDocument()
38+
})
39+
40+
it('applies maxDecimals', () => {
41+
render(<MoneyDisplay value={Money('$100.999')} maxDecimals={2} />)
42+
expect(screen.getByText('$101.00')).toBeInTheDocument()
43+
})
44+
45+
it('excludes currency when requested', () => {
46+
render(<MoneyDisplay value={Money('$100.00')} excludeCurrency />)
47+
expect(screen.getByText('100.00')).toBeInTheDocument()
48+
})
49+
})
50+
51+
describe('showSign prop', () => {
52+
it('shows negative sign by default for negative values', () => {
53+
render(<MoneyDisplay value={Money('-$50.00')} />)
54+
expect(screen.getByText(/-\$50\.00|-50\.00/)).toBeInTheDocument()
55+
})
56+
57+
it('shows positive sign when showSign is always', () => {
58+
render(<MoneyDisplay value={Money('$50.00')} showSign="always" />)
59+
expect(screen.getByText(/\+\$50\.00/)).toBeInTheDocument()
60+
})
61+
62+
it('hides sign when showSign is never', () => {
63+
render(<MoneyDisplay value={Money('-$50.00')} showSign="never" />)
64+
expect(screen.getByText('$50.00')).toBeInTheDocument()
65+
})
66+
})
67+
68+
describe('custom rendering', () => {
69+
it('passes parts to children function', () => {
70+
render(
71+
<MoneyDisplay value={Money('$99.99')}>
72+
{({ formatted, isNegative, isZero }) => (
73+
<span data-testid="custom">
74+
{formatted} - neg:{String(isNegative)} - zero:{String(isZero)}
75+
</span>
76+
)}
77+
</MoneyDisplay>
78+
)
79+
80+
const element = screen.getByTestId('custom')
81+
expect(element).toHaveTextContent('$99.99')
82+
expect(element).toHaveTextContent('neg:false')
83+
expect(element).toHaveTextContent('zero:false')
84+
})
85+
86+
it('provides money instance in parts', () => {
87+
let receivedMoney: Money | null = null
88+
89+
render(
90+
<MoneyDisplay value={Money('$100.00')}>
91+
{({ money }) => {
92+
receivedMoney = money
93+
return <span>test</span>
94+
}}
95+
</MoneyDisplay>
96+
)
97+
98+
expect(receivedMoney).not.toBeNull()
99+
expect(receivedMoney!.toString()).toBe('$100.00')
100+
})
101+
})
102+
103+
describe('styling', () => {
104+
it('applies className', () => {
105+
render(<MoneyDisplay value={Money('$100')} className="my-class" />)
106+
expect(screen.getByText('$100.00')).toHaveClass('my-class')
107+
})
108+
109+
it('applies style', () => {
110+
render(<MoneyDisplay value={Money('$100')} style={{ color: 'red' }} />)
111+
expect(screen.getByText('$100.00')).toHaveStyle({ color: 'red' })
112+
})
113+
114+
it('renders as different element type', () => {
115+
render(<MoneyDisplay value={Money('$100')} as="div" data-testid="money" />)
116+
const element = screen.getByTestId('money')
117+
expect(element.tagName).toBe('DIV')
118+
})
119+
})
120+
})
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { render, screen } from '@testing-library/react'
2+
import userEvent from '@testing-library/user-event'
3+
import { Money } from '@thesis-co/cent'
4+
import { MoneyInput } from '../../src/components/MoneyInput'
5+
6+
describe('MoneyInput', () => {
7+
describe('basic rendering', () => {
8+
it('renders an input element', () => {
9+
render(<MoneyInput name="amount" currency="USD" />)
10+
expect(screen.getByRole('textbox')).toBeInTheDocument()
11+
})
12+
13+
it('displays controlled value', () => {
14+
render(<MoneyInput name="amount" currency="USD" value={Money('$100.00')} />)
15+
expect(screen.getByRole('textbox')).toHaveValue('100.00')
16+
})
17+
18+
it('displays placeholder', () => {
19+
render(<MoneyInput name="amount" currency="USD" placeholder="Enter amount" />)
20+
expect(screen.getByPlaceholderText('Enter amount')).toBeInTheDocument()
21+
})
22+
})
23+
24+
describe('user input', () => {
25+
it('calls onChange with parsed Money value', async () => {
26+
const user = userEvent.setup()
27+
const onChange = jest.fn()
28+
29+
render(<MoneyInput name="amount" currency="USD" onChange={onChange} />)
30+
31+
const input = screen.getByRole('textbox')
32+
await user.type(input, '50.00')
33+
34+
expect(onChange).toHaveBeenCalled()
35+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
36+
expect(lastCall.target.name).toBe('amount')
37+
expect(lastCall.target.value).not.toBeNull()
38+
})
39+
40+
it('calls onValueChange with Money value', async () => {
41+
const user = userEvent.setup()
42+
const onValueChange = jest.fn()
43+
44+
render(<MoneyInput name="amount" currency="USD" onValueChange={onValueChange} />)
45+
46+
const input = screen.getByRole('textbox')
47+
await user.type(input, '75')
48+
49+
expect(onValueChange).toHaveBeenCalled()
50+
})
51+
52+
it('handles clearing input', async () => {
53+
const user = userEvent.setup()
54+
const onChange = jest.fn()
55+
56+
render(<MoneyInput name="amount" currency="USD" value={Money('$100')} onChange={onChange} />)
57+
58+
const input = screen.getByRole('textbox')
59+
await user.clear(input)
60+
61+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
62+
expect(lastCall.target.value).toBeNull()
63+
})
64+
})
65+
66+
describe('formatting', () => {
67+
it('formats value on blur when formatOnBlur is true with controlled value', () => {
68+
const value = Money('$1234.50')
69+
render(
70+
<MoneyInput name="amount" currency="USD" value={value} formatOnBlur={true} />
71+
)
72+
73+
const input = screen.getByRole('textbox')
74+
// Controlled value should display formatted (without currency since excludeCurrency)
75+
expect(input).toHaveValue('1,234.50')
76+
})
77+
78+
it('displays unformatted value when controlled value is set', () => {
79+
const value = Money('$100')
80+
render(
81+
<MoneyInput name="amount" currency="USD" value={value} formatOnBlur={true} />
82+
)
83+
84+
const input = screen.getByRole('textbox')
85+
expect(input).toHaveValue('100.00')
86+
})
87+
})
88+
89+
describe('validation', () => {
90+
it('allows negative values by default', async () => {
91+
const user = userEvent.setup()
92+
const onChange = jest.fn()
93+
94+
render(<MoneyInput name="amount" currency="USD" onChange={onChange} />)
95+
96+
const input = screen.getByRole('textbox')
97+
await user.type(input, '-50')
98+
99+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
100+
expect(lastCall.target.value?.isNegative()).toBe(true)
101+
})
102+
103+
it('converts negative to positive when allowNegative is false', async () => {
104+
const user = userEvent.setup()
105+
const onChange = jest.fn()
106+
107+
render(<MoneyInput name="amount" currency="USD" onChange={onChange} allowNegative={false} />)
108+
109+
const input = screen.getByRole('textbox')
110+
await user.type(input, '-50')
111+
112+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
113+
expect(lastCall.target.value?.isNegative()).toBe(false)
114+
})
115+
})
116+
117+
describe('props passthrough', () => {
118+
it('passes disabled prop', () => {
119+
render(<MoneyInput name="amount" currency="USD" disabled />)
120+
expect(screen.getByRole('textbox')).toBeDisabled()
121+
})
122+
123+
it('passes className', () => {
124+
render(<MoneyInput name="amount" currency="USD" className="my-input" />)
125+
expect(screen.getByRole('textbox')).toHaveClass('my-input')
126+
})
127+
128+
it('sets inputMode to decimal', () => {
129+
render(<MoneyInput name="amount" currency="USD" />)
130+
expect(screen.getByRole('textbox')).toHaveAttribute('inputMode', 'decimal')
131+
})
132+
})
133+
134+
describe('ref forwarding', () => {
135+
it('forwards ref to input element', () => {
136+
const ref = { current: null as HTMLInputElement | null }
137+
render(<MoneyInput name="amount" currency="USD" ref={ref} />)
138+
expect(ref.current).toBeInstanceOf(HTMLInputElement)
139+
})
140+
})
141+
})

0 commit comments

Comments
 (0)