зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
6a415eb4c4
Коммит
befccf53df
|
@ -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
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче