Merge pull request #14312 from mozilla/FXA-5999

feat(l10n): Create Localized wrapper and React l10n test setup
This commit is contained in:
Lauren Zugai 2022-10-26 10:59:28 -05:00 коммит произвёл GitHub
Родитель a923af8d89 befccf53df
Коммит 38e53418d9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
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>
),
}));

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

@ -21461,13 +21461,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"
@ -25361,6 +25354,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
@ -25383,8 +25380,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
@ -41353,13 +41348,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"
@ -47182,19 +47170,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"