Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,79 @@
<?php
namespace Drupal\Tests\system\Functional\Ajax;
use Drupal\ajax_test\Controller\AjaxTestController;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\BrowserTestBase;
/**
* Performs tests on opening and manipulating dialogs via AJAX commands.
*
* @group Ajax
*/
class OffCanvasDialogTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['ajax_test'];
/**
* Test sending AJAX requests to open and manipulate off-canvas dialog.
*
* @dataProvider dialogPosition
*/
public function testDialog($position) {
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// Set up variables for this test.
$dialog_renderable = AjaxTestController::dialogContents();
$dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
$off_canvas_expected_response = [
'command' => 'openDialog',
'selector' => '#drupal-off-canvas',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' =>
[
'title' => 'AJAX Dialog & contents',
'modal' => FALSE,
'autoResize' => FALSE,
'resizable' => 'w',
'draggable' => FALSE,
'drupalAutoButtons' => FALSE,
'drupalOffCanvasPosition' => $position ?: 'side',
'buttons' => [],
'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-' . ($position ?: 'side'),
'width' => 300,
],
'effect' => 'fade',
'speed' => 1000,
];
// Emulate going to the JS version of the page and check the JSON response.
$wrapper_format = $position && ($position !== 'side') ? 'drupal_dialog.off_canvas_' . $position : 'drupal_dialog.off_canvas';
$ajax_result = $this->drupalGet('ajax-test/dialog-contents', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => $wrapper_format]]);
$ajax_result = Json::decode($ajax_result);
$this->assertEquals($off_canvas_expected_response, $ajax_result[3], 'off-canvas dialog JSON response matches.');
}
/**
* The data provider for potential dialog positions.
*
* @return array
*/
public static function dialogPosition() {
return [
[NULL],
['side'],
['top'],
];
}
}

View file

@ -50,7 +50,15 @@ class PageTest extends BrowserTestBase {
// Visit an administrative page that runs a test batch, and check that the
// title shown during batch execution (which the batch callback function
// saved as a variable) matches the theme used on the administrative page.
// Run initial step only first.
$this->maximumMetaRefreshCount = 0;
$this->drupalGet('batch-test/test-title');
$this->assertText('Batch Test', 'The test is in the html output.');
// Leave the batch process running.
$this->maximumMetaRefreshCount = NULL;
$this->drupalGet('batch-test/test-title');
// The stack should contain the title shown on the progress page.
$this->assertEqual(batch_test_stack(), ['Batch Test'], 'The batch title is shown on the batch page.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');

View file

@ -175,7 +175,6 @@ class ProcessingTest extends BrowserTestBase {
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Triggers a pass if the texts were found in order in the raw content.
*

View file

@ -2,14 +2,15 @@
namespace Drupal\Tests\system\Functional\Bootstrap;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests drupal_set_message() and related functions.
* Tests the Messenger service.
*
* @group Bootstrap
*/
class DrupalSetMessageTest extends BrowserTestBase {
class DrupalMessengerServiceTest extends BrowserTestBase {
/**
* Modules to enable.
@ -19,12 +20,12 @@ class DrupalSetMessageTest extends BrowserTestBase {
public static $modules = ['system_test'];
/**
* Tests drupal_set_message().
* Tests Messenger service.
*/
public function testDrupalSetMessage() {
// The page at system-test/drupal-set-message sets two messages and then
// removes the first before it is displayed.
$this->drupalGet('system-test/drupal-set-message');
public function testDrupalMessengerService() {
// The page at system_test.messenger_service route sets two messages and
// then removes the first before it is displayed.
$this->drupalGet(Url::fromRoute('system_test.messenger_service'));
$this->assertNoText('First message (removed).');
$this->assertRaw(t('Second message with <em>markup!</em> (not removed).'));

View file

@ -0,0 +1,192 @@
<?php
namespace Drupal\Tests\system\Functional\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
/**
* Provides test assertions for testing page-level cache contexts & tags.
*
* Can be used by test classes that extend \Drupal\Tests\BrowserTestBase.
*/
trait AssertPageCacheContextsAndTagsTrait {
/**
* Enables page caching.
*/
protected function enablePageCaching() {
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Gets a specific header value as array.
*
* @param string $header_name
* The header name.
*
* @return string[]
* The header value, potentially exploded by spaces.
*/
protected function getCacheHeaderValues($header_name) {
$header_value = $this->drupalGetHeader($header_name);
if (empty($header_value)) {
return [];
}
else {
return explode(' ', $header_value);
}
}
/**
* Asserts whether an expected cache context was present in the last response.
*
* @param string $expected_cache_context
* The expected cache context.
*/
protected function assertCacheContext($expected_cache_context) {
$cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
$this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
}
/**
* Asserts that a cache context was not present in the last response.
*
* @param string $not_expected_cache_context
* The expected cache context.
*/
protected function assertNoCacheContext($not_expected_cache_context) {
$cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
$this->assertFalse(in_array($not_expected_cache_context, $cache_contexts), "'" . $not_expected_cache_context . "' is not present in the X-Drupal-Cache-Contexts header.");
}
/**
* Asserts page cache miss, then hit for the given URL; checks cache headers.
*
* @param \Drupal\Core\Url $url
* The URL to test.
* @param string[] $expected_contexts
* The expected cache contexts for the given URL.
* @param string[] $expected_tags
* The expected cache tags for the given URL.
*/
protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
$absolute_url = $url->setAbsolute()->toString();
sort($expected_contexts);
sort($expected_tags);
// Assert cache miss + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert cache hit + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert page cache item + expected cache tags.
$cid_parts = [$url->setAbsolute()->toString(), 'html'];
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('page')->get($cid);
sort($cache_entry->tags);
$this->assertEqual($cache_entry->tags, $expected_tags);
$this->debugCacheTags($cache_entry->tags, $expected_tags);
}
/**
* Provides debug information for cache tags.
*
* @param string[] $actual_tags
* The actual cache tags.
* @param string[] $expected_tags
* The expected cache tags.
*/
protected function debugCacheTags(array $actual_tags, array $expected_tags) {
if ($actual_tags !== $expected_tags) {
debug('Unwanted cache tags in response: ' . implode(',', array_diff($actual_tags, $expected_tags)));
debug('Missing cache tags in response: ' . implode(',', array_diff($expected_tags, $actual_tags)));
}
}
/**
* Ensures that some cache tags are present in the current response.
*
* @param string[] $expected_tags
* The expected tags.
* @param bool $include_default_tags
* (optional) Whether the default cache tags should be included.
*/
protected function assertCacheTags(array $expected_tags, $include_default_tags = TRUE) {
// The anonymous role cache tag is only added if the user is anonymous.
if ($include_default_tags) {
if (\Drupal::currentUser()->isAnonymous()) {
$expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']);
}
$expected_tags[] = 'http_response';
}
$actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
$expected_tags = array_unique($expected_tags);
sort($expected_tags);
sort($actual_tags);
$this->assertIdentical($actual_tags, $expected_tags);
$this->debugCacheTags($actual_tags, $expected_tags);
}
/**
* Ensures that some cache contexts are present in the current response.
*
* @param string[] $expected_contexts
* The expected cache contexts.
* @param string $message
* (optional) A verbose message to output.
* @param bool $include_default_contexts
* (optional) Whether the default contexts should automatically be included.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertCacheContexts(array $expected_contexts, $message = NULL, $include_default_contexts = TRUE) {
if ($include_default_contexts) {
$default_contexts = ['languages:language_interface', 'theme'];
// Add the user.permission context to the list of default contexts except
// when user is already there.
if (!in_array('user', $expected_contexts)) {
$default_contexts[] = 'user.permissions';
}
$expected_contexts = Cache::mergeContexts($expected_contexts, $default_contexts);
}
$actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts');
sort($expected_contexts);
sort($actual_contexts);
$match = $actual_contexts === $expected_contexts;
if (!$match) {
debug('Unwanted cache contexts in response: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
debug('Missing cache contexts in response: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
}
$this->assertIdentical($actual_contexts, $expected_contexts, $message);
// For compatibility with both BrowserTestBase and WebTestBase always return
// a boolean.
return $match;
}
/**
* Asserts the max age header.
*
* @param int $max_age
*/
protected function assertCacheMaxAge($max_age) {
$cache_control_header = $this->drupalGetHeader('Cache-Control');
if (strpos($cache_control_header, 'max-age:' . $max_age) === FALSE) {
debug('Expected max-age:' . $max_age . '; Response max-age:' . $cache_control_header);
}
$this->assertTrue(strpos($cache_control_header, 'max-age:' . $max_age));
}
}

View file

@ -4,7 +4,7 @@ namespace Drupal\Tests\system\Functional\Cache;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\FormattableMarkup;
/**
* Provides helper methods for page cache tags tests.
@ -44,14 +44,14 @@ abstract class PageCacheTagsTestBase extends BrowserTestBase {
*/
protected function verifyPageCache(Url $url, $hit_or_miss, $tags = FALSE) {
$this->drupalGet($url);
$message = SafeMarkup::format('Page cache @hit_or_miss for %path.', ['@hit_or_miss' => $hit_or_miss, '%path' => $url->toString()]);
$message = new FormattableMarkup('Page cache @hit_or_miss for %path.', ['@hit_or_miss' => $hit_or_miss, '%path' => $url->toString()]);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), $hit_or_miss, $message);
if ($hit_or_miss === 'HIT' && is_array($tags)) {
$absolute_url = $url->setAbsolute()->toString();
$cid_parts = [$absolute_url, 'html'];
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
$cache_entry = \Drupal::cache('page')->get($cid);
sort($cache_entry->tags);
$tags = array_unique($tags);
sort($tags);

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\system\Functional\Cache;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the 'session.exists' cache context service.
*
* @group Cache
*/
class SessionExistsCacheContextTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['session_exists_cache_context_test'];
/**
* Tests \Drupal\Core\Cache\Context\SessionExistsCacheContext::getContext().
*/
public function testCacheContext() {
$this->dumpHeaders = TRUE;
// 1. No session (anonymous).
$this->assertSessionCookieOnClient(FALSE);
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSessionCookieOnClient(FALSE);
$this->assertRaw('Session does not exist!');
$this->assertRaw('[session.exists]=0');
// 2. Session (authenticated).
$this->assertSessionCookieOnClient(FALSE);
$this->drupalLogin($this->rootUser);
$this->assertSessionCookieOnClient(TRUE);
$this->assertRaw('Session exists!');
$this->assertRaw('[session.exists]=1');
$this->drupalLogout();
$this->assertSessionCookieOnClient(FALSE);
$this->assertRaw('Session does not exist!');
$this->assertRaw('[session.exists]=0');
// 3. Session (anonymous).
$this->assertSessionCookieOnClient(FALSE);
$this->drupalGet(Url::fromRoute('<front>', [], ['query' => ['trigger_session' => 1]]));
$this->assertSessionCookieOnClient(TRUE);
$this->assertRaw('Session does not exist!');
$this->assertRaw('[session.exists]=0');
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSessionCookieOnClient(TRUE);
$this->assertRaw('Session exists!');
$this->assertRaw('[session.exists]=1');
}
/**
* Asserts whether a session cookie is present on the client or not.
*/
public function assertSessionCookieOnClient($expected_present) {
$this->assertEqual($expected_present, (bool) $this->getSession()->getCookie($this->getSessionName()), 'Session cookie exists.');
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\Tests\BrowserTestBase;
/**
* Tests alteration of arguments passed to \Drupal::moduleHandler->alter().
*
* @group Common
*/
class AlterTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'common_test'];
/**
* Tests if the theme has been altered.
*/
public function testDrupalAlter() {
// This test depends on Bartik, so make sure that it is always the current
// active theme.
\Drupal::service('theme_handler')->install(['bartik']);
\Drupal::theme()->setActiveTheme(\Drupal::service('theme.initialization')->initTheme('bartik'));
$array = ['foo' => 'bar'];
$entity = new \stdClass();
$entity->foo = 'bar';
// Verify alteration of a single argument.
$array_copy = $array;
$array_expected = ['foo' => 'Drupal theme'];
\Drupal::moduleHandler()->alter('drupal_alter', $array_copy);
\Drupal::theme()->alter('drupal_alter', $array_copy);
$this->assertEqual($array_copy, $array_expected, 'Single array was altered.');
$entity_copy = clone $entity;
$entity_expected = clone $entity;
$entity_expected->foo = 'Drupal theme';
\Drupal::moduleHandler()->alter('drupal_alter', $entity_copy);
\Drupal::theme()->alter('drupal_alter', $entity_copy);
$this->assertEqual($entity_copy, $entity_expected, 'Single object was altered.');
// Verify alteration of multiple arguments.
$array_copy = $array;
$array_expected = ['foo' => 'Drupal theme'];
$entity_copy = clone $entity;
$entity_expected = clone $entity;
$entity_expected->foo = 'Drupal theme';
$array2_copy = $array;
$array2_expected = ['foo' => 'Drupal theme'];
\Drupal::moduleHandler()->alter('drupal_alter', $array_copy, $entity_copy, $array2_copy);
\Drupal::theme()->alter('drupal_alter', $array_copy, $entity_copy, $array2_copy);
$this->assertEqual($array_copy, $array_expected, 'First argument to \Drupal::moduleHandler->alter() was altered.');
$this->assertEqual($entity_copy, $entity_expected, 'Second argument to \Drupal::moduleHandler->alter() was altered.');
$this->assertEqual($array2_copy, $array2_expected, 'Third argument to \Drupal::moduleHandler->alter() was altered.');
// Verify alteration order when passing an array of types to \Drupal::moduleHandler->alter().
// common_test_module_implements_alter() places 'block' implementation after
// other modules.
$array_copy = $array;
$array_expected = ['foo' => 'Drupal block theme'];
\Drupal::moduleHandler()->alter(['drupal_alter', 'drupal_alter_foo'], $array_copy);
\Drupal::theme()->alter(['drupal_alter', 'drupal_alter_foo'], $array_copy);
$this->assertEqual($array_copy, $array_expected, 'hook_TYPE_alter() implementations ran in correct order.');
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Verifies that bubbleable metadata of early rendering is not lost.
*
* @group Common
*/
class EarlyRenderingControllerTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $dumpHeaders = TRUE;
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'early_rendering_controller_test'];
/**
* Tests theme preprocess functions being able to attach assets.
*/
public function testEarlyRendering() {
// Render array: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.render_array'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.render_array.early'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertCacheTag('foo');
// AjaxResponse: non-early & early.
// @todo Add cache tags assertion when AjaxResponse is made cacheable in
// https://www.drupal.org/node/956186.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response.early'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
// Basic Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response.early'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
// Response object with attachments: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments.early'));
$this->assertResponse(500);
$this->assertRaw('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\early_rendering_controller_test\AttachmentsTestResponse.');
// Cacheable Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response.early'));
$this->assertResponse(500);
$this->assertRaw('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\early_rendering_controller_test\CacheableTestResponse.');
// Basic domain object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object'));
$this->assertResponse(200);
$this->assertRaw('TestDomainObject');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object.early'));
$this->assertResponse(200);
$this->assertRaw('TestDomainObject');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
// Basic domain object with attachments: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments'));
$this->assertResponse(200);
$this->assertRaw('AttachmentsTestDomainObject');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments.early'));
$this->assertResponse(500);
$this->assertRaw('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\early_rendering_controller_test\AttachmentsTestDomainObject.');
// Cacheable Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object'));
$this->assertResponse(200);
$this->assertRaw('CacheableTestDomainObject');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object.early'));
$this->assertResponse(500);
$this->assertRaw('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\early_rendering_controller_test\CacheableTestDomainObject.');
// The exceptions are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink($this->root . '/' . $this->siteDirectory . '/error.log');
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the format_date() function.
*
* @group Common
*/
class FormatDateTest extends BrowserTestBase {
/**
* Tests admin-defined formats in format_date().
*/
public function testAdminDefinedFormatDate() {
// Create and log in an admin user.
$this->drupalLogin($this->drupalCreateUser(['administer site configuration']));
// Add new date format.
$edit = [
'id' => 'example_style',
'label' => 'Example Style',
'date_format_pattern' => 'j M y',
];
$this->drupalPostForm('admin/config/regional/date-time/formats/add', $edit, t('Add format'));
// Add a second date format with a different case than the first.
$edit = [
'id' => 'example_style_uppercase',
'label' => 'Example Style Uppercase',
'date_format_pattern' => 'j M Y',
];
$this->drupalPostForm('admin/config/regional/date-time/formats/add', $edit, t('Add format'));
$this->assertText(t('Custom date format added.'));
$timestamp = strtotime('2007-03-10T00:00:00+00:00');
$this->assertIdentical(format_date($timestamp, 'example_style', '', 'America/Los_Angeles'), '9 Mar 07');
$this->assertIdentical(format_date($timestamp, 'example_style_uppercase', '', 'America/Los_Angeles'), '9 Mar 2007');
$this->assertIdentical(format_date($timestamp, 'undefined_style'), format_date($timestamp, 'fallback'), 'Test format_date() defaulting to `fallback` when $type not found.');
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\node\NodeInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that anonymous users are not served any JavaScript in the Standard
* installation profile.
*
* @group Common
*/
class NoJavaScriptAnonymousTest extends BrowserTestBase {
protected $profile = 'standard';
protected function setUp() {
parent::setUp();
// Grant the anonymous user the permission to look at user profiles.
user_role_grant_permissions('anonymous', ['access user profiles']);
}
/**
* Tests that anonymous users are not served any JavaScript.
*/
public function testNoJavaScript() {
// Create a node that is listed on the frontpage.
$this->drupalCreateNode([
'promote' => NodeInterface::PROMOTED,
]);
$user = $this->drupalCreateUser();
// Test frontpage.
$this->drupalGet('');
$this->assertNoJavaScriptExceptHtml5Shiv();
// Test node page.
$this->drupalGet('node/1');
$this->assertNoJavaScriptExceptHtml5Shiv();
// Test user profile page.
$this->drupalGet('user/' . $user->id());
$this->assertNoJavaScriptExceptHtml5Shiv();
}
/**
* Passes if no JavaScript is found on the page except the HTML5 shiv.
*
* The HTML5 shiv is necessary for e.g. the <article> tag which Drupal 8 uses
* to work in older browsers like Internet Explorer 8.
*/
protected function assertNoJavaScriptExceptHtml5Shiv() {
// Ensure drupalSettings is not set.
$settings = $this->getDrupalSettings();
$this->assertTrue(empty($settings), 'drupalSettings is not set.');
// Ensure the HTML5 shiv exists.
$this->assertRaw('html5shiv/html5shiv.min.js', 'HTML5 shiv JavaScript exists.');
// Ensure no other JavaScript file exists on the page, while ignoring the
// HTML5 shiv.
$this->assertNoPattern('/(?<!html5shiv\.min)\.js/', "No other JavaScript exists.");
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Performs integration tests on drupal_render().
*
* @group Common
*/
class RenderWebTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['common_test'];
/**
* Asserts the cache context for the wrapper format is always present.
*/
public function testWrapperFormatCacheContext() {
$this->drupalGet('common-test/type-link-active-class');
$this->assertIdentical(0, strpos($this->getSession()->getPage()->getContent(), "<!DOCTYPE html>\n<html"));
$this->assertIdentical('text/html; charset=UTF-8', $this->drupalGetHeader('Content-Type'));
$this->assertTitle('Test active link class | Drupal');
$this->assertCacheContext('url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT);
$this->drupalGet('common-test/type-link-active-class', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'json']]);
$this->assertIdentical('application/json', $this->drupalGetHeader('Content-Type'));
$json = Json::decode($this->getSession()->getPage()->getContent());
$this->assertEqual(['content', 'title'], array_keys($json));
$this->assertIdentical('Test active link class', $json['title']);
$this->assertCacheContext('url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT);
}
}

View file

@ -0,0 +1,320 @@
<?php
namespace Drupal\Tests\system\Functional\Common;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Confirm that \Drupal\Core\Url,
* \Drupal\Component\Utility\UrlHelper::filterQueryParameters(),
* \Drupal\Component\Utility\UrlHelper::buildQuery(), and
* \Drupal\Core\Utility\LinkGeneratorInterface::generate()
* work correctly with various input.
*
* @group Common
*/
class UrlTest extends BrowserTestBase {
public static $modules = ['common_test', 'url_alter_test'];
/**
* Confirms that invalid URLs are filtered in link generating functions.
*/
public function testLinkXSS() {
// Test \Drupal::l().
$text = $this->randomMachineName();
$path = "<SCRIPT>alert('XSS')</SCRIPT>";
$encoded_path = "3CSCRIPT%3Ealert%28%27XSS%27%29%3C/SCRIPT%3E";
$link = \Drupal::l($text, Url::fromUserInput('/' . $path));
$this->assertTrue(strpos($link, $encoded_path) !== FALSE && strpos($link, $path) === FALSE, format_string('XSS attack @path was filtered by \Drupal\Core\Utility\LinkGeneratorInterface::generate().', ['@path' => $path]));
// Test \Drupal\Core\Url.
$link = Url::fromUri('base:' . $path)->toString();
$this->assertTrue(strpos($link, $encoded_path) !== FALSE && strpos($link, $path) === FALSE, format_string('XSS attack @path was filtered by #theme', ['@path' => $path]));
}
/**
* Tests that #type=link bubbles outbound route/path processors' metadata.
*/
public function testLinkBubbleableMetadata() {
$cases = [
['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT], []],
['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => Cache::PERMANENT], []],
['Route processor link', 'route:system.run_cron', [], ['contexts' => ['session'], 'tags' => [], 'max-age' => Cache::PERMANENT], ['placeholders' => []]],
['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.site', 'session'], 'tags' => [], 'max-age' => Cache::PERMANENT], ['placeholders' => []]],
['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT], []],
['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT], []],
];
foreach ($cases as $case) {
list($title, $uri, $options, $expected_cacheability, $expected_attachments) = $case;
$expected_cacheability['contexts'] = Cache::mergeContexts($expected_cacheability['contexts'], ['languages:language_interface', 'theme', 'user.permissions']);
$link = [
'#type' => 'link',
'#title' => $title,
'#options' => $options,
'#url' => Url::fromUri($uri),
];
\Drupal::service('renderer')->renderRoot($link);
$this->pass($title);
$this->assertEqual($expected_cacheability, $link['#cache']);
$this->assertEqual($expected_attachments, $link['#attached']);
}
}
/**
* Tests that default and custom attributes are handled correctly on links.
*/
public function testLinkAttributes() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
// Test that hreflang is added when a link has a known language.
$language = new Language(['id' => 'fr', 'name' => 'French']);
$hreflang_link = [
'#type' => 'link',
'#options' => [
'language' => $language,
],
'#url' => Url::fromUri('https://www.drupal.org'),
'#title' => 'bar',
];
$langcode = $language->getId();
// Test that the default hreflang handling for links does not override a
// hreflang attribute explicitly set in the render array.
$hreflang_override_link = $hreflang_link;
$hreflang_override_link['#options']['attributes']['hreflang'] = 'foo';
$rendered = $renderer->renderRoot($hreflang_link);
$this->assertTrue($this->hasAttribute('hreflang', $rendered, $langcode), format_string('hreflang attribute with value @langcode is present on a rendered link when langcode is provided in the render array.', ['@langcode' => $langcode]));
$rendered = $renderer->renderRoot($hreflang_override_link);
$this->assertTrue($this->hasAttribute('hreflang', $rendered, 'foo'), format_string('hreflang attribute with value @hreflang is present on a rendered link when @hreflang is provided in the render array.', ['@hreflang' => 'foo']));
// Test the active class in links produced by
// \Drupal\Core\Utility\LinkGeneratorInterface::generate() and #type 'link'.
$options_no_query = [];
$options_query = [
'query' => [
'foo' => 'bar',
'one' => 'two',
],
];
$options_query_reverse = [
'query' => [
'one' => 'two',
'foo' => 'bar',
],
];
// Test #type link.
$path = 'common-test/type-link-active-class';
$this->drupalGet($path, $options_no_query);
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', [':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active']);
$this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page is marked active.');
$links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', [':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active']);
$this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page with a query string when the current page has no query string is not marked active.');
$this->drupalGet($path, $options_query);
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', [':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active']);
$this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page with a query string that matches the current query string is marked active.');
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', [':href' => Url::fromRoute('common_test.l_active_class', [], $options_query_reverse)->toString(), ':class' => 'is-active']);
$this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.');
$links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', [':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active']);
$this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page without a query string when the current page has a query string is not marked active.');
// Test adding a custom class in links produced by
// \Drupal\Core\Utility\LinkGeneratorInterface::generate() and #type 'link'.
// Test the link generator.
$class_l = $this->randomMachineName();
$link_l = \Drupal::l($this->randomMachineName(), new Url('<current>', [], ['attributes' => ['class' => [$class_l]]]));
$this->assertTrue($this->hasAttribute('class', $link_l, $class_l), format_string('Custom class @class is present on link when requested by l()', ['@class' => $class_l]));
// Test #type.
$class_theme = $this->randomMachineName();
$type_link = [
'#type' => 'link',
'#title' => $this->randomMachineName(),
'#url' => Url::fromRoute('<current>'),
'#options' => [
'attributes' => [
'class' => [$class_theme],
],
],
];
$link_theme = $renderer->renderRoot($type_link);
$this->assertTrue($this->hasAttribute('class', $link_theme, $class_theme), format_string('Custom class @class is present on link when requested by #type', ['@class' => $class_theme]));
}
/**
* Tests that link functions support render arrays as 'text'.
*/
public function testLinkRenderArrayText() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
// Build a link with the link generator for reference.
$l = \Drupal::l('foo', Url::fromUri('https://www.drupal.org'));
// Test a renderable array passed to the link generator.
$renderer->executeInRenderContext(new RenderContext(), function () use ($renderer, $l) {
$renderable_text = ['#markup' => 'foo'];
$l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org'));
$this->assertEqual($l_renderable_text, $l);
});
// Test a themed link with plain text 'text'.
$type_link_plain_array = [
'#type' => 'link',
'#title' => 'foo',
'#url' => Url::fromUri('https://www.drupal.org'),
];
$type_link_plain = $renderer->renderRoot($type_link_plain_array);
$this->assertEqual($type_link_plain, $l);
// Build a themed link with renderable 'text'.
$type_link_nested_array = [
'#type' => 'link',
'#title' => ['#markup' => 'foo'],
'#url' => Url::fromUri('https://www.drupal.org'),
];
$type_link_nested = $renderer->renderRoot($type_link_nested_array);
$this->assertEqual($type_link_nested, $l);
}
/**
* Checks for class existence in link.
*
* @param $link
* URL to search.
* @param $class
* Element class to search for.
*
* @return bool
* TRUE if the class is found, FALSE otherwise.
*/
private function hasAttribute($attribute, $link, $class) {
return preg_match('|' . $attribute . '="([^\"\s]+\s+)*' . $class . '|', $link);
}
/**
* Tests UrlHelper::filterQueryParameters().
*/
public function testDrupalGetQueryParameters() {
$original = [
'a' => 1,
'b' => [
'd' => 4,
'e' => [
'f' => 5,
],
],
'c' => 3,
];
// First-level exclusion.
$result = $original;
unset($result['b']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, ['b']), $result, "'b' was removed.");
// Second-level exclusion.
$result = $original;
unset($result['b']['d']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, ['b[d]']), $result, "'b[d]' was removed.");
// Third-level exclusion.
$result = $original;
unset($result['b']['e']['f']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, ['b[e][f]']), $result, "'b[e][f]' was removed.");
// Multiple exclusions.
$result = $original;
unset($result['a'], $result['b']['e'], $result['c']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, ['a', 'b[e]', 'c']), $result, "'a', 'b[e]', 'c' were removed.");
}
/**
* Tests UrlHelper::parse().
*/
public function testDrupalParseUrl() {
// Relative, absolute, and external URLs, without/with explicit script path,
// without/with Drupal path.
foreach (['', '/', 'https://www.drupal.org/'] as $absolute) {
foreach (['', 'index.php/'] as $script) {
foreach (['', 'foo/bar'] as $path) {
$url = $absolute . $script . $path . '?foo=bar&bar=baz&baz#foo';
$expected = [
'path' => $absolute . $script . $path,
'query' => ['foo' => 'bar', 'bar' => 'baz', 'baz' => ''],
'fragment' => 'foo',
];
$this->assertEqual(UrlHelper::parse($url), $expected, 'URL parsed correctly.');
}
}
}
// Relative URL that is known to confuse parse_url().
$url = 'foo/bar:1';
$result = [
'path' => 'foo/bar:1',
'query' => [],
'fragment' => '',
];
$this->assertEqual(UrlHelper::parse($url), $result, 'Relative URL parsed correctly.');
// Test that drupal can recognize an absolute URL. Used to prevent attack vectors.
$url = 'https://www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo';
$this->assertTrue(UrlHelper::isExternal($url), 'Correctly identified an external URL.');
// Test that UrlHelper::parse() does not allow spoofing a URL to force a malicious redirect.
$parts = UrlHelper::parse('forged:http://cwe.mitre.org/data/definitions/601.html');
$this->assertFalse(UrlHelper::isValid($parts['path'], TRUE), '\Drupal\Component\Utility\UrlHelper::isValid() correctly parsed a forged URL.');
}
/**
* Tests external URL handling.
*/
public function testExternalUrls() {
$test_url = 'https://www.drupal.org/';
// Verify external URL can contain a fragment.
$url = $test_url . '#drupal';
$result = Url::fromUri($url)->toString();
$this->assertEqual($url, $result, 'External URL with fragment works without a fragment in $options.');
// Verify fragment can be overridden in an external URL.
$url = $test_url . '#drupal';
$fragment = $this->randomMachineName(10);
$result = Url::fromUri($url, ['fragment' => $fragment])->toString();
$this->assertEqual($test_url . '#' . $fragment, $result, 'External URL fragment is overridden with a custom fragment in $options.');
// Verify external URL can contain a query string.
$url = $test_url . '?drupal=awesome';
$result = Url::fromUri($url)->toString();
$this->assertEqual($url, $result);
// Verify external URL can be extended with a query string.
$url = $test_url;
$query = ['awesome' => 'drupal'];
$result = Url::fromUri($url, ['query' => $query])->toString();
$this->assertSame('https://www.drupal.org/?awesome=drupal', $result);
// Verify query string can be extended in an external URL.
$url = $test_url . '?drupal=awesome';
$query = ['awesome' => 'drupal'];
$result = Url::fromUri($url, ['query' => $query])->toString();
$this->assertEqual('https://www.drupal.org/?drupal=awesome&awesome=drupal', $result);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\system\Functional\Condition;
use Drupal\node\Entity\Node;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that condition plugins basic form handling is working.
*
* Checks condition forms and submission and gives a very cursory check to make
* sure the configuration that was submitted actually causes the condition to
* validate correctly.
*
* @group Condition
*/
class ConditionFormTest extends BrowserTestBase {
public static $modules = ['node', 'condition_test'];
/**
* Submit the condition_node_type_test_form to test condition forms.
*/
public function testConfigForm() {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$article = Node::create([
'type' => 'article',
'title' => $this->randomMachineName(),
]);
$article->save();
$this->drupalGet('condition_test');
$this->assertField('bundles[article]', 'There is an article bundle selector.');
$this->assertField('bundles[page]', 'There is a page bundle selector.');
$this->drupalPostForm(NULL, ['bundles[page]' => 'page', 'bundles[article]' => 'article'], t('Submit'));
// @see \Drupal\condition_test\FormController::submitForm()
$this->assertText('Bundle: page');
$this->assertText('Bundle: article');
$this->assertText('Executed successfully.', 'The form configured condition executed properly.');
}
}

View file

@ -4,7 +4,6 @@ namespace Drupal\Tests\system\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use GuzzleHttp\Cookie\CookieJar;
/**
* Tests protecting routes by requiring CSRF token in the request header.
@ -27,7 +26,7 @@ class CsrfRequestHeaderTest extends BrowserTestBase {
* uses the deprecated _access_rest_csrf.
*/
public function testRouteAccess() {
$client = \Drupal::httpClient();
$client = $this->getHttpClient();
$csrf_token_paths = ['deprecated/session/token', 'session/token'];
// Test using the both the current path and a test path that returns
// a token using the deprecated 'rest' value.
@ -44,11 +43,6 @@ class CsrfRequestHeaderTest extends BrowserTestBase {
$url = Url::fromRoute($route_name)
->setAbsolute(TRUE)
->toString();
$domain = parse_url($url, PHP_URL_HOST);
$session_id = $this->getSession()->getCookie($this->getSessionName());
/** @var \GuzzleHttp\Cookie\CookieJar $cookies */
$cookies = CookieJar::fromArray([$this->getSessionName() => $session_id], $domain);
$post_options = [
'headers' => ['Accept' => 'text/plain'],
'http_errors' => FALSE,
@ -60,7 +54,7 @@ class CsrfRequestHeaderTest extends BrowserTestBase {
// Add cookies to POST options so that all other requests are for the
// authenticated user.
$post_options['cookies'] = $cookies;
$post_options['cookies'] = $this->getSessionCookies();
// Test that access is denied with no token in header.
$result = $client->post($url, $post_options);

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\Tests\system\Functional\Database;
use Drupal\KernelTests\Core\Database\DatabaseTestBase as DatabaseKernelTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Base class for databases database tests.
*/
abstract class DatabaseTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['database_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
DatabaseKernelTestBase::addSampleData();
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace Drupal\Tests\system\Functional\Database;
/**
* Fetches into a class.
*
* PDO supports using a new instance of an arbitrary class for records
* rather than just a stdClass or array. This class is for testing that
* functionality. (See testQueryFetchClass() below)
*/
class FakeRecord {}

View file

@ -0,0 +1,169 @@
<?php
namespace Drupal\Tests\system\Functional\Database;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the pager query select extender.
*
* @group Database
*/
class SelectPagerDefaultTest extends DatabaseTestBase {
/**
* Confirms that a pager query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
public function testEvenPagerQuery() {
// To keep the test from being too brittle, we determine up front
// what the page count should be dynamically, and pass the control
// information forward to the actual query on the other side of the
// HTTP request.
$limit = 2;
$count = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$correct_number = $limit;
$num_pages = floor($count / $limit);
// If there is no remainder from rounding, subtract 1 since we index from 0.
if (!($num_pages * $limit < $count)) {
$num_pages--;
}
for ($page = 0; $page <= $num_pages; ++$page) {
$this->drupalGet('database_test/pager_query_even/' . $limit, ['query' => ['page' => $page]]);
$data = json_decode($this->getSession()->getPage()->getContent());
if ($page == $num_pages) {
$correct_number = $count - ($limit * $page);
}
$this->assertCount($correct_number, $data->names, format_string('Correct number of records returned by pager: @number', ['@number' => $correct_number]));
}
}
/**
* Confirms that a pager query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
public function testOddPagerQuery() {
// To keep the test from being too brittle, we determine up front
// what the page count should be dynamically, and pass the control
// information forward to the actual query on the other side of the
// HTTP request.
$limit = 2;
$count = db_query('SELECT COUNT(*) FROM {test_task}')->fetchField();
$correct_number = $limit;
$num_pages = floor($count / $limit);
// If there is no remainder from rounding, subtract 1 since we index from 0.
if (!($num_pages * $limit < $count)) {
$num_pages--;
}
for ($page = 0; $page <= $num_pages; ++$page) {
$this->drupalGet('database_test/pager_query_odd/' . $limit, ['query' => ['page' => $page]]);
$data = json_decode($this->getSession()->getPage()->getContent());
if ($page == $num_pages) {
$correct_number = $count - ($limit * $page);
}
$this->assertCount($correct_number, $data->names, format_string('Correct number of records returned by pager: @number', ['@number' => $correct_number]));
}
}
/**
* Confirms that a pager query results with an inner pager query are valid.
*
* This is a regression test for #467984.
*/
public function testInnerPagerQuery() {
$query = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender');
$query
->fields('t', ['age'])
->orderBy('age')
->limit(5);
$outer_query = db_select($query);
$outer_query->addField('subquery', 'age');
$ages = $outer_query
->execute()
->fetchCol();
$this->assertEqual($ages, [25, 26, 27, 28], 'Inner pager query returned the correct ages.');
}
/**
* Confirms that a paging query results with a having expression are valid.
*
* This is a regression test for #467984.
*/
public function testHavingPagerQuery() {
$query = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender');
$query
->fields('t', ['name'])
->orderBy('name')
->groupBy('name')
->having('MAX(age) > :count', [':count' => 26])
->limit(5);
$ages = $query
->execute()
->fetchCol();
$this->assertEqual($ages, ['George', 'Ringo'], 'Pager query with having expression returned the correct ages.');
}
/**
* Confirms that every pager gets a valid, non-overlapping element ID.
*/
public function testElementNumbers() {
$request = Request::createFromGlobals();
$request->query->replace([
'page' => '3, 2, 1, 0',
]);
\Drupal::getContainer()->get('request_stack')->push($request);
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->element(2)
->fields('t', ['name'])
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'Paul', 'Pager query #1 with a specified element ID returned the correct results.');
// Setting an element smaller than the previous one
// should not overwrite the pager $maxElement with a smaller value.
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->element(1)
->fields('t', ['name'])
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'George', 'Pager query #2 with a specified element ID returned the correct results.');
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->fields('t', ['name'])
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'John', 'Pager query #3 with a generated element ID returned the correct results.');
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\system\Functional\Database;
/**
* Tests the tablesort query extender.
*
* @group Database
*/
class SelectTableSortDefaultTest extends DatabaseTestBase {
/**
* Confirms that a tablesort query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
public function testTableSortQuery() {
$sorts = [
['field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'],
['field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'],
['field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'],
['field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'],
// more elements here
];
foreach ($sorts as $sort) {
$this->drupalGet('database_test/tablesort/', ['query' => ['order' => $sort['field'], 'sort' => $sort['sort']]]);
$data = json_decode($this->getSession()->getPage()->getContent());
$first = array_shift($data->tasks);
$last = array_pop($data->tasks);
$this->assertEqual($first->task, $sort['first'], 'Items appear in the correct order.');
$this->assertEqual($last->task, $sort['last'], 'Items appear in the correct order.');
}
}
/**
* Confirms precedence of tablesorts headers.
*
* If a tablesort's orderByHeader is called before another orderBy, then its
* header happens first.
*/
public function testTableSortQueryFirst() {
$sorts = [
['field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'],
['field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'],
['field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'],
['field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'],
// more elements here
];
foreach ($sorts as $sort) {
$this->drupalGet('database_test/tablesort_first/', ['query' => ['order' => $sort['field'], 'sort' => $sort['sort']]]);
$data = json_decode($this->getSession()->getPage()->getContent());
$first = array_shift($data->tasks);
$last = array_pop($data->tasks);
$this->assertEqual($first->task, $sort['first'], format_string('Items appear in the correct order sorting by @field @sort.', ['@field' => $sort['field'], '@sort' => $sort['sort']]));
$this->assertEqual($last->task, $sort['last'], format_string('Items appear in the correct order sorting by @field @sort.', ['@field' => $sort['field'], '@sort' => $sort['sort']]));
}
}
/**
* Confirms that tableselect is rendered without error.
*
* Specifically that no sort is set in a tableselect, and that header links
* are correct.
*/
public function testTableSortDefaultSort() {
$assert = $this->assertSession();
$this->drupalGet('database_test/tablesort_default_sort');
// Verify that the table was displayed. Just the header is checked for
// because if there were any fatal errors or exceptions in displaying the
// sorted table, it would not print the table.
$assert->pageTextContains(t('Username'));
// Verify that the header links are built properly.
$assert->linkByHrefExists('database_test/tablesort_default_sort');
$assert->responseMatches('/\<a.*title\=\"' . t('sort by Username') . '\".*\>/');
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\Tests\system\Functional\Database;
use Drupal\Core\Database\Database;
/**
* Tests the temporary query functionality.
*
* @group Database
*/
class TemporaryQueryTest extends DatabaseTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['database_test'];
/**
* Returns the number of rows of a table.
*/
public function countTableRows($table_name) {
return db_select($table_name)->countQuery()->execute()->fetchField();
}
/**
* Confirms that temporary tables work and are limited to one request.
*/
public function testTemporaryQuery() {
$this->drupalGet('database_test/db_query_temporary');
$data = json_decode($this->getSession()->getPage()->getContent());
if ($data) {
$this->assertEqual($this->countTableRows('test'), $data->row_count, 'The temporary table contains the correct amount of rows.');
$this->assertFalse(Database::getConnection()->schema()->tableExists($data->table_name), 'The temporary table is, indeed, temporary.');
}
else {
$this->fail('The creation of the temporary table failed.');
}
// Now try to run two db_query_temporary() in the same request.
$table_name_test = db_query_temporary('SELECT name FROM {test}', []);
$table_name_task = db_query_temporary('SELECT pid FROM {test_task}', []);
$this->assertEqual($this->countTableRows($table_name_test), $this->countTableRows('test'), 'A temporary table was created successfully in this request.');
$this->assertEqual($this->countTableRows($table_name_task), $this->countTableRows('test_task'), 'A second temporary table was created successfully in this request.');
// Check that leading whitespace and comments do not cause problems
// in the modified query.
$sql = "
-- Let's select some rows into a temporary table
SELECT name FROM {test}
";
$table_name_test = db_query_temporary($sql, []);
$this->assertEqual($this->countTableRows($table_name_test), $this->countTableRows('test'), 'Leading white space and comments do not interfere with temporary table creation.');
}
}

View file

@ -33,7 +33,7 @@ class DrupalDateTimeTest extends BrowserTestBase {
$options = [
'query' => [
'date' => 'Tue+Sep+17+2013+21%3A35%3A31+GMT%2B0100+(BST)#',
]
],
];
// Query the AJAX Timezone Callback with a long-format date.
$response = $this->drupalGet('system/timezone/BST/3600/1', $options);

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\system\Functional\DrupalKernel;
use Drupal\Tests\BrowserTestBase;
/**
* Ensures that the container rebuild works as expected.
*
* @group DrupalKernel
*/
class ContainerRebuildWebTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['service_provider_test'];
/**
* Sets a different deployment identifier.
*/
public function testSetContainerRebuildWithDifferentDeploymentIdentifier() {
$assert = $this->assertSession();
// Ensure the parameter is not set.
$this->drupalGet('<front>');
$assert->responseHeaderEquals('container_rebuild_indicator', NULL);
$this->writeSettings(['settings' => ['deployment_identifier' => (object) ['value' => 'new-identifier', 'required' => TRUE]]]);
$this->drupalGet('<front>');
$assert->responseHeaderEquals('container_rebuild_indicator', 'new-identifier');
}
/**
* Tests container invalidation.
*/
public function testContainerInvalidation() {
$assert = $this->assertSession();
// Ensure that parameter is not set.
$this->drupalGet('<front>');
$assert->responseHeaderEquals('container_rebuild_test_parameter', NULL);
// Ensure that after setting the parameter, without a container rebuild the
// parameter is still not set.
$this->writeSettings(['settings' => ['container_rebuild_test_parameter' => (object) ['value' => 'rebuild_me_please', 'required' => TRUE]]]);
$this->drupalGet('<front>');
$assert->responseHeaderEquals('container_rebuild_test_parameter', NULL);
// Ensure that after container invalidation the parameter is set.
\Drupal::service('kernel')->invalidateContainer();
$this->drupalGet('<front>');
$assert->responseHeaderEquals('container_rebuild_test_parameter', 'rebuild_me_please');
}
}

View file

@ -21,7 +21,7 @@ class ConfigEntityImportTest extends BrowserTestBase {
*
* @var array
*/
public static $modules = ['action', 'block', 'filter', 'image', 'search', 'search_extra_type'];
public static $modules = ['action', 'block', 'filter', 'image', 'search', 'search_extra_type', 'config_test'];
/**
* {@inheritdoc}
@ -41,7 +41,9 @@ class ConfigEntityImportTest extends BrowserTestBase {
$this->doFilterFormatUpdate();
$this->doImageStyleUpdate();
$this->doSearchPageUpdate();
$this->doThirdPartySettingsUpdate();
}
/**
* Tests updating a action during import.
*/
@ -171,6 +173,34 @@ class ConfigEntityImportTest extends BrowserTestBase {
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests updating of third party settings.
*/
protected function doThirdPartySettingsUpdate() {
// Create a test action with a known label.
$name = 'system.action.third_party_settings_test';
/** @var \Drupal\config_test\Entity\ConfigTest $entity */
$entity = Action::create([
'id' => 'third_party_settings_test',
'plugin' => 'action_message_action',
]);
$entity->save();
$this->assertIdentical([], $entity->getThirdPartyProviders());
// Get a copy of the configuration before the third party setting is added.
$no_third_part_setting_config = $this->container->get('config.storage')->read($name);
// Add a third party setting.
$entity->setThirdPartySetting('config_test', 'integer', 1);
$entity->save();
$this->assertIdentical(1, $entity->getThirdPartySetting('config_test', 'integer'));
$has_third_part_setting_config = $this->container->get('config.storage')->read($name);
// Ensure configuration imports can completely remove third party settings.
$this->assertConfigUpdateImport($name, $has_third_part_setting_config, $no_third_part_setting_config);
}
/**
* Tests that a single set of plugin config stays in sync.
*

View file

@ -10,7 +10,7 @@ use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\system\Tests\Cache\PageCacheTagsTestBase;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
@ -153,7 +153,7 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
* @return string[]
* An array of the additional cache contexts.
*
* @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity()
* @see \Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase::createEntity()
*/
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
return [];
@ -167,7 +167,7 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
* @return array
* An array of the additional cache tags.
*
* @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity()
* @see \Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase::createEntity()
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
return [];
@ -412,7 +412,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
$this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
$this->pass("Test listing of referencing entities.", 'Debug');
// Prime the page cache for the listing of referencing entities.
$this->verifyPageCache($listing_url, 'MISS');
@ -431,7 +430,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
$this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
$this->pass("Test listing containing referenced entity.", 'Debug');
// Prime the page cache for the listing containing the referenced entity.
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags);
@ -441,7 +439,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
$this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
// Verify that after modifying the referenced entity, there is a cache miss
// for every route except the one for the non-referencing entity.
$this->pass("Test modification of referenced entity.", 'Debug');
@ -458,7 +455,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify that after modifying the referencing entity, there is a cache miss
// for every route except the ones for the non-referencing entity and the
// empty entity listing.
@ -475,7 +471,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify that after modifying the non-referencing entity, there is a cache
// miss only for the non-referencing entity route.
$this->pass("Test modification of non-referencing entity.", 'Debug');
@ -489,7 +484,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
// Verify cache hits.
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
// Verify that after modifying the entity's display, there is a cache miss
// for both the referencing entity, and the listing of referencing
@ -509,7 +503,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($listing_url, 'HIT');
}
if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) {
// Verify that after modifying the corresponding bundle entity, there is a
// cache miss for both the referencing entity, and the listing of
@ -543,7 +536,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
}
}
if ($this->entity->getEntityType()->get('field_ui_base_route')) {
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
@ -561,7 +553,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
$this->pass("Test modification of referenced entity's configurable field.", 'Debug');
@ -579,7 +570,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($listing_url, 'HIT');
}
// Verify that after invalidating the entity's cache tag directly, there is
// a cache miss for every route except the ones for the non-referencing
// entity and the empty entity listing.
@ -611,7 +601,6 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
if (!empty($view_cache_tag)) {
// Verify that after invalidating the generic entity type's view cache tag
// directly, there is a cache miss for both the referencing entity, and the

View file

@ -0,0 +1,168 @@
<?php
namespace Drupal\Tests\system\Functional\Entity;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the entity form.
*
* @group Entity
*/
class EntityFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['entity_test', 'language'];
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(['administer entity_test content', 'view test entity']);
$this->drupalLogin($web_user);
// Add a language.
ConfigurableLanguage::createFromLangcode('ro')->save();
}
/**
* Tests basic form CRUD functionality.
*/
public function testFormCRUD() {
// All entity variations have to have the same results.
foreach (entity_test_entity_types() as $entity_type) {
$this->doTestFormCRUD($entity_type);
}
}
/**
* Tests basic multilingual form CRUD functionality.
*/
public function testMultilingualFormCRUD() {
// All entity variations have to have the same results.
foreach (entity_test_entity_types(ENTITY_TEST_TYPES_MULTILINGUAL) as $entity_type) {
$this->doTestMultilingualFormCRUD($entity_type);
}
}
/**
* Tests hook_entity_form_display_alter().
*
* @see entity_test_entity_form_display_alter()
*/
public function testEntityFormDisplayAlter() {
$this->drupalGet('entity_test/add');
$altered_field = $this->xpath('//input[@name="field_test_text[0][value]" and @size="42"]');
$this->assertTrue(count($altered_field) === 1, 'The altered field has the correct size value.');
}
/**
* Executes the form CRUD tests for the given entity type.
*
* @param string $entity_type
* The entity type to run the tests with.
*/
protected function doTestFormCRUD($entity_type) {
$name1 = $this->randomMachineName(8);
$name2 = $this->randomMachineName(10);
$edit = [
'name[0][value]' => $name1,
'field_test_text[0][value]' => $this->randomMachineName(16),
];
$this->drupalPostForm($entity_type . '/add', $edit, t('Save'));
$entity = $this->loadEntityByName($entity_type, $name1);
$this->assertTrue($entity, format_string('%entity_type: Entity found in the database.', ['%entity_type' => $entity_type]));
$edit['name[0][value]'] = $name2;
$this->drupalPostForm($entity_type . '/manage/' . $entity->id() . '/edit', $edit, t('Save'));
$entity = $this->loadEntityByName($entity_type, $name1);
$this->assertFalse($entity, format_string('%entity_type: The entity has been modified.', ['%entity_type' => $entity_type]));
$entity = $this->loadEntityByName($entity_type, $name2);
$this->assertTrue($entity, format_string('%entity_type: Modified entity found in the database.', ['%entity_type' => $entity_type]));
$this->assertNotEqual($entity->name->value, $name1, format_string('%entity_type: The entity name has been modified.', ['%entity_type' => $entity_type]));
$this->drupalGet($entity_type . '/manage/' . $entity->id() . '/edit');
$this->clickLink(t('Delete'));
$this->drupalPostForm(NULL, [], t('Delete'));
$entity = $this->loadEntityByName($entity_type, $name2);
$this->assertFalse($entity, format_string('%entity_type: Entity not found in the database.', ['%entity_type' => $entity_type]));
}
/**
* Executes the multilingual form CRUD tests for the given entity type ID.
*
* @param string $entity_type_id
* The ID of entity type to run the tests with.
*/
protected function doTestMultilingualFormCRUD($entity_type_id) {
$name1 = $this->randomMachineName(8);
$name1_ro = $this->randomMachineName(9);
$name2_ro = $this->randomMachineName(11);
$edit = [
'name[0][value]' => $name1,
'field_test_text[0][value]' => $this->randomMachineName(16),
];
$this->drupalPostForm($entity_type_id . '/add', $edit, t('Save'));
$entity = $this->loadEntityByName($entity_type_id, $name1);
$this->assertTrue($entity, format_string('%entity_type: Entity found in the database.', ['%entity_type' => $entity_type_id]));
// Add a translation to the newly created entity without using the Content
// translation module.
$entity->addTranslation('ro', ['name' => $name1_ro])->save();
$translated_entity = $this->loadEntityByName($entity_type_id, $name1)->getTranslation('ro');
$this->assertEqual($translated_entity->name->value, $name1_ro, format_string('%entity_type: The translation has been added.', ['%entity_type' => $entity_type_id]));
$edit['name[0][value]'] = $name2_ro;
$this->drupalPostForm('ro/' . $entity_type_id . '/manage/' . $entity->id() . '/edit', $edit, t('Save'));
$translated_entity = $this->loadEntityByName($entity_type_id, $name1)->getTranslation('ro');
$this->assertTrue($translated_entity, format_string('%entity_type: Modified translation found in the database.', ['%entity_type' => $entity_type_id]));
$this->assertEqual($translated_entity->name->value, $name2_ro, format_string('%entity_type: The name of the translation has been modified.', ['%entity_type' => $entity_type_id]));
$this->drupalGet('ro/' . $entity_type_id . '/manage/' . $entity->id() . '/edit');
$this->clickLink(t('Delete'));
$this->drupalPostForm(NULL, [], t('Delete Romanian translation'));
$entity = $this->loadEntityByName($entity_type_id, $name1);
$this->assertNotNull($entity, format_string('%entity_type: The original entity still exists.', ['%entity_type' => $entity_type_id]));
$this->assertFalse($entity->hasTranslation('ro'), format_string('%entity_type: Entity translation does not exist anymore.', ['%entity_type' => $entity_type_id]));
}
/**
* Loads a test entity by name always resetting the storage cache.
*/
protected function loadEntityByName($entity_type, $name) {
// Always load the entity from the database to ensure that changes are
// correctly picked up.
$entity_storage = $this->container->get('entity.manager')->getStorage($entity_type);
$entity_storage->resetCache();
$entities = $entity_storage->loadByProperties(['name' => $name]);
return $entities ? current($entities) : NULL;
}
/**
* Checks that validation handlers works as expected.
*/
public function testValidationHandlers() {
/** @var \Drupal\Core\State\StateInterface $state */
$state = $this->container->get('state');
// Check that from-level validation handlers can be defined and can alter
// the form array.
$state->set('entity_test.form.validate.test', 'form-level');
$this->drupalPostForm('entity_test/add', [], 'Save');
$this->assertTrue($state->get('entity_test.form.validate.result'), 'Form-level validation handlers behave correctly.');
// Check that defining a button-level validation handler causes an exception
// to be thrown.
$state->set('entity_test.form.validate.test', 'button-level');
$this->drupalPostForm('entity_test/add', [], 'Save');
$this->assertEqual($state->get('entity_test.form.save.exception'), 'Drupal\Core\Entity\EntityStorageException: Entity validation was skipped.', 'Button-level validation handlers behave correctly.');
}
}

View file

@ -6,9 +6,15 @@ use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Utility\Html;
use Drupal\Core\Language\LanguageInterface;
use Drupal\comment\CommentInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media\Entity\Media;
use Drupal\node\Entity\Node;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\node\NodeInterface;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Entity\User;
use Drupal\comment\Entity\Comment;
@ -17,22 +23,51 @@ use Drupal\comment\Entity\Comment;
*
* @group entity_reference
*/
class EntityReferenceSelectionAccessTest extends BrowserTestBase {
class EntityReferenceSelectionAccessTest extends KernelTestBase {
use CommentTestTrait;
use ContentTypeCreationTrait;
use MediaTypeCreationTrait;
use UserCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'comment'];
public static $modules = ['comment', 'field', 'file', 'image', 'node', 'media', 'system', 'taxonomy', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create an Article node type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->installSchema('system', 'sequences');
$this->installSchema('comment', ['comment_entity_statistics']);
$this->installSchema('file', ['file_usage']);
$this->installEntitySchema('comment');
$this->installEntitySchema('file');
$this->installEntitySchema('media');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->installConfig(['comment', 'field', 'media', 'node', 'taxonomy', 'user']);
// Create the anonymous and the admin users.
$anonymous_user = User::create([
'uid' => 0,
'name' => '',
]);
$anonymous_user->save();
$admin_user = User::create([
'uid' => 1,
'name' => 'admin',
'status' => 1,
]);
$admin_user->save();
}
/**
@ -74,9 +109,7 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$selection_options = [
'target_type' => 'node',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
],
'target_bundles' => NULL,
];
// Build a set of test data.
@ -112,8 +145,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
}
// Test as a non-admin.
$normal_user = $this->drupalCreateUser(['access content']);
\Drupal::currentUser()->setAccount($normal_user);
$normal_user = $this->createUser(['access content']);
$this->setCurrentUser($normal_user);
$referenceable_tests = [
[
'arguments' => [
@ -164,8 +197,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$this->assertReferenceable($selection_options, $referenceable_tests, 'Node handler');
// Test as an admin.
$admin_user = $this->drupalCreateUser(['access content', 'bypass node access']);
\Drupal::currentUser()->setAccount($admin_user);
$content_admin = $this->createUser(['access content', 'bypass node access']);
$this->setCurrentUser($content_admin);
$referenceable_tests = [
[
'arguments' => [
@ -200,10 +233,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$selection_options = [
'target_type' => 'user',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
'include_anonymous' => TRUE,
],
'target_bundles' => NULL,
'include_anonymous' => TRUE,
];
// Build a set of test data.
@ -243,7 +274,7 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
}
// Test as a non-admin.
\Drupal::currentUser()->setAccount($users['non_admin']);
$this->setCurrentUser($users['non_admin']);
$referenceable_tests = [
[
'arguments' => [
@ -282,7 +313,7 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'User handler');
\Drupal::currentUser()->setAccount($users['admin']);
$this->setCurrentUser($users['admin']);
$referenceable_tests = [
[
'arguments' => [
@ -322,7 +353,7 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$this->assertReferenceable($selection_options, $referenceable_tests, 'User handler (admin)');
// Test the 'include_anonymous' option.
$selection_options['handler_settings']['include_anonymous'] = FALSE;
$selection_options['include_anonymous'] = FALSE;
$referenceable_tests = [
[
'arguments' => [
@ -361,12 +392,11 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$selection_options = [
'target_type' => 'comment',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
],
'target_bundles' => NULL,
];
// Build a set of test data.
$this->createContentType(['type' => 'article', 'name' => 'Article']);
$node_values = [
'published' => [
'type' => 'article',
@ -437,8 +467,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
}
// Test as a non-admin.
$normal_user = $this->drupalCreateUser(['access content', 'access comments']);
\Drupal::currentUser()->setAccount($normal_user);
$normal_user = $this->createUser(['access content', 'access comments']);
$this->setCurrentUser($normal_user);
$referenceable_tests = [
[
'arguments' => [
@ -476,8 +506,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$this->assertReferenceable($selection_options, $referenceable_tests, 'Comment handler');
// Test as a comment admin.
$admin_user = $this->drupalCreateUser(['access content', 'access comments', 'administer comments']);
\Drupal::currentUser()->setAccount($admin_user);
$admin_user = $this->createUser(['access content', 'access comments', 'administer comments']);
$this->setCurrentUser($admin_user);
$referenceable_tests = [
[
'arguments' => [
@ -494,8 +524,8 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$this->assertReferenceable($selection_options, $referenceable_tests, 'Comment handler (comment admin)');
// Test as a node and comment admin.
$admin_user = $this->drupalCreateUser(['access content', 'access comments', 'administer comments', 'bypass node access']);
\Drupal::currentUser()->setAccount($admin_user);
$admin_user = $this->createUser(['access content', 'access comments', 'administer comments', 'bypass node access']);
$this->setCurrentUser($admin_user);
$referenceable_tests = [
[
'arguments' => [
@ -513,4 +543,236 @@ class EntityReferenceSelectionAccessTest extends BrowserTestBase {
$this->assertReferenceable($selection_options, $referenceable_tests, 'Comment handler (comment + node admin)');
}
/**
* Test the term-specific overrides of the selection handler.
*/
public function testTermHandler() {
// Create a 'Tags' vocabulary.
Vocabulary::create([
'name' => 'Tags',
'description' => $this->randomMachineName(),
'vid' => 'tags',
])->save();
$selection_options = [
'target_type' => 'taxonomy_term',
'handler' => 'default',
'target_bundles' => NULL,
];
// Build a set of test data.
$term_values = [
'published1' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published1',
],
'published2' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published2',
],
'unpublished' => [
'vid' => 'tags',
'status' => 0,
'name' => 'Term unpublished',
],
'published3' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published3',
'parent' => 'unpublished',
],
'published4' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published4',
'parent' => 'published3',
],
];
$terms = [];
$term_labels = [];
foreach ($term_values as $key => $values) {
$term = Term::create($values);
if (isset($values['parent'])) {
$term->parent->entity = $terms[$values['parent']];
}
$term->save();
$terms[$key] = $term;
$term_labels[$key] = Html::escape($term->label());
}
// Test as a non-admin.
$normal_user = $this->createUser(['access content']);
$this->setCurrentUser($normal_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
$terms['published2']->id() => $term_labels['published2'],
],
],
],
[
'arguments' => [
['published1', 'CONTAINS'],
['Published1', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
],
],
],
[
'arguments' => [
['published2', 'CONTAINS'],
['Published2', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published2']->id() => $term_labels['published2'],
],
],
],
[
'arguments' => [
['invalid term', 'CONTAINS'],
],
'result' => [],
],
[
'arguments' => [
['Term unpublished', 'CONTAINS'],
],
'result' => [],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Term handler');
// Test as an admin.
$admin_user = $this->createUser(['access content', 'administer taxonomy']);
$this->setCurrentUser($admin_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
$terms['published2']->id() => $term_labels['published2'],
$terms['unpublished']->id() => $term_labels['unpublished'],
$terms['published3']->id() => '-' . $term_labels['published3'],
$terms['published4']->id() => '--' . $term_labels['published4'],
],
],
],
[
'arguments' => [
['Term unpublished', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['unpublished']->id() => $term_labels['unpublished'],
],
],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Term handler (admin)');
}
/**
* Tests the selection handler for the media entity type.
*/
public function testMediaHandler() {
$selection_options = [
'target_type' => 'media',
'handler' => 'default',
'target_bundles' => NULL,
];
// Build a set of test data.
$media_type = $this->createMediaType('file');
$media_values = [
'published' => [
'bundle' => $media_type->id(),
'status' => 1,
'name' => 'Media published',
'uid' => 1,
],
'unpublished' => [
'bundle' => $media_type->id(),
'status' => 0,
'name' => 'Media unpublished',
'uid' => 1,
],
];
$media_entities = [];
$media_labels = [];
foreach ($media_values as $key => $values) {
$media = Media::create($values);
$media->save();
$media_entities[$key] = $media;
$media_labels[$key] = Html::escape($media->label());
}
// Test as a non-admin.
$normal_user = $this->createUser(['view media']);
$this->setCurrentUser($normal_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
$media_type->id() => [
$media_entities['published']->id() => $media_labels['published'],
],
],
],
[
'arguments' => [
['Media unpublished', 'CONTAINS'],
],
'result' => [],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Media handler');
// Test as an admin.
$admin_user = $this->createUser(['view media', 'administer media']);
$this->setCurrentUser($admin_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
$media_type->id() => [
$media_entities['published']->id() => $media_labels['published'],
$media_entities['unpublished']->id() => $media_labels['unpublished'],
],
],
],
[
'arguments' => [
['Media unpublished', 'CONTAINS'],
],
'result' => [
$media_type->id() => [
$media_entities['unpublished']->id() => $media_labels['unpublished'],
],
],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Media handler (admin)');
}
}

View file

@ -3,6 +3,8 @@
namespace Drupal\Tests\system\Functional\Entity;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
@ -60,20 +62,42 @@ class EntityRevisionsTest extends BrowserTestBase {
* The entity type to run the tests with.
*/
protected function runRevisionsTests($entity_type) {
// Create a translatable test field.
$field_storage = FieldStorageConfig::create([
'entity_type' => $entity_type,
'field_name' => 'translatable_test_field',
'type' => 'text',
'cardinality' => 2,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'label' => $this->randomMachineName(),
'bundle' => $entity_type,
'translatable' => TRUE,
]);
$field->save();
entity_get_form_display($entity_type, $entity_type, 'default')
->setComponent('translatable_test_field')
->save();
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($entity_type);
// Create initial entity.
$entity = $this->container->get('entity_type.manager')
->getStorage($entity_type)
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $storage
->create([
'name' => 'foo',
'user_id' => $this->webUser->id(),
]);
$entity->field_test_text->value = 'bar';
$entity->translatable_test_field->value = 'bar';
$entity->addTranslation('de', ['name' => 'foo - de']);
$entity->save();
$names = [];
$texts = [];
$created = [];
$values = [];
$revision_ids = [];
// Create three revisions.
@ -81,45 +105,74 @@ class EntityRevisionsTest extends BrowserTestBase {
for ($i = 0; $i < $revision_count; $i++) {
$legacy_revision_id = $entity->revision_id->value;
$legacy_name = $entity->name->value;
$legacy_text = $entity->field_test_text->value;
$legacy_text = $entity->translatable_test_field->value;
$entity = $this->container->get('entity_type.manager')
->getStorage($entity_type)->load($entity->id->value);
$entity = $storage->load($entity->id->value);
$entity->setNewRevision(TRUE);
$names[] = $entity->name->value = $this->randomMachineName(32);
$texts[] = $entity->field_test_text->value = $this->randomMachineName(32);
$created[] = $entity->created->value = time() + $i + 1;
$values['en'][$i] = [
'name' => $this->randomMachineName(32),
'translatable_test_field' => [
$this->randomMachineName(32),
$this->randomMachineName(32),
],
'created' => time() + $i + 1,
];
$entity->set('name', $values['en'][$i]['name']);
$entity->set('translatable_test_field', $values['en'][$i]['translatable_test_field']);
$entity->set('created', $values['en'][$i]['created']);
$entity->save();
$revision_ids[] = $entity->revision_id->value;
// Add some values for a translation of this revision.
if ($entity->getEntityType()->isTranslatable()) {
$values['de'][$i] = [
'name' => $this->randomMachineName(32),
'translatable_test_field' => [
$this->randomMachineName(32),
$this->randomMachineName(32),
],
];
$translation = $entity->getTranslation('de');
$translation->set('name', $values['de'][$i]['name']);
$translation->set('translatable_test_field', $values['de'][$i]['translatable_test_field']);
$translation->save();
}
// Check that the fields and properties contain new content.
$this->assertTrue($entity->revision_id->value > $legacy_revision_id, format_string('%entity_type: Revision ID changed.', ['%entity_type' => $entity_type]));
$this->assertNotEqual($entity->name->value, $legacy_name, format_string('%entity_type: Name changed.', ['%entity_type' => $entity_type]));
$this->assertNotEqual($entity->field_test_text->value, $legacy_text, format_string('%entity_type: Text changed.', ['%entity_type' => $entity_type]));
$this->assertNotEqual($entity->translatable_test_field->value, $legacy_text, format_string('%entity_type: Text changed.', ['%entity_type' => $entity_type]));
}
$storage = $this->container->get('entity_type.manager')->getStorage($entity_type);
$revisions = $storage->loadMultipleRevisions($revision_ids);
for ($i = 0; $i < $revision_count; $i++) {
// Load specific revision.
$entity_revision = $storage->loadRevision($revision_ids[$i]);
$entity_revision = $revisions[$revision_ids[$i]];
// Check if properties and fields contain the revision specific content.
$this->assertEqual($entity_revision->revision_id->value, $revision_ids[$i], format_string('%entity_type: Revision ID matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->name->value, $names[$i], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->field_test_text->value, $texts[$i], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->name->value, $values['en'][$i]['name'], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->translatable_test_field[0]->value, $values['en'][$i]['translatable_test_field'][0], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->translatable_test_field[1]->value, $values['en'][$i]['translatable_test_field'][1], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
// Check the translated values.
if ($entity->getEntityType()->isTranslatable()) {
$revision_translation = $entity_revision->getTranslation('de');
$this->assertEqual($revision_translation->name->value, $values['de'][$i]['name'], format_string('%entity_type: Name matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($revision_translation->translatable_test_field[0]->value, $values['de'][$i]['translatable_test_field'][0], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
$this->assertEqual($revision_translation->translatable_test_field[1]->value, $values['de'][$i]['translatable_test_field'][1], format_string('%entity_type: Text matches.', ['%entity_type' => $entity_type]));
}
// Check non-revisioned values are loaded.
$this->assertTrue(isset($entity_revision->created->value), format_string('%entity_type: Non-revisioned field is loaded.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->created->value, $created[2], format_string('%entity_type: Non-revisioned field value is the same between revisions.', ['%entity_type' => $entity_type]));
$this->assertEqual($entity_revision->created->value, $values['en'][2]['created'], format_string('%entity_type: Non-revisioned field value is the same between revisions.', ['%entity_type' => $entity_type]));
}
// Confirm the correct revision text appears in the edit form.
$entity = $this->container->get('entity_type.manager')
->getStorage($entity_type)
->load($entity->id->value);
$entity = $storage->load($entity->id->value);
$this->drupalGet($entity_type . '/manage/' . $entity->id->value . '/edit');
$this->assertFieldById('edit-name-0-value', $entity->name->value, format_string('%entity_type: Name matches in UI.', ['%entity_type' => $entity_type]));
$this->assertFieldById('edit-field-test-text-0-value', $entity->field_test_text->value, format_string('%entity_type: Text matches in UI.', ['%entity_type' => $entity_type]));
$this->assertFieldById('edit-translatable-test-field-0-value', $entity->translatable_test_field->value, format_string('%entity_type: Text matches in UI.', ['%entity_type' => $entity_type]));
}
/**
@ -135,28 +188,84 @@ class EntityRevisionsTest extends BrowserTestBase {
$entity->addTranslation('de', ['name' => 'default revision - de']);
$entity->save();
$forward_revision = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev')->loadUnchanged($entity->id());
$pending_revision = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev')->loadUnchanged($entity->id());
$forward_revision->setNewRevision();
$forward_revision->isDefaultRevision(FALSE);
$pending_revision->setNewRevision();
$pending_revision->isDefaultRevision(FALSE);
$forward_revision->name = 'forward revision - en';
$forward_revision->save();
$pending_revision->name = 'pending revision - en';
$pending_revision->save();
$forward_revision_translation = $forward_revision->getTranslation('de');
$forward_revision_translation->name = 'forward revision - de';
$forward_revision_translation->save();
$pending_revision_translation = $pending_revision->getTranslation('de');
$pending_revision_translation->name = 'pending revision - de';
$pending_revision_translation->save();
// Check that the entity revision is upcasted in the correct language.
$revision_url = 'entity_test_mulrev/' . $entity->id() . '/revision/' . $forward_revision->getRevisionId() . '/view';
$revision_url = 'entity_test_mulrev/' . $entity->id() . '/revision/' . $pending_revision->getRevisionId() . '/view';
$this->drupalGet($revision_url);
$this->assertText('forward revision - en');
$this->assertNoText('forward revision - de');
$this->assertText('pending revision - en');
$this->assertNoText('pending revision - de');
$this->drupalGet('de/' . $revision_url);
$this->assertText('forward revision - de');
$this->assertNoText('forward revision - en');
$this->assertText('pending revision - de');
$this->assertNoText('pending revision - en');
}
/**
* Tests manual revert of the revision ID value.
*
* @covers \Drupal\Core\Entity\ContentEntityBase::getRevisionId
* @covers \Drupal\Core\Entity\ContentEntityBase::getLoadedRevisionId
* @covers \Drupal\Core\Entity\ContentEntityBase::setNewRevision
* @covers \Drupal\Core\Entity\ContentEntityBase::isNewRevision
*/
public function testNewRevisionRevert() {
$entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']);
$entity->save();
// Check that revision ID field is reset while the loaded revision ID is
// preserved when flagging a new revision.
$revision_id = $entity->getRevisionId();
$entity->setNewRevision();
$this->assertNull($entity->getRevisionId());
$this->assertEquals($revision_id, $entity->getLoadedRevisionId());
$this->assertTrue($entity->isNewRevision());
// Check that after manually restoring the original revision ID, the entity
// is stored without creating a new revision.
$key = $entity->getEntityType()->getKey('revision');
$entity->set($key, $revision_id);
$entity->save();
$this->assertEquals($revision_id, $entity->getRevisionId());
$this->assertEquals($revision_id, $entity->getLoadedRevisionId());
// Check that manually restoring the original revision ID causes the "new
// revision" state to be reverted.
$entity->setNewRevision();
$this->assertNull($entity->getRevisionId());
$this->assertEquals($revision_id, $entity->getLoadedRevisionId());
$this->assertTrue($entity->isNewRevision());
$entity->set($key, $revision_id);
$this->assertFalse($entity->isNewRevision());
$this->assertEquals($revision_id, $entity->getRevisionId());
$this->assertEquals($revision_id, $entity->getLoadedRevisionId());
// Check that flagging a new revision again works correctly.
$entity->setNewRevision();
$this->assertNull($entity->getRevisionId());
$this->assertEquals($revision_id, $entity->getLoadedRevisionId());
$this->assertTrue($entity->isNewRevision());
// Check that calling setNewRevision() on a new entity without a revision ID
// and then with a revision ID does not unset the revision ID.
$entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']);
$entity->set('revision_id', NULL);
$entity->set('revision_id', 5);
$this->assertTrue($entity->isNewRevision());
$entity->setNewRevision();
$this->assertEquals(5, $entity->get('revision_id')->value);
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Drupal\Tests\system\Functional\Entity;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests entity translation form.
*
* @group Entity
*/
class EntityTranslationFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['entity_test', 'language', 'node'];
protected $langcodes;
protected function setUp() {
parent::setUp();
// Enable translations for the test entity type.
\Drupal::state()->set('entity_test.translation', TRUE);
// Create test languages.
$this->langcodes = [];
for ($i = 0; $i < 2; ++$i) {
$language = ConfigurableLanguage::create([
'id' => 'l' . $i,
'label' => $this->randomString(),
]);
$this->langcodes[$i] = $language->id();
$language->save();
}
}
/**
* Tests entity form language.
*/
public function testEntityFormLanguage() {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer content types']);
$this->drupalLogin($web_user);
// Create a node with language LanguageInterface::LANGCODE_NOT_SPECIFIED.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$form_langcode = \Drupal::state()->get('entity_test.form_langcode');
$this->drupalPostForm(NULL, $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertTrue($node->language()->getId() == $form_langcode, 'Form language is the same as the entity language.');
// Edit the node and test the form language.
$this->drupalGet($this->langcodes[0] . '/node/' . $node->id() . '/edit');
$form_langcode = \Drupal::state()->get('entity_test.form_langcode');
$this->assertTrue($node->language()->getId() == $form_langcode, 'Form language is the same as the entity language.');
// Explicitly set form langcode.
$langcode = $this->langcodes[0];
$form_state_additions['langcode'] = $langcode;
\Drupal::service('entity.form_builder')->getForm($node, 'default', $form_state_additions);
$form_langcode = \Drupal::state()->get('entity_test.form_langcode');
$this->assertTrue($langcode == $form_langcode, 'Form language is the same as the language parameter.');
// Enable language selector.
$this->drupalGet('admin/structure/types/manage/page');
$edit = ['language_configuration[language_alterable]' => TRUE, 'language_configuration[langcode]' => LanguageInterface::LANGCODE_NOT_SPECIFIED];
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', ['%type' => 'Basic page']), 'Basic page content type has been updated.');
// Create a node with language.
$edit = [];
$langcode = $this->langcodes[0];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$edit['langcode[0][value]'] = $langcode;
$this->drupalPostForm('node/add/page', $edit, t('Save'));
$this->assertText(t('Basic page @title has been created.', ['@title' => $edit['title[0][value]']]), 'Basic page created.');
// Verify that the creation message contains a link to a node.
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'node/']);
$this->assert(isset($view_link), 'The message area contains a link to a node');
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertTrue($node, 'Node found in database.');
// Make body translatable.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$this->assertTrue($field_storage->isTranslatable(), 'Field body is translatable.');
// Create a body translation and check the form language.
$langcode2 = $this->langcodes[1];
$translation = $node->addTranslation($langcode2);
$translation->title->value = $this->randomString();
$translation->body->value = $this->randomMachineName(16);
$translation->setOwnerId($web_user->id());
$node->save();
$this->drupalGet($langcode2 . '/node/' . $node->id() . '/edit');
$form_langcode = \Drupal::state()->get('entity_test.form_langcode');
$this->assertTrue($langcode2 == $form_langcode, "Node edit form language is $langcode2.");
}
}

View file

@ -42,7 +42,7 @@ class EntityViewControllerTest extends BrowserTestBase {
* Tests EntityViewController.
*/
public function testEntityViewController() {
$get_label_markup = function($label) {
$get_label_markup = function ($label) {
return '<h1 class="page-title">
<div class="field field--name-name field--type-string field--label-hidden field__item">' . $label . '</div>
</h1>';

View file

@ -0,0 +1,149 @@
<?php
namespace Drupal\Tests\system\Functional\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
/**
* Provides helper methods for Entity cache tags tests; for entities with URIs.
*/
abstract class EntityWithUriCacheTagsTestBase extends EntityCacheTagsTestBase {
/**
* Tests cache tags presence and invalidation of the entity at its URI.
*
* Tests the following cache tags:
* - "<entity type>_view"
* - "<entity_type>:<entity ID>"
*/
public function testEntityUri() {
$entity_url = $this->entity->urlInfo();
$entity_type = $this->entity->getEntityTypeId();
// Selects the view mode that will be used.
$view_mode = $this->selectViewMode($entity_type);
// The default cache contexts for rendered entities.
$entity_cache_contexts = $this->getDefaultCacheContexts();
// Generate the standardized entity cache tags.
$cache_tag = $this->entity->getCacheTags();
$view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags();
$render_cache_tag = 'rendered';
$this->pass("Test entity.", 'Debug');
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($entity_url, 'HIT');
// Also verify the existence of an entity render cache entry, if this entity
// type supports render caching.
if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) {
$cache_keys = ['entity_view', $entity_type, $this->entity->id(), $view_mode];
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
$redirected_cid = NULL;
$additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->entity);
if (count($additional_cache_contexts)) {
$redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts));
}
$expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag);
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity));
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, [$render_cache_tag]);
$this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid);
}
// Verify that after modifying the entity, there is a cache miss.
$this->pass("Test modification of entity.", 'Debug');
$this->entity->save();
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
// Verify that after modifying the entity's display, there is a cache miss.
$this->pass("Test modification of entity's '$view_mode' display.", 'Debug');
$entity_display = entity_get_display($entity_type, $this->entity->bundle(), $view_mode);
$entity_display->save();
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) {
// Verify that after modifying the corresponding bundle entity, there is a
// cache miss.
$this->pass("Test modification of entity's bundle entity.", 'Debug');
$bundle_entity = $this->container->get('entity_type.manager')
->getStorage($bundle_entity_type_id)
->load($this->entity->bundle());
$bundle_entity->save();
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
}
if ($this->entity->getEntityType()->get('field_ui_base_route')) {
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
$this->pass("Test modification of entity's configurable field.", 'Debug');
$field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field';
$field_storage = FieldStorageConfig::load($field_storage_name);
$field_storage->save();
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
$this->pass("Test modification of entity's configurable field.", 'Debug');
$field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field';
$field = FieldConfig::load($field_name);
$field->save();
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
}
// Verify that after invalidating the entity's cache tag directly, there is
// a cache miss.
$this->pass("Test invalidation of entity's cache tag.", 'Debug');
Cache::invalidateTags($this->entity->getCacheTagsToInvalidate());
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
// Verify that after invalidating the generic entity type's view cache tag
// directly, there is a cache miss.
$this->pass("Test invalidation of entity's 'view' cache tag.", 'Debug');
Cache::invalidateTags($view_cache_tag);
$this->verifyPageCache($entity_url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($entity_url, 'HIT');
// Verify that after deleting the entity, there is a cache miss.
$this->pass('Test deletion of entity.', 'Debug');
$this->entity->delete();
$this->verifyPageCache($entity_url, 'MISS');
$this->assertResponse(404);
}
/**
* Gets the default cache contexts for rendered entities.
*
* @return array
* The default cache contexts for rendered entities.
*/
protected function getDefaultCacheContexts() {
return ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
}
}

View file

@ -0,0 +1,299 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Traits;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity_test\FieldStorageDefinition;
/**
* Provides some test methods used to update existing entity definitions.
*/
trait EntityDefinitionTestTrait {
/**
* Enables a new entity type definition.
*/
protected function enableNewEntityType() {
$this->state->set('entity_test_new', TRUE);
$this->entityManager->clearCachedDefinitions();
$this->entityDefinitionUpdateManager->applyUpdates();
}
/**
* Resets the entity type definition.
*/
protected function resetEntityType() {
$this->state->set('entity_test_update.entity_type', NULL);
$this->entityManager->clearCachedDefinitions();
$this->entityDefinitionUpdateManager->applyUpdates();
}
/**
* Updates the 'entity_test_update' entity type to revisionable.
*/
protected function updateEntityTypeToRevisionable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$keys = $entity_type->getKeys();
$keys['revision'] = 'revision_id';
$entity_type->set('entity_keys', $keys);
$entity_type->set('revision_table', 'entity_test_update_revision');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Updates the 'entity_test_update' entity type not revisionable.
*/
protected function updateEntityTypeToNotRevisionable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$keys = $entity_type->getKeys();
unset($keys['revision']);
$entity_type->set('entity_keys', $keys);
$entity_type->set('revision_table', NULL);
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Updates the 'entity_test_update' entity type to translatable.
*/
protected function updateEntityTypeToTranslatable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('translatable', TRUE);
$entity_type->set('data_table', 'entity_test_update_data');
if ($entity_type->isRevisionable()) {
$entity_type->set('revision_data_table', 'entity_test_update_revision_data');
}
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Updates the 'entity_test_update' entity type to not translatable.
*/
protected function updateEntityTypeToNotTranslatable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('translatable', FALSE);
$entity_type->set('data_table', NULL);
if ($entity_type->isRevisionable()) {
$entity_type->set('revision_data_table', NULL);
}
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Updates the 'entity_test_update' entity type to revisionable and
* translatable.
*/
protected function updateEntityTypeToRevisionableAndTranslatable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$keys = $entity_type->getKeys();
$keys['revision'] = 'revision_id';
$entity_type->set('entity_keys', $keys);
$entity_type->set('translatable', TRUE);
$entity_type->set('data_table', 'entity_test_update_data');
$entity_type->set('revision_table', 'entity_test_update_revision');
$entity_type->set('revision_data_table', 'entity_test_update_revision_data');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Adds a new base field to the 'entity_test_update' entity type.
*
* @param string $type
* (optional) The field type for the new field. Defaults to 'string'.
* @param string $entity_type_id
* (optional) The entity type ID the base field should be attached to.
* Defaults to 'entity_test_update'.
* @param bool $is_revisionable
* (optional) If the base field should be revisionable or not. Defaults to
* FALSE.
*/
protected function addBaseField($type = 'string', $entity_type_id = 'entity_test_update', $is_revisionable = FALSE) {
$definitions['new_base_field'] = BaseFieldDefinition::create($type)
->setName('new_base_field')
->setRevisionable($is_revisionable)
->setLabel(t('A new base field'));
$this->state->set($entity_type_id . '.additional_base_field_definitions', $definitions);
}
/**
* Adds a long-named base field to the 'entity_test_update' entity type.
*/
protected function addLongNameBaseField() {
$key = 'entity_test_update.additional_base_field_definitions';
$definitions = $this->state->get($key, []);
$definitions['new_long_named_entity_reference_base_field'] = BaseFieldDefinition::create('entity_reference')
->setName('new_long_named_entity_reference_base_field')
->setLabel(t('A new long-named base field'))
->setSetting('target_type', 'user')
->setSetting('handler', 'default');
$this->state->set($key, $definitions);
}
/**
* Adds a new revisionable base field to the 'entity_test_update' entity type.
*
* @param string $type
* (optional) The field type for the new field. Defaults to 'string'.
*/
protected function addRevisionableBaseField($type = 'string') {
$definitions['new_base_field'] = BaseFieldDefinition::create($type)
->setName('new_base_field')
->setLabel(t('A new revisionable base field'))
->setRevisionable(TRUE);
$this->state->set('entity_test_update.additional_base_field_definitions', $definitions);
}
/**
* Modifies the new base field from 'string' to 'text'.
*/
protected function modifyBaseField() {
$this->addBaseField('text');
}
/**
* Promotes a field to an entity key.
*/
protected function makeBaseFieldEntityKey() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_keys = $entity_type->getKeys();
$entity_keys['new_base_field'] = 'new_base_field';
$entity_type->set('entity_keys', $entity_keys);
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Removes the new base field from the 'entity_test_update' entity type.
*
* @param string $entity_type_id
* (optional) The entity type ID the base field should be attached to.
*/
protected function removeBaseField($entity_type_id = 'entity_test_update') {
$this->state->delete($entity_type_id . '.additional_base_field_definitions');
}
/**
* Adds a single-field index to the base field.
*/
protected function addBaseFieldIndex() {
$this->state->set('entity_test_update.additional_field_index.entity_test_update.new_base_field', TRUE);
}
/**
* Removes the index added in addBaseFieldIndex().
*/
protected function removeBaseFieldIndex() {
$this->state->delete('entity_test_update.additional_field_index.entity_test_update.new_base_field');
}
/**
* Adds a new bundle field to the 'entity_test_update' entity type.
*
* @param string $type
* (optional) The field type for the new field. Defaults to 'string'.
*/
protected function addBundleField($type = 'string') {
$definitions['new_bundle_field'] = FieldStorageDefinition::create($type)
->setName('new_bundle_field')
->setLabel(t('A new bundle field'))
->setTargetEntityTypeId('entity_test_update');
$this->state->set('entity_test_update.additional_field_storage_definitions', $definitions);
$this->state->set('entity_test_update.additional_bundle_field_definitions.test_bundle', $definitions);
}
/**
* Modifies the new bundle field from 'string' to 'text'.
*/
protected function modifyBundleField() {
$this->addBundleField('text');
}
/**
* Removes the new bundle field from the 'entity_test_update' entity type.
*/
protected function removeBundleField() {
$this->state->delete('entity_test_update.additional_field_storage_definitions');
$this->state->delete('entity_test_update.additional_bundle_field_definitions.test_bundle');
}
/**
* Adds an index to the 'entity_test_update' entity type's base table.
*
* @see \Drupal\entity_test\EntityTestStorageSchema::getEntitySchema()
*/
protected function addEntityIndex() {
$indexes = [
'entity_test_update__new_index' => ['name', 'test_single_property'],
];
$this->state->set('entity_test_update.additional_entity_indexes', $indexes);
}
/**
* Removes the index added in addEntityIndex().
*/
protected function removeEntityIndex() {
$this->state->delete('entity_test_update.additional_entity_indexes');
}
/**
* Renames the base table to 'entity_test_update_new'.
*/
protected function renameBaseTable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('base_table', 'entity_test_update_new');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Renames the data table to 'entity_test_update_data_new'.
*/
protected function renameDataTable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('data_table', 'entity_test_update_data_new');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Renames the revision table to 'entity_test_update_revision_new'.
*/
protected function renameRevisionBaseTable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('revision_table', 'entity_test_update_revision_new');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Renames the revision data table to 'entity_test_update_revision_data_new'.
*/
protected function renameRevisionDataTable() {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->set('revision_data_table', 'entity_test_update_revision_data_new');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
/**
* Removes the entity type.
*/
protected function deleteEntityType() {
$this->state->set('entity_test_update.entity_type', 'null');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
/**
* Runs LangcodeToAsciiUpdateTest with a dump filled with content.
*
* @group Entity
* @group legacy
*/
class LangcodeToAsciiUpdateFilledTest extends LangcodeToAsciiUpdateTest {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../fixtures/update/drupal-8.filled.standard.php.gz',
];
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests that the entity langcode fields have been updated to varchar_ascii.
*
* @group Entity
* @group legacy
*/
class LangcodeToAsciiUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../fixtures/update/drupal-8.bare.standard.php.gz',
];
}
/**
* Tests that the column collation has been updated on MySQL.
*/
public function testLangcodeColumnCollation() {
// Only testable on MySQL.
// @see https://www.drupal.org/node/301038
if (Database::getConnection()->databaseType() !== 'mysql') {
$this->pass('This test can only run on MySQL');
return;
}
// Check a few different tables.
$tables = [
'node_field_data' => ['langcode'],
'users_field_data' => ['langcode', 'preferred_langcode', 'preferred_admin_langcode'],
];
foreach ($tables as $table => $columns) {
foreach ($columns as $column) {
// Depending on MYSQL versions you get different collations.
$this->assertContains($this->getColumnCollation($table, $column), ['utf8mb4_0900_ai_ci', 'utf8mb4_general_ci'], 'Found correct starting collation for ' . $table . '.' . $column);
}
}
// Apply updates.
$this->runUpdates();
foreach ($tables as $table => $columns) {
foreach ($columns as $column) {
$this->assertEqual('ascii_general_ci', $this->getColumnCollation($table, $column), 'Found correct updated collation for ' . $table . '.' . $column);
}
}
}
/**
* Determine the column collation.
*
* @param string $table
* The table name.
* @param string $column
* The column name.
*/
protected function getColumnCollation($table, $column) {
$query = Database::getConnection()->query("SHOW FULL COLUMNS FROM {" . $table . "}");
while ($row = $query->fetchAssoc()) {
if ($row['Field'] === $column) {
return $row['Collation'];
}
}
$this->fail('No collation found for ' . $table . '.' . $column);
}
}

View file

@ -0,0 +1,236 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\views\Entity\View;
/**
* Tests the upgrade path for moving the revision metadata fields.
*
* @group Update
* @group legacy
*/
class MoveRevisionMetadataFieldsUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../tests/fixtures/update/drupal-8.2.0.bare.standard_with_entity_test_revlog_enabled.php.gz',
__DIR__ . '/../../../../../tests/fixtures/update/drupal-8.entity-data-revision-metadata-fields-2248983.php',
__DIR__ . '/../../../../../tests/fixtures/update/drupal-8.views-revision-metadata-fields-2248983.php',
];
}
/**
* Tests that the revision metadata fields are moved correctly.
*/
public function testSystemUpdate8400() {
$this->runUpdates();
foreach (['entity_test_revlog', 'entity_test_mul_revlog'] as $entity_type_id) {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$entity_type = $storage->getEntityType();
$revision_metadata_field_names = $entity_type->getRevisionMetadataKeys();
$database_schema = \Drupal::database()->schema();
// Test that the revision metadata fields are present only in the
// revision table.
foreach ($revision_metadata_field_names as $revision_metadata_field_name) {
if ($entity_type->isTranslatable()) {
$this->assertFalse($database_schema->fieldExists($entity_type->getDataTable(), $revision_metadata_field_name));
$this->assertFalse($database_schema->fieldExists($entity_type->getRevisionDataTable(), $revision_metadata_field_name));
}
else {
$this->assertFalse($database_schema->fieldExists($entity_type->getBaseTable(), $revision_metadata_field_name));
}
$this->assertTrue($database_schema->fieldExists($entity_type->getRevisionTable(), $revision_metadata_field_name));
}
// Test that the revision metadata values have been transferred correctly
// and that the moved fields are accessible.
/** @var \Drupal\Core\Entity\RevisionLogInterface $entity_rev_first */
$entity_rev_first = $storage->loadRevision(1);
$this->assertEqual($entity_rev_first->getRevisionUserId(), '1');
$this->assertEqual($entity_rev_first->getRevisionLogMessage(), 'first revision');
$this->assertEqual($entity_rev_first->getRevisionCreationTime(), '1476268517');
/** @var \Drupal\Core\Entity\RevisionLogInterface $entity_rev_second */
$entity_rev_second = $storage->loadRevision(2);
$this->assertEqual($entity_rev_second->getRevisionUserId(), '1');
$this->assertEqual($entity_rev_second->getRevisionLogMessage(), 'second revision');
$this->assertEqual($entity_rev_second->getRevisionCreationTime(), '1476268518');
// Test that the views using revision metadata fields are updated
// properly.
$view = View::load($entity_type_id . '_for_2248983');
$displays = $view->get('display');
foreach ($displays as $display => $display_data) {
foreach ($display_data['display_options']['fields'] as $property_data) {
if (in_array($property_data['field'], $revision_metadata_field_names)) {
$this->assertEqual($property_data['table'], $entity_type->getRevisionTable());
}
}
}
}
}
/**
* Tests the addition of required revision metadata keys.
*
* This test ensures that already cached entity instances will only return the
* required revision metadata keys they have been cached with and only new
* instances will return all the new required revision metadata keys.
*/
public function testAddingRequiredRevisionMetadataKeys() {
// Ensure that cached entity types without required revision metadata keys
// will not return any of the newly added required revision metadata keys.
// Contains no revision metadata keys and the property holding the required
// metadata keys is empty, the entity type id is "entity_test_mul_revlog".
$cached_with_no_metadata_keys = 'Tzo4MjoiRHJ1cGFsXFRlc3RzXHN5c3RlbVxGdW5jdGlvbmFsXEVudGl0eVxVcGRhdGVcVGVzdFJldmlzaW9uTWV0YWRhdGFCY0xheWVyRW50aXR5VHlwZSI6Mzk6e3M6MjU6IgAqAHJldmlzaW9uX21ldGFkYXRhX2tleXMiO2E6MDp7fXM6MzE6IgAqAHJlcXVpcmVkUmV2aXNpb25NZXRhZGF0YUtleXMiO2E6MDp7fXM6MTU6IgAqAHN0YXRpY19jYWNoZSI7YjoxO3M6MTU6IgAqAHJlbmRlcl9jYWNoZSI7YjoxO3M6MTk6IgAqAHBlcnNpc3RlbnRfY2FjaGUiO2I6MTtzOjE0OiIAKgBlbnRpdHlfa2V5cyI7YTo1OntzOjg6InJldmlzaW9uIjtzOjA6IiI7czo2OiJidW5kbGUiO3M6MDoiIjtzOjg6Imxhbmdjb2RlIjtzOjA6IiI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoyOToicmV2aXNpb25fdHJhbnNsYXRpb25fYWZmZWN0ZWQiO3M6Mjk6InJldmlzaW9uX3RyYW5zbGF0aW9uX2FmZmVjdGVkIjt9czo1OiIAKgBpZCI7czoyMjoiZW50aXR5X3Rlc3RfbXVsX3JldmxvZyI7czoxNjoiACoAb3JpZ2luYWxDbGFzcyI7TjtzOjExOiIAKgBoYW5kbGVycyI7YTozOntzOjY6ImFjY2VzcyI7czo0NToiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eUFjY2Vzc0NvbnRyb2xIYW5kbGVyIjtzOjc6InN0b3JhZ2UiO3M6NDY6IkRydXBhbFxDb3JlXEVudGl0eVxTcWxcU3FsQ29udGVudEVudGl0eVN0b3JhZ2UiO3M6MTI6InZpZXdfYnVpbGRlciI7czozNjoiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eVZpZXdCdWlsZGVyIjt9czoxOToiACoAYWRtaW5fcGVybWlzc2lvbiI7TjtzOjI1OiIAKgBwZXJtaXNzaW9uX2dyYW51bGFyaXR5IjtzOjExOiJlbnRpdHlfdHlwZSI7czo4OiIAKgBsaW5rcyI7YTowOnt9czoxNzoiACoAbGFiZWxfY2FsbGJhY2siO047czoyMToiACoAYnVuZGxlX2VudGl0eV90eXBlIjtOO3M6MTI6IgAqAGJ1bmRsZV9vZiI7TjtzOjE1OiIAKgBidW5kbGVfbGFiZWwiO047czoxMzoiACoAYmFzZV90YWJsZSI7TjtzOjIyOiIAKgByZXZpc2lvbl9kYXRhX3RhYmxlIjtOO3M6MTc6IgAqAHJldmlzaW9uX3RhYmxlIjtOO3M6MTM6IgAqAGRhdGFfdGFibGUiO047czoxNToiACoAdHJhbnNsYXRhYmxlIjtiOjA7czoxOToiACoAc2hvd19yZXZpc2lvbl91aSI7YjowO3M6ODoiACoAbGFiZWwiO3M6MDoiIjtzOjE5OiIAKgBsYWJlbF9jb2xsZWN0aW9uIjtzOjA6IiI7czoxNzoiACoAbGFiZWxfc2luZ3VsYXIiO3M6MDoiIjtzOjE1OiIAKgBsYWJlbF9wbHVyYWwiO3M6MDoiIjtzOjE0OiIAKgBsYWJlbF9jb3VudCI7YTowOnt9czoxNToiACoAdXJpX2NhbGxiYWNrIjtOO3M6ODoiACoAZ3JvdXAiO047czoxNDoiACoAZ3JvdXBfbGFiZWwiO047czoyMjoiACoAZmllbGRfdWlfYmFzZV9yb3V0ZSI7TjtzOjI2OiIAKgBjb21tb25fcmVmZXJlbmNlX3RhcmdldCI7YjowO3M6MjI6IgAqAGxpc3RfY2FjaGVfY29udGV4dHMiO2E6MDp7fXM6MTg6IgAqAGxpc3RfY2FjaGVfdGFncyI7YToxOntpOjA7czo5OiJ0ZXN0X2xpc3QiO31zOjE0OiIAKgBjb25zdHJhaW50cyI7YTowOnt9czoxMzoiACoAYWRkaXRpb25hbCI7YTowOnt9czo4OiIAKgBjbGFzcyI7TjtzOjExOiIAKgBwcm92aWRlciI7TjtzOjIwOiIAKgBzdHJpbmdUcmFuc2xhdGlvbiI7Tjt9';
/** @var \Drupal\Tests\system\Functional\Entity\Update\TestRevisionMetadataBcLayerEntityType $entity_type */
$entity_type = unserialize(base64_decode($cached_with_no_metadata_keys));
$required_revision_metadata_keys_no_bc = [];
$this->assertEquals($required_revision_metadata_keys_no_bc, $entity_type->getRevisionMetadataKeys(FALSE));
$required_revision_metadata_keys_with_bc = $required_revision_metadata_keys_no_bc + [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$this->assertEquals($required_revision_metadata_keys_with_bc, $entity_type->getRevisionMetadataKeys(TRUE));
// Ensure that cached entity types with only one required revision metadata
// key will return only that one after a second required revision metadata
// key has been added.
// Contains one revision metadata key - revision_default which is also
// contained in the property holding the required revision metadata keys,
// the entity type id is "entity_test_mul_revlog".
$cached_with_metadata_key_revision_default = 'Tzo4MjoiRHJ1cGFsXFRlc3RzXHN5c3RlbVxGdW5jdGlvbmFsXEVudGl0eVxVcGRhdGVcVGVzdFJldmlzaW9uTWV0YWRhdGFCY0xheWVyRW50aXR5VHlwZSI6Mzk6e3M6MjU6IgAqAHJldmlzaW9uX21ldGFkYXRhX2tleXMiO2E6MTp7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7fXM6MzE6IgAqAHJlcXVpcmVkUmV2aXNpb25NZXRhZGF0YUtleXMiO2E6MTp7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7fXM6MTU6IgAqAHN0YXRpY19jYWNoZSI7YjoxO3M6MTU6IgAqAHJlbmRlcl9jYWNoZSI7YjoxO3M6MTk6IgAqAHBlcnNpc3RlbnRfY2FjaGUiO2I6MTtzOjE0OiIAKgBlbnRpdHlfa2V5cyI7YTo1OntzOjg6InJldmlzaW9uIjtzOjA6IiI7czo2OiJidW5kbGUiO3M6MDoiIjtzOjg6Imxhbmdjb2RlIjtzOjA6IiI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoyOToicmV2aXNpb25fdHJhbnNsYXRpb25fYWZmZWN0ZWQiO3M6Mjk6InJldmlzaW9uX3RyYW5zbGF0aW9uX2FmZmVjdGVkIjt9czo1OiIAKgBpZCI7czoyMjoiZW50aXR5X3Rlc3RfbXVsX3JldmxvZyI7czoxNjoiACoAb3JpZ2luYWxDbGFzcyI7TjtzOjExOiIAKgBoYW5kbGVycyI7YTozOntzOjY6ImFjY2VzcyI7czo0NToiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eUFjY2Vzc0NvbnRyb2xIYW5kbGVyIjtzOjc6InN0b3JhZ2UiO3M6NDY6IkRydXBhbFxDb3JlXEVudGl0eVxTcWxcU3FsQ29udGVudEVudGl0eVN0b3JhZ2UiO3M6MTI6InZpZXdfYnVpbGRlciI7czozNjoiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eVZpZXdCdWlsZGVyIjt9czoxOToiACoAYWRtaW5fcGVybWlzc2lvbiI7TjtzOjI1OiIAKgBwZXJtaXNzaW9uX2dyYW51bGFyaXR5IjtzOjExOiJlbnRpdHlfdHlwZSI7czo4OiIAKgBsaW5rcyI7YTowOnt9czoxNzoiACoAbGFiZWxfY2FsbGJhY2siO047czoyMToiACoAYnVuZGxlX2VudGl0eV90eXBlIjtOO3M6MTI6IgAqAGJ1bmRsZV9vZiI7TjtzOjE1OiIAKgBidW5kbGVfbGFiZWwiO047czoxMzoiACoAYmFzZV90YWJsZSI7TjtzOjIyOiIAKgByZXZpc2lvbl9kYXRhX3RhYmxlIjtOO3M6MTc6IgAqAHJldmlzaW9uX3RhYmxlIjtOO3M6MTM6IgAqAGRhdGFfdGFibGUiO047czoxNToiACoAdHJhbnNsYXRhYmxlIjtiOjA7czoxOToiACoAc2hvd19yZXZpc2lvbl91aSI7YjowO3M6ODoiACoAbGFiZWwiO3M6MDoiIjtzOjE5OiIAKgBsYWJlbF9jb2xsZWN0aW9uIjtzOjA6IiI7czoxNzoiACoAbGFiZWxfc2luZ3VsYXIiO3M6MDoiIjtzOjE1OiIAKgBsYWJlbF9wbHVyYWwiO3M6MDoiIjtzOjE0OiIAKgBsYWJlbF9jb3VudCI7YTowOnt9czoxNToiACoAdXJpX2NhbGxiYWNrIjtOO3M6ODoiACoAZ3JvdXAiO047czoxNDoiACoAZ3JvdXBfbGFiZWwiO047czoyMjoiACoAZmllbGRfdWlfYmFzZV9yb3V0ZSI7TjtzOjI2OiIAKgBjb21tb25fcmVmZXJlbmNlX3RhcmdldCI7YjowO3M6MjI6IgAqAGxpc3RfY2FjaGVfY29udGV4dHMiO2E6MDp7fXM6MTg6IgAqAGxpc3RfY2FjaGVfdGFncyI7YToxOntpOjA7czo5OiJ0ZXN0X2xpc3QiO31zOjE0OiIAKgBjb25zdHJhaW50cyI7YTowOnt9czoxMzoiACoAYWRkaXRpb25hbCI7YTowOnt9czo4OiIAKgBjbGFzcyI7TjtzOjExOiIAKgBwcm92aWRlciI7TjtzOjIwOiIAKgBzdHJpbmdUcmFuc2xhdGlvbiI7Tjt9';
$entity_type = unserialize(base64_decode($cached_with_metadata_key_revision_default));
$required_revision_metadata_keys_no_bc = [
'revision_default' => 'revision_default',
];
$this->assertEquals($required_revision_metadata_keys_no_bc, $entity_type->getRevisionMetadataKeys(FALSE));
$required_revision_metadata_keys_with_bc = $required_revision_metadata_keys_no_bc + [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$this->assertEquals($required_revision_metadata_keys_with_bc, $entity_type->getRevisionMetadataKeys(TRUE));
// Ensure that newly instantiated entity types will return the two required
// revision metadata keys.
$entity_type = new TestRevisionMetadataBcLayerEntityType(['id' => 'test']);
$required_revision_metadata_keys = [
'revision_default' => 'revision_default',
'second_required_key' => 'second_required_key',
];
$this->assertEquals($required_revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE));
// Load an entity type from the cache with no revision metadata keys in the
// annotation.
$entity_last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
$entity_type = $entity_last_installed_schema_repository->getLastInstalledDefinition('entity_test_mul_revlog');
$revision_metadata_keys = [];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE));
$revision_metadata_keys = [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE));
// Load an entity type without using the cache with no revision metadata
// keys in the annotation.
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type_manager->useCaches(FALSE);
$entity_type = $entity_type_manager->getDefinition('entity_test_mul_revlog');
$revision_metadata_keys = [
'revision_default' => 'revision_default',
];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE));
$revision_metadata_keys = [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
'revision_default' => 'revision_default',
];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE));
// Ensure that the BC layer will not be triggered if one of the required
// revision metadata keys is defined in the annotation.
$definition = [
'id' => 'entity_test_mul_revlog',
'revision_metadata_keys' => [
'revision_default' => 'revision_default',
],
];
$entity_type = new ContentEntityType($definition);
$revision_metadata_keys = [
'revision_default' => 'revision_default',
];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE));
// Ensure that the BC layer will be triggered if no revision metadata keys
// have been defined in the annotation.
$definition = [
'id' => 'entity_test_mul_revlog',
];
$entity_type = new ContentEntityType($definition);
$revision_metadata_keys = [
'revision_default' => 'revision_default',
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE));
}
/**
* Tests that the revision metadata key BC layer was updated correctly.
*/
public function testSystemUpdate8501() {
$this->runUpdates();
/** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $definition_update_manager */
$definition_update_manager = $this->container->get('entity.definition_update_manager');
foreach (['block_content', 'node'] as $entity_type_id) {
$installed_entity_type = $definition_update_manager->getEntityType($entity_type_id);
$revision_metadata_keys = $installed_entity_type->get('revision_metadata_keys');
$this->assertTrue(isset($revision_metadata_keys['revision_default']));
$required_revision_metadata_keys = $installed_entity_type->get('requiredRevisionMetadataKeys');
$this->assertTrue(isset($required_revision_metadata_keys['revision_default']));
}
}
}
/**
* Test entity type class for adding new required revision metadata keys.
*/
class TestRevisionMetadataBcLayerEntityType extends ContentEntityType {
/**
* {@inheritdoc}
*/
public function __construct($definition) {
// Only new instances should provide the required revision metadata keys.
// The cached instances should return only what already has been stored
// under the property $revision_metadata_keys. The BC layer in
// ::getRevisionMetadataKeys() has to detect if the revision metadata keys
// have been provided by the entity type annotation, therefore we add keys
// to the property $requiredRevisionMetadataKeys only if those keys aren't
// set in the entity type annotation.
if (!isset($definition['revision_metadata_keys']['second_required_key'])) {
$this->requiredRevisionMetadataKeys['second_required_key'] = 'second_required_key';
}
parent::__construct($definition);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
/**
* Tests converting a non-translatable entity type with data to revisionable.
*
* @group Entity
* @group Update
* @group legacy
*/
class SqlContentEntityStorageSchemaConverterNonTranslatableTest extends SqlContentEntityStorageSchemaConverterTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update.php.gz',
__DIR__ . '/../../../../fixtures/update/drupal-8.entity-test-schema-converter-enabled.php',
];
}
}

View file

@ -0,0 +1,170 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
/**
* Defines a class for testing the conversion of entity types to revisionable.
*/
abstract class SqlContentEntityStorageSchemaConverterTestBase extends UpdatePathTestBase {
use EntityDefinitionTestTrait;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $entityDefinitionUpdateManager;
/**
* The last installed schema repository service.
*
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
*/
protected $lastInstalledSchemaRepository;
/**
* The key-value collection for tracking installed storage schema.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $installedStorageSchema;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->entityManager = \Drupal::entityManager();
$this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
$this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository');
$this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
$this->state = \Drupal::state();
}
/**
* Tests the conversion of an entity type to revisionable.
*/
public function testMakeRevisionable() {
// Check that entity type is not revisionable prior to running the update
// process.
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$this->assertFalse($entity_test_update->isRevisionable());
$translatable = $entity_test_update->isTranslatable();
// Make the entity type revisionable and run the updates.
if ($translatable) {
$this->updateEntityTypeToRevisionableAndTranslatable();
}
else {
$this->updateEntityTypeToRevisionable();
}
$this->runUpdates();
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$field_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
$this->assertTrue($entity_test_update->isRevisionable());
$this->assertEquals($translatable, isset($field_storage_definitions['revision_translation_affected']));
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
$this->assertEqual(count($storage->loadMultiple()), 102, 'All test entities were found.');
// Check that each field value was copied correctly to the revision tables.
for ($i = 1; $i <= 102; $i++) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $storage->loadRevision($i);
$this->assertEqual($i, $revision->id());
$this->assertEqual($i, $revision->getRevisionId());
$this->assertEqual($i . ' - test single property', $revision->test_single_property->value);
$this->assertEqual($i . ' - test multiple properties - value1', $revision->test_multiple_properties->value1);
$this->assertEqual($i . ' - test multiple properties - value2', $revision->test_multiple_properties->value2);
$this->assertEqual($i . ' - test single property multiple values 0', $revision->test_single_property_multiple_values->value);
$this->assertEqual($i . ' - test single property multiple values 1', $revision->test_single_property_multiple_values[1]->value);
$this->assertEqual($i . ' - test multiple properties multiple values - value1 0', $revision->test_multiple_properties_multiple_values[0]->value1);
$this->assertEqual($i . ' - test multiple properties multiple values - value2 0', $revision->test_multiple_properties_multiple_values[0]->value2);
$this->assertEqual($i . ' - test multiple properties multiple values - value1 1', $revision->test_multiple_properties_multiple_values[1]->value1);
$this->assertEqual($i . ' - test multiple properties multiple values - value2 1', $revision->test_multiple_properties_multiple_values[1]->value2);
$this->assertEqual($i . ' - field test configurable field - value1 0', $revision->field_test_configurable_field[0]->value1);
$this->assertEqual($i . ' - field test configurable field - value2 0', $revision->field_test_configurable_field[0]->value2);
$this->assertEqual($i . ' - field test configurable field - value1 1', $revision->field_test_configurable_field[1]->value1);
$this->assertEqual($i . ' - field test configurable field - value2 1', $revision->field_test_configurable_field[1]->value2);
$this->assertEqual($i . ' - test entity base field info', $revision->test_entity_base_field_info->value);
// Do the same checks for translated field values if the entity type is
// translatable.
if (!$translatable) {
continue;
}
// Check that the correct initial value was provided for the
// 'revision_translation_affected' field.
$this->assertTrue($revision->revision_translation_affected->value);
$translation = $revision->getTranslation('ro');
$this->assertEqual($i . ' - test single property - ro', $translation->test_single_property->value);
$this->assertEqual($i . ' - test multiple properties - value1 - ro', $translation->test_multiple_properties->value1);
$this->assertEqual($i . ' - test multiple properties - value2 - ro', $translation->test_multiple_properties->value2);
$this->assertEqual($i . ' - test single property multiple values 0 - ro', $translation->test_single_property_multiple_values[0]->value);
$this->assertEqual($i . ' - test single property multiple values 1 - ro', $translation->test_single_property_multiple_values[1]->value);
$this->assertEqual($i . ' - test multiple properties multiple values - value1 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value1);
$this->assertEqual($i . ' - test multiple properties multiple values - value2 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value2);
$this->assertEqual($i . ' - test multiple properties multiple values - value1 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value1);
$this->assertEqual($i . ' - test multiple properties multiple values - value2 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value2);
$this->assertEqual($i . ' - field test configurable field - value1 0 - ro', $translation->field_test_configurable_field[0]->value1);
$this->assertEqual($i . ' - field test configurable field - value2 0 - ro', $translation->field_test_configurable_field[0]->value2);
$this->assertEqual($i . ' - field test configurable field - value1 1 - ro', $translation->field_test_configurable_field[1]->value1);
$this->assertEqual($i . ' - field test configurable field - value2 1 - ro', $translation->field_test_configurable_field[1]->value2);
$this->assertEqual($i . ' - test entity base field info - ro', $translation->test_entity_base_field_info->value);
}
// Check that temporary tables have been removed at the end of the process.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
}
// Check that backup tables have been removed at the end of the process.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name, 'old_')));
}
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
/**
* Tests converting a translatable entity type with data to revisionable.
*
* @group Entity
* @group Update
* @group legacy
*/
class SqlContentEntityStorageSchemaConverterTranslatableTest extends SqlContentEntityStorageSchemaConverterTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz',
__DIR__ . '/../../../../fixtures/update/drupal-8.entity-test-schema-converter-enabled.php',
];
}
/**
* Tests that a failed "make revisionable" update preserves the existing data.
*/
public function testMakeRevisionableErrorHandling() {
$original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
$original_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
foreach ($original_storage_definitions as $storage_definition) {
$original_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
}
// Check that entity type is not revisionable prior to running the update
// process.
$this->assertFalse($original_entity_type->isRevisionable());
// Make the update throw an exception during the entity save process.
\Drupal::state()->set('entity_test_update.throw_exception', TRUE);
// Since the update process is interrupted by the exception thrown above,
// we can not do the full post update testing offered by UpdatePathTestBase.
$this->checkFailedUpdates = FALSE;
// Make the entity type revisionable and run the updates.
$this->updateEntityTypeToRevisionableAndTranslatable();
$this->runUpdates();
// Check that the update failed.
$this->assertRaw('<strong>' . t('Failed:') . '</strong>');
// Check that the last installed entity type definition is kept as
// non-revisionable.
$new_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$this->assertFalse($new_entity_type->isRevisionable(), 'The entity type is kept unchanged.');
// Check that the last installed field storage definitions did not change by
// looking at the 'langcode' field, which is updated automatically.
$new_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
$langcode_key = $original_entity_type->getKey('langcode');
$this->assertEqual($original_storage_definitions[$langcode_key]->isRevisionable(), $new_storage_definitions[$langcode_key]->isRevisionable(), "The 'langcode' field is kept unchanged.");
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
// Check that installed storage schema did not change.
$new_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
$this->assertEqual($original_entity_schema_data, $new_entity_schema_data);
foreach ($new_storage_definitions as $storage_definition) {
$new_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
}
$this->assertEqual($original_field_schema_data, $new_field_schema_data);
// Check that temporary tables have been removed.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
}
// Check that the original tables still exist and their data is intact.
$this->assertTrue($schema->tableExists('entity_test_update'));
$this->assertTrue($schema->tableExists('entity_test_update_data'));
$base_table_count = \Drupal::database()->select('entity_test_update')
->countQuery()
->execute()
->fetchField();
$this->assertEqual($base_table_count, 102);
$data_table_count = \Drupal::database()->select('entity_test_update_data')
->countQuery()
->execute()
->fetchField();
// There are two records for each entity, one for English and one for
// Romanian.
$this->assertEqual($data_table_count, 204);
$base_table_row = \Drupal::database()->select('entity_test_update')
->fields('entity_test_update')
->condition('id', 1, '=')
->condition('langcode', 'en', '=')
->execute()
->fetchAllAssoc('id');
$this->assertEqual('843e9ac7-3351-4cc1-a202-2dbffffae21c', $base_table_row[1]->uuid);
$data_table_row = \Drupal::database()->select('entity_test_update_data')
->fields('entity_test_update_data')
->condition('id', 1, '=')
->condition('langcode', 'en', '=')
->execute()
->fetchAllAssoc('id');
$this->assertEqual('1 - test single property', $data_table_row[1]->test_single_property);
$this->assertEqual('1 - test multiple properties - value1', $data_table_row[1]->test_multiple_properties__value1);
$this->assertEqual('1 - test multiple properties - value2', $data_table_row[1]->test_multiple_properties__value2);
$this->assertEqual('1 - test entity base field info', $data_table_row[1]->test_entity_base_field_info);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
/**
* Runs SqlContentEntityStorageSchemaIndexTest with a dump filled with content.
*
* @group Entity
* @group legacy
*/
class SqlContentEntityStorageSchemaIndexFilledTest extends SqlContentEntityStorageSchemaIndexTest {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
parent::setDatabaseDumpFiles();
$this->databaseDumpFiles[0] = __DIR__ . '/../../../../fixtures/update/drupal-8.filled.standard.php.gz';
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests that a newly-added index is properly created during database updates.
*
* @group Entity
* @group legacy
*/
class SqlContentEntityStorageSchemaIndexTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../fixtures/update/drupal-8.bare.standard.php.gz',
];
}
/**
* Tests entity and field schema database updates and execution order.
*/
public function testIndex() {
// The initial Drupal 8 database dump before any updates does not include
// the entity ID in the entity field data table indices that were added in
// https://www.drupal.org/node/2261669.
$this->assertTrue(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode exists prior to running updates.');
$this->assertFalse(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode does not exist prior to running updates.');
$this->assertFalse(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode does not exist prior to running updates.');
// Running database updates should update the entity schemata to add the
// indices from https://www.drupal.org/node/2261669.
$this->runUpdates();
$this->assertFalse(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode properly removed.');
$this->assertTrue(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode properly created on the node_field_data table.');
$this->assertTrue(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode properly created on the user_field_data table.');
}
}

View file

@ -0,0 +1,189 @@
<?php
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Update\DbUpdatesTrait;
/**
* Tests performing entity updates through the Update API.
*
* @group Entity
*/
class UpdateApiEntityDefinitionUpdateTest extends BrowserTestBase {
use DbUpdatesTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test'];
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $updatesManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->entityManager = $this->container->get('entity.manager');
$this->updatesManager = $this->container->get('entity.definition_update_manager');
$admin = $this->drupalCreateUser([], FALSE, TRUE);
$this->drupalLogin($admin);
}
/**
* Tests that individual updates applied sequentially work as expected.
*/
public function testSingleUpdates() {
// Create a test entity.
$user_ids = [mt_rand(), mt_rand()];
$entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => $user_ids]);
$entity->save();
// Check that only a single value is stored for 'user_id'.
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id->target_id, $user_ids[0]);
// Make 'user_id' multiple by applying updates.
$this->enableUpdates('entity_test', 'entity_definition_updates', 8001);
$this->applyUpdates();
// Ensure the 'entity_test__user_id' table got created.
$this->assertTrue(\Drupal::database()->schema()->tableExists('entity_test__user_id'));
// Check that data was correctly migrated.
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id->target_id, $user_ids[0]);
// Store multiple data and check it is correctly stored.
$entity->user_id = $user_ids;
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 2);
$this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]);
$this->assertEqual($entity->user_id[1]->target_id, $user_ids[1]);
// Make 'user_id' single again by applying updates.
$this->enableUpdates('entity_test', 'entity_definition_updates', 8002);
$this->applyUpdates();
// Check that data was correctly migrated/dropped.
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id->target_id, $user_ids[0]);
// Check that only a single value is stored for 'user_id' again.
$entity->user_id = $user_ids;
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]);
}
/**
* Tests that multiple updates applied in bulk work as expected.
*/
public function testMultipleUpdates() {
// Create a test entity.
$user_ids = [mt_rand(), mt_rand()];
$entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => $user_ids]);
$entity->save();
// Check that only a single value is stored for 'user_id'.
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id->target_id, $user_ids[0]);
// Make 'user_id' multiple and then single again by applying updates.
$this->enableUpdates('entity_test', 'entity_definition_updates', 8002);
$this->applyUpdates();
// Check that data was correctly migrated back and forth.
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id->target_id, $user_ids[0]);
// Check that only a single value is stored for 'user_id' again.
$entity->user_id = $user_ids;
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertEqual(count($entity->user_id), 1);
$this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]);
}
/**
* Tests that entity updates are correctly reported in the status report page.
*/
public function testStatusReport() {
// Create a test entity.
$entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => mt_rand()]);
$entity->save();
// Check that the status report initially displays no error.
$this->drupalGet('admin/reports/status');
$this->assertNoRaw('Out of date');
$this->assertNoRaw('Mismatched entity and/or field definitions');
// Enable an entity update and check that we have a dedicated status report
// item.
$this->container->get('state')->set('entity_test.remove_name_field', TRUE);
$this->drupalGet('admin/reports/status');
$this->assertNoRaw('Out of date');
$this->assertRaw('Mismatched entity and/or field definitions');
// Enable a db update and check that now the entity update status report
// item is no longer displayed. We assume an update function will fix the
// mismatch.
$this->enableUpdates('entity_test', 'status_report', 8001);
$this->drupalGet('admin/reports/status');
$this->assertRaw('Out of date');
$this->assertRaw('Mismatched entity and/or field definitions');
// Apply db updates and check that entity updates were not applied.
$this->applyUpdates();
$this->drupalGet('admin/reports/status');
$this->assertNoRaw('Out of date');
$this->assertRaw('Mismatched entity and/or field definitions');
// Apply the entity updates and check that the entity update status report
// item is no longer displayed.
$this->updatesManager->applyUpdates();
$this->drupalGet('admin/reports/status');
$this->assertNoRaw('Out of date');
$this->assertNoRaw('Mismatched entity and/or field definitions');
}
/**
* Reloads the specified entity.
*
* @param \Drupal\entity_test\Entity\EntityTest $entity
* An entity object.
*
* @return \Drupal\entity_test\Entity\EntityTest
* The reloaded entity object.
*/
protected function reloadEntity(EntityTest $entity) {
$this->entityManager->useCaches(FALSE);
$this->entityManager->getStorage('entity_test')->resetCache([$entity->id()]);
return EntityTest::load($entity->id());
}
}

View file

@ -11,9 +11,9 @@ use Drupal\Tests\BrowserTestBase;
*/
class ConfigTest extends BrowserTestBase {
protected function setUp(){
protected function setUp() {
parent::setUp();
$this->drupalLogin ($this->drupalCreateUser(['administer site configuration']));
$this->drupalLogin($this->drupalCreateUser(['administer site configuration']));
}
/**

View file

@ -6,7 +6,7 @@ use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the log message added by file_save_htacess().
* Tests the log message added by file_save_htaccess().
*
* @group File
*/

View file

@ -20,7 +20,7 @@ class FileTransferTest extends BrowserTestBase {
protected function setUp() {
parent::setUp();
$this->testConnection = TestFileTransfer::factory(\Drupal::root(), ['hostname' => $this->hostname, 'username' => $this->username, 'password' => $this->password, 'port' => $this->port]);
$this->testConnection = TestFileTransfer::factory($this->root, ['hostname' => $this->hostname, 'username' => $this->username, 'password' => $this->password, 'port' => $this->port]);
}
public function _getFakeModuleFiles() {
@ -28,11 +28,11 @@ class FileTransferTest extends BrowserTestBase {
'fake.module',
'fake.info.yml',
'theme' => [
'fake.html.twig'
'fake.html.twig',
],
'inc' => [
'fake.inc'
]
'fake.inc',
],
];
return $files;
}
@ -60,7 +60,7 @@ class FileTransferTest extends BrowserTestBase {
$this->_writeDirectory($base . DIRECTORY_SEPARATOR . $key, $file);
}
else {
//just write the filename into the file
// just write the filename into the file
file_put_contents($base . DIRECTORY_SEPARATOR . $file, $file);
}
}
@ -82,7 +82,7 @@ class FileTransferTest extends BrowserTestBase {
$gotit = TRUE;
try {
$this->testConnection->copyDirectory($source, \Drupal::root() . '/' . PublicStream::basePath());
$this->testConnection->copyDirectory($source, $this->root . '/' . PublicStream::basePath());
}
catch (FileTransferException $e) {
$gotit = FALSE;

View file

@ -16,6 +16,8 @@ class TestFileTransfer extends FileTransfer {
/**
* This is for testing the CopyRecursive logic.
*
* @var bool
*/
public $shouldIsDirectoryReturnTrue = FALSE;
@ -46,7 +48,7 @@ class TestFileTransfer extends FileTransfer {
public function removeFileJailed($destination) {
if (!ftp_delete($this->connection, $item)) {
throw new FileTransferException('Unable to remove to file @file.', NULL, ['@file' => $item]);
throw new FileTransferException('Unable to remove the file @file.', NULL, ['@file' => $item]);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Utility\Xss;
use Drupal\Tests\BrowserTestBase;
/**
* Tests hook_form_alter() and hook_form_FORM_ID_alter().
*
* @group Form
*/
class AlterTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'form_test'];
/**
* Tests execution order of hook_form_alter() and hook_form_FORM_ID_alter().
*/
public function testExecutionOrder() {
$this->drupalGet('form-test/alter');
// Ensure that the order is first by module, then for a given module, the
// id-specific one after the generic one.
$expected = [
'block_form_form_test_alter_form_alter() executed.',
'form_test_form_alter() executed.',
'form_test_form_form_test_alter_form_alter() executed.',
'system_form_form_test_alter_form_alter() executed.',
];
$content = preg_replace('/\s+/', ' ', Xss::filter($this->getSession()->getPage()->getContent(), []));
$this->assert(strpos($content, implode(' ', $expected)) !== FALSE, 'Form alter hooks executed in the expected order.');
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests altering forms to be rebuilt so there are multiple steps.
*
* @group Form
*/
class ArbitraryRebuildTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['text', 'form_test'];
protected function setUp() {
parent::setUp();
// Auto-create a field for testing.
FieldStorageConfig::create([
'entity_type' => 'user',
'field_name' => 'test_multiple',
'type' => 'text',
'cardinality' => -1,
'translatable' => FALSE,
])->save();
FieldConfig::create([
'entity_type' => 'user',
'field_name' => 'test_multiple',
'bundle' => 'user',
'label' => 'Test a multiple valued field',
])->save();
entity_get_form_display('user', 'user', 'register')
->setComponent('test_multiple', [
'type' => 'text_textfield',
'weight' => 0,
])
->save();
}
/**
* Tests a basic rebuild with the user registration form.
*/
public function testUserRegistrationRebuild() {
$edit = [
'name' => 'foo',
'mail' => 'bar@example.com',
];
$this->drupalPostForm('user/register', $edit, 'Rebuild');
$this->assertText('Form rebuilt.');
$this->assertFieldByName('name', 'foo', 'Entered username has been kept.');
$this->assertFieldByName('mail', 'bar@example.com', 'Entered mail address has been kept.');
}
/**
* Tests a rebuild caused by a multiple value field.
*/
public function testUserRegistrationMultipleField() {
$edit = [
'name' => 'foo',
'mail' => 'bar@example.com',
];
$this->drupalPostForm('user/register', $edit, t('Add another item'));
$this->assertText('Test a multiple valued field', 'Form has been rebuilt.');
$this->assertFieldByName('name', 'foo', 'Entered username has been kept.');
$this->assertFieldByName('mail', 'bar@example.com', 'Entered mail address has been kept.');
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
/**
* Tests form API checkbox handling of various combinations of #default_value
* and #return_value.
*
* @group Form
*/
class CheckboxTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
public function testFormCheckbox() {
// Ensure that the checked state is determined and rendered correctly for
// tricky combinations of default and return values.
foreach ([FALSE, NULL, TRUE, 0, '0', '', 1, '1', 'foobar', '1foobar'] as $default_value) {
// Only values that can be used for array indices are supported for
// #return_value, with the exception of integer 0, which is not supported.
// @see \Drupal\Core\Render\Element\Checkbox::processCheckbox().
foreach (['0', '', 1, '1', 'foobar', '1foobar'] as $return_value) {
$form_array = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestCheckboxTypeJugglingForm', $default_value, $return_value);
$form = \Drupal::service('renderer')->renderRoot($form_array);
if ($default_value === TRUE) {
$checked = TRUE;
}
elseif ($return_value === '0') {
$checked = ($default_value === '0');
}
elseif ($return_value === '') {
$checked = ($default_value === '');
}
elseif ($return_value === 1 || $return_value === '1') {
$checked = ($default_value === 1 || $default_value === '1');
}
elseif ($return_value === 'foobar') {
$checked = ($default_value === 'foobar');
}
elseif ($return_value === '1foobar') {
$checked = ($default_value === '1foobar');
}
$checked_in_html = strpos($form, 'checked') !== FALSE;
$message = format_string('#default_value is %default_value #return_value is %return_value.', ['%default_value' => var_export($default_value, TRUE), '%return_value' => var_export($return_value, TRUE)]);
$this->assertIdentical($checked, $checked_in_html, $message);
}
}
// Ensure that $form_state->getValues() is populated correctly for a
// checkboxes group that includes a 0-indexed array of options.
$this->drupalPostForm('form-test/checkboxes-zero/1', [], 'Save');
$results = json_decode($this->getSession()->getPage()->getContent());
$this->assertIdentical($results->checkbox_off, [0, 0, 0], 'All three in checkbox_off are zeroes: off.');
$this->assertIdentical($results->checkbox_zero_default, ['0', 0, 0], 'The first choice is on in checkbox_zero_default');
$this->assertIdentical($results->checkbox_string_zero_default, ['0', 0, 0], 'The first choice is on in checkbox_string_zero_default');
// Due to Mink driver differences, we cannot submit an empty checkbox value
// to drupalPostForm(), even if that empty value is the 'true' value for
// the checkbox.
$this->drupalGet('form-test/checkboxes-zero/1');
$this->assertSession()->fieldExists('checkbox_off[0]')->check();
$this->drupalPostForm(NULL, NULL, 'Save');
$results = json_decode($this->getSession()->getPage()->getContent());
$this->assertIdentical($results->checkbox_off, ['0', 0, 0], 'The first choice is on in checkbox_off but the rest is not');
// Ensure that each checkbox is rendered correctly for a checkboxes group
// that includes a 0-indexed array of options.
$this->drupalPostForm('form-test/checkboxes-zero/0', [], 'Save');
$checkboxes = $this->xpath('//input[@type="checkbox"]');
$this->assertIdentical(count($checkboxes), 9, 'Correct number of checkboxes found.');
foreach ($checkboxes as $checkbox) {
$checked = $checkbox->isChecked();
$name = $checkbox->getAttribute('name');
$this->assertIdentical($checked, $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', format_string('Checkbox %name correctly checked', ['%name' => $name]));
}
// Due to Mink driver differences, we cannot submit an empty checkbox value
// to drupalPostForm(), even if that empty value is the 'true' value for
// the checkbox.
$this->drupalGet('form-test/checkboxes-zero/0');
$this->assertSession()->fieldExists('checkbox_off[0]')->check();
$this->drupalPostForm(NULL, NULL, 'Save');
$checkboxes = $this->xpath('//input[@type="checkbox"]');
$this->assertIdentical(count($checkboxes), 9, 'Correct number of checkboxes found.');
foreach ($checkboxes as $checkbox) {
$checked = $checkbox->isChecked();
$name = (string) $checkbox->getAttribute('name');
$this->assertIdentical($checked, $name == 'checkbox_off[0]' || $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', format_string('Checkbox %name correctly checked', ['%name' => $name]));
}
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests confirmation forms.
*
* @group Form
*/
class ConfirmFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
public function testConfirmForm() {
// Test the building of the form.
$this->drupalGet('form-test/confirm-form');
$site_name = $this->config('system.site')->get('name');
$this->assertTitle(t('ConfirmFormTestForm::getQuestion(). | @site-name', ['@site-name' => $site_name]), 'The question was found as the page title.');
$this->assertText(t('ConfirmFormTestForm::getDescription().'), 'The description was used.');
$this->assertFieldByXPath('//input[@id="edit-submit"]', t('ConfirmFormTestForm::getConfirmText().'), 'The confirm text was used.');
// Test cancelling the form.
$this->clickLink(t('ConfirmFormTestForm::getCancelText().'));
$this->assertUrl('form-test/autocomplete', [], "The form's cancel link was followed.");
// Test submitting the form.
$this->drupalPostForm('form-test/confirm-form', NULL, t('ConfirmFormTestForm::getConfirmText().'));
$this->assertText('The ConfirmFormTestForm::submitForm() method was used for this form.');
$this->assertUrl('', [], "The form's redirect was followed.");
// Test submitting the form with a destination.
$this->drupalPostForm('form-test/confirm-form', NULL, t('ConfirmFormTestForm::getConfirmText().'), ['query' => ['destination' => 'admin/config']]);
$this->assertUrl('admin/config', [], "The form's redirect was not followed, the destination query string was followed.");
// Test cancelling the form with a complex destination.
$this->drupalGet('form-test/confirm-form-array-path');
$this->clickLink(t('ConfirmFormArrayPathTestForm::getCancelText().'));
$this->assertUrl('form-test/confirm-form', ['query' => ['destination' => 'admin/config']], "The form's complex cancel link was followed.");
}
/**
* Tests that the confirm form does not use external destinations.
*/
public function testConfirmFormWithExternalDestination() {
$this->drupalGet('form-test/confirm-form');
$this->assertCancelLinkUrl(Url::fromRoute('form_test.route8'));
$this->drupalGet('form-test/confirm-form', ['query' => ['destination' => 'node']]);
$this->assertCancelLinkUrl(Url::fromUri('internal:/node'));
$this->drupalGet('form-test/confirm-form', ['query' => ['destination' => 'http://example.com']]);
$this->assertCancelLinkUrl(Url::fromRoute('form_test.route8'));
$this->drupalGet('form-test/confirm-form', ['query' => ['destination' => '<front>']]);
$this->assertCancelLinkUrl(Url::fromRoute('<front>'));
// Other invalid destinations, should fall back to the form default.
$this->drupalGet('form-test/confirm-form', ['query' => ['destination' => '/http://example.com']]);
$this->assertCancelLinkUrl(Url::fromRoute('form_test.route8'));
}
/**
* Asserts that a cancel link is present pointing to the provided URL.
*
* @param \Drupal\Core\Url $url
* The url to check for.
* @param string $message
* The assert message.
* @param string $group
* The assertion group.
*
* @return bool
* Result of the assertion.
*/
public function assertCancelLinkUrl(Url $url, $message = '', $group = 'Other') {
$links = $this->xpath('//a[@href=:url]', [':url' => $url->toString()]);
$message = ($message ? $message : new FormattableMarkup('Cancel link with URL %url found.', ['%url' => $url->toString()]));
return $this->assertTrue(isset($links[0]), $message, $group);
}
}

View file

@ -0,0 +1,234 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
/**
* Tests building and processing of core form elements.
*
* @group Form
*/
class ElementTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests placeholder text for elements that support placeholders.
*/
public function testPlaceHolderText() {
$this->drupalGet('form-test/placeholder-text');
$expected = 'placeholder-text';
// Test to make sure non-textarea elements have the proper placeholder text.
foreach (['textfield', 'tel', 'url', 'password', 'email', 'number'] as $type) {
$element = $this->xpath('//input[@id=:id and @placeholder=:expected]', [
':id' => 'edit-' . $type,
':expected' => $expected,
]);
$this->assertTrue(!empty($element), format_string('Placeholder text placed in @type.', ['@type' => $type]));
}
// Test to make sure textarea has the proper placeholder text.
$element = $this->xpath('//textarea[@id=:id and @placeholder=:expected]', [
':id' => 'edit-textarea',
':expected' => $expected,
]);
$this->assertTrue(!empty($element), 'Placeholder text placed in textarea.');
}
/**
* Tests expansion of #options for #type checkboxes and radios.
*/
public function testOptions() {
$this->drupalGet('form-test/checkboxes-radios');
// Verify that all options appear in their defined order.
foreach (['checkbox', 'radio'] as $type) {
$elements = $this->xpath('//input[@type=:type]', [':type' => $type]);
$expected_values = ['0', 'foo', '1', 'bar', '>'];
foreach ($elements as $element) {
$expected = array_shift($expected_values);
$this->assertIdentical((string) $element->getAttribute('value'), $expected);
}
}
// Verify that the choices are admin filtered as expected.
$this->assertRaw("<em>Special Char</em>alert('checkboxes');");
$this->assertRaw("<em>Special Char</em>alert('radios');");
$this->assertRaw('<em>Bar - checkboxes</em>');
$this->assertRaw('<em>Bar - radios</em>');
// Enable customized option sub-elements.
$this->drupalGet('form-test/checkboxes-radios/customize');
// Verify that all options appear in their defined order, taking a custom
// #weight into account.
foreach (['checkbox', 'radio'] as $type) {
$elements = $this->xpath('//input[@type=:type]', [':type' => $type]);
$expected_values = ['0', 'foo', 'bar', '>', '1'];
foreach ($elements as $element) {
$expected = array_shift($expected_values);
$this->assertIdentical((string) $element->getAttribute('value'), $expected);
}
}
// Verify that custom #description properties are output.
foreach (['checkboxes', 'radios'] as $type) {
$elements = $this->xpath('//input[@id=:id]/following-sibling::div[@class=:class]', [
':id' => 'edit-' . $type . '-foo',
':class' => 'description',
]);
$this->assertTrue(count($elements), format_string('Custom %type option description found.', [
'%type' => $type,
]));
}
}
/**
* Tests correct checked attribute for radios element.
*/
public function testRadiosChecked() {
// Verify that there is only one radio option checked.
$this->drupalGet('form-test/radios-checked');
$elements = $this->xpath('//input[@name="radios" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('0', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-string" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('bar', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-boolean-true" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('1', $elements[0]->getValue());
// A default value of FALSE indicates that nothing is set.
$elements = $this->xpath('//input[@name="radios-boolean-false" and @checked]');
$this->assertCount(0, $elements);
$elements = $this->xpath('//input[@name="radios-boolean-any" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('All', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-string-zero" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('0', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-int-non-zero" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('10', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-int-non-zero-as-string" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('100', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-empty-string" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('0', $elements[0]->getValue());
$elements = $this->xpath('//input[@name="radios-empty-array" and @checked]');
$this->assertCount(0, $elements);
$elements = $this->xpath('//input[@name="radios-key-FALSE" and @checked]');
$this->assertCount(1, $elements);
$this->assertSame('0', $elements[0]->getValue());
}
/**
* Tests wrapper ids for checkboxes and radios.
*/
public function testWrapperIds() {
$this->drupalGet('form-test/checkboxes-radios');
// Verify that wrapper id is different from element id.
foreach (['checkboxes', 'radios'] as $type) {
$element_ids = $this->xpath('//div[@id=:id]', [':id' => 'edit-' . $type]);
$wrapper_ids = $this->xpath('//fieldset[@id=:id]', [':id' => 'edit-' . $type . '--wrapper']);
$this->assertTrue(count($element_ids) == 1, format_string('A single element id found for type %type', ['%type' => $type]));
$this->assertTrue(count($wrapper_ids) == 1, format_string('A single wrapper id found for type %type', ['%type' => $type]));
}
}
/**
* Tests button classes.
*/
public function testButtonClasses() {
$this->drupalGet('form-test/button-class');
// Just contains(@class, "button") won't do because then
// "button--foo" would contain "button". Instead, check
// " button ". Make sure it matches in the beginning and the end too
// by adding a space before and after.
$this->assertEqual(2, count($this->xpath('//*[contains(concat(" ", @class, " "), " button ")]')));
$this->assertEqual(1, count($this->xpath('//*[contains(concat(" ", @class, " "), " button--foo ")]')));
$this->assertEqual(1, count($this->xpath('//*[contains(concat(" ", @class, " "), " button--danger ")]')));
}
/**
* Tests the #group property.
*/
public function testGroupElements() {
$this->drupalGet('form-test/group-details');
$elements = $this->xpath('//div[@class="details-wrapper"]//div[@class="details-wrapper"]//label');
$this->assertTrue(count($elements) == 1);
$this->drupalGet('form-test/group-container');
$elements = $this->xpath('//div[@id="edit-container"]//div[@class="details-wrapper"]//label');
$this->assertTrue(count($elements) == 1);
$this->drupalGet('form-test/group-fieldset');
$elements = $this->xpath('//fieldset[@id="edit-fieldset"]//div[@id="edit-meta"]//label');
$this->assertTrue(count($elements) == 1);
$this->drupalGet('form-test/group-vertical-tabs');
$elements = $this->xpath('//div[@data-vertical-tabs-panes]//details[@id="edit-meta"]//label');
$this->assertTrue(count($elements) == 1);
$elements = $this->xpath('//div[@data-vertical-tabs-panes]//details[@id="edit-meta-2"]//label');
$this->assertTrue(count($elements) == 1);
}
/**
* Tests the #required property on details and fieldset elements.
*/
public function testRequiredFieldsetsAndDetails() {
$this->drupalGet('form-test/group-details');
$this->assertFalse($this->cssSelect('summary.form-required'));
$this->drupalGet('form-test/group-details/1');
$this->assertTrue($this->cssSelect('summary.form-required'));
$this->drupalGet('form-test/group-fieldset');
$this->assertFalse($this->cssSelect('span.form-required'));
$this->drupalGet('form-test/group-fieldset/1');
$this->assertTrue($this->cssSelect('span.form-required'));
}
/**
* Tests a form with a autocomplete setting..
*/
public function testFormAutocomplete() {
$this->drupalGet('form-test/autocomplete');
$result = $this->xpath('//input[@id="edit-autocomplete-1" and contains(@data-autocomplete-path, "form-test/autocomplete-1")]');
$this->assertEqual(count($result), 0, 'Ensure that the user does not have access to the autocompletion');
$result = $this->xpath('//input[@id="edit-autocomplete-2" and contains(@data-autocomplete-path, "form-test/autocomplete-2/value")]');
$this->assertEqual(count($result), 0, 'Ensure that the user does not have access to the autocompletion');
$user = $this->drupalCreateUser(['access autocomplete test']);
$this->drupalLogin($user);
$this->drupalGet('form-test/autocomplete');
// Make sure that the autocomplete library is added.
$this->assertRaw('core/misc/autocomplete.js');
$result = $this->xpath('//input[@id="edit-autocomplete-1" and contains(@data-autocomplete-path, "form-test/autocomplete-1")]');
$this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion');
$result = $this->xpath('//input[@id="edit-autocomplete-2" and contains(@data-autocomplete-path, "form-test/autocomplete-2/value")]');
$this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion');
}
/**
* Tests form element error messages.
*/
public function testFormElementErrors() {
$this->drupalPostForm('form_test/details-form', [], 'Submit');
$this->assertText('I am an error on the details element.');
}
/**
* Tests summary attributes of details.
*/
public function testDetailsSummaryAttributes() {
$this->drupalGet('form-test/group-details');
$this->assertTrue($this->cssSelect('summary[data-summary-attribute="test"]'));
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the container form element for expected behavior.
*
* @group Form
*/
class ElementsContainerTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests the #optional container property.
*/
public function testOptionalContainerElements() {
$this->drupalGet('form-test/optional-container');
$assertSession = $this->assertSession();
$assertSession->elementNotExists('css', 'div.empty_optional');
$assertSession->elementExists('css', 'div.empty_nonoptional');
$assertSession->elementExists('css', 'div.nonempty_optional');
$assertSession->elementExists('css', 'div.nonempty_nonoptional');
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\form_test\Form\FormTestLabelForm;
use Drupal\Tests\BrowserTestBase;
/**
* Tests form element labels, required markers and associated output.
*
* @group Form
*/
class ElementsLabelsTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Test form elements, labels, title attributes and required marks output
* correctly and have the correct label option class if needed.
*/
public function testFormLabels() {
$this->drupalGet('form_test/form-labels');
// Check that the checkbox/radio processing is not interfering with
// basic placement.
$elements = $this->xpath('//input[@id="edit-form-checkboxes-test-third-checkbox"]/following-sibling::label[@for="edit-form-checkboxes-test-third-checkbox" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label follows field and label option class correct for regular checkboxes.');
// Make sure the label is rendered for checkboxes.
$elements = $this->xpath('//input[@id="edit-form-checkboxes-test-0"]/following-sibling::label[@for="edit-form-checkboxes-test-0" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label 0 found checkbox.');
$elements = $this->xpath('//input[@id="edit-form-radios-test-second-radio"]/following-sibling::label[@for="edit-form-radios-test-second-radio" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label follows field and label option class correct for regular radios.');
// Make sure the label is rendered for radios.
$elements = $this->xpath('//input[@id="edit-form-radios-test-0"]/following-sibling::label[@for="edit-form-radios-test-0" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label 0 found radios.');
// Exercise various defaults for checkboxes and modifications to ensure
// appropriate override and correct behavior.
$elements = $this->xpath('//input[@id="edit-form-checkbox-test"]/following-sibling::label[@for="edit-form-checkbox-test" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label follows field and label option class correct for a checkbox by default.');
// Exercise various defaults for textboxes and modifications to ensure
// appropriate override and correct behavior.
$elements = $this->xpath('//label[@for="edit-form-textfield-test-title-and-required" and @class="js-form-required form-required"]/following-sibling::input[@id="edit-form-textfield-test-title-and-required"]');
$this->assertTrue(isset($elements[0]), 'Label precedes textfield, with required marker inside label.');
$elements = $this->xpath('//input[@id="edit-form-textfield-test-no-title-required"]/preceding-sibling::label[@for="edit-form-textfield-test-no-title-required" and @class="js-form-required form-required"]');
$this->assertTrue(isset($elements[0]), 'Label tag with required marker precedes required textfield with no title.');
$elements = $this->xpath('//input[@id="edit-form-textfield-test-title-invisible"]/preceding-sibling::label[@for="edit-form-textfield-test-title-invisible" and @class="visually-hidden"]');
$this->assertTrue(isset($elements[0]), 'Label preceding field and label class is visually-hidden.');
$elements = $this->xpath('//input[@id="edit-form-textfield-test-title"]/preceding-sibling::span[@class="js-form-required form-required"]');
$this->assertFalse(isset($elements[0]), 'No required marker on non-required field.');
$elements = $this->xpath('//input[@id="edit-form-textfield-test-title-after"]/following-sibling::label[@for="edit-form-textfield-test-title-after" and @class="option"]');
$this->assertTrue(isset($elements[0]), 'Label after field and label option class correct for text field.');
$elements = $this->xpath('//label[@for="edit-form-textfield-test-title-no-show"]');
$this->assertFalse(isset($elements[0]), 'No label tag when title set not to display.');
$elements = $this->xpath('//div[contains(@class, "js-form-item-form-textfield-test-title-invisible") and contains(@class, "form-no-label")]');
$this->assertTrue(isset($elements[0]), 'Field class is form-no-label when there is no label.');
// Check #field_prefix and #field_suffix placement.
$elements = $this->xpath('//span[@class="field-prefix"]/following-sibling::div[@id="edit-form-radios-test"]');
$this->assertTrue(isset($elements[0]), 'Properly placed the #field_prefix element after the label and before the field.');
$elements = $this->xpath('//span[@class="field-suffix"]/preceding-sibling::div[@id="edit-form-radios-test"]');
$this->assertTrue(isset($elements[0]), 'Properly places the #field_suffix element immediately after the form field.');
// Check #prefix and #suffix placement.
$elements = $this->xpath('//div[@id="form-test-textfield-title-prefix"]/following-sibling::div[contains(@class, \'js-form-item-form-textfield-test-title\')]');
$this->assertTrue(isset($elements[0]), 'Properly places the #prefix element before the form item.');
$elements = $this->xpath('//div[@id="form-test-textfield-title-suffix"]/preceding-sibling::div[contains(@class, \'js-form-item-form-textfield-test-title\')]');
$this->assertTrue(isset($elements[0]), 'Properly places the #suffix element before the form item.');
// Check title attribute for radios and checkboxes.
$elements = $this->xpath('//div[@id="edit-form-checkboxes-title-attribute"]');
$this->assertEqual($elements[0]->getAttribute('title'), 'Checkboxes test' . ' (' . t('Required') . ')', 'Title attribute found.');
$elements = $this->xpath('//div[@id="edit-form-radios-title-attribute"]');
$this->assertEqual($elements[0]->getAttribute('title'), 'Radios test' . ' (' . t('Required') . ')', 'Title attribute found.');
$elements = $this->xpath('//fieldset[@id="edit-form-checkboxes-title-invisible--wrapper"]/legend/span[contains(@class, "visually-hidden")]');
$this->assertTrue(!empty($elements), "Title/Label not displayed when 'visually-hidden' attribute is set in checkboxes.");
$elements = $this->xpath('//fieldset[@id="edit-form-radios-title-invisible--wrapper"]/legend/span[contains(@class, "visually-hidden")]');
$this->assertTrue(!empty($elements), "Title/Label not displayed when 'visually-hidden' attribute is set in radios.");
}
/**
* Tests XSS-protection of element labels.
*/
public function testTitleEscaping() {
$this->drupalGet('form_test/form-labels');
foreach (FormTestLabelForm::$typesWithTitle as $type) {
$this->assertSession()->responseContains("$type alert('XSS') is XSS filtered!");
$this->assertSession()->responseNotContains("$type <script>alert('XSS')</script> is XSS filtered!");
}
}
/**
* Tests different display options for form element descriptions.
*/
public function testFormDescriptions() {
$this->drupalGet('form_test/form-descriptions');
// Check #description placement with #description_display='after'.
$field_id = 'edit-form-textfield-test-description-after';
$description_id = $field_id . '--description';
$elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[@id="' . $description_id . '"]');
$this->assertTrue(isset($elements[0]), t('Properly places the #description element after the form item.'));
// Check #description placement with #description_display='before'.
$field_id = 'edit-form-textfield-test-description-before';
$description_id = $field_id . '--description';
$elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/preceding-sibling::div[@id="' . $description_id . '"]');
$this->assertTrue(isset($elements[0]), t('Properly places the #description element before the form item.'));
// Check if the class is 'visually-hidden' on the form element description
// for the option with #description_display='invisible' and also check that
// the description is placed after the form element.
$field_id = 'edit-form-textfield-test-description-invisible';
$description_id = $field_id . '--description';
$elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[contains(@class, "visually-hidden")]');
$this->assertTrue(isset($elements[0]), t('Properly renders the #description element visually-hidden.'));
}
/**
* Test forms in theme-less environments.
*/
public function testFormsInThemeLessEnvironments() {
$form = $this->getFormWithLimitedProperties();
$render_service = $this->container->get('renderer');
// This should not throw any notices.
$render_service->renderPlain($form);
}
/**
* Return a form with element with not all properties defined.
*/
protected function getFormWithLimitedProperties() {
$form = [];
$form['fieldset'] = [
'#type' => 'fieldset',
'#title' => 'Fieldset',
];
return $form;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the vertical_tabs form element for expected behavior.
*
* @group Form
*/
class ElementsVerticalTabsTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* A user with permission to access vertical_tab_test_tabs.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A normal user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access vertical_tab_test tabs']);
$this->webUser = $this->drupalCreateUser();
$this->drupalLogin($this->adminUser);
}
/**
* Ensures that vertical-tabs.js is included before collapse.js.
*
* Otherwise, collapse.js adds "SHOW" or "HIDE" labels to the tabs.
*/
public function testJavaScriptOrdering() {
$this->drupalGet('form_test/vertical-tabs');
$content = $this->getSession()->getPage()->getContent();
$position1 = strpos($content, 'core/misc/vertical-tabs.js');
$position2 = strpos($content, 'core/misc/collapse.js');
$this->assertTrue($position1 !== FALSE && $position2 !== FALSE && $position1 < $position2, 'vertical-tabs.js is included before collapse.js');
}
/**
* Ensures that vertical tab markup is not shown if user has no tab access.
*/
public function testWrapperNotShownWhenEmpty() {
// Test admin user can see vertical tabs and wrapper.
$this->drupalGet('form_test/vertical-tabs');
$wrapper = $this->xpath("//div[@data-vertical-tabs-panes]");
$this->assertTrue(isset($wrapper[0]), 'Vertical tab panes found.');
// Test wrapper markup not present for non-privileged web user.
$this->drupalLogin($this->webUser);
$this->drupalGet('form_test/vertical-tabs');
$wrapper = $this->xpath("//div[@data-vertical-tabs-panes]");
$this->assertFalse(isset($wrapper[0]), 'Vertical tab wrappers are not displayed to unprivileged users.');
}
/**
* Ensures that default vertical tab is correctly selected.
*/
public function testDefaultTab() {
$this->drupalGet('form_test/vertical-tabs');
$value = $this->assertSession()
->elementExists('css', 'input[name="vertical_tabs__active_tab"]')
->getValue();
$this->assertSame('edit-tab3', $value, t('The default vertical tab is correctly selected.'));
}
/**
* Ensures that vertical tab form values are cleaned.
*/
public function testDefaultTabCleaned() {
$values = Json::decode($this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit')));
$this->assertFalse(isset($values['vertical_tabs__active_tab']), new FormattableMarkup('%element was removed.', ['%element' => 'vertical_tabs__active_tab']));
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the form API email element.
*
* @group Form
*/
class EmailTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests that #type 'email' fields are properly validated.
*/
public function testFormEmail() {
$edit = [];
$edit['email'] = 'invalid';
$edit['email_required'] = ' ';
$this->drupalPostForm('form-test/email', $edit, 'Submit');
$this->assertRaw(t('The email address %mail is not valid.', ['%mail' => 'invalid']));
$this->assertRaw(t('@name field is required.', ['@name' => 'Address']));
$edit = [];
$edit['email_required'] = ' foo.bar@example.com ';
$this->drupalPostForm('form-test/email', $edit, 'Submit');
$values = Json::decode($this->getSession()->getPage()->getContent());
$this->assertIdentical($values['email'], '');
$this->assertEqual($values['email_required'], 'foo.bar@example.com');
$edit = [];
$edit['email'] = 'foo@example.com';
$edit['email_required'] = 'example@drupal.org';
$this->drupalPostForm('form-test/email', $edit, 'Submit');
$values = Json::decode($this->getSession()->getPage()->getContent());
$this->assertEqual($values['email'], 'foo@example.com');
$this->assertEqual($values['email_required'], 'example@drupal.org');
}
}

View file

@ -2,15 +2,14 @@
namespace Drupal\Tests\system\Functional\Form;
use Drupal\system\Tests\System\SystemConfigFormTestBase;
use Drupal\form_test\FormTestObject;
use Drupal\Tests\BrowserTestBase;
/**
* Tests building a form from an object.
*
* @group Form
*/
class FormObjectTest extends SystemConfigFormTestBase {
class FormObjectTest extends BrowserTestBase {
/**
* Modules to enable.
@ -19,19 +18,6 @@ class FormObjectTest extends SystemConfigFormTestBase {
*/
public static $modules = ['form_test'];
protected function setUp() {
parent::setUp();
$this->form = new FormTestObject($this->container->get('config.factory'));
$this->values = [
'bananas' => [
'#value' => $this->randomString(10),
'#config_name' => 'form_test.object',
'#config_key' => 'bananas',
],
];
}
/**
* Tests using an object as the form callback.
*

View file

@ -0,0 +1,114 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
/**
* Tests form storage from cached pages.
*
* @group Form
*/
class FormStoragePageCacheTest extends BrowserTestBase {
/**
* @var array
*/
public static $modules = ['form_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Return the build id of the current form.
*/
protected function getFormBuildId() {
$build_id_fields = $this->xpath('//input[@name="form_build_id"]');
$this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page');
return (string) $build_id_fields[0]->getAttribute('value');
}
/**
* Build-id is regenerated when validating cached form.
*/
public function testValidateFormStorageOnCachedPage() {
$this->drupalGet('form-test/form-storage-page-cache');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_initial = $this->getFormBuildId();
// Trigger validation error by submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_first_validation = $this->getFormBuildId();
$this->assertNotEqual($build_id_initial, $build_id_first_validation, 'Build id changes when form validation fails');
// Trigger validation error by again submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_second_validation = $this->getFormBuildId();
$this->assertEqual($build_id_first_validation, $build_id_second_validation, 'Build id remains the same when form validation fails subsequently');
// Repeat the test sequence but this time with a page loaded from the cache.
$this->drupalGet('form-test/form-storage-page-cache');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_from_cache_initial = $this->getFormBuildId();
$this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request');
// Trigger validation error by submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_from_cache_first_validation = $this->getFormBuildId();
$this->assertNotEqual($build_id_initial, $build_id_from_cache_first_validation, 'Build id changes when form validation fails');
$this->assertNotEqual($build_id_first_validation, $build_id_from_cache_first_validation, 'Build id from first user is not reused');
// Trigger validation error by again submitting an empty title.
$edit = ['title' => ''];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_from_cache_second_validation = $this->getFormBuildId();
$this->assertEqual($build_id_from_cache_first_validation, $build_id_from_cache_second_validation, 'Build id remains the same when form validation fails subsequently');
}
/**
* Build-id is regenerated when rebuilding cached form.
*/
public function testRebuildFormStorageOnCachedPage() {
$this->drupalGet('form-test/form-storage-page-cache');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
$this->assertText('No old build id', 'No old build id on the page');
$build_id_initial = $this->getFormBuildId();
// Trigger rebuild, should regenerate build id. When a submit handler
// triggers a rebuild, the form is built twice in the same POST request,
// and during the second build, there is an old build ID, but because the
// form is not cached during the initial GET request, it is different from
// that initial build ID.
$edit = ['title' => 'something'];
$this->drupalPostForm(NULL, $edit, 'Rebuild');
$this->assertNoText('No old build id', 'There is no old build id on the page.');
$this->assertNoText($build_id_initial, 'The old build id is not the initial build id.');
$build_id_first_rebuild = $this->getFormBuildId();
$this->assertNotEqual($build_id_initial, $build_id_first_rebuild, 'Build id changes on first rebuild.');
// Trigger subsequent rebuild, should regenerate the build id again.
$edit = ['title' => 'something'];
$this->drupalPostForm(NULL, $edit, 'Rebuild');
$this->assertText($build_id_first_rebuild, 'First build id as old build id on the page');
$build_id_second_rebuild = $this->getFormBuildId();
$this->assertNotEqual($build_id_first_rebuild, $build_id_second_rebuild, 'Build id changes on second rebuild.');
}
}

View file

@ -0,0 +1,773 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormState;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\form_test\Form\FormTestDisabledElementsForm;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\RoleInterface;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests various form element validation mechanisms.
*
* @group Form
*/
class FormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['filter', 'form_test', 'file', 'datetime'];
protected function setUp() {
parent::setUp();
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
]);
$filtered_html_format->save();
$filtered_html_permission = $filtered_html_format->getPermissionName();
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, [$filtered_html_permission]);
}
/**
* Check several empty values for required forms elements.
*
* Carriage returns, tabs, spaces, and unchecked checkbox elements are not
* valid content for a required field.
*
* If the form field is found in $form_state->getErrors() then the test pass.
*/
public function testRequiredFields() {
// Originates from https://www.drupal.org/node/117748.
// Sets of empty strings and arrays.
$empty_strings = ['""' => "", '"\n"' => "\n", '" "' => " ", '"\t"' => "\t", '" \n\t "' => " \n\t ", '"\n\n\n\n\n"' => "\n\n\n\n\n"];
$empty_arrays = ['array()' => []];
$empty_checkbox = [NULL];
$elements['textfield']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'textfield'];
$elements['textfield']['empty_values'] = $empty_strings;
$elements['telephone']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'tel'];
$elements['telephone']['empty_values'] = $empty_strings;
$elements['url']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'url'];
$elements['url']['empty_values'] = $empty_strings;
$elements['search']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'search'];
$elements['search']['empty_values'] = $empty_strings;
$elements['password']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'password'];
$elements['password']['empty_values'] = $empty_strings;
$elements['password_confirm']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'password_confirm'];
// Provide empty values for both password fields.
foreach ($empty_strings as $key => $value) {
$elements['password_confirm']['empty_values'][$key] = ['pass1' => $value, 'pass2' => $value];
}
$elements['textarea']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'textarea'];
$elements['textarea']['empty_values'] = $empty_strings;
$elements['radios']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'radios', '#options' => ['' => t('None'), $this->randomMachineName(), $this->randomMachineName(), $this->randomMachineName()]];
$elements['radios']['empty_values'] = $empty_arrays;
$elements['checkbox']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'checkbox', '#required' => TRUE];
$elements['checkbox']['empty_values'] = $empty_checkbox;
$elements['checkboxes']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'checkboxes', '#options' => [$this->randomMachineName(), $this->randomMachineName(), $this->randomMachineName()]];
$elements['checkboxes']['empty_values'] = $empty_arrays;
$elements['select']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'select', '#options' => ['' => t('None'), $this->randomMachineName(), $this->randomMachineName(), $this->randomMachineName()]];
$elements['select']['empty_values'] = $empty_strings;
$elements['file']['element'] = ['#title' => $this->randomMachineName(), '#type' => 'file'];
$elements['file']['empty_values'] = $empty_strings;
// Regular expression to find the expected marker on required elements.
$required_marker_preg = '@<.*?class=".*?js-form-required.*form-required.*?">@';
// Go through all the elements and all the empty values for them.
foreach ($elements as $type => $data) {
foreach ($data['empty_values'] as $key => $empty) {
foreach ([TRUE, FALSE] as $required) {
$form_id = $this->randomMachineName();
$form = [];
$form_state = new FormState();
$form['op'] = ['#type' => 'submit', '#value' => t('Submit')];
$element = $data['element']['#title'];
$form[$element] = $data['element'];
$form[$element]['#required'] = $required;
$user_input[$element] = $empty;
$user_input['form_id'] = $form_id;
$form_state->setUserInput($user_input);
$form_state->setFormObject(new StubForm($form_id, $form));
$form_state->setMethod('POST');
// The form token CSRF protection should not interfere with this test,
// so we bypass it by setting the token to FALSE.
$form['#token'] = FALSE;
\Drupal::formBuilder()->prepareForm($form_id, $form, $form_state);
\Drupal::formBuilder()->processForm($form_id, $form, $form_state);
$errors = $form_state->getErrors();
// Form elements of type 'radios' throw all sorts of PHP notices
// when you try to render them like this, so we ignore those for
// testing the required marker.
// @todo Fix this work-around (https://www.drupal.org/node/588438).
$form_output = ($type == 'radios') ? '' : \Drupal::service('renderer')->renderRoot($form);
if ($required) {
// Make sure we have a form error for this element.
$this->assertTrue(isset($errors[$element]), "Check empty($key) '$type' field '$element'");
if (!empty($form_output)) {
// Make sure the form element is marked as required.
$this->assertTrue(preg_match($required_marker_preg, $form_output), "Required '$type' field is marked as required");
}
}
else {
if (!empty($form_output)) {
// Make sure the form element is *not* marked as required.
$this->assertFalse(preg_match($required_marker_preg, $form_output), "Optional '$type' field is not marked as required");
}
if ($type == 'select') {
// Select elements are going to have validation errors with empty
// input, since those are illegal choices. Just make sure the
// error is not "field is required".
$this->assertTrue((empty($errors[$element]) || strpos('field is required', (string) $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element");
}
else {
// Make sure there is *no* form error for this element.
$this->assertTrue(empty($errors[$element]), "Optional '$type' field '$element' has no errors with empty input");
}
}
}
}
}
// Clear the expected form error messages so they don't appear as exceptions.
\Drupal::messenger()->deleteAll();
}
/**
* Tests validation for required checkbox, select, and radio elements.
*
* Submits a test form containing several types of form elements. The form
* is submitted twice, first without values for required fields and then
* with values. Each submission is checked for relevant error messages.
*
* @see \Drupal\form_test\Form\FormTestValidateRequiredForm
*/
public function testRequiredCheckboxesRadio() {
$form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestValidateRequiredForm');
// Attempt to submit the form with no required fields set.
$edit = [];
$this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
// The only error messages that should appear are the relevant 'required'
// messages for each field.
$expected = [];
foreach (['textfield', 'checkboxes', 'select', 'radios'] as $key) {
if (isset($form[$key]['#required_error'])) {
$expected[] = $form[$key]['#required_error'];
}
elseif (isset($form[$key]['#form_test_required_error'])) {
$expected[] = $form[$key]['#form_test_required_error'];
}
else {
$expected[] = t('@name field is required.', ['@name' => $form[$key]['#title']]);
}
}
// Check the page for error messages.
$errors = $this->xpath('//div[contains(@class, "error")]//li');
foreach ($errors as $error) {
$expected_key = array_search($error->getText(), $expected);
// If the error message is not one of the expected messages, fail.
if ($expected_key === FALSE) {
$this->fail(format_string("Unexpected error message: @error", ['@error' => $error[0]]));
}
// Remove the expected message from the list once it is found.
else {
unset($expected[$expected_key]);
}
}
// Fail if any expected messages were not found.
foreach ($expected as $not_found) {
$this->fail(format_string("Found error message: @error", ['@error' => $not_found]));
}
// Verify that input elements are still empty.
$this->assertFieldByName('textfield', '');
$this->assertNoFieldChecked('edit-checkboxes-foo');
$this->assertNoFieldChecked('edit-checkboxes-bar');
$this->assertOptionSelected('edit-select', '');
$this->assertNoFieldChecked('edit-radios-foo');
$this->assertNoFieldChecked('edit-radios-bar');
$this->assertNoFieldChecked('edit-radios-optional-foo');
$this->assertNoFieldChecked('edit-radios-optional-bar');
$this->assertNoFieldChecked('edit-radios-optional-default-value-false-foo');
$this->assertNoFieldChecked('edit-radios-optional-default-value-false-bar');
// Submit again with required fields set and verify that there are no
// error messages.
$edit = [
'textfield' => $this->randomString(),
'checkboxes[foo]' => TRUE,
'select' => 'foo',
'radios' => 'bar',
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed when all required fields are filled.');
$this->assertRaw("The form_test_validate_required_form form was submitted successfully.", 'Validation form submitted successfully.');
}
/**
* Tests that input is retained for safe elements even with an invalid token.
*
* Submits a test form containing several types of form elements.
*/
public function testInputWithInvalidToken() {
// We need to be logged in to have CSRF tokens.
$account = $this->createUser();
$this->drupalLogin($account);
// Submit again with required fields set but an invalid form token and
// verify that all the values are retained.
$this->drupalGet(Url::fromRoute('form_test.validate_required'));
$this->assertSession()
->elementExists('css', 'input[name="form_token"]')
->setValue('invalid token');
$edit = [
'textfield' => $this->randomString(),
'checkboxes[bar]' => TRUE,
'select' => 'bar',
'radios' => 'foo',
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
// Verify that input elements retained the posted values.
$this->assertFieldByName('textfield', $edit['textfield']);
$this->assertNoFieldChecked('edit-checkboxes-foo');
$this->assertFieldChecked('edit-checkboxes-bar');
$this->assertOptionSelected('edit-select', 'bar');
$this->assertFieldChecked('edit-radios-foo');
// Check another form that has a textarea input.
$this->drupalGet(Url::fromRoute('form_test.required'));
$this->assertSession()
->elementExists('css', 'input[name="form_token"]')
->setValue('invalid token');
$edit = [
'textfield' => $this->randomString(),
'textarea' => $this->randomString() . "\n",
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
$this->assertFieldByName('textfield', $edit['textfield']);
$this->assertFieldByName('textarea', $edit['textarea']);
// Check another form that has a number input.
$this->drupalGet(Url::fromRoute('form_test.number'));
$this->assertSession()
->elementExists('css', 'input[name="form_token"]')
->setValue('invalid token');
$edit = [
'integer_step' => mt_rand(1, 100),
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
$this->assertFieldByName('integer_step', $edit['integer_step']);
// Check a form with a Url field
$this->drupalGet(Url::fromRoute('form_test.url'));
$this->assertSession()
->elementExists('css', 'input[name="form_token"]')
->setValue('invalid token');
$edit = [
'url' => $this->randomString(),
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
$this->assertFieldByName('url', $edit['url']);
}
/**
* CSRF tokens for GET forms should not be added by default.
*/
public function testGetFormsCsrfToken() {
// We need to be logged in to have CSRF tokens.
$account = $this->createUser();
$this->drupalLogin($account);
$this->drupalGet(Url::fromRoute('form_test.get_form'));
$this->assertNoRaw('form_token');
}
/**
* Tests validation for required textfield element without title.
*
* Submits a test form containing a textfield form element without title.
* The form is submitted twice, first without value for the required field
* and then with value. Each submission is checked for relevant error
* messages.
*
* @see \Drupal\form_test\Form\FormTestValidateRequiredNoTitleForm
*/
public function testRequiredTextfieldNoTitle() {
// Attempt to submit the form with no required field set.
$edit = [];
$this->drupalPostForm('form-test/validate-required-no-title', $edit, 'Submit');
$this->assertNoRaw("The form_test_validate_required_form_no_title form was submitted successfully.", 'Validation form submitted successfully.');
// Check the page for the error class on the textfield.
$this->assertFieldByXPath('//input[contains(@class, "error")]', FALSE, 'Error input form element class found.');
// Check the page for the aria-invalid attribute on the textfield.
$this->assertFieldByXPath('//input[contains(@aria-invalid, "true")]', FALSE, 'Aria invalid attribute found.');
// Submit again with required fields set and verify that there are no
// error messages.
$edit = [
'textfield' => $this->randomString(),
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$this->assertNoFieldByXpath('//input[contains(@class, "error")]', FALSE, 'No error input form element class found.');
$this->assertRaw("The form_test_validate_required_form_no_title form was submitted successfully.", 'Validation form submitted successfully.');
}
/**
* Test default value handling for checkboxes.
*
* @see _form_test_checkbox()
*/
public function testCheckboxProcessing() {
// First, try to submit without the required checkbox.
$edit = [];
$this->drupalPostForm('form-test/checkbox', $edit, t('Submit'));
$this->assertRaw(t('@name field is required.', ['@name' => 'required_checkbox']), 'A required checkbox is actually mandatory');
// Now try to submit the form correctly.
$this->drupalPostForm(NULL, ['required_checkbox' => 1], t('Submit'));
$values = Json::decode($this->getSession()->getPage()->getContent());
$expected_values = [
'disabled_checkbox_on' => 'disabled_checkbox_on',
'disabled_checkbox_off' => 0,
'checkbox_on' => 'checkbox_on',
'checkbox_off' => 0,
'zero_checkbox_on' => '0',
'zero_checkbox_off' => 0,
];
foreach ($expected_values as $widget => $expected_value) {
$this->assertSame($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', [
'%widget' => var_export($widget, TRUE),
'%expected' => var_export($expected_value, TRUE),
'%value' => var_export($values[$widget], TRUE),
]));
}
}
/**
* Tests validation of #type 'select' elements.
*/
public function testSelect() {
$form = \Drupal::formBuilder()->getForm('Drupal\form_test\Form\FormTestSelectForm');
$this->drupalGet('form-test/select');
// Verify that the options are escaped as expected.
$this->assertEscaped('<strong>four</strong>');
$this->assertNoRaw('<strong>four</strong>');
// Posting without any values should throw validation errors.
$this->drupalPostForm(NULL, [], 'Submit');
$no_errors = [
'select',
'select_required',
'select_optional',
'empty_value',
'empty_value_one',
'no_default_optional',
'no_default_empty_option_optional',
'no_default_empty_value_optional',
'multiple',
'multiple_no_default',
];
foreach ($no_errors as $key) {
$this->assertNoText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
}
$expected_errors = [
'no_default',
'no_default_empty_option',
'no_default_empty_value',
'no_default_empty_value_one',
'multiple_no_default_required',
];
foreach ($expected_errors as $key) {
$this->assertText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
}
// Post values for required fields.
$edit = [
'no_default' => 'three',
'no_default_empty_option' => 'three',
'no_default_empty_value' => 'three',
'no_default_empty_value_one' => 'three',
'multiple_no_default_required[]' => 'three',
];
$this->drupalPostForm(NULL, $edit, 'Submit');
$values = Json::decode($this->getSession()->getPage()->getContent());
// Verify expected values.
$expected = [
'select' => 'one',
'empty_value' => 'one',
'empty_value_one' => 'one',
'no_default' => 'three',
'no_default_optional' => 'one',
'no_default_optional_empty_value' => '',
'no_default_empty_option' => 'three',
'no_default_empty_option_optional' => '',
'no_default_empty_value' => 'three',
'no_default_empty_value_one' => 'three',
'no_default_empty_value_optional' => 0,
'multiple' => ['two' => 'two'],
'multiple_no_default' => [],
'multiple_no_default_required' => ['three' => 'three'],
];
foreach ($expected as $key => $value) {
$this->assertIdentical($values[$key], $value, format_string('@name: @actual is equal to @expected.', [
'@name' => $key,
'@actual' => var_export($values[$key], TRUE),
'@expected' => var_export($value, TRUE),
]));
}
}
/**
* Tests a select element when #options is not set.
*/
public function testEmptySelect() {
$this->drupalGet('form-test/empty-select');
$this->assertFieldByXPath("//select[1]", NULL, 'Select element found.');
$this->assertNoFieldByXPath("//select[1]/option", NULL, 'No option element found.');
}
/**
* Tests validation of #type 'number' and 'range' elements.
*/
public function testNumber() {
$form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestNumberForm');
// Array with all the error messages to be checked.
$error_messages = [
'no_number' => '%name must be a number.',
'too_low' => '%name must be higher than or equal to %min.',
'too_high' => '%name must be lower than or equal to %max.',
'step_mismatch' => '%name is not a valid number.',
];
// The expected errors.
$expected = [
'integer_no_number' => 'no_number',
'integer_no_step' => 0,
'integer_no_step_step_error' => 'step_mismatch',
'integer_step' => 0,
'integer_step_error' => 'step_mismatch',
'integer_step_min' => 0,
'integer_step_min_error' => 'too_low',
'integer_step_max' => 0,
'integer_step_max_error' => 'too_high',
'integer_step_min_border' => 0,
'integer_step_max_border' => 0,
'integer_step_based_on_min' => 0,
'integer_step_based_on_min_error' => 'step_mismatch',
'float_small_step' => 0,
'float_step_no_error' => 0,
'float_step_error' => 'step_mismatch',
'float_step_hard_no_error' => 0,
'float_step_hard_error' => 'step_mismatch',
'float_step_any_no_error' => 0,
];
// First test the number element type, then range.
foreach (['form-test/number', 'form-test/number/range'] as $path) {
// Post form and show errors.
$this->drupalPostForm($path, [], 'Submit');
foreach ($expected as $element => $error) {
// Create placeholder array.
$placeholders = [
'%name' => $form[$element]['#title'],
'%min' => isset($form[$element]['#min']) ? $form[$element]['#min'] : '0',
'%max' => isset($form[$element]['#max']) ? $form[$element]['#max'] : '0',
];
foreach ($error_messages as $id => $message) {
// Check if the error exists on the page, if the current message ID is
// expected. Otherwise ensure that the error message is not present.
if ($id === $error) {
$this->assertRaw(format_string($message, $placeholders));
}
else {
$this->assertNoRaw(format_string($message, $placeholders));
}
}
}
}
}
/**
* Tests default value handling of #type 'range' elements.
*/
public function testRange() {
$this->drupalPostForm('form-test/range', [], 'Submit');
$values = json_decode($this->getSession()->getPage()->getContent());
$this->assertEqual($values->with_default_value, 18);
$this->assertEqual($values->float, 10.5);
$this->assertEqual($values->integer, 6);
$this->assertEqual($values->offset, 6.9);
$this->drupalPostForm('form-test/range/invalid', [], 'Submit');
$this->assertFieldByXPath('//input[@type="range" and contains(@class, "error")]', NULL, 'Range element has the error class.');
}
/**
* Tests validation of #type 'color' elements.
*/
public function testColorValidation() {
// Keys are inputs, values are expected results.
$values = [
'' => '#000000',
'#000' => '#000000',
'AAA' => '#aaaaaa',
'#af0DEE' => '#af0dee',
'#99ccBc' => '#99ccbc',
'#aabbcc' => '#aabbcc',
'123456' => '#123456',
];
// Tests that valid values are properly normalized.
foreach ($values as $input => $expected) {
$edit = [
'color' => $input,
];
$this->drupalPostForm('form-test/color', $edit, 'Submit');
$result = json_decode($this->getSession()->getPage()->getContent());
$this->assertEqual($result->color, $expected);
}
// Tests invalid values are rejected.
$values = ['#0008', '#1234', '#fffffg', '#abcdef22', '17', '#uaa'];
foreach ($values as $input) {
$edit = [
'color' => $input,
];
$this->drupalPostForm('form-test/color', $edit, 'Submit');
$this->assertRaw(t('%name must be a valid color.', ['%name' => 'Color']));
}
}
/**
* Test handling of disabled elements.
*
* @see _form_test_disabled_elements()
*/
public function testDisabledElements() {
// Get the raw form in its original state.
$form_state = new FormState();
$form = (new FormTestDisabledElementsForm())->buildForm([], $form_state);
// Build a submission that tries to hijack the form by submitting input for
// elements that are disabled.
$edit = [];
foreach (Element::children($form) as $key) {
if (isset($form[$key]['#test_hijack_value'])) {
if (is_array($form[$key]['#test_hijack_value'])) {
foreach ($form[$key]['#test_hijack_value'] as $subkey => $value) {
$edit[$key . '[' . $subkey . ']'] = $value;
}
}
else {
$edit[$key] = $form[$key]['#test_hijack_value'];
}
}
}
// Submit the form with no input, as the browser does for disabled elements,
// and fetch the $form_state->getValues() that is passed to the submit handler.
$this->drupalPostForm('form-test/disabled-elements', [], t('Submit'));
$returned_values['normal'] = Json::decode($this->getSession()->getPage()->getContent());
// Do the same with input, as could happen if JavaScript un-disables an
// element. drupalPostForm() emulates a browser by not submitting input for
// disabled elements, so we need to un-disable those elements first.
$this->drupalGet('form-test/disabled-elements');
$disabled_elements = [];
foreach ($this->xpath('//*[@disabled]') as $element) {
$disabled_elements[] = (string) $element->getAttribute('name');
}
// All the elements should be marked as disabled, including the ones below
// the disabled container.
$actual_count = count($disabled_elements);
$expected_count = 42;
$this->assertEqual($actual_count, $expected_count, new FormattableMarkup('Found @actual elements with disabled property (expected @expected).', [
'@actual' => count($disabled_elements),
'@expected' => $expected_count,
]));
// Mink does not "see" hidden elements, so we need to set the value of the
// hidden element directly.
$this->assertSession()
->elementExists('css', 'input[name="hidden"]')
->setValue($edit['hidden']);
unset($edit['hidden']);
$this->drupalPostForm(NULL, $edit, t('Submit'));
$returned_values['hijacked'] = Json::decode($this->getSession()->getPage()->getContent());
// Ensure that the returned values match the form's default values in both
// cases.
foreach ($returned_values as $values) {
$this->assertFormValuesDefault($values, $form);
}
}
/**
* Assert that the values submitted to a form matches the default values of the elements.
*/
public function assertFormValuesDefault($values, $form) {
foreach (Element::children($form) as $key) {
if (isset($form[$key]['#default_value'])) {
if (isset($form[$key]['#expected_value'])) {
$expected_value = $form[$key]['#expected_value'];
}
else {
$expected_value = $form[$key]['#default_value'];
}
if ($key == 'checkboxes_multiple') {
// Checkboxes values are not filtered out.
$values[$key] = array_filter($values[$key]);
}
$this->assertIdentical($expected_value, $values[$key], format_string('Default value for %type: expected %expected, returned %returned.', ['%type' => $key, '%expected' => var_export($expected_value, TRUE), '%returned' => var_export($values[$key], TRUE)]));
}
// Recurse children.
$this->assertFormValuesDefault($values, $form[$key]);
}
}
/**
* Verify markup for disabled form elements.
*
* @see _form_test_disabled_elements()
*/
public function testDisabledMarkup() {
$this->drupalGet('form-test/disabled-elements');
$form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestDisabledElementsForm');
$type_map = [
'textarea' => 'textarea',
'select' => 'select',
'weight' => 'select',
'datetime' => 'datetime',
];
foreach ($form as $name => $item) {
// Skip special #types.
if (!isset($item['#type']) || in_array($item['#type'], ['hidden', 'text_format'])) {
continue;
}
// Setup XPath and CSS class depending on #type.
if (in_array($item['#type'], ['button', 'submit'])) {
$path = "//!type[contains(@class, :div-class) and @value=:value]";
$class = 'is-disabled';
}
elseif (in_array($item['#type'], ['image_button'])) {
$path = "//!type[contains(@class, :div-class) and @value=:value]";
$class = 'is-disabled';
}
else {
// starts-with() required for checkboxes.
$path = "//div[contains(@class, :div-class)]/descendant::!type[starts-with(@name, :name)]";
$class = 'form-disabled';
}
// Replace DOM element name in $path according to #type.
$type = 'input';
if (isset($type_map[$item['#type']])) {
$type = $type_map[$item['#type']];
}
if (isset($item['#value']) && is_object($item['#value'])) {
$item['#value'] = (string) $item['#value'];
}
$path = strtr($path, ['!type' => $type]);
// Verify that the element exists.
$element = $this->xpath($path, [
':name' => Html::escape($name),
':div-class' => $class,
':value' => isset($item['#value']) ? $item['#value'] : '',
]);
$this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', ['%type' => $item['#type']]));
}
// Verify special element #type text-format.
$element = $this->xpath('//div[contains(@class, :div-class)]/descendant::textarea[@name=:name]', [
':name' => 'text_format[value]',
':div-class' => 'form-disabled',
]);
$this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', ['%type' => 'text_format[value]']));
$element = $this->xpath('//div[contains(@class, :div-class)]/descendant::select[@name=:name]', [
':name' => 'text_format[format]',
':div-class' => 'form-disabled',
]);
$this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', ['%type' => 'text_format[format]']));
}
/**
* Test Form API protections against input forgery.
*
* @see \Drupal\form_test\Form\FormTestInputForgeryForm
*/
public function testInputForgery() {
$this->drupalGet('form-test/input-forgery');
// The value for checkboxes[two] was changed using post render to simulate
// an input forgery.
// @see \Drupal\form_test\Form\FormTestInputForgeryForm::postRender
$this->drupalPostForm(NULL, ['checkboxes[one]' => TRUE, 'checkboxes[two]' => TRUE], t('Submit'));
$this->assertText('An illegal choice has been detected.', 'Input forgery was detected.');
}
/**
* Tests required attribute.
*/
public function testRequiredAttribute() {
$this->drupalGet('form-test/required-attribute');
$expected = 'required';
// Test to make sure the elements have the proper required attribute.
foreach (['textfield', 'password'] as $type) {
$element = $this->xpath('//input[@id=:id and @required=:expected]', [
':id' => 'edit-' . $type,
':expected' => $expected,
]);
$this->assertTrue(!empty($element), format_string('The @type has the proper required attribute.', ['@type' => $type]));
}
// Test to make sure textarea has the proper required attribute.
$element = $this->xpath('//textarea[@id=:id and @required=:expected]', [
':id' => 'edit-textarea',
':expected' => $expected,
]);
$this->assertTrue(!empty($element), 'The textarea has the proper required attribute.');
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the language select form element prints and submits the right
* options.
*
* @group Form
*/
class LanguageSelectElementTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test', 'language'];
/**
* Tests that the options printed by the language select element are correct.
*/
public function testLanguageSelectElementOptions() {
// Add some languages.
ConfigurableLanguage::create([
'id' => 'aaa',
'label' => $this->randomMachineName(),
])->save();
ConfigurableLanguage::create([
'id' => 'bbb',
'label' => $this->randomMachineName(),
])->save();
\Drupal::languageManager()->reset();
$this->drupalGet('form-test/language_select');
// Check that the language fields were rendered on the page.
$ids = [
'edit-languages-all' => LanguageInterface::STATE_ALL,
'edit-languages-configurable' => LanguageInterface::STATE_CONFIGURABLE,
'edit-languages-locked' => LanguageInterface::STATE_LOCKED,
'edit-languages-config-and-locked' => LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_LOCKED,
];
foreach ($ids as $id => $flags) {
$this->assertField($id, format_string('The @id field was found on the page.', ['@id' => $id]));
$options = [];
/* @var $language_manager \Drupal\Core\Language\LanguageManagerInterface */
$language_manager = $this->container->get('language_manager');
foreach ($language_manager->getLanguages($flags) as $langcode => $language) {
$options[$langcode] = $language->isLocked() ? t('- @name -', ['@name' => $language->getName()]) : $language->getName();
}
$this->_testLanguageSelectElementOptions($id, $options);
}
// Test that the #options were not altered by #languages.
$this->assertField('edit-language-custom-options', format_string('The @id field was found on the page.', ['@id' => 'edit-language-custom-options']));
$this->_testLanguageSelectElementOptions('edit-language-custom-options', ['opt1' => 'First option', 'opt2' => 'Second option', 'opt3' => 'Third option']);
}
/**
* Tests the case when the language select elements should not be printed.
*
* This happens when the language module is disabled.
*/
public function testHiddenLanguageSelectElement() {
// Disable the language module, so that the language select field will not
// be rendered.
$this->container->get('module_installer')->uninstall(['language']);
$this->drupalGet('form-test/language_select');
// Check that the language fields were rendered on the page.
$ids = ['edit-languages-all', 'edit-languages-configurable', 'edit-languages-locked', 'edit-languages-config-and-locked'];
foreach ($ids as $id) {
$this->assertNoField($id, format_string('The @id field was not found on the page.', ['@id' => $id]));
}
// Check that the submitted values were the default values of the language
// field elements.
$edit = [];
$this->drupalPostForm(NULL, $edit, t('Submit'));
$values = Json::decode($this->getSession()->getPage()->getContent());
$this->assertEqual($values['languages_all'], 'xx');
$this->assertEqual($values['languages_configurable'], 'en');
$this->assertEqual($values['languages_locked'], LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->assertEqual($values['languages_config_and_locked'], 'dummy_value');
$this->assertEqual($values['language_custom_options'], 'opt2');
}
/**
* Helper function to check the options of a language select form element.
*
* @param string $id
* The id of the language select element to check.
*
* @param array $options
* An array with options to compare with.
*/
protected function _testLanguageSelectElementOptions($id, $options) {
// Check that the options in the language field are exactly the same,
// including the order, as the languages sent as a parameter.
$elements = $this->xpath("//select[@id='" . $id . "']");
$count = 0;
/** @var \Behat\Mink\Element\NodeElement $option */
foreach ($elements[0]->findAll('css', 'option') as $option) {
$count++;
$option_title = current($options);
$this->assertEqual($option->getText(), $option_title);
next($options);
}
$this->assertEqual($count, count($options), format_string('The number of languages and the number of options shown by the language element are the same: @languages languages, @number options', ['@languages' => count($options), '@number' => $count]));
}
}

View file

@ -51,4 +51,29 @@ class ModulesListFormWebTest extends BrowserTestBase {
$this->assertText('simpletest');
}
public function testModulesListFormWithInvalidInfoFile() {
$broken_info_yml = <<<BROKEN
name: Module With Broken Info file
type: module
BROKEN;
$path = \Drupal::service('site.path') . "/modules/broken";
mkdir($path, 0777, TRUE);
file_put_contents("$path/broken.info.yml", $broken_info_yml);
$this->drupalLogin(
$this->drupalCreateUser(
['administer modules', 'administer permissions']
)
);
$this->drupalGet('admin/modules');
$this->assertSession()->statusCodeEquals(200);
// Confirm that the error message is shown.
$this->assertSession()
->pageTextContains('Modules could not be listed due to an error: Missing required keys (core) in ' . $path . '/broken.info.yml');
// Check that the module filter text box is available.
$this->assertTrue($this->xpath('//input[@name="text"]'));
}
}

View file

@ -34,7 +34,6 @@ class RedirectTest extends BrowserTestBase {
$this->drupalPostForm($path, $edit, t('Submit'));
$this->assertUrl($edit['destination'], [], 'Basic redirection works.');
// Test without redirection.
$edit = [
'redirection' => FALSE,

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the form API Response element.
*
* @group Form
*/
class ResponseTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests that enforced responses propagate through subscribers and middleware.
*/
public function testFormResponse() {
$edit = [
'content' => $this->randomString(),
'status' => 200,
];
$this->drupalPostForm('form-test/response', $edit, 'Submit');
$content = Json::decode($this->getSession()->getPage()->getContent());
$this->assertResponse(200);
$this->assertIdentical($edit['content'], $content, 'Response content matches');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Response-Event'), 'Response handled by kernel response subscriber');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Stack-Middleware'), 'Response handled by kernel middleware');
$edit = [
'content' => $this->randomString(),
'status' => 418,
];
$this->drupalPostForm('form-test/response', $edit, 'Submit');
$content = Json::decode($this->getSession()->getPage()->getContent());
$this->assertResponse(418);
$this->assertIdentical($edit['content'], $content, 'Response content matches');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Response-Event'), 'Response handled by kernel response subscriber');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Stack-Middleware'), 'Response handled by kernel middleware');
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests proper removal of submitted form values using
* \Drupal\Core\Form\FormState::cleanValues() when having forms with elements
* containing buttons like "managed_file".
*
* @group Form
*/
class StateValuesCleanAdvancedTest extends BrowserTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['file', 'form_test'];
/**
* An image file path for uploading.
*/
protected $image;
/**
* Tests \Drupal\Core\Form\FormState::cleanValues().
*/
public function testFormStateValuesCleanAdvanced() {
// Get an image for uploading.
$image_files = $this->drupalGetTestFiles('image');
$this->image = current($image_files);
// Check if the physical file is there.
$this->assertTrue(is_file($this->image->uri), "The image file we're going to upload exists.");
// "Browse" for the desired file.
$edit = ['files[image]' => \Drupal::service('file_system')->realpath($this->image->uri)];
// Post the form.
$this->drupalPostForm('form_test/form-state-values-clean-advanced', $edit, t('Submit'));
// Expecting a 200 HTTP code.
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\BrowserTestBase;
/**
* Tests proper removal of submitted form values using
* \Drupal\Core\Form\FormState::cleanValues().
*
* @group Form
*/
class StateValuesCleanTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests \Drupal\Core\Form\FormState::cleanValues().
*/
public function testFormStateValuesClean() {
$this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit'));
$values = Json::decode($this->getSession()->getPage()->getContent());
// Setup the expected result.
$result = [
'beer' => 1000,
'baz' => ['beer' => 2000],
];
// Verify that all internal Form API elements were removed.
$this->assertFalse(isset($values['form_id']), format_string('%element was removed.', ['%element' => 'form_id']));
$this->assertFalse(isset($values['form_token']), format_string('%element was removed.', ['%element' => 'form_token']));
$this->assertFalse(isset($values['form_build_id']), format_string('%element was removed.', ['%element' => 'form_build_id']));
$this->assertFalse(isset($values['op']), format_string('%element was removed.', ['%element' => 'op']));
// Verify that all buttons were removed.
$this->assertFalse(isset($values['foo']), format_string('%element was removed.', ['%element' => 'foo']));
$this->assertFalse(isset($values['bar']), format_string('%element was removed.', ['%element' => 'bar']));
$this->assertFalse(isset($values['baz']['foo']), format_string('%element was removed.', ['%element' => 'foo']));
$this->assertFalse(isset($values['baz']['baz']), format_string('%element was removed.', ['%element' => 'baz']));
// Verify values manually added for cleaning were removed.
$this->assertFalse(isset($values['wine']), new FormattableMarkup('%element was removed.', ['%element' => 'wine']));
// Verify that nested form value still exists.
$this->assertTrue(isset($values['baz']['beer']), 'Nested form value still exists.');
// Verify that actual form values equal resulting form values.
$this->assertEqual($values, $result, 'Expected form values equal actual form values.');
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a stub form for testing purposes.
*
* @internal
*/
class StubForm extends FormBase {
/**
* The form array.
*
* @var array
*/
protected $form;
/**
* The form ID.
*
* @var string
*/
protected $formId;
/**
* Constructs a StubForm.
*
* @param string $form_id
* The form ID.
* @param array $form
* The form array.
*/
public function __construct($form_id, $form) {
$this->formId = $form_id;
$this->form = $form;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
$this->formId;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
return $this->form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the SystemConfigFormTestBase class.
*
* @group Form
*/
class SystemConfigFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests the SystemConfigFormTestBase class.
*/
public function testSystemConfigForm() {
$this->drupalGet('form-test/system-config-form');
$element = $this->xpath('//div[@id = :id]/input[contains(@class, :class)]', [':id' => 'edit-actions', ':class' => 'button--primary']);
$this->assertTrue($element, 'The primary action submit button was found.');
$this->drupalPostForm(NULL, [], t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the form API URL element.
*
* @group Form
*/
class UrlTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
protected $profile = 'testing';
/**
* Tests that #type 'url' fields are properly validated and trimmed.
*/
public function testFormUrl() {
$edit = [];
$edit['url'] = 'http://';
$edit['url_required'] = ' ';
$this->drupalPostForm('form-test/url', $edit, 'Submit');
$this->assertRaw(t('The URL %url is not valid.', ['%url' => 'http://']));
$this->assertRaw(t('@name field is required.', ['@name' => 'Required URL']));
$edit = [];
$edit['url'] = "\n";
$edit['url_required'] = 'http://example.com/ ';
$this->drupalPostForm('form-test/url', $edit, 'Submit');
$values = Json::decode($this->getSession()->getPage()->getContent());
$this->assertIdentical($values['url'], '');
$this->assertEqual($values['url_required'], 'http://example.com/');
$edit = [];
$edit['url'] = 'http://foo.bar.example.com/';
$edit['url_required'] = 'https://www.drupal.org/node/1174630?page=0&foo=bar#new';
$this->drupalPostForm('form-test/url', $edit, 'Submit');
$values = Json::decode($this->getSession()->getPage()->getContent());
$this->assertEqual($values['url'], $edit['url']);
$this->assertEqual($values['url_required'], $edit['url_required']);
}
}

View file

@ -0,0 +1,247 @@
<?php
namespace Drupal\Tests\system\Functional\Form;
use Drupal\Core\Render\Element;
use Drupal\Tests\BrowserTestBase;
/**
* Tests form processing and alteration via form validation handlers.
*
* @group Form
*/
class ValidationTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests #element_validate and #validate.
*/
public function testValidate() {
$this->drupalGet('form-test/validate');
// Verify that #element_validate handlers can alter the form and submitted
// form values.
$edit = [
'name' => 'element_validate',
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertFieldByName('name', '#value changed by #element_validate', 'Form element #value was altered.');
$this->assertText('Name value: value changed by setValueForElement() in #element_validate', 'Form element value in $form_state was altered.');
// Verify that #validate handlers can alter the form and submitted
// form values.
$edit = [
'name' => 'validate',
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertFieldByName('name', '#value changed by #validate', 'Form element #value was altered.');
$this->assertText('Name value: value changed by setValueForElement() in #validate', 'Form element value in $form_state was altered.');
// Verify that #element_validate handlers can make form elements
// inaccessible, but values persist.
$edit = [
'name' => 'element_validate_access',
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertNoFieldByName('name', 'Form element was hidden.');
$this->assertText('Name value: element_validate_access', 'Value for inaccessible form element exists.');
// Verify that value for inaccessible form element persists.
$this->drupalPostForm(NULL, [], 'Save');
$this->assertNoFieldByName('name', 'Form element was hidden.');
$this->assertText('Name value: element_validate_access', 'Value for inaccessible form element exists.');
// Verify that #validate handlers don't run if the CSRF token is invalid.
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet('form-test/validate');
// $this->assertSession()->fieldExists() does not recognize hidden fields,
// which breaks $this->drupalPostForm() if we try to change the value of a
// hidden field such as form_token.
$this->assertSession()
->elementExists('css', 'input[name="form_token"]')
->setValue('invalid_token');
$this->drupalPostForm(NULL, ['name' => 'validate'], 'Save');
$this->assertNoFieldByName('name', '#value changed by #validate', 'Form element #value was not altered.');
$this->assertNoText('Name value: value changed by setValueForElement() in #validate', 'Form element value in $form_state was not altered.');
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
}
/**
* Tests that a form with a disabled CSRF token can be validated.
*/
public function testDisabledToken() {
$this->drupalPostForm('form-test/validate-no-token', [], 'Save');
$this->assertText('The form_test_validate_no_token form has been submitted successfully.');
}
/**
* Tests partial form validation through #limit_validation_errors.
*/
public function testValidateLimitErrors() {
$edit = [
'test' => 'invalid',
'test_numeric_index[0]' => 'invalid',
'test_substring[foo]' => 'invalid',
];
$path = 'form-test/limit-validation-errors';
// Render the form, and verify that the buttons with limited server-side
// validation have the proper 'formnovalidate' attribute (to prevent
// client-side validation by the browser).
$this->drupalGet($path);
$expected = 'formnovalidate';
foreach (['partial', 'partial-numeric-index', 'substring'] as $type) {
$element = $this->xpath('//input[@id=:id and @formnovalidate=:expected]', [
':id' => 'edit-' . $type,
':expected' => $expected,
]);
$this->assertTrue(!empty($element), format_string('The @type button has the proper formnovalidate attribute.', ['@type' => $type]));
}
// The button with full server-side validation should not have the
// 'formnovalidate' attribute.
$element = $this->xpath('//input[@id=:id and not(@formnovalidate)]', [
':id' => 'edit-full',
]);
$this->assertTrue(!empty($element), 'The button with full server-side validation does not have the formnovalidate attribute.');
// Submit the form by pressing the 'Partial validate' button (uses
// #limit_validation_errors) and ensure that the title field is not
// validated, but the #element_validate handler for the 'test' field
// is triggered.
$this->drupalPostForm($path, $edit, t('Partial validate'));
$this->assertNoText(t('@name field is required.', ['@name' => 'Title']));
$this->assertText('Test element is invalid');
// Edge case of #limit_validation_errors containing numeric indexes: same
// thing with the 'Partial validate (numeric index)' button and the
// 'test_numeric_index' field.
$this->drupalPostForm($path, $edit, t('Partial validate (numeric index)'));
$this->assertNoText(t('@name field is required.', ['@name' => 'Title']));
$this->assertText('Test (numeric index) element is invalid');
// Ensure something like 'foobar' isn't considered "inside" 'foo'.
$this->drupalPostForm($path, $edit, t('Partial validate (substring)'));
$this->assertNoText(t('@name field is required.', ['@name' => 'Title']));
$this->assertText('Test (substring) foo element is invalid');
// Ensure not validated values are not available to submit handlers.
$this->drupalPostForm($path, ['title' => '', 'test' => 'valid'], t('Partial validate'));
$this->assertText('Only validated values appear in the form values.');
// Now test full form validation and ensure that the #element_validate
// handler is still triggered.
$this->drupalPostForm($path, $edit, t('Full validate'));
$this->assertText(t('@name field is required.', ['@name' => 'Title']));
$this->assertText('Test element is invalid');
}
/**
* Tests #pattern validation.
*/
public function testPatternValidation() {
$textfield_error = t('%name field is not in the right format.', ['%name' => 'One digit followed by lowercase letters']);
$tel_error = t('%name field is not in the right format.', ['%name' => 'Everything except numbers']);
$password_error = t('%name field is not in the right format.', ['%name' => 'Password']);
// Invalid textfield, valid tel.
$edit = [
'textfield' => 'invalid',
'tel' => 'valid',
];
$this->drupalPostForm('form-test/pattern', $edit, 'Submit');
$this->assertRaw($textfield_error);
$this->assertNoRaw($tel_error);
$this->assertNoRaw($password_error);
// Valid textfield, invalid tel, valid password.
$edit = [
'textfield' => '7seven',
'tel' => '818937',
'password' => '0100110',
];
$this->drupalPostForm('form-test/pattern', $edit, 'Submit');
$this->assertNoRaw($textfield_error);
$this->assertRaw($tel_error);
$this->assertNoRaw($password_error);
// Non required fields are not validated if empty.
$edit = [
'textfield' => '',
'tel' => '',
];
$this->drupalPostForm('form-test/pattern', $edit, 'Submit');
$this->assertNoRaw($textfield_error);
$this->assertNoRaw($tel_error);
$this->assertNoRaw($password_error);
// Invalid password.
$edit = [
'password' => $this->randomMachineName(),
];
$this->drupalPostForm('form-test/pattern', $edit, 'Submit');
$this->assertNoRaw($textfield_error);
$this->assertNoRaw($tel_error);
$this->assertRaw($password_error);
// The pattern attribute overrides #pattern and is not validated on the
// server side.
$edit = [
'textfield' => '',
'tel' => '',
'url' => 'http://www.example.com/',
];
$this->drupalPostForm('form-test/pattern', $edit, 'Submit');
$this->assertNoRaw(t('%name field is not in the right format.', ['%name' => 'Client side validation']));
}
/**
* Tests #required with custom validation errors.
*
* @see \Drupal\form_test\Form\FormTestValidateRequiredForm
*/
public function testCustomRequiredError() {
$form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestValidateRequiredForm');
// Verify that a custom #required error can be set.
$edit = [];
$this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
foreach (Element::children($form) as $key) {
if (isset($form[$key]['#required_error'])) {
$this->assertNoText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
$this->assertText($form[$key]['#required_error']);
}
elseif (isset($form[$key]['#form_test_required_error'])) {
$this->assertNoText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
$this->assertText($form[$key]['#form_test_required_error']);
}
}
$this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
// Verify that no custom validation error appears with valid values.
$edit = [
'textfield' => $this->randomString(),
'checkboxes[foo]' => TRUE,
'select' => 'foo',
];
$this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
foreach (Element::children($form) as $key) {
if (isset($form[$key]['#required_error'])) {
$this->assertNoText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
$this->assertNoText($form[$key]['#required_error']);
}
elseif (isset($form[$key]['#form_test_required_error'])) {
$this->assertNoText(t('@name field is required.', ['@name' => $form[$key]['#title']]));
$this->assertNoText($form[$key]['#form_test_required_error']);
}
}
$this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\system\Functional\Rest\ActionResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class ActionHalJsonAnonTest extends ActionResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\system\Functional\Rest\ActionResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ActionHalJsonBasicAuthTest extends ActionResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\system\Functional\Rest\ActionResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class ActionHalJsonCookieTest extends ActionResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\system\Functional\Rest\MenuResourceTestBase;
/**
* @group hal
*/
class MenuHalJsonAnonTest extends MenuResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\system\Functional\Rest\MenuResourceTestBase;
/**
* @group hal
*/
class MenuHalJsonBasicAuthTest extends MenuResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\system\Functional\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\system\Functional\Rest\MenuResourceTestBase;
/**
* @group hal
*/
class MenuHalJsonCookieTest extends MenuResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\system\Functional\Mail;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Site\Settings;
use Drupal\Tests\BrowserTestBase;
@ -14,6 +13,7 @@ use Drupal\Tests\BrowserTestBase;
* @group Mail
*/
class HtmlToTextTest extends BrowserTestBase {
/**
* Converts a string to its PHP source equivalent for display in test messages.
*
@ -48,7 +48,7 @@ class HtmlToTextTest extends BrowserTestBase {
* \Drupal\Core\Mail\MailFormatHelper::htmlToText().
*/
protected function assertHtmlToText($html, $text, $message, $allowed_tags = NULL) {
preg_match_all('/<([a-z0-6]+)/', Unicode::strtolower($html), $matches);
preg_match_all('/<([a-z0-6]+)/', mb_strtolower($html), $matches);
$tested_tags = implode(', ', array_unique($matches[1]));
$message .= ' (' . $tested_tags . ')';
$result = MailFormatHelper::htmlToText($html, $allowed_tags);
@ -241,8 +241,8 @@ class HtmlToTextTest extends BrowserTestBase {
if (!$pass) {
$this->verbose($this->stringToHtml($output));
}
$output_upper = Unicode::strtoupper($output);
$upper_input = Unicode::strtoupper($input);
$output_upper = mb_strtoupper($output);
$upper_input = mb_strtoupper($input);
$upper_output = MailFormatHelper::htmlToText($upper_input);
$pass = $this->assertEqual(
$upper_output,
@ -347,8 +347,8 @@ class HtmlToTextTest extends BrowserTestBase {
$maximum_line_length = 0;
foreach (explode($eol, $output) as $line) {
// We must use strlen() rather than Unicode::strlen() in order to count
// octets rather than characters.
// We must use strlen() rather than mb_strlen() in order to count octets
// rather than characters.
$maximum_line_length = max($maximum_line_length, strlen($line . $eol));
}
$verbose = 'Maximum line length found was ' . $maximum_line_length . ' octets.';

View file

@ -2,7 +2,13 @@
namespace Drupal\Tests\system\Functional\Mail;
use Drupal\Component\Utility\Random;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Mail\Plugin\Mail\TestMailCollector;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
use Drupal\system_mail_failure_test\Plugin\Mail\TestPhpMailFailure;
@ -18,7 +24,7 @@ class MailTest extends BrowserTestBase {
*
* @var array
*/
public static $modules = ['simpletest', 'system_mail_failure_test'];
public static $modules = ['simpletest', 'system_mail_failure_test', 'mail_html_test', 'file', 'image'];
/**
* Assert that the pluggable mail system is functional.
@ -90,13 +96,191 @@ class MailTest extends BrowserTestBase {
$this->assertEqual($reply_email, $sent_message['headers']['Reply-to'], 'Message reply-to headers are set.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
// Test that long site names containing characters that need MIME encoding
// works as expected.
$this->config('system.site')->set('name', 'Drépal this is a very long test sentence to test what happens with very long site names')->save();
// Send an email and check that the From-header contains the site name.
\Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
$this->assertEqual($from_email, $sent_message['headers']['From'], 'Message is sent from the site email account.');
$this->assertEquals('=?UTF-8?B?RHLDqXBhbCB0aGlzIGlzIGEgdmVyeSBsb25nIHRlc3Qgc2VudGVuY2UgdG8gdGU=?= <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly encoded.');
$this->assertEquals('Drépal this is a very long test sentence to te <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
$this->assertFalse(isset($sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
}
/**
* Checks that relative paths in mails are converted into absolute URLs.
*/
public function testConvertRelativeUrlsIntoAbsolute() {
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Use the HTML compatible state system collector mail backend.
$this->config('system.mail')->set('interface.default', 'test_html_mail_collector')->save();
// Fetch the hostname and port for matching against.
$http_host = \Drupal::request()->getSchemeAndHttpHost();
// Random generator.
$random = new Random();
// One random tag name.
$tag_name = strtolower($random->name(8, TRUE));
// Test root relative urls.
foreach (['href', 'src'] as $attribute) {
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', []);
$html = "<$tag_name $attribute=\"/root-relative\">root relative url in mail test</$tag_name>";
$expected_html = "<$tag_name $attribute=\"{$http_host}/root-relative\">root relative url in mail test</$tag_name>";
// Prepare render array.
$render = ['#markup' => Markup::create($html)];
// Send a test message that simpletest_mail_alter should cancel.
\Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Wrap the expected HTML and assert.
$expected_html = MailFormatHelper::wrapMail($expected_html);
$this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails.");
}
// Test protocol relative urls.
foreach (['href', 'src'] as $attribute) {
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', []);
$html = "<$tag_name $attribute=\"//example.com/protocol-relative\">protocol relative url in mail test</$tag_name>";
$expected_html = "<$tag_name $attribute=\"//example.com/protocol-relative\">protocol relative url in mail test</$tag_name>";
// Prepare render array.
$render = ['#markup' => Markup::create($html)];
// Send a test message that simpletest_mail_alter should cancel.
\Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Wrap the expected HTML and assert.
$expected_html = MailFormatHelper::wrapMail($expected_html);
$this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails.");
}
// Test absolute urls.
foreach (['href', 'src'] as $attribute) {
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', []);
$html = "<$tag_name $attribute=\"http://example.com/absolute\">absolute url in mail test</$tag_name>";
$expected_html = "<$tag_name $attribute=\"http://example.com/absolute\">absolute url in mail test</$tag_name>";
// Prepare render array.
$render = ['#markup' => Markup::create($html)];
// Send a test message that simpletest_mail_alter should cancel.
\Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Wrap the expected HTML and assert.
$expected_html = MailFormatHelper::wrapMail($expected_html);
$this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails.");
}
}
/**
* Checks that mails built from render arrays contain absolute paths.
*
* By default Drupal uses relative paths for images and links. When sending
* emails, absolute paths should be used instead.
*/
public function testRenderedElementsUseAbsolutePaths() {
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Use the HTML compatible state system collector mail backend.
$this->config('system.mail')->set('interface.default', 'test_html_mail_collector')->save();
// Fetch the hostname and port for matching against.
$http_host = \Drupal::request()->getSchemeAndHttpHost();
// Random generator.
$random = new Random();
$image_name = $random->name();
// Create an image file.
$file = File::create(['uri' => "public://{$image_name}.png", 'filename' => "{$image_name}.png"]);
$file->save();
$base_path = base_path();
$path_pairs = [
'root relative' => [$file->getFileUri(), "{$http_host}{$base_path}{$this->publicFilesDirectory}/{$image_name}.png"],
'protocol relative' => ['//example.com/image.png', '//example.com/image.png'],
'absolute' => ['http://example.com/image.png', 'http://example.com/image.png'],
];
// Test images.
foreach ($path_pairs as $test_type => $paths) {
list($input_path, $expected_path) = $paths;
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', []);
// Build the render array.
$render = [
'#theme' => 'image',
'#uri' => $input_path,
];
$expected_html = "<img src=\"$expected_path\" alt=\"\" />";
// Send a test message that simpletest_mail_alter should cancel.
\Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Wrap the expected HTML and assert.
$expected_html = MailFormatHelper::wrapMail($expected_html);
$this->assertSame($expected_html, $sent_message['body'], "Asserting that {$test_type} paths are converted properly.");
}
// Test links.
$path_pairs = [
'root relative' => [Url::fromUserInput('/path/to/something'), "{$http_host}{$base_path}path/to/something"],
'protocol relative' => [Url::fromUri('//example.com/image.png'), '//example.com/image.png'],
'absolute' => [Url::fromUri('http://example.com/image.png'), 'http://example.com/image.png'],
];
foreach ($path_pairs as $paths) {
list($input_path, $expected_path) = $paths;
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', []);
// Build the render array.
$render = [
'#title' => 'Link',
'#type' => 'link',
'#url' => $input_path,
];
$expected_html = "<a href=\"$expected_path\">Link</a>";
// Send a test message that simpletest_mail_alter should cancel.
\Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Wrap the expected HTML and assert.
$expected_html = MailFormatHelper::wrapMail($expected_html);
$this->assertSame($expected_html, $sent_message['body']);
}
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
/**
* Provides test assertions for verifying breadcrumbs.
*/
trait AssertBreadcrumbTrait {
use AssertMenuActiveTrailTrait;
/**
* Assert that a given path shows certain breadcrumb links.
*
* @param \Drupal\Core\Url|string $goto
* (optional) A path or URL to pass to
* Drupal\simpletest\WebTestBase::drupalGet().
* @param array $trail
* An associative array whose keys are expected breadcrumb link paths and
* whose values are expected breadcrumb link texts (not sanitized).
* @param string $page_title
* (optional) A page title to additionally assert via
* Drupal\simpletest\WebTestBase::assertTitle(). Without site name suffix.
* @param array $tree
* (optional) An associative array whose keys are link paths and whose
* values are link titles (not sanitized) of an expected active trail in a
* menu tree output on the page.
* @param $last_active
* (optional) Whether the last link in $tree is expected to be active (TRUE)
* or just to be in the active trail (FALSE).
*/
protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, array $tree = [], $last_active = TRUE) {
if (isset($goto)) {
$this->drupalGet($goto);
}
$this->assertBreadcrumbParts($trail);
// Additionally assert page title, if given.
if (isset($page_title)) {
$this->assertTitle(strtr('@title | Drupal', ['@title' => $page_title]));
}
// Additionally assert active trail in a menu tree output, if given.
if ($tree) {
$this->assertMenuActiveTrail($tree, $last_active);
}
}
/**
* Assert that a trail exists in the internal browser.
*
* @param array $trail
* An associative array whose keys are expected breadcrumb link paths and
* whose values are expected breadcrumb link texts (not sanitized).
*/
protected function assertBreadcrumbParts($trail) {
// Compare paths with actual breadcrumb.
$parts = $this->getBreadcrumbParts();
$pass = TRUE;
// There may be more than one breadcrumb on the page. If $trail is empty
// this test would go into an infinite loop, so we need to check that too.
while ($trail && !empty($parts)) {
foreach ($trail as $path => $title) {
// If the path is empty, generate the path from the <front> route. If
// the path does not start with a leading slash, then run it through
// Url::fromUri('base:')->toString() to get the correct base
// prepended.
if ($path == '') {
$url = Url::fromRoute('<front>')->toString();
}
elseif ($path[0] != '/') {
$url = Url::fromUri('base:' . $path)->toString();
}
else {
$url = $path;
}
$part = array_shift($parts);
$pass = ($pass && $part['href'] === $url && $part['text'] === Html::escape($title));
}
}
// No parts must be left, or an expected "Home" will always pass.
$pass = ($pass && empty($parts));
$this->assertTrue($pass, format_string('Breadcrumb %parts found on @path.', [
'%parts' => implode(' » ', $trail),
'@path' => $this->getUrl(),
]));
}
/**
* Returns the breadcrumb contents of the current page in the internal browser.
*/
protected function getBreadcrumbParts() {
$parts = [];
$elements = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
if (!empty($elements)) {
foreach ($elements as $element) {
$parts[] = [
'text' => $element->getText(),
'href' => $element->getAttribute('href'),
'title' => $element->getAttribute('title'),
];
}
}
return $parts;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Core\Url;
/**
* Provides test assertions for verifying the active menu trail.
*/
trait AssertMenuActiveTrailTrait {
/**
* Assert that active trail exists in a menu tree output.
*
* @param array $tree
* An associative array whose keys are link paths and whose
* values are link titles (not sanitized) of an expected active trail in a
* menu tree output on the page.
* @param bool $last_active
* Whether the last link in $tree is expected to be active (TRUE)
* or just to be in the active trail (FALSE).
*/
protected function assertMenuActiveTrail($tree, $last_active) {
end($tree);
$active_link_path = key($tree);
$active_link_title = array_pop($tree);
$xpath = '';
if ($tree) {
$i = 0;
foreach ($tree as $link_path => $link_title) {
$part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::');
$part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]';
$part_args = [
':class' => 'menu-item--active-trail',
':href' => Url::fromUri('base:' . $link_path)->toString(),
':title' => $link_title,
];
$xpath .= $this->buildXPathQuery($part_xpath, $part_args);
$i++;
}
$elements = $this->xpath($xpath);
$this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.');
// Append prefix for active link asserted below.
$xpath .= '/following-sibling::ul/descendant::';
}
else {
$xpath .= '//';
}
$xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : '');
$xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]';
$args = [
':class-trail' => 'menu-item--active-trail',
':class-active' => 'is-active',
':href' => Url::fromUri('base:' . $active_link_path)->toString(),
':title' => $active_link_title,
];
$elements = $this->xpath($xpath, $args);
$this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', [
'%title' => $active_link_title,
'%tree' => implode(' » ', $tree),
]));
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Tests\BrowserTestBase;
/**
* Tests breadcrumbs functionality.
*
* @group Menu
*/
class BreadcrumbFrontCacheContextsTest extends BrowserTestBase {
use AssertBreadcrumbTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'block',
'node',
'path',
'user',
];
/**
* A test node with path alias.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodeWithAlias;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$user = $this->drupalCreateUser();
$this->drupalCreateContentType([
'type' => 'page',
]);
// Create a node for front page.
$node_front = $this->drupalCreateNode([
'uid' => $user->id(),
]);
// Create a node with a random alias.
$this->nodeWithAlias = $this->drupalCreateNode([
'uid' => $user->id(),
'type' => 'page',
'path' => '/' . $this->randomMachineName(),
]);
// Configure 'node' as front page.
$this->config('system.site')
->set('page.front', '/node/' . $node_front->id())
->save();
\Drupal::cache('render')->deleteAll();
}
/**
* Validate that breadcrumb markup get the right cache contexts.
*
* Checking that the breadcrumb will be printed on node canonical routes even
* if it was rendered for the <front> page first.
*/
public function testBreadcrumbsFrontPageCache() {
// Hit front page first as anonymous user with 'cold' render cache.
$this->drupalGet('<front>');
$web_assert = $this->assertSession();
// Verify that no breadcrumb block presents.
$web_assert->elementNotExists('css', '.block-system-breadcrumb-block');
// Verify that breadcrumb appears correctly for the test content
// (which is not set as front page).
$this->drupalGet($this->nodeWithAlias->path->alias);
$breadcrumbs = $this->assertSession()->elementExists('css', '.block-system-breadcrumb-block');
$crumbs = $breadcrumbs->findAll('css', 'ol li');
$this->assertTrue(count($crumbs) === 1);
$this->assertTrue($crumbs[0]->getText() === 'Home');
}
}

View file

@ -0,0 +1,384 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\RoleInterface;
/**
* Tests breadcrumbs functionality.
*
* @group Menu
*/
class BreadcrumbTest extends BrowserTestBase {
use AssertBreadcrumbTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['menu_test', 'block'];
/**
* An administrative user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A regular user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* Test paths in the Standard profile.
*
* @var string
*/
protected $profile = 'standard';
protected function setUp() {
parent::setUp();
$perms = array_keys(\Drupal::service('user.permissions')->getPermissions());
$this->adminUser = $this->drupalCreateUser($perms);
$this->drupalLogin($this->adminUser);
// This test puts menu links in the Tools menu and then tests for their
// presence on the page, so we need to ensure that the Tools block will be
// displayed in the admin theme.
$this->drupalPlaceBlock('system_menu_block:tools', [
'region' => 'content',
'theme' => $this->config('system.theme')->get('admin'),
]);
}
/**
* Tests breadcrumbs on node and administrative paths.
*/
public function testBreadCrumbs() {
// Prepare common base breadcrumb elements.
$home = ['' => 'Home'];
$admin = $home + ['admin' => t('Administration')];
$config = $admin + ['admin/config' => t('Configuration')];
$type = 'article';
// Verify Taxonomy administration breadcrumbs.
$trail = $admin + [
'admin/structure' => t('Structure'),
];
$this->assertBreadcrumb('admin/structure/taxonomy', $trail);
$trail += [
'admin/structure/taxonomy' => t('Taxonomy'),
];
$this->assertBreadcrumb('admin/structure/taxonomy/manage/tags', $trail);
$trail += [
'admin/structure/taxonomy/manage/tags' => t('Edit Tags'),
];
$this->assertBreadcrumb('admin/structure/taxonomy/manage/tags/overview', $trail);
$this->assertBreadcrumb('admin/structure/taxonomy/manage/tags/add', $trail);
// Verify Menu administration breadcrumbs.
$trail = $admin + [
'admin/structure' => t('Structure'),
];
$this->assertBreadcrumb('admin/structure/menu', $trail);
$trail += [
'admin/structure/menu' => t('Menus'),
];
$this->assertBreadcrumb('admin/structure/menu/manage/tools', $trail);
$trail += [
'admin/structure/menu/manage/tools' => t('Tools'),
];
$this->assertBreadcrumb("admin/structure/menu/link/node.add_page/edit", $trail);
$this->assertBreadcrumb('admin/structure/menu/manage/tools/add', $trail);
// Verify Node administration breadcrumbs.
$trail = $admin + [
'admin/structure' => t('Structure'),
'admin/structure/types' => t('Content types'),
];
$this->assertBreadcrumb('admin/structure/types/add', $trail);
$this->assertBreadcrumb("admin/structure/types/manage/$type", $trail);
$trail += [
"admin/structure/types/manage/$type" => t('Article'),
];
$this->assertBreadcrumb("admin/structure/types/manage/$type/fields", $trail);
$this->assertBreadcrumb("admin/structure/types/manage/$type/display", $trail);
$trail_teaser = $trail + [
"admin/structure/types/manage/$type/display" => t('Manage display'),
];
$this->assertBreadcrumb("admin/structure/types/manage/$type/display/teaser", $trail_teaser);
$this->assertBreadcrumb("admin/structure/types/manage/$type/delete", $trail);
$trail += [
"admin/structure/types/manage/$type/fields" => t('Manage fields'),
];
$this->assertBreadcrumb("admin/structure/types/manage/$type/fields/node.$type.body", $trail);
// Verify Filter text format administration breadcrumbs.
$filter_formats = filter_formats();
$format = reset($filter_formats);
$format_id = $format->id();
$trail = $config + [
'admin/config/content' => t('Content authoring'),
];
$this->assertBreadcrumb('admin/config/content/formats', $trail);
$trail += [
'admin/config/content/formats' => t('Text formats and editors'),
];
$this->assertBreadcrumb('admin/config/content/formats/add', $trail);
$this->assertBreadcrumb("admin/config/content/formats/manage/$format_id", $trail);
// @todo Remove this part once we have a _title_callback, see
// https://www.drupal.org/node/2076085.
$trail += [
"admin/config/content/formats/manage/$format_id" => $format->label(),
];
$this->assertBreadcrumb("admin/config/content/formats/manage/$format_id/disable", $trail);
// Verify node breadcrumbs (without menu link).
$node1 = $this->drupalCreateNode();
$nid1 = $node1->id();
$trail = $home;
$this->assertBreadcrumb("node/$nid1", $trail);
// Also verify that the node does not appear elsewhere (e.g., menu trees).
$this->assertNoLink($node1->getTitle());
// Also verify that the node does not appear elsewhere (e.g., menu trees).
$this->assertNoLink($node1->getTitle());
$trail += [
"node/$nid1" => $node1->getTitle(),
];
$this->assertBreadcrumb("node/$nid1/edit", $trail);
// Verify that breadcrumb on node listing page contains "Home" only.
$trail = [];
$this->assertBreadcrumb('node', $trail);
// Verify node breadcrumbs (in menu).
// Do this separately for Main menu and Tools menu, since only the
// latter is a preferred menu by default.
// @todo Also test all themes? Manually testing led to the suspicion that
// breadcrumbs may differ, possibly due to theme overrides.
$menus = ['main', 'tools'];
// Alter node type menu settings.
$node_type = NodeType::load($type);
$node_type->setThirdPartySetting('menu_ui', 'available_menus', $menus);
$node_type->setThirdPartySetting('menu_ui', 'parent', 'tools:');
$node_type->save();
foreach ($menus as $menu) {
// Create a parent node in the current menu.
$title = $this->randomMachineName();
$node2 = $this->drupalCreateNode([
'type' => $type,
'title' => $title,
'menu' => [
'enabled' => 1,
'title' => 'Parent ' . $title,
'description' => '',
'menu_name' => $menu,
'parent' => '',
],
]);
if ($menu == 'tools') {
$parent = $node2;
}
}
// Create a Tools menu link for 'node', move the last parent node menu
// link below it, and verify a full breadcrumb for the last child node.
$menu = 'tools';
$edit = [
'title[0][value]' => 'Root',
'link[0][uri]' => '/node',
];
$this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$menu_links = entity_load_multiple_by_properties('menu_link_content', ['title' => 'Root']);
$link = reset($menu_links);
$edit = [
'menu[menu_parent]' => $link->getMenuName() . ':' . $link->getPluginId(),
];
$this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save'));
$expected = [
"node" => $link->getTitle(),
];
$trail = $home + $expected;
$tree = $expected + [
'node/' . $parent->id() => $parent->menu['title'],
];
$trail += [
'node/' . $parent->id() => $parent->menu['title'],
];
// Add a taxonomy term/tag to last node, and add a link for that term to the
// Tools menu.
$tags = [
'Drupal' => [],
'Breadcrumbs' => [],
];
$edit = [
'field_tags[target_id]' => implode(',', array_keys($tags)),
];
$this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save'));
// Put both terms into a hierarchy Drupal » Breadcrumbs. Required for both
// the menu links and the terms itself, since taxonomy_term_page() resets
// the breadcrumb based on taxonomy term hierarchy.
$parent_tid = 0;
foreach ($tags as $name => $null) {
$terms = entity_load_multiple_by_properties('taxonomy_term', ['name' => $name]);
$term = reset($terms);
$tags[$name]['term'] = $term;
if ($parent_tid) {
$edit = [
'parent[]' => [$parent_tid],
];
$this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save'));
}
$parent_tid = $term->id();
}
$parent_mlid = '';
foreach ($tags as $name => $data) {
$term = $data['term'];
$edit = [
'title[0][value]' => "$name link",
'link[0][uri]' => "/taxonomy/term/{$term->id()}",
'menu_parent' => "$menu:{$parent_mlid}",
'enabled[value]' => 1,
];
$this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$menu_links = entity_load_multiple_by_properties('menu_link_content', [
'title' => $edit['title[0][value]'],
'link.uri' => 'internal:/taxonomy/term/' . $term->id(),
]);
$tags[$name]['link'] = reset($menu_links);
$parent_mlid = $tags[$name]['link']->getPluginId();
}
// Verify expected breadcrumbs for menu links.
$trail = $home;
$tree = [];
// Logout the user because we want to check the active class as well, which
// is just rendered as anonymous user.
$this->drupalLogout();
foreach ($tags as $name => $data) {
$term = $data['term'];
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $link */
$link = $data['link'];
$link_path = $link->getUrlObject()->getInternalPath();
$tree += [
$link_path => $link->getTitle(),
];
$this->assertBreadcrumb($link_path, $trail, $term->getName(), $tree);
$this->assertEscaped($parent->getTitle(), 'Tagged node found.');
// Additionally make sure that this link appears only once; i.e., the
// untranslated menu links automatically generated from menu router items
// ('taxonomy/term/%') should never be translated and appear in any menu
// other than the breadcrumb trail.
$elements = $this->xpath('//nav[@id=:menu]/descendant::a[@href=:href]', [
':menu' => 'block-bartik-tools',
':href' => Url::fromUri('base:' . $link_path)->toString(),
]);
$this->assertTrue(count($elements) == 1, "Link to {$link_path} appears only once.");
// Next iteration should expect this tag as parent link.
// Note: Term name, not link name, due to taxonomy_term_page().
$trail += [
$link_path => $term->getName(),
];
}
// Verify breadcrumbs on user and user/%.
// We need to log back in and out below, and cannot simply grant the
// 'administer users' permission, since user_page() makes your head explode.
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, [
'access user profiles',
]);
// Verify breadcrumb on front page.
$this->assertBreadcrumb('<front>', []);
// Verify breadcrumb on user pages (without menu link) for anonymous user.
$trail = $home;
$this->assertBreadcrumb('user', $trail, t('Log in'));
$this->assertBreadcrumb('user/' . $this->adminUser->id(), $trail, $this->adminUser->getUsername());
// Verify breadcrumb on user pages (without menu link) for registered users.
$this->drupalLogin($this->adminUser);
$trail = $home;
$this->assertBreadcrumb('user', $trail, $this->adminUser->getUsername());
$this->assertBreadcrumb('user/' . $this->adminUser->id(), $trail, $this->adminUser->getUsername());
$trail += [
'user/' . $this->adminUser->id() => $this->adminUser->getUsername(),
];
$this->assertBreadcrumb('user/' . $this->adminUser->id() . '/edit', $trail, $this->adminUser->getUsername());
// Create a second user to verify breadcrumb on user pages again.
$this->webUser = $this->drupalCreateUser([
'administer users',
'access user profiles',
]);
$this->drupalLogin($this->webUser);
// Verify correct breadcrumb and page title on another user's account pages.
$trail = $home;
$this->assertBreadcrumb('user/' . $this->adminUser->id(), $trail, $this->adminUser->getUsername());
$trail += [
'user/' . $this->adminUser->id() => $this->adminUser->getUsername(),
];
$this->assertBreadcrumb('user/' . $this->adminUser->id() . '/edit', $trail, $this->adminUser->getUsername());
// Verify correct breadcrumb and page title when viewing own user account.
$trail = $home;
$this->assertBreadcrumb('user/' . $this->webUser->id(), $trail, $this->webUser->getUsername());
$trail += [
'user/' . $this->webUser->id() => $this->webUser->getUsername(),
];
$this->assertBreadcrumb('user/' . $this->webUser->id() . '/edit', $trail, $this->webUser->getUsername());
// Create an only slightly privileged user being able to access site reports
// but not administration pages.
$this->webUser = $this->drupalCreateUser([
'access site reports',
]);
$this->drupalLogin($this->webUser);
// Verify that we can access recent log entries, there is a corresponding
// page title, and that the breadcrumb is just the Home link (because the
// user is not able to access "Administer".
$trail = $home;
$this->assertBreadcrumb('admin', $trail, t('Access denied'));
$this->assertSession()->statusCodeEquals(403);
// Since the 'admin' path is not accessible, we still expect only the Home
// link.
$this->assertBreadcrumb('admin/reports', $trail, t('Reports'));
$this->assertSession()->statusCodeNotEquals(403);
// Since the Reports page is accessible, that will show.
$trail += ['admin/reports' => t('Reports')];
$this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages'));
$this->assertSession()->statusCodeNotEquals(403);
// Ensure that the breadcrumb is safe against XSS.
$this->drupalGet('menu-test/breadcrumb1/breadcrumb2/breadcrumb3');
$this->assertRaw('<script>alert(12);</script>');
$this->assertEscaped('<script>alert(123);</script>');
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests local actions derived from router and added/altered via hooks.
*
* @group Menu
*/
class LocalActionTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var string[]
*/
public static $modules = ['block', 'menu_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Tests appearance of local actions.
*/
public function testLocalAction() {
$this->drupalGet('menu-test-local-action');
// Ensure that both menu and route based actions are shown.
$this->assertLocalAction([
[Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
[Url::fromRoute('menu_test.local_action4'), Html::escape("<script>alert('Welcome to the jungle!')</script>")],
[Url::fromRoute('menu_test.local_action4'), Html::escape("<script>alert('Welcome to the derived jungle!')</script>")],
[Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'],
[Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'],
[Url::fromRoute('menu_test.local_action5'), 'Title override'],
]);
// Test a local action title that changes based on a config value.
$this->drupalGet(Url::fromRoute('menu_test.local_action6'));
$this->assertLocalAction([
[Url::fromRoute('menu_test.local_action5'), 'Original title'],
]);
// Verify the expected cache tag in the response headers.
$header_values = explode(' ', $this->drupalGetHeader('x-drupal-cache-tags'));
$this->assertTrue(in_array('config:menu_test.links.action', $header_values), "Found 'config:menu_test.links.action' cache tag in header");
/** @var \Drupal\Core\Config\Config $config */
$config = $this->container->get('config.factory')->getEditable('menu_test.links.action');
$config->set('title', 'New title');
$config->save();
$this->drupalGet(Url::fromRoute('menu_test.local_action6'));
$this->assertLocalAction([
[Url::fromRoute('menu_test.local_action5'), 'New title'],
]);
}
/**
* Asserts local actions in the page output.
*
* @param array $actions
* A list of expected action link titles, keyed by the hrefs.
*/
protected function assertLocalAction(array $actions) {
$elements = $this->xpath('//a[contains(@class, :class)]', [
':class' => 'button-action',
]);
$index = 0;
foreach ($actions as $action) {
/** @var \Drupal\Core\Url $url */
list($url, $title) = $action;
// SimpleXML gives us the unescaped text, not the actual escaped markup,
// so use a pattern instead to check the raw content.
// This behaviour is a bug in libxml, see
// https://bugs.php.net/bug.php?id=49437.
$this->assertPattern('@<a [^>]*class="[^"]*button-action[^"]*"[^>]*>' . preg_quote($title, '@') . '</@');
$this->assertEqual($elements[$index]->getAttribute('href'), $url->toString());
$index++;
}
}
}

View file

@ -0,0 +1,281 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests local tasks derived from router and added/altered via hooks.
*
* @group Menu
*/
class LocalTasksTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var string[]
*/
public static $modules = ['block', 'menu_test', 'entity_test', 'node'];
/**
* The local tasks block under testing.
*
* @var \Drupal\block\Entity\Block
*/
protected $sut;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->sut = $this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
}
/**
* Asserts local tasks in the page output.
*
* @param array $routes
* A list of expected local tasks, prepared as an array of route names and
* their associated route parameters, to assert on the page (in the given
* order).
* @param int $level
* (optional) The local tasks level to assert; 0 for primary, 1 for
* secondary. Defaults to 0.
*/
protected function assertLocalTasks(array $routes, $level = 0) {
$elements = $this->xpath('//*[contains(@class, :class)]//a', [
':class' => $level == 0 ? 'tabs primary' : 'tabs secondary',
]);
$this->assertTrue(count($elements), 'Local tasks found.');
foreach ($routes as $index => $route_info) {
list($route_name, $route_parameters) = $route_info;
$expected = Url::fromRoute($route_name, $route_parameters)->toString();
$method = ($elements[$index]->getAttribute('href') == $expected ? 'pass' : 'fail');
$this->{$method}(format_string('Task @number href @value equals @expected.', [
'@number' => $index + 1,
'@value' => $elements[$index]->getAttribute('href'),
'@expected' => $expected,
]));
}
}
/**
* Ensures that some local task appears.
*
* @param string $title
* The expected title.
*
* @return bool
* TRUE if the local task exists on the page.
*/
protected function assertLocalTaskAppers($title) {
// SimpleXML gives us the unescaped text, not the actual escaped markup,
// so use a pattern instead to check the raw content.
// This behaviour is a bug in libxml, see
// https://bugs.php.net/bug.php?id=49437.
return $this->assertPattern('@<a [^>]*>' . preg_quote($title, '@') . '</a>@');
}
/**
* Asserts that the local tasks on the specified level are not being printed.
*
* @param int $level
* (optional) The local tasks level to assert; 0 for primary, 1 for
* secondary. Defaults to 0.
*/
protected function assertNoLocalTasks($level = 0) {
$elements = $this->xpath('//*[contains(@class, :class)]//a', [
':class' => $level == 0 ? 'tabs primary' : 'tabs secondary',
]);
$this->assertFalse(count($elements), 'Local tasks not found.');
}
/**
* Tests the plugin based local tasks.
*/
public function testPluginLocalTask() {
// Verify local tasks defined in the hook.
$this->drupalGet(Url::fromRoute('menu_test.tasks_default'));
$this->assertLocalTasks([
['menu_test.tasks_default', []],
['menu_test.router_test1', ['bar' => 'unsafe']],
['menu_test.router_test1', ['bar' => '1']],
['menu_test.router_test2', ['bar' => '2']],
]);
// Verify that script tags are escaped on output.
$title = Html::escape("Task 1 <script>alert('Welcome to the jungle!')</script>");
$this->assertLocalTaskAppers($title);
$title = Html::escape("<script>alert('Welcome to the derived jungle!')</script>");
$this->assertLocalTaskAppers($title);
// Verify that local tasks appear as defined in the router.
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view'));
$this->assertLocalTasks([
['menu_test.local_task_test_tasks_view', []],
['menu_test.local_task_test_tasks_edit', []],
['menu_test.local_task_test_tasks_settings', []],
['menu_test.local_task_test_tasks_settings_dynamic', []],
]);
$title = Html::escape("<script>alert('Welcome to the jungle!')</script>");
$this->assertLocalTaskAppers($title);
// Ensure the view tab is active.
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
$this->assertEqual(1, count($result), 'There is just a single active tab.');
$this->assertEqual('View(active tab)', $result[0]->getText(), 'The view tab is active.');
// Verify that local tasks in the second level appear.
$sub_tasks = [
['menu_test.local_task_test_tasks_settings_sub1', []],
['menu_test.local_task_test_tasks_settings_sub2', []],
['menu_test.local_task_test_tasks_settings_sub3', []],
['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']],
['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive2']],
];
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
$this->assertLocalTasks($sub_tasks, 1);
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
$this->assertEqual(1, count($result), 'There is just a single active tab.');
$this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings_sub1'));
$this->assertLocalTasks($sub_tasks, 1);
$result = $this->xpath('//ul[contains(@class, "tabs")]//a[contains(@class, "active")]');
$this->assertEqual(2, count($result), 'There are tabs active on both levels.');
$this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
$this->assertEqual('Dynamic title for TestTasksSettingsSub1(active tab)', $result[1]->getText(), 'The sub1 tab is active.');
$this->assertCacheTag('kittens:ragdoll');
$this->assertCacheTag('kittens:dwarf-cat');
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']));
$this->assertLocalTasks($sub_tasks, 1);
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
$this->assertEqual(2, count($result), 'There are tabs active on both levels.');
$this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
$this->assertEqual('Derive 1(active tab)', $result[1]->getText(), 'The derive1 tab is active.');
// Ensures that the local tasks contains the proper 'provider key'
$definitions = $this->container->get('plugin.manager.menu.local_task')->getDefinitions();
$this->assertEqual($definitions['menu_test.local_task_test_tasks_view']['provider'], 'menu_test');
$this->assertEqual($definitions['menu_test.local_task_test_tasks_edit']['provider'], 'menu_test');
$this->assertEqual($definitions['menu_test.local_task_test_tasks_settings']['provider'], 'menu_test');
$this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub1']['provider'], 'menu_test');
$this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub2']['provider'], 'menu_test');
$this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub3']['provider'], 'menu_test');
// Test that we we correctly apply the active class to tabs where one of the
// request attributes is upcast to an entity object.
$entity = \Drupal::entityManager()->getStorage('entity_test')->create(['bundle' => 'test']);
$entity->save();
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']));
$tasks = [
['menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']],
['menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']],
];
$this->assertLocalTasks($tasks, 0);
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
$this->assertEqual(1, count($result), 'There is one active tab.');
$this->assertEqual('upcasting sub1(active tab)', $result[0]->getText(), 'The "upcasting sub1" tab is active.');
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']));
$tasks = [
['menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']],
['menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']],
];
$this->assertLocalTasks($tasks, 0);
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
$this->assertEqual(1, count($result), 'There is one active tab.');
$this->assertEqual('upcasting sub2(active tab)', $result[0]->getText(), 'The "upcasting sub2" tab is active.');
}
/**
* Tests that local task blocks are configurable to show a specific level.
*/
public function testLocalTaskBlock() {
// Remove the default block and create a new one.
$this->sut->delete();
$this->sut = $this->drupalPlaceBlock('local_tasks_block', [
'id' => 'tabs_block',
'primary' => TRUE,
'secondary' => FALSE,
]);
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
// Verify that local tasks in the first level appear.
$this->assertLocalTasks([
['menu_test.local_task_test_tasks_view', []],
['menu_test.local_task_test_tasks_edit', []],
['menu_test.local_task_test_tasks_settings', []],
]);
// Verify that local tasks in the second level doesn't appear.
$this->assertNoLocalTasks(1);
$this->sut->delete();
$this->sut = $this->drupalPlaceBlock('local_tasks_block', [
'id' => 'tabs_block',
'primary' => FALSE,
'secondary' => TRUE,
]);
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
// Verify that local tasks in the first level doesn't appear.
$this->assertNoLocalTasks(0);
// Verify that local tasks in the second level appear.
$sub_tasks = [
['menu_test.local_task_test_tasks_settings_sub1', []],
['menu_test.local_task_test_tasks_settings_sub2', []],
['menu_test.local_task_test_tasks_settings_sub3', []],
['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']],
['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive2']],
];
$this->assertLocalTasks($sub_tasks, 1);
}
/**
* Test that local tasks blocks cache is invalidated correctly.
*/
public function testLocalTaskBlockCache() {
$this->drupalLogin($this->rootUser);
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalGet('/admin/structure/types/manage/page');
// Only the Edit task. The block avoids showing a single tab.
$this->assertNoLocalTasks();
// Field UI adds the usual Manage fields etc tabs.
\Drupal::service('module_installer')->install(['field_ui']);
$this->drupalGet('/admin/structure/types/manage/page');
$this->assertLocalTasks([
['entity.node_type.edit_form', ['node_type' => 'page']],
['entity.node.field_ui_fields', ['node_type' => 'page']],
['entity.entity_form_display.node.default', ['node_type' => 'page']],
['entity.entity_view_display.node.default', ['node_type' => 'page']],
]);
}
}

View file

@ -38,7 +38,7 @@ class MenuAccessTest extends BrowserTestBase {
// Test that there's link rendered on the route.
$this->drupalGet('menu_test_access_check_session');
$this->assertLink('Test custom route access check');
// Page still accessible but thre should not be menu link.
// Page is still accessible but there should be no menu link.
$this->drupalGet('menu_test_access_check_session');
$this->assertResponse(200);
$this->assertNoLink('Test custom route access check');

View file

@ -0,0 +1,336 @@
<?php
namespace Drupal\Tests\system\Functional\Menu;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests menu router and default menu link functionality.
*
* @group Menu
*/
class MenuRouterTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'menu_test', 'test_page_test'];
/**
* Name of the administrative theme to use for tests.
*
* @var string
*/
protected $adminTheme;
/**
* Name of the default theme to use for tests.
*
* @var string
*/
protected $defaultTheme;
protected function setUp() {
// Enable dummy module that implements hook_menu.
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:tools');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests menu integration.
*/
public function testMenuIntegration() {
$this->doTestTitleMenuCallback();
$this->doTestMenuOptionalPlaceholders();
$this->doTestMenuHierarchy();
$this->doTestMenuOnRoute();
$this->doTestMenuName();
$this->doTestMenuLinksDiscoveredAlter();
$this->doTestHookMenuIntegration();
$this->doTestExoticPath();
}
/**
* Test local tasks with route placeholders.
*/
protected function doTestHookMenuIntegration() {
// Generate base path with random argument.
$machine_name = $this->randomMachineName(8);
$base_path = 'foo/' . $machine_name;
$this->drupalGet($base_path);
// Confirm correct controller activated.
$this->assertText('test1');
// Confirm local task links are displayed.
$this->assertLink('Local task A');
$this->assertLink('Local task B');
$this->assertNoLink('Local task C');
$this->assertEscaped("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES, 'UTF-8');
// Confirm correct local task href.
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test1', ['bar' => $machine_name])->toString());
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test2', ['bar' => $machine_name])->toString());
}
/**
* Test title callback set to FALSE.
*/
protected function doTestTitleCallbackFalse() {
$this->drupalGet('test-page');
$this->assertText('A title with @placeholder', 'Raw text found on the page');
$this->assertNoText(t('A title with @placeholder', ['@placeholder' => 'some other text']), 'Text with placeholder substitutions not found.');
}
/**
* Tests page title of MENU_CALLBACKs.
*/
protected function doTestTitleMenuCallback() {
// Verify that the menu router item title is not visible.
$this->drupalGet('');
$this->assertNoText(t('Menu Callback Title'));
// Verify that the menu router item title is output as page title.
$this->drupalGet('menu_callback_title');
$this->assertText(t('Menu Callback Title'));
}
/**
* Tests menu item descriptions.
*/
protected function doTestDescriptionMenuItems() {
// Verify that the menu router item title is output as page title.
$this->drupalGet('menu_callback_description');
$this->assertText(t('Menu item description text'));
}
/**
* Tests for menu_name parameter for default menu links.
*/
protected function doTestMenuName() {
$admin_user = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($admin_user);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.menu_name_test');
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->getMenuName(), 'original', 'Menu name is "original".');
// Change the menu_name parameter in menu_test.module, then force a menu
// rebuild.
menu_test_menu_name('changed');
$menu_link_manager->rebuild();
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.menu_name_test');
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->getMenuName(), 'changed', 'Menu name was successfully changed after rebuild.');
}
/**
* Tests menu links added in hook_menu_links_discovered_alter().
*/
protected function doTestMenuLinksDiscoveredAlter() {
// Check that machine name does not need to be defined since it is already
// set as the key of each menu link.
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.custom');
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->getPluginId(), 'menu_test.custom', 'Menu links added at hook_menu_links_discovered_alter() obtain the machine name from the $links key.');
// Make sure that rebuilding the menu tree does not produce duplicates of
// links added by hook_menu_links_discovered_alter().
\Drupal::service('router.builder')->rebuild();
$this->drupalGet('menu-test');
$this->assertUniqueText('Custom link', 'Menu links added by hook_menu_links_discovered_alter() do not duplicate after a menu rebuild.');
}
/**
* Tests for menu hierarchy.
*/
protected function doTestMenuHierarchy() {
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent');
$parent_link = reset($menu_links);
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent_child');
$child_link = reset($menu_links);
$menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent_child2');
$unattached_child_link = reset($menu_links);
$this->assertEqual($child_link->getParent(), $parent_link->getPluginId(), 'The parent of a directly attached child is correct.');
$this->assertEqual($unattached_child_link->getParent(), $child_link->getPluginId(), 'The parent of a non-directly attached child is correct.');
}
/**
* Test menu links that have optional placeholders.
*/
protected function doTestMenuOptionalPlaceholders() {
$this->drupalGet('menu-test/optional');
$this->assertResponse(200);
$this->assertText('Sometimes there is no placeholder.');
$this->drupalGet('menu-test/optional/foobar');
$this->assertResponse(200);
$this->assertText("Sometimes there is a placeholder: 'foobar'.");
}
/**
* Tests a menu on a router page.
*/
protected function doTestMenuOnRoute() {
\Drupal::service('module_installer')->install(['router_test']);
\Drupal::service('router.builder')->rebuild();
$this->resetAll();
$this->drupalGet('router_test/test2');
$this->assertLinkByHref('menu_no_title_callback');
$this->assertLinkByHref('menu-title-test/case1');
$this->assertLinkByHref('menu-title-test/case2');
$this->assertLinkByHref('menu-title-test/case3');
}
/**
* Test path containing "exotic" characters.
*/
protected function doTestExoticPath() {
// "Special" ASCII characters.
$path =
"menu-test/ -._~!$'\"()*@[]?&+%#,;=:" .
// Characters that look like a percent-escaped string.
"%23%25%26%2B%2F%3F" .
// Characters from various non-ASCII alphabets.
"éøïвβ中國書۞";
$this->drupalGet($path);
$this->assertRaw('This is the menuTestCallback content.');
$this->assertNoText(t('The website encountered an unexpected error. Please try again later.'));
}
/**
* Make sure the maintenance mode can be bypassed using an EventSubscriber.
*
* @see \Drupal\menu_test\EventSubscriber\MaintenanceModeSubscriber::onKernelRequestMaintenance()
*/
public function testMaintenanceModeLoginPaths() {
$this->container->get('state')->set('system.maintenance_mode', TRUE);
$offline_message = t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', ['@site' => $this->config('system.site')->get('name')]);
$this->drupalGet('test-page');
$this->assertText($offline_message);
$this->drupalGet('menu_login_callback');
$this->assertText('This is TestControllers::testLogin.', 'Maintenance mode can be bypassed using an event subscriber.');
$this->container->get('state')->set('system.maintenance_mode', FALSE);
}
/**
* Test that an authenticated user hitting 'user/login' gets redirected to
* 'user' and 'user/register' gets redirected to the user edit page.
*/
public function testAuthUserUserLogin() {
$web_user = $this->drupalCreateUser([]);
$this->drupalLogin($web_user);
$this->drupalGet('user/login');
// Check that we got to 'user'.
$this->assertUrl($this->loggedInUser->url('canonical', ['absolute' => TRUE]));
// user/register should redirect to user/UID/edit.
$this->drupalGet('user/register');
$this->assertUrl($this->loggedInUser->url('edit-form', ['absolute' => TRUE]));
}
/**
* Tests theme integration.
*/
public function testThemeIntegration() {
$this->defaultTheme = 'bartik';
$this->adminTheme = 'seven';
$theme_handler = $this->container->get('theme_handler');
$theme_handler->install([$this->defaultTheme, $this->adminTheme]);
$this->config('system.theme')
->set('default', $this->defaultTheme)
->set('admin', $this->adminTheme)
->save();
$this->doTestThemeCallbackMaintenanceMode();
$this->doTestThemeCallbackFakeTheme();
$this->doTestThemeCallbackAdministrative();
$this->doTestThemeCallbackNoThemeRequested();
$this->doTestThemeCallbackOptionalTheme();
}
/**
* Test the theme negotiation when it is set to use an administrative theme.
*/
protected function doTestThemeCallbackAdministrative() {
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertText('Active theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme negotiation.');
$this->assertRaw('seven/css/base/elements.css', "The administrative theme's CSS appears on the page.");
}
/**
* Test the theme negotiation when the site is in maintenance mode.
*/
protected function doTestThemeCallbackMaintenanceMode() {
$this->container->get('state')->set('system.maintenance_mode', TRUE);
// For a regular user, the fact that the site is in maintenance mode means
// we expect the theme callback system to be bypassed entirely.
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertRaw('bartik/css/base/elements.css', "The maintenance theme's CSS appears on the page.");
// An administrator, however, should continue to see the requested theme.
$admin_user = $this->drupalCreateUser(['access site in maintenance mode']);
$this->drupalLogin($admin_user);
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertText('Active theme: seven. Actual theme: seven.', 'The theme negotiation system is correctly triggered for an administrator when the site is in maintenance mode.');
$this->assertRaw('seven/css/base/elements.css', "The administrative theme's CSS appears on the page.");
$this->container->get('state')->set('system.maintenance_mode', FALSE);
}
/**
* Test the theme negotiation when it is set to use an optional theme.
*/
protected function doTestThemeCallbackOptionalTheme() {
// Request a theme that is not installed.
$this->drupalGet('menu-test/theme-callback/use-test-theme');
$this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that is not installed is requested.');
$this->assertRaw('bartik/css/base/elements.css', "The default theme's CSS appears on the page.");
// Now install the theme and request it again.
$theme_handler = $this->container->get('theme_handler');
$theme_handler->install(['test_theme']);
$this->drupalGet('menu-test/theme-callback/use-test-theme');
$this->assertText('Active theme: test_theme. Actual theme: test_theme.', 'The theme negotiation system uses an optional theme once it has been installed.');
$this->assertRaw('test_theme/kitten.css', "The optional theme's CSS appears on the page.");
$theme_handler->uninstall(['test_theme']);
}
/**
* Test the theme negotiation when it is set to use a theme that does not exist.
*/
protected function doTestThemeCallbackFakeTheme() {
$this->drupalGet('menu-test/theme-callback/use-fake-theme');
$this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that does not exist is requested.');
$this->assertRaw('bartik/css/base/elements.css', "The default theme's CSS appears on the page.");
}
/**
* Test the theme negotiation when no theme is requested.
*/
protected function doTestThemeCallbackNoThemeRequested() {
$this->drupalGet('menu-test/theme-callback/no-theme-requested');
$this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when no theme is requested.');
$this->assertRaw('bartik/css/base/elements.css', "The default theme's CSS appears on the page.");
}
}

View file

@ -13,9 +13,16 @@ class ClassLoaderTest extends BrowserTestBase {
/**
* The expected result from calling the module-provided class' method.
*
* @var string
*/
protected $expected = 'Drupal\\module_autoload_test\\SomeClass::testMethod() was invoked.';
/**
* {@inheritdoc}
*/
protected $apcuEnsureUniquePrefix = TRUE;
/**
* Tests that module-provided classes can be loaded when a module is enabled.
*

View file

@ -0,0 +1,203 @@
<?php
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Component\Utility\Unicode;
/**
* Enable module without dependency enabled.
*
* @group Module
*/
class DependencyTest extends ModuleTestBase {
/**
* Checks functionality of project namespaces for dependencies.
*/
public function testProjectNamespaceForDependencies() {
$edit = [
'modules[filter][enable]' => TRUE,
];
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// Enable module with project namespace to ensure nothing breaks.
$edit = [
'modules[system_project_namespace_test][enable]' => TRUE,
];
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->assertModules(['system_project_namespace_test'], TRUE);
}
/**
* Attempts to enable the Content Translation module without Language enabled.
*/
public function testEnableWithoutDependency() {
// Attempt to enable Content Translation without Language enabled.
$edit = [];
$edit['modules[content_translation][enable]'] = 'content_translation';
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->assertText(t('Some required modules must be enabled'), 'Dependency required.');
$this->assertModules(['content_translation', 'language'], FALSE);
// Assert that the language tables weren't enabled.
$this->assertTableCount('language', FALSE);
$this->drupalPostForm(NULL, NULL, t('Continue'));
$this->assertText(t('2 modules have been enabled: Content Translation, Language.'), 'Modules status has been updated.');
$this->assertModules(['content_translation', 'language'], TRUE);
// Assert that the language YAML files were created.
$storage = $this->container->get('config.storage');
$this->assertTrue(count($storage->listAll('language.entity.')) > 0, 'Language config entity files exist.');
}
/**
* Attempts to enable a module with a missing dependency.
*/
public function testMissingModules() {
// Test that the system_dependencies_test module is marked
// as missing a dependency.
$this->drupalGet('admin/modules');
$this->assertRaw(t('@module (<span class="admin-missing">missing</span>)', ['@module' => Unicode::ucfirst('_missing_dependency')]), 'A module with missing dependencies is marked as such.');
$checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[system_dependencies_test][enable]"]');
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
/**
* Tests enabling a module that depends on an incompatible version of a module.
*/
public function testIncompatibleModuleVersionDependency() {
// Test that the system_incompatible_module_version_dependencies_test is
// marked as having an incompatible dependency.
$this->drupalGet('admin/modules');
$this->assertRaw(t('@module (<span class="admin-missing">incompatible with</span> version @version)', [
'@module' => 'System incompatible module version test (>2.0)',
'@version' => '1.0',
]), 'A module that depends on an incompatible version of a module is marked as such.');
$checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[system_incompatible_module_version_dependencies_test][enable]"]');
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
/**
* Tests enabling a module that depends on a module with an incompatible core version.
*/
public function testIncompatibleCoreVersionDependency() {
// Test that the system_incompatible_core_version_dependencies_test is
// marked as having an incompatible dependency.
$this->drupalGet('admin/modules');
$this->assertRaw(t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
'@module' => 'System incompatible core version test',
]), 'A module that depends on a module with an incompatible core version is marked as such.');
$checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[system_incompatible_core_version_dependencies_test][enable]"]');
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
/**
* Tests failing PHP version requirements.
*/
public function testIncompatiblePhpVersionDependency() {
$this->drupalGet('admin/modules');
$this->assertRaw('This module requires PHP version 6502.* and is incompatible with PHP version ' . phpversion() . '.', 'User is informed when the PHP dependency requirement of a module is not met.');
$checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[system_incompatible_php_version_test][enable]"]');
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
/**
* Tests enabling a module that depends on a module which fails hook_requirements().
*/
public function testEnableRequirementsFailureDependency() {
\Drupal::service('module_installer')->install(['comment']);
$this->assertModules(['requirements1_test'], FALSE);
$this->assertModules(['requirements2_test'], FALSE);
// Attempt to install both modules at the same time.
$edit = [];
$edit['modules[requirements1_test][enable]'] = 'requirements1_test';
$edit['modules[requirements2_test][enable]'] = 'requirements2_test';
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// Makes sure the modules were NOT installed.
$this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.');
$this->assertModules(['requirements1_test'], FALSE);
$this->assertModules(['requirements2_test'], FALSE);
// Makes sure that already enabled modules the failing modules depend on
// were not disabled.
$this->assertModules(['comment'], TRUE);
}
/**
* Tests that module dependencies are enabled in the correct order in the UI.
*
* Dependencies should be enabled before their dependents.
*/
public function testModuleEnableOrder() {
\Drupal::service('module_installer')->install(['module_test'], FALSE);
$this->resetAll();
$this->assertModules(['module_test'], TRUE);
\Drupal::state()->set('module_test.dependency', 'dependency');
// module_test creates a dependency chain:
// - color depends on config
// - config depends on help
$expected_order = ['help', 'config', 'color'];
// Enable the modules through the UI, verifying that the dependency chain
// is correct.
$edit = [];
$edit['modules[color][enable]'] = 'color';
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->assertModules(['color'], FALSE);
// Note that dependencies are sorted alphabetically in the confirmation
// message.
$this->assertText(t('You must enable the Configuration Manager, Help modules to install Color.'));
$edit['modules[config][enable]'] = 'config';
$edit['modules[help][enable]'] = 'help';
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->assertModules(['color', 'config', 'help'], TRUE);
// Check the actual order which is saved by module_test_modules_enabled().
$module_order = \Drupal::state()->get('module_test.install_order') ?: [];
$this->assertIdentical($module_order, $expected_order);
}
/**
* Tests attempting to uninstall a module that has installed dependents.
*/
public function testUninstallDependents() {
// Enable the forum module.
$edit = ['modules[forum][enable]' => 'forum'];
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->drupalPostForm(NULL, [], t('Continue'));
$this->assertModules(['forum'], TRUE);
// Check that the comment module cannot be uninstalled.
$this->drupalGet('admin/modules/uninstall');
$checkbox = $this->xpath('//input[@type="checkbox" and @name="uninstall[comment]" and @disabled="disabled"]');
$this->assert(count($checkbox) == 1, 'Checkbox for uninstalling the comment module is disabled.');
// Delete any forum terms.
$vid = $this->config('forum.settings')->get('vocabulary');
// Ensure taxonomy has been loaded into the test-runner after forum was
// enabled.
\Drupal::moduleHandler()->load('taxonomy');
$terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]);
foreach ($terms as $term) {
$term->delete();
}
// Uninstall the forum module, and check that taxonomy now can also be
// uninstalled.
$edit = ['uninstall[forum]' => 'forum'];
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
// Uninstall comment module.
$edit = ['uninstall[comment]' => 'comment'];
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Drupal\Tests\system\Functional\Module;
/**
* Attempts enabling a module that fails hook_requirements('install').
*
* @group Module
*/
class HookRequirementsTest extends ModuleTestBase {
/**
* Assert that a module cannot be installed if it fails hook_requirements().
*/
public function testHookRequirementsFailure() {
$this->assertModules(['requirements1_test'], FALSE);
// Attempt to install the requirements1_test module.
$edit = [];
$edit['modules[requirements1_test][enable]'] = 'requirements1_test';
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// Makes sure the module was NOT installed.
$this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.');
$this->assertModules(['requirements1_test'], FALSE);
}
}

View file

@ -0,0 +1,359 @@
<?php
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\workspaces\Entity\Workspace;
/**
* Install/uninstall core module and confirm table creation/deletion.
*
* @group Module
*/
class InstallUninstallTest extends ModuleTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system_test', 'dblog', 'taxonomy', 'update_test_postupdate'];
/**
* Tests that a fixed set of modules can be installed and uninstalled.
*/
public function testInstallUninstall() {
// Set a variable so that the hook implementations in system_test.module
// will display messages via
// \Drupal\Core\Messenger\MessengerInterface::addStatus().
$this->container->get('state')->set('system_test.verbose_module_hooks', TRUE);
// Install and uninstall module_test to ensure hook_preinstall_module and
// hook_preuninstall_module are fired as expected.
$this->container->get('module_installer')->install(['module_test']);
$this->assertEqual($this->container->get('state')->get('system_test_preinstall_module'), 'module_test');
$this->container->get('module_installer')->uninstall(['module_test']);
$this->assertEqual($this->container->get('state')->get('system_test_preuninstall_module'), 'module_test');
$this->resetAll();
$all_modules = system_rebuild_module_data();
// Test help on required modules, but do not test uninstalling.
$required_modules = array_filter($all_modules, function ($module) {
if (!empty($module->info['required']) || $module->status == TRUE) {
if ($module->info['package'] != 'Testing' && empty($module->info['hidden'])) {
return TRUE;
}
}
return FALSE;
});
$required_modules['help'] = $all_modules['help'];
// Test uninstalling without hidden, required, and already enabled modules.
$all_modules = array_filter($all_modules, function ($module) {
if (!empty($module->info['hidden']) || !empty($module->info['required']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
return FALSE;
}
return TRUE;
});
// Install the Help module, and verify it installed successfully.
unset($all_modules['help']);
$this->assertModuleNotInstalled('help');
$edit = [];
$edit["modules[help][enable]"] = TRUE;
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->assertText('has been enabled', 'Modules status has been updated.');
$this->assertText(t('hook_modules_installed fired for help'));
$this->assertModuleSuccessfullyInstalled('help');
// Test help for the required modules.
foreach ($required_modules as $name => $module) {
$this->assertHelp($name, $module->info['name']);
}
// Go through each module in the list and try to install and uninstall
// it with its dependencies.
foreach ($all_modules as $name => $module) {
$was_installed_list = \Drupal::moduleHandler()->getModuleList();
// Start a list of modules that we expect to be installed this time.
$modules_to_install = [$name];
foreach (array_keys($module->requires) as $dependency) {
if (isset($all_modules[$dependency])) {
$modules_to_install[] = $dependency;
}
}
// Check that each module is not yet enabled and does not have any
// database tables yet.
foreach ($modules_to_install as $module_to_install) {
$this->assertModuleNotInstalled($module_to_install);
}
// Install the module.
$edit = [];
$package = $module->info['package'];
$edit['modules[' . $name . '][enable]'] = TRUE;
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// Handle experimental modules, which require a confirmation screen.
if ($package == 'Core (Experimental)') {
$this->assertText('Are you sure you wish to enable experimental modules?');
if (count($modules_to_install) > 1) {
// When there are experimental modules, needed dependencies do not
// result in the same page title, but there will be expected text
// indicating they need to be enabled.
$this->assertText('You must enable');
}
$this->drupalPostForm(NULL, [], t('Continue'));
}
// Handle the case where modules were installed along with this one and
// where we therefore hit a confirmation screen.
elseif (count($modules_to_install) > 1) {
// Verify that we are on the correct form and that the expected text
// about enabling dependencies appears.
$this->assertText('Some required modules must be enabled');
$this->assertText('You must enable');
$this->drupalPostForm(NULL, [], t('Continue'));
}
// List the module display names to check the confirmation message.
$module_names = [];
foreach ($modules_to_install as $module_to_install) {
$module_names[] = $all_modules[$module_to_install]->info['name'];
}
$expected_text = \Drupal::translation()->formatPlural(count($module_names), 'Module @name has been enabled.', '@count modules have been enabled: @names.', [
'@name' => $module_names[0],
'@names' => implode(', ', $module_names),
]);
$this->assertText($expected_text, 'Modules status has been updated.');
// Check that hook_modules_installed() was invoked with the expected list
// of modules, that each module's database tables now exist, and that
// appropriate messages appear in the logs.
foreach ($modules_to_install as $module_to_install) {
$this->assertText(t('hook_modules_installed fired for @module', ['@module' => $module_to_install]));
$this->assertLogMessage('system', "%module module installed.", ['%module' => $module_to_install], RfcLogLevel::INFO);
$this->assertInstallModuleUpdates($module_to_install);
$this->assertModuleSuccessfullyInstalled($module_to_install);
}
// Verify the help page.
$this->assertHelp($name, $module->info['name']);
// Uninstall the original module, plus everything else that was installed
// with it.
if ($name == 'forum') {
// Forum has an extra step to be able to uninstall it.
$this->preUninstallForum();
}
// Delete all workspaces before uninstall.
if ($name == 'workspaces') {
$workspaces = Workspace::loadMultiple();
\Drupal::entityTypeManager()->getStorage('workspace')->delete($workspaces);
}
$now_installed_list = \Drupal::moduleHandler()->getModuleList();
$added_modules = array_diff(array_keys($now_installed_list), array_keys($was_installed_list));
while ($added_modules) {
$initial_count = count($added_modules);
foreach ($added_modules as $to_uninstall) {
// See if we can currently uninstall this module (if its dependencies
// have been uninstalled), and do so if we can.
$this->drupalGet('admin/modules/uninstall');
$field_name = "uninstall[$to_uninstall]";
$has_checkbox = $this->xpath('//input[@type="checkbox" and @name="' . $field_name . '"]');
$disabled = $this->xpath('//input[@type="checkbox" and @name="' . $field_name . '" and @disabled="disabled"]');
if (!empty($has_checkbox) && empty($disabled)) {
// This one is eligible for being uninstalled.
$package = $all_modules[$to_uninstall]->info['package'];
$this->assertSuccessfulUninstall($to_uninstall, $package);
$added_modules = array_diff($added_modules, [$to_uninstall]);
}
}
// If we were not able to find a module to uninstall, fail and exit the
// loop.
$final_count = count($added_modules);
if ($initial_count == $final_count) {
$this->fail('Remaining modules could not be uninstalled for ' . $name);
break;
}
}
}
// Uninstall the help module and put it back into the list of modules.
$all_modules['help'] = $required_modules['help'];
$this->assertSuccessfulUninstall('help', $required_modules['help']->info['package']);
// Now that all modules have been tested, go back and try to enable them
// all again at once. This tests two things:
// - That each module can be successfully enabled again after being
// uninstalled.
// - That enabling more than one module at the same time does not lead to
// any errors.
$edit = [];
$experimental = FALSE;
foreach ($all_modules as $name => $module) {
$edit['modules[' . $name . '][enable]'] = TRUE;
// Track whether there is at least one experimental module.
if ($module->info['package'] == 'Core (Experimental)') {
$experimental = TRUE;
}
}
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// If there are experimental modules, click the confirm form.
if ($experimental) {
$this->assertText('Are you sure you wish to enable experimental modules?');
$this->drupalPostForm(NULL, [], t('Continue'));
}
// The string tested here is translatable but we are only using a part of it
// so using a translated string is wrong. Doing so would create a new string
// to translate.
$this->assertText(new FormattableMarkup('@count modules have been enabled: ', ['@count' => count($all_modules)]), 'Modules status has been updated.');
}
/**
* Asserts that a module is not yet installed.
*
* @param string $name
* Name of the module to check.
*/
protected function assertModuleNotInstalled($name) {
$this->assertModules([$name], FALSE);
$this->assertModuleTablesDoNotExist($name);
}
/**
* Asserts that a module was successfully installed.
*
* @param string $name
* Name of the module to check.
*/
protected function assertModuleSuccessfullyInstalled($name) {
$this->assertModules([$name], TRUE);
$this->assertModuleTablesExist($name);
$this->assertModuleConfig($name);
}
/**
* Uninstalls a module and asserts that it was done correctly.
*
* @param string $module
* The name of the module to uninstall.
* @param string $package
* (optional) The package of the module to uninstall. Defaults
* to 'Core'.
*/
protected function assertSuccessfulUninstall($module, $package = 'Core') {
$edit = [];
$edit['uninstall[' . $module . ']'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
$this->assertModules([$module], FALSE);
// Check that the appropriate hook was fired and the appropriate log
// message appears. (But don't check for the log message if the dblog
// module was just uninstalled, since the {watchdog} table won't be there
// anymore.)
$this->assertText(t('hook_modules_uninstalled fired for @module', ['@module' => $module]));
$this->assertLogMessage('system', "%module module uninstalled.", ['%module' => $module], RfcLogLevel::INFO);
// Check that the module's database tables no longer exist.
$this->assertModuleTablesDoNotExist($module);
// Check that the module's config files no longer exist.
$this->assertNoModuleConfig($module);
$this->assertUninstallModuleUpdates($module);
}
/**
* Asserts the module post update functions after install.
*
* @param string $module
* The module that got installed.
*/
protected function assertInstallModuleUpdates($module) {
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$all_update_functions = $post_update_registry->getPendingUpdateFunctions();
$empty_result = TRUE;
foreach ($all_update_functions as $function) {
list($function_module,) = explode('_post_update_', $function);
if ($module === $function_module) {
$empty_result = FALSE;
break;
}
}
$this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.');
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
switch ($module) {
case 'block':
$this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
break;
case 'update_test_postupdate':
$this->assertFalse(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates));
break;
}
}
/**
* Asserts the module post update functions after uninstall.
*
* @param string $module
* The module that got installed.
*/
protected function assertUninstallModuleUpdates($module) {
/** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
$post_update_registry = \Drupal::service('update.post_update_registry');
$all_update_functions = $post_update_registry->getPendingUpdateFunctions();
switch ($module) {
case 'block':
$this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.');
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
$this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.');
break;
}
}
/**
* Verifies a module's help.
*
* Verifies that the module help page from hook_help() exists and can be
* displayed, and that it contains the phrase "Foo Bar module", where "Foo
* Bar" is the name of the module from the .info.yml file.
*
* @param string $module
* Machine name of the module to verify.
* @param string $name
* Human-readable name of the module to verify.
*/
protected function assertHelp($module, $name) {
$this->drupalGet('admin/help/' . $module);
$this->assertResponse(200, "Help for $module displayed successfully");
$this->assertText($name . ' module', "'$name module' is on the help page for $module");
$this->assertLink('online documentation for the ' . $name . ' module', 0, "Correct online documentation link is in the help page for $module");
}
/**
* Deletes forum taxonomy terms, so Forum can be uninstalled.
*/
protected function preUninstallForum() {
// There only should be a 'General discussion' term in the 'forums'
// vocabulary, but just delete any terms there in case the name changes.
$query = \Drupal::entityQuery('taxonomy_term');
$query->condition('vid', 'forums');
$ids = $query->execute();
$storage = \Drupal::entityManager()->getStorage('taxonomy_term');
$terms = $storage->loadMultiple($ids);
$storage->delete($terms);
}
}

View file

@ -56,8 +56,9 @@ abstract class ModuleTestBase extends BrowserTestBase {
public function assertModuleTablesExist($module) {
$tables = array_keys(drupal_get_module_schema($module));
$tables_exist = TRUE;
$schema = Database::getConnection()->schema();
foreach ($tables as $table) {
if (!db_table_exists($table)) {
if (!$schema->tableExists($table)) {
$tables_exist = FALSE;
}
}
@ -73,8 +74,9 @@ abstract class ModuleTestBase extends BrowserTestBase {
public function assertModuleTablesDoNotExist($module) {
$tables = array_keys(drupal_get_module_schema($module));
$tables_exist = FALSE;
$schema = Database::getConnection()->schema();
foreach ($tables as $table) {
if (db_table_exists($table)) {
if ($schema->tableExists($table)) {
$tables_exist = TRUE;
}
}
@ -87,8 +89,10 @@ abstract class ModuleTestBase extends BrowserTestBase {
* @param string $module
* The name of the module.
*
* @return bool
* TRUE if configuration has been installed, FALSE otherwise.
* @return bool|null
* TRUE if configuration has been installed, FALSE otherwise. Returns NULL
* if the module configuration directory does not exist or does not contain
* any configuration files.
*/
public function assertModuleConfig($module) {
$module_config_dir = drupal_get_path('module', $module) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
@ -107,16 +111,18 @@ abstract class ModuleTestBase extends BrowserTestBase {
}
$this->assertTrue($all_names);
$module_config_dependencies = \Drupal::service('config.manager')->findConfigEntityDependents('module', [$module]);
// Look up each default configuration object name in the active
// configuration, and if it exists, remove it from the stack.
// Only default config that belongs to $module is guaranteed to exist; any
// other default config depends on whether other modules are enabled. Thus,
// list all default config once more, but filtered by $module.
$names = $module_file_storage->listAll($module . '.');
$names = $module_file_storage->listAll();
foreach ($names as $key => $name) {
if ($this->config($name)->get()) {
unset($names[$key]);
}
// All configuration in a module's config/install directory should depend
// on the module as it must be removed on uninstall or the module will not
// be re-installable.
$this->assertTrue(strpos($name, $module . '.') === 0 || isset($module_config_dependencies[$name]), "Configuration $name provided by $module in its config/install directory does not depend on it.");
}
// Verify that all configuration has been installed (which means that $names
// is empty).

View file

@ -0,0 +1,178 @@
<?php
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\taxonomy\Functional\TaxonomyTestTrait;
/**
* Tests that modules which provide entity types can be uninstalled.
*
* @group Module
*/
class PrepareUninstallTest extends BrowserTestBase {
use TaxonomyTestTrait;
/**
* An array of node objects.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* An array of taxonomy term objects.
*
* @var \Drupal\taxonomy\TermInterface[]
*/
protected $terms;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'taxonomy', 'entity_test'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($admin_user);
// Create 10 nodes.
for ($i = 1; $i <= 5; $i++) {
$this->nodes[] = $this->drupalCreateNode(['type' => 'page']);
$this->nodes[] = $this->drupalCreateNode(['type' => 'article']);
}
// Create 3 top-level taxonomy terms, each with 11 children.
$vocabulary = $this->createVocabulary();
for ($i = 1; $i <= 3; $i++) {
$term = $this->createTerm($vocabulary);
$this->terms[] = $term;
for ($j = 1; $j <= 11; $j++) {
$this->terms[] = $this->createTerm($vocabulary, ['parent' => ['target_id' => $term->id()]]);
}
}
}
/**
* Tests that Node and Taxonomy can be uninstalled.
*/
public function testUninstall() {
// Check that Taxonomy cannot be uninstalled yet.
$this->drupalGet('admin/modules/uninstall');
$this->assertText('Remove content items');
$this->assertLinkByHref('admin/modules/uninstall/entity/taxonomy_term');
// Delete Taxonomy term data.
$this->drupalGet('admin/modules/uninstall/entity/taxonomy_term');
$term_count = count($this->terms);
for ($i = 1; $i < 11; $i++) {
$this->assertText($this->terms[$term_count - $i]->label());
}
$term_count = $term_count - 10;
$this->assertText("And $term_count more taxonomy terms.");
$this->assertText('This action cannot be undone.');
$this->assertText('Make a backup of your database if you want to be able to restore these items.');
$this->drupalPostForm(NULL, [], t('Delete all taxonomy terms'));
// Check that we are redirected to the uninstall page and data has been
// removed.
$this->assertUrl('admin/modules/uninstall', []);
$this->assertText('All taxonomy terms have been deleted.');
// Check that there is no more data to be deleted, Taxonomy is ready to be
// uninstalled.
$this->assertText('Enables the categorization of content.');
$this->assertNoLinkByHref('admin/modules/uninstall/entity/taxonomy_term');
// Uninstall the Taxonomy module.
$this->drupalPostForm('admin/modules/uninstall', ['uninstall[taxonomy]' => TRUE], t('Uninstall'));
$this->drupalPostForm(NULL, [], t('Uninstall'));
$this->assertText('The selected modules have been uninstalled.');
$this->assertNoText('Enables the categorization of content.');
// Check Node cannot be uninstalled yet, there is content to be removed.
$this->drupalGet('admin/modules/uninstall');
$this->assertText('Remove content items');
$this->assertLinkByHref('admin/modules/uninstall/entity/node');
// Delete Node data.
$this->drupalGet('admin/modules/uninstall/entity/node');
// All 10 nodes should be listed.
foreach ($this->nodes as $node) {
$this->assertText($node->label());
}
// Ensures there is no more count when not necessary.
$this->assertNoText('And 0 more content');
$this->assertText('This action cannot be undone.');
$this->assertText('Make a backup of your database if you want to be able to restore these items.');
// Create another node so we have 11.
$this->nodes[] = $this->drupalCreateNode(['type' => 'page']);
$this->drupalGet('admin/modules/uninstall/entity/node');
// Ensures singular case is used when a single entity is left after listing
// the first 10's labels.
$this->assertText('And 1 more content item.');
// Create another node so we have 12.
$this->nodes[] = $this->drupalCreateNode(['type' => 'article']);
$this->drupalGet('admin/modules/uninstall/entity/node');
// Ensures singular case is used when a single entity is left after listing
// the first 10's labels.
$this->assertText('And 2 more content items.');
$this->drupalPostForm(NULL, [], t('Delete all content items'));
// Check we are redirected to the uninstall page and data has been removed.
$this->assertUrl('admin/modules/uninstall', []);
$this->assertText('All content items have been deleted.');
// Check there is no more data to be deleted, Node is ready to be
// uninstalled.
$this->assertText('Allows content to be submitted to the site and displayed on pages.');
$this->assertNoLinkByHref('admin/modules/uninstall/entity/node');
// Uninstall Node module.
$this->drupalPostForm('admin/modules/uninstall', ['uninstall[node]' => TRUE], t('Uninstall'));
$this->drupalPostForm(NULL, [], t('Uninstall'));
$this->assertText('The selected modules have been uninstalled.');
$this->assertNoText('Allows content to be submitted to the site and displayed on pages.');
// Ensure the proper response when accessing a non-existent entity type.
$this->drupalGet('admin/modules/uninstall/entity/node');
$this->assertResponse(404, 'Entity types that do not exist result in a 404.');
// Test an entity type which does not have any existing entities.
$this->drupalGet('admin/modules/uninstall/entity/entity_test_no_label');
$this->assertText('There are 0 entity test without label entities to delete.');
$button_xpath = '//input[@type="submit"][@value="Delete all entity test without label entities"]';
$this->assertNoFieldByXPath($button_xpath, NULL, 'Button with value "Delete all entity test without label entities" not found');
// Test an entity type without a label.
/** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
$storage = $this->container->get('entity.manager')
->getStorage('entity_test_no_label');
$storage->create([
'id' => mb_strtolower($this->randomMachineName()),
'name' => $this->randomMachineName(),
])->save();
$this->drupalGet('admin/modules/uninstall/entity/entity_test_no_label');
$this->assertText('This will delete 1 entity test without label.');
$this->assertFieldByXPath($button_xpath, NULL, 'Button with value "Delete all entity test without label entities" found');
$storage->create([
'id' => mb_strtolower($this->randomMachineName()),
'name' => $this->randomMachineName(),
])->save();
$this->drupalGet('admin/modules/uninstall/entity/entity_test_no_label');
$this->assertText('This will delete 2 entity test without label entities.');
}
}

View file

@ -3,7 +3,7 @@
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
@ -57,6 +57,17 @@ class UninstallTest extends BrowserTestBase {
$this->drupalGet('admin/modules/uninstall');
$this->assertTitle(t('Uninstall') . ' | Drupal');
foreach (\Drupal::service('extension.list.module')->getAllInstalledInfo() as $module => $info) {
$field_name = "uninstall[$module]";
if (!empty($info['required'])) {
// A required module should not be listed on the uninstall page.
$this->assertSession()->fieldNotExists($field_name);
}
else {
$this->assertSession()->fieldExists($field_name);
}
}
// Be sure labels are rendered properly.
// @see regression https://www.drupal.org/node/2512106
$this->assertRaw('<label for="edit-uninstall-node" class="module-name table-filter-text-source">Node</label>');
@ -103,7 +114,7 @@ class UninstallTest extends BrowserTestBase {
// cleared during the uninstall.
\Drupal::cache()->set('uninstall_test', 'test_uninstall_page', Cache::PERMANENT);
$cached = \Drupal::cache()->get('uninstall_test');
$this->assertEqual($cached->data, 'test_uninstall_page', SafeMarkup::format('Cache entry found: @bin', ['@bin' => $cached->data]));
$this->assertEqual($cached->data, 'test_uninstall_page', new FormattableMarkup('Cache entry found: @bin', ['@bin' => $cached->data]));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\Tests\system\Functional\Module;
/**
* Tests module version dependencies.
*
* @group Module
*/
class VersionTest extends ModuleTestBase {
/**
* Test version dependencies.
*/
public function testModuleVersions() {
$dependencies = [
// Alternating between being compatible and incompatible with 8.x-2.4-beta3.
// The first is always a compatible.
'common_test',
// Branch incompatibility.
'common_test (1.x)',
// Branch compatibility.
'common_test (2.x)',
// Another branch incompatibility.
'common_test (>2.x)',
// Another branch compatibility.
'common_test (<=2.x)',
// Another branch incompatibility.
'common_test (<2.x)',
// Another branch compatibility.
'common_test (>=2.x)',
// Nonsense, misses a dash. Incompatible with everything.
'common_test (=8.x2.x, >=2.4)',
// Core version is optional. Compatible.
'common_test (=8.x-2.x, >=2.4-alpha2)',
// Test !=, explicitly incompatible.
'common_test (=2.x, !=2.4-beta3)',
// Three operations. Compatible.
'common_test (=2.x, !=2.3, <2.5)',
// Testing extra version. Incompatible.
'common_test (<=2.4-beta2)',
// Testing extra version. Compatible.
'common_test (>2.4-beta2)',
// Testing extra version. Incompatible.
'common_test (>2.4-rc0)',
];
\Drupal::state()->set('system_test.dependencies', $dependencies);
$n = count($dependencies);
for ($i = 0; $i < $n; $i++) {
$this->drupalGet('admin/modules');
$checkbox = $this->xpath('//input[@id="edit-modules-module-test-enable"]');
$this->assertEqual(!empty($checkbox[0]->getAttribute('disabled')), $i % 2, $dependencies[$i]);
}
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\Tests\system\Functional\Page;
use Drupal\Tests\BrowserTestBase;
/**
* Tests default HTML metatags on a page.
*
* @group Page
*/
class DefaultMetatagsTest extends BrowserTestBase {
/**
* Tests meta tags.
*/
public function testMetaTag() {
$this->drupalGet('');
// Ensures that the charset metatag is on the page.
$result = $this->xpath('//meta[@charset="utf-8"]');
$this->assertEqual(count($result), 1);
// Ensure that the charset one is the first metatag.
$result = $this->xpath('//meta');
$this->assertEqual((string) $result[0]->getAttribute('charset'), 'utf-8');
// Ensure that the shortcut icon is on the page.
$result = $this->xpath('//link[@rel = "shortcut icon"]');
$this->assertEqual(count($result), 1, 'The shortcut icon is present.');
}
}

View file

@ -0,0 +1,333 @@
<?php
namespace Drupal\Tests\system\Functional\Pager;
use Behat\Mink\Element\NodeElement;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests pager functionality.
*
* @group Pager
*/
class PagerTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['dblog', 'pager_test'];
/**
* A user with permission to access site reports.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected $profile = 'testing';
protected function setUp() {
parent::setUp();
// Insert 300 log messages.
$logger = $this->container->get('logger.factory')->get('pager_test');
for ($i = 0; $i < 300; $i++) {
$logger->debug($this->randomString());
}
$this->adminUser = $this->drupalCreateUser([
'access site reports',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests markup and CSS classes of pager links.
*/
public function testActiveClass() {
// Verify first page.
$this->drupalGet('admin/reports/dblog');
$current_page = 0;
$this->assertPagerItems($current_page);
// Verify any page but first/last.
$current_page++;
$this->drupalGet('admin/reports/dblog', ['query' => ['page' => $current_page]]);
$this->assertPagerItems($current_page);
// Verify last page.
$elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--last']);
preg_match('@page=(\d+)@', $elements[0]->getAttribute('href'), $matches);
$current_page = (int) $matches[1];
$this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]->getAttribute('href'), ['external' => TRUE]);
$this->assertPagerItems($current_page);
}
/**
* Test proper functioning of the query parameters and the pager cache context.
*/
public function testPagerQueryParametersAndCacheContext() {
// First page.
$this->drupalGet('pager-test/query-parameters');
$this->assertText(t('Pager calls: 0'), 'Initial call to pager shows 0 calls.');
$this->assertText('[url.query_args.pagers:0]=0.0');
$this->assertCacheContext('url.query_args');
// Go to last page, the count of pager calls need to go to 1.
$elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--last']);
$elements[0]->click();
$this->assertText(t('Pager calls: 1'), 'First link call to pager shows 1 calls.');
$this->assertText('[url.query_args.pagers:0]=0.60');
$this->assertCacheContext('url.query_args');
// Reset counter to 0.
$this->drupalGet('pager-test/query-parameters');
// Go back to first page, the count of pager calls need to go to 2.
$elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--last']);
$elements[0]->click();
$elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--first']);
$elements[0]->click();
$this->assertText(t('Pager calls: 2'), 'Second link call to pager shows 2 calls.');
$this->assertText('[url.query_args.pagers:0]=0.0');
$this->assertCacheContext('url.query_args');
}
/**
* Test proper functioning of multiple pagers.
*/
public function testMultiplePagers() {
// First page.
$this->drupalGet('pager-test/multiple-pagers');
// Test data.
// Expected URL query string param is 0-indexed.
// Expected page per pager is 1-indexed.
$test_data = [
// With no query, all pagers set to first page.
[
'input_query' => NULL,
'expected_page' => [0 => '1', 1 => '1', 4 => '1'],
'expected_query' => '?page=0,0,,,0',
],
// Blanks around page numbers should not be relevant.
[
'input_query' => '?page=2 , 10,,, 5 ,,',
'expected_page' => [0 => '3', 1 => '11', 4 => '6'],
'expected_query' => '?page=2,10,,,5',
],
// Blanks within page numbers should lead to only the first integer
// to be considered.
[
'input_query' => '?page=2 , 3 0,,, 4 13 ,,',
'expected_page' => [0 => '3', 1 => '4', 4 => '5'],
'expected_query' => '?page=2,3,,,4',
],
// If floats are passed as page numbers, only the integer part is
// returned.
[
'input_query' => '?page=2.1,6.999,,,5.',
'expected_page' => [0 => '3', 1 => '7', 4 => '6'],
'expected_query' => '?page=2,6,,,5',
],
// Partial page fragment, undefined pagers set to first page.
[
'input_query' => '?page=5,2',
'expected_page' => [0 => '6', 1 => '3', 4 => '1'],
'expected_query' => '?page=5,2,,,0',
],
// Partial page fragment, undefined pagers set to first page.
[
'input_query' => '?page=,2',
'expected_page' => [0 => '1', 1 => '3', 4 => '1'],
'expected_query' => '?page=0,2,,,0',
],
// Partial page fragment, undefined pagers set to first page.
[
'input_query' => '?page=,',
'expected_page' => [0 => '1', 1 => '1', 4 => '1'],
'expected_query' => '?page=0,0,,,0',
],
// With overflow pages, all pagers set to max page.
[
'input_query' => '?page=99,99,,,99',
'expected_page' => [0 => '16', 1 => '16', 4 => '16'],
'expected_query' => '?page=15,15,,,15',
],
// Wrong value for the page resets pager to first page.
[
'input_query' => '?page=bar,5,foo,qux,bet',
'expected_page' => [0 => '1', 1 => '6', 4 => '1'],
'expected_query' => '?page=0,5,,,0',
],
];
// We loop through the page with the test data query parameters, and check
// that the active page for each pager element has the expected page
// (1-indexed) and resulting query parameter
foreach ($test_data as $data) {
$input_query = str_replace(' ', '%20', $data['input_query']);
$this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $input_query, ['external' => TRUE]);
foreach ([0, 1, 4] as $pager_element) {
$active_page = $this->cssSelect("div.test-pager-{$pager_element} ul.pager__items li.is-active:contains('{$data['expected_page'][$pager_element]}')");
$destination = str_replace('%2C', ',', $active_page[0]->find('css', 'a')->getAttribute('href'));
$this->assertEqual($destination, $data['expected_query']);
}
}
}
/**
* Test proper functioning of the ellipsis.
*/
public function testPagerEllipsis() {
// Insert 100 extra log messages to get 9 pages.
$logger = $this->container->get('logger.factory')->get('pager_test');
for ($i = 0; $i < 100; $i++) {
$logger->debug($this->randomString());
}
$this->drupalGet('admin/reports/dblog');
$elements = $this->cssSelect(".pager__item--ellipsis:contains('…')");
$this->assertEqual(count($elements), 0, 'No ellipsis has been set.');
// Insert an extra 50 log messages to get 10 pages.
$logger = $this->container->get('logger.factory')->get('pager_test');
for ($i = 0; $i < 50; $i++) {
$logger->debug($this->randomString());
}
$this->drupalGet('admin/reports/dblog');
$elements = $this->cssSelect(".pager__item--ellipsis:contains('…')");
$this->assertEqual(count($elements), 1, 'Found the ellipsis.');
}
/**
* Asserts pager items and links.
*
* @param int $current_page
* The current pager page the internal browser is on.
*/
protected function assertPagerItems($current_page) {
$elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
$this->assertTrue(!empty($elements), 'Pager found.');
// Make current page 1-based.
$current_page++;
// Extract first/previous and next/last items.
// first/previous only exist, if the current page is not the first.
if ($current_page > 1) {
$first = array_shift($elements);
$previous = array_shift($elements);
}
// next/last always exist, unless the current page is the last.
if ($current_page != count($elements)) {
$last = array_pop($elements);
$next = array_pop($elements);
}
// We remove elements from the $elements array in the following code, so
// we store the total number of pages for verifying the "last" link.
$total_pages = count($elements);
// Verify items and links to pages.
foreach ($elements as $page => $element) {
// Make item/page index 1-based.
$page++;
if ($current_page == $page) {
$this->assertClass($element, 'is-active', 'Element for current page has .is-active class.');
$link = $element->find('css', 'a');
$this->assertTrue($link, 'Element for current page has link.');
$destination = $link->getAttribute('href');
// URL query string param is 0-indexed.
$this->assertEqual($destination, '?page=' . ($page - 1));
}
else {
$this->assertNoClass($element, 'is-active', "Element for page $page has no .is-active class.");
$this->assertClass($element, 'pager__item', "Element for page $page has .pager__item class.");
$link = $element->find('css', 'a');
$this->assertTrue($link, "Link to page $page found.");
$destination = $link->getAttribute('href');
$this->assertEqual($destination, '?page=' . ($page - 1));
}
unset($elements[--$page]);
}
// Verify that no other items remain untested.
$this->assertTrue(empty($elements), 'All expected items found.');
// Verify first/previous and next/last items and links.
if (isset($first)) {
$this->assertClass($first, 'pager__item--first', 'Element for first page has .pager__item--first class.');
$link = $first->find('css', 'a');
$this->assertTrue($link, 'Link to first page found.');
$this->assertNoClass($link, 'is-active', 'Link to first page is not active.');
$destination = $link->getAttribute('href');
$this->assertEqual($destination, '?page=0');
}
if (isset($previous)) {
$this->assertClass($previous, 'pager__item--previous', 'Element for first page has .pager__item--previous class.');
$link = $previous->find('css', 'a');
$this->assertTrue($link, 'Link to previous page found.');
$this->assertNoClass($link, 'is-active', 'Link to previous page is not active.');
$destination = $link->getAttribute('href');
// URL query string param is 0-indexed, $current_page is 1-indexed.
$this->assertEqual($destination, '?page=' . ($current_page - 2));
}
if (isset($next)) {
$this->assertClass($next, 'pager__item--next', 'Element for next page has .pager__item--next class.');
$link = $next->find('css', 'a');
$this->assertTrue($link, 'Link to next page found.');
$this->assertNoClass($link, 'is-active', 'Link to next page is not active.');
$destination = $link->getAttribute('href');
// URL query string param is 0-indexed, $current_page is 1-indexed.
$this->assertEqual($destination, '?page=' . $current_page);
}
if (isset($last)) {
$link = $last->find('css', 'a');
$this->assertClass($last, 'pager__item--last', 'Element for last page has .pager__item--last class.');
$this->assertTrue($link, 'Link to last page found.');
$this->assertNoClass($link, 'is-active', 'Link to last page is not active.');
$destination = $link->getAttribute('href');
// URL query string param is 0-indexed.
$this->assertEqual($destination, '?page=' . ($total_pages - 1));
}
}
/**
* Asserts that an element has a given class.
*
* @param \Behat\Mink\Element\NodeElement $element
* The element to test.
* @param string $class
* The class to assert.
* @param string $message
* (optional) A verbose message to output.
*/
protected function assertClass(NodeElement $element, $class, $message = NULL) {
if (!isset($message)) {
$message = "Class .$class found.";
}
$this->assertTrue($element->hasClass($class) !== FALSE, $message);
}
/**
* Asserts that an element does not have a given class.
*
* @param \Behat\Mink\Element\NodeElement $element
* The element to test.
* @param string $class
* The class to assert.
* @param string $message
* (optional) A verbose message to output.
*/
protected function assertNoClass(NodeElement $element, $class, $message = NULL) {
if (!isset($message)) {
$message = "Class .$class not found.";
}
$this->assertTrue($element->hasClass($class) === FALSE, $message);
}
}

View file

@ -67,9 +67,9 @@ class AjaxPageStateTest extends BrowserTestBase {
"query" =>
[
'ajax_page_state' => [
'libraries' => 'core/html5shiv'
]
]
'libraries' => 'core/html5shiv',
],
],
]
);
$this->assertNoRaw(

View file

@ -0,0 +1,144 @@
<?php
namespace Drupal\Tests\system\Functional\Render;
use Drupal\Tests\BrowserTestBase;
/**
* Functional tests for HtmlResponseAttachmentsProcessor.
*
* @group Render
*/
class HtmlResponseAttachmentsTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['render_attached_test'];
/**
* Test rendering of ['#attached'].
*/
public function testAttachments() {
// Test ['#attached']['http_header] = ['Status', $code].
$this->drupalGet('/render_attached_test/teapot');
$this->assertResponse(418);
$this->assertHeader('X-Drupal-Cache', 'MISS');
// Repeat for the cache.
$this->drupalGet('/render_attached_test/teapot');
$this->assertResponse(418);
$this->assertHeader('X-Drupal-Cache', 'HIT');
// Test ['#attached']['http_header'] with various replacement rules.
$this->drupalGet('/render_attached_test/header');
$this->assertTeapotHeaders();
$this->assertHeader('X-Drupal-Cache', 'MISS');
// Repeat for the cache.
$this->drupalGet('/render_attached_test/header');
$this->assertHeader('X-Drupal-Cache', 'HIT');
// Test ['#attached']['feed'].
$this->drupalGet('/render_attached_test/feed');
$this->assertHeader('X-Drupal-Cache', 'MISS');
$this->assertFeed();
// Repeat for the cache.
$this->drupalGet('/render_attached_test/feed');
$this->assertHeader('X-Drupal-Cache', 'HIT');
// Test ['#attached']['html_head'].
$this->drupalGet('/render_attached_test/head');
$this->assertHeader('X-Drupal-Cache', 'MISS');
$this->assertHead();
// Repeat for the cache.
$this->drupalGet('/render_attached_test/head');
$this->assertHeader('X-Drupal-Cache', 'HIT');
// Test ['#attached']['html_head_link'] when outputted as HTTP header.
$this->drupalGet('/render_attached_test/html_header_link');
$expected_link_headers = [
'</foo?bar=&lt;baz&gt;&amp;baz=false>; rel="alternate"',
'</foo/bar>; hreflang="nl"; rel="alternate"',
];
$this->assertEqual($this->getSession()->getResponseHeaders()['Link'], $expected_link_headers);
}
/**
* Test caching of ['#attached'].
*/
public function testRenderCachedBlock() {
// Make sure our test block is visible.
$this->drupalPlaceBlock('attached_rendering_block', ['region' => 'content']);
// Get the front page, which should now have our visible block.
$this->drupalGet('');
// Make sure our block is visible.
$this->assertText('Markup from attached_rendering_block.');
// Test that all our attached items are present.
$this->assertFeed();
$this->assertHead();
$this->assertResponse(418);
$this->assertTeapotHeaders();
// Reload the page, to test caching.
$this->drupalGet('');
// Make sure our block is visible.
$this->assertText('Markup from attached_rendering_block.');
// The header should be present again.
$this->assertHeader('X-Test-Teapot', 'Teapot Mode Active');
}
/**
* Helper function to make assertions about added HTTP headers.
*/
protected function assertTeapotHeaders() {
$headers = $this->getSession()->getResponseHeaders();
$this->assertEquals($headers['X-Test-Teapot'], ['Teapot Mode Active']);
$this->assertEquals($headers['X-Test-Teapot-Replace'], ['Teapot replaced']);
$this->assertEquals($headers['X-Test-Teapot-No-Replace'], ['This value is not replaced', 'This one is added']);
}
/**
* Helper function to make assertions about the presence of an RSS feed.
*/
protected function assertFeed() {
// Discover the DOM element for the feed link.
$test_meta = $this->xpath('//head/link[@href="test://url"]');
$this->assertEqual(1, count($test_meta), 'Link has URL.');
// Reconcile the other attributes.
$test_meta_attributes = [
'href' => 'test://url',
'rel' => 'alternate',
'type' => 'application/rss+xml',
'title' => 'Your RSS feed.',
];
$test_meta = reset($test_meta);
if (empty($test_meta)) {
$this->fail('Unable to find feed link.');
}
else {
foreach ($test_meta_attributes as $attribute => $value) {
$this->assertEquals($value, $test_meta->getAttribute($attribute));
}
}
}
/**
* Helper function to make assertions about HTML head elements.
*/
protected function assertHead() {
// Discover the DOM element for the meta link.
$test_meta = $this->xpath('//head/meta[@test-attribute="testvalue"]');
$this->assertEqual(1, count($test_meta), 'There\'s only one test attribute.');
// Grab the only DOM element.
$test_meta = reset($test_meta);
if (empty($test_meta)) {
$this->fail('Unable to find the head meta.');
}
else {
$this->assertEqual($test_meta->getAttribute('test-attribute'), 'testvalue');
}
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\system\Functional\Render;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Functional test verifying that render array throws 406 for non-HTML requests.
*
* @group Render
*/
class RenderArrayNonHtmlSubscriberTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['render_array_non_html_subscriber_test'];
/**
* Tests handling of responses by events subscriber.
*/
public function testResponses() {
// Test that event subscriber does not interfere with normal requests.
$url = Url::fromRoute('render_array_non_html_subscriber_test.render_array');
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(200);
$this->assertRaw(t('Controller response successfully rendered.'));
// Test that correct response code is returned for any non-HTML format.
foreach (['json', 'hal+json', 'xml', 'foo'] as $format) {
$url = Url::fromRoute('render_array_non_html_subscriber_test.render_array', [
'_format' => $format,
]);
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(406);
$this->assertNoRaw(t('Controller response successfully rendered.'));
}
// Test that event subscriber does not interfere with raw string responses.
$url = Url::fromRoute('render_array_non_html_subscriber_test.raw_string', [
'_format' => 'foo',
]);
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(200);
$this->assertRaw(t('Raw controller response.'));
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\system\Functional\Render;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests that URL bubbleable metadata is correctly bubbled.
*
* @group Render
*/
class UrlBubbleableMetadataBubblingTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['cache_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
}
/**
* Tests that URL bubbleable metadata is correctly bubbled.
*/
public function testUrlBubbleableMetadataBubbling() {
// Test that regular URLs bubble up bubbleable metadata when converted to
// string.
$url = Url::fromRoute('cache_test.url_bubbling');
$this->drupalGet($url);
$this->assertCacheContext('url.site');
$this->assertRaw($url->setAbsolute()->toString());
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\system\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ActionJsonAnonTest extends ActionResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\system\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ActionJsonBasicAuthTest extends ActionResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\system\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ActionJsonCookieTest extends ActionResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\system\Functional\Rest;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\system\Entity\Action;
use Drupal\user\RoleInterface;
abstract class ActionResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'action';
/**
* @var \Drupal\system\ActionConfigEntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer actions']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$action = Action::create([
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
'type' => 'user',
'label' => t('Add the anonymous role to the selected users'),
'configuration' => [
'rid' => RoleInterface::ANONYMOUS_ID,
],
'plugin' => 'user_add_role_action',
]);
$action->save();
return $action;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'configuration' => [
'rid' => 'anonymous',
],
'dependencies' => [
'config' => ['user.role.anonymous'],
'module' => ['user'],
],
'id' => 'user_add_role_action.anonymous',
'label' => 'Add the anonymous role to the selected users',
'langcode' => 'en',
'plugin' => 'user_add_role_action',
'status' => TRUE,
'type' => 'user',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return [
'user.permissions',
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

Some files were not shown because too many files have changed in this diff Show more