- money_field_test.go: compile gates (moved from the smoke file and extended), model column gate, save-time value check, list projection - settings secret projection and put without a database; tabs, settings richeditor sanitize and the danger flag as internal unit tests - Postgres: TestMoneyFieldSave, TestSettingsSecretPostgres and the Phase 18 payload cases of TestPhase10OpenAPIConformance - vitest: money.ts helpers spec, MoneyField blur-only and server-error cases - gate arrays and the cabana README Testing section name every test
102 lines
4.8 KiB
TypeScript
102 lines
4.8 KiB
TypeScript
// The exact-decimal money helpers of the money field and list column
|
||
// (Phase 18 D-07 with the amended D-09 and D-10): text in, text out, never a
|
||
// JS number. No application names appear in framework tests.
|
||
import { describe, expect, it } from 'vitest'
|
||
import { decimalSeparator, formatMoney, parseMoneyInput, trimMoneyDisplay } from '../../src/app/money'
|
||
|
||
const spaces = (s: string) => s.replace(/\s/g, ' ')
|
||
|
||
describe('parseMoneyInput', () => {
|
||
it('accepts a comma or a dot as the separator and strips spaces and no-break spaces', () => {
|
||
expect(parseMoneyInput('1234,56', 4)).toEqual({ value: '1234.56', error: null })
|
||
expect(parseMoneyInput('1234.56', 4)).toEqual({ value: '1234.56', error: null })
|
||
expect(parseMoneyInput(' 1 234,5 ', 4)).toEqual({ value: '1234.5', error: null })
|
||
expect(parseMoneyInput('1 234,56', 4)).toEqual({ value: '1234.56', error: null })
|
||
expect(parseMoneyInput('7', 2)).toEqual({ value: '7', error: null })
|
||
})
|
||
|
||
it('keeps a leading minus and refuses a minus anywhere else', () => {
|
||
expect(parseMoneyInput('-0,5', 4)).toEqual({ value: '-0.5', error: null })
|
||
expect(parseMoneyInput('5-', 4)).toEqual({ value: null, error: 'invalid' })
|
||
expect(parseMoneyInput('--5', 4)).toEqual({ value: null, error: 'invalid' })
|
||
expect(parseMoneyInput('+5', 4)).toEqual({ value: null, error: 'invalid' })
|
||
})
|
||
|
||
it('is null for empty or blank text', () => {
|
||
expect(parseMoneyInput('', 2)).toEqual({ value: null, error: null })
|
||
expect(parseMoneyInput(' ', 2)).toEqual({ value: null, error: null })
|
||
})
|
||
|
||
it('refuses letters, exponents, two separators and lone separators as invalid', () => {
|
||
for (const text of ['abc', '1e3', '1E3', '1.2.3', '1,234.56', '1.234,56', '.5', '5.', ',', '0x10', 'NaN', 'Infinity', '12 a']) {
|
||
expect(parseMoneyInput(text, 4), text).toEqual({ value: null, error: 'invalid' })
|
||
}
|
||
})
|
||
|
||
it('refuses more significant fraction digits than the scale, never rounding or truncating', () => {
|
||
expect(parseMoneyInput('12.34567', 4)).toEqual({ value: null, error: 'too_precise' })
|
||
expect(parseMoneyInput('1,234', 2)).toEqual({ value: null, error: 'too_precise' })
|
||
expect(parseMoneyInput('1.234', 3)).toEqual({ value: '1.234', error: null })
|
||
expect(parseMoneyInput('0.0001', 4)).toEqual({ value: '0.0001', error: null })
|
||
})
|
||
|
||
it('accepts and keeps trailing zeros beyond the scale, as the server does', () => {
|
||
expect(parseMoneyInput('12.5000', 2)).toEqual({ value: '12.5000', error: null })
|
||
expect(parseMoneyInput('39,840000', 4)).toEqual({ value: '39.840000', error: null })
|
||
expect(parseMoneyInput('12.5001', 2)).toEqual({ value: null, error: 'too_precise' })
|
||
})
|
||
|
||
it('keeps amounts beyond the float range digit for digit', () => {
|
||
expect(parseMoneyInput('99999999999999999999.9999', 4)).toEqual({ value: '99999999999999999999.9999', error: null })
|
||
expect(parseMoneyInput('0.1', 4).value).toBe('0.1')
|
||
})
|
||
})
|
||
|
||
describe('trimMoneyDisplay', () => {
|
||
it('trims trailing fraction zeros down to two decimals and keeps the rest', () => {
|
||
expect(trimMoneyDisplay('39.8400')).toBe('39.84')
|
||
expect(trimMoneyDisplay('85.4193')).toBe('85.4193')
|
||
expect(trimMoneyDisplay('85.4190')).toBe('85.419')
|
||
expect(trimMoneyDisplay('5.0000')).toBe('5.00')
|
||
expect(trimMoneyDisplay('-0.5000')).toBe('-0.50')
|
||
expect(trimMoneyDisplay('7')).toBe('7')
|
||
expect(trimMoneyDisplay('7.5')).toBe('7.5')
|
||
})
|
||
})
|
||
|
||
describe('decimalSeparator', () => {
|
||
it('is a dot in en and a comma in pl', () => {
|
||
expect(decimalSeparator('en')).toBe('.')
|
||
expect(decimalSeparator('pl')).toBe(',')
|
||
})
|
||
})
|
||
|
||
describe('formatMoney', () => {
|
||
it('formats en and pl with the ISO code and at least two decimals', () => {
|
||
expect(spaces(formatMoney('39.8400', 'PLN', 'en'))).toBe('PLN 39.84')
|
||
expect(spaces(formatMoney('39.8400', 'PLN', 'pl'))).toBe('39,84 PLN')
|
||
expect(spaces(formatMoney('7', 'EUR', 'en'))).toBe('EUR 7.00')
|
||
expect(spaces(formatMoney('1234567.5', 'EUR', 'pl'))).toBe('1 234 567,50 EUR')
|
||
})
|
||
|
||
it('keeps up to scale decimals and never rounds a four-decimal amount', () => {
|
||
expect(spaces(formatMoney('85.4193', 'PLN', 'en', 4))).toBe('PLN 85.4193')
|
||
expect(spaces(formatMoney('99999999.9999', 'PLN', 'en', 4))).toBe('PLN 99,999,999.9999')
|
||
expect(spaces(formatMoney('99999999.9999', 'PLN', 'pl', 4))).toBe('99 999 999,9999 PLN')
|
||
expect(spaces(formatMoney('12.5', 'EUR', 'en', 2))).toBe('EUR 12.50')
|
||
})
|
||
|
||
it('formats a negative amount', () => {
|
||
expect(spaces(formatMoney('-0.5000', 'PLN', 'en'))).toBe('-PLN 0.50')
|
||
})
|
||
|
||
it('formats the amount alone without a currency', () => {
|
||
expect(spaces(formatMoney('12.5', '', 'en'))).toBe('12.50')
|
||
expect(spaces(formatMoney('1234.5678', '', 'pl'))).toBe('1234,5678')
|
||
})
|
||
|
||
it('appends an unknown code to the plain amount instead of throwing', () => {
|
||
expect(spaces(formatMoney('12.5', 'ZZZZ', 'en'))).toBe('12.50 ZZZZ')
|
||
})
|
||
})
|