Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verifies help for experimental modules.
*
* @group help
*/
class ExperimentalHelpTest extends BrowserTestBase {
/**
* Modules to install.
*
* The experimental_module_test module implements hook_help() and is in the
* Core (Experimental) package.
*
* @var array
*/
protected static $modules = [
'help',
'experimental_module_test',
'help_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access help pages']);
}
/**
* Verifies that a warning message is displayed for experimental modules.
*/
public function testExperimentalHelp(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/help/experimental_module_test');
$this->assertSession()->statusMessageContains('This module is experimental.', 'warning');
// Regular modules should not display the message.
$this->drupalGet('admin/help/help_page_test');
$this->assertSession()->statusMessageNotContains('This module is experimental.');
// Ensure the actual help page is displayed to avoid a false positive.
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('online documentation for the Help Page Test module');
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for help.
*
* @group help
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests display of help block.
*
* @group help
*/
class HelpBlockTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'help',
'help_page_test',
'block',
'more_help_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The help block instance.
*
* @var \Drupal\block\Entity\Block
*/
protected $helpBlock;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->helpBlock = $this->placeBlock('help_block');
}
/**
* Logs in users, tests help pages.
*/
public function testHelp(): void {
$this->drupalGet('help_page_test/has_help');
$this->assertSession()->pageTextContains('I have help!');
$this->assertSession()->pageTextContains($this->helpBlock->label());
$this->drupalGet('help_page_test/no_help');
// The help block should not appear when there is no help.
$this->assertSession()->pageTextNotContains($this->helpBlock->label());
// Ensure that if two hook_help() implementations both return a render array
// the output is as expected.
$this->drupalGet('help_page_test/test_array');
$this->assertSession()->pageTextContains('Help text from more_help_page_test_help module.');
$this->assertSession()->pageTextContains('Help text from help_page_test_help module.');
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verify the order of the help page.
*
* @group help
*/
class HelpPageOrderTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['help', 'help_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Strings to search for on admin/help, in order.
*
* @var string[]
*/
protected $stringOrder = [
'Module overviews are provided',
'This description should appear',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in user.
$account = $this->drupalCreateUser([
'access help pages',
'view the administration theme',
'administer permissions',
]);
$this->drupalLogin($account);
}
/**
* Tests the order of the help page.
*/
public function testHelp(): void {
$pos = 0;
$this->drupalGet('admin/help');
$page_text = $this->getTextContent();
foreach ($this->stringOrder as $item) {
$new_pos = strpos($page_text, $item, $pos);
$this->assertGreaterThan($pos, $new_pos, "Order of $item is not correct on help page");
$pos = $new_pos;
}
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
/**
* Verify the order of the help page with an alter hook.
*
* @group help
*/
class HelpPageReverseOrderTest extends HelpPageOrderTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['more_help_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Strings to search for on admin/help, in order.
*
* These are reversed, due to the alter hook.
*
* @var string[]
*/
protected $stringOrder = [
'This description should appear',
'Module overviews are provided',
];
}

View File

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verify help display and user access to help based on permissions.
*
* @group help
*/
class HelpTest extends BrowserTestBase {
/**
* Modules to install.
*
* The help_test module implements hook_help() but does not provide a module
* overview page. The help_page_test module has a page section plugin that
* returns no links.
*
* @var array
*/
protected static $modules = [
'block_content',
'breakpoint',
'editor',
'help',
'help_page_test',
'help_test',
'history',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'claro';
/**
* The admin user that will be created.
*
* @var \Drupal\user\Entity\User|false
*/
protected $adminUser;
/**
* The anonymous user that will be created.
*
* @var \Drupal\user\Entity\User|false
*/
protected $anyUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users.
$this->adminUser = $this->drupalCreateUser([
'access help pages',
'view the administration theme',
'administer permissions',
]);
$this->anyUser = $this->drupalCreateUser([]);
}
/**
* Logs in users, tests help pages.
*/
public function testHelp(): void {
// Log in the root user to ensure as many admin links appear as possible on
// the module overview pages.
$this->drupalLogin($this->drupalCreateUser([
'access help pages',
'access administration pages',
]));
$this->verifyHelp();
// Log in the regular user.
$this->drupalLogin($this->anyUser);
$this->verifyHelp(403);
// Verify that introductory help text exists, goes for 100% module coverage.
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/help');
$this->assertSession()->responseContains('For more information, refer to the help listed on this page or to the <a href="https://www.drupal.org/documentation">online documentation</a> and <a href="https://www.drupal.org/support">support</a> pages at <a href="https://www.drupal.org">drupal.org</a>.');
// Verify that hook_help() section title and description appear.
$this->assertSession()->responseContains('<h2>Module overviews</h2>');
$this->assertSession()->responseContains('<p>Module overviews are provided by modules. Overviews available for your installed modules:</p>');
// Verify that an empty section is handled correctly.
$this->assertSession()->responseContains('<h2>Empty section</h2>');
$this->assertSession()->responseContains('<p>This description should appear.</p>');
$this->assertSession()->pageTextContains('There is currently nothing in this section.');
// Make sure links are properly added for modules implementing hook_help().
foreach ($this->getModuleList() as $module => $name) {
$this->assertSession()->linkExists($name, 0, "Link properly added to $name (admin/help/$module)");
}
// Ensure a module which does not provide a module overview page is handled
// correctly.
$module_name = \Drupal::service('extension.list.module')->getName('help_test');
$this->clickLink($module_name);
$this->assertSession()->pageTextContains('No help is available for module ' . $module_name);
// Verify that the order of topics is alphabetical by displayed module
// name, by checking the order of some modules, including some that would
// have a different order if it was done by machine name instead.
$this->drupalGet('admin/help');
$page_text = $this->getTextContent();
$start = strpos($page_text, 'Module overviews');
$pos = $start;
$list = ['Block', 'Block Content', 'Breakpoint', 'History', 'Text Editor'];
foreach ($list as $name) {
$this->assertSession()->linkExists($name);
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos, "Order of $name is not correct on page");
$pos = $new_pos;
}
}
/**
* Verifies the logged in user has access to the various help pages.
*
* @param int $response
* (optional) An HTTP response code. Defaults to 200.
*/
protected function verifyHelp($response = 200): void {
$this->drupalGet('admin/index');
$this->assertSession()->statusCodeEquals($response);
if ($response == 200) {
$this->assertSession()->pageTextContains('This page shows you all available administration tasks for each module.');
}
else {
$this->assertSession()->pageTextNotContains('This page shows you all available administration tasks for each module.');
}
$module_list = \Drupal::service('extension.list.module');
foreach ($this->getModuleList() as $module => $name) {
// View module help page.
$this->drupalGet('admin/help/' . $module);
$this->assertSession()->statusCodeEquals($response);
if ($response == 200) {
$this->assertSession()->titleEquals("$name | Drupal");
$this->assertEquals($name, $this->cssSelect('h1.page-title')[0]->getText(), "$module heading was displayed");
$info = $module_list->getExtensionInfo($module);
$admin_tasks = \Drupal::service('system.module_admin_links_helper')->getModuleAdminLinks($module);
if ($module_permissions_link = \Drupal::service('user.module_permissions_link_helper')->getModulePermissionsLink($module, $info['name'])) {
$admin_tasks["user.admin_permissions.{$module}"] = $module_permissions_link;
}
if (!empty($admin_tasks)) {
$this->assertSession()->pageTextContains($name . ' administration pages');
}
foreach ($admin_tasks as $task) {
$this->assertSession()->linkExists($task['title']);
// Ensure there are no double escaped '&' or '<' characters.
$this->assertSession()->assertNoEscaped('&amp;');
$this->assertSession()->assertNoEscaped('&lt;');
// Ensure there are no escaped '<' characters.
$this->assertSession()->assertNoEscaped('<');
}
// Ensure there are no double escaped '&' or '<' characters.
$this->assertSession()->assertNoEscaped('&amp;');
$this->assertSession()->assertNoEscaped('&lt;');
// The help for CKEditor 5 intentionally has escaped '<' so leave this
// iteration before the assertion below.
if ($module === 'ckeditor5') {
continue;
}
// Ensure there are no escaped '<' characters.
$this->assertSession()->assertNoEscaped('<');
}
}
}
/**
* Gets the list of enabled modules that implement hook_help().
*
* @return array
* A list of enabled modules.
*/
protected function getModuleList() {
$modules = [];
$module_data = $this->container->get('extension.list.module')->getList();
\Drupal::moduleHandler()->invokeAllWith(
'help',
function (callable $hook, string $module) use (&$modules, $module_data) {
$modules[$module] = $module_data[$module]->info['name'];
}
);
return $modules;
}
}

View File

@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\Traits\Core\CronRunTrait;
use Drupal\help\Plugin\Search\HelpSearch;
// cspell:ignore asdrsad barmm foomm hilfetestmodul sdeeeee sqruct testen
// cspell:ignore wcsrefsdf übersetzung
/**
* Verifies help topic search.
*
* @group help
*/
class HelpTopicSearchTest extends HelpTopicTranslatedTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'search',
'locale',
'language',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Log in.
$this->drupalLogin($this->createUser([
'access help pages',
'administer site configuration',
'view the administration theme',
'administer permissions',
'administer languages',
'administer search',
'access test help',
'search content',
]));
// Add English language and set to default.
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm([
'predefined_langcode' => 'en',
], 'Add language');
$this->drupalGet('admin/config/regional/language');
$this->submitForm([
'site_default_language' => 'en',
], 'Save configuration');
// When default language is changed, the container is rebuilt in the child
// site, so a rebuild in the main site is required to use the new container
// here.
$this->rebuildContainer();
// Before running cron, verify that a search returns no results and shows
// warning.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(0);
$this->assertSession()->statusMessageContains('Help search is not fully indexed', 'warning');
// Run cron until the topics are fully indexed, with a limit of 100 runs
// to avoid infinite loops.
$num_runs = 100;
$plugin = HelpSearch::create($this->container, [], 'help_search', []);
do {
$this->cronRun();
$remaining = $plugin->indexStatus()['remaining'];
} while (--$num_runs && $remaining);
$this->assertNotEmpty($num_runs);
$this->assertEmpty($remaining);
// Visit the Search settings page and verify it says 100% indexed.
$this->drupalGet('admin/config/search/pages');
$this->assertSession()->pageTextContains('100% of the site has been indexed');
// Search and verify there is no warning.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(1);
$this->assertSession()->statusMessageNotContains('Help search is not fully indexed');
}
/**
* Tests help topic search.
*/
public function testHelpSearch(): void {
$german = \Drupal::languageManager()->getLanguage('de');
$session = $this->assertSession();
// Verify that when we search in English for a word that is only in
// English text, we find the topic. Note that these "words" are provided
// by the topics that come from
// \Drupal\help_topics_test\Plugin\HelpSection\TestHelpSection.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'not-a-word-german'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foomm Foreign heading');
// Verify when we search in English for a word that only exists in German,
// we get no results.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-german'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Verify when we search in English for a word that exists in one topic
// in English and a different topic in German, we only get the one English
// topic.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'sqruct'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'asdrsad'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foomm Foreign heading');
// All of the above tests used the TestHelpSection plugin. Also verify
// that we can search for translated regular help topics, in both English
// and German.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
// Click the link and verify we ended up on the topic page.
$this->clickLink('ABC Help Test module');
$session->pageTextContains('This is a test');
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'non-word-german'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC-Hilfetestmodul');
$this->clickLink('ABC-Hilfetestmodul');
$session->pageTextContains('Übersetzung testen.');
// Verify that we can search from the admin/help page.
$this->drupalGet('admin/help');
$session->pageTextContains('Search help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
// Same for German.
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'non-word-german'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC-Hilfetestmodul');
// Verify we can search for title text (other searches used text
// that was part of the body).
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'wcsrefsdf'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Barmm Foreign sdeeeee');
// Just changing the title and running cron is not enough to reindex so
// 'sdeeeee' still hits a match. The content is updated because the help
// topic is rendered each time.
\Drupal::state()->set('help_topics_test:translated_title', 'Updated translated title');
$this->cronRun();
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Updated translated title');
// Searching for the updated test shouldn't produce a match.
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'translated title'], 'Search');
$this->assertSearchResultsCount(0);
// Clear the caches and re-run cron - this should re-index the help.
$this->rebuildAll();
$this->cronRun();
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$this->assertSearchResultsCount(0);
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'translated title'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Updated translated title');
// Verify the cache tags and contexts.
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'config:search.page.help_search');
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'search_index:help_search');
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
// Log in as a user that does not have permission to see TestHelpSection
// items, and verify they can still search for help topics but not see these
// items.
$this->drupalLogin($this->createUser([
'access help pages',
'administer site configuration',
'view the administration theme',
'administer permissions',
'administer languages',
'administer search',
'search content',
]));
$this->drupalGet('admin/help');
$session->pageTextContains('Search help');
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Uninstall the test module and verify its topics are immediately not
// searchable.
\Drupal::service('module_installer')->uninstall(['help_topics_test']);
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(0);
}
/**
* Tests uninstalling the help_topics module.
*/
public function testUninstall(): void {
\Drupal::service('module_installer')->uninstall(['help_topics_test']);
// Ensure we can uninstall help_topics and use the help system without
// breaking.
$this->drupalLogin($this->createUser([
'administer modules',
'access help pages',
]));
$edit = [];
$edit['uninstall[help]'] = TRUE;
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->statusMessageContains('The selected modules have been uninstalled.', 'status');
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(404);
}
/**
* Tests uninstalling the search module.
*/
public function testUninstallSearch(): void {
// Ensure we can uninstall search and use the help system without
// breaking.
$this->drupalLogin($this->createUser([
'administer modules',
'access help pages',
]));
$edit = [];
$edit['uninstall[search]'] = TRUE;
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->statusMessageContains('The selected modules have been uninstalled.', 'status');
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(200);
// Rebuild the container to reflect the latest changes.
$this->rebuildContainer();
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('help'), 'The help module is still installed.');
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('search'), 'The search module is uninstalled.');
}
/**
* Asserts that help search returned the expected number of results.
*
* @param int $count
* The expected number of search results.
*
* @internal
*/
protected function assertSearchResultsCount(int $count): void {
$this->assertSession()->elementsCount('css', '.help_search-results > li', $count);
}
}

View File

@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
/**
* Verifies help topic display and user access to help based on permissions.
*
* @group help
*/
class HelpTopicTest extends BrowserTestBase {
use AssertBreadcrumbTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'help_topics_test',
'help',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The admin user that will be created.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The user who can see help but not the special route.
*
* @var \Drupal\user\UserInterface
*/
protected $noTestUser;
/**
* The anonymous user that will be created.
*
* @var \Drupal\user\UserInterface
*/
protected $anyUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// These tests rely on some markup from the 'stark' theme and we test theme
// provided help topics.
\Drupal::service('theme_installer')->install(['help_topics_test_theme']);
// Place various blocks.
$settings = [
'theme' => 'stark',
'region' => 'help',
];
$this->placeBlock('help_block', $settings);
$this->placeBlock('local_tasks_block', $settings);
$this->placeBlock('local_actions_block', $settings);
$this->placeBlock('page_title_block', $settings);
$this->placeBlock('system_breadcrumb_block', $settings);
// Create users.
$this->adminUser = $this->createUser([
'access administration pages',
'access help pages',
'view the administration theme',
'administer permissions',
'administer site configuration',
'access test help',
]);
$this->noTestUser = $this->createUser([
'access help pages',
'view the administration theme',
'administer permissions',
'administer site configuration',
]);
$this->anyUser = $this->createUser([]);
}
/**
* Tests the main help page and individual pages for topics.
*/
public function testHelp(): void {
$session = $this->assertSession();
// Log in the regular user.
$this->drupalLogin($this->anyUser);
$this->verifyHelp(403);
// Log in the admin user.
$this->drupalLogin($this->adminUser);
$this->verifyHelp();
$this->verifyBreadCrumb();
// Verify that help topics text appears on admin/help, and cache tags.
$this->drupalGet('admin/help');
$session->responseContains('<h2>Topics</h2>');
$session->pageTextContains('Topics can be provided by modules or themes');
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'core.extension');
// Verify links for help topics and order.
$page_text = $this->getTextContent();
$start = strpos($page_text, 'Topics can be provided');
$pos = $start;
foreach ($this->getTopicList() as $info) {
$name = $info['name'];
$session->linkExists($name);
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos, "Order of $name is not correct on page");
$pos = $new_pos;
}
// Ensure the plugin manager alter hook works as expected.
$session->linkExists('ABC Help Test module');
\Drupal::state()->set('help_topics_test.test:top_level', FALSE);
\Drupal::service('plugin.manager.help_topic')->clearCachedDefinitions();
$this->drupalGet('admin/help');
$session->linkNotExists('ABC Help Test module');
\Drupal::state()->set('help_topics_test.test:top_level', TRUE);
\Drupal::service('plugin.manager.help_topic')->clearCachedDefinitions();
$this->drupalGet('admin/help');
// Ensure all the expected links are present before uninstalling.
$session->linkExists('ABC Help Test module');
$session->linkExists('ABC Help Test');
$session->linkExists('XYZ Help Test theme');
// Uninstall the test module and verify the topics are gone, after
// reloading page.
$this->container->get('module_installer')->uninstall(['help_topics_test']);
$this->drupalGet('admin/help');
$session->linkNotExists('ABC Help Test module');
$session->linkNotExists('ABC Help Test');
$session->linkExists('XYZ Help Test theme');
// Uninstall the test theme and verify the topic is gone.
$this->container->get('theme_installer')->uninstall(['help_topics_test_theme']);
$this->drupalGet('admin/help');
$session->linkNotExists('XYZ Help Test theme');
}
/**
* Verifies the logged in user has access to various help links and pages.
*
* @param int $response
* (optional) The HTTP response code to test for. If it's 200 (default),
* the test verifies the user sees the help; if it's not, it verifies they
* are denied access.
*/
protected function verifyHelp($response = 200): void {
// Verify access to help topic pages.
foreach ($this->getTopicList() as $topic => $info) {
// View help topic page.
$this->drupalGet('admin/help/topic/' . $topic);
$session = $this->assertSession();
$session->statusCodeEquals($response);
if ($response == 200) {
// Verify page information.
$name = $info['name'];
$session->titleEquals($name . ' | Drupal');
$session->responseContains('<h1>' . $name . '</h1>');
foreach ($info['tags'] as $tag) {
$session->responseHeaderContains('X-Drupal-Cache-Tags', $tag);
}
}
}
}
/**
* Verifies links on various topic pages.
*/
public function testHelpLinks(): void {
$session = $this->assertSession();
$this->drupalLogin($this->adminUser);
// Verify links on the test top-level page.
$page = 'admin/help/topic/help_topics_test.test';
// Array element is the page text if you click through.
$links = [
'Linked topic' => 'This topic is not supposed to be top-level',
'Additional topic' => 'This topic should get listed automatically',
'URL test topic' => 'It is used to test URLs',
];
foreach ($links as $link_text => $page_text) {
$this->drupalGet($page);
$this->clickLink($link_text);
$session->pageTextContains($page_text);
}
// Verify theme provided help topics work and can be related.
$this->drupalGet('admin/help/topic/help_topics_test_theme.test');
$session->pageTextContains('This is a theme provided topic.');
$this->assertStringContainsString('This is a theme provided topic.', $session->elementExists('css', 'article')->getText());
$this->clickLink('Additional topic');
$session->linkExists('XYZ Help Test theme');
// Verify that the non-top-level topics do not appear on the Help page.
$this->drupalGet('admin/help');
$session->linkNotExists('Linked topic');
$session->linkNotExists('Additional topic');
// Verify links and non-links on the URL test page.
$this->drupalGet('admin/help/topic/help_topics_test.test_urls');
$links = [
'not a route' => FALSE,
'missing params' => FALSE,
'invalid params' => FALSE,
'valid link' => TRUE,
'Additional topic' => TRUE,
'Missing help topic not_a_topic' => FALSE,
];
foreach ($links as $text => $should_be_link) {
if ($should_be_link) {
$session->linkExists($text);
}
else {
// Should be text that is not a link.
$session->pageTextContains($text);
$session->linkNotExists($text);
}
}
// Verify that the "no test" user, who should not be able to access
// the 'valid link' URL, sees it as not a link.
$this->drupalLogin($this->noTestUser);
$this->drupalGet('admin/help/topic/help_topics_test.test_urls');
$session->pageTextContains('valid link');
$session->linkNotExists('valid link');
}
/**
* Gets a list of topic IDs to test.
*
* @return array
* A list of topics to test, in the order in which they should appear. The
* keys are the machine names of the topics. The values are arrays with the
* following elements:
* - name: Displayed name.
* - tags: Cache tags to test for.
*/
protected function getTopicList() {
return [
'help_topics_test.test' => [
'name' => 'ABC Help Test module',
'tags' => ['core.extension'],
],
'help_topics_derivatives:test_derived_topic' => [
'name' => 'Label for test_derived_topic',
'tags' => ['foobar'],
],
'help_topics_test_direct_yml' => [
'name' => 'Test direct yaml topic label',
'tags' => ['foobar'],
],
];
}
/**
* Tests breadcrumb on a help topic page.
*/
public function verifyBreadCrumb(): void {
// Verify Help Topics administration breadcrumbs.
$trail = [
'' => 'Home',
'admin' => 'Administration',
'admin/help' => 'Help',
];
$this->assertBreadcrumb('admin/help/topic/help_topics_test.test', $trail);
// Ensure we are on the expected help topic page.
$this->assertSession()->pageTextContains('Also there should be a related topic link below to the Help module topic page and the linked topic.');
// Verify that another page does not have the help breadcrumb.
$trail = [
'' => 'Home',
'admin' => 'Administration',
'admin/config' => 'Configuration',
'admin/config/system' => 'System',
];
$this->assertBreadcrumb('admin/config/system/site-information', $trail);
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore hilfetestmodul übersetzung
/**
* Provides a base class for functional help topic tests that use translation.
*
* Installs in German, with a small PO file, and sets up the task, help, and
* page title blocks.
*/
abstract class HelpTopicTranslatedTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'help_topics_test',
'help',
'block',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// These tests rely on some markup from the 'Claro' theme, as well as an
// optional block added when Claro is enabled.
\Drupal::service('theme_installer')->install(['claro']);
\Drupal::configFactory()->getEditable('system.theme')
->set('admin', 'claro')
->save(TRUE);
// Place various blocks.
$settings = [
'theme' => 'claro',
'region' => 'help',
];
$this->placeBlock('help_block', $settings);
$this->placeBlock('local_tasks_block', $settings);
$this->placeBlock('local_actions_block', $settings);
$this->placeBlock('page_title_block', $settings);
// Create user.
$this->drupalLogin($this->createUser([
'access help pages',
'view the administration theme',
'administer permissions',
]));
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// Install in German. This will ensure the language and locale modules are
// installed.
$parameters['parameters']['langcode'] = 'de';
// Create a po file so we don't attempt to download one from
// localize.drupal.org and to have a test translation that will not change.
\Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE);
$contents = <<<PO
msgid ""
msgstr ""
msgid "ABC Help Test module"
msgstr "ABC-Hilfetestmodul"
msgid "Test translation."
msgstr "Übersetzung testen."
msgid "Non-word-item to translate."
msgstr "Non-word-german sdfwedrsdf."
PO;
$version = explode('.', \Drupal::VERSION)[0] . '.0.0';
file_put_contents($this->publicFilesDirectory . "/translations/drupal-{$version}.de.po", $contents);
return $parameters;
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
// cspell:ignore hilfetestmodul testen übersetzung
/**
* Verifies help topic translations.
*
* @group help
*/
class HelpTopicTranslationTest extends HelpTopicTranslatedTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create user and log in.
$this->drupalLogin($this->createUser([
'access help pages',
'view the administration theme',
'administer permissions',
]));
}
/**
* Tests help topic translations.
*/
public function testHelpTopicTranslations(): void {
$session = $this->assertSession();
// Verify that help topic link is translated on admin/help.
$this->drupalGet('admin/help');
$session->linkExists('ABC-Hilfetestmodul');
// Verify that the language cache tag appears on admin/help.
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
// Verify that help topic is translated.
$this->drupalGet('admin/help/topic/help_topics_test.test');
$session->pageTextContains('ABC-Hilfetestmodul');
$session->pageTextContains('Übersetzung testen.');
// Verify that the language cache tag appears on a topic page.
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
}
}

View File

@ -0,0 +1,353 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Component\FrontMatter\FrontMatter;
use Drupal\Tests\BrowserTestBase;
use Drupal\help\HelpTopicDiscovery;
use Drupal\help_topics_twig_tester\HelpTestTwigNodeVisitor;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\AssertionFailedError;
/**
* Verifies that all core Help topics can be rendered and comply with standards.
*
* @group help
* @group #slow
*/
class HelpTopicsSyntaxTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'help',
'help_topics_twig_tester',
'locale',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that all Core help topics can be rendered and have good syntax.
*/
public function testHelpTopics(): void {
$this->drupalLogin($this->createUser([
'administer modules',
'access help pages',
]));
// Enable all modules and themes, so that all routes mentioned in topics
// will be defined.
$module_directories = $this->listDirectories('module');
$modules_to_install = array_keys($module_directories);
\Drupal::service('module_installer')->install($modules_to_install);
$theme_directories = $this->listDirectories('theme');
\Drupal::service('theme_installer')->install(array_keys($theme_directories));
$directories = $module_directories + $theme_directories +
$this->listDirectories('profile');
$directories['core'] = \Drupal::root() . '/core/help_topics';
$directories['bad_help_topics'] = \Drupal::service('extension.list.module')->getPath('help_topics_test') . '/bad_help_topics/syntax/';
// Filter out directories outside of core. If you want to run this test
// on a contrib/custom module, remove the next line.
$directories = array_filter($directories, function ($directory) {
return str_starts_with($directory, 'core');
});
// Verify that a few key modules, themes, and profiles are listed, so that
// we can be certain our directory list is complete and we will be testing
// all existing help topics. If these lines in the test fail in the future,
// it is probably because something we chose to list here is being removed.
// Substitute another item of the same type that still exists, so that this
// test can continue.
$this->assertArrayHasKey('system', $directories, 'System module is being scanned');
$this->assertArrayHasKey('help', $directories, 'Help module is being scanned');
$this->assertArrayHasKey('claro', $directories, 'Claro theme is being scanned');
$this->assertArrayHasKey('standard', $directories, 'Standard profile is being scanned');
$definitions = (new HelpTopicDiscovery($directories))->getDefinitions();
$this->assertGreaterThan(0, count($definitions), 'At least 1 topic was found');
// Test each topic for compliance with standards, or for failing in the
// right way.
foreach (array_keys($definitions) as $id) {
if (str_starts_with($id, 'bad_help_topics.')) {
$this->verifyBadTopic($id, $definitions);
}
else {
$this->verifyTopic($id, $definitions);
}
}
}
/**
* Verifies rendering and standards compliance of one help topic.
*
* @param string $id
* ID of the topic to verify.
* @param array $definitions
* Array of all topic definitions, keyed by ID.
* @param int $response
* Expected response from visiting the page for the topic.
*/
protected function verifyTopic($id, $definitions, $response = 200): void {
$definition = $definitions[$id];
HelpTestTwigNodeVisitor::setStateValue('manner', 0);
// Visit the URL for the topic.
$this->drupalGet('admin/help/topic/' . $id);
// Verify the title and response.
$session = $this->assertSession();
$session->statusCodeEquals($response);
if ($response == 200) {
$session->titleEquals($definition['label'] . ' | Drupal');
}
// Verify that all the related topics exist. Also check to see if any of
// them are top-level (we will need that in the next section).
$has_top_level_related = FALSE;
if (isset($definition['related'])) {
foreach ($definition['related'] as $related_id) {
$this->assertArrayHasKey($related_id, $definitions, 'Topic ' . $id . ' is only related to topics that exist: ' . $related_id);
$has_top_level_related = $has_top_level_related || !empty($definitions[$related_id]['top_level']);
}
}
// Verify this is either top-level or related to a top-level topic.
$this->assertTrue(!empty($definition['top_level']) || $has_top_level_related, 'Topic ' . $id . ' is either top-level or related to at least one other top-level topic');
// Verify that the label is not empty.
$this->assertNotEmpty($definition['label'], 'Topic ' . $id . ' has a non-empty label');
// Test the syntax and contents of the Twig file (without the front
// matter, which is tested in other ways above). We need to render the
// template several times with variations, so read it in once.
$template = file_get_contents($definition[HelpTopicDiscovery::FILE_KEY]);
$template_text = FrontMatter::create($template)->getContent();
// Verify that the body is not empty and is valid HTML.
$text = $this->renderHelpTopic($template_text, 'bare_body');
$this->assertNotEmpty($text, 'Topic ' . $id . ' contains some text outside of front matter');
$this->validateHtml($text, $id);
$max_chunk_num = HelpTestTwigNodeVisitor::getState()['max_chunk'];
$this->assertTrue($max_chunk_num >= 0, 'Topic ' . $id . ' has at least one translated chunk');
// Verify that each chunk of the translated text is locale-safe and
// valid HTML.
$chunk_num = 0;
$number_checked = 0;
while ($chunk_num <= $max_chunk_num) {
$chunk_str = $id . ' section ' . $chunk_num;
// Render the topic, asking for just one chunk, and extract the chunk.
// Note that some chunks may not actually get rendered, if they are inside
// set statements, because we skip rendering variable output.
HelpTestTwigNodeVisitor::setStateValue('return_chunk', $chunk_num);
$text = $this->renderHelpTopic($template_text, 'translated_chunk');
$matches = [];
$matched = preg_match('|' . HelpTestTwigNodeVisitor::DELIMITER . '(.*)' . HelpTestTwigNodeVisitor::DELIMITER . '|', $text, $matches);
if ($matched) {
$number_checked++;
$text = $matches[1];
$this->assertNotEmpty($text, 'Topic ' . $chunk_str . ' contains text');
// Verify the chunk is OK.
$this->assertTrue(locale_string_is_safe($text), 'Topic ' . $chunk_str . ' translatable string is locale-safe');
$this->validateHtml($text, $chunk_str);
}
$chunk_num++;
}
$this->assertTrue($number_checked > 0, 'Tested at least one translated chunk in ' . $id);
// Validate the HTML in the body with the translated text replaced by a
// dummy string, to verify that HTML syntax is not partly in and partly out
// of the translated text.
$text = $this->renderHelpTopic($template_text, 'replace_translated');
$this->validateHtml($text, $id);
// Verify that if we remove all the translated text, whitespace, and
// HTML tags, there is nothing left (that is, all text is translated).
$text = preg_replace('|\s+|', '', $this->renderHelpTopic($template_text, 'remove_translated'));
$this->assertEmpty($text, 'Topic ' . $id . ' Twig file has all of its text translated');
// Verify that the Twig url() function was not used.
$this->assertStringNotContainsString('url(', $template, 'Topic ' . $id . ' appears to use the url() function. Replace with help_topic_link() or help_topic_route(). See https://drupal.org/node/3074421');
}
/**
* Validates the HTML and header hierarchy for topic text.
*
* @param string $body
* Body text to validate.
* @param string $id
* ID of help topic (for error messages).
*/
protected function validateHtml(string $body, string $id): void {
$doc = new \DOMDocument();
$doc->strictErrorChecking = TRUE;
$doc->validateOnParse = FALSE;
libxml_use_internal_errors(TRUE);
if (!$doc->loadXML('<html><body>' . $body . '</body></html>')) {
foreach (libxml_get_errors() as $error) {
$this->fail('Topic ' . $id . ' fails HTML validation: ' . $error->message);
}
libxml_clear_errors();
}
// Check for headings hierarchy.
$levels = [1, 2, 3, 4, 5, 6];
foreach ($levels as $level) {
$num_headings[$level] = $doc->getElementsByTagName('h' . $level)->length;
if ($level == 1) {
$this->assertSame(0, $num_headings[1], 'Topic ' . $id . ' has no H1 tag');
// Set num_headings to 1 for this level, so the rest of the hierarchy
// can be tested using simpler code.
$num_headings[1] = 1;
}
else {
// We should either not have this heading, or if we do have one at this
// level, we should also have the next-smaller level. That is, if we
// have an h3, we should have also had an h2.
$this->assertTrue($num_headings[$level - 1] > 0 || $num_headings[$level] == 0,
'Topic ' . $id . ' has the correct H2-H6 heading hierarchy');
}
}
}
/**
* Verifies that a bad topic fails in the expected way.
*
* @param string $id
* ID of the topic to verify. It should start with "bad_help_topics.".
* @param array $definitions
* Array of all topic definitions, keyed by ID.
*/
protected function verifyBadTopic($id, $definitions): void {
$bad_topic_type = substr($id, 16);
// Topics should fail verifyTopic() in specific ways.
$found_error = FALSE;
try {
$this->verifyTopic($id, $definitions, 404);
}
catch (ExpectationFailedException | AssertionFailedError $e) {
$found_error = TRUE;
$message = $e->getMessage();
switch ($bad_topic_type) {
case 'related':
$this->assertStringContainsString('only related to topics that exist', $message);
break;
case 'bad_html':
case 'bad_html2':
case 'bad_html3':
$this->assertStringContainsString('Opening and ending tag mismatch', $message);
break;
case 'top_level':
$this->assertStringContainsString('is either top-level or related to at least one other top-level topic', $message);
break;
case 'empty':
$this->assertStringContainsString('contains some text outside of front matter', $message);
break;
case 'translated':
$this->assertStringContainsString('Twig file has all of its text translated', $message);
break;
case 'locale':
$this->assertStringContainsString('translatable string is locale-safe', $message);
break;
case 'h1':
$this->assertStringContainsString('has no H1 tag', $message);
break;
case 'hierarchy':
$this->assertStringContainsString('has the correct H2-H6 heading hierarchy', $message);
break;
case 'url_func_used':
$this->assertStringContainsString('appears to use the url() function', $message);
break;
default:
// This was an unexpected error.
throw $e;
}
}
if (!$found_error) {
$this->fail('Bad help topic ' . $bad_topic_type . ' did not fail as expected');
}
}
/**
* Lists the extension help topic directories of a certain type.
*
* @param string $type
* The type of extension to list: module, theme, or profile.
*
* @return string[]
* An array of all of the help topic directories for this type of
* extension, keyed by extension short name.
*/
protected function listDirectories($type) {
$directories = [];
// Find the extensions of this type, even if they are not installed, but
// excluding test ones.
$lister = \Drupal::service('extension.list.' . $type);
foreach ($lister->getAllAvailableInfo() as $name => $info) {
// Skip obsolete and deprecated modules.
if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE || $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
continue;
}
$path = $lister->getPath($name);
// You can tell test modules because they are in package 'Testing', but
// test themes are only known by being found in test directories. So...
// exclude things in test directories.
if (!str_contains($path, '/tests') && !str_contains($path, '/testing')) {
$directories[$name] = $path . '/help_topics';
}
}
return $directories;
}
/**
* Renders a help topic in a special manner.
*
* @param string $content
* Template text, without the front matter.
* @param string $manner
* The special processing choice for topic rendering.
*
* @return string
* The rendered topic.
*/
protected function renderHelpTopic(string $content, string $manner): string {
// Set up the special state variables for rendering.
HelpTestTwigNodeVisitor::setStateValue('manner', $manner);
HelpTestTwigNodeVisitor::setStateValue('max_chunk', -1);
HelpTestTwigNodeVisitor::setStateValue('chunk_count', -1);
// Add a random comment to the end, to thwart caching, and render. We need
// the HelpTestTwigNodeVisitor class to hit it each time we render.
$build = [
'#type' => 'inline_template',
'#template' => $content . "\n{# " . rand() . " #}",
];
return (string) \Drupal::service('renderer')->renderInIsolation($build);
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verify no help is displayed for modules not providing any help.
*
* @group help
*/
class NoHelpTest extends BrowserTestBase {
/**
* Modules to install.
*
* Use one of the test modules that do not implement hook_help().
*
* @var array
*/
protected static $modules = ['help', 'menu_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The user who will be created.
*
* @var \Drupal\user\Entity\User|false
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access help pages']);
}
/**
* Ensures modules not implementing help do not appear on admin/help.
*/
public function testMainPageNoHelp(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Module overviews are provided by modules');
$this->assertFalse(\Drupal::moduleHandler()->hasImplementations('help', 'menu_test'), 'The menu_test module does not implement hook_help');
// Make sure the test module menu_test does not display a help link on
// admin/help.
$this->assertSession()->pageTextNotContains(\Drupal::service('extension.list.module')->getName('menu_test'));
// Ensure that the module overview help page for a module that does not
// implement hook_help() results in a 404.
$this->drupalGet('admin/help/menu_test');
$this->assertSession()->statusCodeEquals(404);
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Routing\RouteMatch;
use Drupal\help_test\SupernovaGenerator;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the empty HTML page.
*
* @group help
*/
class HelpEmptyPageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'help_test', 'user', 'path_alias'];
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
$container->set('url_generator', new SupernovaGenerator());
}
/**
* Ensures that no URL generator is called on a page without hook_help().
*/
public function testEmptyHookHelp(): void {
$all_modules = \Drupal::service('extension.list.module')->getList();
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, already enabled modules and modules in the
// Testing package.
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
return FALSE;
}
return TRUE;
});
$this->enableModules(array_keys($all_modules));
$this->installEntitySchema('menu_link_content');
$route = \Drupal::service('router.route_provider')->getRouteByName('<front>');
\Drupal::service('module_handler')->invokeAll('help', ['<front>', new RouteMatch('<front>', $route)]);
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Kernel;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\search\Plugin\SearchIndexingInterface;
/**
* Tests search plugin behaviors.
*
* @group help
*
* @see \Drupal\help\Plugin\Search\HelpSearch
*/
class HelpSearchPluginTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['help', 'search'];
/**
* Tests search plugin annotation and interfaces.
*/
public function testAnnotation(): void {
/** @var \Drupal\search\SearchPluginManager $manager */
$manager = \Drupal::service('plugin.manager.search');
/** @var \Drupal\help\Plugin\Search\HelpSearch $plugin */
$plugin = $manager->createInstance('help_search');
$this->assertInstanceOf(AccessibleInterface::class, $plugin);
$this->assertInstanceOf(SearchIndexingInterface::class, $plugin);
$this->assertSame('Help', (string) $plugin->getPluginDefinition()['title']);
$this->assertTrue($plugin->usesAdminTheme());
}
}

View File

@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Unit;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\help\HelpTopicDiscovery;
use Drupal\help\HelpTopicTwig;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass \Drupal\help\HelpTopicDiscovery
* @group help
*/
class HelpTopicDiscoveryTest extends UnitTestCase {
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionMissingLabel(): void {
vfsStream::setup('root');
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
// The content of the help topic does not matter.
'test.topic.html.twig' => '',
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig does not contain the required key with name='label'");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidYamlKey(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
foo: bar
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid key='foo'");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidTopLevel(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
top_level: bar
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid value for 'top_level' key, the value must be a Boolean");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidRelated(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
related: "one, two"
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid value for 'related' key, the value must be an array of strings");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testHelpTopicsExtensionProviderSpecialCase(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'help' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['help' => vfsStream::url('root/modules/help/help_topics')]);
$this->assertArrayHasKey('core.topic', $discovery->getDefinitions());
}
/**
* @covers ::findAll
*/
public function testHelpTopicsInCore(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
vfsStream::create([
'core' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
]);
$discovery = new HelpTopicDiscovery(['core' => vfsStream::url('root/core/help_topics')]);
$this->assertArrayHasKey('core.topic', $discovery->getDefinitions());
}
/**
* @covers ::findAll
*/
public function testHelpTopicsBrokenYaml(): void {
vfsStream::setup('root');
$topic_content = <<<EOF
---
foo : [bar}
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'help' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['help' => vfsStream::url('root/modules/help/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("Malformed YAML in help topic \"vfs://root/modules/help/help_topics/core.topic.html.twig\":");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testHelpTopicsDefinition(): void {
$container = new ContainerBuilder();
$container->set('string_translation', $this->getStringTranslationStub());
\Drupal::setContainer($container);
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'Test'
top_level: true
related:
- one
- two
- three
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'foo' => [
'help_topics' => [
'foo.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['foo' => vfsStream::url('root/modules/foo/help_topics')]);
$definition = $discovery->getDefinitions()['foo.topic'];
$this->assertEquals('Test', $definition['label']);
$this->assertInstanceOf(TranslatableMarkup::class, $definition['label']);
$this->assertTrue($definition['top_level']);
// Each related plugin ID should be trimmed.
$this->assertSame(['one', 'two', 'three'], $definition['related']);
$this->assertSame('foo', $definition['provider']);
$this->assertSame(HelpTopicTwig::class, $definition['class']);
$this->assertSame(vfsStream::url('root/modules/foo/help_topics/foo.topic.html.twig'), $definition['_discovered_file_path']);
$this->assertSame('foo.topic', $definition['id']);
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Unit;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\help\HelpTopicTwigLoader;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use Twig\Error\LoaderError;
/**
* Unit test for the HelpTopicTwigLoader class.
*
* @coversDefaultClass \Drupal\help\HelpTopicTwigLoader
* @group help
*/
class HelpTopicTwigLoaderTest extends UnitTestCase {
/**
* The help topic loader instance to test.
*
* @var \Drupal\help\HelpTopicTwigLoader
*/
protected $helpLoader;
/**
* The virtual directories to use in testing.
*
* @var array
*/
protected $directories;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpVfs();
$module_handler = $this->createMock(ModuleHandlerInterface::class);
$module_handler
->method('getModuleDirectories')
->willReturn($this->directories['module']);
/** @var \Drupal\Core\Extension\ThemeHandlerInterface|\Prophecy\Prophecy\ObjectProphecy $module_handler */
$theme_handler = $this->createMock(ThemeHandlerInterface::class);
$theme_handler
->method('getThemeDirectories')
->willReturn($this->directories['theme']);
$this->helpLoader = new HelpTopicTwigLoader('\fake\root\path', $module_handler, $theme_handler);
}
/**
* @covers ::__construct
*/
public function testConstructor(): void {
// Verify that the module/theme directories were added in the constructor,
// and non-existent directories were omitted.
$paths = $this->helpLoader->getPaths(HelpTopicTwigLoader::MAIN_NAMESPACE);
$this->assertCount(2, $paths);
$this->assertContains($this->directories['module']['test'] . '/help_topics', $paths);
$this->assertContains($this->directories['theme']['test'] . '/help_topics', $paths);
}
/**
* @covers ::getSourceContext
*/
public function testGetSourceContext(): void {
$source = $this->helpLoader->getSourceContext('@' . HelpTopicTwigLoader::MAIN_NAMESPACE . '/test.topic.html.twig');
$this->assertEquals('{% line 4 %}<h2>Test</h2>', $source->getCode());
}
/**
* @covers ::getSourceContext
*/
public function testGetSourceContextException(): void {
$this->expectException(LoaderError::class);
$this->expectExceptionMessage("Malformed YAML in help topic \"vfs://root/modules/test/help_topics/test.invalid_yaml.html.twig\":");
$this->helpLoader->getSourceContext('@' . HelpTopicTwigLoader::MAIN_NAMESPACE . '/test.invalid_yaml.html.twig');
}
/**
* Sets up the virtual file system.
*/
protected function setUpVfs(): void {
$content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
$invalid_content = <<<EOF
---
foo : [bar}
---
<h2>Test</h2>
EOF;
$help_topics_dir = [
'help_topics' => [
'test.topic.html.twig' => $content,
'test.invalid_yaml.html.twig' => $invalid_content,
],
];
vfsStream::setup('root');
vfsStream::create([
'modules' => [
'test' => $help_topics_dir,
],
'themes' => [
'test' => $help_topics_dir,
],
]);
$this->directories = [
'root' => vfsStream::url('root'),
'module' => [
'test' => vfsStream::url('root/modules/test'),
'not_a_dir' => vfsStream::url('root/modules/not_a_dir'),
],
'theme' => [
'test' => vfsStream::url('root/themes/test'),
'not_a_dir' => vfsStream::url('root/themes/not_a_dir'),
],
];
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Unit;
use Drupal\Core\Cache\Cache;
use Drupal\help\HelpTopicTwig;
use Drupal\Tests\UnitTestCase;
use Twig\Template;
use Twig\TemplateWrapper;
/**
* Unit test for the HelpTopicTwig class.
*
* Note that the toUrl() and toLink() methods are not covered, because they
* have calls to new Url() and new Link() in them, so they cannot be unit
* tested.
*
* @coversDefaultClass \Drupal\help\HelpTopicTwig
* @group help
*/
class HelpTopicTwigTest extends UnitTestCase {
/**
* The help topic instance to test.
*
* @var \Drupal\help\HelpTopicTwig
*/
protected $helpTopic;
/**
* The plugin information to use for setting up a test topic.
*
* @var array
*/
const PLUGIN_INFORMATION = [
'id' => 'test.topic',
'provider' => 'test',
'label' => 'This is the topic label',
'top_level' => TRUE,
'related' => ['something'],
'body' => '<p>This is the topic body</p>',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->helpTopic = new HelpTopicTwig([],
self::PLUGIN_INFORMATION['id'],
self::PLUGIN_INFORMATION,
$this->getTwigMock());
}
/**
* @covers ::getBody
* @covers ::getLabel
*/
public function testText(): void {
$this->assertEquals($this->helpTopic->getBody(),
['#markup' => self::PLUGIN_INFORMATION['body']]);
$this->assertEquals($this->helpTopic->getLabel(),
self::PLUGIN_INFORMATION['label']);
}
/**
* @covers ::getProvider
* @covers ::isTopLevel
* @covers ::getRelated
*/
public function testDefinition(): void {
$this->assertEquals($this->helpTopic->getProvider(),
self::PLUGIN_INFORMATION['provider']);
$this->assertEquals($this->helpTopic->isTopLevel(),
self::PLUGIN_INFORMATION['top_level']);
$this->assertEquals($this->helpTopic->getRelated(),
self::PLUGIN_INFORMATION['related']);
}
/**
* @covers ::getCacheContexts
* @covers ::getCacheTags
* @covers ::getCacheMaxAge
*/
public function testCacheInfo(): void {
$this->assertEquals([], $this->helpTopic->getCacheContexts());
$this->assertEquals(['core.extension'], $this->helpTopic->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $this->helpTopic->getCacheMaxAge());
}
/**
* Creates a mock Twig loader class for the test.
*/
protected function getTwigMock() {
$twig = $this
->getMockBuilder('Drupal\Core\Template\TwigEnvironment')
->disableOriginalConstructor()
->getMock();
$template = $this
->getMockBuilder(Template::class)
->onlyMethods(['render', 'getTemplateName', 'getDebugInfo', 'getSourceContext', 'doDisplay'])
->setConstructorArgs([$twig])
->getMock();
$template
->method('render')
->willReturn(self::PLUGIN_INFORMATION['body']);
$twig
->method('load')
->willReturn(new TemplateWrapper($twig, $template));
return $twig;
}
}