diff --git a/packages/fuselage-ui-kit/jest.config.ts b/packages/fuselage-ui-kit/jest.config.ts new file mode 100644 index 0000000000000..23a14f54fde9f --- /dev/null +++ b/packages/fuselage-ui-kit/jest.config.ts @@ -0,0 +1,27 @@ +export default { + preset: 'ts-jest', + errorOnDeprecated: true, + testEnvironment: 'jsdom', + modulePathIgnorePatterns: ['/dist/'], + transform: { + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + sourceMaps: true, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: false, + dynamicImport: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 216efd02ef542..9279489216594 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -32,6 +32,7 @@ ".:build:clean": "rimraf dist", ".:build:esm": "tsc -p tsconfig-esm.json", ".:build:cjs": "tsc -p tsconfig-cjs.json", + "test": "jest", "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "typecheck": "tsc --noEmit", "docs": "cross-env NODE_ENV=production build-storybook -o ../../static/fuselage-ui-kit", @@ -81,6 +82,8 @@ "@storybook/source-loader": "~6.5.16", "@storybook/theming": "~6.5.16", "@tanstack/react-query": "^4.16.1", + "@testing-library/react": "^14.2.2", + "@testing-library/react-hooks": "^8.0.1", "@types/babel__core": "^7.20.3", "@types/babel__preset-env": "^7.9.4", "@types/react": "~17.0.69", @@ -88,14 +91,17 @@ "babel-loader": "~8.2.5", "cross-env": "^7.0.3", "eslint": "~8.45.0", + "i18next": "^23.10.1", + "jest": "^29.7.0", "normalize.css": "^8.0.1", "npm-run-all": "^4.1.5", "prettier": "~2.8.8", "react-docgen-typescript-plugin": "~1.0.5", "react-dom": "^17.0.2", - "react-i18next": "~13.2.2", + "react-i18next": "^14.1.0", "rimraf": "^3.0.2", "storybook-dark-mode": "~3.0.1", + "ts-jest": "^29.1.2", "tslib": "^2.5.3", "typescript": "~5.3.3" }, diff --git a/packages/fuselage-ui-kit/src/hooks/useAppTranslation.spec.tsx b/packages/fuselage-ui-kit/src/hooks/useAppTranslation.spec.tsx new file mode 100644 index 0000000000000..fe84b1ffb7718 --- /dev/null +++ b/packages/fuselage-ui-kit/src/hooks/useAppTranslation.spec.tsx @@ -0,0 +1,139 @@ +import { renderHook } from '@testing-library/react-hooks'; +import * as i18next from 'i18next'; +import type { FunctionComponent } from 'react'; +import { Suspense } from 'react'; +import { I18nextProvider, initReactI18next } from 'react-i18next'; + +import { AppIdProvider } from '../contexts/AppIdContext'; +import { useAppTranslation } from './useAppTranslation'; + +let i18n: i18next.i18n; + +beforeEach(async () => { + i18n = i18next.createInstance().use(initReactI18next); + + await i18n.init({ + lng: 'en', + resources: { + en: { + 'translation': { + test: 'a quick brown fox', + }, + 'app-test': { + test: 'jumped over the lazy dog', + }, + 'app-test-core': { + test: 'this should not be used', + }, + }, + }, + }); +}); + +it('should work with normal app ID (`test`)', async () => { + const { result } = renderHook(() => useAppTranslation().t('test'), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe('jumped over the lazy dog'); +}); + +it('should work with core app ID (`test-core`)', async () => { + const { result } = renderHook(() => useAppTranslation().t('test'), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe('a quick brown fox'); +}); + +describe('with suspense', () => { + let i18n: i18next.i18n; + + beforeEach(async () => { + i18n = i18next + .createInstance() + .use({ + type: 'backend', + init: () => undefined, + read: async (language, namespace) => { + await new Promise((resolve) => queueMicrotask(resolve)); + + if (language === 'en' && namespace === 'core') { + return { + test: 'a quick brown fox', + }; + } + + if (language === 'en' && namespace === 'app-test') { + return { + test: 'jumped over the lazy dog', + }; + } + + throw new Error(`Unexpected read request: ${language} ${namespace}`); + }, + } satisfies i18next.BackendModule) + .use(initReactI18next); + + await i18n.init({ + lng: 'en', + defaultNS: 'core', + partialBundledLanguages: true, + react: { + useSuspense: true, + }, + }); + }); + + it('should work with normal app ID (`test`)', async () => { + const FakeFallback: FunctionComponent = jest.fn(() => null); + + const { result, waitForNextUpdate } = renderHook( + () => useAppTranslation().t('test'), + { + wrapper: ({ children }) => ( + + }> + {children} + + + ), + } + ); + + await waitForNextUpdate(); + + expect(result.current).toBe('jumped over the lazy dog'); + expect(FakeFallback).toHaveBeenCalled(); + }); + + it('should work with core app ID (`test-core`)', async () => { + const FakeFallback: FunctionComponent = jest.fn(() => null); + + const { result, waitForNextUpdate } = renderHook( + () => useAppTranslation().t('test'), + { + wrapper: ({ children }) => ( + + }> + {children} + + + ), + } + ); + + await waitForNextUpdate(); + + expect(result.current).toBe('a quick brown fox'); + expect(FakeFallback).toHaveBeenCalled(); + }); +}); diff --git a/packages/fuselage-ui-kit/src/hooks/useAppTranslation.ts b/packages/fuselage-ui-kit/src/hooks/useAppTranslation.ts index c29cf09533864..5925f2fada10f 100644 --- a/packages/fuselage-ui-kit/src/hooks/useAppTranslation.ts +++ b/packages/fuselage-ui-kit/src/hooks/useAppTranslation.ts @@ -5,7 +5,7 @@ import { useAppId } from '../contexts/AppIdContext'; export const useAppTranslation = () => { const appId = useAppId(); - const appNs = `app-${appId}`; + const appNs = appId.endsWith(`-core`) ? undefined : `app-${appId}`; useDebugValue(appNs); diff --git a/yarn.lock b/yarn.lock index 8993ef8f84cb5..d2a910b75b9b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2946,6 +2946,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9": + version: 7.24.4 + resolution: "@babel/runtime@npm:7.24.4" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 2f27d4c0ffac7ae7999ac0385e1106f2a06992a8bdcbf3da06adcac7413863cd08c198c2e4e970041bbea849e17f02e1df18875539b6afba76c781b6b59a07c3 + languageName: node + linkType: hard + "@babel/runtime@npm:~7.22.15": version: 7.22.15 resolution: "@babel/runtime@npm:7.22.15" @@ -8751,6 +8760,8 @@ __metadata: "@storybook/source-loader": ~6.5.16 "@storybook/theming": ~6.5.16 "@tanstack/react-query": ^4.16.1 + "@testing-library/react": ^14.2.2 + "@testing-library/react-hooks": ^8.0.1 "@types/babel__core": ^7.20.3 "@types/babel__preset-env": ^7.9.4 "@types/react": ~17.0.69 @@ -8758,14 +8769,17 @@ __metadata: babel-loader: ~8.2.5 cross-env: ^7.0.3 eslint: ~8.45.0 + i18next: ^23.10.1 + jest: ^29.7.0 normalize.css: ^8.0.1 npm-run-all: ^4.1.5 prettier: ~2.8.8 react-docgen-typescript-plugin: ~1.0.5 react-dom: ^17.0.2 - react-i18next: ~13.2.2 + react-i18next: ^14.1.0 rimraf: ^3.0.2 storybook-dark-mode: ~3.0.1 + ts-jest: ^29.1.2 tslib: ^2.5.3 typescript: ~5.3.3 peerDependencies: @@ -12546,6 +12560,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.2.2": + version: 14.2.2 + resolution: "@testing-library/react@npm:14.2.2" + dependencies: + "@babel/runtime": ^7.12.5 + "@testing-library/dom": ^9.0.0 + "@types/react-dom": ^18.0.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: cb73df588592d9101429f057eaa6f320fc12524d5eb2acc8a16002c1ee2d9422a49e44841003bba42974c9ae1ced6b134f0d647826eca42ab8f19e4592971b16 + languageName: node + linkType: hard + "@testing-library/user-event@npm:^13.2.1, @testing-library/user-event@npm:~13.5.0": version: 13.5.0 resolution: "@testing-library/user-event@npm:13.5.0" @@ -25592,6 +25620,15 @@ __metadata: languageName: node linkType: hard +"i18next@npm:^23.10.1": + version: 23.10.1 + resolution: "i18next@npm:23.10.1" + dependencies: + "@babel/runtime": ^7.23.2 + checksum: 4aec10ddb0bb841f15b9b023daa59977052bc706ca4e94643b12b17640731862bde596c9797491638f6d9e7f125722ea9b1e87394c7aebbb72f45c20396f79d9 + languageName: node + linkType: hard + "i18next@npm:~21.6.16": version: 21.6.16 resolution: "i18next@npm:21.6.16" @@ -27313,7 +27350,7 @@ __metadata: languageName: node linkType: hard -"jest-cli@npm:^29.5.0, jest-cli@npm:^29.6.4": +"jest-cli@npm:^29.5.0, jest-cli@npm:^29.6.4, jest-cli@npm:^29.7.0": version: 29.7.0 resolution: "jest-cli@npm:29.7.0" dependencies: @@ -27842,6 +27879,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": ^29.7.0 + "@jest/types": ^29.6.3 + import-local: ^3.0.2 + jest-cli: ^29.7.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 17ca8d67504a7dbb1998cf3c3077ec9031ba3eb512da8d71cb91bcabb2b8995c4e4b292b740cb9bf1cbff5ce3e110b3f7c777b0cefb6f41ab05445f248d0ee0b + languageName: node + linkType: hard + "jest@npm:~29.5.0": version: 29.5.0 resolution: "jest@npm:29.5.0" @@ -34903,6 +34959,24 @@ __metadata: languageName: node linkType: hard +"react-i18next@npm:^14.1.0": + version: 14.1.0 + resolution: "react-i18next@npm:14.1.0" + dependencies: + "@babel/runtime": ^7.23.9 + html-parse-stringify: ^3.0.1 + peerDependencies: + i18next: ">= 23.2.3" + react: ">= 16.8.0" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 96fbc4b0919b9f0c639f9f3eb35eecac528174aa97e3b1af469cfdbff4b34e40ae8969c26c0b737691a5fa9b56bb13093524cfca79b93cbd58f1319530da31b2 + languageName: node + linkType: hard + "react-i18next@npm:~13.2.2": version: 13.2.2 resolution: "react-i18next@npm:13.2.2"