From befccf53df44eebe3422a384a8a5b8930f8bcad8 Mon Sep 17 00:00:00 2001 From: Lauren Zugai Date: Thu, 13 Oct 2022 10:11:52 -0500 Subject: [PATCH] feat(l10n): Create Localized wrapper and React l10n test setup Because: * We want better l10n testing across the codebase This commit: * Creates a Localized wrapper to use in place of Localized that requires children (fallback text) * Adds a mock for the Localized wrapper in Jest's setupTests to avoid copying and pasting this into every test file * Creates test-utils in fxa-react and exports functions to use for testing the mock that all IDs exist in the 'en' bundle, that fallback text matches the message in the bundle, and that the message doesn't contain straight apostrophes or quotes * Adds tests and a test.ftl file for the test-utils l10n functions to ensure they test what we think they do * Moves concatenating settings.ftl out of webpack and into grunt due to 1) needing a test version of settings.ftl at a different path, else tests may fail in CI with new or changed strings since the l10n repo won't have them immediately, 2) wanting to retest FTL changes without having the settings app running (jest does not run nicely with webpack), 3) consistency with the auth-server * Uses this new test functionality in a few Settings tests covering various use cases Closes FXA-5999 --- .gitignore | 1 + .../fxa-react/lib/test-utils/index.test.tsx | 151 ++++++++++++++++ packages/fxa-react/lib/test-utils/index.ts | 164 ++++++++++++++++++ packages/fxa-react/lib/test-utils/test.ftl | 11 ++ packages/fxa-react/lib/utils.tsx | 14 ++ packages/fxa-settings/.rescriptsrc.js | 17 -- packages/fxa-settings/Gruntfile.js | 43 +++++ packages/fxa-settings/package.json | 21 ++- packages/fxa-settings/pm2.config.js | 9 + .../src/components/BentoMenu/index.test.tsx | 7 + .../src/components/BentoMenu/index.tsx | 5 +- .../src/components/Profile/index.test.tsx | 34 ++-- .../src/components/Profile/index.tsx | 20 ++- .../src/components/Security/index.test.tsx | 16 +- .../src/components/Security/index.tsx | 9 +- .../src/{setupTests.js => setupTests.tsx} | 9 + yarn.lock | 33 +--- 17 files changed, 478 insertions(+), 86 deletions(-) create mode 100644 packages/fxa-react/lib/test-utils/index.test.tsx create mode 100644 packages/fxa-react/lib/test-utils/index.ts create mode 100644 packages/fxa-react/lib/test-utils/test.ftl create mode 100644 packages/fxa-react/lib/utils.tsx create mode 100644 packages/fxa-settings/Gruntfile.js rename packages/fxa-settings/src/{setupTests.js => setupTests.tsx} (53%) diff --git a/.gitignore b/.gitignore index 696dc0586a..015f87578c 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ packages/fxa-react/**/*.d.ts # fxa-settings packages/fxa-settings/fxa-content-server-l10n/ packages/fxa-settings/public/locales +packages/fxa-settings/test/ diff --git a/packages/fxa-react/lib/test-utils/index.test.tsx b/packages/fxa-react/lib/test-utils/index.test.tsx new file mode 100644 index 0000000000..6c3e574171 --- /dev/null +++ b/packages/fxa-react/lib/test-utils/index.test.tsx @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { FtlMsg, FtlMsgProps } from '../utils'; +import { getFtlBundle, testL10n } from '.'; +import { FluentBundle } from '@fluent/bundle'; + +jest.mock('fxa-react/lib/utils', () => ({ + FtlMsg: (props: FtlMsgProps) => ( +
+ {props.children} +
+ ), +})); + +const simpleComponent = (id: string, fallbackText = '') => ( + {fallbackText} +); + +const componentWithAttrs = (id: string, fallbackText = '') => ( + +

{fallbackText}

+
+); + +const componentWithVar = (fallbackText: string, name: string) => ( + +

{fallbackText}

+
+); + +describe('testL10n', () => { + let bundle: FluentBundle; + beforeAll(async () => { + bundle = await getFtlBundle(null); + }); + + it('throws if FTL ID is not found', () => { + expect(() => { + render(simpleComponent('not-in-bundle')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError( + 'Could not retrieve Fluent message tied to ID: not-in-bundle' + ); + }); + + it('throws if FTL ID is present, but message is not found', () => { + expect(() => { + render(simpleComponent('test-missing-message')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError( + 'Could not retrieve Fluent message tied to ID: test-missing-message' + ); + }); + + it('throws if FTL ID and attributes are present, but message is not found', () => { + expect(() => { + render(componentWithAttrs('test-missing-message-attrs')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError( + 'Could not retrieve Fluent message tied to ID: test-missing-message-attrs' + ); + }); + + it('successfully tests simple messages', () => { + expect(() => { + render(simpleComponent('test-simple', 'Simple and clean')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).not.toThrow(); + }); + + it('successfully tests messages containing attributes', () => { + expect(() => { + render(componentWithAttrs('test-attrs', 'When you walk away')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).not.toThrow(); + }); + + it('successfully tests messages with terms', () => { + expect(() => { + render(simpleComponent('test-term', 'Lately, Mozilla is all I need')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).not.toThrow(); + }); + + describe('testMessage', () => { + it('throws if FTL message does not match fallback text', () => { + expect(() => { + render(simpleComponent('test-simple', 'testing 123')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError('Fallback text does not match Fluent message'); + }); + + it('throws if FTL message contains straight apostrophe', () => { + expect(() => { + render( + simpleComponent('test-straight-apostrophe', "you don't hear me say") + ); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError('Fluent message contains a straight apostrophe'); + }); + + it('throws if FTL message contains straight quote', () => { + expect(() => { + render(simpleComponent('test-straight-quote', '"please, don’t go"')); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError('Fluent message contains a straight quote'); + }); + + it('throws if FTL message expects variable not provided', () => { + const name = 'Sora'; + expect(() => { + render(componentWithVar(`${name} smiled at me`, name)); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle); + }).toThrowError('Unknown variable: $name'); + }); + + it('successfully tests messages with variables', () => { + const name = 'Sora'; + expect(() => { + render(componentWithVar(`${name} smiled at me`, name)); + + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle, { name }); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/fxa-react/lib/test-utils/index.ts b/packages/fxa-react/lib/test-utils/index.ts new file mode 100644 index 0000000000..66971f40f1 --- /dev/null +++ b/packages/fxa-react/lib/test-utils/index.ts @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import path from 'path'; +import fs from 'fs'; +import { FluentBundle, FluentResource, FluentVariable } from '@fluent/bundle'; +import { Pattern } from '@fluent/bundle/esm/ast'; +import { queries, Screen } from '@testing-library/react'; + +type PackageName = 'settings' | 'payments' | null; + +// Testing locales other than the default will load bundles from the l10n repo. +async function getFtlFromPackage(packageName: PackageName, locale: string) { + let ftlPath: string; + + switch (packageName) { + case 'settings': + if (locale === 'en') { + ftlPath = path.join( + __dirname, + '..', + '..', + '..', + 'fxa-settings', + 'test', + 'settings.ftl' + ); + } else { + // TODO: Not currently used, but probably want to add one translation test + ftlPath = path.join( + __dirname, + '..', + '..', + '..', + 'fxa-settings', + 'public', + 'locales', + locale, + 'settings.ftl' + ); + } + break; + case 'payments': + // TODO: Not currently used. We need to set up test stuff for payments similarly, FXA-5996 + ftlPath = path.join( + __dirname, + '..', + '..', + '..', + 'fxa-payments-server', + 'public', + 'locales', + locale, + 'main.ftl' + ); + break; + default: + ftlPath = path.join(__dirname, 'test.ftl'); + break; + } + return fs.promises.readFile(ftlPath, 'utf8'); +} + +/** + * Get the specified FTL file/bundle. + * @packageName Which package to load the bundle for + * @locale Which locale FTL bundle to load. 'en' will load `test/[name].ftl` and other + * locales pull from the cloned l10n repo in `public`. + */ +export async function getFtlBundle(packageName: PackageName, locale = 'en') { + const messages = await getFtlFromPackage(packageName, locale); + const resource = new FluentResource(messages); + const bundle = new FluentBundle(locale, { useIsolating: false }); + bundle.addResource(resource); + return bundle; +} + +function testMessage( + bundle: FluentBundle, + pattern: Pattern, + fallbackText: string | null, + ftlArgs?: Record +) { + const ftlMsg = bundle.formatPattern(pattern, ftlArgs); + + // We allow for .includes because fallback text comes from `textContent` within the + // `FtlMsg` wrapper which may contain more than one component and string + if (!fallbackText?.includes(ftlMsg)) { + throw Error( + `Fallback text does not match Fluent message.\nFallback text: ${fallbackText}\nFluent message: ${ftlMsg}` + ); + } + + if (ftlMsg.includes("'")) { + throw Error( + `Fluent message contains a straight apostrophe (') and must be updated to its curly equivalent (’). Fluent message: ${ftlMsg}` + ); + } + + if (ftlMsg.includes('"')) { + throw Error( + `Fluent message contains a straight quote (") and must be updated to its curly equivalent (“”). Fluent message: ${ftlMsg}` + ); + } +} + +/** + * Convenience function for running `testL10n` against all mocked `FtlMsg`s + * (`data-testid='ftlmsg-mock'`) found. + * @param screen + * @param bundle Fluent bundle created during test setup + */ +export function testAllL10n( + { getAllByTestId }: Screen, + bundle: FluentBundle +) { + const ftlMsgMocks = getAllByTestId('ftlmsg-mock'); + ftlMsgMocks.forEach((ftlMsgMock) => { + testL10n(ftlMsgMock, bundle); + }); +} + +/** + * Takes in a mocked FtlMsg and tests that: + * * Fluent IDs and message are present in the Fluent bundle + * * Fluent messages match fallback text + * * Fluent messages don't contain any straight apostrophes or quotes + * * Variables are provided + * @param ftlMsgMock Mocked version of `FtlMsg` (`data-testid='ftlmsg-mock'`) + * @param bundle Fluent bundle created during test setup + * @param ftlArgs Optional Fluent variables to be passed into the message + */ +export function testL10n( + ftlMsgMock: HTMLElement, + bundle: FluentBundle, + ftlArgs?: Record +) { + const ftlId = ftlMsgMock.getAttribute('id')!; + const fallbackText = ftlMsgMock.textContent; + const ftlBundleMsg = bundle.getMessage(ftlId); + + // nested attributes can happen when we define something like: + // `profile-picture = + // .header = Picture` + const nestedAttrValues = Object.values(ftlBundleMsg?.attributes || {}); + + if ( + ftlBundleMsg === undefined || + (ftlBundleMsg.value === null && nestedAttrValues.length === 0) + ) { + throw Error(`Could not retrieve Fluent message tied to ID: ${ftlId}`); + } + + if (ftlBundleMsg.value) { + testMessage(bundle, ftlBundleMsg.value, fallbackText, ftlArgs); + } + + if (nestedAttrValues) { + nestedAttrValues.forEach((nestedAttrValue) => + testMessage(bundle, nestedAttrValue, fallbackText, ftlArgs) + ); + } +} diff --git a/packages/fxa-react/lib/test-utils/test.ftl b/packages/fxa-react/lib/test-utils/test.ftl new file mode 100644 index 0000000000..91b7adb364 --- /dev/null +++ b/packages/fxa-react/lib/test-utils/test.ftl @@ -0,0 +1,11 @@ +-term = Mozilla +test-simple = Simple and clean +test-missing-message = +test-missing-message-attrs = + .header = +test-attrs = + .header = When you walk away +test-straight-apostrophe = you don't hear me say +test-straight-quote = "please, don’t go" +test-var = { $name } smiled at me +test-term = Lately, { -term } is all I need diff --git a/packages/fxa-react/lib/utils.tsx b/packages/fxa-react/lib/utils.tsx new file mode 100644 index 0000000000..1c4e1b93c4 --- /dev/null +++ b/packages/fxa-react/lib/utils.tsx @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import { Localized, LocalizedProps } from '@fluent/react'; + +export type FtlMsgProps = { + children: React.ReactNode; +} & LocalizedProps; + +export const FtlMsg = (props: FtlMsgProps) => ( + {props.children} +); diff --git a/packages/fxa-settings/.rescriptsrc.js b/packages/fxa-settings/.rescriptsrc.js index 0781b9e4ff..a3fba49844 100644 --- a/packages/fxa-settings/.rescriptsrc.js +++ b/packages/fxa-settings/.rescriptsrc.js @@ -4,20 +4,8 @@ const path = require('path'); const fs = require('fs'); -const { default: WebpackWatchPlugin } = require('webpack-watch-files-plugin'); -const MergeIntoSingleFilePlugin = require('webpack-merge-and-include-globally'); const { permitAdditionalJSImports } = require('fxa-react/configs/rescripts'); -const watchFtlPlugin = new WebpackWatchPlugin({ - files: ['src/**/*.ftl'], -}); - -const mergeFtlPlugin = new MergeIntoSingleFilePlugin({ - files: { - '../public/locales/en-US/settings.ftl': ['.license.header', 'src/**/*.ftl'], - }, -}); - module.exports = [ { devServer: (config) => { @@ -45,11 +33,6 @@ module.exports = [ }; } - newConfig = { - ...newConfig, - plugins: [...newConfig.plugins, watchFtlPlugin, mergeFtlPlugin], - }; - return newConfig; }, }, diff --git a/packages/fxa-settings/Gruntfile.js b/packages/fxa-settings/Gruntfile.js new file mode 100644 index 0000000000..73551c716d --- /dev/null +++ b/packages/fxa-settings/Gruntfile.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict'; + +module.exports = function (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + ftl: { + src: ['.license.header', 'src/**/*.ftl'], + // TODO: change dest to `en` in FXA-6003 + dest: 'public/locales/en-US/settings.ftl', + }, + + // We need this for tests because we pull the latest from `fxa-content-server-l10n` + // and place those in our `public` directory at `postinstall` time, and sometimes we have + // FTL updates on our side that haven't landed yet on the l10n side. We want to test + // against _our_ latest, and not necessarily the l10n repo's latest. + 'ftl-test': { + src: ['.license.header', 'src/**/*.ftl'], + dest: 'test/settings.ftl', + }, + }, + watch: { + ftl: { + files: ['src/**/*.ftl'], + tasks: ['merge-ftl'], + options: { + interrupt: true, + }, + }, + }, + }); + + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-concat'); + + grunt.registerTask('merge-ftl', ['concat:ftl']); + grunt.registerTask('merge-ftl:test', ['concat:ftl-test']); + grunt.registerTask('watch-ftl', ['watch:ftl']); +}; diff --git a/packages/fxa-settings/package.json b/packages/fxa-settings/package.json index 210a2d9179..27af587739 100644 --- a/packages/fxa-settings/package.json +++ b/packages/fxa-settings/package.json @@ -4,21 +4,24 @@ "homepage": "https://accounts.firefox.com/settings", "private": true, "scripts": { - "postinstall": "../../_scripts/clone-l10n.sh fxa-settings", + "postinstall": "grunt merge-ftl &&../../_scripts/clone-l10n.sh fxa-settings", "build-css": "tailwindcss -i ./src/styles/tailwind.css -o ./src/styles/tailwind.out.css --postcss", "build-storybook": "NODE_ENV=production STORYBOOK_BUILD=1 npm run build-css && build-storybook", - "build": "tsc --build ../fxa-react && NODE_ENV=production npm run build-css && SKIP_PREFLIGHT_CHECK=true INLINE_RUNTIME_CHUNK=false rescripts build", + "build": "tsc --build ../fxa-react && NODE_ENV=production npm run build-css && npm run merge-ftl && SKIP_PREFLIGHT_CHECK=true INLINE_RUNTIME_CHUNK=false rescripts build", "eject": "react-scripts eject", "lint:eslint": "eslint . .storybook", "lint": "npm-run-all --parallel lint:eslint", "restart": "npm run build-css && pm2 restart pm2.config.js", - "start": "npm run build-css && pm2 start pm2.config.js && ../../_scripts/check-url.sh localhost:3000/settings/static/js/bundle.js", + "start": "npm run build-css && grunt merge-ftl && pm2 start pm2.config.js && ../../_scripts/check-url.sh localhost:3000/settings/static/js/bundle.js", "stop": "pm2 stop pm2.config.js", "delete": "pm2 delete pm2.config.js", "storybook": "STORYBOOK_BUILD=1 npm run build-css && start-storybook -p 6008 --no-version-updates", - "test": "SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false", + "test": "yarn merge-ftl:test && SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false", "test:watch": "SKIP_PREFLIGHT_CHECK=true rescripts test", - "test:coverage": "yarn test --coverage --watchAll=false" + "test:coverage": "yarn test --coverage --watchAll=false", + "merge-ftl": "grunt merge-ftl", + "merge-ftl:test": "grunt merge-ftl:test", + "watch-ftl": "grunt watch-ftl" }, "jest": { "resetMocks": false, @@ -115,6 +118,10 @@ "eslint-plugin-jest": "^24.5.2", "eslint-plugin-react": "^7.31.10", "fxa-shared": "workspace:*", + "grunt": "^1.5.3", + "grunt-cli": "^1.4.3", + "grunt-contrib-concat": "^2.1.0", + "grunt-contrib-watch": "^1.1.0", "jest-watch-typeahead": "0.6.5", "mutationobserver-shim": "^0.3.7", "npm-run-all": "^4.1.5", @@ -125,8 +132,6 @@ "storybook-addon-rtl": "^0.4.3", "style-loader": "^1.3.0", "tailwindcss": "^3.2.0", - "webpack": "^4.43.0", - "webpack-merge-and-include-globally": "^2.3.4", - "webpack-watch-files-plugin": "^1.2.1" + "webpack": "^4.43.0" } } diff --git a/packages/fxa-settings/pm2.config.js b/packages/fxa-settings/pm2.config.js index 83aa592dc8..23f6f5de79 100644 --- a/packages/fxa-settings/pm2.config.js +++ b/packages/fxa-settings/pm2.config.js @@ -53,5 +53,14 @@ module.exports = { ignore_watch: ['src/styles/tailwind.out.*'], time: true, }, + { + name: 'settings-ftl', + script: 'yarn grunt watch-ftl', + cwd: __dirname, + filter_env: ['npm_'], + max_restarts: '1', + min_uptime: '2m', + time: true, + }, ], }; diff --git a/packages/fxa-settings/src/components/BentoMenu/index.test.tsx b/packages/fxa-settings/src/components/BentoMenu/index.test.tsx index f978eed531..6dd00ee7f4 100644 --- a/packages/fxa-settings/src/components/BentoMenu/index.test.tsx +++ b/packages/fxa-settings/src/components/BentoMenu/index.test.tsx @@ -5,8 +5,14 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import BentoMenu from '.'; +import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; +import { FluentBundle } from '@fluent/bundle'; describe('BentoMenu', () => { + let bundle: FluentBundle; + beforeAll(async () => { + bundle = await getFtlBundle('settings'); + }); const dropDownId = 'drop-down-bento-menu'; it('renders and toggles as expected with default values', () => { @@ -23,6 +29,7 @@ describe('BentoMenu', () => { fireEvent.click(toggleButton); expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); expect(screen.queryByTestId(dropDownId)).toBeInTheDocument(); + testAllL10n(screen, bundle); fireEvent.click(toggleButton); expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); diff --git a/packages/fxa-settings/src/components/BentoMenu/index.tsx b/packages/fxa-settings/src/components/BentoMenu/index.tsx index ea2663b101..0ea07c2a32 100644 --- a/packages/fxa-settings/src/components/BentoMenu/index.tsx +++ b/packages/fxa-settings/src/components/BentoMenu/index.tsx @@ -17,6 +17,7 @@ import vpnIcon from './vpn-logo.svg'; import { ReactComponent as BentoIcon } from './bento.svg'; import { ReactComponent as CloseIcon } from 'fxa-react/images/close.svg'; import { Localized, useLocalization } from '@fluent/react'; +import { FtlMsg } from 'fxa-react/lib/utils'; export const BentoMenu = () => { const [isRevealed, setRevealed] = useState(false); @@ -154,7 +155,7 @@ export const BentoMenu = () => { - + { > Made by Mozilla - + diff --git a/packages/fxa-settings/src/components/Profile/index.test.tsx b/packages/fxa-settings/src/components/Profile/index.test.tsx index a81a0ab4bb..1ae544485b 100644 --- a/packages/fxa-settings/src/components/Profile/index.test.tsx +++ b/packages/fxa-settings/src/components/Profile/index.test.tsx @@ -6,28 +6,30 @@ import React from 'react'; import '@testing-library/jest-dom/extend-expect'; import { Profile } from '.'; import { mockAppContext, renderWithRouter } from '../../models/mocks'; -import { Account, AppContext } from '../../models'; +import { AppContext } from '../../models'; +import { MOCK_PROFILE_EMPTY } from './mocks'; +import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; +import { screen } from '@testing-library/react'; +import { FluentBundle } from '@fluent/bundle'; -const account = { - avatar: { url: null, id: null }, - primaryEmail: { - email: 'vladikoff@mozilla.com', - }, - emails: [], - displayName: 'Vlad', -} as unknown as Account; - -// todo: -// add test cases for different states, including secondary email describe('Profile', () => { + let bundle: FluentBundle; + beforeAll(async () => { + bundle = await getFtlBundle('settings'); + }); + it('renders "fresh load" with correct content', async () => { - const { findByText } = renderWithRouter( - + renderWithRouter( + ); + testAllL10n(screen, bundle); - expect(await findByText('Vlad')).toBeTruthy(); - expect(await findByText('vladikoff@mozilla.com')).toBeTruthy(); + await screen.findByAltText('Default avatar'); + expect(await screen.findAllByText('None')).toHaveLength(2); + await screen.findByText('johndope@example.com'); }); }); diff --git a/packages/fxa-settings/src/components/Profile/index.tsx b/packages/fxa-settings/src/components/Profile/index.tsx index 84d57605e4..e699f4c67c 100644 --- a/packages/fxa-settings/src/components/Profile/index.tsx +++ b/packages/fxa-settings/src/components/Profile/index.tsx @@ -1,9 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + import React from 'react'; import { useAccount } from '../../models'; import { UnitRow } from '../UnitRow'; import { UnitRowSecondaryEmail } from '../UnitRowSecondaryEmail'; import { HomePath } from '../../constants'; -import { Localized } from '@fluent/react'; +import { FtlMsg } from 'fxa-react/lib/utils'; export const Profile = () => { const { avatar, primaryEmail, displayName } = useAccount(); @@ -12,11 +16,11 @@ export const Profile = () => {

- Profile + Profile

- + { prefixDataTestId="avatar" {...{ avatar }} /> - +
- + { route="/settings/display_name" prefixDataTestId="display-name" /> - +
- + { headerValueClassName="break-all" prefixDataTestId="primary-email" /> - +
diff --git a/packages/fxa-settings/src/components/Security/index.test.tsx b/packages/fxa-settings/src/components/Security/index.test.tsx index 2921d4b74f..9896bddb15 100644 --- a/packages/fxa-settings/src/components/Security/index.test.tsx +++ b/packages/fxa-settings/src/components/Security/index.test.tsx @@ -7,8 +7,15 @@ import { screen } from '@testing-library/react'; import Security from '.'; import { mockAppContext, renderWithRouter } from '../../models/mocks'; import { Account, AppContext } from '../../models'; +import { getFtlBundle, testL10n } from 'fxa-react/lib/test-utils'; +import { FluentBundle } from '@fluent/bundle'; describe('Security', () => { + let bundle: FluentBundle; + beforeAll(async () => { + bundle = await getFtlBundle('settings'); + }); + it('renders "fresh load" with correct content', async () => { const account = { avatar: { url: null, id: null }, @@ -68,7 +75,7 @@ describe('Security', () => { passwordCreated: 1234567890, hasPassword: true, } as unknown as Account; - const createDate = new Date(1234567890).getDate(); + const createDate = `1/${new Date(1234567890).getDate()}/1970`; renderWithRouter( @@ -76,8 +83,13 @@ describe('Security', () => { ); const passwordRouteLink = screen.getByTestId('password-unit-row-route'); + const ftlMsgMock = screen.getByTestId('ftlmsg-mock'); + testL10n(ftlMsgMock, bundle, { + date: createDate, + }); + await screen.findByText('••••••••••••••••••'); - await screen.findByText(`Created 1/${createDate}/1970`); + await screen.findByText(`Created ${createDate}`); expect(passwordRouteLink).toHaveTextContent('Change'); expect(passwordRouteLink).toHaveAttribute( diff --git a/packages/fxa-settings/src/components/Security/index.tsx b/packages/fxa-settings/src/components/Security/index.tsx index 8e3048c4bf..76da136423 100644 --- a/packages/fxa-settings/src/components/Security/index.tsx +++ b/packages/fxa-settings/src/components/Security/index.tsx @@ -8,6 +8,7 @@ import UnitRowRecoveryKey from '../UnitRowRecoveryKey'; import UnitRowTwoStepAuth from '../UnitRowTwoStepAuth'; import { UnitRow } from '../UnitRow'; import { useAccount } from '../../models'; +import { FtlMsg } from 'fxa-react/lib/utils'; const PwdDate = ({ passwordCreated }: { passwordCreated: number }) => { const pwdDateText = Intl.DateTimeFormat('default', { @@ -17,11 +18,11 @@ const PwdDate = ({ passwordCreated }: { passwordCreated: number }) => { }).format(new Date(passwordCreated)); return ( - +

Created {pwdDateText}

-
+ ); }; @@ -62,8 +63,8 @@ export const Security = () => { ) : (

- Set a password to sync and use certain account - security features. + Set a password to sync and use certain account security + features.

)} diff --git a/packages/fxa-settings/src/setupTests.js b/packages/fxa-settings/src/setupTests.tsx similarity index 53% rename from packages/fxa-settings/src/setupTests.js rename to packages/fxa-settings/src/setupTests.tsx index 8c81f88ecb..d2e50f24f2 100644 --- a/packages/fxa-settings/src/setupTests.js +++ b/packages/fxa-settings/src/setupTests.tsx @@ -3,3 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import '@testing-library/jest-dom/extend-expect'; +import { FtlMsgProps } from 'fxa-react/lib/utils'; + +jest.mock('fxa-react/lib/utils', () => ({ + FtlMsg: (props: FtlMsgProps) => ( +
+ {props.children} +
+ ), +})); diff --git a/yarn.lock b/yarn.lock index f6446bd610..633bf846da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21449,13 +21449,6 @@ __metadata: languageName: node linkType: hard -"es6-promisify@npm:^6.1.1": - version: 6.1.1 - resolution: "es6-promisify@npm:6.1.1" - checksum: e57dfa8b6533387e6cae115bdc1591e4e6e7648443741360c4f4f8f1d2c17d1f0fb293ccd3f86193f016c236ed15f336e075784eab7ec9a67af0aed2b949dd7c - languageName: node - linkType: hard - "es6-shim@npm:^0.35.5": version: 0.35.5 resolution: "es6-shim@npm:0.35.5" @@ -25349,6 +25342,10 @@ fsevents@~2.1.1: fxa-shared: "workspace:*" get-orientation: ^1.1.2 graphql: ^15.6.1 + grunt: ^1.5.3 + grunt-cli: ^1.4.3 + grunt-contrib-concat: ^2.1.0 + grunt-contrib-watch: ^1.1.0 jest-watch-typeahead: 0.6.5 lodash.groupby: ^4.6.0 mutationobserver-shim: ^0.3.7 @@ -25371,8 +25368,6 @@ fsevents@~2.1.1: typescript: ^4.8.2 uuid: ^9.0.0 webpack: ^4.43.0 - webpack-merge-and-include-globally: ^2.3.4 - webpack-watch-files-plugin: ^1.2.1 languageName: unknown linkType: soft @@ -41341,13 +41336,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"rev-hash@npm:^3.0.0": - version: 3.0.0 - resolution: "rev-hash@npm:3.0.0" - checksum: d7af9e411b945cae85c40d3d5e061589b5e84b16d3ffddcee948766ca4e70e7040942fc9aee0e8a4d64e1edf832730687bbcaf63577b8b6ca59866116bd8f6bd - languageName: node - linkType: hard - "rework-visit@npm:1.0.0": version: 1.0.0 resolution: "rework-visit@npm:1.0.0" @@ -47170,19 +47158,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"webpack-merge-and-include-globally@npm:^2.3.4": - version: 2.3.4 - resolution: "webpack-merge-and-include-globally@npm:2.3.4" - dependencies: - es6-promisify: ^6.1.1 - glob: ^7.1.6 - rev-hash: ^3.0.0 - peerDependencies: - webpack: ">=1.0.0" - checksum: 00e99409653303ef9e99ba9f622a35f890110932d6feecaa65779f2bf8237a892ac5905bb4a4ea4fd41dd2e584461ee9de4f366e7ce307737e9c0e5b9693fe39 - languageName: node - linkType: hard - "webpack-merge@npm:^5.7.3": version: 5.7.3 resolution: "webpack-merge@npm:5.7.3"