From 62afaddb2a24b9f848a858df45de8d6488c2136e Mon Sep 17 00:00:00 2001 From: Felix Mosheev <9304194+felixmosh@users.noreply.github.com> Date: Tue, 12 Sep 2023 22:48:25 +0300 Subject: [PATCH] Add Vite tests --- __tests__/vite/client-hmr.spec.js | 402 ++++++++++++++++++++++++++++++ lib/vite/client-hmr.js | 2 +- 2 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 __tests__/vite/client-hmr.spec.js diff --git a/__tests__/vite/client-hmr.spec.js b/__tests__/vite/client-hmr.spec.js new file mode 100644 index 0000000..7a49336 --- /dev/null +++ b/__tests__/vite/client-hmr.spec.js @@ -0,0 +1,402 @@ +global.mockImport = { + meta: { + hot: { + on: jest.fn(), + }, + }, +}; + +const applyClientHMR = require('../../lib/vite/client-hmr'); + +function whenHotTriggeredWith(changedFiles) { + const listenerCallback = mockImport.meta.hot.on.mock.calls[0][1]; + return listenerCallback({ changedFiles }); +} + +describe('client-hmr', () => { + let i18nMock; + let reloadError; + + beforeEach(() => { + reloadError = undefined; + + i18nMock = { + reloadResources: jest.fn().mockImplementation((_lang, _ns, callbackFn) => { + if (typeof callbackFn === 'function') { + callbackFn(reloadError); + } + return Promise.resolve(); + }), + changeLanguage: jest.fn(), + languages: ['en', 'de', 'en-US'], + }; + + mockImport.meta.hot.on.mockReset(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should register a listener on the key `i18next-hmr:locale-changed`', () => { + applyClientHMR(i18nMock); + expect(mockImport.meta.hot.on).toHaveBeenCalledWith( + 'i18next-hmr:locale-changed', + expect.any(Function) + ); + }); + + it('should warn regarding missing backend options once', () => { + jest.spyOn(global.console, 'warn'); + i18nMock.options = { ns: ['name-space'] }; + + applyClientHMR(i18nMock); + whenHotTriggeredWith(['en/name-space']); + + expect(global.console.warn).toHaveBeenCalledTimes(1); + expect(global.console.warn).toHaveBeenCalledWith( + expect.stringContaining('i18next-http-backend not found'), + expect.any(String), + expect.any(String) + ); + }); + + it('should use backend options from global options as cache killer param', () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + whenHotTriggeredWith(['en/name-space']); + + expect(i18nMock.options.backend).toHaveProperty('queryStringParams', { _: expect.any(Number) }); + }); + + it('should use backend options from services as cache killer param', () => { + i18nMock.services = { + ...i18nMock.services, + backendConnector: { backend: { options: {} } }, + }; + i18nMock.language = 'en'; + i18nMock.options = { ns: ['name-space'] }; + + applyClientHMR(i18nMock); + + whenHotTriggeredWith(['en/name-space']); + + expect(i18nMock.services.backendConnector.backend.options).toHaveProperty('queryStringParams', { + _: expect.any(Number), + }); + }); + + it('should trigger reload when translation file changed', async () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en'], + ['name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en'); + }); + + it('should trigger reload when i18n given as a getter function', async () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(() => i18nMock); + + await whenHotTriggeredWith(['en/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en'], + ['name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en'); + }); + + it('should pass changed filed to the i18next getter', () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + const getter = jest.fn().mockImplementation(() => i18nMock); + const changedFiles = ['en/name-space']; + + applyClientHMR(getter); + whenHotTriggeredWith(changedFiles); + + expect(getter).toHaveBeenCalledWith({ changedFiles }); + }); + + it('should trigger reload when lng-country combination file changed', () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en-US'; + + applyClientHMR(i18nMock); + + whenHotTriggeredWith(['en-US/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should trigger reload when translation file changed with nested namespace', async () => { + i18nMock.options = { backend: {}, ns: ['name-space', 'nested/name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en/nested/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en'], + ['nested/name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en'); + }); + + it('should trigger reload when translation file with backslashes (windows)', async () => { + i18nMock.options = { backend: {}, ns: ['name-space', 'nested/name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en\\nested\\name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en'], + ['nested/name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en'); + }); + + it('should not trigger changeLanguage when current lang is not the one that was edited', async () => { + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + i18nMock.languages.push('otherLang'); + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['otherLang/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['otherLang'], + ['name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).not.toHaveBeenCalled(); + }); + + it('should notify that reload resource failed', async () => { + jest.spyOn(global.console, 'error'); + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + reloadError = 'reload failed'; + + applyClientHMR(i18nMock); + await whenHotTriggeredWith(['en/name-space']); + + expect(i18nMock.changeLanguage).not.toHaveBeenCalled(); + expect(global.console.error).toHaveBeenCalledWith( + expect.stringContaining(reloadError), + expect.any(String), + expect.any(String) + ); + }); + + it('should ignore changes of none loaded namespace', async () => { + jest.spyOn(global.console, 'log'); + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en/none-loaded-ns']); + + expect(global.console.log).not.toHaveBeenCalledWith( + expect.stringContaining('Got an update with'), + expect.any(String) + ); + expect(i18nMock.reloadResources).not.toHaveBeenCalled(); + expect(i18nMock.changeLanguage).not.toHaveBeenCalled(); + }); + + it('should distinguish containing namespaces names', async () => { + jest.spyOn(global.console, 'log'); + i18nMock.options = { backend: {}, ns: ['name-space'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en/none-loaded-name-space']); + + expect(global.console.log).not.toHaveBeenCalledWith( + expect.stringContaining('Got an update with'), + expect.any(String) + ); + expect(i18nMock.reloadResources).not.toHaveBeenCalled(); + expect(i18nMock.changeLanguage).not.toHaveBeenCalled(); + }); + + it('should support fallbackNS as optional ns', async () => { + i18nMock.options = { + backend: {}, + ns: ['nested/name-space'], + fallbackNS: ['nested/fallback-name-space'], + }; + i18nMock.language = 'en-US'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['nested/fallback-name-space/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['nested/fallback-name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should support defaultNS as optional ns', async () => { + i18nMock.options = { + backend: {}, + ns: [], + defaultNS: ['common'], + }; + i18nMock.language = 'en-US'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['common/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['common'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should support complex localePath {{ns}}/locales/{{lng}}.json', async () => { + i18nMock.options = { backend: {}, ns: ['nested/name-space'] }; + i18nMock.language = 'en-US'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['nested/name-space/locales/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['nested/name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should support options.supportedLngs as a language source', async () => { + i18nMock.options = { + backend: {}, + ns: ['nested/name-space'], + fallbackNS: ['nested/fallback-name-space'], + supportedLngs: ['en-US'], + }; + i18nMock.language = 'en-US'; + i18nMock.languages = []; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['nested/fallback-name-space/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['nested/fallback-name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should support options.lng as a language source', async () => { + i18nMock.options = { + backend: {}, + ns: ['nested/name-space'], + fallbackNS: ['nested/fallback-name-space'], + lng: 'en-US', + }; + i18nMock.language = 'en-US'; + i18nMock.languages = []; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['nested/fallback-name-space/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['nested/fallback-name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + it('should support options.fallbackLng as a language source', async () => { + i18nMock.options = { + backend: {}, + ns: ['nested/name-space'], + fallbackNS: ['nested/fallback-name-space'], + fallbackLng: 'en-US', + }; + i18nMock.language = 'en-US'; + i18nMock.languages = []; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['nested/fallback-name-space/en-US']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en-US'], + ['nested/fallback-name-space'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalledWith('en-US'); + }); + + describe('multiple files', () => { + it('should support change of multiple files', async () => { + i18nMock.options = { backend: {}, ns: ['name-space', 'name-space2'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['en/name-space', 'en/name-space2', 'de/name-space']); + + expect(i18nMock.reloadResources).toHaveBeenCalledWith( + ['en', 'de'], + ['name-space', 'name-space2'], + expect.any(Function) + ); + expect(i18nMock.changeLanguage).toHaveBeenCalled(); + }); + + it('should not trigger `changeLanguage` when modified files are not related to the current language', async () => { + i18nMock.options = { backend: {}, ns: ['name-space', 'name-space2'] }; + i18nMock.language = 'en'; + + applyClientHMR(i18nMock); + + await whenHotTriggeredWith(['de/name-space', 'de/name-space2']); + + expect(i18nMock.changeLanguage).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/lib/vite/client-hmr.js b/lib/vite/client-hmr.js index b3cc6a3..1557a73 100644 --- a/lib/vite/client-hmr.js +++ b/lib/vite/client-hmr.js @@ -1,4 +1,4 @@ -const { extractList, printList, reloadTranslations, log } = require('./utils'); +const { extractList, printList, reloadTranslations, log } = require('../utils'); module.exports = function applyViteClientHMR(i18nOrGetter) { if (import.meta.hot) {