diff --git a/.eslintrc b/.eslintrc index fc7ed5bddb2..495469ce751 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,9 +5,6 @@ "browser": true, "commonjs": true }, - "globals": { - "expect": true - }, "rules": { "no-restricted-syntax": [ "error", @@ -26,9 +23,19 @@ }, "overrides": [ { - "files": "spec/javascripts/**/*", + "files": ["*.spec.*", "*-spec.*", "*_spec.*", "spec/**"], + "globals": { + "expect": true + }, "rules": { - "react/jsx-props-no-spreading": "off" + "react/jsx-props-no-spreading": "off", + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true, + "packageDir": "." + } + ] } } ] diff --git a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx index 62197457c70..421205d75db 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx @@ -1,7 +1,7 @@ import { useContext, useMemo, useEffect, useRef, useState } from 'react'; +import { useI18n } from '@18f/identity-react-i18n'; import AcuantContext from '../context/acuant'; import useAsset from '../hooks/use-asset'; -import useI18n from '../hooks/use-i18n'; import useInstanceId from '../hooks/use-instance-id'; import useImmutableCallback from '../hooks/use-immutable-callback'; diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index ecd02c65676..54dddca2561 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -7,13 +7,13 @@ import { useEffect, useImperativeHandle, } from 'react'; +import { useI18n } from '@18f/identity-react-i18n'; import AnalyticsContext from '../context/analytics'; import AcuantContext from '../context/acuant'; import AcuantCaptureCanvas from './acuant-capture-canvas'; import FileInput from './file-input'; import FullScreen from './full-screen'; import Button from './button'; -import useI18n from '../hooks/use-i18n'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import useIfStillMounted from '../hooks/use-if-still-mounted'; diff --git a/app/javascript/packages/document-capture/components/block-link.jsx b/app/javascript/packages/document-capture/components/block-link.jsx index 439e383ff90..ce27360d70e 100644 --- a/app/javascript/packages/document-capture/components/block-link.jsx +++ b/app/javascript/packages/document-capture/components/block-link.jsx @@ -1,4 +1,4 @@ -import useI18n from '../hooks/use-i18n'; +import { useI18n } from '@18f/identity-react-i18n'; /** @typedef {import('react').ReactNode} ReactNode */ diff --git a/app/javascript/packages/document-capture/components/desktop-document-disclosure.jsx b/app/javascript/packages/document-capture/components/desktop-document-disclosure.jsx index b902cca10b7..9df6ee1c151 100644 --- a/app/javascript/packages/document-capture/components/desktop-document-disclosure.jsx +++ b/app/javascript/packages/document-capture/components/desktop-document-disclosure.jsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; +import { useI18n } from '@18f/identity-react-i18n'; import DeviceContext from '../context/device'; -import useI18n from '../hooks/use-i18n'; /** * Renders a document usage disclosure for desktop devices only. On mobile devices, an equivalent diff --git a/app/javascript/packages/document-capture/components/document-capture.jsx b/app/javascript/packages/document-capture/components/document-capture.jsx index ebfe00c77fb..26bdf01672a 100644 --- a/app/javascript/packages/document-capture/components/document-capture.jsx +++ b/app/javascript/packages/document-capture/components/document-capture.jsx @@ -1,5 +1,6 @@ import { useState, useMemo, useContext } from 'react'; import { Alert } from '@18f/identity-components'; +import { useI18n } from '@18f/identity-react-i18n'; import FormSteps from './form-steps'; import { UploadFormEntriesError } from '../services/upload'; import DocumentsStep, { documentsStepValidator } from './documents-step'; @@ -9,7 +10,6 @@ import ServiceProviderContext from '../context/service-provider'; import Submission from './submission'; import SubmissionStatus from './submission-status'; import DesktopDocumentDisclosure from './desktop-document-disclosure'; -import useI18n from '../hooks/use-i18n'; import { RetrySubmissionError } from './submission-complete'; import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload'; import SuspenseErrorBoundary from './suspense-error-boundary'; diff --git a/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx b/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx index a68b5cd3c14..c19f5897c92 100644 --- a/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx @@ -1,6 +1,6 @@ +import { useI18n } from '@18f/identity-react-i18n'; import AcuantCapture from './acuant-capture'; import FormErrorMessage, { CameraAccessDeclinedError } from './form-error-message'; -import useI18n from '../hooks/use-i18n'; /** @typedef {import('./form-steps').FormStepError<*>} FormStepError */ /** @typedef {import('./form-steps').RegisterFieldCallback} RegisterFieldCallback */ diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx index 62c2e5c7a09..2b1f5d635fa 100644 --- a/app/javascript/packages/document-capture/components/documents-step.jsx +++ b/app/javascript/packages/document-capture/components/documents-step.jsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; +import { useI18n } from '@18f/identity-react-i18n'; import BlockLink from './block-link'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; -import useI18n from '../hooks/use-i18n'; import DeviceContext from '../context/device'; import ServiceProviderContext from '../context/service-provider'; import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload'; diff --git a/app/javascript/packages/document-capture/components/file-input.jsx b/app/javascript/packages/document-capture/components/file-input.jsx index c7ceea03ec8..00a13acdd5d 100644 --- a/app/javascript/packages/document-capture/components/file-input.jsx +++ b/app/javascript/packages/document-capture/components/file-input.jsx @@ -7,10 +7,10 @@ import { useRef, useImperativeHandle, } from 'react'; +import { useI18n } from '@18f/identity-react-i18n'; import FileImage from './file-image'; import DeviceContext from '../context/device'; import useInstanceId from '../hooks/use-instance-id'; -import useI18n from '../hooks/use-i18n'; import usePrevious from '../hooks/use-previous'; /** @typedef {import('react').MouseEvent} ReactMouseEvent */ diff --git a/app/javascript/packages/document-capture/components/form-error-message.jsx b/app/javascript/packages/document-capture/components/form-error-message.jsx index c8f0ff465fd..96acec110dc 100644 --- a/app/javascript/packages/document-capture/components/form-error-message.jsx +++ b/app/javascript/packages/document-capture/components/form-error-message.jsx @@ -1,6 +1,6 @@ +import { useI18n } from '@18f/identity-react-i18n'; import { UploadFormEntryError } from '../services/upload'; import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload'; -import useI18n from '../hooks/use-i18n'; /** @typedef {import('react').ReactNode} ReactNode */ diff --git a/app/javascript/packages/document-capture/components/form-steps.jsx b/app/javascript/packages/document-capture/components/form-steps.jsx index b029c796fa5..40d06dbfba4 100644 --- a/app/javascript/packages/document-capture/components/form-steps.jsx +++ b/app/javascript/packages/document-capture/components/form-steps.jsx @@ -1,10 +1,10 @@ import { useEffect, useRef, useState } from 'react'; import { Alert } from '@18f/identity-components'; +import { useI18n } from '@18f/identity-react-i18n'; import Button from './button'; import PageHeading from './page-heading'; import FormErrorMessage, { RequiredValueMissingError } from './form-error-message'; import PromptOnNavigate from './prompt-on-navigate'; -import useI18n from '../hooks/use-i18n'; import useHistoryParam from '../hooks/use-history-param'; import useForceRender from '../hooks/use-force-render'; import useDidUpdateEffect from '../hooks/use-did-update-effect'; diff --git a/app/javascript/packages/document-capture/components/full-screen.jsx b/app/javascript/packages/document-capture/components/full-screen.jsx index 20410e03627..d4aedd14c46 100644 --- a/app/javascript/packages/document-capture/components/full-screen.jsx +++ b/app/javascript/packages/document-capture/components/full-screen.jsx @@ -1,6 +1,6 @@ import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import useI18n from '../hooks/use-i18n'; +import { useI18n } from '@18f/identity-react-i18n'; import useAsset from '../hooks/use-asset'; import useToggleBodyClassByPresence from '../hooks/use-toggle-body-class-by-presence'; import useImmutableCallback from '../hooks/use-immutable-callback'; diff --git a/app/javascript/packages/document-capture/components/review-issues-step.jsx b/app/javascript/packages/document-capture/components/review-issues-step.jsx index ed401c9322e..d0b5fe7964c 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.jsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.jsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { hasMediaAccess } from '@18f/identity-device'; -import useI18n from '../hooks/use-i18n'; +import { useI18n } from '@18f/identity-react-i18n'; import DeviceContext from '../context/device'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; import AcuantCapture from './acuant-capture'; diff --git a/app/javascript/packages/document-capture/components/selfie-capture.jsx b/app/javascript/packages/document-capture/components/selfie-capture.jsx index 9c51902a984..60ca20fd71f 100644 --- a/app/javascript/packages/document-capture/components/selfie-capture.jsx +++ b/app/javascript/packages/document-capture/components/selfie-capture.jsx @@ -8,9 +8,9 @@ import { useImperativeHandle, } from 'react'; import { Icon } from '@18f/identity-components'; +import { useI18n } from '@18f/identity-react-i18n'; import FileImage from './file-image'; import useIfStillMounted from '../hooks/use-if-still-mounted'; -import useI18n from '../hooks/use-i18n'; import useInstanceId from '../hooks/use-instance-id'; import useFocusFallbackRef from '../hooks/use-focus-fallback-ref'; import './selfie-capture.scss'; diff --git a/app/javascript/packages/document-capture/components/selfie-step.jsx b/app/javascript/packages/document-capture/components/selfie-step.jsx index 349d4df3633..4e240f4d185 100644 --- a/app/javascript/packages/document-capture/components/selfie-step.jsx +++ b/app/javascript/packages/document-capture/components/selfie-step.jsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { hasMediaAccess } from '@18f/identity-device'; -import useI18n from '../hooks/use-i18n'; +import { useI18n } from '@18f/identity-react-i18n'; import DeviceContext from '../context/device'; import AcuantCapture from './acuant-capture'; import SelfieCapture from './selfie-capture'; diff --git a/app/javascript/packages/document-capture/components/submission-interstitial.jsx b/app/javascript/packages/document-capture/components/submission-interstitial.jsx index da69a5c2f5b..cc8b690dc69 100644 --- a/app/javascript/packages/document-capture/components/submission-interstitial.jsx +++ b/app/javascript/packages/document-capture/components/submission-interstitial.jsx @@ -1,5 +1,5 @@ import { useRef, useEffect } from 'react'; -import useI18n from '../hooks/use-i18n'; +import { useI18n } from '@18f/identity-react-i18n'; import useAsset from '../hooks/use-asset'; import PageHeading from './page-heading'; diff --git a/app/javascript/packages/document-capture/context/i18n.js b/app/javascript/packages/document-capture/context/i18n.js deleted file mode 100644 index 7c4c52c8836..00000000000 --- a/app/javascript/packages/document-capture/context/i18n.js +++ /dev/null @@ -1,7 +0,0 @@ -import { createContext } from 'react'; - -const I18nContext = createContext({}); - -I18nContext.displayName = 'I18nContext'; - -export default I18nContext; diff --git a/app/javascript/packages/document-capture/context/index.js b/app/javascript/packages/document-capture/context/index.js index 82c04af63a5..ebb651bca38 100644 --- a/app/javascript/packages/document-capture/context/index.js +++ b/app/javascript/packages/document-capture/context/index.js @@ -1,5 +1,4 @@ export { default as AssetContext } from './asset'; -export { default as I18nContext } from './i18n'; export { default as DeviceContext } from './device'; export { default as AcuantContext, Provider as AcuantContextProvider } from './acuant'; export { default as UploadContext, Provider as UploadContextProvider } from './upload'; diff --git a/app/javascript/packages/document-capture/hooks/use-i18n.js b/app/javascript/packages/react-i18n/index.js similarity index 89% rename from app/javascript/packages/document-capture/hooks/use-i18n.js rename to app/javascript/packages/react-i18n/index.js index d46080b767d..ee91fe18c73 100644 --- a/app/javascript/packages/document-capture/hooks/use-i18n.js +++ b/app/javascript/packages/react-i18n/index.js @@ -1,9 +1,12 @@ -import { createElement, cloneElement, useContext, useMemo } from 'react'; +import { createElement, cloneElement, createContext, useContext, useMemo } from 'react'; import { I18n } from '@18f/identity-i18n'; -import I18nContext from '../context/i18n'; /** @typedef {import('react').FC|import('react').ComponentClass} Component */ +export const I18nContext = createContext({}); + +I18nContext.displayName = 'I18nContext'; + /** * Given an HTML string and an object of tag names to React component, returns a new React node * where the mapped tag names are replaced by the resulting element of the rendered component. @@ -49,14 +52,9 @@ export function formatHTML(html, handlers) { return parts.filter(Boolean); } -function useI18n() { +export function useI18n() { const strings = useContext(I18nContext); const { t } = useMemo(() => new I18n({ strings }), [strings]); - return { - t, - formatHTML, - }; + return { t, formatHTML }; } - -export default useI18n; diff --git a/app/javascript/packages/react-i18n/index.spec.jsx b/app/javascript/packages/react-i18n/index.spec.jsx new file mode 100644 index 00000000000..9a8a1e622bc --- /dev/null +++ b/app/javascript/packages/react-i18n/index.spec.jsx @@ -0,0 +1,110 @@ +import { useContext } from 'react'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { I18nContext, useI18n } from './index.js'; + +describe('I18nContext', () => { + it('defaults to empty object', () => { + const { result } = renderHook(() => useContext(I18nContext)); + + expect(result.current).to.deep.equal({}); + }); +}); + +describe('useI18n', () => { + describe('formatHTML', () => { + let formatHTML; + before(() => { + const { result } = renderHook(() => useI18n()); + formatHTML = result.current.formatHTML; + }); + + it('returns html string treated as escaped text without handler', () => { + const formatted = formatHTML('Hello world!', {}); + + const { container } = render(formatted); + + expect(container.innerHTML).to.equal('Hello <strong>world</strong>!'); + }); + + it('returns html string chunked by component handlers', () => { + const formatted = formatHTML('Hello world!', { + strong: ({ children }) => {children}, + }); + + const { container } = render(formatted); + + expect(container.innerHTML).to.equal('Hello world!'); + }); + + it('returns html string chunked by string handlers', () => { + const formatted = formatHTML('Hello world!', { + strong: 'strong', + }); + + const { container } = render(formatted); + + expect(container.innerHTML).to.equal('Hello world!'); + }); + + it('returns html string chunked by multiple handlers', () => { + const formatted = formatHTML( + 'Message: Hello world!', + { + 'lg-custom': () => 'Greetings', + strong: ({ children }) => {children}, + }, + ); + + const { container } = render(formatted); + + expect(container.innerHTML).to.equal('Message: Greetings world!'); + }); + + it('removes dangling empty text fragment', () => { + const formatted = formatHTML('Hello world', { + strong: ({ children }) => {children}, + }); + + const { container } = render(formatted); + + expect(container.childNodes).to.have.lengthOf(2); + }); + + it('allows (but discards) attributes in the input string', () => { + const formatted = formatHTML( + 'Hello world', + { + strong: ({ children }) => {children}, + }, + ); + + const { container } = render(formatted); + + expect(container.querySelectorAll('[data-after]')).to.have.lengthOf(2); + expect(container.querySelectorAll('[data-before]')).to.have.lengthOf(0); + }); + }); + + describe('t', () => { + it('returns localized key value', () => { + const { result } = renderHook(() => useI18n(), { + wrapper: ({ children }) => ( + {children} + ), + }); + + const { t } = result.current; + + expect(t('sample')).to.equal('translation'); + }); + + it('falls back to key value', () => { + const { result } = renderHook(() => useI18n()); + + const { t } = result.current; + + expect(t('sample')).to.equal('sample'); + }); + }); +}); diff --git a/app/javascript/packages/react-i18n/package.json b/app/javascript/packages/react-i18n/package.json new file mode 100644 index 00000000000..0906ab47a9f --- /dev/null +++ b/app/javascript/packages/react-i18n/package.json @@ -0,0 +1,8 @@ +{ + "name": "@18f/identity-react-i18n", + "private": true, + "version": "1.0.0", + "dependencies": { + "react": "^17.0.1" + } +} diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index 726e8b2d98b..7e9c809f4c7 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -2,7 +2,6 @@ import { render } from 'react-dom'; import { DocumentCapture, AssetContext, - I18nContext, DeviceContext, AcuantContextProvider, UploadContextProvider, @@ -12,6 +11,7 @@ import { import { loadPolyfills } from '@18f/identity-polyfill'; import { isCameraCapableMobile } from '@18f/identity-device'; import { trackEvent } from '@18f/identity-analytics'; +import { I18nContext } from '@18f/identity-react-i18n'; /** @typedef {import('@18f/identity-i18n').I18n} I18n */ diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 8bf9756a2bf..f8e81eaed98 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -64,6 +64,7 @@ search: - app/views/shared/newrelic/_browser_instrumentation.html.erb - app/javascript/app/local-time.js - app/javascript/packages/rails-i18n-webpack-plugin + - app/javascript/packages/react-i18n - app/javascript/packages/i18n ## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`: diff --git a/package.json b/package.json index 6a792beb2ac..f350892e855 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "@testing-library/react": "^11.2.2", "@testing-library/react-hooks": "^3.7.0", "@testing-library/user-event": "^12.6.0", + "@types/chai": "^4.2.22", + "@types/dirty-chai": "^2.0.2", + "@types/mocha": "^9.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/sinon": "^9.0.11", @@ -61,6 +64,7 @@ "sinon-chai": "^3.5.0", "svgo": "^1.3.2", "typescript": "^4.1.3", + "webpack": "^4.46.0", "webpack-dev-server": "^3.11.1" } } diff --git a/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx index 54c64c5ef35..743e1843402 100644 --- a/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx @@ -9,7 +9,7 @@ import AcuantCapture, { } from '@18f/identity-document-capture/components/acuant-capture'; import { AcuantContextProvider, AnalyticsContext } from '@18f/identity-document-capture'; import DeviceContext from '@18f/identity-document-capture/context/device'; -import I18nContext from '@18f/identity-document-capture/context/i18n'; +import { I18nContext } from '@18f/identity-react-i18n'; import { render, useAcuant } from '../../../support/document-capture'; import { getFixtureFile } from '../../../support/file'; diff --git a/spec/javascripts/packages/document-capture/components/documents-step-spec.jsx b/spec/javascripts/packages/document-capture/components/documents-step-spec.jsx index 12cc36efdb5..ed98383829d 100644 --- a/spec/javascripts/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/documents-step-spec.jsx @@ -1,10 +1,7 @@ import userEvent from '@testing-library/user-event'; import sinon from 'sinon'; -import { - I18nContext, - DeviceContext, - ServiceProviderContextProvider, -} from '@18f/identity-document-capture'; +import { DeviceContext, ServiceProviderContextProvider } from '@18f/identity-document-capture'; +import { I18nContext } from '@18f/identity-react-i18n'; import DocumentsStep from '@18f/identity-document-capture/components/documents-step'; import { render } from '../../../support/document-capture'; import { getFixtureFile } from '../../../support/file'; diff --git a/spec/javascripts/packages/document-capture/components/form-error-message-spec.jsx b/spec/javascripts/packages/document-capture/components/form-error-message-spec.jsx index 5c1f6c1a73f..b9ab3346bcd 100644 --- a/spec/javascripts/packages/document-capture/components/form-error-message-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/form-error-message-spec.jsx @@ -1,4 +1,4 @@ -import { I18nContext } from '@18f/identity-document-capture'; +import { I18nContext } from '@18f/identity-react-i18n'; import FormErrorMessage, { RequiredValueMissingError, CameraAccessDeclinedError, diff --git a/spec/javascripts/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascripts/packages/document-capture/components/review-issues-step-spec.jsx index f3476efb6df..30d0c138f84 100644 --- a/spec/javascripts/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/review-issues-step-spec.jsx @@ -1,13 +1,13 @@ import userEvent from '@testing-library/user-event'; import sinon from 'sinon'; import { - I18nContext, ServiceProviderContextProvider, UploadContextProvider, } from '@18f/identity-document-capture'; import ReviewIssuesStep, { reviewIssuesStepValidator, } from '@18f/identity-document-capture/components/review-issues-step'; +import { I18nContext } from '@18f/identity-react-i18n'; import { render } from '../../../support/document-capture'; import { useSandbox } from '../../../support/sinon'; import { getFixtureFile } from '../../../support/file'; diff --git a/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx index be0e12b2c4f..ed1f6d68273 100644 --- a/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx @@ -1,7 +1,7 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; import { cleanup } from '@testing-library/react'; -import { I18nContext } from '@18f/identity-document-capture'; +import { I18nContext } from '@18f/identity-react-i18n'; import SelfieCapture from '@18f/identity-document-capture/components/selfie-capture'; import { render } from '../../../support/document-capture'; import { useSandbox } from '../../../support/sinon'; diff --git a/spec/javascripts/packages/document-capture/context/i18n-spec.jsx b/spec/javascripts/packages/document-capture/context/i18n-spec.jsx deleted file mode 100644 index 6f764e8b1aa..00000000000 --- a/spec/javascripts/packages/document-capture/context/i18n-spec.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import I18nContext from '@18f/identity-document-capture/context/i18n'; - -describe('document-capture/context/i18n', () => { - it('defaults to empty object', () => { - const { result } = renderHook(() => useContext(I18nContext)); - - expect(result.current).to.deep.equal({}); - }); -}); diff --git a/spec/javascripts/packages/document-capture/hooks/use-i18n-spec.jsx b/spec/javascripts/packages/document-capture/hooks/use-i18n-spec.jsx index 6871e914915..e69de29bb2d 100644 --- a/spec/javascripts/packages/document-capture/hooks/use-i18n-spec.jsx +++ b/spec/javascripts/packages/document-capture/hooks/use-i18n-spec.jsx @@ -1,96 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import I18nContext from '@18f/identity-document-capture/context/i18n'; -import useI18n, { formatHTML } from '@18f/identity-document-capture/hooks/use-i18n'; -import { render } from '../../../support/document-capture'; - -describe('document-capture/hooks/use-i18n', () => { - describe('formatHTML', () => { - it('returns html string treated as escaped text without handler', () => { - const formatted = formatHTML('Hello world!', {}); - - const { container } = render(formatted); - - expect(container.innerHTML).to.equal('Hello <strong>world</strong>!'); - }); - - it('returns html string chunked by component handlers', () => { - const formatted = formatHTML('Hello world!', { - strong: ({ children }) => {children}, - }); - - const { container } = render(formatted); - - expect(container.innerHTML).to.equal('Hello world!'); - }); - - it('returns html string chunked by string handlers', () => { - const formatted = formatHTML('Hello world!', { - strong: 'strong', - }); - - const { container } = render(formatted); - - expect(container.innerHTML).to.equal('Hello world!'); - }); - - it('returns html string chunked by multiple handlers', () => { - const formatted = formatHTML( - 'Message: Hello world!', - { - 'lg-custom': () => 'Greetings', - strong: ({ children }) => {children}, - }, - ); - - const { container } = render(formatted); - - expect(container.innerHTML).to.equal('Message: Greetings world!'); - }); - - it('removes dangling empty text fragment', () => { - const formatted = formatHTML('Hello world', { - strong: ({ children }) => {children}, - }); - - const { container } = render(formatted); - - expect(container.childNodes).to.have.lengthOf(2); - }); - - it('allows (but discards) attributes in the input string', () => { - const formatted = formatHTML( - 'Hello world', - { - strong: ({ children }) => {children}, - }, - ); - - const { container } = render(formatted); - - expect(container.querySelectorAll('[data-after]')).to.have.lengthOf(2); - expect(container.querySelectorAll('[data-before]')).to.have.lengthOf(0); - }); - }); - - describe('t', () => { - it('returns localized key value', () => { - const { result } = renderHook(() => useI18n(), { - wrapper: ({ children }) => ( - {children} - ), - }); - - const { t } = result.current; - - expect(t('sample')).to.equal('translation'); - }); - - it('falls back to key value', () => { - const { result } = renderHook(() => useI18n()); - - const { t } = result.current; - - expect(t('sample')).to.equal('sample'); - }); - }); -}); diff --git a/spec/javascripts/spec_helper.d.ts b/spec/javascripts/spec_helper.d.ts new file mode 100644 index 00000000000..b0c66d489bc --- /dev/null +++ b/spec/javascripts/spec_helper.d.ts @@ -0,0 +1,5 @@ +import { expect as _expect } from 'chai'; + +declare global { + const expect: typeof _expect; +} diff --git a/tsconfig.json b/tsconfig.json index 84f59039cad..f4809738f9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "app/components", "app/javascript/app/phone-internationalization.js", "app/javascript/packages", - "app/javascript/packs" + "app/javascript/packs", + "spec/javascripts/spec_helper.d.ts" ], "exclude": [ "**/fixtures", diff --git a/yarn.lock b/yarn.lock index e54dfc2f642..26933f6c409 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1202,6 +1202,26 @@ resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.0.tgz#10ca75692575744d0117098148a8dc84cbee6682" integrity sha512-Jjzp5EqU0hNpADctc/UqhiFbY1y2MqIxBVa2S4dBlbnZHTLPMuggoL5q43X63LpsOIINRDirBjP56DUUKIUWIA== +"@types/chai-as-promised@*": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601" + integrity sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA== + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.2.22": + version "4.2.22" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7" + integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ== + +"@types/dirty-chai@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/dirty-chai/-/dirty-chai-2.0.2.tgz#eeac4802329a41ed7815ac0c1a6360335bf77d0c" + integrity sha512-BruwIN/UQEU0ePghxEX+OyjngpOfOUKJQh3cmfeq2h2Su/g001iljVi3+Y2y2EFp3IPgjf4sMrRU33Hxv1FUqw== + dependencies: + "@types/chai" "*" + "@types/chai-as-promised" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -1244,6 +1264,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/mocha@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.0.0.tgz#3205bcd15ada9bc681ac20bef64e9e6df88fd297" + integrity sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA== + "@types/node@*": version "14.11.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"