adam-26/intl-fmt

View on GitHub
test/unit/format.js

Summary

Maintainability
F
2 wks
Test Coverage
import expect, {createSpy, spyOn} from 'expect';
import IntlMessageFormat from 'tag-messageformat';
import IntlRelativeFormat from 'tag-relativeformat';
import IntlPluralFormat from '../../src/plural';
import {defaultErrorHandler} from "../../src/utils";
import * as f from '../../src/format';

describe('format API', () => {
    const IS_PROD = process.env.NODE_ENV === 'production';
    const IRF_THRESHOLDS = {...IntlRelativeFormat.thresholds};

    let consoleError;
    let config;
    let state;

    beforeEach(() => {
        consoleError = spyOn(console, 'error');

        config = {
            locale: 'en',
            defaultMessages: {},

            messages: {
                no_args: 'Hello, World!',
                with_arg: 'Hello, {name}!',
                with_named_format: 'It is {now, date, year-only}',
                with_html: 'Hello, <b>{name}</b>!',

                missing: undefined,
                empty: '',
                invalid: 'invalid {}',
                missing_value: 'missing {arg_missing}',
                missing_named_format: 'missing {now, date, format_missing}',
                select_missing_other: '{gender, select, female {woman} male {man}}',
                plural_missing_other: 'I have {numPeople, plural, =0 {zero points} one {a point}}.',
            },

            formats: {
                date: {
                    'year-only': {
                        year: 'numeric',
                    },
                    missing: undefined,
                },

                time: {
                    'hour-only': {
                        hour: '2-digit',
                        hour12: false,
                    },
                    missing: undefined,
                },

                relative: {
                    'seconds': {
                        units: 'second',
                    },
                    missing: undefined,
                },

                number: {
                    'percent': {
                        style: 'percent',
                        minimumFractionDigits: 2,
                    },
                    missing: undefined,
                },
            },

            defaultLocale: 'en',
            defaultFormats: {},
            defaultOptions: {},
            requireOther: true,
            onError: defaultErrorHandler
        };

        state = {
            getDateTimeFormat: createSpy().andCall((...args) => new Intl.DateTimeFormat(...args)),
            getNumberFormat  : createSpy().andCall((...args) => new Intl.NumberFormat(...args)),
            getMessageFormat : createSpy().andCall((...args) => new IntlMessageFormat(...args)),
            getRelativeFormat: createSpy().andCall((...args) => new IntlRelativeFormat(...args)),
            getPluralFormat  : createSpy().andCall((...args) => new IntlPluralFormat(...args)),

            now: () => 0,
        };
    });

    afterEach(() => {
        f.setProd(IS_PROD);
        consoleError.restore();
    });

    describe('exports', () => {
        const intlFormatPropNames = [
            'formatDate',
            'formatTime',
            'formatRelative',
            'formatNumber',
            'formatPlural',
            'formatMessage',
            'formatHTMLMessage'
        ];

        intlFormatPropNames.forEach((name) => {
            it(`exports \`${name}\``, () => {
                expect(f[name]).toBeA('function');
            });
        });
    });

    describe('formatDate()', () => {
        let df;
        let formatDate;

        beforeEach(() => {
            df = new Intl.DateTimeFormat(config.locale);
            formatDate = f.formatDate.bind(null, config, state);
        });

        it('fallsback and warns when no value is provided', () => {
            expect(formatDate()).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(1);
            expect(consoleError.calls[0].arguments[0]).toContain(
                '[Intl Format] Error formatting date.\nRangeError'
            );
        });

        it('fallsback and warns when a non-finite value is provided', () => {
            expect(formatDate(NaN)).toBe('Invalid Date');
            expect(formatDate('')).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(2);
        });

        it('formats falsy finite values', () => {
            expect(formatDate(false)).toBe(df.format(false));
            expect(formatDate(null)).toBe(df.format(null));
            expect(formatDate(0)).toBe(df.format(0));
        });

        it('formats date instance values', () => {
            expect(formatDate(new Date(0))).toBe(df.format(new Date(0)));
        });

        it('formats date string values', () => {
            expect(formatDate(new Date(0).toString())).toBe(df.format(new Date(0)));
        });

        it('formats date ms timestamp values', () => {
            const timestamp = Date.now();
            expect(formatDate(timestamp)).toBe(df.format(timestamp));
        });

        it('uses configured defaultOptions', () => {
            const {locale, formats} = config;
            formatDate = f.formatDate.bind(null, {
                ...config,
                defaultOptions: { date: { format: 'year-only' } }
            }, state);

            const date   = new Date();
            df = new Intl.DateTimeFormat(locale, formats.date['year-only']);
            expect(formatDate(date)).toBe(df.format(date));
        });

        describe('options', () => {
            it('accepts empty options', () => {
                expect(formatDate(0, {})).toBe(df.format(0));
            });

            it('accepts valid Intl.DateTimeFormat options', () => {
                expect(() => formatDate(0, {year: 'numeric'})).toNotThrow();
            });

            it('fallsback and warns on invalid Intl.DateTimeFormat options', () => {
                expect(formatDate(0, {year: 'invalid'})).toBe(String(new Date(0)));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    '[Intl Format] Error formatting date.\nRangeError'
                );
            });

            it('uses configured named formats', () => {
                const date   = new Date();
                const format = 'year-only';

                const {locale, formats} = config;
                df = new Intl.DateTimeFormat(locale, formats.date[format]);

                expect(formatDate(date, {format})).toBe(df.format(date));
            });

            it('uses named formats as defaults', () => {
                const date   = new Date();
                const opts   = {month: 'numeric'};
                const format = 'year-only';

                const {locale, formats} = config;
                df = new Intl.DateTimeFormat(locale, {
                    ...opts,
                    ...formats.date[format],
                });

                expect(formatDate(date, {...opts, format})).toBe(df.format(date));
            });

            it('handles missing named formats and warns', () => {
                const date   = new Date();
                const format = 'missing';

                df = new Intl.DateTimeFormat(config.locale);

                expect(formatDate(date, {format})).toBe(df.format(date));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toBe(
                    `[Intl Format] No date format named: ${format}`
                );
            });
        });
    });

    describe('formatTime()', () => {
        let df;
        let formatTime;

        beforeEach(() => {
            df = new Intl.DateTimeFormat(config.locale, {
                hour: 'numeric',
                minute: 'numeric',
            });

            formatTime = f.formatTime.bind(null, config, state);
        });

        it('fallsback and warns when no value is provided', () => {
            expect(formatTime()).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(1);
            expect(consoleError.calls[0].arguments[0]).toContain(
                '[Intl Format] Error formatting time.\nRangeError'
            );
        });

        it('fallsback and warns when a non-finite value is provided', () => {
            expect(formatTime(NaN)).toBe('Invalid Date');
            expect(formatTime('')).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(2);
        });

        it('formats falsy finite values', () => {
            expect(formatTime(false)).toBe(df.format(false));
            expect(formatTime(null)).toBe(df.format(null));
            expect(formatTime(0)).toBe(df.format(0));
        });

        it('formats date instance values', () => {
            expect(formatTime(new Date(0))).toBe(df.format(new Date(0)));
        });

        it('formats date string values', () => {
            expect(formatTime(new Date(0).toString())).toBe(df.format(new Date(0)));
        });

        it('formats date ms timestamp values', () => {
            const timestamp = Date.now();
            expect(formatTime(timestamp)).toBe(df.format(timestamp));
        });

        it('uses configured defaultOptions', () => {
            const {locale, formats} = config;
            formatTime = f.formatTime.bind(null, {
                ...config,
                defaultOptions: { time: { format: 'hour-only' } }
            }, state);

            const date   = new Date();
            df = new Intl.DateTimeFormat(locale, formats.time['hour-only']);
            expect(formatTime(date)).toBe(df.format(date));
        });

        describe('options', () => {
            it('accepts empty options', () => {
                expect(formatTime(0, {})).toBe(df.format(0));
            });

            it('accepts valid Intl.DateTimeFormat options', () => {
                expect(() => formatTime(0, {hour: '2-digit'})).toNotThrow();
            });

            it('fallsback and warns on invalid Intl.DateTimeFormat options', () => {
                expect(formatTime(0, {hour: 'invalid'})).toBe(String(new Date(0)));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    '[Intl Format] Error formatting time.\nRangeError'
                );
            });

            it('uses configured named formats', () => {
                const date   = new Date();
                const format = 'hour-only';

                const {locale, formats} = config;
                df = new Intl.DateTimeFormat(locale, formats.time[format]);

                expect(formatTime(date, {format})).toBe(df.format(date));
            });

            it('uses named formats as defaults', () => {
                const date   = new Date();
                const opts   = {minute: '2-digit'};
                const format = 'hour-only';

                const {locale, formats} = config;
                df = new Intl.DateTimeFormat(locale, {
                    ...opts,
                    ...formats.time[format],
                });

                expect(formatTime(date, {...opts, format})).toBe(df.format(date));
            });

            it('handles missing named formats and warns', () => {
                const date   = new Date();
                const format = 'missing';

                expect(formatTime(date, {format})).toBe(df.format(date));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toBe(
                    `[Intl Format] No time format named: ${format}`
                );
            });

            it('should set default values', () => {
                const date = new Date();
                const {locale} = config;
                const day = 'numeric';
                df = new Intl.DateTimeFormat(locale, {hour: 'numeric', minute: 'numeric', day});
                expect(formatTime(date, {day})).toBe(df.format(date));
            });

            it('should not set default values when second is provided', () => {
                const date = new Date();
                const {locale} = config;
                const second = 'numeric';
                df = new Intl.DateTimeFormat(locale, {second});
                expect(formatTime(date, {second})).toBe(df.format(date));
            });

            it('should not set default values when minute is provided', () => {
                const date = new Date();
                const {locale} = config;
                const minute = 'numeric';
                df = new Intl.DateTimeFormat(locale, {minute});
                expect(formatTime(date, {minute})).toBe(df.format(date));
            });

            it('should not set default values when hour is provided', () => {
                const date = new Date();
                const {locale} = config;
                const hour = 'numeric';
                df = new Intl.DateTimeFormat(locale, {hour});
                expect(formatTime(date, {hour})).toBe(df.format(date));
            });
        });
    });

    describe('formatRelative()', () => {
        let now;
        let rf;
        let formatRelative;

        beforeEach(() => {
            now = state.now();
            rf = new IntlRelativeFormat(config.locale);
            formatRelative = f.formatRelative.bind(null, config, state);
        });

        it('fallsback and warns when no value is provided', () => {
            expect(formatRelative()).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(1);
            expect(consoleError.calls[0].arguments[0]).toContain(
                '[Intl Format] Error formatting relative time.\nRangeError'
            );
        });

        it('fallsback and warns when a non-finite value is provided', () => {
            expect(formatRelative(NaN)).toBe('Invalid Date');
            expect(formatRelative('')).toBe('Invalid Date');
            expect(consoleError.calls.length).toBe(2);
        });

        it('formats falsy finite values', () => {
            expect(formatRelative(false)).toBe(rf.format(false, {now}));
            expect(formatRelative(null)).toBe(rf.format(null, {now}));
            expect(formatRelative(0)).toBe(rf.format(0, {now}));
        });

        it('formats date instance values', () => {
            expect(formatRelative(new Date(0))).toBe(rf.format(new Date(0), {now}));
        });

        it('formats date string values', () => {
            expect(formatRelative(new Date(0).toString())).toBe(rf.format(new Date(0), {now}));
        });

        it('formats date ms timestamp values', () => {
            const timestamp = Date.now();
            expect(formatRelative(timestamp)).toBe(rf.format(timestamp, {now}));
        });

        it('formats with the expected thresholds', () => {
            const timestamp = now - (1000 * 59);
            expect(IntlRelativeFormat.thresholds).toEqual(IRF_THRESHOLDS);
            expect(formatRelative(timestamp)).toNotBe(rf.format(timestamp, {now}));
            expect(formatRelative(timestamp)).toBe('59 seconds ago');
            expect(IntlRelativeFormat.thresholds).toEqual(IRF_THRESHOLDS);
            expect(formatRelative(NaN)).toBe('Invalid Date');
            expect(IntlRelativeFormat.thresholds).toEqual(IRF_THRESHOLDS);
        });

        it('uses configured defaultOptions', () => {
            const {locale, formats} = config;
            formatRelative = f.formatRelative.bind(null, {
                ...config,
                defaultOptions: { relative: { format: 'seconds' } }
            }, state);

            const date   = -(1000 * 120);
            rf = new IntlRelativeFormat(locale, formats.relative.seconds);
            expect(formatRelative(date)).toBe(rf.format(date, {now}));
        });

        describe('options', () => {
            it('accepts empty options', () => {
                expect(formatRelative(0, {})).toBe(rf.format(0, {now}));
            });

            it('accepts valid IntlRelativeFormat options', () => {
                expect(() => formatRelative(0, {units: 'second'})).toNotThrow();
            });

            it('fallsback and warns on invalid IntlRelativeFormat options', () => {
                expect(formatRelative(0, {units: 'invalid'})).toBe(String(new Date(0)));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toBe(
                    '[Intl Format] Error formatting relative time.\nError: "invalid" is not a valid TagRelativeFormat `units` value, it must be one of: "second", "minute", "hour", "day", "month", "year", "second-short", "minute-short", "hour-short", "day-short", "month-short", "year-short"'
                );
            });

            it('uses configured named formats', () => {
                const date   = -(1000 * 120);
                const format = 'seconds';

                const {locale, formats} = config;
                rf = new IntlRelativeFormat(locale, formats.relative[format]);

                expect(formatRelative(date, {format})).toBe(rf.format(date, {now}));
            });

            it('uses named formats as defaults', () => {
                const date   = 0;
                const opts   = {style: 'numeric'};
                const format = 'seconds';

                const {locale, formats} = config;
                rf = new IntlRelativeFormat(locale, {
                    ...opts,
                    ...formats.relative[format],
                });

                expect(formatRelative(date, {...opts, format})).toBe(rf.format(date, {now}));
            });

            it('handles missing named formats and warns', () => {
                const date   = new Date();
                const format = 'missing';

                rf = new IntlRelativeFormat(config.locale);

                expect(formatRelative(date, {format})).toBe(rf.format(date, {now}));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toBe(
                    `[Intl Format] No relative format named: ${format}`
                );
            });

            describe('now', () => {
                it('accepts a `now` option', () => {
                    now = 1000;
                    expect(formatRelative(0, {now})).toBe(rf.format(0, {now}));
                });

                it('defaults to `state.now()` when no value is provided', () => {
                    now = 2000;
                    state.now = () => now;

                    expect(formatRelative(1000)).toBe(rf.format(1000, {now}));
                });

                it('does not throw or warn when a non-finite value is provided', () => {
                    expect(() => formatRelative(0, {now: NaN})).toNotThrow();
                    expect(() => formatRelative(0, {now: ''})).toNotThrow();
                    expect(consoleError.calls.length).toBe(0);
                });

                it('formats falsy finite values', () => {
                    expect(formatRelative(0, {now: false})).toBe(rf.format(0, {now: false}));
                    expect(formatRelative(0, {now: null})).toBe(rf.format(0, {now: null}));
                    expect(formatRelative(0, {now: 0})).toBe(rf.format(0, {now: 0}));
                });

                it('formats date instance values', () => {
                    now = new Date(1000);
                    expect(formatRelative(0, {now})).toBe(rf.format(0, {now}));
                });

                it('formats date string values', () => {
                    now = 1000;
                    const dateString = new Date(now).toString();
                    expect(formatRelative(0, {now: dateString})).toBe(rf.format(0, {now}));
                });

                it('formats date ms timestamp values', () => {
                    now = 1000;
                    expect(formatRelative(0, {now})).toBe(rf.format(0, {now}));
                });
            });
        });
    });

    describe('formatNumber()', () => {
        let nf;
        let formatNumber;

        beforeEach(() => {
            nf = new Intl.NumberFormat(config.locale);
            formatNumber = f.formatNumber.bind(null, config, state);
        });

        it('returns "NaN" when no value is provided', () => {
            expect(nf.format()).toBe('NaN');
            expect(formatNumber()).toBe('NaN');
        });

        it('returns "NaN" when a non-number value is provided', () => {
            expect(nf.format(NaN)).toBe('NaN');
            expect(formatNumber(NaN)).toBe('NaN');
        });

        it('formats falsy values', () => {
            expect(formatNumber(false)).toBe(nf.format(false));
            expect(formatNumber(null)).toBe(nf.format(null));
            expect(formatNumber('')).toBe(nf.format(''));
            expect(formatNumber(0)).toBe(nf.format(0));
        });

        it('formats number values', () => {
            expect(formatNumber(1000)).toBe(nf.format(1000));
            expect(formatNumber(1.1)).toBe(nf.format(1.1));
        });

        it('formats string values parsed as numbers', () => {
            expect(Number('1000')).toBe(1000);
            expect(formatNumber('1000')).toBe(nf.format('1000'));
            expect(Number('1.10')).toBe(1.1);
            expect(formatNumber('1.10')).toBe(nf.format('1.10'));
        });

        it('uses configured defaultOptions', () => {
            const {locale, formats} = config;
            const num    = 0.505;
            formatNumber = f.formatNumber.bind(null, {
                ...config,
                defaultOptions: { number: { format: 'percent' } }
            }, state);

            nf = new Intl.NumberFormat(locale, formats.number.percent);
            expect(formatNumber(num)).toBe(nf.format(num));
        });

        describe('options', () => {
            it('accepts empty options', () => {
                expect(formatNumber(1000, {})).toBe(nf.format(1000));
            });

            it('accepts valid Intl.NumberFormat options', () => {
                expect(() => formatNumber(0, {style: 'percent'})).toNotThrow();
            });

            it('fallsback and warns on invalid Intl.NumberFormat options', () => {
                expect(formatNumber(0, {style: 'invalid'})).toBe(String(0));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    '[Intl Format] Error formatting number.\nRangeError'
                );
            });

            it('uses configured named formats', () => {
                const num    = 0.505;
                const format = 'percent';

                const {locale, formats} = config;
                nf = new Intl.NumberFormat(locale, formats.number[format]);

                expect(formatNumber(num, {format})).toBe(nf.format(num));
            });

            it('uses named formats as defaults', () => {
                const num    = 0.500059;
                const opts   = {maximumFractionDigits: 3};
                const format = 'percent';

                const {locale, formats} = config;
                nf = new Intl.NumberFormat(locale, {
                    ...opts,
                    ...formats.number[format],
                });

                expect(formatNumber(num, {...opts, format})).toBe(nf.format(num));
            });

            it('handles missing named formats and warns', () => {
                const num    = 1000;
                const format = 'missing';

                nf = new Intl.NumberFormat(config.locale);

                expect(formatNumber(num, {format})).toBe(nf.format(num));
                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toBe(
                    `[Intl Format] No number format named: ${format}`
                );
            });
        });
    });

    describe('formatPlural()', () => {
        let pf;
        let formatPlural;

        beforeEach(() => {
            pf = new IntlPluralFormat(config.locale);
            formatPlural = f.formatPlural.bind(null, config, state);
        });

        it('formats falsy values', () => {
            expect(formatPlural(undefined)).toBe(pf.format(undefined));
            expect(formatPlural(false)).toBe(pf.format(false));
            expect(formatPlural(null)).toBe(pf.format(null));
            expect(formatPlural(NaN)).toBe(pf.format(NaN));
            expect(formatPlural('')).toBe(pf.format(''));
            expect(formatPlural(0)).toBe(pf.format(0));
        });

        it('formats integer values', () => {
            expect(formatPlural(0)).toBe(pf.format(0));
            expect(formatPlural(1)).toBe(pf.format(1));
            expect(formatPlural(2)).toBe(pf.format(2));
        });

        it('formats decimal values', () => {
            expect(formatPlural(0.1)).toBe(pf.format(0.1));
            expect(formatPlural(1.0)).toBe(pf.format(1.0));
            expect(formatPlural(1.1)).toBe(pf.format(1.1));
        });

        it('formats string values parsed as numbers', () => {
            expect(Number('0')).toBe(0);
            expect(formatPlural('0')).toBe(pf.format('0'));
            expect(Number('1')).toBe(1);
            expect(formatPlural('1')).toBe(pf.format('1'));

            expect(Number('0.1')).toBe(0.1);
            expect(formatPlural('0.1')).toBe(pf.format('0.1'));
            expect(Number('1.0')).toBe(1.0);
            expect(formatPlural('1.0')).toBe(pf.format('1.0'));
        });

        it('uses configured defaultOptions', () => {
            formatPlural = f.formatPlural.bind(null, {
                ...config,
                defaultOptions: { plural: { style: 'ordinal' } }
            }, state);

            pf = new IntlPluralFormat(config.locale, {style: 'ordinal'});
            expect(formatPlural(22)).toBe(pf.format(22));
        });

        describe('options', () => {
            it('accepts empty options', () => {
                expect(formatPlural(0, {})).toBe(pf.format(0));
            });

            it('accepts valid IntlPluralFormat options', () => {
                expect(() => formatPlural(22, {style: 'ordinal'})).toNotThrow();
            });

            describe('ordinals', () => {
                it('formats using ordinal plural rules', () => {
                    const opts = {style: 'ordinal'};
                    pf = new IntlPluralFormat(config.locale, opts);

                    expect(formatPlural(22, opts)).toBe(pf.format(22));
                });
            });
        });
    });

    describe('formatMessage()', () => {
        let formatMessage;

        beforeEach(() => {
            formatMessage = f.formatMessage.bind(null, config, state);
        });

        it('throws when no Message Descriptor is provided', () => {
            expect(() => formatMessage()).toThrow(
                '[Intl Format] An `id` must be provided to format a message.'
            );
        });

        it('throws when Message Descriptor `id` is missing or falsy', () => {
            expect(() => formatMessage({})).toThrow(
                '[Intl Format] An `id` must be provided to format a message.'
            );

            [undefined, null, false, 0, ''].forEach((id) => {
                expect(() => formatMessage({id: id})).toThrow(
                    '[Intl Format] An `id` must be provided to format a message.'
                );
            });
        });

        it('warns when select is missing `other` option', () => {
            const msg = formatMessage(
                { id: '1', defaultMessage: config.messages.select_missing_other },
                { gender: 'female' });

            expect(msg).toBe('{gender, select, female {woman} male {man}}');
            expect(consoleError.calls.length).toBe(2);
            expect(consoleError.calls[0].arguments[0]).toContain(
                '[Intl Format] Error formatting the default message for: "1"\nError');
            expect(consoleError.calls[1].arguments[0]).toContain(
                '[Intl Format] Cannot format message: "1", using message source as fallback.');
        });

        it('warns when plural is missing `other` option', () => {
            const msg = formatMessage(
                { id: '1', defaultMessage: config.messages.plural_missing_other },
                { numPeople: 0 });

            expect(msg).toBe('I have {numPeople, plural, =0 {zero points} one {a point}}.');
            expect(consoleError.calls.length).toBe(2);
            expect(consoleError.calls[0].arguments[0]).toContain(
                '[Intl Format] Error formatting the default message for: "1"\nError');
            expect(consoleError.calls[1].arguments[0]).toContain(
                '[Intl Format] Cannot format message: "1", using message source as fallback.');
        });

        it('no warning when select is missing `other` and `requireOther: false`', () => {
            const { locale, messages } = config;
            const values = { gender: 'female' };
            const mf = new IntlMessageFormat(
                messages.select_missing_other, locale, {},
                { requireOther: false });

            // bind formatMessage with `requireOther: false`
            formatMessage = f.formatMessage.bind(null, { ...config, requireOther: false }, state);
            const msg = formatMessage({ id: '1', defaultMessage: messages.select_missing_other }, values);

            expect(msg).toBe(mf.format(values));
            expect(consoleError.calls.length).toBe(0);
        });

        it('no warning when plural is missing `other` and `requireOther: false`', () => {
            const { locale, messages } = config;
            const values = { numPeople: 0 };
            const mf = new IntlMessageFormat(
                messages.plural_missing_other, locale, {},
                { requireOther: false });

            // bind formatMessage with `requireOther: false`
            formatMessage = f.formatMessage.bind(null, { ...config, requireOther: false }, state);
            const msg = formatMessage({ id: '1', defaultMessage: messages.plural_missing_other }, values);

            expect(msg).toBe(mf.format(values));
            expect(consoleError.calls.length).toBe(0);
        });

        it('formats message using assigned StringFormat instance', () => {
            function SimpleStringFormat(id) { this.id = id; }
            SimpleStringFormat.prototype.format = function () { return 'world'; };
            function SimpleStringFormatFactory(id) { return new SimpleStringFormat(id); }

            const {locale, messages} = config;
            formatMessage = f.formatMessage.bind(null, { ...config, stringFormatFactory: SimpleStringFormatFactory }, state);
            const mf = new IntlMessageFormat(messages.with_arg, locale, {}, {
                stringFormatFactory: SimpleStringFormatFactory
            });

            expect(mf.format({name: 'bob'})).toBe('Hello, world!');
            expect(formatMessage({id: 'with_arg'}, {name: 'bob'})).toBe(mf.format({name: 'bob'}));
        });

        it('formats basic messages', () => {
            const {locale, messages} = config;
            const mf = new IntlMessageFormat(messages.no_args, locale);

            expect(formatMessage({id: 'no_args'})).toBe(mf.format());
        });

        it('formats defaultMessages text in preference to the messageDescriptor defaultValue', () => {
            const {locale} = config;
            const msgId = 'no_args';
            const defaultMsg = 'a default message msg with no args';
            formatMessage = f.formatMessage.bind(null, {...config, defaultMessages: { [msgId]: defaultMsg }, messages: {} }, state);
            const mf = new IntlMessageFormat(defaultMsg, locale);

            expect(formatMessage({id: msgId})).toBe(mf.format());
        });

        it('formats messages with placeholders', () => {
            const {locale, messages} = config;
            const mf = new IntlMessageFormat(messages.with_arg, locale);
            const values = {name: 'Eric'};

            expect(formatMessage({id: 'with_arg'}, values)).toBe(mf.format(values));
        });

        it('formats messages with named formats', () => {
            const {locale, messages, formats} = config;
            const mf = new IntlMessageFormat(messages.with_named_format, locale, formats);
            const values = {now: Date.now()};

            expect(formatMessage({id: 'with_named_format'}, values)).toBe(mf.format(values));
        });

        it('avoids formatting when no values and in production', () => {
            const {messages} = config;

            f.setProd(true);
            expect(formatMessage({id: 'no_args'})).toBe(messages.no_args);
            expect(state.getMessageFormat.calls.length).toBe(0);

            const values = {foo: 'foo'};
            expect(formatMessage({id: 'no_args'}, values)).toBe(messages.no_args);
            expect(state.getMessageFormat.calls.length).toBe(1);

            f.setProd(false);
            expect(formatMessage({id: 'no_args'})).toBe(messages.no_args);
            expect(state.getMessageFormat.calls.length).toBe(2);
        });

        it('uses configured defaultOptions', () => {
            const {locale, messages} = config;
            function TestFormatter() {}
            TestFormatter.prototype.format = function () {
                return 'hello';
            };

            formatMessage = f.formatMessage.bind(null, {
                ...config,
                defaultOptions: { message: { stringFormatFactory: () => new TestFormatter() } }
            }, state);

            const values = { name: 'bob' };
            const mf = new IntlMessageFormat(messages.with_arg, locale);
            expect(formatMessage({ id: '1', defaultMessage: messages.with_arg}, values))
                .toBe(mf.format(values, { stringFormatFactory: () => new TestFormatter() }));
        });

        describe('fallbacks', () => {
            it('formats message with missing named formats', () => {
                const {locale, messages} = config;
                const mf = new IntlMessageFormat(messages.missing_named_format, locale);
                const values = {now: Date.now()};

                expect(formatMessage({id: 'missing_named_format'}, values)).toBe(mf.format(values));
            });

            it('formats `defaultMessage` when message is missing', () => {
                const {locale, messages} = config;
                const mf = new IntlMessageFormat(messages.with_arg, locale);
                const id = 'missing';
                const values = {name: 'Eric'};

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.with_arg,
                }, values)).toBe(mf.format(values));
            });

            it('warns when `message` is missing and locales are different', () => {
                config.locale = 'fr';

                let {locale, messages, defaultLocale} = config;
                let mf = new IntlMessageFormat(messages.with_arg, locale);
                let id = 'missing';
                let values = {name: 'Eric'};

                expect(locale).toNotEqual(defaultLocale);

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.with_arg,
                }, values)).toBe(mf.format(values));

                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Missing message: "${id}" for locale: "${locale}", using default message as fallback.`
                );
            });

            it('warns when `message` and `defaultMessage` are missing', () => {
                let {locale, messages} = config;
                let id = 'missing';
                let values = {name: 'Eric'};

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.missing,
                }, values)).toBe(id);

                expect(consoleError.calls.length).toBe(2);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Missing message: "${id}" for locale: "${locale}"`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message id as fallback.`
                );
            });

            it('formats `defaultMessage` when message has a syntax error', () => {
                const {locale, messages} = config;
                const mf = new IntlMessageFormat(messages.with_arg, locale);
                const id = 'invalid';
                const values = {name: 'Eric'};

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.with_arg,
                }, values)).toBe(mf.format(values));

                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Error formatting message: "${id}" for locale: "${locale}", using default message as fallback.`
                );
            });

            it('formats `defaultMessage` when message has missing values', () => {
                const {locale, messages} = config;
                const mf = new IntlMessageFormat(messages.with_arg, locale);
                const id = 'missing_value';
                const values = {name: 'Eric'};

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.with_arg,
                }, values)).toBe(mf.format(values));

                expect(consoleError.calls.length).toBe(1);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Error formatting message: "${id}" for locale: "${locale}", using default message as fallback.`
                );
            });

            it('returns message source when message and `defaultMessage` have formatting errors', () => {
                const {locale, messages} = config;
                const id = 'missing_value';

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.invalid,
                })).toBe(messages[id]);

                expect(consoleError.calls.length).toBe(3);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Error formatting message: "${id}" for locale: "${locale}"`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Error formatting the default message for: "${id}"`
                );
                expect(consoleError.calls[2].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message source as fallback.`
                );
            });

            it('returns message source when formatting error and missing `defaultMessage`', () => {
                const {locale, messages} = config;
                const id = 'missing_value';

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.missing,
                })).toBe(messages[id]);

                expect(consoleError.calls.length).toBe(2);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Error formatting message: "${id}" for locale: "${locale}"`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message source as fallback.`
                );
            });

            it('returns `defaultMessage` source when formatting errors and missing message', () => {
                config.locale = 'en-US';

                const {locale, messages} = config;
                const id = 'missing';

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages.invalid,
                })).toBe(messages.invalid);

                expect(consoleError.calls.length).toBe(3);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Missing message: "${id}" for locale: "${locale}", using default message as fallback.`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Error formatting the default message for: "${id}"`
                );
                expect(consoleError.calls[2].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message source as fallback.`
                );
            });

            it('returns message `id` when message and `defaultMessage` are missing', () => {
                const id = 'missing';

                expect(formatMessage({id: id})).toBe(id);

                expect(consoleError.calls.length).toBe(2);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Missing message: "${id}" for locale: "${config.locale}"`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message id as fallback.`
                );
            });

            it('returns message `id` when message and `defaultMessage` are empty', () => {
                const {locale, messages} = config;
                const id = 'empty';

                expect(formatMessage({
                    id: id,
                    defaultMessage: messages[id],
                })).toBe(id);

                expect(consoleError.calls.length).toBe(2);
                expect(consoleError.calls[0].arguments[0]).toContain(
                    `[Intl Format] Missing message: "${id}" for locale: "${locale}"`
                );
                expect(consoleError.calls[1].arguments[0]).toContain(
                    `[Intl Format] Cannot format message: "${id}", using message id as fallback.`
                );
            });
        });
    });

    describe('formatHTMLMessage()', () => {
        let formatHTMLMessage;

        beforeEach(() => {
            formatHTMLMessage = f.formatHTMLMessage.bind(null, config, state);
        });

        it('formats HTML messages', () => {
            const {locale, messages} = config;
            const mf = new IntlMessageFormat(messages.with_html, locale);
            const values = {name: 'Eric'};

            expect(formatHTMLMessage({id: 'with_html'}, values)).toBe(mf.format(values));
        });

        it('html-escapes string values', () => {
            const {locale, messages} = config;
            const mf = new IntlMessageFormat(messages.with_html, locale);
            const values = {name: '<i>Eric</i>'};
            const escapedValues = {name: '&lt;i&gt;Eric&lt;/i&gt;'};

            expect(formatHTMLMessage({id: 'with_html'}, values)).toBe(mf.format(escapedValues));
        });
    });
});