first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains database additions for testing the help module permission.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$role = Yaml::decode(file_get_contents(__DIR__ . '/drupal-10.access-help-pages.yml'));
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'user.role.content_editor',
'data' => serialize($role),
])
->execute();

View File

@@ -0,0 +1,46 @@
uuid: 91cd52b8-a979-426b-9761-039a5a184b74
langcode: en
status: true
dependencies:
config:
- node.type.article
- node.type.page
- taxonomy.vocabulary.tags
module:
- comment
- contextual
- file
- node
- path
- system
- taxonomy
- toolbar
_core:
default_config_hash: Wur9kcEOwY1Jal81NssKnz3RhVJxAvBwyWQBGcA_1Go
id: content_editor
label: 'Content editor'
weight: 2
is_admin: false
permissions:
- 'access administration pages'
- 'access content overview'
- 'access contextual links'
- 'access files overview'
- 'access toolbar'
- 'administer url aliases'
- 'create article content'
- 'create page content'
- 'create terms in tags'
- 'create url aliases'
- 'delete article revisions'
- 'delete own article content'
- 'delete own page content'
- 'delete page revisions'
- 'edit own article content'
- 'edit own comments'
- 'edit own page content'
- 'edit terms in tags'
- 'revert all revisions'
- 'view all revisions'
- 'view own unpublished content'
- 'view the administration theme'

View File

@@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains database additions for testing the upgrade path for help topics.
*
* @see https://www.drupal.org/node/3087499
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
// Enable experimental help_topics module.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['help_topics'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Structure of configured block and search page.
$search_page = Yaml::decode(file_get_contents(__DIR__ . '/search.page.help_search.yml'));
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'search.page.help_search',
'data' => serialize($search_page),
])
->execute();

View File

@@ -0,0 +1,14 @@
uuid: c0c6e5d5-c0b1-4874-8c94-ffed8929f5d7
langcode: en
status: true
dependencies:
module:
- help_topics
_core:
default_config_hash: RZ-bcSekNSsAbIPLW7Gmyd3uUjIOSrPvnb8RCCZYJm8
id: help_search
label: Help
path: help
weight: 0
plugin: help_search
configuration: { }

View File

@@ -0,0 +1,11 @@
name: 'Help Page Test'
type: module
description: 'Module to test the help page.'
package: Testing
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Help Page Test module to test the help page.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_page_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.help_page_test':
// Make the help text conform to core standards. See
// \Drupal\system\Tests\Functional\GenericModuleTestBase::assertHookHelp().
return t('Read the <a href=":url">online documentation for the Help Page Test module</a>.', [':url' => 'http://www.example.com']);
case 'help_page_test.has_help':
return t('I have help!');
case 'help_page_test.test_array':
return ['#markup' => 'Help text from help_page_test_help module.'];
}
// Ensure that hook_help() can return an empty string and not cause the block
// to display.
return '';
}

View File

@@ -0,0 +1,20 @@
help_page_test.has_help:
path: '/help_page_test/has_help'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::hasHelp'
requirements:
_access: 'TRUE'
help_page_test.no_help:
path: '/help_page_test/no_help'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::noHelp'
requirements:
_access: 'TRUE'
help_page_test.test_array:
path: '/help_page_test/test_array'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::testArray'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\help_page_test;
/**
* Provides controllers for testing the help block.
*/
class HelpPageTestController {
/**
* Provides a route with help.
*
* @return array
* A render array.
*/
public function hasHelp() {
return ['#markup' => 'A route with help.'];
}
/**
* Provides a route with no help.
*
* @return array
* A render array.
*/
public function noHelp() {
return ['#markup' => 'A route without help.'];
}
/**
* Provides a route which has multiple array returns from hook_help().
*
* @return array
* A render array.
*/
public function testArray() {
return ['#markup' => 'A route which has multiple array returns from hook_help().'];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\help_page_test\Plugin\HelpSection;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Drupal\help\Attribute\HelpSection;
/**
* Provides an empty section for the help page, for testing.
*/
#[HelpSection(
id: 'empty_section',
title: new TranslatableMarkup('Empty section'),
description: new TranslatableMarkup('This description should appear.')
)]
class EmptyHelpSection extends HelpSectionPluginBase {
/**
* {@inheritdoc}
*/
public function listTopics() {
return [];
}
}

View File

@@ -0,0 +1,10 @@
name: help_test
type: module
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Test Help module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_test_help($route_name, RouteMatchInterface $route_match) {
// Do not implement a module overview page to test an empty implementation.
// @see \Drupal\help\Tests\HelpTest
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\help_test;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
/**
* Implements a URL generator which always thrown an exception.
*/
class SupernovaGenerator implements UrlGeneratorInterface {
/**
* {@inheritdoc}
*/
public function setContext(RequestContext $context) {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function getContext(): RequestContext {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH): string {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function getPathFromRoute($name, $parameters = []) {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function generateFromRoute($name, $parameters = [], $options = [], $collect_bubbleable_metadata = FALSE) {
throw new \Exception();
}
}

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with bad HTML syntax'
top_level: true
---
<p>{% trans %}Body goes here{% endtrans %}</h3>

View File

@@ -0,0 +1,6 @@
---
label: 'Bad HTML syntax within trans section'
top_level: true
---
<p>{% trans %}<a href="/foo">Text here{% endtrans %}</a></p>

View File

@@ -0,0 +1,5 @@
---
label: 'Unclosed HTML tag'
top_level: true
---
<p>{% trans %}<a href="/foo">Text here{% endtrans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Help topic containing no body'
top_level: true
---

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with H1 header'
top_level: true
---
<h1>{% trans %}Body goes here{% endtrans %}</h1>

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with h3 without an h2'
top_level: true
---
<h3>{% trans %}Body goes here{% endtrans %}</h3>

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with locale-unsafe tag'
top_level: true
---
<p>{% trans %}some translated text and a <script>alert('hello')</script>{% endtrans %}</p>

View File

@@ -0,0 +1,7 @@
---
label: 'Help topic related to nonexistent topic'
top_level: true
related:
- this.is.not.a.valid.help_topic.id
---
<p>{% trans %}Body goes here{% trans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Help topic not top level or related to top level'
---
<p>{% trans %}Body goes here{% endtrans %}</p>

View File

@@ -0,0 +1,6 @@
---
label: 'Help topic with untranslated text'
top_level: true
---
<p>Body goes here</p>
<p>{% trans %}some translated text too{% endtrans %}</p>

View File

@@ -0,0 +1,9 @@
---
label: 'URL test topic that uses outdated url function'
top_level: true
---
{% set link_uses_url_func = render_var(url('valid.route')) %}
<p>{% trans %}This topic should be top-level. It is used to test URLs{% endtrans %}</p>
<ul>
<li>{% trans %}Valid link, but generated with <code>url()</code> instead of <code>help_route_link()</code> or <code>help_topic_link()</code>: {{ link_uses_url_func }}{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,6 @@
---
label: 'Additional topic'
related:
- help_topics_test.test
---
<p>{% trans %}This topic should get listed automatically on the Help test topic.{% endtrans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Linked topic'
---
<p>{% trans %}This topic is not supposed to be top-level.{% endtrans %}</p>

View File

@@ -0,0 +1,11 @@
---
label: "ABC Help Test module"
top_level: true
related:
- help_topics_test.linked
- does_not_exist.and_no_error
---
{% set help_topic_link = render_var(help_topic_link('help_topics_test.test_urls')) %}
<p>{% trans %}This is a test. It should link to the URL test topic {{ help_topic_link }}. Also there should be a related topic link below to the Help module topic page and the linked topic.{% endtrans %}</p>
<p>{% trans %}Non-word-item to translate.{% endtrans %}</p>
<p>{% trans %}Test translation.{% endtrans %}</p>

View File

@@ -0,0 +1,19 @@
---
label: 'URL test topic'
top_level: true
---
{% set non_route_link = render_var(help_route_link('not a route', 'not_a_real_route')) %}
{% set missing_params_link = render_var(help_route_link('missing params', 'help_topics_test.test_route')) %}
{% set invalid_params_link = render_var(help_route_link('invalid params', 'help_topics_test.test_route', {'int_param': 'not_an_int'})) %}
{% set valid_link = render_var(help_route_link('valid link', 'help_topics_test.test_route', {'int_param': 2})) %}
{% set topic_link = render_var(help_topic_link('help_topics_test.additional')) %}
{% set not_a_topic = render_var(help_topic_link('not_a_topic')) %}
<p>{% trans %}This topic should be top-level. It is used to test URLs{% endtrans %}</p>
<ul>
<li>{% trans %}Should not be a link: {{ non_route_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ missing_params_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ invalid_params_link }}{% endtrans %}</li>
<li>{% trans %}Should be a link if user has access: {{ valid_link }}{% endtrans %}</li>
<li>{% trans %}Should be a link: {{ topic_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ not_a_topic }}{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,9 @@
help_topics_test_direct_yml:
class: 'Drupal\help_topics_test\Plugin\HelpTopic\TestHelpTopicPlugin'
top_level: true
related: {}
label: "Test direct yaml topic label"
body: "Test direct yaml body"
help_topics_derivatives:
deriver: 'Drupal\help_topics_test\Plugin\Deriver\TestHelpTopicDeriver'

View File

@@ -0,0 +1,13 @@
# The name of this module is deliberately different from its machine
# name to test the presented order of help topics.
name: 'ABC Help Test'
type: module
description: 'Support module for help testing.'
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Test module for help.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_topics_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.help_topics_test':
return 'Some kind of non-empty output for testing';
}
}
/**
* Implements hook_help_topics_info_alter().
*/
function help_topics_test_help_topics_info_alter(array &$info) {
// To prevent false positive search results limit list to testing topis only.
$filter = fn(string $key) => str_starts_with($key, 'help_topics_test') || in_array($key, [
'help_topics_test_direct_yml',
'help_topics_derivatives:test_derived_topic',
], TRUE);
$info = array_filter($info, $filter, ARRAY_FILTER_USE_KEY);
$info['help_topics_test.test']['top_level'] = \Drupal::state()->get('help_topics_test.test:top_level', TRUE);
}

View File

@@ -0,0 +1,2 @@
access test help:
title: 'Access the test help section'

View File

@@ -0,0 +1,11 @@
help_topics_test.test_route:
path: '/help_topics_test/{int_param<\d+>}'
defaults:
_controller: '\Drupal\help_topics_test\Controller\HelpTopicsTestController::testPage'
_title: 'Page for testing URLs in topics'
requirements:
_permission: 'access test help'
options:
parameters:
required_param:
type: int

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\help_topics_test\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Returns the response for help_topics_test routes.
*/
class HelpTopicsTestController extends ControllerBase {
/**
* Displays a dummy page for testing.
*
* @param int $int_param
* Required parameter (ignored).
*
* @return array
* Render array for the dummy page.
*/
public function testPage(int $int_param) {
$build = [
'#markup' => 'You have reached the help topics test routing page.',
];
return $build;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\help_topics_test\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverInterface;
/**
* A test discovery deriver for fake help topics.
*/
class TestHelpTopicDeriver implements DeriverInterface {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$prefix = $base_plugin_definition['id'];
$id = 'test_derived_topic';
$plugin_id = $prefix . ':' . $id;
$definitions[$id] = [
'plugin_id' => $plugin_id,
'id' => $plugin_id,
'class' => 'Drupal\\help_topics_test\\Plugin\\HelpTopic\\TestHelpTopicPlugin',
'label' => 'Label for ' . $id,
'body' => 'Body for ' . $id,
'top_level' => TRUE,
'related' => [],
'provider' => 'help_topics_test',
];
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
return $base_plugin_definition;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpSection;
use Drupal\help\SearchableHelpInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Drupal\help\Attribute\HelpSection;
use Drupal\Core\StringTranslation\TranslatableMarkup;
// cspell:ignore asdrsad barmm foomm sqruct wcsrefsdf sdeeeee
/**
* Provides a searchable help section for testing.
*/
#[HelpSection(
id: 'help_topics_test',
title: new TranslatableMarkup('Test section'),
description: new TranslatableMarkup('For testing search'),
permission: 'access test help',
weight: 100
)]
class TestHelpSection extends HelpSectionPluginBase implements SearchableHelpInterface {
/**
* {@inheritdoc}
*/
public function listTopics() {
return [
Link::fromTextAndUrl('Foo', Url::fromUri('https://foo.com')),
Link::fromTextAndUrl('Bar', Url::fromUri('https://bar.com')),
];
}
/**
* {@inheritdoc}
*/
public function listSearchableTopics() {
return ['foo', 'bar'];
}
/**
* {@inheritdoc}
*/
public function renderTopicForSearch($topic_id, LanguageInterface $language) {
switch ($topic_id) {
case 'foo':
if ($language->getId() == 'en') {
return [
'title' => 'Foo in English title wcsrefsdf',
'text' => 'Something about foo body not-a-word-english sqruct',
'url' => Url::fromUri('https://foo.com'),
];
}
return [
'title' => 'Foomm Foreign heading',
'text' => 'Fake foreign foo text not-a-word-german asdrsad',
'url' => Url::fromUri('https://mm.foo.com'),
];
case 'bar':
if ($language->getId() == 'en') {
return [
'title' => 'Bar in English',
'text' => 'Something about bar another-word-english asdrsad',
'url' => Url::fromUri('https://bar.com'),
];
}
return [
'title' => \Drupal::state()->get('help_topics_test:translated_title', 'Barmm Foreign sdeeeee'),
'text' => 'Fake foreign barmm another-word-german sqruct',
'url' => Url::fromUri('https://mm.bar.com'),
];
default:
throw new \InvalidArgumentException('Unexpected ID encountered');
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpTopic;
use Drupal\Core\Cache\Cache;
use Drupal\help\HelpTopicPluginBase;
/**
* A fake help topic plugin for testing.
*/
class TestHelpTopicPlugin extends HelpTopicPluginBase {
/**
* {@inheritdoc}
*/
public function getBody() {
return [
'#type' => 'markup',
'#markup' => $this->pluginDefinition['body'],
];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['foobar'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@@ -0,0 +1,11 @@
name: 'Help Topics Twig Tester'
type: module
description: 'Support module for help testing.'
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
help_test_twig.extension:
class: Drupal\help_topics_twig_tester\HelpTestTwigExtension
arguments: []
tags:
- { name: twig.extension, priority: 500 }

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\help_topics_twig_tester;
use Twig\Extension\AbstractExtension;
/**
* Defines and registers Drupal Twig extensions for testing help topics.
*/
class HelpTestTwigExtension extends AbstractExtension {
/**
* {@inheritdoc}
*/
public function getNodeVisitors() {
return [
new HelpTestTwigNodeVisitor(),
];
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Drupal\help_topics_twig_tester;
use Drupal\Core\Template\TwigNodeTrans;
use Twig\Environment;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Node\TextNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
/**
* Defines a Twig node visitor for testing help topics.
*
* See static::setStateValue() for information on the special processing
* this class can do.
*/
class HelpTestTwigNodeVisitor implements NodeVisitorInterface {
/**
* Delimiter placed around single translated chunks.
*/
public const DELIMITER = 'Not Likely To Be Inside A Template';
/**
* Name used in \Drupal::state() for saving state information.
*/
protected const STATE_NAME = 'help_test_twig_node_visitor';
/**
* {@inheritdoc}
*/
public function enterNode(Node $node, Environment $env): Node {
return $node;
}
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node, Environment $env): ?Node {
$processing = static::getState();
if (!$processing['manner']) {
return $node;
}
// For all special processing, we want to remove variables, set statements,
// and assorted Twig expression calls (if, do, etc.).
if ($node instanceof SetNode || $node instanceof PrintNode ||
$node instanceof AbstractExpression) {
return NULL;
}
if ($node instanceof TwigNodeTrans) {
// Count the number of translated chunks.
$this_chunk = $processing['chunk_count'] + 1;
static::setStateValue('chunk_count', $this_chunk);
if ($this_chunk > $processing['max_chunk']) {
static::setStateValue('max_chunk', $this_chunk);
}
if ($processing['manner'] == 'remove_translated') {
// Remove all translated text.
return NULL;
}
elseif ($processing['manner'] == 'replace_translated') {
// Replace with a dummy string.
$node = new TextNode('dummy', 0);
}
elseif ($processing['manner'] == 'translated_chunk') {
// Return the text only if it's the next chunk we're supposed to return.
// Add a wrapper, because non-translated nodes will still be returned.
if ($this_chunk == $processing['return_chunk']) {
return new TextNode(static::DELIMITER . $this->extractText($node) . static::DELIMITER, 0);
}
else {
return NULL;
}
}
}
if ($processing['manner'] == 'remove_translated' && $node instanceof TextNode) {
// For this processing, we also want to remove all HTML tags and
// whitespace from TextNodes.
$text = $node->getAttribute('data');
$text = strip_tags($text);
$text = preg_replace('|\s+|', '', $text);
return new TextNode($text, 0);
}
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority() {
return -100;
}
/**
* Extracts the text from a translated text object.
*
* @param \Drupal\Core\Template\TwigNodeTrans $node
* Translated text node.
*
* @return string
* Text in the node.
*/
protected function extractText(TwigNodeTrans $node) {
// Extract the singular/body and optional plural text from the
// TwigNodeTrans object.
$bodies = $node->getNode('body');
if (!count($bodies)) {
$bodies = [$bodies];
}
if ($node->hasNode('plural')) {
$plural = $node->getNode('plural');
if (!count($plural)) {
$bodies[] = $plural;
}
else {
foreach ($plural as $item) {
$bodies[] = $item;
}
}
}
// Extract the text from each component of the singular/plural strings.
$text = '';
foreach ($bodies as $body) {
if ($body->hasAttribute('data')) {
$text .= $body->getAttribute('data');
}
}
return trim($text);
}
/**
* Returns the state information.
*
* @return array
* The state information.
*/
public static function getState() {
return \Drupal::state()->get(static::STATE_NAME, ['manner' => 0]);
}
/**
* Sets state information.
*
* @param string $key
* Key to set. Possible keys:
* - manner: Type of special processing to do when rendering. Values:
* - 0: No processing.
* - remove_translated: Remove all translated text, HTML tags, and
* whitespace.
* - replace_translated: Replace all translated text with dummy text.
* - translated_chunk: Remove all translated text except one designated
* chunk (see return_chunk below).
* - bare_body (or any other non-zero value): Remove variables, set
* statements, and Twig programming, but leave everything else intact.
* - chunk_count: Current index of translated chunks. Reset to -1 before
* each rendering run. (Used internally by this class.)
* - max_chunk: Maximum index of translated chunks. Reset to -1 before
* each rendering run.
* - return_chunk: Chunk index to keep intact for translated_chunk
* processing. All others are removed.
* @param $value
* Value to set for $key.
*/
public static function setStateValue(string $key, $value) {
$state = \Drupal::state();
$values = $state->get(static::STATE_NAME, ['manner' => 0]);
$values[$key] = $value;
$state->set(static::STATE_NAME, $values);
}
}

View File

@@ -0,0 +1,11 @@
name: 'More Help Page Test'
type: module
description: 'Module to test the help page.'
package: Testing
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* More Help Page Test module to test the help blocks.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function more_help_page_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Return help for the same route as the help_page_test module.
case 'help_page_test.test_array':
return ['#markup' => 'Help text from more_help_page_test_help module.'];
}
}
/**
* Implements hook_help_section_info_alter().
*/
function more_help_page_test_help_section_info_alter(array &$info) {
$info['hook_help']['weight'] = 500;
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\user\Entity\Role;
/**
* Tests help_post_update_add_permissions_to_roles().
*
* @group help
* @group legacy
* @group #slow
*
* @see help_post_update_add_permissions_to_roles()
*/
class AddPermissionsUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
__DIR__ . '/../../fixtures/update/drupal-10.access-help-pages.php',
];
}
/**
* Tests adding 'access help pages' permission.
*/
public function testUpdate(): void {
$roles = Role::loadMultiple();
$this->assertGreaterThan(2, count($roles));
foreach ($roles as $role) {
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
}
$this->runUpdates();
$role = Role::load(Role::ANONYMOUS_ID);
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
$role = Role::load(Role::AUTHENTICATED_ID);
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
// Admin roles have the permission and do not need assigned.
$role = Role::load('administrator');
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
$role = Role::load('content_editor');
$permissions = $role->toArray()['permissions'];
$this->assertContains('access help pages', $permissions);
}
}

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 enable.
*
* 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,196 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\BrowserTestBase;
/**
* Verify help display and user access to help based on permissions.
*
* @group help
*/
class HelpTest extends BrowserTestBase {
/**
* Modules to enable.
*
* 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.
*/
protected $adminUser;
/**
* The anonymous user that will be created.
*/
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, new FormattableMarkup('Link properly added to @name (admin/help/@module)', ['@module' => $module, '@name' => $name]));
}
// 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) {
$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,312 @@
<?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
* @group #slow
*/
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,298 @@
<?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;
/**
* Modules to enable.
*
* @var array
*/
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) {
// 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() {
// 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) {
$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) {
$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) {
$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) {
// 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,63 @@
<?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 enable.
*
* 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.
*/
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,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\block\Entity\Block;
use Drupal\search\Entity\SearchPage;
/**
* Tests merging help topics module when the module is not installed.
*
* @group Update
* @group #slow
*/
class HelpTopicsMerge extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
];
}
/**
* Tests upgrading help module for help topics.
*
* @see \help_update_10200()
* @see \help_post_update_help_topics_search()
* @see \help_post_update_help_topics_disable()
*/
public function testHelpTopicsMerge(): void {
$moduleHandler = \Drupal::moduleHandler();
$this->assertTrue($moduleHandler->moduleExists('help'));
$this->assertFalse($moduleHandler->moduleExists('help_topics'));
$this->assertTrue($moduleHandler->moduleExists('search'));
$this->assertFalse(\Drupal::database()->schema()->tableExists('help_search_items'));
// No configuration present.
$this->assertNull(SearchPage::load('help_search'));
$this->assertNull(Block::load('claro_help_search'));
// Run updates.
$this->runUpdates();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('help_topics'));
$this->assertTrue(\Drupal::database()->schema()->tableExists('help_search_items'));
// Search module's configuration is installed.
$this->assertNotNull(Block::load('claro_help_search'));
$this->assertNotNull(SearchPage::load('help_search'));
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests merging help topics module when the module is enabled.
*
* @group Update
* @group #slow
*/
class HelpTopicsUninstall extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
__DIR__ . '/../../../../tests/fixtures/update/help-topics-3087499.php',
];
}
/**
* Tests upgrading help module for help topics.
*
* @see \help_update_10200()
* @see \help_post_update_help_topics_search()
* @see \help_post_update_help_topics_uninstall()
*/
public function testHelpTopicsMerge(): void {
$module_handler = \Drupal::moduleHandler();
$this->assertTrue($module_handler->moduleExists('help'));
$this->assertTrue($module_handler->moduleExists('help_topics'));
$this->assertTrue($module_handler->moduleExists('search'));
$this->assertFalse(\Drupal::database()->schema()->tableExists('help_search_items'));
$dependencies = $this
->config('search.page.help_search')
->get('dependencies.module');
$this->assertTrue(in_array('help_topics', $dependencies, TRUE));
$this->assertFalse(in_array('help', $dependencies, TRUE));
// Run updates.
$this->runUpdates();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('help_topics'));
$this->assertTrue(\Drupal::database()->schema()->tableExists('help_search_items'));
$dependencies = $this
->config('search.page.help_search')
->get('dependencies.module');
$this->assertFalse(in_array('help_topics', $dependencies, TRUE));
$this->assertTrue(in_array('help', $dependencies, TRUE));
}
}

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) {
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() {
$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,115 @@
<?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->getMockForAbstractClass(Template::class, [$twig], '', TRUE, TRUE, TRUE, ['render']);
$template
->method('render')
->willReturn(self::PLUGIN_INFORMATION['body']);
$twig
->method('load')
->willReturn(new TemplateWrapper($twig, $template));
return $twig;
}
}

View File

@@ -0,0 +1,7 @@
---
label: 'XYZ Help Test theme'
top_level: true
related:
- help_topics_test.additional
---
<p>{% trans %}This is a theme provided topic.{% endtrans %}</p>

View File

@@ -0,0 +1,10 @@
name: Test Help Topics
type: theme
base theme: stable9
description: A theme to test help topics.
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222