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
This commit is contained in:
Lauren Zugai 2022-10-13 10:11:52 -05:00
Родитель 6a415eb4c4
Коммит befccf53df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0C86B71E24811D10
17 изменённых файлов: 478 добавлений и 86 удалений

1
.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/

Просмотреть файл

@ -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) => (
<div data-testid="ftlmsg-mock" id={props.id}>
{props.children}
</div>
),
}));
const simpleComponent = (id: string, fallbackText = '') => (
<FtlMsg {...{ id }}>{fallbackText}</FtlMsg>
);
const componentWithAttrs = (id: string, fallbackText = '') => (
<FtlMsg {...{ id }} attrs={{ header: true }}>
<h2>{fallbackText}</h2>
</FtlMsg>
);
const componentWithVar = (fallbackText: string, name: string) => (
<FtlMsg id="test-var" vars={{ name }}>
<p>{fallbackText}</p>
</FtlMsg>
);
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, dont 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();
});
});
});

Просмотреть файл

@ -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<string, FluentVariable>
) {
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<typeof queries>,
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<string, FluentVariable>
) {
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)
);
}
}

Просмотреть файл

@ -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, dont go"
test-var = { $name } smiled at me
test-term = Lately, { -term } is all I need

Просмотреть файл

@ -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) => (
<Localized {...props}>{props.children}</Localized>
);

Просмотреть файл

@ -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;
},
},

Просмотреть файл

@ -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']);
};

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -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,
},
],
};

Просмотреть файл

@ -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');

Просмотреть файл

@ -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 = () => {
</li>
</ul>
</div>
<Localized id="bento-menu-made-by-mozilla">
<FtlMsg id="bento-menu-made-by-mozilla">
<LinkExternal
data-testid="mozilla-link"
className="link-blue text-xs w-full text-center block m-2 p-2 hover:bg-grey-100"
@ -162,7 +163,7 @@ export const BentoMenu = () => {
>
Made by Mozilla
</LinkExternal>
</Localized>
</FtlMsg>
</div>
</div>
</div>

Просмотреть файл

@ -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" <Profile/> with correct content', async () => {
const { findByText } = renderWithRouter(
<AppContext.Provider value={mockAppContext({ account })}>
renderWithRouter(
<AppContext.Provider
value={mockAppContext({ account: MOCK_PROFILE_EMPTY })}
>
<Profile />
</AppContext.Provider>
);
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');
});
});

Просмотреть файл

@ -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 = () => {
<section className="mt-11" data-testid="settings-profile">
<h2 className="font-header font-bold mobileLandscape:ltr:ml-6 mobileLandscape:rtl:ml-6 ltr:ml-4 rtl:mr-4 mb-4 relative">
<span id="profile" className="nav-anchor"></span>
<Localized id="profile-heading">Profile</Localized>
<FtlMsg id="profile-heading">Profile</FtlMsg>
</h2>
<div className="bg-white tablet:rounded-xl shadow">
<Localized id="profile-picture" attrs={{ header: true }}>
<FtlMsg id="profile-picture" attrs={{ header: true }}>
<UnitRow
header="Picture"
headerId="profile-picture"
@ -25,11 +29,11 @@ export const Profile = () => {
prefixDataTestId="avatar"
{...{ avatar }}
/>
</Localized>
</FtlMsg>
<hr className="unit-row-hr" />
<Localized id="profile-display-name" attrs={{ header: true }}>
<FtlMsg id="profile-display-name" attrs={{ header: true }}>
<UnitRow
header="Display name"
headerId="display-name"
@ -38,11 +42,11 @@ export const Profile = () => {
route="/settings/display_name"
prefixDataTestId="display-name"
/>
</Localized>
</FtlMsg>
<hr className="unit-row-hr" />
<Localized id="profile-primary-email" attrs={{ header: true }}>
<FtlMsg id="profile-primary-email" attrs={{ header: true }}>
<UnitRow
header="Primary email"
headerId="primary-email"
@ -50,7 +54,7 @@ export const Profile = () => {
headerValueClassName="break-all"
prefixDataTestId="primary-email"
/>
</Localized>
</FtlMsg>
<hr className="unit-row-hr" />

Просмотреть файл

@ -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" <Security/> 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(
<AppContext.Provider value={mockAppContext({ account })}>
<Security />
@ -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(

Просмотреть файл

@ -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 (
<Localized id="security-password-created-date" vars={{ date: pwdDateText }}>
<FtlMsg id="security-password-created-date" vars={{ date: pwdDateText }}>
<p className="text-grey-400 text-xs mobileLandscape:mt-3">
Created {pwdDateText}
</p>
</Localized>
</FtlMsg>
);
};
@ -62,8 +63,8 @@ export const Security = () => {
) : (
<Localized id="security-set-password">
<p className="text-sm mt-3">
Set a password to sync and use certain account
security features.
Set a password to sync and use certain account security
features.
</p>
</Localized>
)}

Просмотреть файл

@ -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) => (
<div data-testid="ftlmsg-mock" id={props.id}>
{props.children}
</div>
),
}));

Просмотреть файл

@ -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"