5 * Contains \Drupal\Tests\Core\Plugin\ContextHandlerTest.
8 namespace Drupal\Tests\Core\Plugin;
10 use Drupal\Component\Plugin\ConfigurablePluginInterface;
11 use Drupal\Component\Plugin\Exception\ContextException;
12 use Drupal\Core\Cache\NullBackend;
13 use Drupal\Core\DependencyInjection\ClassResolverInterface;
14 use Drupal\Core\DependencyInjection\ContainerBuilder;
15 use Drupal\Core\Extension\ModuleHandlerInterface;
16 use Drupal\Core\Plugin\Context\ContextDefinition;
17 use Drupal\Core\Plugin\Context\ContextHandler;
18 use Drupal\Core\Plugin\ContextAwarePluginInterface;
19 use Drupal\Core\TypedData\DataDefinition;
20 use Drupal\Core\TypedData\Plugin\DataType\StringData;
21 use Drupal\Core\TypedData\TypedDataManager;
22 use Drupal\Core\Validation\ConstraintManager;
23 use Drupal\Tests\UnitTestCase;
24 use Prophecy\Argument;
27 * @coversDefaultClass \Drupal\Core\Plugin\Context\ContextHandler
30 class ContextHandlerTest extends UnitTestCase {
33 * The context handler.
35 * @var \Drupal\Core\Plugin\Context\ContextHandler
37 protected $contextHandler;
42 protected function setUp() {
45 $this->contextHandler = new ContextHandler();
47 $namespaces = new \ArrayObject([
48 'Drupal\\Core\\TypedData' => $this->root . '/core/lib/Drupal/Core/TypedData',
49 'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation',
51 $cache_backend = new NullBackend('cache');
52 $module_handler = $this->prophesize(ModuleHandlerInterface::class);
53 $class_resolver = $this->prophesize(ClassResolverInterface::class);
54 $class_resolver->getInstanceFromDefinition(Argument::type('string'))->will(function ($arguments) {
55 $class_name = $arguments[0];
56 return new $class_name();
58 $type_data_manager = new TypedDataManager($namespaces, $cache_backend, $module_handler->reveal(), $class_resolver->reveal());
59 $type_data_manager->setValidationConstraintManager(
60 new ConstraintManager($namespaces, $cache_backend, $module_handler->reveal())
63 $container = new ContainerBuilder();
64 $container->set('typed_data_manager', $type_data_manager);
65 \Drupal::setContainer($container);
69 * @covers ::checkRequirements
71 * @dataProvider providerTestCheckRequirements
73 public function testCheckRequirements($contexts, $requirements, $expected) {
74 $this->assertSame($expected, $this->contextHandler->checkRequirements($contexts, $requirements));
78 * Provides data for testCheckRequirements().
80 public function providerTestCheckRequirements() {
81 $requirement_optional = new ContextDefinition();
82 $requirement_optional->setRequired(FALSE);
84 $requirement_any = new ContextDefinition();
85 $requirement_any->setRequired(TRUE);
87 $context_any = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
88 $context_any->expects($this->atLeastOnce())
89 ->method('getContextDefinition')
90 ->will($this->returnValue(new ContextDefinition('any')));
92 $requirement_specific = new ContextDefinition('string');
93 $requirement_specific->setConstraints(['Blank' => []]);
95 $context_constraint_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
96 $context_constraint_mismatch->expects($this->atLeastOnce())
97 ->method('getContextDefinition')
98 ->will($this->returnValue(new ContextDefinition('foo')));
99 $context_datatype_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
100 $context_datatype_mismatch->expects($this->atLeastOnce())
101 ->method('getContextDefinition')
102 ->will($this->returnValue(new ContextDefinition('fuzzy')));
104 $context_definition_specific = new ContextDefinition('string');
105 $context_definition_specific->setConstraints(['Blank' => []]);
106 $context_specific = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
107 $context_specific->expects($this->atLeastOnce())
108 ->method('getContextDefinition')
109 ->will($this->returnValue($context_definition_specific));
112 $data[] = [[], [], TRUE];
113 $data[] = [[], [$requirement_any], FALSE];
114 $data[] = [[], [$requirement_optional], TRUE];
115 $data[] = [[], [$requirement_any, $requirement_optional], FALSE];
116 $data[] = [[$context_any], [$requirement_any], TRUE];
117 $data[] = [[$context_constraint_mismatch], [$requirement_specific], FALSE];
118 $data[] = [[$context_datatype_mismatch], [$requirement_specific], FALSE];
119 $data[] = [[$context_specific], [$requirement_specific], TRUE];
125 * @covers ::getMatchingContexts
127 * @dataProvider providerTestGetMatchingContexts
129 public function testGetMatchingContexts($contexts, $requirement, $expected = NULL) {
130 if (is_null($expected)) {
131 $expected = $contexts;
133 $this->assertSame($expected, $this->contextHandler->getMatchingContexts($contexts, $requirement));
137 * Provides data for testGetMatchingContexts().
139 public function providerTestGetMatchingContexts() {
140 $requirement_any = new ContextDefinition();
142 $requirement_specific = new ContextDefinition('string');
143 $requirement_specific->setConstraints(['Blank' => []]);
145 $context_any = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
146 $context_any->expects($this->atLeastOnce())
147 ->method('getContextDefinition')
148 ->will($this->returnValue(new ContextDefinition('any')));
149 $context_constraint_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
150 $context_constraint_mismatch->expects($this->atLeastOnce())
151 ->method('getContextDefinition')
152 ->will($this->returnValue(new ContextDefinition('foo')));
153 $context_datatype_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
154 $context_datatype_mismatch->expects($this->atLeastOnce())
155 ->method('getContextDefinition')
156 ->will($this->returnValue(new ContextDefinition('fuzzy')));
157 $context_definition_specific = new ContextDefinition('string');
158 $context_definition_specific->setConstraints(['Blank' => []]);
159 $context_specific = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
160 $context_specific->expects($this->atLeastOnce())
161 ->method('getContextDefinition')
162 ->will($this->returnValue($context_definition_specific));
165 // No context will return no valid contexts.
166 $data[] = [[], $requirement_any];
167 // A context with a generic matching requirement is valid.
168 $data[] = [[$context_any], $requirement_any];
169 // A context with a specific matching requirement is valid.
170 $data[] = [[$context_specific], $requirement_specific];
172 // A context with a mismatched constraint is invalid.
173 $data[] = [[$context_constraint_mismatch], $requirement_specific, []];
174 // A context with a mismatched datatype is invalid.
175 $data[] = [[$context_datatype_mismatch], $requirement_specific, []];
181 * @covers ::filterPluginDefinitionsByContexts
183 * @dataProvider providerTestFilterPluginDefinitionsByContexts
185 public function testFilterPluginDefinitionsByContexts($has_context, $definitions, $expected) {
187 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
188 $expected_context_definition = (new ContextDefinition('string'))->setConstraints(['Blank' => []]);
189 $context->expects($this->atLeastOnce())
190 ->method('getContextDefinition')
191 ->will($this->returnValue($expected_context_definition));
192 $contexts = [$context];
198 $this->assertSame($expected, $this->contextHandler->filterPluginDefinitionsByContexts($contexts, $definitions));
202 * Provides data for testFilterPluginDefinitionsByContexts().
204 public function providerTestFilterPluginDefinitionsByContexts() {
208 // No context and no plugins, no plugins available.
209 $data[] = [FALSE, $plugins, []];
211 $plugins = ['expected_plugin' => []];
212 // No context, all plugins available.
213 $data[] = [FALSE, $plugins, $plugins];
215 $plugins = ['expected_plugin' => ['context' => []]];
216 // No context, all plugins available.
217 $data[] = [FALSE, $plugins, $plugins];
219 $plugins = ['expected_plugin' => ['context' => ['context1' => new ContextDefinition('string')]]];
220 // Missing context, no plugins available.
221 $data[] = [FALSE, $plugins, []];
222 // Satisfied context, all plugins available.
223 $data[] = [TRUE, $plugins, $plugins];
225 $mismatched_context_definition = (new ContextDefinition('expected_data_type'))->setConstraints(['mismatched_constraint_name' => 'mismatched_constraint_value']);
226 $plugins = ['expected_plugin' => ['context' => ['context1' => $mismatched_context_definition]]];
227 // Mismatched constraints, no plugins available.
228 $data[] = [TRUE, $plugins, []];
230 $optional_mismatched_context_definition = clone $mismatched_context_definition;
231 $optional_mismatched_context_definition->setRequired(FALSE);
232 $plugins = ['expected_plugin' => ['context' => ['context1' => $optional_mismatched_context_definition]]];
233 // Optional mismatched constraint, all plugins available.
234 $data[] = [FALSE, $plugins, $plugins];
236 $expected_context_definition = (new ContextDefinition('string'))->setConstraints(['Blank' => []]);
237 $plugins = ['expected_plugin' => ['context' => ['context1' => $expected_context_definition]]];
238 // Satisfied context with constraint, all plugins available.
239 $data[] = [TRUE, $plugins, $plugins];
241 $optional_expected_context_definition = clone $expected_context_definition;
242 $optional_expected_context_definition->setRequired(FALSE);
243 $plugins = ['expected_plugin' => ['context' => ['context1' => $optional_expected_context_definition]]];
244 // Optional unsatisfied context, all plugins available.
245 $data[] = [FALSE, $plugins, $plugins];
247 $unexpected_context_definition = (new ContextDefinition('unexpected_data_type'))->setConstraints(['mismatched_constraint_name' => 'mismatched_constraint_value']);
249 'unexpected_plugin' => ['context' => ['context1' => $unexpected_context_definition]],
250 'expected_plugin' => ['context' => ['context2' => new ContextDefinition('string')]],
252 // Context only satisfies one plugin.
253 $data[] = [TRUE, $plugins, ['expected_plugin' => $plugins['expected_plugin']]];
259 * @covers ::applyContextMapping
261 public function testApplyContextMapping() {
262 $context_hit_data = StringData::createInstance(DataDefinition::create('string'));
263 $context_hit_data->setValue('foo');
264 $context_hit = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
265 $context_hit->expects($this->atLeastOnce())
266 ->method('getContextData')
267 ->will($this->returnValue($context_hit_data));
268 $context_miss_data = StringData::createInstance(DataDefinition::create('string'));
269 $context_miss_data->setValue('bar');
270 $context_hit->expects($this->atLeastOnce())
271 ->method('hasContextValue')
273 $context_miss = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
274 $context_miss->expects($this->never())
275 ->method('getContextData');
278 'hit' => $context_hit,
279 'miss' => $context_miss,
282 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
284 $plugin = $this->getMock('Drupal\Core\Plugin\ContextAwarePluginInterface');
285 $plugin->expects($this->once())
286 ->method('getContextMapping')
288 $plugin->expects($this->once())
289 ->method('getContextDefinitions')
290 ->will($this->returnValue(['hit' => $context_definition]));
291 $plugin->expects($this->once())
292 ->method('setContextValue')
293 ->with('hit', $context_hit_data);
295 // Make sure that the cacheability metadata is passed to the plugin context.
296 $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
297 $plugin_context->expects($this->once())
298 ->method('addCacheableDependency')
299 ->with($context_hit);
300 $plugin->expects($this->once())
301 ->method('getContext')
303 ->willReturn($plugin_context);
305 $this->contextHandler->applyContextMapping($plugin, $contexts);
309 * @covers ::applyContextMapping
311 public function testApplyContextMappingMissingRequired() {
312 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
313 $context->expects($this->never())
314 ->method('getContextValue');
320 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
321 $context_definition->expects($this->atLeastOnce())
322 ->method('isRequired')
325 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
326 $plugin->expects($this->once())
327 ->method('getContextMapping')
329 $plugin->expects($this->once())
330 ->method('getContextDefinitions')
331 ->will($this->returnValue(['hit' => $context_definition]));
332 $plugin->expects($this->never())
333 ->method('setContextValue');
335 // No context, so no cacheability metadata can be passed along.
336 $plugin->expects($this->never())
337 ->method('getContext');
339 $this->setExpectedException(ContextException::class, 'Required contexts without a value: hit.');
340 $this->contextHandler->applyContextMapping($plugin, $contexts);
344 * @covers ::applyContextMapping
346 public function testApplyContextMappingMissingNotRequired() {
347 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
348 $context->expects($this->never())
349 ->method('getContextValue');
355 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
356 $context_definition->expects($this->atLeastOnce())
357 ->method('isRequired')
360 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
361 $plugin->expects($this->once())
362 ->method('getContextMapping')
363 ->willReturn(['optional' => 'missing']);
364 $plugin->expects($this->once())
365 ->method('getContextDefinitions')
366 ->will($this->returnValue(['optional' => $context_definition]));
367 $plugin->expects($this->never())
368 ->method('setContextValue');
370 // No context, so no cacheability metadata can be passed along.
371 $plugin->expects($this->never())
372 ->method('getContext');
374 $this->contextHandler->applyContextMapping($plugin, $contexts);
378 * @covers ::applyContextMapping
380 public function testApplyContextMappingNoValueRequired() {
381 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
382 $context->expects($this->never())
383 ->method('getContextValue');
384 $context->expects($this->atLeastOnce())
385 ->method('hasContextValue')
392 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
393 $context_definition->expects($this->atLeastOnce())
394 ->method('isRequired')
397 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
398 $plugin->expects($this->once())
399 ->method('getContextMapping')
401 $plugin->expects($this->once())
402 ->method('getContextDefinitions')
403 ->will($this->returnValue(['hit' => $context_definition]));
404 $plugin->expects($this->never())
405 ->method('setContextValue');
407 $this->setExpectedException(ContextException::class, 'Required contexts without a value: hit.');
408 $this->contextHandler->applyContextMapping($plugin, $contexts);
413 * @covers ::applyContextMapping
415 public function testApplyContextMappingNoValueNonRequired() {
416 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
417 $context->expects($this->never())
418 ->method('getContextValue');
419 $context->expects($this->atLeastOnce())
420 ->method('hasContextValue')
427 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
428 $context_definition->expects($this->atLeastOnce())
429 ->method('isRequired')
432 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
433 $plugin->expects($this->once())
434 ->method('getContextMapping')
436 $plugin->expects($this->once())
437 ->method('getContextDefinitions')
438 ->will($this->returnValue(['hit' => $context_definition]));
439 $plugin->expects($this->never())
440 ->method('setContextValue');
442 $this->contextHandler->applyContextMapping($plugin, $contexts);
446 * @covers ::applyContextMapping
448 public function testApplyContextMappingConfigurableAssigned() {
449 $context_data = StringData::createInstance(DataDefinition::create('string'));
450 $context_data->setValue('foo');
451 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
452 $context->expects($this->atLeastOnce())
453 ->method('getContextData')
454 ->will($this->returnValue($context_data));
455 $context->expects($this->atLeastOnce())
456 ->method('hasContextValue')
463 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
465 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
466 $plugin->expects($this->once())
467 ->method('getContextMapping')
469 $plugin->expects($this->once())
470 ->method('getContextDefinitions')
471 ->will($this->returnValue(['hit' => $context_definition]));
472 $plugin->expects($this->once())
473 ->method('setContextValue')
474 ->with('hit', $context_data);
476 // Make sure that the cacheability metadata is passed to the plugin context.
477 $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
478 $plugin_context->expects($this->once())
479 ->method('addCacheableDependency')
481 $plugin->expects($this->once())
482 ->method('getContext')
484 ->willReturn($plugin_context);
486 $this->contextHandler->applyContextMapping($plugin, $contexts, ['hit' => 'name']);
490 * @covers ::applyContextMapping
492 public function testApplyContextMappingConfigurableAssignedMiss() {
493 $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
494 $context->expects($this->never())
495 ->method('getContextValue');
501 $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
503 $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
504 $plugin->expects($this->once())
505 ->method('getContextMapping')
507 $plugin->expects($this->once())
508 ->method('getContextDefinitions')
509 ->will($this->returnValue(['hit' => $context_definition]));
510 $plugin->expects($this->never())
511 ->method('setContextValue');
513 $this->setExpectedException(ContextException::class, 'Assigned contexts were not satisfied: miss');
514 $this->contextHandler->applyContextMapping($plugin, $contexts, ['miss' => 'name']);
519 interface TestConfigurableContextAwarePluginInterface extends ContextAwarePluginInterface, ConfigurablePluginInterface {