PageContextTest.php

Namespace

Drupal\Tests\navigation\Unit

File

core/modules/navigation/tests/src/Unit/PageContextTest.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Tests\navigation\Unit;

use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\navigation\EntityRouteHelper;
use Drupal\navigation\Plugin\TopBarItem\PageContext;
use Drupal\Tests\UnitTestCase;
use Drupal\workflows\StateInterface;
use Drupal\workflows\WorkflowInterface;
use Drupal\workflows\WorkflowTypeInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;

/**
 * Tests the PageContext Top Bar item build output.
 */
class PageContextTest extends UnitTestCase {
  use StringTranslationTrait;
  
  /**
   * {@inheritdoc}
   */
  protected function setUp() : void {
    parent::setUp();
    $container = new ContainerBuilder();
    $container->set('string_translation', $this->getStringTranslationStub());
    \Drupal::setContainer($container);
  }
  
  /**
   * Tests the build method when no entity is present on the route.
   */
  public function testBuildWhenNoEntityOnRoute() : void {
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn(NULL);
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class)
      ->reveal();
    $moderation_information = $this->prophesize(ModerationInformationInterface::class)
      ->reveal();
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, $moderation_information);
    $build = $plugin->build();
    $this->assertSame([
      '#cache' => [
        'contexts' => [
          'route',
        ],
      ],
    ], $build);
  }
  
  /**
   * Tests the build of an entity label within the page context plugin.
   *
   * @param mixed $label_value
   *   The return value of the entity label method. Can be a string,
   *   render array, stringable object, or an invalid value.
   * @param string|array|null $expected
   *   The expected parsed label value for the page context. If the label is
   *   invalid, the value should be NULL.
   */
  public function testBuildEntityLabel(mixed $label_value, string|array|null $expected) : void {
    // Route returns an entity with different label return types.
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->label()
      ->willReturn($label_value);
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class)
      ->reveal();
    $moderation_information = $this->prophesize(ModerationInformationInterface::class)
      ->reveal();
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, $moderation_information);
    $build = $plugin->build();
    $expected_base = [
      '#cache' => [
        'contexts' => [
          'route',
        ],
      ],
    ];
    if ($expected === NULL) {
      // Invalid label → no components added.
      $this->assertSame($expected_base, $build);
      return;
    }
    // Title component should be present as the first item.
    $this->assertArrayHasKey(0, $build);
    $this->assertSame('component', $build[0]['#type']);
    $this->assertSame('navigation:title', $build[0]['#component']);
    $this->assertSame('database', $build[0]['#props']['icon']);
    $this->assertSame($expected, $build[0]['#slots']['content']);
  }
  
  /**
   * Data provider for entity label scenarios.
   *
   * @return array
   *   [label_return_value, expected]
   */
  public static function entityLabelProvider() : array {
    $stringable = new class  implements \Stringable {
      
      /**
       * Dummy string method.
       *
       * @return string
       *   The dummy entity label.
       */
      public function __toString() : string {
        return 'Stringable Label';
      }

};
    return [
      'string' => [
        'My Label',
        'My Label',
      ],
      'stringable' => [
        $stringable,
        'Stringable Label',
      ],
      'render_array' => [
        [
          '#markup' => 'Rendered Label',
        ],
        NULL,
      ],
      'invalid' => [
        new \stdClass(),
        NULL,
      ],
    ];
  }
  
  /**
   * Tests the status badge for published and unpublished entities.
   */
  public function testBuildStatusBadge() : void {
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->willImplement(EntityPublishedInterface::class);
    $entity->isPublished()
      ->willReturn(TRUE);
    $entity->label()
      ->willReturn('Published Title');
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class)
      ->reveal();
    $published_plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, NULL);
    $build = $published_plugin->build();
    $this->assertSame('Published Title', $build[0]['#slots']['content']);
    $this->assertSame('navigation:badge', $build[1]['#component']);
    $this->assertSame('Published', $build[1]['#slots']['label']);
    $this->assertSame('success', $build[1]['#props']['status']);
    // Now assert the Unpublished path.
    $entity->isPublished()
      ->willReturn(FALSE);
    $entity->label()
      ->willReturn('Unpublished Title');
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, NULL);
    $build = $plugin->build();
    $this->assertSame('Unpublished Title', $build[0]['#slots']['content']);
    $this->assertSame('Unpublished', $build[1]['#slots']['label']);
    $this->assertSame('info', $build[1]['#props']['status']);
  }
  
  /**
   * Tests content moderation build output with no pending revisions.
   */
  public function testBuildContentModerationNoPending() : void {
    // Mock moderated content entity with state 'draft'.
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->get('moderation_state')
      ->willReturn((object) [
      'value' => 'draft',
    ]);
    $entity->isDefaultRevision()
      ->willReturn(TRUE);
    $entity->getEntityTypeId()
      ->willReturn('node');
    $entity->id()
      ->willReturn('1');
    $entity->label()
      ->willReturn('Example Title');
    // Workflow chain: workflow -> type plugin -> state('draft')->label() => 'Draft'.
    $state = $this->prophesize(StateInterface::class);
    $state->label()
      ->willReturn('Draft');
    $type = $this->prophesize(WorkflowTypeInterface::class);
    $type->getState('draft')
      ->willReturn($state->reveal());
    $workflow = $this->prophesize(WorkflowInterface::class);
    $workflow->getTypePlugin()
      ->willReturn($type->reveal());
    $moderation = $this->prophesize(ModerationInformationInterface::class);
    $moderation->isModeratedEntity($entity->reveal())
      ->willReturn(TRUE);
    $moderation->getWorkflowForEntity($entity->reveal())
      ->willReturn($workflow->reveal());
    $moderation->hasPendingRevision($entity->reveal())
      ->willReturn(FALSE);
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class)
      ->reveal();
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, $moderation->reveal());
    $build = $plugin->build();
    // Title component.
    $this->assertSame('Example Title', $build[0]['#slots']['content']);
    // Badge component with label Draft, default status info (not published interface).
    $this->assertSame('navigation:badge', $build[1]['#component']);
    $this->assertSame('Draft', $build[1]['#slots']['label']);
    $this->assertSame('info', $build[1]['#props']['status']);
  }
  
  /**
   * Tests the content moderation build when is active with a pending entity.
   */
  public function testBuildContentModerationWithPendingActive() : void {
    // Entity current state is 'draft', active is 'published'.
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->get('moderation_state')
      ->willReturn((object) [
      'value' => 'draft',
    ]);
    $entity->isDefaultRevision()
      ->willReturn(TRUE);
    $entity->getEntityTypeId()
      ->willReturn('node');
    $entity->id()
      ->willReturn('1');
    $entity->label()
      ->willReturn('Example Title');
    $active = $this->prophesize(ContentEntityInterface::class);
    $active->get('moderation_state')
      ->willReturn((object) [
      'value' => 'published',
    ]);
    // State objects and labels.
    $draft_state = $this->prophesize(StateInterface::class);
    $draft_state->label()
      ->willReturn('Draft');
    $published_state = $this->prophesize(StateInterface::class);
    $published_state->label()
      ->willReturn('Published');
    $type = $this->prophesize(WorkflowTypeInterface::class);
    $type->getState('draft')
      ->willReturn($draft_state->reveal());
    $type->getState('published')
      ->willReturn($published_state->reveal());
    $workflow = $this->prophesize(WorkflowInterface::class);
    $workflow->getTypePlugin()
      ->willReturn($type->reveal());
    $moderation = $this->prophesize(ModerationInformationInterface::class);
    $moderation->isModeratedEntity($entity->reveal())
      ->willReturn(TRUE);
    $moderation->getWorkflowForEntity($entity->reveal())
      ->willReturn($workflow->reveal());
    $moderation->getWorkflowForEntity($active->reveal())
      ->willReturn($workflow->reveal());
    $moderation->hasPendingRevision($entity->reveal())
      ->willReturn(TRUE);
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class);
    $entity_repository->getActive('node', '1')
      ->willReturn($active->reveal());
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository->reveal(), $moderation->reveal());
    $build = $plugin->build();
    $this->assertSame('Example Title', $build[0]['#slots']['content']);
    $this->assertSame('navigation:badge', $build[1]['#component']);
    $this->assertSame('Draft (Published available)', $build[1]['#slots']['label']);
    // Status still defaults to info as entity might not be EntityPublishedInterface here.
    $this->assertSame('info', $build[1]['#props']['status']);
  }
  
  /**
   * Tests the behavior of a plugin with no valid badge present.
   */
  public function testNoValidBadge() : void {
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->label()
      ->willReturn('Simple Title');
    $route_helper = $this->prophesize(EntityRouteHelper::class);
    $route_helper->getContentEntityFromRoute()
      ->willReturn($entity->reveal());
    $entity_repository = $this->prophesize(EntityRepositoryInterface::class)
      ->reveal();
    $plugin = new PageContext([], 'page_context', [], $route_helper->reveal(), $entity_repository, NULL);
    $build = $plugin->build();
    $this->assertSame('Simple Title', $build[0]['#slots']['content']);
    // Only one component (title) should be present.
    $this->assertArrayNotHasKey(1, $build);
  }

}

Classes

Title Deprecated Summary
PageContextTest Tests the PageContext Top Bar item build output.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.