зеркало из https://github.com/mozilla/fxa.git
Merge pull request #14312 from mozilla/FXA-5999
feat(l10n): Create Localized wrapper and React l10n test setup
This commit is contained in:
Коммит
38e53418d9
|
@ -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, 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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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, don’t 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>
|
||||
),
|
||||
}));
|
33
yarn.lock
33
yarn.lock
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче