Issue #3493070 by penyaskito, griffynh, wim leers, longwave, pdureau, effulgentsia, xjm, phenaproxima, mradcliffe, danielveza, lauriii, catch: SDC `enum` props should have translatable labels: use `meta:enum`

merge-requests/11791/merge
Lee Rowlands 2025-06-05 10:06:29 +10:00
parent 6bdfd060b1
commit 9d55d1e60e
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
17 changed files with 691 additions and 13 deletions

View File

@ -2,6 +2,24 @@
"$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata-full.schema.json",
"$schema": "http://json-schema.org/draft-04/schema#",
"$defs": {
"propDefinition": {
"$ref": "http://json-schema.org/draft-04/schema#",
"meta:enum": {
"type": "object",
"minItems": 1,
"uniqueItems": true,
"patternProperties": {
"additionalProperties": false,
"^[a-zA-Z0-9_-]*$": {
"type": "string"
}
}
},
"x-translation-context": {
"type": "string",
"title": "Translation Context"
}
},
"slotDefinition": {
"type": "object",
"additionalProperties": false,
@ -160,7 +178,7 @@
]
},
"props": {
"$ref": "http://json-schema.org/draft-04/schema#"
"$ref": "#/$defs/propDefinition"
},
"slots": {
"$ref": "metadata.schema.json#/$defs/slotDefinition"

View File

@ -2,6 +2,24 @@
"$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json",
"$schema": "http://json-schema.org/draft-04/schema#",
"$defs": {
"propDefinition": {
"$ref": "http://json-schema.org/draft-04/schema#",
"meta:enum": {
"type": "object",
"minItems": 1,
"uniqueItems": true,
"patternProperties": {
"additionalProperties": false,
"^[a-zA-Z0-9_-]*$": {
"type": "string"
}
}
},
"x-translation-context": {
"type": "string",
"title": "Translation Context"
}
},
"slotDefinition": {
"type": "object",
"additionalProperties": false,
@ -208,7 +226,7 @@
]
},
"props": {
"$ref": "http://json-schema.org/draft-04/schema#"
"$ref": "#/$defs/propDefinition"
},
"slots": {
"$ref": "#/$defs/slotDefinition"

View File

@ -13,6 +13,11 @@ class ComponentMetadata {
use StringTranslationTrait;
/**
* The ID of the component, in the form of provider:machine_name.
*/
public readonly string $id;
/**
* The absolute path to the component directory.
*
@ -115,6 +120,7 @@ class ComponentMetadata {
if (str_starts_with($path, $app_root)) {
$path = substr($path, strlen($app_root));
}
$this->id = $metadata_info['id'];
$this->mandatorySchemas = $enforce_schemas;
$this->path = $path;
@ -149,7 +155,7 @@ class ComponentMetadata {
private function parseSchemaInfo(array $metadata_info): ?array {
if (empty($metadata_info['props'])) {
if ($this->mandatorySchemas) {
throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $metadata_info['id']));
throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $this->id));
}
$schema = NULL;
}
@ -167,6 +173,12 @@ class ComponentMetadata {
$schema_props = $metadata_info['props'];
foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) {
$type = $prop_schema['type'] ?? '';
if (isset($prop_schema['enum'], $prop_schema['meta:enum'])) {
$enum_keys_diff = array_diff($prop_schema['enum'], array_keys($prop_schema['meta:enum']));
if (!empty($enum_keys_diff)) {
throw new InvalidComponentException(sprintf('The values for the %s prop enum in component %s must be defined in meta:enum.', $name, $this->id));
}
}
$schema['properties'][$name]['type'] = array_unique([
...(array) $type,
'object',
@ -197,6 +209,14 @@ class ComponentMetadata {
* The normalized value object.
*/
public function normalize(): array {
$meta = [];
if (!empty($this->schema['properties'])) {
foreach ($this->schema['properties'] as $prop_name => $prop_definition) {
if (!empty($prop_definition['meta:enum'])) {
$meta['properties'][$prop_name] = $this->getEnumOptions($prop_name);
}
}
}
return [
'path' => $this->path,
'machineName' => $this->machineName,
@ -204,7 +224,42 @@ class ComponentMetadata {
'name' => $this->name,
'group' => $this->group,
'variants' => $this->variants,
'meta' => $meta,
];
}
/**
* Get translated options labels from enumeration.
*
* @param string $propertyName
* The enum property name.
*
* @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup>
* An array with enum options as keys and the (non-rendered)
* translated labels as values.
*/
public function getEnumOptions(string $propertyName): array {
$options = [];
if (isset($this->schema['properties'][$propertyName])) {
$prop_definition = $this->schema['properties'][$propertyName];
if (!empty($prop_definition['enum'])) {
$translation_context = $prop_definition['x-translation-context'] ?? '';
// We convert ['a', 'b'], into ['a' => t('a'), 'b' => t('b')].
$options = array_combine(
$prop_definition['enum'],
// @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
array_map(fn($value) => $this->t($value, [], ['context' => $translation_context]), $prop_definition['enum']),
);
if (!empty($prop_definition['meta:enum'])) {
foreach ($prop_definition['meta:enum'] as $enum_value => $enum_label) {
$options[$enum_value] =
// @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
$this->t($enum_label, [], ['context' => $translation_context]);
}
}
}
}
return $options;
}
}

View File

@ -21,6 +21,9 @@ props:
enum:
- info
- success
meta:enum:
info: Information
success: Success
slots:
label:
type: string

View File

@ -23,6 +23,9 @@ props:
enum:
- ellipsis
- xs
meta:enum:
ellipsis: Ellipsis
xs: 'Extra-small'
extra_classes:
type: array
title: Extra classes.
@ -43,6 +46,15 @@ props:
- h5
- h6
- span
meta:enum:
h1: Heading 1
h2: Heading 2
h3: Heading 3
h4: Heading 4
h5: Heading 5
h6: Heading 6
span: Inline
x-translation-context: HTML tag
# Provide a default value
default: h2
icon:

View File

@ -37,6 +37,17 @@ props:
- primary
- small-offset
- weight--400
meta:enum:
collapsible: Collapsible
dark: Dark
expand--down: Expands down
expand--side: Expands to the side
large: Large
non-interactive: Non-Interactive
primary: Primary
small-offset: Small offset
weight--400: Weight 400
x-translation-context: Toolbar button modifiers
extra_classes:
type: array
title: Extra classes.
@ -53,6 +64,11 @@ props:
- a
- button
- span
meta:enum:
a: Link
button: Button
span: Inline
x-translation-context: HTML tag
# Provide a default value
default: button
icon:

View File

@ -29,6 +29,10 @@ props:
enum:
- ''
- _blank
meta:enum:
'': 'Open in same window'
_blank: 'Open in a new window'
x-translation-context: Banner link target
image:
title: Media Image
description: Background image for the banner.

View File

@ -7,6 +7,7 @@
<div {{ attributes }}>
<div class="component--my-banner--header">
<h3>{{ heading }}</h3>
<p>CTA target selected value: {{ componentMetadata.meta.properties.ctaTarget[ctaTarget] }}</p>
{% include 'sdc_test:my-cta' with { text: ctaText, href: ctaHref, target: ctaTarget } only %}
</div>
<div class="component--my-banner--body">

View File

@ -24,3 +24,7 @@ props:
- power
- like
- external
meta:enum:
power: 'Power'
like: 'Like'
external: 'External'

View File

@ -17,12 +17,23 @@ props:
type: string
title: URL
format: uri
examples:
- https://drupal.org
target:
type: string
title: Target
description: The target for opening the link.
enum:
- ''
- _blank
- '_blank'
meta:enum:
'': 'Open in same window'
_blank: 'Open in a new window'
x-translation-context: CTA link target
default: ''
examples:
- ''
- '_blank'
attributes:
type: Drupal\Core\Template\Attribute
name: Attributes

View File

@ -6,5 +6,5 @@
{% endif %}
<a {{ attributes }} href="{{ href }}">
{{ text }}
{{ text }} ({{ componentMetadata.meta.properties.target[target] }})
</a>

View File

@ -24,3 +24,7 @@ props:
- power
- like
- external
meta:enum:
power: 'Power'
like: 'Like'
external: 'External'

View File

@ -27,6 +27,12 @@ props:
- timer
- serves
- difficulty
meta:enum:
knife: Knife
timer: Timer
serves: Serves
difficulty: Difficulty
x-translation-context: Umami recipes icon names
slots:
label:
type: string

View File

@ -24,6 +24,10 @@ props:
enum:
- article
- div
meta:enum:
article: Article
div: Container
x-translation-context: HTML tag
# Provide a default value
default: article

View File

@ -30,6 +30,15 @@ props:
- h5
- h6
- span
meta:enum:
h1: Heading 1
h2: Heading 2
h3: Heading 3
h4: Heading 4
h5: Heading 5
h6: Heading 6
span: Inline
x-translation-context: HTML tag
# Provide a default value
default: h2

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
// cspell:ignore Abre er bânnêh en una nueba bentana la mîmma
namespace Drupal\KernelTests\Components;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\locale\StringInterface;
use Drupal\locale\StringStorageInterface;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;
/**
* Tests the component can be translated.
*
* @group sdc
*/
class ComponentTranslationTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'sdc_test',
'locale',
'language',
];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected StringStorageInterface $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = $this->container->get('locale.storage');
ConfigurableLanguage::createFromLangcode('epa')->save();
$this->container->get('string_translation')->setDefaultLangcode('epa');
$this->installSchema('locale', [
'locales_location',
'locales_source',
'locales_target',
]);
}
/**
* Test that components render enum props correctly with their translations.
*/
public function testEnumPropsCanBeTranslated(): void {
$bannerString = $this->buildSourceString(['source' => 'Open in a new window', 'context' => 'Banner link target']);
$bannerString->save();
$ctaString = $this->buildSourceString(['source' => 'Open in a new window', 'context' => 'CTA link target']);
$ctaString->save();
$ctaEmptyString = $this->buildSourceString(['source' => 'Open in same window', 'context' => 'CTA link target']);
$ctaEmptyString->save();
$this->createTranslation($bannerString, 'epa', ['translation' => 'Abre er bânnêh en una nueba bentana']);
$this->createTranslation($ctaString, 'epa', ['translation' => 'Abre er CTA en una nueba bentana']);
$this->createTranslation($ctaEmptyString, 'epa', ['translation' => 'Abre er CTA en la mîmma bentana']);
$build = [
'banner' => [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'heading' => 'I am a banner',
'ctaText' => 'Click me',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '_blank',
],
],
'cta' => [
'#type' => 'component',
'#component' => 'sdc_test:my-cta',
'#props' => [
'text' => 'Click me',
'href' => 'https://www.example.org',
'target' => '_blank',
],
],
'cta_with_empty_enum' => [
'#type' => 'component',
'#component' => 'sdc_test:my-cta',
'#props' => [
'text' => 'Click me',
'href' => 'https://www.example.org',
'target' => '',
],
],
];
\Drupal::state()->set('sdc_test_component', $build);
$response = $this->request(Request::create('sdc-test-component'));
$crawler = new Crawler($response->getContent());
// Assert that even if the source is the same, the translations depend on
// the enum context.
$this->assertStringContainsString('Abre er bânnêh en una nueba bentana', $crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"]')->outerHtml());
$this->assertStringContainsString('Abre er CTA en una nueba bentana', $crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"]:nth-of-type(1)')->outerHtml());
$this->assertStringContainsString('Abre er CTA en la mîmma bentana', $crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"]:nth-of-type(2)')->outerHtml());
}
/**
* Creates random source string object.
*
* @param array $values
* The values array.
*
* @return \Drupal\locale\StringInterface
* A locale string.
*/
protected function buildSourceString(array $values = []): StringInterface {
return $this->storage->createString($values += [
'source' => $this->randomMachineName(100),
'context' => $this->randomMachineName(20),
]);
}
/**
* Creates single translation for source string.
*
* @param \Drupal\locale\StringInterface $source
* The source string.
* @param string $langcode
* The language code.
* @param array $values
* The values array.
*
* @return \Drupal\locale\StringInterface
* The translated string object.
*/
protected function createTranslation(StringInterface $source, $langcode, array $values = []): StringInterface {
return $this->storage->createTranslation($values + [
'lid' => $source->lid,
'language' => $langcode,
'translation' => $this->randomMachineName(100),
])->save();
}
/**
* Passes a request to the HTTP kernel and returns a response.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
protected function request(Request $request): Response {
// @todo We should replace this when https://drupal.org/i/3390193 lands.
// Reset the request stack.
// \Drupal\KernelTests\KernelTestBase::bootKernel() pushes a bogus request
// to boot the kernel, but it is also needed for any URL generation in tests
// to work. We also need to reset the request stack every time we make a
// request.
$request_stack = $this->container->get('request_stack');
while ($request_stack->getCurrentRequest() !== NULL) {
$request_stack->pop();
}
$http_kernel = $this->container->get('http_kernel');
self::assertInstanceOf(HttpKernelInterface::class, $http_kernel);
$response = $http_kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, FALSE);
$content = $response->getContent();
self::assertNotFalse($content);
$this->setRawContent($content);
self::assertInstanceOf(TerminableInterface::class, $http_kernel);
$http_kernel->terminate($request, $response);
return $response;
}
}

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Theme\Component\ComponentMetadata;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Tests\UnitTestCaseTest;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* Unit tests for the component metadata class.
@ -18,10 +20,13 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests that the correct data is returned for each property.
*
* @dataProvider dataProviderMetadata
*/
public function testMetadata(array $metadata_info, array $expectations): void {
#[DataProvider('dataProviderMetadata')]
public function testMetadata(array $metadata_info, array $expectations, bool $missing_schema, ?\Throwable $expectedException = NULL): void {
if ($expectedException !== NULL) {
$this->expectException($expectedException::class);
$this->expectExceptionMessage($expectedException->getMessage());
}
$metadata = new ComponentMetadata($metadata_info, 'foo/', FALSE);
$this->assertSame($expectations['path'], $metadata->path);
$this->assertSame($expectations['status'], $metadata->status);
@ -31,18 +36,23 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests the correct checks when enforcing schemas or not.
*
* @dataProvider dataProviderMetadata
*/
public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema): void {
#[DataProvider('dataProviderMetadata')]
public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema, ?\Throwable $expected_exception = NULL): void {
if ($missing_schema) {
$this->expectException(InvalidComponentException::class);
$this->expectExceptionMessage('The component "' . $metadata_info['id'] . '" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.');
new ComponentMetadata($metadata_info, 'foo/', TRUE);
}
else {
if ($expected_exception !== NULL) {
$this->expectException($expected_exception::class);
$this->expectExceptionMessage($expected_exception->getMessage());
}
new ComponentMetadata($metadata_info, 'foo/', TRUE);
$this->expectNotToPerformAssertions();
if ($expected_exception === NULL) {
$this->expectNotToPerformAssertions();
}
}
}
@ -71,7 +81,7 @@ class ComponentMetadataTest extends UnitTestCaseTest {
],
TRUE,
],
'complete example with schema' => [
'complete example with schema, but no meta:enum' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
@ -136,7 +146,321 @@ class ComponentMetadataTest extends UnitTestCaseTest {
],
FALSE,
],
'complete example with schema, but no matching meta:enum' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
'machineName' => 'my-button',
'path' => 'foo/my-other/path',
'name' => 'Button',
'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
'libraryOverrides' => ['dependencies' => ['core/drupal']],
'group' => 'my-group',
'props' => [
'type' => 'object',
'required' => ['text'],
'properties' => [
'text' => [
'type' => 'string',
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => 'string',
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'fav' => 'Favorite',
'external' => 'External',
],
],
],
],
],
[
'path' => 'my-other/path',
'status' => 'stable',
'thumbnail' => '',
'group' => 'my-group',
'additionalProperties' => FALSE,
'props' => [
'type' => 'object',
'required' => ['text'],
'additionalProperties' => FALSE,
'properties' => [
'text' => [
'type' => ['string', 'object'],
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => ['string', 'object'],
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'fav' => 'Favorite',
'external' => 'External',
],
],
],
],
],
FALSE,
new InvalidComponentException('The values for the iconType prop enum in component core:my-button must be defined in meta:enum.'),
],
'complete example with schema (including meta:enum)' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
'machineName' => 'my-button',
'path' => 'foo/my-other/path',
'name' => 'Button',
'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
'libraryOverrides' => ['dependencies' => ['core/drupal']],
'group' => 'my-group',
'props' => [
'type' => 'object',
'required' => ['text'],
'properties' => [
'text' => [
'type' => 'string',
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => 'string',
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'like' => 'Like',
'external' => 'External',
],
],
],
],
],
[
'path' => 'my-other/path',
'status' => 'stable',
'thumbnail' => '',
'group' => 'my-group',
'additionalProperties' => FALSE,
'props' => [
'type' => 'object',
'required' => ['text'],
'additionalProperties' => FALSE,
'properties' => [
'text' => [
'type' => ['string', 'object'],
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => ['string', 'object'],
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'like' => 'Like',
'external' => 'External',
],
],
],
],
],
FALSE,
],
'complete example with schema (including meta:enum and x-translation-context)' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
'machineName' => 'my-button',
'path' => 'foo/my-other/path',
'name' => 'Button',
'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
'libraryOverrides' => ['dependencies' => ['core/drupal']],
'group' => 'my-group',
'props' => [
'type' => 'object',
'required' => ['text'],
'properties' => [
'text' => [
'type' => 'string',
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => 'string',
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'like' => 'Like',
'external' => 'External',
],
'x-translation-context' => 'Icon Type',
],
],
],
],
[
'path' => 'my-other/path',
'status' => 'stable',
'thumbnail' => '',
'group' => 'my-group',
'additionalProperties' => FALSE,
'props' => [
'type' => 'object',
'required' => ['text'],
'additionalProperties' => FALSE,
'properties' => [
'text' => [
'type' => ['string', 'object'],
'title' => 'Title',
'description' => 'The title for the button',
'minLength' => 2,
'examples' => ['Press', 'Submit now'],
],
'iconType' => [
'type' => ['string', 'object'],
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
'meta:enum' => [
'power' => 'Power',
'like' => 'Like',
'external' => 'External',
],
'x-translation-context' => 'Icon Type',
],
],
],
],
FALSE,
],
];
}
public static function dataProviderEnumOptionsMetadata(): array {
$common_schema = [
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
'machineName' => 'my-button',
'path' => 'foo/my-other/path',
'name' => 'Button',
];
return [
'no meta:enum' => [$common_schema +
[
'props' => [
'type' => 'object',
'required' => ['text'],
'properties' => [
'iconType' => [
'type' => 'string',
'title' => 'Icon Type',
'enum' => [
'power',
'like',
'external',
],
],
],
],
],
'iconType',
[
'power' => 'power',
'like' => 'like',
'external' => 'external',
],
'',
],
'meta:enum, with x-translation-context' => [$common_schema +
[
'props' => [
'type' => 'object',
'required' => ['text'],
'properties' => [
'target' => [
'type' => 'string',
'title' => 'Icon Type',
'enum' => [
'',
'_blank',
],
'meta:enum' => [
'' => 'Opens in same window',
'_blank' => 'Opens in new window',
],
'x-translation-context' => 'Link target',
],
],
],
],
'target',
[
'' => 'Opens in same window',
'_blank' => 'Opens in new window',
],
'Link target',
],
];
}
/**
* @covers ::getEnumOptions
*/
#[DataProvider('dataProviderEnumOptionsMetadata')]
public function testGetEnumOptions(array $metadata_info, string $prop_name, array $expected_values, string $expected_context): void {
$translation = $this->getStringTranslationStub();
$container = new ContainerBuilder();
$container->set('string_translation', $translation);
\Drupal::setContainer($container);
$component_metadata = new ComponentMetadata($metadata_info, 'foo/', TRUE);
$options = $component_metadata->getEnumOptions($prop_name);
$rendered_options = array_map(fn($value) => (string) $value, $options);
$this->assertSame($expected_values, $rendered_options);
foreach ($options as $translatable) {
$this->assertSame($expected_context, $translatable->getOption('context'));
}
}
}