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"