[Tools] Add "values" property validation (#22538)

* [Tools] Add "values" property validation

* Fix values validation

* Fix typo in values regex

* Fix whitespaces handling

* Fix curly braces in regex

* Fix missing/unused values differentiation

* Use intl-messageformat-parser for parsing values from defaultMessage
This commit is contained in:
Leanid Shutau 2018-10-25 14:09:23 +03:00 committed by GitHub
parent 40c232b111
commit 71d284469b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 354 additions and 136 deletions

View file

@ -308,6 +308,7 @@
"has-ansi": "^3.0.0",
"husky": "^0.14.3",
"image-diff": "1.6.0",
"intl-messageformat-parser": "^1.4.0",
"istanbul-instrumenter-loader": "3.0.0",
"jest": "^23.5.0",
"jest-cli": "^23.5.0",

View file

@ -287,10 +287,14 @@ import { injectI18n, intlShape } from '@kbn/i18n/react';
const MyComponentContent = ({ intl }) => (
<input
type="text"
placeholder={intl.formatMessage({
id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
defaultMessage: 'Search',
})}
placeholder={intl.formatMessage(
{
id: 'welcome',
defaultMessage: 'Hello {name}, you have {unreadCount, number}\
{unreadCount, plural, one {message} other {messages}}',
},
{ name, unreadCount }
)}
/>
);

View file

@ -111,9 +111,9 @@ Click \'Confirm overwrite\' to import and overwrite existing objects. Any change
const statusMsg = hasErrors
? this.props.intl.formatMessage(
{ id: 'kbn.home.tutorial.savedObject.unableToAddErrorMessage',
defaultMessage: 'Unable to add {errorsLength} of {savedObjectsLength} kibana objects, Error: ${errors[0].error.message}'
defaultMessage: 'Unable to add {errorsLength} of {savedObjectsLength} kibana objects, Error: {errorMessage}'
},
{ errorsLength: errors.length, savedObjectsLength: this.props.savedObjects.length })
{ errorsLength: errors.length, savedObjectsLength: this.props.savedObjects.length, errorMessage: errors[0].error.message })
: this.props.intl.formatMessage(
{ id: 'kbn.home.tutorial.savedObject.addedLabel',
defaultMessage: '{savedObjectsLength} saved objects successfully added'

View file

@ -10,8 +10,28 @@ exports[`i18n utils should create verbose parser error message 1`] = `
`;
exports[`i18n utils should not escape linebreaks 1`] = `
"Text
"Text
with
line-breaks
"
`;
exports[`i18n utils should throw if "values" has a value that is unused in the message 1`] = `
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
[url]."
`;
exports[`i18n utils should throw if "values" property is not provided and defaultMessage requires it 1`] = `
"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"):
[username,password,url]."
`;
exports[`i18n utils should throw if "values" property is provided and defaultMessage doesn't include any references 1`] = `
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
[url,username]."
`;
exports[`i18n utils should throw if some key is missing in "values" 1`] = `
"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"):
[password]."
`;

View file

@ -19,3 +19,4 @@
export const DEFAULT_MESSAGE_KEY = 'defaultMessage';
export const CONTEXT_KEY = 'context';
export const VALUES_KEY = 'values';

View file

@ -14,7 +14,7 @@ Array [
exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`;
exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"defaultMessage value in Handlebars i18n should be a string (\\"message-id\\")."`;
exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`;

View file

@ -20,10 +20,10 @@ Array [
]
`;
exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`;
exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id should be a string literal."`;
exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Object with defaultMessage property is not passed to i18n() or i18n.translate() function call (\\"message-id\\")."`;
exports[`dev/i18n/extractors/i18n_call throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { formatJSString } from '../utils';
import { formatJSString, checkValuesProperty } from '../utils';
import { createFailError } from '../../run';
const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g;
@ -57,28 +57,38 @@ export function* extractHandlebarsMessages(buffer) {
}
const properties = JSON.parse(propertiesString.slice(1, -1));
const message = formatJSString(properties.defaultMessage);
if (typeof message !== 'string') {
if (typeof properties.defaultMessage !== 'string') {
throw createFailError(
`defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
);
}
if (properties.context != null && typeof properties.context !== 'string') {
throw createFailError(
`Context value in Handlebars i18n should be a string ("${messageId}").`
);
}
const message = formatJSString(properties.defaultMessage);
const context = formatJSString(properties.context);
if (!message) {
throw createFailError(
`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`
);
}
const context = formatJSString(properties.context);
const valuesObject = properties.values;
if (context != null && typeof context !== 'string') {
if (valuesObject != null && typeof valuesObject !== 'object') {
throw createFailError(
`Context value in Handlebars i18n should be a string ("${messageId}").`
`"values" value should be an object in Handlebars i18n ("${messageId}").`
);
}
checkValuesProperty(Object.keys(valuesObject || {}), message, messageId);
yield [messageId, { message, context }];
}
}

View file

@ -19,16 +19,20 @@
import cheerio from 'cheerio';
import { parse } from '@babel/parser';
import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types';
import { isDirectiveLiteral, isObjectExpression } from '@babel/types';
import {
isPropertyWithKey,
formatHTMLString,
formatJSString,
traverseNodes,
checkValuesProperty,
createParserErrorMessage,
extractMessageValueFromNode,
extractValuesKeysFromNode,
extractContextValueFromNode,
} from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY, VALUES_KEY } from '../constants';
import { createFailError } from '../../run';
/**
@ -41,9 +45,10 @@ const I18N_FILTER_MARKER = '| i18n: ';
/**
* Extract default message from an angular filter expression argument
* @param {string} expression JavaScript code containing a filter object
* @returns {string} Default message
* @param {string} messageId id of the message
* @returns {{ message?: string, context?: string, valuesKeys: string[]] }}
*/
function parseFilterObjectExpression(expression) {
function parseFilterObjectExpression(expression, messageId) {
let ast;
try {
@ -60,34 +65,33 @@ function parseFilterObjectExpression(expression) {
throw error;
}
for (const node of traverseNodes(ast.program.body)) {
if (!isObjectExpression(node)) {
continue;
}
const objectExpressionNode = [...traverseNodes(ast.program.body)].find(node =>
isObjectExpression(node)
);
let message;
let context;
for (const property of node.properties) {
if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) {
if (!isStringLiteral(property.value)) {
throw createFailError(`defaultMessage value should be a string literal.`);
}
message = formatJSString(property.value.value);
} else if (isPropertyWithKey(property, CONTEXT_KEY)) {
if (!isStringLiteral(property.value)) {
throw createFailError(`context value should be a string literal.`);
}
context = formatJSString(property.value.value);
}
}
return { message, context };
if (!objectExpressionNode) {
return {};
}
return null;
const [messageProperty, contextProperty, valuesProperty] = [
DEFAULT_MESSAGE_KEY,
CONTEXT_KEY,
VALUES_KEY,
].map(key => objectExpressionNode.properties.find(property => isPropertyWithKey(property, key)));
const message = messageProperty
? formatJSString(extractMessageValueFromNode(messageProperty.value, messageId))
: undefined;
const context = contextProperty
? formatJSString(extractContextValueFromNode(contextProperty.value, messageId))
: undefined;
const valuesKeys = valuesProperty
? extractValuesKeysFromNode(valuesProperty.value, messageId)
: [];
return { message, context, valuesKeys };
}
function parseIdExpression(expression) {
@ -106,13 +110,10 @@ function parseIdExpression(expression) {
throw error;
}
for (const node of traverseNodes(ast.program.directives)) {
if (isDirectiveLiteral(node)) {
return formatJSString(node.value);
}
}
return null;
const stringNode = [...traverseNodes(ast.program.directives)].find(node =>
isDirectiveLiteral(node)
);
return stringNode ? formatJSString(stringNode.value) : null;
}
function trimCurlyBraces(string) {
@ -170,7 +171,10 @@ function* getFilterMessages(htmlContent) {
throw createFailError(`Empty "id" value in angular filter expression is not allowed.`);
}
const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {};
const { message, context, valuesKeys } = parseFilterObjectExpression(
filterObjectExpression,
messageId
);
if (!message) {
throw createFailError(
@ -178,6 +182,8 @@ function* getFilterMessages(htmlContent) {
);
}
checkValuesProperty(valuesKeys, message, messageId);
yield [messageId, { message, context }];
}
}
@ -185,14 +191,17 @@ function* getFilterMessages(htmlContent) {
function* getDirectiveMessages(htmlContent) {
const $ = cheerio.load(htmlContent);
const elements = $('[i18n-id]').map(function (idx, el) {
const $el = $(el);
return {
id: $el.attr('i18n-id'),
defaultMessage: $el.attr('i18n-default-message'),
context: $el.attr('i18n-context'),
};
}).toArray();
const elements = $('[i18n-id]')
.map(function (idx, el) {
const $el = $(el);
return {
id: $el.attr('i18n-id'),
defaultMessage: $el.attr('i18n-default-message'),
context: $el.attr('i18n-context'),
values: $el.attr('i18n-values'),
};
})
.toArray();
for (const element of elements) {
const messageId = formatHTMLString(element.id);
@ -207,6 +216,16 @@ function* getDirectiveMessages(htmlContent) {
);
}
if (element.values) {
const nodes = parse(`+${element.values}`).program.body;
const valuesObjectNode = [...traverseNodes(nodes)].find(node => isObjectExpression(node));
const valuesKeys = extractValuesKeysFromNode(valuesObjectNode);
checkValuesProperty(valuesKeys, message, messageId);
} else {
checkValuesProperty([], message, messageId);
}
yield [messageId, { message, context: formatHTMLString(element.context) || undefined }];
}
}

View file

@ -17,10 +17,18 @@
* under the License.
*/
import { isObjectExpression, isStringLiteral } from '@babel/types';
import { isObjectExpression } from '@babel/types';
import { isPropertyWithKey, formatJSString } from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants';
import {
isPropertyWithKey,
formatJSString,
checkValuesProperty,
extractMessageIdFromNode,
extractMessageValueFromNode,
extractContextValueFromNode,
extractValuesKeysFromNode,
} from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY, VALUES_KEY } from '../constants';
import { createFailError } from '../../run';
/**
@ -28,45 +36,31 @@ import { createFailError } from '../../run';
*/
export function extractI18nCallMessages(node) {
const [idSubTree, optionsSubTree] = node.arguments;
if (!isStringLiteral(idSubTree)) {
throw createFailError(`Message id in i18n() or i18n.translate() should be a string literal.`);
}
const messageId = idSubTree.value;
const messageId = extractMessageIdFromNode(idSubTree);
if (!messageId) {
throw createFailError(`Empty "id" value in i18n() or i18n.translate() is not allowed.`);
}
let message;
let context;
if (!isObjectExpression(optionsSubTree)) {
throw createFailError(
`Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").`
`Object with defaultMessage property is not passed to i18n() or i18n.translate() function call ("${messageId}").`
);
}
for (const prop of optionsSubTree.properties) {
if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) {
if (!isStringLiteral(prop.value)) {
throw createFailError(
`defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
);
}
const [messageProperty, contextProperty, valuesProperty] = [
DEFAULT_MESSAGE_KEY,
CONTEXT_KEY,
VALUES_KEY,
].map(key => optionsSubTree.properties.find(property => isPropertyWithKey(property, key)));
message = formatJSString(prop.value.value);
} else if (isPropertyWithKey(prop, CONTEXT_KEY)) {
if (!isStringLiteral(prop.value)) {
throw createFailError(
`context value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
);
}
const message = messageProperty
? formatJSString(extractMessageValueFromNode(messageProperty.value, messageId))
: undefined;
context = formatJSString(prop.value.value);
}
}
const context = contextProperty
? formatJSString(extractContextValueFromNode(contextProperty.value, messageId))
: undefined;
if (!message) {
throw createFailError(
@ -74,5 +68,11 @@ export function extractI18nCallMessages(node) {
);
}
const valuesKeys = valuesProperty
? extractValuesKeysFromNode(valuesProperty.value, messageId)
: [];
checkValuesProperty(valuesKeys, message, messageId);
return [messageId, { message, context }];
}

View file

@ -17,43 +17,28 @@
* under the License.
*/
import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types';
import { isJSXIdentifier, isObjectExpression, isJSXExpressionContainer } from '@babel/types';
import { isPropertyWithKey, formatJSString, formatHTMLString } from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants';
import {
isPropertyWithKey,
formatJSString,
formatHTMLString,
extractMessageIdFromNode,
extractMessageValueFromNode,
extractContextValueFromNode,
extractValuesKeysFromNode,
checkValuesProperty,
} from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY, VALUES_KEY } from '../constants';
import { createFailError } from '../../run';
function extractMessageId(value) {
if (!isStringLiteral(value)) {
throw createFailError(`Message id should be a string literal.`);
}
return value.value;
}
function extractMessageValue(value, id) {
if (!isStringLiteral(value)) {
throw createFailError(`defaultMessage value should be a string literal ("${id}").`);
}
return value.value;
}
function extractContextValue(value, id) {
if (!isStringLiteral(value)) {
throw createFailError(`context value should be a string literal ("${id}").`);
}
return value.value;
}
/**
* Extract default messages from ReactJS intl.formatMessage(...) AST
* @param node Babel parser AST node
* @returns {[string, string][]} Array of id-message tuples
*/
export function extractIntlMessages(node) {
const options = node.arguments[0];
const [options, valuesNode] = node.arguments;
if (!isObjectExpression(options)) {
throw createFailError(
@ -68,7 +53,7 @@ export function extractIntlMessages(node) {
].map(key => options.properties.find(property => isPropertyWithKey(property, key)));
const messageId = messageIdProperty
? formatJSString(extractMessageId(messageIdProperty.value))
? formatJSString(extractMessageIdFromNode(messageIdProperty.value))
: undefined;
if (!messageId) {
@ -76,7 +61,11 @@ export function extractIntlMessages(node) {
}
const message = messageProperty
? formatJSString(extractMessageValue(messageProperty.value, messageId))
? formatJSString(extractMessageValueFromNode(messageProperty.value, messageId))
: undefined;
const context = contextProperty
? formatJSString(extractContextValueFromNode(contextProperty.value, messageId))
: undefined;
if (!message) {
@ -85,9 +74,9 @@ export function extractIntlMessages(node) {
);
}
const context = contextProperty
? formatJSString(extractContextValue(contextProperty.value, messageId))
: undefined;
const valuesKeys = valuesNode ? extractValuesKeysFromNode(valuesNode, messageId) : [];
checkValuesProperty(valuesKeys, message, messageId);
return [messageId, { message, context }];
}
@ -98,22 +87,27 @@ export function extractIntlMessages(node) {
* @returns {[string, string][]} Array of id-message tuples
*/
export function extractFormattedMessages(node) {
const [messageIdProperty, messageProperty, contextProperty] = [
const [messageIdAttribute, messageAttribute, contextAttribute, valuesAttribute] = [
'id',
DEFAULT_MESSAGE_KEY,
CONTEXT_KEY,
VALUES_KEY,
].map(key => node.attributes.find(attribute => isJSXIdentifier(attribute.name, { name: key })));
const messageId = messageIdProperty
? formatHTMLString(extractMessageId(messageIdProperty.value))
const messageId = messageIdAttribute
? formatHTMLString(extractMessageIdFromNode(messageIdAttribute.value))
: undefined;
if (!messageId) {
throw createFailError(`Empty "id" value in <FormattedMessage> is not allowed.`);
}
const message = messageProperty
? formatHTMLString(extractMessageValue(messageProperty.value, messageId))
const message = messageAttribute
? formatHTMLString(extractMessageValueFromNode(messageAttribute.value, messageId))
: undefined;
const context = contextAttribute
? formatHTMLString(extractContextValueFromNode(contextAttribute.value, messageId))
: undefined;
if (!message) {
@ -122,9 +116,21 @@ export function extractFormattedMessages(node) {
);
}
const context = contextProperty
? formatHTMLString(extractContextValue(contextProperty.value, messageId))
: undefined;
if (
valuesAttribute &&
(!isJSXExpressionContainer(valuesAttribute.value) ||
!isObjectExpression(valuesAttribute.value.expression))
) {
throw createFailError(
`"values" value in <FormattedMessage> should be an object ("${messageId}").`
);
}
const valuesKeys = valuesAttribute
? extractValuesKeysFromNode(valuesAttribute.value.expression, messageId)
: [];
checkValuesProperty(valuesKeys, message, messageId);
return [messageId, { message, context }];
}

View file

@ -20,14 +20,19 @@
import {
isCallExpression,
isIdentifier,
isObjectProperty,
isMemberExpression,
isNode,
isObjectExpression,
isObjectProperty,
isStringLiteral,
} from '@babel/types';
import fs from 'fs';
import glob from 'glob';
import { promisify } from 'util';
import chalk from 'chalk';
import parser from 'intl-messageformat-parser';
import { createFailError } from '../run';
const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g;
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;
@ -36,6 +41,10 @@ export const readFileAsync = promisify(fs.readFile);
export const writeFileAsync = promisify(fs.writeFile);
export const globAsync = promisify(glob);
export function difference(left = [], right = []) {
return left.filter(value => !right.includes(value));
}
export function isPropertyWithKey(property, identifierName) {
return isObjectProperty(property) && isIdentifier(property.key, { name: identifierName });
}
@ -114,3 +123,102 @@ export function createParserErrorMessage(content, error) {
return `${error.message}:\n${context}`;
}
/**
* Checks whether values from "values" and "defaultMessage" correspond to each other.
*
* @param {string[]} valuesKeys array of "values" property keys
* @param {string} defaultMessage "defaultMessage" value
* @param {string} messageId message id for fail errors
* @throws if "values" and "defaultMessage" don't correspond to each other
*/
export function checkValuesProperty(valuesKeys, defaultMessage, messageId) {
// skip validation if defaultMessage doesn't use ICU and values prop has no keys
if (!valuesKeys.length && !defaultMessage.includes('{')) {
return;
}
let defaultMessageAst;
try {
defaultMessageAst = parser.parse(defaultMessage);
} catch (error) {
if (error.name === 'SyntaxError') {
const errorWithContext = createParserErrorMessage(defaultMessage, {
loc: {
line: error.location.start.line,
column: error.location.start.column - 1,
},
message: error.message,
});
throw createFailError(
`Couldn't parse default message ("${messageId}"):\n${errorWithContext}`
);
}
throw error;
}
const ARGUMENT_ELEMENT_TYPE = 'argumentElement';
// skip validation if intl-messageformat-parser didn't return an AST with nonempty elements array
if (!defaultMessageAst || !defaultMessageAst.elements || !defaultMessageAst.elements.length) {
return;
}
const defaultMessageValueReferences = defaultMessageAst.elements.reduce((keys, element) => {
if (element.type === ARGUMENT_ELEMENT_TYPE) {
keys.push(element.id);
}
return keys;
}, []);
const missingValuesKeys = difference(defaultMessageValueReferences, valuesKeys);
if (missingValuesKeys.length) {
throw createFailError(
`some properties are missing in "values" object ("${messageId}"):\n[${missingValuesKeys}].`
);
}
const unusedValuesKeys = difference(valuesKeys, defaultMessageValueReferences);
if (unusedValuesKeys.length) {
throw createFailError(
`"values" object contains unused properties ("${messageId}"):\n[${unusedValuesKeys}].`
);
}
}
export function extractMessageIdFromNode(node) {
if (!isStringLiteral(node)) {
throw createFailError(`Message id should be a string literal.`);
}
return node.value;
}
export function extractMessageValueFromNode(node, messageId) {
if (!isStringLiteral(node)) {
throw createFailError(`defaultMessage value should be a string literal ("${messageId}").`);
}
return node.value;
}
export function extractContextValueFromNode(node, messageId) {
if (!isStringLiteral(node)) {
throw createFailError(`context value should be a string literal ("${messageId}").`);
}
return node.value;
}
export function extractValuesKeysFromNode(node, messageId) {
if (!isObjectExpression(node)) {
throw createFailError(`"values" value should be an object expression ("${messageId}").`);
}
return node.properties.map(
property => (isStringLiteral(property.key) ? property.key.value : property.key.name)
);
}

View file

@ -25,6 +25,7 @@ import {
isPropertyWithKey,
traverseNodes,
formatJSString,
checkValuesProperty,
createParserErrorMessage,
} from './utils';
@ -53,7 +54,7 @@ describe('i18n utils', () => {
test('should not escape linebreaks', () => {
expect(
formatJSString(`Text \n with
formatJSString(`Text\n with
line-breaks
`)
).toMatchSnapshot();
@ -104,4 +105,52 @@ describe('i18n utils', () => {
expect(createParserErrorMessage(content, error)).toMatchSnapshot();
}
});
test('should validate conformity of "values" and "defaultMessage"', () => {
const valuesKeys = ['url', 'username', 'password'];
const defaultMessage = 'Test message with {username}, {password} and [markdown link]({url}).';
const messageId = 'namespace.message.id';
expect(() => checkValuesProperty(valuesKeys, defaultMessage, messageId)).not.toThrow();
});
test('should throw if "values" has a value that is unused in the message', () => {
const valuesKeys = ['username', 'url', 'password'];
const defaultMessage = 'Test message with {username} and {password}.';
const messageId = 'namespace.message.id';
expect(() =>
checkValuesProperty(valuesKeys, defaultMessage, messageId)
).toThrowErrorMatchingSnapshot();
});
test('should throw if some key is missing in "values"', () => {
const valuesKeys = ['url', 'username'];
const defaultMessage = 'Test message with {username}, {password} and [markdown link]({url}).';
const messageId = 'namespace.message.id';
expect(() =>
checkValuesProperty(valuesKeys, defaultMessage, messageId)
).toThrowErrorMatchingSnapshot();
});
test('should throw if "values" property is not provided and defaultMessage requires it', () => {
const valuesKeys = [];
const defaultMessage = 'Test message with {username}, {password} and [markdown link]({url}).';
const messageId = 'namespace.message.id';
expect(() =>
checkValuesProperty(valuesKeys, defaultMessage, messageId)
).toThrowErrorMatchingSnapshot();
});
test(`should throw if "values" property is provided and defaultMessage doesn't include any references`, () => {
const valuesKeys = ['url', 'username'];
const defaultMessage = 'Test message';
const messageId = 'namespace.message.id';
expect(() =>
checkValuesProperty(valuesKeys, defaultMessage, messageId)
).toThrowErrorMatchingSnapshot();
});
});

View file

@ -8351,7 +8351,7 @@ intl-format-cache@^2.0.5, intl-format-cache@^2.1.0:
resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316"
integrity sha1-BKNp/sv61tpgBbrh8UMzMy3PkxY=
intl-messageformat-parser@1.4.0:
intl-messageformat-parser@1.4.0, intl-messageformat-parser@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075"
integrity sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=