src/i18n/__tests__/I18n.spec.js

Summary

Maintainability
C
7 hrs
Test Coverage
import MockApi from '../../api/Api'
import I18n from '../I18n'

jest.mock('../../api/Api')

describe('Internationalisation (I18n)', () => {
    let mockApi
    let i18n

    const mockTranslations = {
        general_settings: 'General settings',
        yes: 'Yup',
        no: 'Nope',
        system_settings_in_french: 'Paramètres du système',
        // 'escapes': 'Characters may be escaped! Even\nnewlines?!?',
        string_with_variable: 'Some times $$variable$$ are useful',
    }

    const mockUnicode = 'Param\\u00e8tres du syst\\u00e8me'
    const mockEscape =
        'Characters\\ may \\b\\e \\e\\s\\c\\a\\p\\e\\d\\!\\\\ Even\\\nnewline\\s\\?\\!\\?'
    const mockPropsFile =
        `${
            'general_settings=General settings\n' +
            'yes=Yup\n' +
            'no=Nope\n\n# Blank lines and commends - ignored?\n#\n\n' +
            'system_settings_in_french='
        }${mockUnicode}\n` + `escapes=${mockEscape}\n`

    beforeEach(() => {
        mockApi = MockApi.getApi()
        i18n = new I18n()
    })

    afterEach(() => {
        MockApi.mockReset()
    })

    it('should not be allowed to be called without new', () => {
        expect(() => I18n()).toThrowErrorMatchingSnapshot()
    })

    it('should set an instance of Api onto the SystemConfiguration instance', () => {
        expect(i18n.api).toBe(mockApi)
    })

    it('addSource() should be a function', () => {
        expect(i18n.addSource).toBeInstanceOf(Function)
    })

    it('addStrings() should be a function', () => {
        expect(i18n.addStrings).toBeInstanceOf(Function)
    })

    it('load() should be a function', () => {
        expect(i18n.load).toBeInstanceOf(Function)
    })

    it('should set the passed sources onto the object', () => {
        const sources = ['translation_18n']

        i18n = new I18n(sources)

        expect(i18n.sources).toBe(sources)
    })

    it('should use the passed Api object', () => {
        mockApi = jest.fn()

        i18n = new I18n([], mockApi)

        expect(i18n.api).toBe(mockApi)
    })

    it("getTranslations() should throw an error is translations haven't been loaded yet", () => {
        expect.assertions(1)

        try {
            i18n.getTranslation('some_string')
        } catch (err) {
            expect(err.message).toMatchSnapshot()
        }
    })

    describe('getI18n', () => {
        it('should be a function on the I18n class', () => {
            expect(typeof I18n.getI18n).toBe('function')
        })

        it('should return a new instanceof I18n', () => {
            expect(I18n.getI18n()).toBeInstanceOf(I18n)
        })
    })

    describe('addStrings()', () => {
        it('accepts a single string', () => {
            i18n.addStrings('yes')
            const strings = Array.from(i18n.strings)
            expect(strings).toContain('yes')
            expect(strings.length).toBe(1)
        })

        it('accepts an array of strings', () => {
            i18n.addStrings(['yes', 'no', 'maybe'])
            const strings = Array.from(i18n.strings)
            expect(strings).toContain('yes')
            expect(strings).toContain('no')
            expect(strings).toContain('maybe')
            expect(strings.length).toBe(3)
        })

        it('handles consequtive calls', () => {
            i18n.addStrings(['yes', 'no'])
            i18n.addStrings('maybe')
            i18n.addStrings('probably')
            const strings = Array.from(i18n.strings)
            expect(strings).toContain('yes')
            expect(strings).toContain('no')
            expect(strings).toContain('maybe')
            expect(strings).toContain('probably')
            expect(strings.length).toBe(4)
        })

        it("doesn't add duplicates", () => {
            i18n.addStrings(['yes', 'no'])
            i18n.addStrings(['no', 'maybe'])
            i18n.addStrings(['maybe', 'probably', 'yes'])
            const strings = Array.from(i18n.strings)
            expect(strings).toContain('yes')
            expect(strings).toContain('no')
            expect(strings).toContain('maybe')
            expect(strings).toContain('probably')
            expect(strings.length).toBe(4)
        })

        it('should not add empty strings', () => {
            jest.spyOn(i18n.strings, 'add')

            i18n.addStrings(['yes', '', '  '])

            expect(i18n.strings.add).toHaveBeenCalledTimes(1)
        })
    })

    describe('load()', () => {
        let apiGet
        let apiPost
        let apiReq

        beforeEach(() => {
            apiGet = mockApi.get.mockReturnValueOnce(
                Promise.resolve(mockTranslations)
            )

            apiPost = mockApi.post.mockReturnValueOnce(
                Promise.resolve(mockTranslations)
            )

            apiReq = mockApi.request.mockReturnValueOnce(
                Promise.resolve(mockPropsFile)
            )

            i18n.addStrings(['yes', 'no'])
        })

        it('should return a promise', () => {
            expect(i18n.load()).toBeInstanceOf(Promise)
        })

        it('should POST to get untranslated strings', () => {
            expect.assertions(4)

            return i18n.load().then(() => {
                expect(apiGet).toHaveBeenCalledTimes(0)
                expect(apiPost).toHaveBeenCalledTimes(1)
                expect(apiReq).toHaveBeenCalledTimes(0)
                expect(i18n.getTranslation('yes')).toEqual(mockTranslations.yes)
            })
        })

        it('should load props files first', () => {
            i18n.addSource('props_file_name')

            expect.assertions(3)

            return i18n.load().then(() => {
                expect(apiGet).toHaveBeenCalledTimes(0)
                expect(apiPost).toHaveBeenCalledTimes(0)
                expect(apiReq).toHaveBeenCalledTimes(1)
            })
        })

        it('keeps going if one props file fails', () => {
            i18n.addSource('props_file_one')
            i18n.addSource('props_file_two')
            i18n.addSource('props_file_three')

            apiReq.mockReset()
            apiReq
                .mockReturnValueOnce(Promise.resolve(mockPropsFile))
                .mockReturnValueOnce(Promise.reject('404 Fail or something'))
                .mockReturnValueOnce(Promise.resolve(''))

            expect.assertions(3)

            return i18n.load().then(() => {
                expect(apiGet).toHaveBeenCalledTimes(0)
                expect(apiPost).toHaveBeenCalledTimes(0)
                expect(apiReq).toHaveBeenCalledTimes(3)
            })
        })

        it('chooses strings based on source order', () => {
            i18n.addSource('slow_props_file')
            i18n.addSource('fast_props_file')

            apiReq.mockReset()
            apiReq
                .mockReturnValueOnce(
                    new Promise((resolve) => {
                        setTimeout(() => {
                            resolve('result=first priority file\n')
                        })
                    })
                )
                .mockReturnValueOnce(
                    Promise.resolve('result=first file to load\n')
                )

            expect.assertions(1)

            return i18n.load().then(() => {
                expect(i18n.getTranslation('result')).toEqual(
                    'first priority file'
                )
            })
        })

        it('should not add the strings if no responses were returned', () => {
            i18n.addStrings(['string_that_has_no_translation'])
            mockApi.post.mockReturnValueOnce(
                Promise.resolve({
                    string_that_has_no_translation:
                        'string_that_has_no_translation',
                })
            )

            return i18n
                .load()
                .then(() =>
                    expect(
                        i18n.translations.string_that_has_no_translation
                    ).toBeUndefined()
                )
        })
    })

    describe('async API', () => {
        beforeEach(() => {
            i18n.api.get.mockReturnValue(Promise.resolve(mockTranslations))

            i18n.api.request.mockReturnValue(Promise.resolve(mockPropsFile))

            i18n.api.post.mockReturnValue(Promise.resolve(mockTranslations))

            i18n.addSource('mockPropsFile')
            i18n.addStrings(Object.keys(mockTranslations))
        })

        describe('getTranslation()', () => {
            it('returns the correct translations', () => {
                expect.assertions(5)

                return i18n.load().then(() => {
                    Object.keys(mockTranslations).forEach((key) => {
                        expect(i18n.getTranslation(key)).toEqual(
                            mockTranslations[key]
                        )
                    })
                })
            })

            it('decodes unicode entities from properties files', () =>
                i18n.load().then(() => {
                    expect(mockApi.get).toHaveBeenCalledTimes(0)
                    expect(mockApi.post).toHaveBeenCalledTimes(1)
                    expect(mockApi.request).toHaveBeenCalledTimes(1)
                    expect(
                        i18n.getTranslation('system_settings_in_french')
                    ).toEqual(mockTranslations.system_settings_in_french)
                    expect(
                        i18n.getTranslation('system_settings_in_french')
                    ).not.toEqual(mockUnicode)
                }))

            it('returns ** string ** for unknown strings', () => {
                expect.assertions(1)

                return i18n.load().then(() => {
                    expect(i18n.getTranslation('string')).toEqual(
                        '** string **'
                    )
                })
            })

            it('replaces $$variable$$ in translations', () => {
                expect.assertions(2)

                return i18n.load().then(() => {
                    const sub1 = i18n.getTranslation('string_with_variable', {
                        variable: 'tests',
                    })
                    const sub2 = i18n.getTranslation('string_with_variable', {
                        variable: 'FUNNY TRANSLATIONS',
                    })

                    expect(sub1).toBe('Some times tests are useful')
                    expect(sub2).toBe(
                        'Some times FUNNY TRANSLATIONS are useful'
                    )
                })
            })
        })

        describe('isTranslated()', () => {
            it('returns true for translated strings', () => {
                expect.assertions(5)

                return i18n.load().then(() => {
                    Object.keys(mockTranslations).forEach((key) => {
                        expect(i18n.isTranslated(key)).toEqual(true)
                    })
                })
            })

            it('returns false for untranslated strings', () => {
                expect.assertions(1)

                return i18n.load().then(() => {
                    expect(i18n.isTranslated('string')).toEqual(false)
                })
            })

            it("totally tilts out if translations haven't been loaded yet", () => {
                expect(() =>
                    i18n.isTranslated('some random string')
                ).toThrowErrorMatchingSnapshot()
            })
        })

        describe('getUntranslatedStrings()', () => {
            it("returns undefined if translations haven't been loaded yet", () => {
                expect(i18n.getUntranslatedStrings()).toEqual(undefined)
            })

            it('returns an array', () => {
                expect.assertions(1)

                return i18n.load().then(() => {
                    expect(i18n.getUntranslatedStrings()).toBeInstanceOf(Array)
                })
            })

            it("doesn't return translated strings", () => {
                i18n.addStrings('string')

                return i18n.load().then(() => {
                    const str = i18n.getUntranslatedStrings()
                    expect(str).toContain('string')
                    expect(str).not.toContain('yes')
                    expect(str).not.toContain('some_random_string')
                })
            })
        })
    })
})