Initial Drupal 11 with DDEV setup
This commit is contained in:
		@ -0,0 +1,338 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Asset;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\UrlHelper;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore abcdefghijklmnop
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests asset aggregation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group asset
 | 
			
		||||
 */
 | 
			
		||||
class AssetOptimizationTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The file assets path settings value.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fileAssetsPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that asset aggregates are rendered and created on disk.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAssetAggregation(): void {
 | 
			
		||||
    // Test aggregation with a custom file_assets_path.
 | 
			
		||||
    $this->fileAssetsPath = $this->publicFilesDirectory . '/test-assets';
 | 
			
		||||
    $settings['settings']['file_assets_path'] = (object) [
 | 
			
		||||
      'value' => $this->fileAssetsPath,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestAggregation($settings);
 | 
			
		||||
 | 
			
		||||
    // Test aggregation with no configured file_assets_path or file_public_path,
 | 
			
		||||
    // since tests run in a multisite, this tests multisite installs where
 | 
			
		||||
    // settings.php is the default.
 | 
			
		||||
    $this->fileAssetsPath = $this->publicFilesDirectory;
 | 
			
		||||
    $settings['settings']['file_public_path'] = (object) [
 | 
			
		||||
      'value' => NULL,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $settings['settings']['file_assets_path'] = (object) [
 | 
			
		||||
      'value' => NULL,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestAggregation($settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a user and requests a page.
 | 
			
		||||
   */
 | 
			
		||||
  protected function requestPage(): void {
 | 
			
		||||
    $user = $this->createUser();
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper to test aggregate file URLs.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $settings
 | 
			
		||||
   *   A settings array to pass to ::writeSettings()
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestAggregation(array $settings): void {
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
    $this->rebuildAll();
 | 
			
		||||
    $this->config('system.performance')->set('css', [
 | 
			
		||||
      'preprocess' => TRUE,
 | 
			
		||||
      'gzip' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->config('system.performance')->set('js', [
 | 
			
		||||
      'preprocess' => TRUE,
 | 
			
		||||
      'gzip' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->requestPage();
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    $page = $session->getPage();
 | 
			
		||||
 | 
			
		||||
    // Collect all the URLs for all the script and styles prior to making any
 | 
			
		||||
    // more requests.
 | 
			
		||||
    $style_elements = $page->findAll('xpath', '//link[@href and @rel="stylesheet"]');
 | 
			
		||||
    $script_elements = $page->findAll('xpath', '//script[@src]');
 | 
			
		||||
    $style_urls = [];
 | 
			
		||||
    foreach ($style_elements as $element) {
 | 
			
		||||
      $style_urls[] = $element->getAttribute('href');
 | 
			
		||||
    }
 | 
			
		||||
    $script_urls = [];
 | 
			
		||||
    foreach ($script_elements as $element) {
 | 
			
		||||
      $script_urls[] = $element->getAttribute('src');
 | 
			
		||||
    }
 | 
			
		||||
    foreach ($style_urls as $url) {
 | 
			
		||||
      $this->assertAggregate($url, TRUE, 'text/css');
 | 
			
		||||
      // Once the file has been requested once, it's on disk. It is possible for
 | 
			
		||||
      // a second request to hit the controller, and then find that another
 | 
			
		||||
      // request has created the file already. Actually simulating this race
 | 
			
		||||
      // condition is not really possible since it relies on timing. However, by
 | 
			
		||||
      // changing the case of the part of the URL that is handled by Drupal
 | 
			
		||||
      // routing, we can force the request to be served by Drupal.
 | 
			
		||||
      $this->assertAggregate(str_replace($this->fileAssetsPath, strtoupper($this->fileAssetsPath), $url), TRUE, 'text/css');
 | 
			
		||||
      $this->assertAggregate($url, FALSE, 'text/css');
 | 
			
		||||
      $this->assertInvalidAggregates($url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    foreach ($script_urls as $url) {
 | 
			
		||||
      $this->assertAggregate($url);
 | 
			
		||||
      $this->assertAggregate($url, FALSE);
 | 
			
		||||
      $this->assertInvalidAggregates($url);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the aggregate header.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   * @param bool $from_php
 | 
			
		||||
   *   (optional) Is the result from PHP or disk? Defaults to TRUE (PHP).
 | 
			
		||||
   * @param string|null $content_type
 | 
			
		||||
   *   The expected content type, or NULL to skip checking.
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertAggregate(string $url, bool $from_php = TRUE, ?string $content_type = NULL): void {
 | 
			
		||||
    $url = $this->getAbsoluteUrl($url);
 | 
			
		||||
    if (!stripos($url, $this->fileAssetsPath) !== FALSE) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    $session->visit($url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $headers = $session->getResponseHeaders();
 | 
			
		||||
    if (isset($content_type)) {
 | 
			
		||||
      $this->assertStringContainsString($content_type, $headers['Content-Type'][0]);
 | 
			
		||||
    }
 | 
			
		||||
    if ($from_php) {
 | 
			
		||||
      $this->assertStringContainsString('no-store', $headers['Cache-Control'][0]);
 | 
			
		||||
      $this->assertArrayHasKey('X-Generator', $headers);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertArrayNotHasKey('X-Generator', $headers);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the aggregate when it is invalid.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @throws \Behat\Mink\Exception\ExpectationException
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertInvalidAggregates(string $url): void {
 | 
			
		||||
    $url = $this->getAbsoluteUrl($url);
 | 
			
		||||
    // Not every script or style on a page is aggregated.
 | 
			
		||||
    if (!str_contains($url, $this->fileAssetsPath)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    $session->visit($this->replaceGroupDelta($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->omitTheme($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(400);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->omitInclude($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(400);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->invalidInclude($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(400);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->invalidExclude($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(400);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->replaceFileNamePrefix($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(400);
 | 
			
		||||
 | 
			
		||||
    $session->visit($this->setInvalidLibrary($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // When an invalid asset hash name is given.
 | 
			
		||||
    $session->visit($this->replaceGroupHash($url));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $current_url = $session->getCurrentUrl();
 | 
			
		||||
    // Redirect to the correct one.
 | 
			
		||||
    $this->assertEquals($url, $current_url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Replaces the delta in the given URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the delta replaced.
 | 
			
		||||
   */
 | 
			
		||||
  protected function replaceGroupDelta(string $url): string {
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    $parts['query']['delta'] = 100;
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Replaces the group hash in the given URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the group hash replaced.
 | 
			
		||||
   */
 | 
			
		||||
  protected function replaceGroupHash(string $url): string {
 | 
			
		||||
    $parts = explode('_', $url, 2);
 | 
			
		||||
    $hash = strtok($parts[1], '.');
 | 
			
		||||
    $parts[1] = str_replace($hash, 'abcdefghijklmnop', $parts[1]);
 | 
			
		||||
    return $this->getAbsoluteUrl(implode('_', $parts));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Replaces the filename prefix in the given URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the file name prefix replaced.
 | 
			
		||||
   */
 | 
			
		||||
  protected function replaceFileNamePrefix(string $url): string {
 | 
			
		||||
    return str_replace(['/css_', '/js_'], '/xyz_', $url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Replaces the 'include' entry in the given URL with an invalid value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the 'include' query set to an invalid value.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setInvalidLibrary(string $url): string {
 | 
			
		||||
    // First replace the hash, so we don't get served the actual file on disk.
 | 
			
		||||
    $url = $this->replaceGroupHash($url);
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    $include = explode(',', UrlHelper::uncompressQueryParameter($parts['query']['include']));
 | 
			
		||||
    $include[] = 'system/llama';
 | 
			
		||||
    $parts['query']['include'] = UrlHelper::compressQueryParameter(implode(',', $include));
 | 
			
		||||
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Removes the 'theme' query parameter from the given URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the 'theme' omitted.
 | 
			
		||||
   */
 | 
			
		||||
  protected function omitTheme(string $url): string {
 | 
			
		||||
    // First replace the hash, so we don't get served the actual file on disk.
 | 
			
		||||
    $url = $this->replaceGroupHash($url);
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    unset($parts['query']['theme']);
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Removes the 'include' query parameter from the given URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with the 'include' parameter omitted.
 | 
			
		||||
   */
 | 
			
		||||
  protected function omitInclude(string $url): string {
 | 
			
		||||
    // First replace the hash, so we don't get served the actual file on disk.
 | 
			
		||||
    $url = $this->replaceGroupHash($url);
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    unset($parts['query']['include']);
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Replaces the 'include' query parameter with an invalid value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with 'include' set to an arbitrary string.
 | 
			
		||||
   */
 | 
			
		||||
  protected function invalidInclude(string $url): string {
 | 
			
		||||
    // First replace the hash, so we don't get served the actual file on disk.
 | 
			
		||||
    $url = $this->replaceGroupHash($url);
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    $parts['query']['include'] = 'abcdefghijklmnop';
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds an invalid 'exclude' query parameter with an invalid value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $url
 | 
			
		||||
   *   The source URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The URL with 'exclude' set to an arbitrary string.
 | 
			
		||||
   */
 | 
			
		||||
  protected function invalidExclude(string $url): string {
 | 
			
		||||
    // First replace the hash, so we don't get served the actual file on disk.
 | 
			
		||||
    $url = $this->replaceGroupHash($url);
 | 
			
		||||
    $parts = UrlHelper::parse($url);
 | 
			
		||||
    $parts['query']['exclude'] = 'abcdefghijklmnop';
 | 
			
		||||
    $query = UrlHelper::buildQuery($parts['query']);
 | 
			
		||||
    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Asset;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests asset aggregation with the Umami install profile.
 | 
			
		||||
 *
 | 
			
		||||
 * Umami includes several core modules as well as the Claro theme, this
 | 
			
		||||
 * results in a more complex asset dependency tree to test than the testing
 | 
			
		||||
 * profile.
 | 
			
		||||
 *
 | 
			
		||||
 * @group asset
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class AssetOptimizationUmamiTest extends AssetOptimizationTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'demo_umami';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function requestPage(): void {
 | 
			
		||||
    $user = $this->createUser([], NULL, TRUE);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,102 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Asset;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests asset aggregation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group asset
 | 
			
		||||
 */
 | 
			
		||||
class UnversionedAssetTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The file assets path settings value.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fileAssetsPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'unversioned_assets_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that unversioned assets cause a new filename when changed.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnversionedAssets(): void {
 | 
			
		||||
    $this->fileAssetsPath = $this->publicFilesDirectory;
 | 
			
		||||
    file_put_contents('public://test.css', '.original-content{display:none;}');
 | 
			
		||||
    // Test aggregation with a custom file_assets_path.
 | 
			
		||||
    $this->config('system.performance')->set('css', [
 | 
			
		||||
      'preprocess' => TRUE,
 | 
			
		||||
      'gzip' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->config('system.performance')->set('js', [
 | 
			
		||||
      'preprocess' => TRUE,
 | 
			
		||||
      'gzip' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    // Ensure that the library discovery cache is empty before the page is
 | 
			
		||||
    // requested and that updated asset URLs are rendered.
 | 
			
		||||
    \Drupal::service('cache.data')->deleteAll();
 | 
			
		||||
    \Drupal::service('cache.page')->deleteAll();
 | 
			
		||||
    $this->drupalGet('<front>');
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    $page = $session->getPage();
 | 
			
		||||
 | 
			
		||||
    $style_elements = $page->findAll('xpath', '//link[@href and @rel="stylesheet"]');
 | 
			
		||||
    $this->assertNotEmpty($style_elements);
 | 
			
		||||
    $href = NULL;
 | 
			
		||||
    foreach ($style_elements as $element) {
 | 
			
		||||
      $href = $element->getAttribute('href');
 | 
			
		||||
      $url = $this->getAbsoluteUrl($href);
 | 
			
		||||
      // Not every script or style on a page is aggregated.
 | 
			
		||||
      if (!str_contains($url, $this->fileAssetsPath)) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      $session = $this->getSession();
 | 
			
		||||
      $session->visit($url);
 | 
			
		||||
      $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
      $aggregate = $session = $session->getPage()->getContent();
 | 
			
		||||
      $this->assertStringContainsString('original-content', $aggregate);
 | 
			
		||||
      $this->assertStringNotContainsString('extra-stuff', $aggregate);
 | 
			
		||||
    }
 | 
			
		||||
    $file = file_get_contents('public://test.css') . '.extra-stuff{display:none;}';
 | 
			
		||||
    file_put_contents('public://test.css', $file);
 | 
			
		||||
    // Clear the library discovery and page caches again so that new URLs are
 | 
			
		||||
    // generated.
 | 
			
		||||
    \Drupal::service('cache.data')->deleteAll();
 | 
			
		||||
    \Drupal::service('cache.page')->deleteAll();
 | 
			
		||||
    $this->drupalGet('<front>');
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    $page = $session->getPage();
 | 
			
		||||
    $style_elements = $page->findAll('xpath', '//link[@href and @rel="stylesheet"]');
 | 
			
		||||
    $this->assertNotEmpty($style_elements);
 | 
			
		||||
    foreach ($style_elements as $element) {
 | 
			
		||||
      $new_href = $element->getAttribute('href');
 | 
			
		||||
      $this->assertNotSame($new_href, $href);
 | 
			
		||||
      $url = $this->getAbsoluteUrl($new_href);
 | 
			
		||||
      // Not every script or style on a page is aggregated.
 | 
			
		||||
      if (!str_contains($url, $this->fileAssetsPath)) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      $session = $this->getSession();
 | 
			
		||||
      $session->visit($url);
 | 
			
		||||
      $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
      $aggregate = $session = $session->getPage()->getContent();
 | 
			
		||||
      $this->assertStringContainsString('original-content', $aggregate);
 | 
			
		||||
      $this->assertStringContainsString('extra-stuff', $aggregate);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Bootstrap;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\DependencyInjection\Container;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Container base class which triggers an error.
 | 
			
		||||
 */
 | 
			
		||||
class ErrorContainer extends Container {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): ?object {
 | 
			
		||||
    if ($id === 'http_kernel') {
 | 
			
		||||
      // Enforce a recoverable error.
 | 
			
		||||
      $callable = function (ErrorContainer $container) {
 | 
			
		||||
      };
 | 
			
		||||
      return $callable(1);
 | 
			
		||||
    }
 | 
			
		||||
    return parent::get($id, $invalidBehavior);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Bootstrap;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\DependencyInjection\Container;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base container which throws an exception.
 | 
			
		||||
 */
 | 
			
		||||
class ExceptionContainer extends Container {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): ?object {
 | 
			
		||||
    if ($id === 'http_kernel') {
 | 
			
		||||
      throw new \Exception('Thrown exception during Container::get');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return parent::get($id, $invalidBehavior);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,310 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Bootstrap;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Render\FormattableMarkup;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests kernel panic when things are really messed up.
 | 
			
		||||
 *
 | 
			
		||||
 * @group system
 | 
			
		||||
 */
 | 
			
		||||
class UncaughtExceptionTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Exceptions thrown by site under test that contain this text are ignored.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $expectedExceptionMessage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['error_service_test', 'error_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $settings_filename = $this->siteDirectory . '/settings.php';
 | 
			
		||||
    chmod($settings_filename, 0777);
 | 
			
		||||
    $settings_php = file_get_contents($settings_filename);
 | 
			
		||||
    $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ErrorContainer.php';\n";
 | 
			
		||||
    $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ExceptionContainer.php';\n";
 | 
			
		||||
    // Ensure we can test errors rather than being caught in
 | 
			
		||||
    // \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware.
 | 
			
		||||
    $settings_php .= "\ndefine('SIMPLETEST_COLLECT_ERRORS', FALSE);\n";
 | 
			
		||||
    file_put_contents($settings_filename, $settings_php);
 | 
			
		||||
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['config']['system.logging']['error_level'] = (object) [
 | 
			
		||||
      'value' => ERROR_REPORTING_DISPLAY_VERBOSE,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests uncaught exception handling when system is in a bad state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUncaughtException(): void {
 | 
			
		||||
    $this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.';
 | 
			
		||||
    \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
 | 
			
		||||
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['config']['system.logging']['error_level'] = (object) [
 | 
			
		||||
      'value' => ERROR_REPORTING_HIDE,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
    $this->assertSession()->pageTextContains('The website encountered an unexpected error. Try again later.');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains($this->expectedExceptionMessage);
 | 
			
		||||
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['config']['system.logging']['error_level'] = (object) [
 | 
			
		||||
      'value' => ERROR_REPORTING_DISPLAY_ALL,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
    $this->assertSession()->pageTextContains('The website encountered an unexpected error. Try again later.');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
 | 
			
		||||
    $this->assertErrorLogged($this->expectedExceptionMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests displaying an uncaught fatal error.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUncaughtFatalError(): void {
 | 
			
		||||
    if (PHP_VERSION_ID >= 80400) {
 | 
			
		||||
      $fatal_error = [
 | 
			
		||||
        '%type' => 'TypeError',
 | 
			
		||||
        '@message' => 'Drupal\error_test\Controller\ErrorTestController::{closure:Drupal\error_test\Controller\ErrorTestController::generateFatalErrors():64}(): Argument #1 ($test) must be of type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 67',
 | 
			
		||||
        '%function' => 'Drupal\error_test\Controller\ErrorTestController->{closure:Drupal\error_test\Controller\ErrorTestController::generateFatalErrors():64}()',
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $fatal_error = [
 | 
			
		||||
        '%type' => 'TypeError',
 | 
			
		||||
        '@message' => 'Drupal\error_test\Controller\ErrorTestController::Drupal\error_test\Controller\{closure}(): Argument #1 ($test) must be of type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 67',
 | 
			
		||||
        '%function' => 'Drupal\error_test\Controller\ErrorTestController->Drupal\error_test\Controller\{closure}()',
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    $this->drupalGet('error-test/generate-fatal-errors');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
    $message = new FormattableMarkup('%type: @message in %function (line ', $fatal_error);
 | 
			
		||||
    $this->assertSession()->responseContains((string) $message);
 | 
			
		||||
    $this->assertSession()->responseContains('<pre class="backtrace">');
 | 
			
		||||
    // Ensure we are escaping but not double escaping.
 | 
			
		||||
    $this->assertSession()->responseContains('>');
 | 
			
		||||
    $this->assertSession()->responseNotContains('&gt;');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests uncaught exception handling with custom exception handler.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUncaughtExceptionCustomExceptionHandler(): void {
 | 
			
		||||
    $settings_filename = $this->siteDirectory . '/settings.php';
 | 
			
		||||
    chmod($settings_filename, 0777);
 | 
			
		||||
    $settings_php = file_get_contents($settings_filename);
 | 
			
		||||
    $settings_php .= "\n";
 | 
			
		||||
    $settings_php .= "set_exception_handler(function() {\n";
 | 
			
		||||
    $settings_php .= "  header('HTTP/1.1 418 I\'m a teapot');\n";
 | 
			
		||||
    $settings_php .= "  print('Oh oh, flying teapots');\n";
 | 
			
		||||
    $settings_php .= "});\n";
 | 
			
		||||
    file_put_contents($settings_filename, $settings_php);
 | 
			
		||||
 | 
			
		||||
    \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(418);
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('The website encountered an unexpected error. Try again later.');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Oh oh, bananas in the instruments');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Oh oh, flying teapots');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a missing dependency on a service.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMissingDependency(): void {
 | 
			
		||||
    $this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed';
 | 
			
		||||
    $this->drupalGet('broken-service-class');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->pageTextContains('The website encountered an unexpected error.');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
 | 
			
		||||
    $this->assertErrorLogged($this->expectedExceptionMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a container which has an error.
 | 
			
		||||
   */
 | 
			
		||||
  public function testErrorContainer(): void {
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['settings']['container_base_class'] = (object) [
 | 
			
		||||
      'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
    \Drupal::service('kernel')->invalidateContainer();
 | 
			
		||||
 | 
			
		||||
    $this->expectedExceptionMessage = PHP_VERSION_ID >= 80400 ?
 | 
			
		||||
    'Drupal\FunctionalTests\Bootstrap\ErrorContainer::{closure:Drupal\FunctionalTests\Bootstrap\ErrorContainer::get():21}(): Argument #1 ($container) must be of type Drupal\FunctionalTests\Bootstrap\ErrorContainer' :
 | 
			
		||||
    'Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closure}(): Argument #1 ($container) must be of type Drupal\FunctionalTests\Bootstrap\ErrorContainer';
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
 | 
			
		||||
    $this->assertErrorLogged($this->expectedExceptionMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a container which has an exception really early.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExceptionContainer(): void {
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['settings']['container_base_class'] = (object) [
 | 
			
		||||
      'value' => '\Drupal\FunctionalTests\Bootstrap\ExceptionContainer',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
    \Drupal::service('kernel')->invalidateContainer();
 | 
			
		||||
 | 
			
		||||
    $this->expectedExceptionMessage = 'Thrown exception during Container::get';
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->pageTextContains('The website encountered an unexpected error');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
 | 
			
		||||
    $this->assertErrorLogged($this->expectedExceptionMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the case when the database connection is gone.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLostDatabaseConnection(): void {
 | 
			
		||||
    $incorrect_username = $this->randomMachineName(16);
 | 
			
		||||
    switch ($this->container->get('database')->driver()) {
 | 
			
		||||
      case 'pgsql':
 | 
			
		||||
      case 'mysql':
 | 
			
		||||
        $this->expectedExceptionMessage = $incorrect_username;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        // We can not carry out this test.
 | 
			
		||||
        $this->markTestSkipped('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We simulate a broken database connection by rewrite settings.php to no
 | 
			
		||||
    // longer have the proper data.
 | 
			
		||||
    $settings['databases']['default']['default']['username'] = (object) [
 | 
			
		||||
      'value' => $incorrect_username,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $settings['databases']['default']['default']['password'] = (object) [
 | 
			
		||||
      'value' => $this->randomMachineName(16),
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
    $this->assertSession()->pageTextContains('DatabaseAccessDeniedException');
 | 
			
		||||
    $this->assertErrorLogged($this->expectedExceptionMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests fallback to PHP error log when an exception is thrown while logging.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLoggerException(): void {
 | 
			
		||||
    // Ensure the test error log is empty before these tests.
 | 
			
		||||
    $this->assertNoErrorsLogged();
 | 
			
		||||
 | 
			
		||||
    $this->expectedExceptionMessage = 'Deforestation';
 | 
			
		||||
    \Drupal::state()->set('error_service_test.break_logger', TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(500);
 | 
			
		||||
    $this->assertSession()->pageTextContains('The website encountered an unexpected error. Try again later.');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
 | 
			
		||||
 | 
			
		||||
    // Find fatal error logged to the error.log
 | 
			
		||||
    $errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
 | 
			
		||||
    $this->assertCount(10, $errors, 'The error + the error that the logging service is broken has been written to the error log.');
 | 
			
		||||
    $this->assertStringContainsString('Failed to log error', $errors[0], 'The error handling logs when an error could not be logged to the logger.');
 | 
			
		||||
 | 
			
		||||
    $expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php';
 | 
			
		||||
    $expected_line = 69;
 | 
			
		||||
    $expected_entry = "Failed to log error: Exception: Deforestation in Drupal\\error_service_test\\MonkeysInTheControlRoom->handle() (line {$expected_line} of {$expected_path})";
 | 
			
		||||
    $this->assertStringContainsString($expected_entry, $errors[0], 'Original error logged to the PHP error log when an exception is thrown by a logger');
 | 
			
		||||
 | 
			
		||||
    // The exception is expected. Do not interpret it as a test failure. Not
 | 
			
		||||
    // using File API; a potential error must trigger a PHP warning.
 | 
			
		||||
    unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that a specific error has been logged to the PHP error log.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $error_message
 | 
			
		||||
   *   The expected error message.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Test\FunctionalTestSetupTrait::prepareEnvironment()
 | 
			
		||||
   * @see \Drupal\Core\DrupalKernel::bootConfiguration()
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertErrorLogged(string $error_message): void {
 | 
			
		||||
    $error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log';
 | 
			
		||||
    $this->assertFileExists($error_log_filename);
 | 
			
		||||
 | 
			
		||||
    $content = file_get_contents($error_log_filename);
 | 
			
		||||
    $rows = explode(PHP_EOL, $content);
 | 
			
		||||
 | 
			
		||||
    // We iterate over the rows in order to be able to remove the logged error
 | 
			
		||||
    // afterwards.
 | 
			
		||||
    $found = FALSE;
 | 
			
		||||
    foreach ($rows as $row_index => $row) {
 | 
			
		||||
      if (str_contains($content, $error_message)) {
 | 
			
		||||
        $found = TRUE;
 | 
			
		||||
        unset($rows[$row_index]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    file_put_contents($error_log_filename, implode("\n", $rows));
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($found, sprintf('The %s error message was logged.', $error_message));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that no errors have been logged to the PHP error.log thus far.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Test\FunctionalTestSetupTrait::prepareEnvironment()
 | 
			
		||||
   * @see \Drupal\Core\DrupalKernel::bootConfiguration()
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertNoErrorsLogged(): void {
 | 
			
		||||
    // Since PHP only creates the error.log file when an actual error is
 | 
			
		||||
    // triggered, it is sufficient to check whether the file exists.
 | 
			
		||||
    $this->assertFileDoesNotExist(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,64 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Breadcrumb;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\block\Traits\BlockCreationTrait;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the breadcrumb of 404 pages.
 | 
			
		||||
 *
 | 
			
		||||
 * @group breadcrumb
 | 
			
		||||
 */
 | 
			
		||||
class Breadcrumb404Test extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use BlockCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'block'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that different 404s don't create unnecessary cache entries.
 | 
			
		||||
   */
 | 
			
		||||
  public function testBreadcrumbOn404Pages(): void {
 | 
			
		||||
    $this->placeBlock('system_breadcrumb_block', ['id' => 'breadcrumb']);
 | 
			
		||||
 | 
			
		||||
    // Prime the cache first.
 | 
			
		||||
    $this->drupalGet('/not-found-1');
 | 
			
		||||
    $base_count = count($this->getBreadcrumbCacheEntries());
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/not-found-2');
 | 
			
		||||
    $next_count = count($this->getBreadcrumbCacheEntries());
 | 
			
		||||
    $this->assertEquals($base_count, $next_count);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/not-found-3');
 | 
			
		||||
    $next_count = count($this->getBreadcrumbCacheEntries());
 | 
			
		||||
    $this->assertEquals($base_count, $next_count);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the breadcrumb cache entries.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   The breadcrumb cache entries.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getBreadcrumbCacheEntries() {
 | 
			
		||||
    $database = \Drupal::database();
 | 
			
		||||
    $cache_entries = $database->select('cache_render')
 | 
			
		||||
      ->fields('cache_render')
 | 
			
		||||
      ->condition('cid', $database->escapeLike('entity_view:block:breadcrumb') . '%', 'LIKE')
 | 
			
		||||
      ->execute()
 | 
			
		||||
      ->fetchAllAssoc('cid');
 | 
			
		||||
    return $cache_entries;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										652
									
								
								web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										652
									
								
								web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,652 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Behat\Mink\Exception\ElementNotFoundException;
 | 
			
		||||
use Behat\Mink\Exception\ExpectationException;
 | 
			
		||||
use Drupal\Component\Serialization\Json;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\Traits\Core\CronRunTrait;
 | 
			
		||||
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 | 
			
		||||
use Drupal\TestTools\Extension\Dump\DebugDump;
 | 
			
		||||
use PHPUnit\Framework\ExpectationFailedException;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore htkey
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests BrowserTestBase functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group browsertestbase
 | 
			
		||||
 */
 | 
			
		||||
class BrowserTestBaseTest extends BrowserTestBase {
 | 
			
		||||
  use PathAliasTestTrait;
 | 
			
		||||
  use CronRunTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'test_page_test',
 | 
			
		||||
    'form_test',
 | 
			
		||||
    'system_test',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that JavaScript Drupal settings can be read.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDrupalSettings(): void {
 | 
			
		||||
    // Trigger a 403 because those pages have very little else going on.
 | 
			
		||||
    $this->drupalGet('admin');
 | 
			
		||||
    $this->assertSame([], $this->getDrupalSettings());
 | 
			
		||||
 | 
			
		||||
    // Now try the same 403 as an authenticated user and verify that Drupal
 | 
			
		||||
    // settings do show up.
 | 
			
		||||
    $account = $this->drupalCreateUser();
 | 
			
		||||
    $this->drupalLogin($account);
 | 
			
		||||
    $this->drupalGet('admin');
 | 
			
		||||
    $this->assertNotSame([], $this->getDrupalSettings());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests basic page test.
 | 
			
		||||
   */
 | 
			
		||||
  public function testGoTo(): void {
 | 
			
		||||
    $account = $this->drupalCreateUser();
 | 
			
		||||
    $this->drupalLogin($account);
 | 
			
		||||
 | 
			
		||||
    // Visit a Drupal page that requires login.
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Test page contains some text.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test page text.');
 | 
			
		||||
 | 
			
		||||
    // Check that returned plain text is correct.
 | 
			
		||||
    $text = $this->getTextContent();
 | 
			
		||||
    $this->assertStringContainsString('Test page text.', $text);
 | 
			
		||||
    $this->assertStringNotContainsString('</html>', $text);
 | 
			
		||||
    // Ensure Drupal Javascript settings are not part of the page text.
 | 
			
		||||
    $this->assertArrayHasKey('currentPathIsAdmin', $this->getDrupalSettings()['path']);
 | 
			
		||||
    $this->assertStringNotContainsString('currentPathIsAdmin', $text);
 | 
			
		||||
 | 
			
		||||
    // Response includes cache tags that we can assert.
 | 
			
		||||
    $this->assertSession()->responseHeaderExists('X-Drupal-Cache-Tags');
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'http_response rendered');
 | 
			
		||||
 | 
			
		||||
    // Test that we can read the JS settings.
 | 
			
		||||
    $js_settings = $this->getDrupalSettings();
 | 
			
		||||
    $this->assertSame('azAZ09();.,\\\/-_{}', $js_settings['test-setting']);
 | 
			
		||||
 | 
			
		||||
    // Test drupalGet with a URL object.
 | 
			
		||||
    $url = Url::fromRoute('test_page_test.render_title');
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Test page contains some text.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Hello Drupal');
 | 
			
		||||
 | 
			
		||||
    // Test that setting headers with drupalGet() works.
 | 
			
		||||
    $this->drupalGet('system-test/header', [], [
 | 
			
		||||
      'Test-Header' => 'header value',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertSession()->responseHeaderExists('Test-Header');
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Test-Header', 'header value');
 | 
			
		||||
 | 
			
		||||
    // Ensure that \Drupal\Tests\UiHelperTrait::isTestUsingGuzzleClient() works
 | 
			
		||||
    // as expected.
 | 
			
		||||
    $this->assertTrue($this->isTestUsingGuzzleClient());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests drupalGet().
 | 
			
		||||
   */
 | 
			
		||||
  public function testDrupalGet(): void {
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('test-page');
 | 
			
		||||
    $this->drupalGet('/test-page');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('test-page');
 | 
			
		||||
    $this->drupalGet('/test-page/');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('/test-page/');
 | 
			
		||||
    // Test alias handling.
 | 
			
		||||
    $this->createPathAlias('/test-page', '/test-alias');
 | 
			
		||||
    $this->rebuildAll();
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('test-alias');
 | 
			
		||||
    $this->drupalGet('/test-page');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('test-alias');
 | 
			
		||||
    $this->drupalGet('/test-page/');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('/test-page/');
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests basic form functionality.
 | 
			
		||||
   */
 | 
			
		||||
  public function testForm(): void {
 | 
			
		||||
    // Ensure the proper response code for a _form route.
 | 
			
		||||
    $this->drupalGet('form-test/object-builder');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Ensure the form and text field exist.
 | 
			
		||||
    $this->assertSession()->elementExists('css', 'form#form-test-form-test-object');
 | 
			
		||||
    $this->assertSession()->fieldExists('bananas');
 | 
			
		||||
 | 
			
		||||
    // Check that the hidden field exists and has a specific value.
 | 
			
		||||
    $this->assertSession()->hiddenFieldExists('strawberry');
 | 
			
		||||
    $this->assertSession()->hiddenFieldExists('red');
 | 
			
		||||
    $this->assertSession()->hiddenFieldExists('red-strawberry-hidden-field');
 | 
			
		||||
    $this->assertSession()->hiddenFieldValueNotEquals('strawberry', 'brown');
 | 
			
		||||
    $this->assertSession()->hiddenFieldValueEquals('strawberry', 'red');
 | 
			
		||||
 | 
			
		||||
    // Check that a hidden field does not exist.
 | 
			
		||||
    $this->assertSession()->hiddenFieldNotExists('bananas');
 | 
			
		||||
    $this->assertSession()->hiddenFieldNotExists('pineapple');
 | 
			
		||||
 | 
			
		||||
    $edit = ['bananas' => 'green'];
 | 
			
		||||
    $this->submitForm($edit, 'Save', 'form-test-form-test-object');
 | 
			
		||||
 | 
			
		||||
    $config_factory = $this->container->get('config.factory');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('green', $value);
 | 
			
		||||
 | 
			
		||||
    // Test submitForm().
 | 
			
		||||
    $this->drupalGet('form-test/object-builder');
 | 
			
		||||
 | 
			
		||||
    // Submit the form using the button label.
 | 
			
		||||
    $this->submitForm(['bananas' => 'red'], 'Save');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('red', $value);
 | 
			
		||||
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('', $value);
 | 
			
		||||
 | 
			
		||||
    // Submit the form using the button id.
 | 
			
		||||
    $this->submitForm(['bananas' => 'blue'], 'edit-submit');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('blue', $value);
 | 
			
		||||
 | 
			
		||||
    // Submit the form using the button name.
 | 
			
		||||
    $this->submitForm(['bananas' => 'purple'], 'op');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('purple', $value);
 | 
			
		||||
 | 
			
		||||
    // Submit using the form attribute of a button.
 | 
			
		||||
    $this->drupalGet('form-test/button-form-attribute');
 | 
			
		||||
    $this->submitForm(['bananas' => 'purple'], 'Attribute Button');
 | 
			
		||||
    $value = $config_factory->get('form_test.object')->get('bananas');
 | 
			
		||||
    $this->assertSame('purple', $value);
 | 
			
		||||
 | 
			
		||||
    // Test submitForm() with no-html response.
 | 
			
		||||
    $this->drupalGet('form_test/form-state-values-clean');
 | 
			
		||||
    $this->submitForm([], 'Submit');
 | 
			
		||||
    $values = Json::decode($this->getSession()->getPage()->getContent());
 | 
			
		||||
    $this->assertSame(1000, $values['beer']);
 | 
			
		||||
 | 
			
		||||
    // Test submitForm() with form by HTML id.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'page']);
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser(['create page content']));
 | 
			
		||||
    $this->drupalGet('form-test/two-instances-of-same-form');
 | 
			
		||||
    $this->getSession()->getPage()->fillField('edit-title-0-value', 'form1');
 | 
			
		||||
    $this->getSession()->getPage()->fillField('edit-title-0-value--2', 'form2');
 | 
			
		||||
    $this->submitForm([], 'Save', 'node-page-form--2');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Page form2 has been created.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests clickLink() functionality.
 | 
			
		||||
   */
 | 
			
		||||
  public function testClickLink(): void {
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->clickLink('Visually identical test links');
 | 
			
		||||
    $this->assertStringContainsString('user/login', $this->getSession()->getCurrentUrl());
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->clickLink('Visually identical test links', 0);
 | 
			
		||||
    $this->assertStringContainsString('user/login', $this->getSession()->getCurrentUrl());
 | 
			
		||||
    $this->drupalGet('test-page');
 | 
			
		||||
    $this->clickLink('Visually identical test links', 1);
 | 
			
		||||
    $this->assertStringContainsString('user/register', $this->getSession()->getCurrentUrl());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testError(): void {
 | 
			
		||||
    $this->expectException('\Exception');
 | 
			
		||||
    $this->expectExceptionMessage('User notice: foo');
 | 
			
		||||
    $this->drupalGet('test-error');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests legacy field asserts which use xpath directly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testXpathAsserts(): void {
 | 
			
		||||
    $this->drupalGet('test-field-xpath');
 | 
			
		||||
    $this->assertSession()->elementTextContains('xpath', '//table/tbody/tr[2]/td[1]', 'one');
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-name', 'Test name');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-options', '2');
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//nonexisting');
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('edit-name', 'wrong value');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldExists('nonexisting');
 | 
			
		||||
      $this->fail('The "nonexisting" field was found.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('edit-name');
 | 
			
		||||
      $this->fail('The "edit-name" field was not found.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests field asserts using textfields.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldAssertsForTextfields(): void {
 | 
			
		||||
    $this->drupalGet('test-field-xpath');
 | 
			
		||||
 | 
			
		||||
    // *** 1. fieldNotExists().
 | 
			
		||||
    $this->assertSession()->fieldNotExists('invalid_name_and_id');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly when searching by name.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('name');
 | 
			
		||||
      $this->fail('The "name" field was not found based on name.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly when searching by id.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('edit-name');
 | 
			
		||||
      $this->fail('The "name" field was not found based on id.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // *** 2. fieldExists().
 | 
			
		||||
    $this->assertSession()->fieldExists('name');
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-name');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if the field does not exist.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldExists('invalid_name_and_id');
 | 
			
		||||
      $this->fail('The "invalid_name_and_id" field was found.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ElementNotFoundException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
    // *** 3. assertNoFieldById().
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('name', 'not the value');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('nonexisting');
 | 
			
		||||
    // Test that the assertion fails correctly if no value is passed in.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('edit-description');
 | 
			
		||||
      $this->fail('The "description" field, with no value was not found.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if a NULL value is passed in.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('name', NULL);
 | 
			
		||||
      $this->fail('The "name" field was not found.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // *** 4. assertFieldById().
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-name');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-name', 'Test name');
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-description');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-description', '');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if no value is passed in.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldValueNotEquals('edit-name', '');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationFailedException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if the wrong value is passed in.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldValueNotEquals('edit-name', 'not the value');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationFailedException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // *** 5. fieldValueNotEquals().
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('name', 'not the value');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if given the right value.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldValueNotEquals('name', 'Test name');
 | 
			
		||||
      $this->fail('fieldValueNotEquals failed to throw an exception.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // *** 6. fieldValueEquals().
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('name', 'Test name');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('description', '');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if given the wrong value.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldValueEquals('name', 'not the value');
 | 
			
		||||
      $this->fail('fieldValueEquals failed to throw an exception.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that text areas can contain new lines.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-test-textarea-with-newline', "Test text with\nnewline");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests legacy field asserts for checkbox field type.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldAssertsForCheckbox(): void {
 | 
			
		||||
    $this->drupalGet('test-field-xpath');
 | 
			
		||||
 | 
			
		||||
    // Part 1 - Test by name.
 | 
			
		||||
    // Test that checkboxes are found/not found correctly by name, when using
 | 
			
		||||
    // '1' or '' to match their 'checked' state.
 | 
			
		||||
    $this->assertSession()->fieldExists('checkbox_enabled');
 | 
			
		||||
    $this->assertSession()->fieldExists('checkbox_disabled');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('checkbox_enabled', '1');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('checkbox_disabled', '');
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('checkbox_enabled', '');
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('checkbox_disabled', '1');
 | 
			
		||||
 | 
			
		||||
    // Test that we have legacy support.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('checkbox_enabled', '1');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('checkbox_disabled', '');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly if given the right value.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldValueNotEquals('checkbox_enabled', '1');
 | 
			
		||||
      $this->fail('fieldValueNotEquals failed to throw an exception.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Part 2 - Test by ID.
 | 
			
		||||
    // Test that checkboxes are found/not found correctly by ID, when using
 | 
			
		||||
    // '1' or '' to match their 'checked' state.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-checkbox-enabled', '1');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-checkbox-disabled', '');
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('edit-checkbox-enabled', '');
 | 
			
		||||
    $this->assertSession()->fieldValueNotEquals('edit-checkbox-disabled', '1');
 | 
			
		||||
 | 
			
		||||
    // Test that checkboxes are found by ID, when using NULL to ignore the
 | 
			
		||||
    // 'checked' state.
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-checkbox-enabled');
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-checkbox-disabled');
 | 
			
		||||
 | 
			
		||||
    // Test that checkboxes are found by ID when passing no second parameter.
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-checkbox-enabled');
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-checkbox-disabled');
 | 
			
		||||
 | 
			
		||||
    // Test that we have legacy support.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-checkbox-enabled', '1');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('edit-checkbox-disabled', '');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly when using NULL to ignore state.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->fieldNotExists('edit-checkbox-disabled', NULL);
 | 
			
		||||
      $this->fail('The "edit-checkbox-disabled" field was not found by ID, using NULL value.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Part 3 - Test the specific 'checked' assertions.
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-checkbox-enabled');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-checkbox-disabled');
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly with non-existent field id.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->checkboxNotChecked('incorrect_checkbox_id');
 | 
			
		||||
      $this->fail('The "incorrect_checkbox_id" field was found');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly for a checkbox that is checked.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->checkboxNotChecked('edit-checkbox-enabled');
 | 
			
		||||
      $this->fail('The "edit-checkbox-enabled" field was not found in a checked state.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that the assertion fails correctly for a checkbox that is not
 | 
			
		||||
    // checked.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->assertSession()->checkboxChecked('edit-checkbox-disabled');
 | 
			
		||||
      $this->fail('The "edit-checkbox-disabled" field was found and checked.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ExpectationException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the ::cronRun() method.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCronRun(): void {
 | 
			
		||||
    $last_cron_time = \Drupal::state()->get('system.cron_last');
 | 
			
		||||
    $this->cronRun();
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(204);
 | 
			
		||||
    $next_cron_time = \Drupal::state()->get('system.cron_last');
 | 
			
		||||
 | 
			
		||||
    $this->assertGreaterThan($last_cron_time, $next_cron_time);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the Drupal install done in \Drupal\Tests\BrowserTestBase::setUp().
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstall(): void {
 | 
			
		||||
    $htaccess_filename = $this->tempFilesDirectory . '/.htaccess';
 | 
			
		||||
    $this->assertFileExists($htaccess_filename);
 | 
			
		||||
 | 
			
		||||
    // Ensure the Update Status module is not installed.
 | 
			
		||||
    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('update'), 'The Update Status module should not be installed.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the assumption that local time is in 'Australia/Sydney'.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLocalTimeZone(): void {
 | 
			
		||||
    $expected = 'Australia/Sydney';
 | 
			
		||||
    // The 'Australia/Sydney' time zone is set in core/tests/bootstrap.php
 | 
			
		||||
    $this->assertEquals($expected, date_default_timezone_get());
 | 
			
		||||
 | 
			
		||||
    // The 'Australia/Sydney' time zone is also set in
 | 
			
		||||
    // FunctionalTestSetupTrait::initConfig().
 | 
			
		||||
    $config_factory = $this->container->get('config.factory');
 | 
			
		||||
    $value = $config_factory->get('system.date')->get('timezone.default');
 | 
			
		||||
    $this->assertEquals($expected, $value);
 | 
			
		||||
 | 
			
		||||
    // Test that users have the correct time zone set.
 | 
			
		||||
    $this->assertEquals($expected, $this->rootUser->getTimeZone());
 | 
			
		||||
    $admin_user = $this->drupalCreateUser(['administer site configuration']);
 | 
			
		||||
    $this->assertEquals($expected, $admin_user->getTimeZone());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the ::checkForMetaRefresh() method.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCheckForMetaRefresh(): void {
 | 
			
		||||
    // Disable following redirects in the client.
 | 
			
		||||
    $this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
 | 
			
		||||
    // Set the maximumMetaRefreshCount to zero to make sure the redirect doesn't
 | 
			
		||||
    // happen when doing a drupalGet.
 | 
			
		||||
    $this->maximumMetaRefreshCount = 0;
 | 
			
		||||
    $this->drupalGet('test-meta-refresh');
 | 
			
		||||
    $this->assertNotEmpty($this->cssSelect('meta[http-equiv="refresh"]'));
 | 
			
		||||
    // Allow one redirect to happen.
 | 
			
		||||
    $this->maximumMetaRefreshCount = 1;
 | 
			
		||||
    $this->checkForMetaRefresh();
 | 
			
		||||
    // Check that we are now on the test page.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test page text.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testGetDefaultDriveInstance(): void {
 | 
			
		||||
    putenv('MINK_DRIVER_ARGS=' . json_encode([NULL, ['key1' => ['key2' => ['key3' => 3, 'key3.1' => 3.1]]]]));
 | 
			
		||||
    $this->getDefaultDriverInstance();
 | 
			
		||||
    $this->assertEquals([NULL, ['key1' => ['key2' => ['key3' => 3, 'key3.1' => 3.1]]]], $this->minkDefaultDriverArgs);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures we can't access modules we shouldn't be able to after install.
 | 
			
		||||
   */
 | 
			
		||||
  public function testProfileModules(): void {
 | 
			
		||||
    $this->expectException(\InvalidArgumentException::class);
 | 
			
		||||
    $this->expectExceptionMessage('The module demo_umami_content does not exist.');
 | 
			
		||||
    $this->assertFileExists('core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.info.yml');
 | 
			
		||||
    \Drupal::service('extension.list.module')->getPathname('demo_umami_content');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the protections provided by .htkey.
 | 
			
		||||
   */
 | 
			
		||||
  public function testHtKey(): void {
 | 
			
		||||
    // Remove the Simpletest private key file so we can test the protection
 | 
			
		||||
    // against requests that forge a valid testing user agent to gain access
 | 
			
		||||
    // to the installer.
 | 
			
		||||
    // @see drupal_valid_test_ua()
 | 
			
		||||
    // Not using File API; a potential error must trigger a PHP warning.
 | 
			
		||||
    $install_url = Url::fromUri('base:core/install.php', ['external' => TRUE, 'absolute' => TRUE])->toString();
 | 
			
		||||
    $this->drupalGet($install_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    unlink($this->siteDirectory . '/.htkey');
 | 
			
		||||
    $this->drupalGet($install_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that a usable session is on the request in test-runner.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSessionOnRequest(): void {
 | 
			
		||||
    /** @var \Symfony\Component\HttpFoundation\Session\Session $session */
 | 
			
		||||
    $session = $this->container->get('request_stack')->getSession();
 | 
			
		||||
 | 
			
		||||
    $session->set('some-val', 'do-not-cleanup');
 | 
			
		||||
    $this->assertEquals('do-not-cleanup', $session->get('some-val'));
 | 
			
		||||
 | 
			
		||||
    $session->set('some-other-val', 'do-cleanup');
 | 
			
		||||
    $this->assertEquals('do-cleanup', $session->remove('some-other-val'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that deprecation headers do not get duplicated.
 | 
			
		||||
   *
 | 
			
		||||
   * @group legacy
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware::__invoke()
 | 
			
		||||
   */
 | 
			
		||||
  public function testDeprecationHeaders(): void {
 | 
			
		||||
    $this->drupalGet('/test-deprecations');
 | 
			
		||||
 | 
			
		||||
    $deprecation_messages = [];
 | 
			
		||||
    foreach ($this->getSession()->getResponseHeaders() as $name => $values) {
 | 
			
		||||
      if (preg_match('/^X-Drupal-Assertion-[0-9]+$/', $name, $matches)) {
 | 
			
		||||
        foreach ($values as $value) {
 | 
			
		||||
          $parameters = unserialize(urldecode($value));
 | 
			
		||||
          if (count($parameters) === 3) {
 | 
			
		||||
            if ($parameters[1] === 'User deprecated function') {
 | 
			
		||||
              $deprecation_messages[] = (string) $parameters[0];
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->assertContains('Test deprecation message', $deprecation_messages);
 | 
			
		||||
    $test_deprecation_messages = array_filter($deprecation_messages, function ($message) {
 | 
			
		||||
      return $message === 'Test deprecation message';
 | 
			
		||||
    });
 | 
			
		||||
    $this->assertCount(1, $test_deprecation_messages);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the dump() function provided by the var-dumper Symfony component.
 | 
			
		||||
   */
 | 
			
		||||
  public function testVarDump(): void {
 | 
			
		||||
    // Dump some variables.
 | 
			
		||||
    $object = (object) [
 | 
			
		||||
      'Aldebaran' => 'Betelgeuse',
 | 
			
		||||
    ];
 | 
			
		||||
    dump($object);
 | 
			
		||||
    dump('Alpheratz');
 | 
			
		||||
 | 
			
		||||
    $dumpString = json_encode(DebugDump::getDumps());
 | 
			
		||||
 | 
			
		||||
    $this->assertStringContainsString('BrowserTestBaseTest::testVarDump', $dumpString);
 | 
			
		||||
    $this->assertStringContainsString('Aldebaran', $dumpString);
 | 
			
		||||
    $this->assertStringContainsString('Betelgeuse', $dumpString);
 | 
			
		||||
    $this->assertStringContainsString('Alpheratz', $dumpString);
 | 
			
		||||
 | 
			
		||||
    // Visit a Drupal page with call to the dump() function to check that dump()
 | 
			
		||||
    // in site code produces output in the requested web page's HTML.
 | 
			
		||||
    $body = $this->drupalGet('test-page-var-dump');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // It is too strict to assert all properties of the Role and it is easy to
 | 
			
		||||
    // break if one of these properties gets removed or gets a new default
 | 
			
		||||
    // value. It should be sufficient to test just a couple of properties.
 | 
			
		||||
    $this->assertStringContainsString('<span class=sf-dump-note>', $body);
 | 
			
		||||
    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">id</span>: "<span class=sf-dump-str title="9 characters">test_role</span>"', $body);
 | 
			
		||||
    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">label</span>: "<span class=sf-dump-str title="9 characters">Test role</span>"', $body);
 | 
			
		||||
    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">permissions</span>: []', $body);
 | 
			
		||||
    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">uuid</span>: "', $body);
 | 
			
		||||
    $this->assertStringContainsString('</samp>}', $body);
 | 
			
		||||
 | 
			
		||||
    // Check that dump() in SUT did not leak into the test's dumps.
 | 
			
		||||
    $this->assertSame($dumpString, json_encode(DebugDump::getDumps()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests an invalid scheme in SIMPLETEST_BASE_URL throws an exception.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSimpleTestBaseUrlValidation(): void {
 | 
			
		||||
    putenv('SIMPLETEST_BASE_URL=mysql://user:pass@localhost/database');
 | 
			
		||||
    $this->expectException(\Exception::class);
 | 
			
		||||
    $this->expectExceptionMessage('You must provide valid scheme for the SIMPLETEST_BASE_URL environment variable. Valid schema are: http, https.');
 | 
			
		||||
    $this->setupBaseUrl();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,75 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests BrowserTestBase functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group browsertestbase
 | 
			
		||||
 */
 | 
			
		||||
class BrowserTestBaseUserAgentTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The user agent string to use.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $agent;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests validation of the User-Agent header we use to perform test requests.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUserAgentValidation(): void {
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
    $system_path = $this->buildUrl(\Drupal::service('extension.list.module')->getPath('system'));
 | 
			
		||||
    $http_path = $system_path . '/tests/http.php/user/login';
 | 
			
		||||
    $https_path = $system_path . '/tests/https.php/user/login';
 | 
			
		||||
    // Generate a valid test User-Agent to pass validation.
 | 
			
		||||
    $this->assertNotFalse(preg_match('/test\d+/', $this->databasePrefix, $matches), 'Database prefix contains test prefix.');
 | 
			
		||||
    $this->agent = drupal_generate_test_ua($matches[0]);
 | 
			
		||||
 | 
			
		||||
    // Test pages only available for testing.
 | 
			
		||||
    $this->drupalGet($http_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(200);
 | 
			
		||||
    $this->drupalGet($https_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Now slightly modify the HMAC on the header, which should not validate.
 | 
			
		||||
    $this->agent = 'X';
 | 
			
		||||
    $this->drupalGet($http_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalGet($https_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Use a real User-Agent and verify that the special files http.php and
 | 
			
		||||
    // https.php can't be accessed.
 | 
			
		||||
    $this->agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
 | 
			
		||||
    $this->drupalGet($http_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalGet($https_path);
 | 
			
		||||
    $assert_session->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareRequest(): void {
 | 
			
		||||
    $session = $this->getSession();
 | 
			
		||||
    if ($this->agent) {
 | 
			
		||||
      $session->setCookie('SIMPLETEST_USER_AGENT', $this->agent);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Components;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the correct rendering of components.
 | 
			
		||||
 *
 | 
			
		||||
 * @group sdc
 | 
			
		||||
 */
 | 
			
		||||
class ComponentRenderTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'sdc_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'sdc_theme_test';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests libraryOverrides.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLibraryOverrides(): void {
 | 
			
		||||
    $build = [
 | 
			
		||||
      '#type' => 'inline_template',
 | 
			
		||||
      '#template' => "{{ include('sdc_theme_test:lib-overrides') }}",
 | 
			
		||||
    ];
 | 
			
		||||
    \Drupal::state()->set('sdc_test_component', $build);
 | 
			
		||||
    $output = $this->drupalGet('sdc-test-component');
 | 
			
		||||
    $this->assertStringContainsString('another-stylesheet.css', $output);
 | 
			
		||||
    // Since libraryOverrides is taking control of CSS, and it's not listing
 | 
			
		||||
    // lib-overrides.css, then it should not be there. Even if it's the CSS
 | 
			
		||||
    // that usually gets auto-attached.
 | 
			
		||||
    $this->assertStringNotContainsString('lib-overrides.css', $output);
 | 
			
		||||
    // Ensure that libraryDependencies adds the expected assets.
 | 
			
		||||
    $this->assertStringContainsString('dialog.position.js', $output);
 | 
			
		||||
    // Ensure that libraryOverrides processes attributes properly.
 | 
			
		||||
    $this->assertMatchesRegularExpression('@<script.*src="[^"]*lib-overrides\.js\?v=1[^"]*".*defer.*bar="foo"></script>@', $output);
 | 
			
		||||
    // Ensure that libraryOverrides processes external CSS properly.
 | 
			
		||||
    $this->assertMatchesRegularExpression('@<link.*href="https://drupal\.org/fake-dependency/styles\.css" />@', $output);
 | 
			
		||||
    // Ensure that libraryOverrides processes external JS properly.
 | 
			
		||||
    $this->assertMatchesRegularExpression('@<script.*src="https://drupal\.org/fake-dependency/index\.min\.js"></script>@', $output);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Config;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\Traits\Core\Config\SchemaConfigListenerTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the functionality of ConfigSchemaChecker in BrowserTestBase tests.
 | 
			
		||||
 *
 | 
			
		||||
 * @group config
 | 
			
		||||
 */
 | 
			
		||||
class SchemaConfigListenerTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use SchemaConfigListenerTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['config_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Container;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test whether deprecation notices are triggered via \Drupal::service().
 | 
			
		||||
 *
 | 
			
		||||
 * Note: this test must be a BrowserTestBase so the container is properly
 | 
			
		||||
 * compiled. The container in KernelTestBase tests is always an instance of
 | 
			
		||||
 * \Drupal\Core\DependencyInjection\ContainerBuilder.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Container
 | 
			
		||||
 * @group legacy
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\Component\DependencyInjection\Container
 | 
			
		||||
 */
 | 
			
		||||
class ServiceDeprecationTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['deprecation_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::get
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetDeprecated(): void {
 | 
			
		||||
    $this->expectDeprecation('The "deprecation_test.service" service is deprecated in drupal:9.0.0 and is removed from drupal:20.0.0. This is a test.');
 | 
			
		||||
    $this->expectDeprecation('The "deprecation_test.alias" alias is deprecated in drupal:9.0.0 and is removed from drupal:20.0.0. This is a test.');
 | 
			
		||||
    // @phpstan-ignore-next-line
 | 
			
		||||
    \Drupal::service('deprecation_test.service');
 | 
			
		||||
    // @phpstan-ignore-next-line
 | 
			
		||||
    \Drupal::service('deprecation_test.alias');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,158 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Recipe;
 | 
			
		||||
 | 
			
		||||
use Drupal\contact\Entity\ContactForm;
 | 
			
		||||
use Drupal\Core\Config\Checkpoint\Checkpoint;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests recipe command.
 | 
			
		||||
 *
 | 
			
		||||
 * BrowserTestBase is used for a proper Drupal install.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Recipe\RecipeCommand
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 */
 | 
			
		||||
class RecipeCommandTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use RecipeTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * Disable strict config schema because this test explicitly makes the
 | 
			
		||||
   * recipe system save invalid config, to prove that it validates it after
 | 
			
		||||
   * the fact and raises an error.
 | 
			
		||||
   */
 | 
			
		||||
  protected $strictConfigSchema = FALSE;
 | 
			
		||||
 | 
			
		||||
  public function testRecipeCommand(): void {
 | 
			
		||||
    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('node'), 'The node module is not installed');
 | 
			
		||||
    $this->assertCheckpointsExist([]);
 | 
			
		||||
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/install_node_with_config');
 | 
			
		||||
    $this->assertSame(0, $process->getExitCode());
 | 
			
		||||
    $this->assertStringContainsString("Applied Install node with config recipe.", $process->getErrorOutput());
 | 
			
		||||
    $this->assertStringContainsString('Install node with config applied successfully', $process->getOutput());
 | 
			
		||||
    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('node'), 'The node module is installed');
 | 
			
		||||
    $this->assertCheckpointsExist(["Backup before the 'Install node with config' recipe."]);
 | 
			
		||||
 | 
			
		||||
    // Ensure recipes can be applied without affecting pre-existing checkpoints.
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/install_two_modules');
 | 
			
		||||
    $this->assertSame(0, $process->getExitCode());
 | 
			
		||||
    $this->assertStringContainsString("Applied Install two modules recipe.", $process->getErrorOutput());
 | 
			
		||||
    $this->assertStringContainsString('Install two modules applied successfully', $process->getOutput());
 | 
			
		||||
    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('node'), 'The node module is installed');
 | 
			
		||||
    $this->assertCheckpointsExist([
 | 
			
		||||
      "Backup before the 'Install node with config' recipe.",
 | 
			
		||||
      "Backup before the 'Install two modules' recipe.",
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Ensure recipes that fail have an exception message.
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/invalid_config', 1);
 | 
			
		||||
    $this->assertStringContainsString("There were validation errors in core.date_format.invalid", $process->getErrorOutput());
 | 
			
		||||
    $this->assertCheckpointsExist([
 | 
			
		||||
      "Backup before the 'Install node with config' recipe.",
 | 
			
		||||
      "Backup before the 'Install two modules' recipe.",
 | 
			
		||||
      // Although the recipe command tried to create a checkpoint, it did not
 | 
			
		||||
      // actually happen, because of https://drupal.org/i/3408523.
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Create a checkpoint so we can test what happens when a recipe does not
 | 
			
		||||
    // create a checkpoint before applying.
 | 
			
		||||
    \Drupal::service('config.storage.checkpoint')->checkpoint('Test log message');
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/no_extensions');
 | 
			
		||||
    $this->assertSame(0, $process->getExitCode());
 | 
			
		||||
    $this->assertStringContainsString("Applied No extensions recipe.", $process->getErrorOutput());
 | 
			
		||||
    $this->assertCheckpointsExist([
 | 
			
		||||
      "Backup before the 'Install node with config' recipe.",
 | 
			
		||||
      "Backup before the 'Install two modules' recipe.",
 | 
			
		||||
      "Test log message",
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertStringContainsString('[notice] A backup checkpoint was not created because nothing has changed since the "Test log message" checkpoint was created.', $process->getOutput());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that errors during config rollback won't steamroll validation errors.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExceptionOnRollback(): void {
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/config_rollback_exception', 1);
 | 
			
		||||
 | 
			
		||||
    // The error from the config importer should be visible.
 | 
			
		||||
    $output = $process->getOutput();
 | 
			
		||||
    $this->assertStringContainsString('There were errors validating the config synchronization.', $output);
 | 
			
		||||
    $this->assertStringContainsString('Provides a filter plugin that is in use', $output);
 | 
			
		||||
    // And the exception that actually *caused* the error should be visible too.
 | 
			
		||||
    $this->assertStringContainsString('There were validation errors in system.image:', $process->getErrorOutput());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the recipe command with a non-existent directory.
 | 
			
		||||
   */
 | 
			
		||||
  public function testErrorOnNonExistentDirectory(): void {
 | 
			
		||||
    $process = $this->applyRecipe('core/tests/fixtures/recipes/does_not_exist', 1);
 | 
			
		||||
 | 
			
		||||
    // The directory error should be the only error visible.
 | 
			
		||||
    $output = trim(preg_replace('/\s+/', ' ', $process->getOutput()));
 | 
			
		||||
    $this->assertSame('[ERROR] The supplied path core/tests/fixtures/recipes/does_not_exist is not a directory', $output);
 | 
			
		||||
    $this->assertEmpty($process->getErrorOutput());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the current set of checkpoints matches the given labels.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string[] $expected_labels
 | 
			
		||||
   *   The labels of every checkpoint that is expected to exist currently, in
 | 
			
		||||
   *   the expected order.
 | 
			
		||||
   */
 | 
			
		||||
  private function assertCheckpointsExist(array $expected_labels): void {
 | 
			
		||||
    $checkpoints = \Drupal::service('config.checkpoints');
 | 
			
		||||
    $labels = array_map(fn (Checkpoint $c) => $c->label, iterator_to_array($checkpoints));
 | 
			
		||||
    $this->assertSame($expected_labels, array_values($labels));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testPassInput(): void {
 | 
			
		||||
    $dir = $this->getDrupalRoot() . '/core/recipes/feedback_contact_form';
 | 
			
		||||
    $this->applyRecipe($dir, options: [
 | 
			
		||||
      '--input=feedback_contact_form.recipient=hello@good.bye',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertSame(['hello@good.bye'], ContactForm::load('feedback')?->getRecipients());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testPassInvalidInput(): void {
 | 
			
		||||
    $dir = $this->getDrupalRoot() . '/core/recipes/feedback_contact_form';
 | 
			
		||||
    $process = $this->applyRecipe($dir, 1, options: [
 | 
			
		||||
      '--input=feedback_contact_form.recipient=nobody',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertStringContainsString('This value is not a valid email address.', $process->getErrorOutput());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testDefaultInputValueFromConfig(): void {
 | 
			
		||||
    $this->config('system.site')
 | 
			
		||||
      ->set('mail', 'goodbye@hello.net')
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->applyRecipe($this->getDrupalRoot() . '/core/recipes/feedback_contact_form');
 | 
			
		||||
    $this->assertSame(['goodbye@hello.net'], ContactForm::load('feedback')?->getRecipients());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function testListInputs(): void {
 | 
			
		||||
    $root = $this->getDrupalRoot();
 | 
			
		||||
 | 
			
		||||
    $output = $this->applyRecipe($root . '/core/recipes/feedback_contact_form', command: 'recipe:info')->getOutput();
 | 
			
		||||
    $this->assertStringContainsString('feedback_contact_form.recipient', $output);
 | 
			
		||||
    $this->assertStringContainsString('The email address that should receive submissions from the feedback form.', $output);
 | 
			
		||||
 | 
			
		||||
    $output = $this->applyRecipe($root . '/core/recipes/page_content_type', command: 'recipe:info')->getOutput();
 | 
			
		||||
    $this->assertStringContainsString('This recipe does not accept any input.', $output);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,118 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Recipe;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\Recipe\Recipe;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Symfony\Component\Process\PhpExecutableFinder;
 | 
			
		||||
use Symfony\Component\Process\Process;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Contains helper methods for interacting with recipes in functional tests.
 | 
			
		||||
 */
 | 
			
		||||
trait RecipeTestTrait {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a recipe in a temporary directory.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string|array<mixed> $data
 | 
			
		||||
   *   The contents of recipe.yml. If passed as an array, will be encoded to
 | 
			
		||||
   *   YAML.
 | 
			
		||||
   * @param string|null $machine_name
 | 
			
		||||
   *   The machine name for the recipe. Will be used as the directory name.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Recipe\Recipe
 | 
			
		||||
   *   The recipe object.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createRecipe(string|array $data, ?string $machine_name = NULL): Recipe {
 | 
			
		||||
    if (is_array($data)) {
 | 
			
		||||
      $data = Yaml::encode($data);
 | 
			
		||||
    }
 | 
			
		||||
    $recipes_dir = $this->siteDirectory . '/recipes';
 | 
			
		||||
    if ($machine_name === NULL) {
 | 
			
		||||
      $dir = uniqid($recipes_dir . '/');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $dir = $recipes_dir . '/' . $machine_name;
 | 
			
		||||
    }
 | 
			
		||||
    mkdir($dir, recursive: TRUE);
 | 
			
		||||
    file_put_contents($dir . '/recipe.yml', $data);
 | 
			
		||||
 | 
			
		||||
    return Recipe::createFromDirectory($dir);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Applies a recipe to the site.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $path
 | 
			
		||||
   *   The path of the recipe to apply. Must be a directory.
 | 
			
		||||
   * @param int $expected_exit_code
 | 
			
		||||
   *   The expected exit code of the `drupal recipe` process. Defaults to 0,
 | 
			
		||||
   *   which indicates that no error occurred.
 | 
			
		||||
   * @param string[] $options
 | 
			
		||||
   *   (optional) Additional options to pass to the `drupal recipe` command.
 | 
			
		||||
   * @param string $command
 | 
			
		||||
   *   (optional) The name of the command to run. Defaults to `recipe`.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Symfony\Component\Process\Process
 | 
			
		||||
   *   The `drupal recipe` command process, after having run.
 | 
			
		||||
   */
 | 
			
		||||
  protected function applyRecipe(string $path, int $expected_exit_code = 0, array $options = [], string $command = 'recipe'): Process {
 | 
			
		||||
    assert($this instanceof BrowserTestBase);
 | 
			
		||||
 | 
			
		||||
    $arguments = [
 | 
			
		||||
      (new PhpExecutableFinder())->find(),
 | 
			
		||||
      'core/scripts/drupal',
 | 
			
		||||
      $command,
 | 
			
		||||
      // Never apply recipes interactively.
 | 
			
		||||
      '--no-interaction',
 | 
			
		||||
      ...$options,
 | 
			
		||||
      $path,
 | 
			
		||||
    ];
 | 
			
		||||
    $process = (new Process($arguments))
 | 
			
		||||
      ->setWorkingDirectory($this->getDrupalRoot())
 | 
			
		||||
      ->setEnv([
 | 
			
		||||
        'DRUPAL_DEV_SITE_PATH' => $this->siteDirectory,
 | 
			
		||||
        // Ensure that the command boots Drupal into a state where it knows it's
 | 
			
		||||
        // a test site.
 | 
			
		||||
        // @see drupal_valid_test_ua()
 | 
			
		||||
        'HTTP_USER_AGENT' => drupal_generate_test_ua($this->databasePrefix),
 | 
			
		||||
      ])
 | 
			
		||||
      ->setTimeout(500);
 | 
			
		||||
 | 
			
		||||
    $process->run();
 | 
			
		||||
    $this->assertSame($expected_exit_code, $process->getExitCode(), sprintf("Process exit code mismatch.\nExpected: %d\nActual: %d\n\nSTDOUT:\n%s\n\nSTDERR:\n%s", $expected_exit_code, $process->getExitCode(), $process->getOutput(), $process->getErrorOutput()));
 | 
			
		||||
    // Applying a recipe:
 | 
			
		||||
    // - creates new checkpoints, hence the "state" service in the test runner
 | 
			
		||||
    //   is outdated
 | 
			
		||||
    // - may install modules, which would cause the entire container in the test
 | 
			
		||||
    //   runner to be outdated.
 | 
			
		||||
    // Hence the entire environment must be rebuilt for assertions to target the
 | 
			
		||||
    // actual post-recipe-application result.
 | 
			
		||||
    // @see \Drupal\Core\Config\Checkpoint\LinearHistory::__construct()
 | 
			
		||||
    $this->rebuildAll();
 | 
			
		||||
    return $process;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Alters an existing recipe.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $path
 | 
			
		||||
   *   The recipe directory path.
 | 
			
		||||
   * @param callable $alter
 | 
			
		||||
   *   A function that will receive the decoded contents of recipe.yml as an
 | 
			
		||||
   *   array. This should returned a modified array to be written to recipe.yml.
 | 
			
		||||
   */
 | 
			
		||||
  protected function alterRecipe(string $path, callable $alter): void {
 | 
			
		||||
    $file = $path . '/recipe.yml';
 | 
			
		||||
    $this->assertFileExists($file);
 | 
			
		||||
    $contents = file_get_contents($file);
 | 
			
		||||
    $contents = Yaml::decode($contents);
 | 
			
		||||
    $contents = $alter($contents);
 | 
			
		||||
    file_put_contents($file, Yaml::encode($contents));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Recipe;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\Checkpoint\Checkpoint;
 | 
			
		||||
use Drupal\Core\Datetime\Entity\DateFormat;
 | 
			
		||||
use Drupal\Core\Recipe\Recipe;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 */
 | 
			
		||||
class RollbackTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use RecipeTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * Disable strict config schema because this test explicitly makes the
 | 
			
		||||
   * recipe system save invalid config, to prove that it validates it after
 | 
			
		||||
   * the fact and raises an error.
 | 
			
		||||
   */
 | 
			
		||||
  protected $strictConfigSchema = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'system',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @testWith ["invalid_config", "core.date_format.invalid"]
 | 
			
		||||
   *           ["recipe_depend_on_invalid", "core.date_format.invalid"]
 | 
			
		||||
   *           ["recipe_depend_on_invalid_config_and_valid_modules", "core.date_format.invalid"]
 | 
			
		||||
   */
 | 
			
		||||
  public function testRollbackForInvalidConfig(string $recipe_fixture, string $expected_invalid_config_name): void {
 | 
			
		||||
    $expected_core_extension_modules = $this->config('core.extension')->get('module');
 | 
			
		||||
 | 
			
		||||
    /** @var string $recipe_fixture */
 | 
			
		||||
    $recipe_fixture = realpath(__DIR__ . "/../../../../fixtures/recipes/$recipe_fixture");
 | 
			
		||||
    $process = $this->applyRecipe($recipe_fixture, 1);
 | 
			
		||||
    $this->assertStringContainsString("There were validation errors in $expected_invalid_config_name:", $process->getErrorOutput());
 | 
			
		||||
    $this->assertCheckpointsExist([
 | 
			
		||||
      "Backup before the '" . Recipe::createFromDirectory($recipe_fixture)->name . "' recipe.",
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // @see invalid_config
 | 
			
		||||
    $date_formats = DateFormat::loadMultiple(['valid', 'invalid']);
 | 
			
		||||
    $this->assertEmpty($date_formats, "The recipe's imported config was not rolled back.");
 | 
			
		||||
 | 
			
		||||
    // @see recipe_depend_on_invalid_config_and_valid_module
 | 
			
		||||
    $this->assertSame($expected_core_extension_modules, $this->config('core.extension')->get('module'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the current set of checkpoints matches the given labels.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string[] $expected_labels
 | 
			
		||||
   *   The labels of every checkpoint that is expected to exist currently, in
 | 
			
		||||
   *   the expected order.
 | 
			
		||||
   */
 | 
			
		||||
  private function assertCheckpointsExist(array $expected_labels): void {
 | 
			
		||||
    $checkpoints = \Drupal::service('config.checkpoints');
 | 
			
		||||
    $labels = array_map(fn (Checkpoint $c) => $c->label, iterator_to_array($checkpoints));
 | 
			
		||||
    $this->assertSame($expected_labels, array_values($labels));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,129 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Recipe;
 | 
			
		||||
 | 
			
		||||
use Drupal\contact\Entity\ContactForm;
 | 
			
		||||
use Drupal\FunctionalTests\Installer\InstallerTestBase;
 | 
			
		||||
use Drupal\shortcut\Entity\Shortcut;
 | 
			
		||||
use Drupal\Tests\standard\Traits\StandardTestTrait;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
use Symfony\Component\Yaml\Yaml as SymfonyYaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installing the Standard recipe via the installer.
 | 
			
		||||
 *
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 */
 | 
			
		||||
class StandardRecipeInstallTest extends InstallerTestBase {
 | 
			
		||||
  use StandardTestTrait {
 | 
			
		||||
    testStandard as doTestStandard;
 | 
			
		||||
  }
 | 
			
		||||
  use RecipeTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = '';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    // Skip permissions hardening so we can write a services file later.
 | 
			
		||||
    $this->settings['settings']['skip_permissions_hardening'] = (object) [
 | 
			
		||||
      'value' => TRUE,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Use a URL to install from a recipe.
 | 
			
		||||
    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?profile=&recipe=core/recipes/standard');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function testStandard(): void {
 | 
			
		||||
    if (!isset($this->rootUser->passRaw) && isset($this->rootUser->pass_raw)) {
 | 
			
		||||
      $this->rootUser->passRaw = $this->rootUser->pass_raw;
 | 
			
		||||
    }
 | 
			
		||||
    // These recipes provide functionality that is only optionally part of the
 | 
			
		||||
    // Standard profile, so we need to explicitly apply them.
 | 
			
		||||
    $this->applyRecipe('core/recipes/editorial_workflow');
 | 
			
		||||
    $this->applyRecipe('core/recipes/audio_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/document_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/image_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/local_video_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/remote_video_media_type');
 | 
			
		||||
 | 
			
		||||
    // Add a Home link to the main menu as Standard expects "Main navigation"
 | 
			
		||||
    // block on the page.
 | 
			
		||||
    $this->drupalGet('admin/structure/menu/manage/main/add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Home',
 | 
			
		||||
      'link[0][uri]' => '<front>',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Standard expects to set the contact form's recipient email to the
 | 
			
		||||
    // system's email address, but our feedback_contact_form recipe hard-codes
 | 
			
		||||
    // it to another value.
 | 
			
		||||
    // @todo This can be removed after https://drupal.org/i/3303126, which
 | 
			
		||||
    //   should make it possible for a recipe to reuse an already-set config
 | 
			
		||||
    //   value.
 | 
			
		||||
    ContactForm::load('feedback')?->setRecipients(['simpletest@example.com'])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Standard ships two shortcuts; ensure they exist.
 | 
			
		||||
    $this->assertCount(2, Shortcut::loadMultiple());
 | 
			
		||||
 | 
			
		||||
    // The installer logs you in.
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
 | 
			
		||||
    $this->doTestStandard();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // Noop. This form is skipped due the parameters set on the URL.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected function installDefaultThemeFromClassProperty(ContainerInterface $container): void {
 | 
			
		||||
    // In this context a default theme makes no sense.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installResponsiveImage(): void {
 | 
			
		||||
    // Overrides StandardTest::installResponsiveImage() in order to use the
 | 
			
		||||
    // recipe.
 | 
			
		||||
    $this->applyRecipe('core/recipes/standard_responsive_images');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    $services_file = DRUPAL_ROOT . '/' . $this->siteDirectory . '/services.yml';
 | 
			
		||||
    // $content = file_get_contents($services_file);
 | 
			
		||||
 | 
			
		||||
    // Disable the super user access.
 | 
			
		||||
    $yaml = new SymfonyYaml();
 | 
			
		||||
    $services = [];
 | 
			
		||||
    $services['parameters']['security.enable_super_user'] = FALSE;
 | 
			
		||||
    file_put_contents($services_file, $yaml->dump($services));
 | 
			
		||||
    parent::setUpSite();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,142 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Recipe;
 | 
			
		||||
 | 
			
		||||
use Drupal\shortcut\Entity\Shortcut;
 | 
			
		||||
use Drupal\Tests\standard\Functional\StandardTest;
 | 
			
		||||
use Drupal\user\RoleInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Standard recipe installation expectations.
 | 
			
		||||
 *
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 */
 | 
			
		||||
class StandardRecipeTest extends StandardTest {
 | 
			
		||||
 | 
			
		||||
  use RecipeTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests Standard installation recipe.
 | 
			
		||||
   */
 | 
			
		||||
  public function testStandard(): void {
 | 
			
		||||
    // Install some modules that Standard has optional integrations with.
 | 
			
		||||
    \Drupal::service('module_installer')->install(['media_library', 'content_moderation']);
 | 
			
		||||
 | 
			
		||||
    // Export all the configuration so we can compare later.
 | 
			
		||||
    $this->copyConfig(\Drupal::service('config.storage'), \Drupal::service('config.storage.sync'));
 | 
			
		||||
 | 
			
		||||
    // Set theme to stark and uninstall the other themes.
 | 
			
		||||
    $theme_installer = \Drupal::service('theme_installer');
 | 
			
		||||
    $theme_installer->install(['stark']);
 | 
			
		||||
    $this->config('system.theme')->set('admin', '')->set('default', 'stark')->save();
 | 
			
		||||
    $theme_installer->uninstall(['claro', 'olivero']);
 | 
			
		||||
 | 
			
		||||
    // Determine which modules to uninstall.
 | 
			
		||||
    $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), ['user', 'system', 'path_alias', \Drupal::database()->getProvider()]);
 | 
			
		||||
    foreach (['shortcut', 'field_config', 'filter_format', 'field_storage_config'] as $entity_type) {
 | 
			
		||||
      $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
 | 
			
		||||
      $storage->delete($storage->loadMultiple());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Uninstall all the modules including the Standard profile.
 | 
			
		||||
    \Drupal::service('module_installer')->uninstall($uninstall);
 | 
			
		||||
 | 
			
		||||
    // Clean up entity displays before recipe import.
 | 
			
		||||
    foreach (['entity_form_display', 'entity_view_display'] as $entity_type) {
 | 
			
		||||
      $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
 | 
			
		||||
      $storage->delete($storage->loadMultiple());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Clean up roles before recipe import.
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('user_role');
 | 
			
		||||
    $roles = $storage->loadMultiple();
 | 
			
		||||
    // Do not delete the administrator role. There would be no user with the
 | 
			
		||||
    // permissions to create content.
 | 
			
		||||
    unset($roles[RoleInterface::ANONYMOUS_ID], $roles[RoleInterface::AUTHENTICATED_ID], $roles['administrator']);
 | 
			
		||||
    $storage->delete($roles);
 | 
			
		||||
 | 
			
		||||
    $this->applyRecipe('core/recipes/standard');
 | 
			
		||||
    // These recipes provide functionality that is only optionally part of the
 | 
			
		||||
    // Standard profile, so we need to explicitly apply them.
 | 
			
		||||
    $this->applyRecipe('core/recipes/editorial_workflow');
 | 
			
		||||
    $this->applyRecipe('core/recipes/audio_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/document_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/image_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/local_video_media_type');
 | 
			
		||||
    $this->applyRecipe('core/recipes/remote_video_media_type');
 | 
			
		||||
 | 
			
		||||
    // Remove the theme we had to install.
 | 
			
		||||
    \Drupal::service('theme_installer')->uninstall(['stark']);
 | 
			
		||||
 | 
			
		||||
    // Add a Home link to the main menu as Standard expects "Main navigation"
 | 
			
		||||
    // block on the page.
 | 
			
		||||
    $this->drupalLogin($this->rootUser);
 | 
			
		||||
    $this->drupalGet('admin/structure/menu/manage/main/add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Home',
 | 
			
		||||
      'link[0][uri]' => '<front>',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Update sync directory config to have the same UUIDs so we can compare.
 | 
			
		||||
    /** @var \Drupal\Core\Config\StorageInterface $sync */
 | 
			
		||||
    $sync = \Drupal::service('config.storage.sync');
 | 
			
		||||
    /** @var \Drupal\Core\Config\StorageInterface $active */
 | 
			
		||||
    $active = \Drupal::service('config.storage');
 | 
			
		||||
    // @todo https://www.drupal.org/i/3439749 Determine if the the _core unset
 | 
			
		||||
    //   is correct.
 | 
			
		||||
    foreach ($active->listAll() as $name) {
 | 
			
		||||
      /** @var mixed[] $active_data */
 | 
			
		||||
      $active_data = $active->read($name);
 | 
			
		||||
      if ($sync->exists($name)) {
 | 
			
		||||
        /** @var mixed[] $sync_data */
 | 
			
		||||
        $sync_data = $sync->read($name);
 | 
			
		||||
        if (isset($sync_data['uuid'])) {
 | 
			
		||||
          $sync_data['uuid'] = $active_data['uuid'];
 | 
			
		||||
        }
 | 
			
		||||
        if (isset($sync_data['_core'])) {
 | 
			
		||||
          unset($sync_data['_core']);
 | 
			
		||||
        }
 | 
			
		||||
        /** @var array $sync_data */
 | 
			
		||||
        $sync->write($name, $sync_data);
 | 
			
		||||
      }
 | 
			
		||||
      if (isset($active_data['_core'])) {
 | 
			
		||||
        unset($active_data['_core']);
 | 
			
		||||
        $active->write($name, $active_data);
 | 
			
		||||
      }
 | 
			
		||||
      // @todo Remove this once https://drupal.org/i/3427564 lands.
 | 
			
		||||
      if ($name === 'node.settings') {
 | 
			
		||||
        unset($active_data['langcode']);
 | 
			
		||||
        $active->write($name, $active_data);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure we have truly rebuilt the standard profile using recipes.
 | 
			
		||||
    // Uncomment the code below to see the differences in a single file.
 | 
			
		||||
    // phpcs:ignore Drupal.Files.LineLength
 | 
			
		||||
    // $this->assertSame($sync->read('node.settings'), $active->read('node.settings'));
 | 
			
		||||
    $comparer = $this->configImporter()->getStorageComparer();
 | 
			
		||||
    $expected_list = $comparer->getEmptyChangelist();
 | 
			
		||||
    // We expect core.extension to be different because standard is no longer
 | 
			
		||||
    // installed.
 | 
			
		||||
    $expected_list['update'] = ['core.extension'];
 | 
			
		||||
    $this->assertSame($expected_list, $comparer->getChangelist());
 | 
			
		||||
 | 
			
		||||
    // Standard ships two shortcuts; ensure they exist.
 | 
			
		||||
    $this->assertCount(2, Shortcut::loadMultiple());
 | 
			
		||||
 | 
			
		||||
    parent::testStandard();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installResponsiveImage(): void {
 | 
			
		||||
    // Overrides StandardTest::installResponsiveImage() in order to use the
 | 
			
		||||
    // recipe.
 | 
			
		||||
    $this->applyRecipe('core/recipes/standard_responsive_images');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Test;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests batch operations during tests execution.
 | 
			
		||||
 *
 | 
			
		||||
 * This demonstrates that a batch will be successfully executed during module
 | 
			
		||||
 * installation when running tests.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Test
 | 
			
		||||
 * @group FunctionalTestSetupTrait
 | 
			
		||||
 */
 | 
			
		||||
class ModuleInstallBatchTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['test_batch_test', 'entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests loading entities created in a batch in test_batch_test_install().
 | 
			
		||||
   */
 | 
			
		||||
  public function testLoadingEntitiesCreatedInBatch(): void {
 | 
			
		||||
    foreach ([1, 2] as $id) {
 | 
			
		||||
      $this->assertNotNull(EntityTest::load($id), 'Successfully loaded entity ' . $id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Core\Test;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Drupal's extension to manage code deprecation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Test
 | 
			
		||||
 * @group legacy
 | 
			
		||||
 */
 | 
			
		||||
class PhpUnitBridgeTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['deprecation_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests deprecation message from deprecation_test_function().
 | 
			
		||||
   */
 | 
			
		||||
  public function testSilencedError(): void {
 | 
			
		||||
    $this->expectDeprecation('This is the deprecation message for deprecation_test_function().');
 | 
			
		||||
    $this->assertEquals('known_return_value', deprecation_test_function());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests deprecation message from deprecated route.
 | 
			
		||||
   */
 | 
			
		||||
  public function testErrorOnSiteUnderTest(): void {
 | 
			
		||||
    $this->expectDeprecation('This is the deprecation message for deprecation_test_function().');
 | 
			
		||||
    $this->drupalGet(Url::fromRoute('deprecation_test.route'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,129 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Datetime;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the functionality of TimestampAgoFormatter core field formatter.
 | 
			
		||||
 *
 | 
			
		||||
 * @group field
 | 
			
		||||
 */
 | 
			
		||||
class TimestampAgoFormatterTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * An array of display options to pass to entity_get_display().
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $displayOptions;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A field storage to use in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field used in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $field;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['entity_test', 'field_ui'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $web_user = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
      'administer entity_test fields',
 | 
			
		||||
      'administer entity_test display',
 | 
			
		||||
      'administer entity_test form display',
 | 
			
		||||
      'view the administration theme',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($web_user);
 | 
			
		||||
 | 
			
		||||
    $field_name = 'field_timestamp';
 | 
			
		||||
    $type = 'timestamp';
 | 
			
		||||
    $widget_type = 'datetime_timestamp';
 | 
			
		||||
    $formatter_type = 'timestamp_ago';
 | 
			
		||||
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => $type,
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
    $this->field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->fieldStorage,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->field->save();
 | 
			
		||||
 | 
			
		||||
    EntityFormDisplay::load('entity_test.entity_test.default')
 | 
			
		||||
      ->setComponent($field_name, ['type' => $widget_type])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->displayOptions = [
 | 
			
		||||
      'type' => $formatter_type,
 | 
			
		||||
      'label' => 'hidden',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    EntityViewDisplay::create([
 | 
			
		||||
      'targetEntityType' => $this->field->getTargetEntityTypeId(),
 | 
			
		||||
      'bundle' => $this->field->getTargetBundle(),
 | 
			
		||||
      'mode' => 'full',
 | 
			
		||||
      'status' => TRUE,
 | 
			
		||||
    ])->setComponent($field_name, $this->displayOptions)
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the formatter settings.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSettings(): void {
 | 
			
		||||
    $this->drupalGet('entity_test/structure/entity_test/display');
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'fields[field_timestamp][region]' => 'content',
 | 
			
		||||
      'fields[field_timestamp][type]' => 'timestamp_ago',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->submitForm([], 'field_timestamp_settings_edit');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'fields[field_timestamp][settings_edit_form][settings][future_format]' => 'ends in @interval',
 | 
			
		||||
      'fields[field_timestamp][settings_edit_form][settings][past_format]' => 'started @interval ago',
 | 
			
		||||
      'fields[field_timestamp][settings_edit_form][settings][granularity]' => 1,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Update');
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->pageTextContains('ends in 1 year');
 | 
			
		||||
    $this->assertSession()->pageTextContains('started 1 year ago');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										182
									
								
								web/core/tests/Drupal/FunctionalTests/Datetime/TimestampTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								web/core/tests/Drupal/FunctionalTests/Datetime/TimestampTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,182 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Datetime;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Datetime\DrupalDateTime;
 | 
			
		||||
use Drupal\Core\Datetime\Entity\DateFormat;
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the functionality of Timestamp core field UI.
 | 
			
		||||
 *
 | 
			
		||||
 * @group field
 | 
			
		||||
 */
 | 
			
		||||
class TimestampTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * An array of display options.
 | 
			
		||||
   *
 | 
			
		||||
   * These options are passed to
 | 
			
		||||
   * EntityDisplayRepositoryInterface::getViewDisplay().
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $displayOptions;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A field storage to use in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field used in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $field;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['node', 'entity_test', 'field_ui'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $web_user = $this->drupalCreateUser([
 | 
			
		||||
      'access content',
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
      'administer entity_test form display',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($web_user);
 | 
			
		||||
    $field_name = 'field_timestamp';
 | 
			
		||||
    $type = 'timestamp';
 | 
			
		||||
    $widget_type = 'datetime_timestamp';
 | 
			
		||||
    $formatter_type = 'timestamp';
 | 
			
		||||
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => $type,
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
    $this->field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->fieldStorage,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
      'description' => 'Description for timestamp field.',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->field->save();
 | 
			
		||||
 | 
			
		||||
    EntityFormDisplay::load('entity_test.entity_test.default')
 | 
			
		||||
      ->setComponent($field_name, ['type' => $widget_type])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->displayOptions = [
 | 
			
		||||
      'type' => $formatter_type,
 | 
			
		||||
      'label' => 'hidden',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    EntityViewDisplay::create([
 | 
			
		||||
      'targetEntityType' => $this->field->getTargetEntityTypeId(),
 | 
			
		||||
      'bundle' => $this->field->getTargetBundle(),
 | 
			
		||||
      'mode' => 'full',
 | 
			
		||||
      'status' => TRUE,
 | 
			
		||||
    ])->setComponent($field_name, $this->displayOptions)
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the "datetime_timestamp" widget.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWidget(): void {
 | 
			
		||||
    // Build up a date in the UTC timezone.
 | 
			
		||||
    $value = '2012-12-31 00:00:00';
 | 
			
		||||
    $date = new DrupalDateTime($value, 'UTC');
 | 
			
		||||
 | 
			
		||||
    // Update the timezone to the system default.
 | 
			
		||||
    $date->setTimezone(timezone_open(date_default_timezone_get()));
 | 
			
		||||
 | 
			
		||||
    // Display creation form.
 | 
			
		||||
    $this->drupalGet('entity_test/add');
 | 
			
		||||
 | 
			
		||||
    // Make sure the field description is properly displayed.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Description for timestamp field.');
 | 
			
		||||
 | 
			
		||||
    // Make sure the "datetime_timestamp" widget is on the page.
 | 
			
		||||
    $this->assertSession()->elementsCount('xpath', '//div[contains(@class, "field--widget-datetime-timestamp") and @id="edit-field-timestamp-wrapper"]', 1);
 | 
			
		||||
 | 
			
		||||
    // Look for the widget elements and make sure they are empty.
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][date]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][date]', '');
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][time]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][time]', '');
 | 
			
		||||
 | 
			
		||||
    // Submit the date.
 | 
			
		||||
    $date_format = DateFormat::load('html_date')->getPattern();
 | 
			
		||||
    $time_format = DateFormat::load('html_time')->getPattern();
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'field_timestamp[0][value][date]' => $date->format($date_format),
 | 
			
		||||
      'field_timestamp[0][value][time]' => $date->format($time_format),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Make sure the submitted date is set as the default in the widget.
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][date]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][date]', $date->format($date_format));
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][time]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][time]', $date->format($time_format));
 | 
			
		||||
 | 
			
		||||
    // Make sure the entity was saved.
 | 
			
		||||
    preg_match('|entity_test/manage/(\d+)|', $this->getSession()->getCurrentUrl(), $match);
 | 
			
		||||
    $id = $match[1];
 | 
			
		||||
    $this->assertSession()->pageTextContains(sprintf('entity_test %s has been created.', $id));
 | 
			
		||||
 | 
			
		||||
    // Make sure the timestamp is output properly with the default formatter.
 | 
			
		||||
    $medium = DateFormat::load('medium')->getPattern();
 | 
			
		||||
    $this->drupalGet('entity_test/' . $id);
 | 
			
		||||
    $this->assertSession()->pageTextContains($date->format($medium));
 | 
			
		||||
 | 
			
		||||
    // Build up a date in the UTC timezone.
 | 
			
		||||
    $value = '2024-01-16 00:00:00';
 | 
			
		||||
    $date = new DrupalDateTime($value, 'UTC');
 | 
			
		||||
 | 
			
		||||
    // Set a default value for the field.
 | 
			
		||||
    $this->field->setDefaultValue($date->getTimestamp())->save();
 | 
			
		||||
 | 
			
		||||
    // Update the timezone to the system default.
 | 
			
		||||
    $date->setTimezone(timezone_open(date_default_timezone_get()));
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('entity_test/add');
 | 
			
		||||
    $date_format = DateFormat::load('html_date')->getPattern();
 | 
			
		||||
    $time_format = DateFormat::load('html_time')->getPattern();
 | 
			
		||||
    // Make sure the default field value is set as the default value in the
 | 
			
		||||
    // widget.
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][date]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][date]', $date->format($date_format));
 | 
			
		||||
    $this->assertSession()->fieldExists('field_timestamp[0][value][time]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('field_timestamp[0][value][time]', $date->format($time_format));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,375 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\DefaultContent;
 | 
			
		||||
 | 
			
		||||
use ColinODell\PsrTestLogger\TestLogger;
 | 
			
		||||
use Drupal\block_content\BlockContentInterface;
 | 
			
		||||
use Drupal\block_content\Entity\BlockContentType;
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\DefaultContent\PreImportEvent;
 | 
			
		||||
use Drupal\Core\DefaultContent\Existing;
 | 
			
		||||
use Drupal\Core\DefaultContent\Finder;
 | 
			
		||||
use Drupal\Core\DefaultContent\Importer;
 | 
			
		||||
use Drupal\Core\DefaultContent\ImportException;
 | 
			
		||||
use Drupal\Core\DefaultContent\InvalidEntityException;
 | 
			
		||||
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityRepositoryInterface;
 | 
			
		||||
use Drupal\Core\File\FileExists;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\file\FileInterface;
 | 
			
		||||
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\language\Entity\ContentLanguageSettings;
 | 
			
		||||
use Drupal\layout_builder\Section;
 | 
			
		||||
use Drupal\media\MediaInterface;
 | 
			
		||||
use Drupal\menu_link_content\MenuLinkContentInterface;
 | 
			
		||||
use Drupal\node\NodeInterface;
 | 
			
		||||
use Drupal\taxonomy\TermInterface;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
 | 
			
		||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
 | 
			
		||||
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
 | 
			
		||||
use Drupal\user\UserInterface;
 | 
			
		||||
use Drupal\workspaces\Entity\Workspace;
 | 
			
		||||
use Psr\Log\LogLevel;
 | 
			
		||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @covers \Drupal\Core\DefaultContent\Importer
 | 
			
		||||
 * @group DefaultContent
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class ContentImportTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use EntityReferenceFieldCreationTrait;
 | 
			
		||||
  use MediaTypeCreationTrait;
 | 
			
		||||
  use RecipeTestTrait;
 | 
			
		||||
  use TaxonomyTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block_content',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'layout_builder',
 | 
			
		||||
    'media',
 | 
			
		||||
    'menu_link_content',
 | 
			
		||||
    'node',
 | 
			
		||||
    'path',
 | 
			
		||||
    'path_alias',
 | 
			
		||||
    'system',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workspaces',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The directory with the source data.
 | 
			
		||||
   */
 | 
			
		||||
  private readonly string $contentDir;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The admin account.
 | 
			
		||||
   */
 | 
			
		||||
  private UserInterface $adminAccount;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->adminAccount = $this->setUpCurrentUser(admin: TRUE);
 | 
			
		||||
 | 
			
		||||
    BlockContentType::create(['id' => 'basic', 'label' => 'Basic'])->save();
 | 
			
		||||
    block_content_add_body_field('basic');
 | 
			
		||||
 | 
			
		||||
    $this->createVocabulary(['vid' => 'tags']);
 | 
			
		||||
    $this->createMediaType('image', ['id' => 'image']);
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'page']);
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article']);
 | 
			
		||||
    $this->createEntityReferenceField('node', 'article', 'field_tags', 'Tags', 'taxonomy_term');
 | 
			
		||||
 | 
			
		||||
    // Create a field with custom serialization, so we can ensure that the
 | 
			
		||||
    // importer handles that properly.
 | 
			
		||||
    $field_storage = FieldStorageConfig::create([
 | 
			
		||||
      'entity_type' => 'taxonomy_term',
 | 
			
		||||
      'field_name' => 'field_serialized_stuff',
 | 
			
		||||
      'type' => 'serialized_property_item_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_storage->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_storage' => $field_storage,
 | 
			
		||||
      'bundle' => 'tags',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
    ContentLanguageSettings::create([
 | 
			
		||||
      'target_entity_type_id' => 'node',
 | 
			
		||||
      'target_bundle' => 'article',
 | 
			
		||||
    ])
 | 
			
		||||
      ->setThirdPartySetting('content_translation', 'enabled', TRUE)
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->contentDir = $this->getDrupalRoot() . '/core/tests/fixtures/default_content';
 | 
			
		||||
    \Drupal::service('file_system')->copy($this->contentDir . '/file/druplicon_copy.png', $this->publicFilesDirectory . '/druplicon_copy.png', FileExists::Error);
 | 
			
		||||
 | 
			
		||||
    // Enable Layout Builder for the Page content type, with custom overrides.
 | 
			
		||||
    \Drupal::service(EntityDisplayRepositoryInterface::class)
 | 
			
		||||
      ->getViewDisplay('node', 'page')
 | 
			
		||||
      ->enableLayoutBuilder()
 | 
			
		||||
      ->setOverridable()
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @return array<array<mixed>>
 | 
			
		||||
   *   An array of test cases, each containing an existing entity handling mode.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerImportEntityThatAlreadyExists(): array {
 | 
			
		||||
    return [
 | 
			
		||||
      [Existing::Error],
 | 
			
		||||
      [Existing::Skip],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @dataProvider providerImportEntityThatAlreadyExists
 | 
			
		||||
   */
 | 
			
		||||
  public function testImportEntityThatAlreadyExists(Existing $existing): void {
 | 
			
		||||
    $this->drupalCreateUser(values: ['uuid' => '94503467-be7f-406c-9795-fc25baa22203']);
 | 
			
		||||
 | 
			
		||||
    if ($existing === Existing::Error) {
 | 
			
		||||
      $this->expectException(ImportException::class);
 | 
			
		||||
      $this->expectExceptionMessage('user 94503467-be7f-406c-9795-fc25baa22203 already exists.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->container->get(Importer::class)
 | 
			
		||||
      ->importContent(new Finder($this->contentDir), $existing);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests importing content directly, via the API.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDirectContentImport(): void {
 | 
			
		||||
    $logger = new TestLogger();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\DefaultContent\Importer $importer */
 | 
			
		||||
    $importer = $this->container->get(Importer::class);
 | 
			
		||||
    $importer->setLogger($logger);
 | 
			
		||||
    $importer->importContent(new Finder($this->contentDir));
 | 
			
		||||
 | 
			
		||||
    $this->assertContentWasImported($this->adminAccount);
 | 
			
		||||
    // We should see a warning about importing a file entity associated with a
 | 
			
		||||
    // file that doesn't exist.
 | 
			
		||||
    $predicate = function (array $record): bool {
 | 
			
		||||
      return (
 | 
			
		||||
        $record['message'] === 'File entity %name was imported, but the associated file (@path) was not found.' &&
 | 
			
		||||
        $record['context']['%name'] === 'dce9cdc3-d9fc-4d37-849d-105e913bb5ad.png' &&
 | 
			
		||||
        $record['context']['@path'] === $this->contentDir . '/file/dce9cdc3-d9fc-4d37-849d-105e913bb5ad.png'
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
    $this->assertTrue($logger->hasRecordThatPasses($predicate, LogLevel::WARNING));
 | 
			
		||||
 | 
			
		||||
    // Visit a page that is published in a non-live workspace; we should not be
 | 
			
		||||
    // able to see it, because we don't have permission.
 | 
			
		||||
    $node_in_workspace = $this->container->get(EntityRepositoryInterface::class)
 | 
			
		||||
      ->loadEntityByUuid('node', '48475954-e878-439c-9d3d-226724a44269');
 | 
			
		||||
    $this->assertInstanceOf(NodeInterface::class, $node_in_workspace);
 | 
			
		||||
    $node_url = $node_in_workspace->toUrl();
 | 
			
		||||
    $this->drupalGet($node_url);
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
    $assert_session->statusCodeEquals(403);
 | 
			
		||||
    // If we log in with administrative privileges (i.e., we can look at any
 | 
			
		||||
    // workspace), we should be able to see it.
 | 
			
		||||
    $this->drupalLogin($this->adminAccount);
 | 
			
		||||
    $this->drupalGet($node_url);
 | 
			
		||||
    $assert_session->statusCodeEquals(200);
 | 
			
		||||
    $assert_session->pageTextContains($node_in_workspace->label());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests importing content directly, via the API, with a different user.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDirectContentImportWithDifferentUser(): void {
 | 
			
		||||
    $user = $this->createUser();
 | 
			
		||||
    $importer = $this->container->get(Importer::class);
 | 
			
		||||
    $importer->importContent(new Finder($this->contentDir), account: $user);
 | 
			
		||||
    $this->assertContentWasImported($user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the importer validates entities before saving them.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityValidationIsTriggered(): void {
 | 
			
		||||
    $dir = uniqid('public://');
 | 
			
		||||
    mkdir($dir);
 | 
			
		||||
 | 
			
		||||
    /** @var string $data */
 | 
			
		||||
    $data = file_get_contents($this->contentDir . '/node/2d3581c3-92c7-4600-8991-a0d4b3741198.yml');
 | 
			
		||||
    $data = Yaml::decode($data);
 | 
			
		||||
    /** @var array{default: array{sticky: array<int, array{value: mixed}>}} $data */
 | 
			
		||||
    $data['default']['sticky'][0]['value'] = 'not a boolean!';
 | 
			
		||||
    file_put_contents($dir . '/invalid.yml', Yaml::encode($data));
 | 
			
		||||
 | 
			
		||||
    $this->expectException(InvalidEntityException::class);
 | 
			
		||||
    $this->expectExceptionMessage("$dir/invalid.yml: sticky.0.value=This value should be of the correct primitive type.");
 | 
			
		||||
    $this->container->get(Importer::class)->importContent(new Finder($dir));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the default content was imported as expected.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Session\AccountInterface $account
 | 
			
		||||
   *   The account that should own the imported content.
 | 
			
		||||
   */
 | 
			
		||||
  private function assertContentWasImported(AccountInterface $account): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
 | 
			
		||||
    $entity_repository = $this->container->get(EntityRepositoryInterface::class);
 | 
			
		||||
 | 
			
		||||
    $node = $entity_repository->loadEntityByUuid('node', 'e1714f23-70c0-4493-8e92-af1901771921');
 | 
			
		||||
    $this->assertInstanceOf(NodeInterface::class, $node);
 | 
			
		||||
    $this->assertSame('Crikey it works!', $node->body->value);
 | 
			
		||||
    $this->assertSame('article', $node->bundle());
 | 
			
		||||
    $this->assertSame('Test Article', $node->label());
 | 
			
		||||
    $tag = $node->field_tags->entity;
 | 
			
		||||
    $this->assertInstanceOf(TermInterface::class, $tag);
 | 
			
		||||
    $this->assertSame('Default Content', $tag->label());
 | 
			
		||||
    $this->assertSame('tags', $tag->bundle());
 | 
			
		||||
    $this->assertSame('550f86ad-aa11-4047-953f-636d42889f85', $tag->uuid());
 | 
			
		||||
    // The tag carries a field with serialized data, so ensure it came through
 | 
			
		||||
    // properly.
 | 
			
		||||
    $this->assertSame('a:2:{i:0;s:2:"Hi";i:1;s:6:"there!";}', $tag->field_serialized_stuff->value);
 | 
			
		||||
    $this->assertSame('94503467-be7f-406c-9795-fc25baa22203', $node->getOwner()->uuid());
 | 
			
		||||
    // The node's URL should use the path alias shipped with the recipe.
 | 
			
		||||
    $node_url = $node->toUrl()->toString();
 | 
			
		||||
    $this->assertSame(Url::fromUserInput('/test-article')->toString(), $node_url);
 | 
			
		||||
 | 
			
		||||
    $media = $entity_repository->loadEntityByUuid('media', '344b943c-b231-4d73-9669-0b0a2be12aa5');
 | 
			
		||||
    $this->assertInstanceOf(MediaInterface::class, $media);
 | 
			
		||||
    $this->assertSame('image', $media->bundle());
 | 
			
		||||
    $this->assertSame('druplicon.png', $media->label());
 | 
			
		||||
    $file = $media->field_media_image->entity;
 | 
			
		||||
    $this->assertInstanceOf(FileInterface::class, $file);
 | 
			
		||||
    $this->assertSame('druplicon.png', $file->getFilename());
 | 
			
		||||
    $this->assertSame('d8404562-efcc-40e3-869e-40132d53fe0b', $file->uuid());
 | 
			
		||||
 | 
			
		||||
    // Another file entity referencing an existing file but already in use by
 | 
			
		||||
    // another entity, should be imported.
 | 
			
		||||
    $same_file_different_entity = $entity_repository->loadEntityByUuid('file', '23a7f61f-1db3-407d-a6dd-eb4731995c9f');
 | 
			
		||||
    $this->assertInstanceOf(FileInterface::class, $same_file_different_entity);
 | 
			
		||||
    $this->assertSame('druplicon-duplicate.png', $same_file_different_entity->getFilename());
 | 
			
		||||
    $this->assertStringEndsWith('/druplicon_0.png', (string) $same_file_different_entity->getFileUri());
 | 
			
		||||
 | 
			
		||||
    // Another file entity that references a file with the same name as, but
 | 
			
		||||
    // different contents than, an existing file, should be imported and the
 | 
			
		||||
    // file should be renamed.
 | 
			
		||||
    $different_file = $entity_repository->loadEntityByUuid('file', 'a6b79928-838f-44bd-a8f0-44c2fff9e4cc');
 | 
			
		||||
    $this->assertInstanceOf(FileInterface::class, $different_file);
 | 
			
		||||
    $this->assertSame('druplicon-different.png', $different_file->getFilename());
 | 
			
		||||
    $this->assertStringEndsWith('/druplicon_1.png', (string) $different_file->getFileUri());
 | 
			
		||||
 | 
			
		||||
    // Another file entity referencing an existing file but one that is not in
 | 
			
		||||
    // use by another entity, should be imported but use the existing file.
 | 
			
		||||
    $different_file = $entity_repository->loadEntityByUuid('file', '7fb09f9f-ba5f-4db4-82ed-aa5ccf7d425d');
 | 
			
		||||
    $this->assertInstanceOf(FileInterface::class, $different_file);
 | 
			
		||||
    $this->assertSame('druplicon_copy.png', $different_file->getFilename());
 | 
			
		||||
    $this->assertStringEndsWith('/druplicon_copy.png', (string) $different_file->getFileUri());
 | 
			
		||||
 | 
			
		||||
    // Our node should have a menu link, and it should use the path alias we
 | 
			
		||||
    // included with the recipe.
 | 
			
		||||
    $menu_link = $entity_repository->loadEntityByUuid('menu_link_content', '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b');
 | 
			
		||||
    $this->assertInstanceOf(MenuLinkContentInterface::class, $menu_link);
 | 
			
		||||
    $this->assertSame($menu_link->getUrlObject()->toString(), $node_url);
 | 
			
		||||
    $this->assertSame('main', $menu_link->getMenuName());
 | 
			
		||||
 | 
			
		||||
    $block_content = $entity_repository->loadEntityByUuid('block_content', 'd9b72b2f-a5ea-4a3f-b10c-28deb7b3b7bf');
 | 
			
		||||
    $this->assertInstanceOf(BlockContentInterface::class, $block_content);
 | 
			
		||||
    $this->assertSame('basic', $block_content->bundle());
 | 
			
		||||
    $this->assertSame('Useful Info', $block_content->label());
 | 
			
		||||
    $this->assertSame("I'd love to put some useful info here.", $block_content->body->value);
 | 
			
		||||
 | 
			
		||||
    // A node with a non-existent owner should be reassigned to the current
 | 
			
		||||
    // user or the user provided to the importer.
 | 
			
		||||
    $node = $entity_repository->loadEntityByUuid('node', '7f1dd75a-0be2-4d3b-be5d-9d1a868b9267');
 | 
			
		||||
    $this->assertInstanceOf(NodeInterface::class, $node);
 | 
			
		||||
    $this->assertSame($account->id(), $node->getOwner()->id());
 | 
			
		||||
 | 
			
		||||
    // Ensure a node with a translation is imported properly.
 | 
			
		||||
    $node = $entity_repository->loadEntityByUuid('node', '2d3581c3-92c7-4600-8991-a0d4b3741198');
 | 
			
		||||
    $this->assertInstanceOf(NodeInterface::class, $node);
 | 
			
		||||
    $translation = $node->getTranslation('fr');
 | 
			
		||||
    $this->assertSame('Perdu en traduction', $translation->label());
 | 
			
		||||
    $this->assertSame("Içi c'est la version français.", $translation->body->value);
 | 
			
		||||
 | 
			
		||||
    // Layout data should be imported.
 | 
			
		||||
    $node = $entity_repository->loadEntityByUuid('node', '32650de8-9edd-48dc-80b8-8bda180ebbac');
 | 
			
		||||
    $this->assertInstanceOf(NodeInterface::class, $node);
 | 
			
		||||
    $section = $node->layout_builder__layout[0]->section;
 | 
			
		||||
    $this->assertInstanceOf(Section::class, $section);
 | 
			
		||||
    $this->assertCount(2, $section->getComponents());
 | 
			
		||||
    $this->assertSame('system_powered_by_block', $section->getComponent('03b45f14-cf74-469a-8398-edf3383ce7fa')->getPluginId());
 | 
			
		||||
 | 
			
		||||
    // Workspaces should have been imported with their parent references intact.
 | 
			
		||||
    $workspaces = Workspace::loadMultiple();
 | 
			
		||||
    $this->assertArrayHasKey('test_workspace', $workspaces);
 | 
			
		||||
    $this->assertSame('test_workspace', $workspaces['inner_test']?->parent->entity->id());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the pre-import event allows skipping certain entities.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPreImportEvent(): void {
 | 
			
		||||
    $invalid_uuid_detected = FALSE;
 | 
			
		||||
 | 
			
		||||
    $listener = function (PreImportEvent $event) use (&$invalid_uuid_detected): void {
 | 
			
		||||
      $event->skip('3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', 'Decided not to!');
 | 
			
		||||
      try {
 | 
			
		||||
        $event->skip('not-a-thing');
 | 
			
		||||
      }
 | 
			
		||||
      catch (\InvalidArgumentException) {
 | 
			
		||||
        $invalid_uuid_detected = TRUE;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    \Drupal::service(EventDispatcherInterface::class)
 | 
			
		||||
      ->addListener(PreImportEvent::class, $listener);
 | 
			
		||||
 | 
			
		||||
    $finder = new Finder($this->contentDir);
 | 
			
		||||
    $this->assertSame('menu_link_content', $finder->data['3434bd5a-d2cd-4f26-bf79-a7f6b951a21b']['_meta']['entity_type']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\DefaultContent\Importer $importer */
 | 
			
		||||
    $importer = \Drupal::service(Importer::class);
 | 
			
		||||
    $logger = new TestLogger();
 | 
			
		||||
    $importer->setLogger($logger);
 | 
			
		||||
    $importer->importContent($finder, Existing::Error);
 | 
			
		||||
 | 
			
		||||
    // The entity we skipped should not be here, and the reason why should have
 | 
			
		||||
    // been logged.
 | 
			
		||||
    $menu_link = \Drupal::service(EntityRepositoryInterface::class)
 | 
			
		||||
      ->loadEntityByUuid('menu_link_content', '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b');
 | 
			
		||||
    $this->assertNull($menu_link);
 | 
			
		||||
    $this->assertTrue($logger->hasInfo([
 | 
			
		||||
      'message' => 'Skipped importing @entity_type @uuid because: %reason',
 | 
			
		||||
      'context' => [
 | 
			
		||||
        '@entity_type' => 'menu_link_content',
 | 
			
		||||
        '@uuid' => '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b',
 | 
			
		||||
        '%reason' => 'Decided not to!',
 | 
			
		||||
      ],
 | 
			
		||||
    ]));
 | 
			
		||||
    // We should have caught an exception for trying to skip an invalid UUID.
 | 
			
		||||
    $this->assertTrue($invalid_uuid_detected);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,142 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the correct mapping of user input on the correct field delta elements.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 */
 | 
			
		||||
class ContentEntityFormCorrectUserInputMappingOnFieldDeltaElementsTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the type of the entity under test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field name with multiple properties being test with the entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $web_user = $this->drupalCreateUser(['administer entity_test content']);
 | 
			
		||||
    $this->drupalLogin($web_user);
 | 
			
		||||
 | 
			
		||||
    // Create a field of field type "shape" with unlimited cardinality on the
 | 
			
		||||
    // entity type "entity_test".
 | 
			
		||||
    $this->entityTypeId = 'entity_test';
 | 
			
		||||
    $this->fieldName = 'shape';
 | 
			
		||||
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'type' => 'shape',
 | 
			
		||||
      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
 | 
			
		||||
    ])
 | 
			
		||||
      ->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Shape',
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay($this->entityTypeId, $this->entityTypeId)
 | 
			
		||||
      ->setComponent($this->fieldName, ['type' => 'shape_only_color_editable_widget'])
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the correct user input mapping on complex fields.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCorrectUserInputMappingOnComplexFields(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      $this->fieldName => [
 | 
			
		||||
        ['shape' => 'rectangle', 'color' => 'green'],
 | 
			
		||||
        ['shape' => 'circle', 'color' => 'blue'],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->entityTypeId . '/manage/' . $entity->id() . '/edit');
 | 
			
		||||
 | 
			
		||||
    // Rearrange the field items.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      "$this->fieldName[0][_weight]" => 0,
 | 
			
		||||
      "$this->fieldName[1][_weight]" => -1,
 | 
			
		||||
    ];
 | 
			
		||||
    // Executing an ajax call is important before saving as it will trigger
 | 
			
		||||
    // form state caching and so if for any reasons the form is rebuilt with
 | 
			
		||||
    // the entity built based on the user submitted values with already
 | 
			
		||||
    // reordered field items then the correct mapping will break after the form
 | 
			
		||||
    // builder maps over the new form the user submitted values based on the
 | 
			
		||||
    // previous delta ordering.
 | 
			
		||||
    //
 | 
			
		||||
    // This is how currently the form building process works and this test
 | 
			
		||||
    // ensures the correct behavior no matter what changes would be made to the
 | 
			
		||||
    // form builder or the content entity forms.
 | 
			
		||||
    $this->submitForm($edit, 'Add another item');
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    $entity = $storage->load($entity->id());
 | 
			
		||||
 | 
			
		||||
    // Assert that after rearranging the field items the user input will be
 | 
			
		||||
    // mapped on the correct delta field items.
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      ['shape' => 'circle', 'color' => 'blue'],
 | 
			
		||||
      ['shape' => 'rectangle', 'color' => 'green'],
 | 
			
		||||
    ], $entity->get($this->fieldName)->getValue());
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->entityTypeId . '/manage/' . $entity->id() . '/edit');
 | 
			
		||||
 | 
			
		||||
    // Delete one of the field items and ensure that the user input is mapped on
 | 
			
		||||
    // the correct delta field items.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      "$this->fieldName[0][_weight]" => 0,
 | 
			
		||||
      "$this->fieldName[1][_weight]" => -1,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, "{$this->fieldName}_0_remove_button");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->load($entity->id());
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      ['shape' => 'rectangle', 'color' => 'green'],
 | 
			
		||||
    ], $entity->get($this->fieldName)->getValue());
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,172 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\TestFileCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests field validation filtering on content entity forms.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 */
 | 
			
		||||
class ContentEntityFormFieldValidationFilteringTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use TestFileCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the type of the entity under test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The single-valued field name being tested with the entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldNameSingle;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The multi-valued field name being tested with the entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldNameMultiple;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The name of the file field being tested with the entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldNameFile;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['entity_test', 'field_test', 'file', 'image'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $web_user = $this->drupalCreateUser(['administer entity_test content']);
 | 
			
		||||
    $this->drupalLogin($web_user);
 | 
			
		||||
 | 
			
		||||
    // Create two fields of field type "test_field", one with single cardinality
 | 
			
		||||
    // and one with unlimited cardinality on the entity type "entity_test". It
 | 
			
		||||
    // is important to use this field type because its default widget has a
 | 
			
		||||
    // custom \Drupal\Core\Field\WidgetInterface::errorElement() implementation.
 | 
			
		||||
    $this->entityTypeId = 'entity_test';
 | 
			
		||||
    $this->fieldNameSingle = 'test_single';
 | 
			
		||||
    $this->fieldNameMultiple = 'test_multiple';
 | 
			
		||||
    $this->fieldNameFile = 'test_file';
 | 
			
		||||
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldNameSingle,
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'type' => 'test_field',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldNameSingle,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Test single',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldNameMultiple,
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'type' => 'test_field',
 | 
			
		||||
      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldNameMultiple,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Test multiple',
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    // Also create a file field to test its '#limit_validation_errors'
 | 
			
		||||
    // implementation.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldNameFile,
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'type' => 'file',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldNameFile,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Test file',
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $this->container->get('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay($this->entityTypeId, $this->entityTypeId, 'default')
 | 
			
		||||
      ->setComponent($this->fieldNameSingle, ['type' => 'test_field_widget'])
 | 
			
		||||
      ->setComponent($this->fieldNameMultiple, ['type' => 'test_field_widget'])
 | 
			
		||||
      ->setComponent($this->fieldNameFile, ['type' => 'file_generic'])
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests field widgets with #limit_validation_errors.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldWidgetsWithLimitedValidationErrors(): void {
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
    $this->drupalGet($this->entityTypeId . '/add');
 | 
			
		||||
 | 
			
		||||
    // The 'Test multiple' field is the only multi-valued field in the form, so
 | 
			
		||||
    // try to add a new item for it. This tests the '#limit_validation_errors'
 | 
			
		||||
    // property set by \Drupal\Core\Field\WidgetBase::formMultipleElements().
 | 
			
		||||
    $assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.js-form-type-textfield input', 1);
 | 
			
		||||
    $this->submitForm([], 'Add another item');
 | 
			
		||||
    $assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.js-form-type-textfield input', 2);
 | 
			
		||||
 | 
			
		||||
    // Now try to upload a file. This tests the '#limit_validation_errors'
 | 
			
		||||
    // property set by
 | 
			
		||||
    // \Drupal\file\Plugin\Field\FieldWidget\FileWidget::process().
 | 
			
		||||
    $text_file = current($this->getTestFiles('text'));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'files[test_file_0]' => \Drupal::service('file_system')->realpath($text_file->uri),
 | 
			
		||||
    ];
 | 
			
		||||
    $assert_session->elementNotExists('css', 'input#edit-test-file-0-remove-button');
 | 
			
		||||
    $this->submitForm($edit, 'Upload');
 | 
			
		||||
    $assert_session->elementExists('css', 'input#edit-test-file-0-remove-button');
 | 
			
		||||
 | 
			
		||||
    // Make the 'Test multiple' field required and check that adding another
 | 
			
		||||
    // item does not throw a validation error.
 | 
			
		||||
    $field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $this->fieldNameMultiple);
 | 
			
		||||
    $field_config->setRequired(TRUE);
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->entityTypeId . '/add');
 | 
			
		||||
    $this->submitForm([], 'Add another item');
 | 
			
		||||
    $assert_session->pageTextNotContains('Test multiple (value 1) field is required.');
 | 
			
		||||
 | 
			
		||||
    // Check that saving the form without entering any value for the required
 | 
			
		||||
    // field still throws the proper validation errors.
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
    $assert_session->pageTextContains('Test single field is required.');
 | 
			
		||||
    $assert_session->pageTextContains('Test multiple (value 1) field is required.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,164 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestBundle;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the delete multiple confirmation form.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @runTestsInSeparateProcesses
 | 
			
		||||
 * @preserveGlobalState disabled
 | 
			
		||||
 */
 | 
			
		||||
class DeleteMultipleFormTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The current user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $account;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['entity_test', 'user', 'language'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    EntityTestBundle::create([
 | 
			
		||||
      'id' => 'default',
 | 
			
		||||
      'label' => 'Default',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->account = $this->drupalCreateUser([
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($this->account);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the delete form for translatable entities.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslatableEntities(): void {
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('es')->save();
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    $selection = [];
 | 
			
		||||
 | 
			
		||||
    $entity1 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity1']);
 | 
			
		||||
    $entity1->addTranslation('es', ['name' => 'entity1 spanish']);
 | 
			
		||||
    $entity1->addTranslation('fr', ['name' => 'entity1 french']);
 | 
			
		||||
    $entity1->save();
 | 
			
		||||
    $selection[$entity1->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    $entity2 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity2']);
 | 
			
		||||
    $entity2->addTranslation('es', ['name' => 'entity2 spanish']);
 | 
			
		||||
    $entity2->addTranslation('fr', ['name' => 'entity2 french']);
 | 
			
		||||
    $entity2->save();
 | 
			
		||||
    $selection[$entity2->id()]['es'] = 'es';
 | 
			
		||||
    $selection[$entity2->id()]['fr'] = 'fr';
 | 
			
		||||
 | 
			
		||||
    $entity3 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity3']);
 | 
			
		||||
    $entity3->addTranslation('es', ['name' => 'entity3 spanish']);
 | 
			
		||||
    $entity3->addTranslation('fr', ['name' => 'entity3 french']);
 | 
			
		||||
    $entity3->save();
 | 
			
		||||
    $selection[$entity3->id()]['fr'] = 'fr';
 | 
			
		||||
 | 
			
		||||
    // This entity will be inaccessible because of
 | 
			
		||||
    // Drupal\entity_test\EntityTestAccessControlHandler.
 | 
			
		||||
    $entity4 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'forbid_access']);
 | 
			
		||||
    $entity4->save();
 | 
			
		||||
    $selection[$entity4->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    // Add the selection to the tempstore just like DeleteAction would.
 | 
			
		||||
    $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm');
 | 
			
		||||
    $tempstore->set($this->account->id() . ':entity_test_mulrevpub', $selection);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/entity_test/delete');
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
    $assert->statusCodeEquals(200);
 | 
			
		||||
    $assert->elementTextContains('css', 'h1', 'Are you sure you want to delete these test entity - revisions, data table, and published interface entities?');
 | 
			
		||||
    $list_selector = '#entity-test-mulrevpub-delete-multiple-confirm-form > ul[data-drupal-selector="edit-entities"]';
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity1 (Original translation) - The following test entity - revisions, data table, and published interface translations will be deleted:');
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity2 spanish');
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity2 french');
 | 
			
		||||
    $assert->elementTextNotContains('css', $list_selector, 'entity3 spanish');
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity3 french');
 | 
			
		||||
    $delete_button = $this->getSession()->getPage()->findButton('Delete');
 | 
			
		||||
    $delete_button->click();
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
    $assert->addressEquals('/user/' . $this->account->id());
 | 
			
		||||
    $assert->responseContains('Deleted 6 items.');
 | 
			
		||||
    $assert->responseContains('1 item has not been deleted because you do not have the necessary permissions.');
 | 
			
		||||
 | 
			
		||||
    \Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache();
 | 
			
		||||
    $remaining_entities = EntityTestMulRevPub::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]);
 | 
			
		||||
    $this->assertCount(3, $remaining_entities);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the delete form for untranslatable entities.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUntranslatableEntities(): void {
 | 
			
		||||
    $selection = [];
 | 
			
		||||
 | 
			
		||||
    $entity1 = EntityTestRev::create(['type' => 'default', 'name' => 'entity1']);
 | 
			
		||||
    $entity1->save();
 | 
			
		||||
    $selection[$entity1->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    $entity2 = EntityTestRev::create(['type' => 'default', 'name' => 'entity2']);
 | 
			
		||||
    $entity2->save();
 | 
			
		||||
    $selection[$entity2->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    // This entity will be inaccessible because of
 | 
			
		||||
    // Drupal\entity_test\EntityTestAccessControlHandler.
 | 
			
		||||
    $entity3 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']);
 | 
			
		||||
    $entity3->save();
 | 
			
		||||
    $selection[$entity3->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    // This entity will be inaccessible because of
 | 
			
		||||
    // Drupal\entity_test\EntityTestAccessControlHandler.
 | 
			
		||||
    $entity4 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']);
 | 
			
		||||
    $entity4->save();
 | 
			
		||||
    $selection[$entity4->id()]['en'] = 'en';
 | 
			
		||||
 | 
			
		||||
    // Add the selection to the tempstore just like DeleteAction would.
 | 
			
		||||
    $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm');
 | 
			
		||||
    $tempstore->set($this->account->id() . ':entity_test_rev', $selection);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/entity_test_rev/delete_multiple');
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
    $assert->statusCodeEquals(200);
 | 
			
		||||
    $assert->elementTextContains('css', 'h1', 'Are you sure you want to delete these test entity - revisions entities?');
 | 
			
		||||
    $list_selector = '#entity-test-rev-delete-multiple-confirm-form > ul[data-drupal-selector="edit-entities"]';
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity1');
 | 
			
		||||
    $assert->elementTextContains('css', $list_selector, 'entity2');
 | 
			
		||||
    $delete_button = $this->getSession()->getPage()->findButton('Delete');
 | 
			
		||||
    $delete_button->click();
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
    $assert->addressEquals('/user/' . $this->account->id());
 | 
			
		||||
    $assert->responseContains('Deleted 2 items.');
 | 
			
		||||
    $assert->responseContains('2 items have not been deleted because you do not have the necessary permissions.');
 | 
			
		||||
 | 
			
		||||
    \Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache();
 | 
			
		||||
    $remaining_entities = EntityTestRev::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]);
 | 
			
		||||
    $this->assertCount(2, $remaining_entities);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,80 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestBundle;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestWithBundle;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that bundle tags are invalidated when entities change.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 */
 | 
			
		||||
class EntityBundleListCacheTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['cache_test', 'entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    EntityTestBundle::create([
 | 
			
		||||
      'id' => 'bundle_a',
 | 
			
		||||
      'label' => 'Bundle A',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    EntityTestBundle::create([
 | 
			
		||||
      'id' => 'bundle_b',
 | 
			
		||||
      'label' => 'Bundle B',
 | 
			
		||||
    ])->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that tags are invalidated when an entity with that bundle changes.
 | 
			
		||||
   */
 | 
			
		||||
  public function testBundleListingCache(): void {
 | 
			
		||||
    // Access to lists of test entities with each bundle.
 | 
			
		||||
    $bundle_a_url = Url::fromRoute('cache_test_list.bundle_tags', ['entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'bundle_a']);
 | 
			
		||||
    $bundle_b_url = Url::fromRoute('cache_test_list.bundle_tags', ['entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'bundle_b']);
 | 
			
		||||
    $this->drupalGet($bundle_a_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
 | 
			
		||||
    $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_a']);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($bundle_a_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
    $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_a']);
 | 
			
		||||
    $this->drupalGet($bundle_b_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
 | 
			
		||||
    $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_b']);
 | 
			
		||||
    $this->drupalGet($bundle_b_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
    $entity1 = EntityTestWithBundle::create(['type' => 'bundle_a', 'name' => 'entity1']);
 | 
			
		||||
    $entity1->save();
 | 
			
		||||
    // Check that tags are invalidated after creating an entity of the current
 | 
			
		||||
    // bundle.
 | 
			
		||||
    $this->drupalGet($bundle_a_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
 | 
			
		||||
    $this->drupalGet($bundle_a_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
    // Check that tags are not invalidated after creating an entity of a
 | 
			
		||||
    // different bundle than the current in the request.
 | 
			
		||||
    $this->drupalGet($bundle_b_url);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Uuid\Uuid;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that an entity with a UUID as ID can be managed.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 */
 | 
			
		||||
class EntityUuidIdTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['block', 'content_translation', 'entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->createLanguageFromLangcode('af');
 | 
			
		||||
    $this->enableContentTranslation('entity_test_uuid_id', 'entity_test_uuid_id');
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the user interface for the test entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUi(): void {
 | 
			
		||||
    $this->drupalLogin($this->createUser([
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'translate entity_test_uuid_id',
 | 
			
		||||
      'view test entity',
 | 
			
		||||
    ]));
 | 
			
		||||
 | 
			
		||||
    // Test adding an entity.
 | 
			
		||||
    $this->drupalGet('/entity_test_uuid_id/add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'Name' => 'Test entity with UUID ID',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('css', 'h1', 'Edit Test entity with UUID ID');
 | 
			
		||||
    $this->assertSession()->addressMatches('#^/entity_test_uuid_id/manage/' . Uuid::VALID_PATTERN . '/edit$#');
 | 
			
		||||
 | 
			
		||||
    // Test translating an entity.
 | 
			
		||||
    $this->clickLink('Translate');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'Name' => 'Afrikaans translation of test entity with UUID ID',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('css', 'h1', 'Afrikaans translation of test entity with UUID ID [Afrikaans translation]');
 | 
			
		||||
    $this->assertSession()->addressMatches('#^/af/entity_test_uuid_id/manage/' . Uuid::VALID_PATTERN . '/edit$#');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,389 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\RevisionLogInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRevPub;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests deleting a revision with revision delete form.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Form\RevisionDeleteForm
 | 
			
		||||
 */
 | 
			
		||||
class RevisionDeleteFormTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'entity_test_revlog',
 | 
			
		||||
    'dblog',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests title by whether entity supports revision creation dates.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPageTitle(): void {
 | 
			
		||||
    foreach (static::providerPageTitle() as $cases) {
 | 
			
		||||
      [$entityTypeId, $expectedQuestion] = $cases;
 | 
			
		||||
      $this->doTestPageTitle($entityTypeId, $expectedQuestion);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests title by whether entity supports revision creation dates.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   The entity type to test.
 | 
			
		||||
   * @param string $expectedQuestion
 | 
			
		||||
   *   The expected question/page title.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getQuestion
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestPageTitle(string $entityTypeId, string $expectedQuestion): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      'type' => $entityTypeId,
 | 
			
		||||
      'name' => 'delete revision',
 | 
			
		||||
    ]);
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
      $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Create a new latest revision.
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->assertSession()->pageTextContains($expectedQuestion);
 | 
			
		||||
    $this->assertSession()->buttonExists('Delete');
 | 
			
		||||
    $this->assertSession()->linkExists('Cancel');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testPageTitle.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerPageTitle(): array {
 | 
			
		||||
    return [
 | 
			
		||||
      ['entity_test_rev', 'Are you sure you want to delete the revision?'],
 | 
			
		||||
      ['entity_test_revlog', 'Are you sure you want to delete the revision from Sun, 11 Jan 2009 - 16:00?'],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cannot delete latest revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  public function testAccessDeleteLatestDefault(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create();
 | 
			
		||||
    $entity->setName('delete revision');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test that revisions can and can't be deleted in various scenarios.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAccessDelete(): void {
 | 
			
		||||
    $this->testAccessDeleteLatestForwardRevision();
 | 
			
		||||
    $this->testAccessDeleteDefault();
 | 
			
		||||
    $this->testAccessDeleteNonLatest();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensure that forward revision can be deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessDeleteLatestForwardRevision(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
 | 
			
		||||
    $entity = EntityTestRevPub::create();
 | 
			
		||||
    $entity->setName('delete revision');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->isDefaultRevision(TRUE);
 | 
			
		||||
    $entity->setPublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->isDefaultRevision(FALSE);
 | 
			
		||||
    $entity->setUnpublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertTrue($entity->access('delete revision', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cannot delete default revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessDeleteDefault(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
 | 
			
		||||
    $entity = EntityTestRevPub::create();
 | 
			
		||||
    $entity->setName('delete revision');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->isDefaultRevision(TRUE);
 | 
			
		||||
    $entity->setPublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->isDefaultRevision(FALSE);
 | 
			
		||||
    $entity->setUnpublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_revpub');
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRevPub $revision */
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    // Check default but not latest.
 | 
			
		||||
    $this->assertTrue($revision->isDefaultRevision());
 | 
			
		||||
    $this->assertFalse($revision->isLatestRevision());
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->assertFalse($revision->access('delete revision', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test can delete non-latest revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessDeleteNonLatest(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create();
 | 
			
		||||
    $entity->setName('delete revision');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity->isDefaultRevision();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_rev');
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertTrue($revision->access('delete revision', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests revision deletion form.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSubmitForm(): void {
 | 
			
		||||
    foreach (static::providerSubmitForm() as $case) {
 | 
			
		||||
      [$permissions, $entityTypeId, $entityLabel, $totalRevisions, $expectedLog, $expectedMessage, $expectedDestination] = $case;
 | 
			
		||||
      $this->doTestSubmitForm($permissions, $entityTypeId, $entityLabel, $totalRevisions, $expectedLog, $expectedMessage, $expectedDestination);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests revision deletion, and expected response after deletion.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $permissions
 | 
			
		||||
   *   If not empty, a user will be created and logged in with these
 | 
			
		||||
   *   permissions.
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   The entity type to test.
 | 
			
		||||
   * @param string $entityLabel
 | 
			
		||||
   *   The entity label, which corresponds to access grants.
 | 
			
		||||
   * @param int $totalRevisions
 | 
			
		||||
   *   Total number of revisions to create.
 | 
			
		||||
   * @param string $expectedLog
 | 
			
		||||
   *   Expected log.
 | 
			
		||||
   * @param string $expectedMessage
 | 
			
		||||
   *   Expected messenger message.
 | 
			
		||||
   * @param string|int $expectedDestination
 | 
			
		||||
   *   Expected destination after deletion.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::submitForm
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, int $totalRevisions, array $expectedLog, string $expectedMessage, $expectedDestination): void {
 | 
			
		||||
    if (count($permissions) > 0) {
 | 
			
		||||
      $this->drupalLogin($this->createUser($permissions));
 | 
			
		||||
    }
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      'type' => $entityTypeId,
 | 
			
		||||
      'name' => $entityLabel,
 | 
			
		||||
    ]);
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
      $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $otherRevisionIds = [];
 | 
			
		||||
    for ($i = 0; $i < $totalRevisions - 1; $i++) {
 | 
			
		||||
      if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
        $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
      }
 | 
			
		||||
      $entity->setNewRevision();
 | 
			
		||||
      $entity->save();
 | 
			
		||||
      $otherRevisionIds[] = $entity->getRevisionId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-delete-form'));
 | 
			
		||||
    $this->submitForm([], 'Delete');
 | 
			
		||||
 | 
			
		||||
    // The revision was deleted.
 | 
			
		||||
    $this->assertNull($storage->loadRevision($revisionId));
 | 
			
		||||
    // Make sure the other revisions were not deleted.
 | 
			
		||||
    foreach ($otherRevisionIds as $otherRevisionId) {
 | 
			
		||||
      $this->assertNotNull($storage->loadRevision($otherRevisionId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Destination.
 | 
			
		||||
    if ($expectedDestination === 404) {
 | 
			
		||||
      $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
      $this->assertSession()->addressEquals($expectedDestination);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Logger log.
 | 
			
		||||
    $logs = $this->getLogs($entity->getEntityType()->getProvider());
 | 
			
		||||
    $this->assertCount(1, $logs);
 | 
			
		||||
    $this->assertEquals("@type: deleted %title revision %revision.", $logs[0]->message);
 | 
			
		||||
    $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
 | 
			
		||||
    // Messenger message.
 | 
			
		||||
    $this->assertSession()->pageTextContains($expectedMessage);
 | 
			
		||||
    \Drupal::database()->delete('watchdog')->execute();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testSubmitForm.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerSubmitForm(): array {
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['not supporting revision log, one revision remaining after delete, no view access'] = [
 | 
			
		||||
      [],
 | 
			
		||||
      'entity_test_rev',
 | 
			
		||||
      'view all revisions, delete revision',
 | 
			
		||||
      2,
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_rev',
 | 
			
		||||
        '%title' => 'view all revisions, delete revision',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Revision of Entity Test Bundle view all revisions, delete revision has been deleted.',
 | 
			
		||||
      '/entity_test_rev/1/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['not supporting revision log, one revision remaining after delete, view access'] = [
 | 
			
		||||
      ['view test entity'],
 | 
			
		||||
      'entity_test_rev',
 | 
			
		||||
      'view, view all revisions, delete revision',
 | 
			
		||||
      2,
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_rev',
 | 
			
		||||
        '%title' => 'view, view all revisions, delete revision',
 | 
			
		||||
        '%revision' => '3',
 | 
			
		||||
      ],
 | 
			
		||||
      'Revision of Entity Test Bundle view, view all revisions, delete revision has been deleted.',
 | 
			
		||||
      '/entity_test_rev/2/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['supporting revision log, one revision remaining after delete, no view access'] = [
 | 
			
		||||
      [],
 | 
			
		||||
      'entity_test_revlog',
 | 
			
		||||
      'view all revisions, delete revision',
 | 
			
		||||
      2,
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_revlog',
 | 
			
		||||
        '%title' => 'view all revisions, delete revision',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view all revisions, delete revision has been deleted.',
 | 
			
		||||
      '/entity_test_revlog/1/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['supporting revision log, one revision remaining after delete, view access'] = [
 | 
			
		||||
      [],
 | 
			
		||||
      'entity_test_revlog',
 | 
			
		||||
      'view, view all revisions, delete revision',
 | 
			
		||||
      2,
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_revlog',
 | 
			
		||||
        '%title' => 'view, view all revisions, delete revision',
 | 
			
		||||
        '%revision' => '3',
 | 
			
		||||
      ],
 | 
			
		||||
      'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view, view all revisions, delete revision has been deleted.',
 | 
			
		||||
      '/entity_test_revlog/2/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads watchdog entries by channel.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $channel
 | 
			
		||||
   *   The logger channel.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string[]
 | 
			
		||||
   *   Watchdog entries.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getLogs(string $channel): array {
 | 
			
		||||
    return \Drupal::database()->select('watchdog')
 | 
			
		||||
      ->fields('watchdog')
 | 
			
		||||
      ->condition('type', $channel)
 | 
			
		||||
      ->execute()
 | 
			
		||||
      ->fetchAll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,393 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\RevisionLogInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRevPub;
 | 
			
		||||
use Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests reverting a revision with revision revert form.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Form\RevisionRevertForm
 | 
			
		||||
 */
 | 
			
		||||
class RevisionRevertFormTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'entity_test_revlog',
 | 
			
		||||
    'dblog',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test form revision revert.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFormRevisionRevert(): void {
 | 
			
		||||
    foreach (self::providerPageTitle() as $page_title) {
 | 
			
		||||
      $this->testPageTitle($page_title[0], $page_title[1]);
 | 
			
		||||
    }
 | 
			
		||||
    $this->testAccessRevertLatestDefault();
 | 
			
		||||
    $this->testAccessRevertLatestForwardRevision();
 | 
			
		||||
    $this->testAccessRevertNonLatest();
 | 
			
		||||
    $this->testPrepareRevision();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests title by whether entity supports revision creation dates.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   The entity type to test.
 | 
			
		||||
   * @param string $expectedQuestion
 | 
			
		||||
   *   The expected question/page title.
 | 
			
		||||
   */
 | 
			
		||||
  protected function testPageTitle(string $entityTypeId, string $expectedQuestion): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      'type' => $entityTypeId,
 | 
			
		||||
      'name' => 'revert',
 | 
			
		||||
    ]);
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
      $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Create a new latest revision.
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-revert-form'));
 | 
			
		||||
    $this->assertSession()->pageTextContains($expectedQuestion);
 | 
			
		||||
    $this->assertSession()->buttonExists('Revert');
 | 
			
		||||
    $this->assertSession()->linkExists('Cancel');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testPageTitle.
 | 
			
		||||
   */
 | 
			
		||||
  protected static function providerPageTitle(): array {
 | 
			
		||||
    return [
 | 
			
		||||
      ['entity_test_rev', 'Are you sure you want to revert the revision?'],
 | 
			
		||||
      ['entity_test_revlog', 'Are you sure you want to revert to the revision from Sun, 11 Jan 2009 - 16:00?'],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cannot revert latest default revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessRevertLatestDefault(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create();
 | 
			
		||||
    $entity->setName('revert');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('revision-revert-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->assertFalse($entity->access('revert', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that forward revisions can be reverted.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessRevertLatestForwardRevision(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRevPub::create();
 | 
			
		||||
    $entity->setName('revert');
 | 
			
		||||
    $entity->isDefaultRevision(TRUE);
 | 
			
		||||
    $entity->setPublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->isDefaultRevision(FALSE);
 | 
			
		||||
    $entity->setUnpublished();
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('revision-revert-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertTrue($entity->access('revert', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test can revert non-latest revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
 | 
			
		||||
   */
 | 
			
		||||
  protected function testAccessRevertNonLatest(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create();
 | 
			
		||||
    $entity->setName('revert');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the entity.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_rev');
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-revert-form'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertTrue($revision->access('revert', $this->rootUser, FALSE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests revision revert, and expected response after revert.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $permissions
 | 
			
		||||
   *   If not empty, a user will be created and logged in with these
 | 
			
		||||
   *   permissions.
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   The entity type to test.
 | 
			
		||||
   * @param string $entityLabel
 | 
			
		||||
   *   The entity label, which corresponds to access grants.
 | 
			
		||||
   * @param string $expectedLog
 | 
			
		||||
   *   Expected log.
 | 
			
		||||
   * @param string $expectedMessage
 | 
			
		||||
   *   Expected messenger message.
 | 
			
		||||
   * @param string $expectedDestination
 | 
			
		||||
   *   Expected destination after deletion.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::submitForm
 | 
			
		||||
   * @dataProvider providerSubmitForm
 | 
			
		||||
   */
 | 
			
		||||
  public function testSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, array $expectedLog, string $expectedMessage, string $expectedDestination): void {
 | 
			
		||||
    if (count($permissions) > 0) {
 | 
			
		||||
      $this->drupalLogin($this->createUser($permissions));
 | 
			
		||||
    }
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      'type' => $entityTypeId,
 | 
			
		||||
      'name' => $entityLabel,
 | 
			
		||||
    ]);
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
      $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision-revert-form'));
 | 
			
		||||
 | 
			
		||||
    $count = $this->countRevisions($entityTypeId);
 | 
			
		||||
    $this->submitForm([], 'Revert');
 | 
			
		||||
 | 
			
		||||
    // A new revision was created.
 | 
			
		||||
    $this->assertEquals($count + 1, $this->countRevisions($entityTypeId));
 | 
			
		||||
 | 
			
		||||
    // Destination.
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals($expectedDestination);
 | 
			
		||||
 | 
			
		||||
    // Logger log.
 | 
			
		||||
    $logs = $this->getLogs($entity->getEntityType()->getProvider());
 | 
			
		||||
    $this->assertCount(1, $logs);
 | 
			
		||||
    $this->assertEquals('@type: reverted %title revision %revision.', $logs[0]->message);
 | 
			
		||||
    $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
 | 
			
		||||
 | 
			
		||||
    // Messenger message.
 | 
			
		||||
    $this->assertSession()->pageTextContains($expectedMessage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testSubmitForm.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerSubmitForm(): array {
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['not supporting revision log, no version history access'] = [
 | 
			
		||||
      ['view test entity'],
 | 
			
		||||
      'entity_test_rev',
 | 
			
		||||
      'view, revert',
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_rev',
 | 
			
		||||
        '%title' => 'view, revert',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity Test Bundle view, revert has been reverted.',
 | 
			
		||||
      '/entity_test_rev/manage/1',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['not supporting revision log, version history access'] = [
 | 
			
		||||
      ['view test entity'],
 | 
			
		||||
      'entity_test_rev',
 | 
			
		||||
      'view, view all revisions, revert',
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_rev',
 | 
			
		||||
        '%title' => 'view, view all revisions, revert',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity Test Bundle view, view all revisions, revert has been reverted.',
 | 
			
		||||
      '/entity_test_rev/1/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['supporting revision log, no version history access'] = [
 | 
			
		||||
      [],
 | 
			
		||||
      'entity_test_revlog',
 | 
			
		||||
      'view, revert',
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_revlog',
 | 
			
		||||
        '%title' => 'view, revert',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Test entity - revisions log view, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
 | 
			
		||||
      '/entity_test_revlog/manage/1',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $data['supporting revision log, version history access'] = [
 | 
			
		||||
      [],
 | 
			
		||||
      'entity_test_revlog',
 | 
			
		||||
      'view, view all revisions, revert',
 | 
			
		||||
      [
 | 
			
		||||
        '@type' => 'entity_test_revlog',
 | 
			
		||||
        '%title' => 'view, view all revisions, revert',
 | 
			
		||||
        '%revision' => '1',
 | 
			
		||||
      ],
 | 
			
		||||
      'Test entity - revisions log view, view all revisions, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
 | 
			
		||||
      '/entity_test_revlog/1/revisions',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the revert process.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::prepareRevision
 | 
			
		||||
   */
 | 
			
		||||
  protected function testPrepareRevision(): void {
 | 
			
		||||
    $user = $this->createUser();
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create([
 | 
			
		||||
      'type' => 'entity_test_revlog',
 | 
			
		||||
      'name' => 'revert',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
    $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    $entity->isDefaultRevision(TRUE);
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $revisionCreationTime = $date->modify('+1 hour')->getTimestamp();
 | 
			
		||||
    $entity->setRevisionCreationTime($revisionCreationTime);
 | 
			
		||||
    $entity->setRevisionUserId(0);
 | 
			
		||||
    $entity->isDefaultRevision(FALSE);
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $targetRevertRevisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Create a another revision so the previous revision can be reverted to.
 | 
			
		||||
    $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
    $entity->isDefaultRevision(FALSE);
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $count = $this->countRevisions($entity->getEntityTypeId());
 | 
			
		||||
 | 
			
		||||
    // Load the revision to be copied.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $targetRevision */
 | 
			
		||||
    $targetRevision = $storage->loadRevision($targetRevertRevisionId);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($targetRevision->toUrl('revision-revert-form'));
 | 
			
		||||
    $this->submitForm([], 'Revert');
 | 
			
		||||
 | 
			
		||||
    // Load the new latest revision.
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $latestRevision */
 | 
			
		||||
    $latestRevision = $storage->loadUnchanged($entity->id());
 | 
			
		||||
    $this->assertEquals($count + 1, $this->countRevisions($entity->getEntityTypeId()));
 | 
			
		||||
    $this->assertEquals('Copy of the revision from <em class="placeholder">Sun, 11 Jan 2009 - 17:00</em>.', $latestRevision->getRevisionLogMessage());
 | 
			
		||||
    $this->assertGreaterThan($revisionCreationTime, $latestRevision->getRevisionCreationTime());
 | 
			
		||||
    $this->assertEquals($user->id(), $latestRevision->getRevisionUserId());
 | 
			
		||||
    $this->assertTrue($latestRevision->isDefaultRevision());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads watchdog entries by channel.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $channel
 | 
			
		||||
   *   The logger channel.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string[]
 | 
			
		||||
   *   Watchdog entries.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getLogs(string $channel): array {
 | 
			
		||||
    return \Drupal::database()->select('watchdog')
 | 
			
		||||
      ->fields('watchdog')
 | 
			
		||||
      ->condition('type', $channel)
 | 
			
		||||
      ->execute()
 | 
			
		||||
      ->fetchAll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Count number of revisions for an entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   The entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @return int
 | 
			
		||||
   *   Number of revisions for an entity type.
 | 
			
		||||
   */
 | 
			
		||||
  protected function countRevisions(string $entityTypeId): int {
 | 
			
		||||
    return (int) \Drupal::entityTypeManager()->getStorage($entityTypeId)
 | 
			
		||||
      ->getQuery()
 | 
			
		||||
      ->accessCheck(FALSE)
 | 
			
		||||
      ->allRevisions()
 | 
			
		||||
      ->count()
 | 
			
		||||
      ->execute();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests revision route provider.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider
 | 
			
		||||
 */
 | 
			
		||||
class RevisionRouteProviderTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests title is from revision in context.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRevisionTitle(): void {
 | 
			
		||||
    $entity = EntityTestRev::create();
 | 
			
		||||
    $entity
 | 
			
		||||
      ->setName('first revision, view revision')
 | 
			
		||||
      ->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // A default revision is created to ensure it is not pulled from the
 | 
			
		||||
    // non-revision entity parameter.
 | 
			
		||||
    $entity
 | 
			
		||||
      ->setName('second revision, view revision')
 | 
			
		||||
      ->setNewRevision();
 | 
			
		||||
    $entity->isDefaultRevision(TRUE);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the object.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_rev');
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision'));
 | 
			
		||||
    $this->assertSession()->responseContains('first revision');
 | 
			
		||||
    $this->assertSession()->responseNotContains('second revision');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,352 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Controller\VersionHistoryController;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests version history page.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Controller\VersionHistoryController
 | 
			
		||||
 */
 | 
			
		||||
class RevisionVersionHistoryTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'entity_test_revlog',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test all revisions appear, in order of revision creation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOrder(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    // Need label to be able to assert order.
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $user = $this->drupalCreateUser([], 'first revision');
 | 
			
		||||
    $entity->setRevisionUser($user);
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $user = $this->drupalCreateUser([], 'second revision');
 | 
			
		||||
    $entity->setRevisionUser($user);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $user = $this->drupalCreateUser([], 'third revision');
 | 
			
		||||
    $entity->setRevisionUser($user);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
    // Order is newest to oldest revision by creation order.
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'third revision');
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', 'second revision');
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', 'first revision');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test current revision is indicated.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\Core\Entity\Controller\VersionHistoryController::revisionOverview
 | 
			
		||||
   */
 | 
			
		||||
  public function testCurrentRevision(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create(['type' => 'entity_test_rev']);
 | 
			
		||||
    // Need label to be able to assert order.
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
    // Current revision text is found on the latest revision row.
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision');
 | 
			
		||||
    $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision');
 | 
			
		||||
    $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(3)', 'Current revision');
 | 
			
		||||
    // Current revision row has 'revision-current' class.
 | 
			
		||||
    $this->assertSession()->elementAttributeContains('css', 'table tbody tr:nth-child(1)', 'class', 'revision-current');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test description with entity implementing revision log.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionRevLog(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $user = $this->drupalCreateUser([], $this->randomMachineName());
 | 
			
		||||
    $entity->setRevisionUser($user);
 | 
			
		||||
    $entity->setRevisionCreationTime((new \DateTime('2 February 2013 4:00:00pm'))->getTimestamp());
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', '2 Feb 2013 - 16:00');
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', $user->getAccountName());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test description with entity implementing revision log, with empty values.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionRevLogNullValues(): void {
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions')->save();
 | 
			
		||||
 | 
			
		||||
    // Check entity values are still null after saving; they did not receive
 | 
			
		||||
    // values from currentUser or some other global context.
 | 
			
		||||
    $this->assertNull($entity->getRevisionUser());
 | 
			
		||||
    $this->assertNull($entity->getRevisionUserId());
 | 
			
		||||
    $this->assertNull($entity->getRevisionLogMessage());
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'by Anonymous (not verified)');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test description with entity, without revision log, no label access.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionNoRevLogNoLabelAccess(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create(['type' => 'entity_test_rev']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', '- Restricted access -');
 | 
			
		||||
    $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(1)', $entity->getName());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test description with entity, without revision log, with label access.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionNoRevLogWithLabelAccess(): void {
 | 
			
		||||
    // Permission grants 'view label' access.
 | 
			
		||||
    $this->drupalLogin($this->createUser(['view test entity']));
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create(['type' => 'entity_test_rev']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', $entity->getName());
 | 
			
		||||
    $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(1)', '- Restricted access -');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test revision link, without access to revision page.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionLinkNoAccess(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 1);
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr a', 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test revision link, with access to revision page.
 | 
			
		||||
   *
 | 
			
		||||
   * Test two revisions. Usually the latest revision only checks canonical
 | 
			
		||||
   * route access, whereas all others will check individual revision access.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionLinkWithAccess(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    // Revision has access to individual revision.
 | 
			
		||||
    $entity->setName('view all revisions, view revision');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $firstRevisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Revision has access to canonical route.
 | 
			
		||||
    $entity->setName('view all revisions, view');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $row1Link = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1) a');
 | 
			
		||||
    $this->assertEquals($entity->toUrl()->toString(), $row1Link->getAttribute('href'));
 | 
			
		||||
    // Reload revision so object has the properties to build a revision link.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_revlog');
 | 
			
		||||
    $firstRevision = $storage->loadRevision($firstRevisionId);
 | 
			
		||||
    $row2Link = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2) a');
 | 
			
		||||
    $this->assertEquals($firstRevision->toUrl('revision')->toString(), $row2Link->getAttribute('href'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test revision log message if supported, and HTML tags are stripped.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getRevisionDescription
 | 
			
		||||
   */
 | 
			
		||||
  public function testDescriptionRevisionLogMessage(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->setRevisionLogMessage('<em>Hello</em> <script>world</script> <strong>123</strong>');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    // Script tags are stripped, while admin-safe tags are retained.
 | 
			
		||||
    $this->assertSession()->elementContains('css', 'table tbody tr:nth-child(1)', '<em>Hello</em> world <strong>123</strong>');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test revert operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::buildRevertRevisionLink
 | 
			
		||||
   */
 | 
			
		||||
  public function testOperationRevertRevision(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setName('view all revisions, revert');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setName('view all revisions, revert');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
 | 
			
		||||
    // Latest revision does not have revert revision operation: reverting latest
 | 
			
		||||
    // revision is not permitted.
 | 
			
		||||
    $row1 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1)');
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision');
 | 
			
		||||
    $this->assertSession()->elementNotExists('named', ['link', 'Revert'], $row1);
 | 
			
		||||
 | 
			
		||||
    // Revision 2 has revert revision operation: granted access.
 | 
			
		||||
    $row2 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2)');
 | 
			
		||||
    $this->assertSession()->elementExists('named', ['link', 'Revert'], $row2);
 | 
			
		||||
 | 
			
		||||
    // Revision 3 does not have revert revision operation: no access.
 | 
			
		||||
    $row3 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(3)');
 | 
			
		||||
    $this->assertSession()->elementNotExists('named', ['link', 'Revert'], $row3);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test delete operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::buildDeleteRevisionLink
 | 
			
		||||
   */
 | 
			
		||||
  public function testOperationDeleteRevision(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test_revlog\Entity\EntityTestWithRevisionLog $entity */
 | 
			
		||||
    $entity = EntityTestWithRevisionLog::create(['type' => 'entity_test_revlog']);
 | 
			
		||||
    $entity->setName('view all revisions');
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setName('view all revisions, delete revision');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->setName('view all revisions, delete revision');
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
 | 
			
		||||
    // Latest revision does not have delete revision operation: deleting latest
 | 
			
		||||
    // revision is not permitted.
 | 
			
		||||
    $row1 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1)');
 | 
			
		||||
    $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision');
 | 
			
		||||
    $this->assertSession()->elementNotExists('named', ['link', 'Delete'], $row1);
 | 
			
		||||
 | 
			
		||||
    // Revision 2 has delete revision operation: granted access.
 | 
			
		||||
    $row2 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2)');
 | 
			
		||||
    $this->assertSession()->elementExists('named', ['link', 'Delete'], $row2);
 | 
			
		||||
 | 
			
		||||
    // Revision 3 does not have delete revision operation: no access.
 | 
			
		||||
    $row3 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(3)');
 | 
			
		||||
    $this->assertSession()->elementNotExists('named', ['link', 'Delete'], $row3);
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test revisions are paginated.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRevisionsPagination(): void {
 | 
			
		||||
    /** @var \Drupal\entity_test\Entity\EntityTestRev $entity */
 | 
			
		||||
    $entity = EntityTestRev::create([
 | 
			
		||||
      'type' => 'entity_test_rev',
 | 
			
		||||
      'name' => 'view all revisions,view revision',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $firstRevisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    for ($i = 0; $i < VersionHistoryController::REVISIONS_PER_PAGE; $i++) {
 | 
			
		||||
      $entity->setNewRevision(TRUE);
 | 
			
		||||
      // We need to change something on the entity for it to be considered a new
 | 
			
		||||
      // revision to display. We need "view all revisions" and "view revision"
 | 
			
		||||
      // in a comma separated string to grant access.
 | 
			
		||||
      $entity->setName('view all revisions,view revision,' . $i)->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', VersionHistoryController::REVISIONS_PER_PAGE);
 | 
			
		||||
    $this->assertSession()->elementExists('css', '.pager');
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage($entity->getEntityTypeId());
 | 
			
		||||
    $firstRevision = $storage->loadRevision($firstRevisionId);
 | 
			
		||||
    $secondRevision = $storage->loadRevision($firstRevisionId + 1);
 | 
			
		||||
    // We should see everything up to the second revision, but not the first.
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($secondRevision->toUrl('revision')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($firstRevision->toUrl('revision')->toString());
 | 
			
		||||
    // The next page should show just the first revision.
 | 
			
		||||
    $this->clickLink('Go to next page');
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 1);
 | 
			
		||||
    $this->assertSession()->elementExists('css', '.pager');
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($secondRevision->toUrl('revision')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($firstRevision->toUrl('revision')->toString());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,84 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests version history page with translations.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Controller\VersionHistoryController
 | 
			
		||||
 */
 | 
			
		||||
final class RevisionVersionHistoryTranslatableTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test_revlog',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'language',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('es')->save();
 | 
			
		||||
 | 
			
		||||
    // Rebuild the container so that the new languages are picked up by services
 | 
			
		||||
    // that hold a list of languages.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the version history page for translations.
 | 
			
		||||
   */
 | 
			
		||||
  public function testVersionHistoryTranslations(): void {
 | 
			
		||||
    $label = 'view all revisions,revert,delete revision';
 | 
			
		||||
    $entity = EntityTestMulWithRevisionLog::create([
 | 
			
		||||
      'name' => $label,
 | 
			
		||||
      'type' => 'entity_test_mul_revlog',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->addTranslation('es', ['label' => 'version history test translations es']);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $firstRevisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->setName($label . ',2')
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->elementsCount('css', 'table tbody tr', 2);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage($entity->getEntityTypeId());
 | 
			
		||||
    $firstRevision = $storage->loadRevision($firstRevisionId);
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($firstRevision->toUrl('revision-revert-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($firstRevision->toUrl('revision-delete-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($firstRevision->getTranslation('es')->toUrl('revision-revert-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($firstRevision->getTranslation('es')->toUrl('revision-delete-form')->toString());
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->getTranslation('es')->toUrl('version-history'));
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExistsExact($firstRevision->toUrl('revision-revert-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExistsExact($firstRevision->toUrl('revision-delete-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($firstRevision->getTranslation('es')->toUrl('revision-revert-form')->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($firstRevision->getTranslation('es')->toUrl('revision-delete-form')->toString());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,118 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\RevisionLogInterface;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests revision view page.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Entity
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Entity\Controller\EntityRevisionViewController
 | 
			
		||||
 */
 | 
			
		||||
class RevisionViewTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'entity_test_revlog',
 | 
			
		||||
    'field',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests revision page.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entityTypeId
 | 
			
		||||
   *   Entity type to test.
 | 
			
		||||
   * @param string $expectedPageTitle
 | 
			
		||||
   *   Expected page title.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::__invoke
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerRevisionPage
 | 
			
		||||
   */
 | 
			
		||||
  public function testRevisionPage(string $entityTypeId, string $expectedPageTitle): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Add a field to test revision page output.
 | 
			
		||||
    $fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'entity_type' => $entityTypeId,
 | 
			
		||||
      'field_name' => 'foo',
 | 
			
		||||
      'type' => 'string',
 | 
			
		||||
    ]);
 | 
			
		||||
    $fieldStorage->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_storage' => $fieldStorage,
 | 
			
		||||
      'bundle' => $entityTypeId,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $displayRepository */
 | 
			
		||||
    $displayRepository = \Drupal::service('entity_display.repository');
 | 
			
		||||
    $displayRepository->getViewDisplay($entityTypeId, $entityTypeId)
 | 
			
		||||
      ->setComponent('foo', [
 | 
			
		||||
        'type' => 'string',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $entity = $storage->create(['type' => $entityTypeId]);
 | 
			
		||||
    $entity->setName('revision 1, view revision');
 | 
			
		||||
    $revision1Body = $this->randomMachineName();
 | 
			
		||||
    $entity->foo = $revision1Body;
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $date = new \DateTime('11 January 2009 4:00:00pm');
 | 
			
		||||
      $entity->setRevisionCreationTime($date->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revisionId = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setName('revision 2, view revision');
 | 
			
		||||
    $revision2Body = $this->randomMachineName();
 | 
			
		||||
    $entity->foo = $revision2Body;
 | 
			
		||||
    if ($entity instanceof RevisionLogInterface) {
 | 
			
		||||
      $entity->setRevisionCreationTime($date->modify('+1 hour')->getTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
    $entity->setNewRevision();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $revision = $storage->loadRevision($revisionId);
 | 
			
		||||
    $this->drupalGet($revision->toUrl('revision'));
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->pageTextContains($expectedPageTitle);
 | 
			
		||||
    $this->assertSession()->pageTextContains($revision1Body);
 | 
			
		||||
    $this->assertSession()->pageTextNotContains($revision2Body);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testRevisionPage.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerRevisionPage(): array {
 | 
			
		||||
    return [
 | 
			
		||||
      ['entity_test_rev', 'Revision of revision 1, view revision'],
 | 
			
		||||
      ['entity_test_revlog', 'Revision of revision 1, view revision from Sun, 11 Jan 2009 - 16:00'],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,109 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\EventSubscriber;
 | 
			
		||||
 | 
			
		||||
use Drupal\file\Entity\File;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the fast 404 functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group EventSubscriber
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\Core\EventSubscriber\Fast404ExceptionHtmlSubscriber
 | 
			
		||||
 */
 | 
			
		||||
class Fast404Test extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['file'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the fast 404 functionality.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFast404(): void {
 | 
			
		||||
    $this->drupalGet('does-not-exist');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    // Regular 404s will contain CSS from the system module.
 | 
			
		||||
    $this->assertSession()->responseContains('modules/system/css/');
 | 
			
		||||
    $this->drupalGet('does-not-exist.txt');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    // Fast 404s do not have any CSS.
 | 
			
		||||
    $this->assertSession()->responseNotContains('modules/system/css/');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache', 'Miss');
 | 
			
		||||
    // Fast 404s can be cached.
 | 
			
		||||
    $this->drupalGet('does-not-exist.txt');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache', 'Hit');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Oops I did it again!');
 | 
			
		||||
 | 
			
		||||
    // Changing configuration should invalidate the cache.
 | 
			
		||||
    $this->config('system.performance')->set('fast_404.html', '<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>Oops I did it again!</h1><p>The requested URL "@path" was not found on this server.</p></body></html>')->save();
 | 
			
		||||
    $this->drupalGet('does-not-exist.txt');
 | 
			
		||||
    $this->assertSession()->responseNotContains('modules/system/css/');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache', 'Miss');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Oops I did it again!');
 | 
			
		||||
 | 
			
		||||
    // Ensure disabling works.
 | 
			
		||||
    $this->config('system.performance')->set('fast_404.enabled', FALSE)->save();
 | 
			
		||||
    $this->drupalGet('does-not-exist.txt');
 | 
			
		||||
    $this->assertSession()->responseContains('modules/system/css/');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache', 'Miss');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Oops I did it again!');
 | 
			
		||||
 | 
			
		||||
    // Ensure settings.php can override settings.
 | 
			
		||||
    $settings['config']['system.performance']['fast_404']['enabled'] = (object) [
 | 
			
		||||
      'value' => TRUE,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
    // Changing settings using an override means we need to rebuild everything.
 | 
			
		||||
    $this->rebuildAll();
 | 
			
		||||
    $this->drupalGet('does-not-exist.txt');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    $this->assertSession()->responseNotContains('modules/system/css/');
 | 
			
		||||
    // Fast 404s returned via the exception subscriber still have the
 | 
			
		||||
    // X-Generator header.
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('X-Generator', 'Drupal');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the fast 404 functionality.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFast404PrivateFiles(): void {
 | 
			
		||||
    $admin = $this->createUser([], NULL, TRUE);
 | 
			
		||||
    $this->drupalLogin($admin);
 | 
			
		||||
 | 
			
		||||
    $file_url = 'system/files/test/private-file-test.txt';
 | 
			
		||||
    $this->drupalGet($file_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
    $this->drupalGet($file_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(404);
 | 
			
		||||
 | 
			
		||||
    // Create a private file for testing accessible by the admin user.
 | 
			
		||||
    \Drupal::service('file_system')->mkdir($this->privateFilesDirectory . '/test');
 | 
			
		||||
    $filepath = 'private://test/private-file-test.txt';
 | 
			
		||||
    $contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data.";
 | 
			
		||||
    file_put_contents($filepath, $contents);
 | 
			
		||||
    $file = File::create([
 | 
			
		||||
      'uri' => $filepath,
 | 
			
		||||
      'uid' => $admin->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $file->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($file_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextContains($contents);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Connection;
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @group Database
 | 
			
		||||
 */
 | 
			
		||||
class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $driver = Database::getConnection()->driver();
 | 
			
		||||
    if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) {
 | 
			
		||||
      $this->markTestSkipped("This test does not support the {$driver} database driver.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $filename = $this->siteDirectory . '/settings.php';
 | 
			
		||||
    chmod($filename, 0777);
 | 
			
		||||
    $contents = file_get_contents($filename);
 | 
			
		||||
 | 
			
		||||
    $autoload = "'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/',";
 | 
			
		||||
    $contents = str_replace($autoload, '', $contents);
 | 
			
		||||
    $namespace_search = "'namespace' => 'Drupal\\\\$driver\\\\Driver\\\\Database\\\\$driver',";
 | 
			
		||||
    $namespace_replace = "'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',";
 | 
			
		||||
    $contents = str_replace($namespace_search, $namespace_replace, $contents);
 | 
			
		||||
 | 
			
		||||
    // Add a replica connection to the database settings.
 | 
			
		||||
    $contents .= "\$databases['default']['replica'][] = array (\n";
 | 
			
		||||
    $contents .= "  'database' => 'db',\n";
 | 
			
		||||
    $contents .= "  'username' => 'db',\n";
 | 
			
		||||
    $contents .= "  'password' => 'db',\n";
 | 
			
		||||
    $contents .= "  'prefix' => 'test22806835',\n";
 | 
			
		||||
    $contents .= "  'host' => 'db',\n";
 | 
			
		||||
    $contents .= "  'port' => 3306,\n";
 | 
			
		||||
    $contents .= "  $namespace_replace\n";
 | 
			
		||||
    $contents .= "  'driver' => 'mysql',\n";
 | 
			
		||||
    $contents .= ");\n";
 | 
			
		||||
 | 
			
		||||
    file_put_contents($filename, $contents);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the site works with Drupal 8 style database connection array.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExistingDrupal8StyleDatabaseConnectionInSettingsPhp(): void {
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser());
 | 
			
		||||
    $this->assertSession()->addressEquals('user/2');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Make sure that we are have tested with the Drupal 8 style database
 | 
			
		||||
    // connection array.
 | 
			
		||||
    $filename = $this->siteDirectory . '/settings.php';
 | 
			
		||||
    $contents = file_get_contents($filename);
 | 
			
		||||
    $driver = Database::getConnection()->driver();
 | 
			
		||||
    $this->assertStringContainsString("'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',", $contents);
 | 
			
		||||
    $this->assertStringContainsString("'driver' => '$driver',", $contents);
 | 
			
		||||
    $this->assertStringNotContainsString("'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/", $contents);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the replica database connection works.
 | 
			
		||||
   */
 | 
			
		||||
  public function testReplicaDrupal8StyleDatabaseConnectionInSettingsPhp(): void {
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser());
 | 
			
		||||
 | 
			
		||||
    $replica = Database::getConnection('replica', 'default');
 | 
			
		||||
    $this->assertInstanceOf(Connection::class, $replica);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								web/core/tests/Drupal/FunctionalTests/FolderTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/core/tests/Drupal/FunctionalTests/FolderTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests BrowserTestBase's treatment of hook_install() during setup.
 | 
			
		||||
 *
 | 
			
		||||
 * Image module is used for test.
 | 
			
		||||
 *
 | 
			
		||||
 * @group browsertestbase
 | 
			
		||||
 */
 | 
			
		||||
class FolderTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['image'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  public function testFolderSetup(): void {
 | 
			
		||||
    $directory = 'public://styles';
 | 
			
		||||
    $this->assertTrue(\Drupal::service('file_system')->prepareDirectory($directory, FALSE), 'Directory created.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test for BrowserTestBase::getTestMethodCaller() in child classes.
 | 
			
		||||
 *
 | 
			
		||||
 * @group browsertestbase
 | 
			
		||||
 */
 | 
			
		||||
class GetTestMethodCallerExtendsTest extends GetTestMethodCallerTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A test method that is not present in the parent class.
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetTestMethodCallerChildClass(): void {
 | 
			
		||||
    $method_caller = $this->getTestMethodCaller();
 | 
			
		||||
    $expected = [
 | 
			
		||||
      'file' => __FILE__,
 | 
			
		||||
      'line' => 25,
 | 
			
		||||
      'function' => __CLASS__ . '->' . __FUNCTION__ . '()',
 | 
			
		||||
      'class' => BrowserTestBase::class,
 | 
			
		||||
      'object' => $this,
 | 
			
		||||
      'type' => '->',
 | 
			
		||||
      'args' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected, $method_caller);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Explicit test for BrowserTestBase::getTestMethodCaller().
 | 
			
		||||
 *
 | 
			
		||||
 * @group browsertestbase
 | 
			
		||||
 */
 | 
			
		||||
class GetTestMethodCallerTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests BrowserTestBase::getTestMethodCaller().
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetTestMethodCaller(): void {
 | 
			
		||||
    $method_caller = $this->getTestMethodCaller();
 | 
			
		||||
    $expected = [
 | 
			
		||||
      'file' => __FILE__,
 | 
			
		||||
      'line' => 25,
 | 
			
		||||
      'function' => __CLASS__ . '->' . __FUNCTION__ . '()',
 | 
			
		||||
      'class' => BrowserTestBase::class,
 | 
			
		||||
      'object' => $this,
 | 
			
		||||
      'type' => '->',
 | 
			
		||||
      'args' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected, $method_caller);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\HttpKernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Content-Length set by Drupal.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Http
 | 
			
		||||
 */
 | 
			
		||||
class ContentLengthTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'http_middleware_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  public function testContentLength(): void {
 | 
			
		||||
    // Fire off a request.
 | 
			
		||||
    $this->drupalGet(Url::fromRoute('http_middleware_test.test_response'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Content-Length', '40');
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('no-alter-content-length', TRUE);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Fire the same exact request but this time length is different.
 | 
			
		||||
    $this->drupalGet(Url::fromRoute('http_middleware_test.test_response'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Content-Length', '41');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,173 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\HttpKernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests CORS provided by Drupal.
 | 
			
		||||
 *
 | 
			
		||||
 * @see sites/default/default.services.yml
 | 
			
		||||
 * @see \Asm89\Stack\Cors
 | 
			
		||||
 * @see \Asm89\Stack\CorsService
 | 
			
		||||
 *
 | 
			
		||||
 * @group Http
 | 
			
		||||
 */
 | 
			
		||||
class CorsIntegrationTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'test_page_test', 'page_cache'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  public function testCrossSiteRequest(): void {
 | 
			
		||||
    // Test default parameters.
 | 
			
		||||
    $cors_config = $this->container->getParameter('cors.config');
 | 
			
		||||
    $this->assertFalse($cors_config['enabled']);
 | 
			
		||||
    $this->assertSame([], $cors_config['allowedHeaders']);
 | 
			
		||||
    $this->assertSame([], $cors_config['allowedMethods']);
 | 
			
		||||
    $this->assertSame(['*'], $cors_config['allowedOrigins']);
 | 
			
		||||
 | 
			
		||||
    $this->assertFalse($cors_config['exposedHeaders']);
 | 
			
		||||
    $this->assertFalse($cors_config['maxAge']);
 | 
			
		||||
    $this->assertFalse($cors_config['supportsCredentials']);
 | 
			
		||||
 | 
			
		||||
    // Enable CORS with the default options.
 | 
			
		||||
    $cors_config['enabled'] = TRUE;
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('cors.config', $cors_config);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Fire off a request.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', '*');
 | 
			
		||||
    $this->assertSession()->responseHeaderNotContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Fire the same exact request. This time it should be cached.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', '*');
 | 
			
		||||
    $this->assertSession()->responseHeaderNotContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Fire a request for a different origin. Verify the CORS header.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.org']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', '*');
 | 
			
		||||
    $this->assertSession()->responseHeaderNotContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Configure the CORS stack to match allowed origins using regex patterns.
 | 
			
		||||
    $cors_config['allowedOrigins'] = [];
 | 
			
		||||
    $cors_config['allowedOriginsPatterns'] = ['#^http://[a-z-]*\.valid.com$#'];
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('cors.config', $cors_config);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Fire a request from an origin that isn't allowed.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://non-valid.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderDoesNotExist('Access-Control-Allow-Origin');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://sub-domain.valid.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://sub-domain.valid.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Test combining allowedOrigins and allowedOriginsPatterns.
 | 
			
		||||
    $cors_config['allowedOrigins'] = ['http://domainA.com'];
 | 
			
		||||
    $cors_config['allowedOriginsPatterns'] = ['#^http://domain[B-Z-]*\.com$#'];
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('cors.config', $cors_config);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Specify an origin that does not match allowedOrigins nor
 | 
			
		||||
    // allowedOriginsPattern.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://non-valid.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderDoesNotExist('Access-Control-Allow-Origin');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin that matches allowedOrigins.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://domainA.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://domainA.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin that matches allowedOriginsPatterns.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://domainX.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://domainX.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Configure the CORS stack to allow a specific origin.
 | 
			
		||||
    $cors_config['allowedOrigins'] = ['http://example.com'];
 | 
			
		||||
    $cors_config['allowedOriginsPatterns'] = [];
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('cors.config', $cors_config);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Fire a request from an origin that isn't allowed.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://non-valid.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderNotContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderNotContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Configure the CORS stack to allow a specific set of origins.
 | 
			
		||||
    $cors_config['allowedOrigins'] = ['http://example.com', 'https://drupal.org'];
 | 
			
		||||
 | 
			
		||||
    $this->setContainerParameter('cors.config', $cors_config);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Fire a request from an origin that isn't allowed.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://non-valid.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', NULL);
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.com');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Specify a valid origin.
 | 
			
		||||
    $this->drupalGet('/test-page', [], ['Origin' => 'https://drupal.org']);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'https://drupal.org');
 | 
			
		||||
    $this->assertSession()->responseHeaderContains('Vary', 'Origin');
 | 
			
		||||
 | 
			
		||||
    // Verify POST still functions with 'Origin' header set to site's domain.
 | 
			
		||||
    $origin = \Drupal::request()->getSchemeAndHttpHost();
 | 
			
		||||
 | 
			
		||||
    /** @var \GuzzleHttp\ClientInterface $httpClient */
 | 
			
		||||
    $httpClient = $this->getSession()->getDriver()->getClient()->getClient();
 | 
			
		||||
    $url = Url::fromUri('base:/test-page');
 | 
			
		||||
    /** @var \Symfony\Component\HttpFoundation\Response $response */
 | 
			
		||||
    $response = $httpClient->request('POST', $url->setAbsolute()->toString(), [
 | 
			
		||||
      'headers' => [
 | 
			
		||||
        'Origin' => $origin,
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertEquals(200, $response->getStatusCode());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\HttpKernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\File\FileSystemInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests invocation of services performing deferred tasks after response flush.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\Core\DestructableInterface
 | 
			
		||||
 *
 | 
			
		||||
 * @group Http
 | 
			
		||||
 */
 | 
			
		||||
class DestructableServiceTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'destructable_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  public function testDestructableServiceExecutionOrder(): void {
 | 
			
		||||
    $file_system = $this->container->get('file_system');
 | 
			
		||||
    assert($file_system instanceof FileSystemInterface);
 | 
			
		||||
    $semaphore = $file_system
 | 
			
		||||
      ->tempnam($file_system->getTempDirectory(), 'destructable_semaphore');
 | 
			
		||||
    $this->drupalGet(Url::fromRoute('destructable', [], ['query' => ['semaphore' => $semaphore]]));
 | 
			
		||||
    // This should be false as the response should flush before running the
 | 
			
		||||
    // test service.
 | 
			
		||||
    $this->assertEmpty(file_get_contents($semaphore), 'Destructable service did not run when response flushed to client.');
 | 
			
		||||
    // The destructable service will sleep for 3 seconds, then run.
 | 
			
		||||
    // To ensure no race conditions on slow test runners, wait another 3s.
 | 
			
		||||
    sleep(6);
 | 
			
		||||
    $this->assertTrue(file_get_contents($semaphore) === 'ran', 'Destructable service did run.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,89 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Image;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests image toolkit setup form.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Image
 | 
			
		||||
 */
 | 
			
		||||
class ToolkitSetupFormTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Admin user account.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\Entity\User
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'image', 'image_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser([
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests Image toolkit setup form.
 | 
			
		||||
   */
 | 
			
		||||
  public function testToolkitSetupForm(): void {
 | 
			
		||||
    // Get form.
 | 
			
		||||
    $this->drupalGet('admin/config/media/image-toolkit');
 | 
			
		||||
 | 
			
		||||
    // Test that default toolkit is GD.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('image_toolkit', 'gd');
 | 
			
		||||
 | 
			
		||||
    // Test changing the jpeg image quality.
 | 
			
		||||
    $edit = ['gd[image_jpeg_quality]' => '70'];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->assertEquals('70', $this->config('system.image.gd')->get('jpeg_quality'));
 | 
			
		||||
 | 
			
		||||
    // Test changing the toolkit.
 | 
			
		||||
    $edit = ['image_toolkit' => 'test'];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->assertEquals('test', $this->config('system.image')->get('toolkit'));
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('test[test_parameter]', '10');
 | 
			
		||||
 | 
			
		||||
    // Test changing the test toolkit parameter.
 | 
			
		||||
    $edit = ['test[test_parameter]' => '0'];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test parameter should be different from 0.');
 | 
			
		||||
    $edit = ['test[test_parameter]' => '20'];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->assertEquals('20', $this->config('system.image.test_toolkit')->get('test_parameter'));
 | 
			
		||||
 | 
			
		||||
    // Test access without the permission 'administer site configuration'.
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser(['access administration pages']));
 | 
			
		||||
    $this->drupalGet('admin/config/media/image-toolkit');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests GD toolkit requirements on the Status Report.
 | 
			
		||||
   */
 | 
			
		||||
  public function testGdToolkitRequirements(): void {
 | 
			
		||||
    // Get Status Report.
 | 
			
		||||
    $this->drupalGet('admin/reports/status');
 | 
			
		||||
    $this->assertSession()->pageTextContains('GD2 image manipulation toolkit');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Supported image file formats: GIF, JPEG, PNG, WEBP, AVIF.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\FileStorage;
 | 
			
		||||
use Drupal\Core\Config\InstallStorage;
 | 
			
		||||
use Drupal\Core\Config\StorageInterface;
 | 
			
		||||
use Drupal\KernelTests\AssertConfigTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides a class for install profiles to check their installed config.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ConfigAfterInstallerTestBase extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  use AssertConfigTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that all the installed config looks like the exported one.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $skipped_config
 | 
			
		||||
   *   An array of skipped config.
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertInstalledConfig(array $skipped_config) {
 | 
			
		||||
    $this->addToAssertionCount(1);
 | 
			
		||||
    /** @var \Drupal\Core\Config\StorageInterface $active_config_storage */
 | 
			
		||||
    $active_config_storage = $this->container->get('config.storage');
 | 
			
		||||
    /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
 | 
			
		||||
    $config_manager = $this->container->get('config.manager');
 | 
			
		||||
 | 
			
		||||
    $default_install_path = 'core/profiles/' . $this->profile . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
 | 
			
		||||
    $profile_config_storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION);
 | 
			
		||||
 | 
			
		||||
    foreach ($profile_config_storage->listAll() as $config_name) {
 | 
			
		||||
      $result = $config_manager->diff($profile_config_storage, $active_config_storage, $config_name);
 | 
			
		||||
      $this->assertConfigDiff($result, $config_name, $skipped_config);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,132 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Core\DrupalKernel;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests distribution profile support with existing settings.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class DistributionProfileExistingSettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The distribution profile info.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $info;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Distribution profile',
 | 
			
		||||
      'distribution' => [
 | 
			
		||||
        'name' => 'My Distribution',
 | 
			
		||||
        'install' => [
 | 
			
		||||
          'theme' => 'olivero',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/my_distribution';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
 | 
			
		||||
 | 
			
		||||
    // Pre-configure hash salt.
 | 
			
		||||
    // Any string is valid, so simply use the class name of this test.
 | 
			
		||||
    $this->settings['settings']['hash_salt'] = (object) [
 | 
			
		||||
      'value' => __CLASS__,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Pre-configure database credentials.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Use the kernel to find the site path because the site.path service should
 | 
			
		||||
    // not be available at this point in the install process.
 | 
			
		||||
    $site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
 | 
			
		||||
    // Pre-configure config directories.
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => $site_path . '/files/config_staging',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Make settings file not writable.
 | 
			
		||||
    $filename = $this->siteDirectory . '/settings.php';
 | 
			
		||||
    // Make the settings file read-only.
 | 
			
		||||
    // Not using File API; a potential error must trigger a PHP warning.
 | 
			
		||||
    chmod($filename, 0444);
 | 
			
		||||
 | 
			
		||||
    // Verify that the distribution name appears.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->info['distribution']['name']);
 | 
			
		||||
    // Verify that the requested theme is used.
 | 
			
		||||
    $this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
 | 
			
		||||
    // Verify that the "Choose profile" step does not appear.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('profile');
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is a distribution profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This step should not appear, since settings.php is fully configured
 | 
			
		||||
    // already.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Confirm that Drupal recognizes this distribution as the current profile.
 | 
			
		||||
    $this->assertEquals('my_distribution', \Drupal::installProfile());
 | 
			
		||||
    $this->assertEquals('my_distribution', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
 | 
			
		||||
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
    $this->assertEquals('my_distribution', \Drupal::installProfile());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,86 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests distribution profile support.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class DistributionProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The distribution profile info.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $info;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Distribution profile',
 | 
			
		||||
      'distribution' => [
 | 
			
		||||
        'name' => 'My Distribution',
 | 
			
		||||
        'install' => [
 | 
			
		||||
          'theme' => 'claro',
 | 
			
		||||
          'finish_url' => '/root-user',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/my_distribution';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
 | 
			
		||||
    file_put_contents("$path/my_distribution.install", "<?php function my_distribution_install() {\Drupal::entityTypeManager()->getStorage('path_alias')->create(['path' => '/user/1', 'alias' => '/root-user'])->save();}");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Verify that the distribution name appears.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->info['distribution']['name']);
 | 
			
		||||
    // Verify that the distribution name is used in the site title.
 | 
			
		||||
    $this->assertSession()->titleEquals('Choose language | ' . $this->info['distribution']['name']);
 | 
			
		||||
    // Verify that the requested theme is used.
 | 
			
		||||
    $this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
 | 
			
		||||
    // Verify that the "Choose profile" step does not appear.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('profile');
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is a distribution profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('root-user');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Confirm that Drupal recognizes this distribution as the current profile.
 | 
			
		||||
    $this->assertEquals('my_distribution', \Drupal::installProfile());
 | 
			
		||||
    $this->assertEquals('my_distribution', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,147 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests distribution profile support with a 'langcode' query string.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\FunctionalTests\Installer\DistributionProfileTranslationTest
 | 
			
		||||
 */
 | 
			
		||||
class DistributionProfileTranslationQueryTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'de';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The distribution profile info.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $info;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Distribution profile',
 | 
			
		||||
      'distribution' => [
 | 
			
		||||
        'name' => 'My Distribution',
 | 
			
		||||
        'langcode' => $this->langcode,
 | 
			
		||||
        'install' => [
 | 
			
		||||
          'theme' => 'claro',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/my_distribution';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.fr.po', $this->getPo('fr'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Pass a different language code than the one set in the distribution
 | 
			
		||||
    // profile. This distribution language should still be used.
 | 
			
		||||
    // The unrouted URL assembler does not exist at this point, so we build the
 | 
			
		||||
    // URL ourselves.
 | 
			
		||||
    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=fr');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // This step is skipped, because the distribution profile uses a fixed
 | 
			
		||||
    // language.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is a distribution profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // The language should have been automatically detected, all following
 | 
			
		||||
    // screens should be translated already.
 | 
			
		||||
    $this->assertSession()->buttonExists('Save and continue de');
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue de';
 | 
			
		||||
 | 
			
		||||
    // Check the language direction.
 | 
			
		||||
    $direction = $this->getSession()->getPage()->find('xpath', '/@dir')->getText();
 | 
			
		||||
    $this->assertEquals('ltr', $direction);
 | 
			
		||||
 | 
			
		||||
    // Verify that the distribution name appears.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->info['distribution']['name']);
 | 
			
		||||
    // Verify that the requested theme is used.
 | 
			
		||||
    $this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
 | 
			
		||||
    // Verify that the "Choose profile" step does not appear.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('profile');
 | 
			
		||||
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getDisplayName());
 | 
			
		||||
 | 
			
		||||
    // Verify German was configured but not English.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    // cspell:ignore deutsch
 | 
			
		||||
    $this->assertSession()->pageTextContains('Deutsch');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('English');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,137 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests distribution profile support.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\FunctionalTests\Installer\DistributionProfileTest
 | 
			
		||||
 */
 | 
			
		||||
class DistributionProfileTranslationTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'de';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The distribution profile info.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $info;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // We set core_version_requirement to '*' for the test so that it does not
 | 
			
		||||
    // need to be updated between major versions.
 | 
			
		||||
    $this->info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Distribution profile',
 | 
			
		||||
      'distribution' => [
 | 
			
		||||
        'name' => 'My Distribution',
 | 
			
		||||
        'langcode' => $this->langcode,
 | 
			
		||||
        'install' => [
 | 
			
		||||
          'theme' => 'claro',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/my_distribution';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
 | 
			
		||||
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // This step is skipped, because the distribution profile uses a fixed
 | 
			
		||||
    // language.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is a distribution profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // The language should have been automatically detected, all following
 | 
			
		||||
    // screens should be translated already.
 | 
			
		||||
    $this->assertSession()->buttonExists('Save and continue de');
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue de';
 | 
			
		||||
 | 
			
		||||
    // Check the language direction.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
 | 
			
		||||
 | 
			
		||||
    // Verify that the distribution name appears.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->info['distribution']['name']);
 | 
			
		||||
    // Verify that the requested theme is used.
 | 
			
		||||
    $this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
 | 
			
		||||
    // Verify that the "Choose profile" step does not appear.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('profile');
 | 
			
		||||
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getDisplayName());
 | 
			
		||||
 | 
			
		||||
    // Verify German was configured but not English.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    // cspell:ignore deutsch
 | 
			
		||||
    $this->assertSession()->pageTextContains('Deutsch');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('English');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests drupal_flush_all_caches() during an install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class DrupalFlushAllCachesInInstallerTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'cache_flush_test';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Cache flush test',
 | 
			
		||||
      'install' => ['language'],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/cache_flush_test';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/cache_flush_test.info.yml", Yaml::encode($info));
 | 
			
		||||
    $php_code = <<<EOF
 | 
			
		||||
<?php
 | 
			
		||||
function cache_flush_test_install() {
 | 
			
		||||
  // Note it is bad practice to call this method during hook_install() as it
 | 
			
		||||
  // results in an additional expensive container rebuild.
 | 
			
		||||
  drupal_flush_all_caches();
 | 
			
		||||
  // Ensure services are available after calling drupal_flush_all_caches().
 | 
			
		||||
  \Drupal::state()->set('cache_flush_test', \Drupal::hasService('language_negotiator'));
 | 
			
		||||
}
 | 
			
		||||
EOF;
 | 
			
		||||
 | 
			
		||||
    file_put_contents("$path/cache_flush_test.install", $php_code);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertTrue(\Drupal::state()->get('cache_flush_test'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Extension\ModuleUninstallValidatorException;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that an install profile can require modules.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallProfileDependenciesTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_install_profile_dependencies';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that an install profile can require modules.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUninstallingModules(): void {
 | 
			
		||||
    $user = $this->drupalCreateUser(['administer modules']);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
    $this->drupalGet('admin/modules/uninstall');
 | 
			
		||||
    $this->assertSession()->fieldDisabled('uninstall[dblog]');
 | 
			
		||||
    $this->getSession()->getPage()->checkField('uninstall[dependency_foo_test]');
 | 
			
		||||
    $this->click('#edit-submit');
 | 
			
		||||
    // Click the confirm button.
 | 
			
		||||
    $this->click('#edit-submit');
 | 
			
		||||
    $this->assertSession()->responseContains('The selected modules have been uninstalled.');
 | 
			
		||||
    // We've uninstalled a module therefore we need to rebuild the container in
 | 
			
		||||
    // the test runner.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
    $this->assertFalse($this->container->get('module_handler')->moduleExists('dependency_foo_test'));
 | 
			
		||||
    try {
 | 
			
		||||
      $this->container->get('module_installer')->uninstall(['dblog']);
 | 
			
		||||
      $this->fail('Uninstalled dblog module.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ModuleUninstallValidatorException $e) {
 | 
			
		||||
      $this->assertStringContainsString("The 'Testing install profile dependencies' install profile requires 'Database Logging'", $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,103 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that an install profile can be uninstalled.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Extension
 | 
			
		||||
 */
 | 
			
		||||
class InstallProfileUninstallTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The profile to install as a basis for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * This profile is used because it contains a submodule.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_config_import';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['config'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a user can uninstall install profiles.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUninstallInstallProfile(): void {
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser(admin: TRUE));
 | 
			
		||||
 | 
			
		||||
    // Ensure that the installation profile is present on the status report.
 | 
			
		||||
    $this->drupalGet('admin/reports/status');
 | 
			
		||||
    $this->assertSession()->pageTextContains("Installation profile");
 | 
			
		||||
    $this->assertSession()->pageTextContains("Testing config import");
 | 
			
		||||
 | 
			
		||||
    // Test uninstalling a module provided by the install profile.
 | 
			
		||||
    $this->drupalGet('admin/modules/uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
 | 
			
		||||
    $this->assertSession()->fieldDisabled('uninstall[testing_config_import]');
 | 
			
		||||
    $this->assertSession()->fieldEnabled('uninstall[testing_config_import_module]')->check();
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('uninstall[testing_config_import_module]');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
 | 
			
		||||
 | 
			
		||||
    // Test that we can reinstall the module from the profile.
 | 
			
		||||
    $this->drupalGet('admin/modules');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Testing config import module');
 | 
			
		||||
    $this->assertSession()->fieldEnabled('modules[testing_config_import_module][enable]')->check();
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Install');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Module Testing config import module has been installed.');
 | 
			
		||||
 | 
			
		||||
    // Install a theme provided by the module.
 | 
			
		||||
    $this->drupalGet('admin/appearance');
 | 
			
		||||
    $this->clickLink("Install Testing config import theme theme");
 | 
			
		||||
    $this->assertSession()->pageTextContains("The Testing config import theme theme has been installed.");
 | 
			
		||||
 | 
			
		||||
    // Test that uninstalling the module and then the profile works.
 | 
			
		||||
    $this->drupalGet('admin/modules/uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
 | 
			
		||||
    $this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following theme(s): testing_config_import_theme");
 | 
			
		||||
    $this->assertSession()->fieldEnabled('uninstall[testing_config_import_module]')->check();
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('uninstall[testing_config_import_module]');
 | 
			
		||||
    $this->drupalGet('admin/appearance');
 | 
			
		||||
    $this->clickLink("Uninstall Testing config import theme theme");
 | 
			
		||||
    $this->assertSession()->pageTextContains("The Testing config import theme theme has been uninstalled.");
 | 
			
		||||
    $this->drupalGet('admin/modules/uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
 | 
			
		||||
    $this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following theme(s): testing_config_import_theme");
 | 
			
		||||
    $this->assertSession()->fieldEnabled('uninstall[testing_config_import]')->check();
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Once uninstalled, the Testing config import profile cannot be reinstalled.');
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Uninstall');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('uninstall[testing_config_import]');
 | 
			
		||||
 | 
			
		||||
    // Test that the module contained in the profile is no longer available to
 | 
			
		||||
    // install.
 | 
			
		||||
    $this->drupalGet('admin/modules');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Testing config import module');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('modules[testing_config_import_module][enable]');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the installation profile is not present on the status report.
 | 
			
		||||
    $this->drupalGet('admin/reports/status');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains("Installation profile");
 | 
			
		||||
    $this->assertSession()->pageTextNotContains("Testing config import");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,66 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with incorrect connection info in settings.php.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerBrokenDatabaseCredentialsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure database credentials in settings.php.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
 | 
			
		||||
    if ($connection_info['default']['driver'] !== 'mysql') {
 | 
			
		||||
      $this->markTestSkipped('This test relies on overriding the mysql driver');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Provide incorrect host name and test the new error messages.
 | 
			
		||||
    $connection_info['default']['host'] = 'localhost';
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the expected requirements problem.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRequirementsProblem(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Requirements problem | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Database settings');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
 | 
			
		||||
    $this->assertSession()->pageTextContains('[Tip: Drupal was attempting to connect to the database server via a socket, but the socket file could not be found. A Unix socket file is used if you do not specify a host name or if you specify the special host name localhost. To connect via TCP/IP use an IP address (127.0.0.1 for IPv4) instead of "localhost". This message normally means that there is no MySQL server running on the system or that you are using an incorrect Unix socket file name when trying to connect to the server.]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,66 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with incorrect connection info in settings.php.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerBrokenDatabasePortSettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure database credentials in settings.php.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
 | 
			
		||||
    if ($connection_info['default']['driver'] !== 'mysql') {
 | 
			
		||||
      $this->markTestSkipped('This test relies on overriding the mysql driver');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Override the port number to random port.
 | 
			
		||||
    $connection_info['default']['port'] = 1234;
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the expected requirements problem.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRequirementsProblem(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Requirements problem | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Database settings');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
 | 
			
		||||
    $this->assertSession()->pageTextContains("[Tip: This message normally means that there is no MySQL server running on the system or that you are using an incorrect host name or port number when trying to connect to the server. You should also check that the TCP/IP port you are using has not been blocked by a firewall or port blocking service.]");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,77 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\Crypt;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installation when a config_sync_directory is set up but does not exist.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerConfigDirectorySetNoDirectoryErrorTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The directory where the sync directory should be created during install.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $configDirectory;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->configDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64();
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => $this->configDirectory . '/sync',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    // Create the files directory early so we can test the error case.
 | 
			
		||||
    mkdir($this->publicFilesDirectory);
 | 
			
		||||
    // Create a file so the directory can not be created.
 | 
			
		||||
    file_put_contents($this->configDirectory, 'Test');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Configure settings.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This step should not appear as we had a failure prior to the settings
 | 
			
		||||
    // screen.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // The parent method asserts that there are no requirements errors, but
 | 
			
		||||
    // this test expects a requirements error in the test method below.
 | 
			
		||||
    // Therefore, we override this method to suppress the parent's assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This step should not appear as we had a failure prior to the settings
 | 
			
		||||
    // screen.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation failed.
 | 
			
		||||
   */
 | 
			
		||||
  public function testError(): void {
 | 
			
		||||
    $this->assertSession()->pageTextContains("An automated attempt to create the directory {$this->configDirectory}/sync failed, possibly due to a permissions problem.");
 | 
			
		||||
    $this->assertDirectoryDoesNotExist($this->configDirectory . '/sync');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\Crypt;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer when a custom config directory set up but does not exist.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerConfigDirectorySetNoDirectoryTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The sync directory created during the install.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $syncDirectory;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->syncDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64() . '/sync';
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => $this->syncDirectory,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertDirectoryExists($this->syncDirectory);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,187 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Core\Installer\Form\SelectProfileForm;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides a base class for testing installing from existing configuration.
 | 
			
		||||
 */
 | 
			
		||||
abstract class InstallerConfigDirectoryTestBase extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This is set by the profile in the core.extension extracted.
 | 
			
		||||
   *
 | 
			
		||||
   * If set to FALSE, then the install will proceed without an install profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string|null|bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $existingSyncDirectory = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This copies a source directory to a destination directory recursively.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $source
 | 
			
		||||
   *   Source directory.
 | 
			
		||||
   * @param string $destination
 | 
			
		||||
   *   Destination directory.
 | 
			
		||||
   */
 | 
			
		||||
  protected function copyDirectory(string $source, string $destination): void {
 | 
			
		||||
    if (!is_dir($destination)) {
 | 
			
		||||
      mkdir($destination, 0755, TRUE);
 | 
			
		||||
    }
 | 
			
		||||
    $files = scandir($source);
 | 
			
		||||
    foreach ($files as $file) {
 | 
			
		||||
      if ($file !== '.' && $file !== '..') {
 | 
			
		||||
        $sourceFile = $source . '/' . $file;
 | 
			
		||||
        $destinationFile = $destination . '/' . $file;
 | 
			
		||||
        if (is_dir($sourceFile)) {
 | 
			
		||||
          $this->copyDirectory($sourceFile, $destinationFile);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          copy($sourceFile, $destinationFile);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment() {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
 | 
			
		||||
    if ($this->profile === NULL) {
 | 
			
		||||
      $core_extension_location = $this->getConfigLocation() . '/core.extension.yml';
 | 
			
		||||
      $core_extension = Yaml::decode(file_get_contents($core_extension_location));
 | 
			
		||||
      $this->profile = $core_extension['profile'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($this->profile !== FALSE) {
 | 
			
		||||
      // Create a profile for testing. We set core_version_requirement to '*'
 | 
			
		||||
      // for the test so that it does not need to be updated between major
 | 
			
		||||
      // versions.
 | 
			
		||||
      $info = [
 | 
			
		||||
        'type' => 'profile',
 | 
			
		||||
        'core_version_requirement' => '*',
 | 
			
		||||
        'name' => 'Configuration installation test profile (' . $this->profile . ')',
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      // File API functions are not available yet.
 | 
			
		||||
      $path = $this->siteDirectory . '/profiles/' . $this->profile;
 | 
			
		||||
 | 
			
		||||
      // Put the sync directory inside the profile.
 | 
			
		||||
      $config_sync_directory = $path . '/config/sync';
 | 
			
		||||
 | 
			
		||||
      mkdir($path, 0777, TRUE);
 | 
			
		||||
      file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info));
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // If we have no profile we must use an existing sync directory.
 | 
			
		||||
      $this->existingSyncDirectory = TRUE;
 | 
			
		||||
      $config_sync_directory = $this->siteDirectory . '/config/sync';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($this->existingSyncDirectory) {
 | 
			
		||||
      $config_sync_directory = $this->siteDirectory . '/config/sync';
 | 
			
		||||
      $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
        'value' => $config_sync_directory,
 | 
			
		||||
        'required' => TRUE,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create config/sync directory and extract tarball contents to it.
 | 
			
		||||
    mkdir($config_sync_directory, 0777, TRUE);
 | 
			
		||||
    $this->copyDirectory($this->getConfigLocation(), $config_sync_directory);
 | 
			
		||||
 | 
			
		||||
    // Add the module that is providing the database driver to the list of
 | 
			
		||||
    // modules that can not be uninstalled in the core.extension configuration.
 | 
			
		||||
    if (file_exists($config_sync_directory . '/core.extension.yml')) {
 | 
			
		||||
      $core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
 | 
			
		||||
      $module = Database::getConnection()->getProvider();
 | 
			
		||||
      if ($module !== 'core') {
 | 
			
		||||
        $core_extension['module'][$module] = 0;
 | 
			
		||||
        $core_extension['module'] = module_config_sort($core_extension['module']);
 | 
			
		||||
      }
 | 
			
		||||
      if ($this->profile === FALSE && array_key_exists('profile', $core_extension)) {
 | 
			
		||||
        // Remove the profile.
 | 
			
		||||
        unset($core_extension['module'][$core_extension['profile']]);
 | 
			
		||||
        unset($core_extension['profile']);
 | 
			
		||||
 | 
			
		||||
        // Set a default theme to the first theme that will be installed as this
 | 
			
		||||
        // can not be retrieved from the profile.
 | 
			
		||||
        $this->defaultTheme = array_key_first($core_extension['theme']);
 | 
			
		||||
      }
 | 
			
		||||
      file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the path to the configuration directory.
 | 
			
		||||
   *
 | 
			
		||||
   * The directory will be copied to the install profile's config/sync
 | 
			
		||||
   * directory for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The path to the configuration directory.
 | 
			
		||||
   */
 | 
			
		||||
  abstract protected function getConfigLocation();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $parameters = parent::installParameters();
 | 
			
		||||
 | 
			
		||||
    // The options that change configuration are disabled when installing from
 | 
			
		||||
    // existing configuration.
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['site_name']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['site_mail']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['enable_update_status_module']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['enable_update_status_emails']);
 | 
			
		||||
 | 
			
		||||
    return $parameters;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation installed the configuration correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    // After installation there is no snapshot and nothing to import.
 | 
			
		||||
    $change_list = $this->configImporter()->getStorageComparer()->getChangelist();
 | 
			
		||||
    $expected = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // The system.mail is changed configuration because the test system
 | 
			
		||||
      // changes it to ensure that mails are not sent.
 | 
			
		||||
      'update' => ['system.mail'],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected, $change_list);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select installation profile.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile() {
 | 
			
		||||
    if ($this->existingSyncDirectory) {
 | 
			
		||||
      $edit = [
 | 
			
		||||
        'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
 | 
			
		||||
      ];
 | 
			
		||||
      $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      parent::setUpProfile();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with database errors.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerDatabaseErrorMessagesTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // We are creating a table here to force an error in the installer because
 | 
			
		||||
    // it will try and create the drupal_install_test table as this is part of
 | 
			
		||||
    // the standard database tests performed by the installer in
 | 
			
		||||
    // Drupal\Core\Database\Install\Tasks.
 | 
			
		||||
    $spec = [
 | 
			
		||||
      'fields' => [
 | 
			
		||||
        'id' => [
 | 
			
		||||
          'type' => 'int',
 | 
			
		||||
          'not null' => TRUE,
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'primary key' => ['id'],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    Database::getConnection('default')->schema()->createTable('drupal_install_test', $spec);
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This step should not appear as we had a failure on the settings screen.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that the error message in the settings step is correct.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSetUpSettingsErrorMessage(): void {
 | 
			
		||||
    $this->assertSession()->responseContains('<ul><li>Failed to <strong>CREATE</strong> a test table');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,37 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with empty settings file.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerEmptySettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Create an empty settings.php file.
 | 
			
		||||
    $path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory;
 | 
			
		||||
    file_put_contents($path . '/settings.php', '');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with broken database connection info in settings.php.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingBrokenDatabaseSettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure database credentials in settings.php.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
 | 
			
		||||
    if ($connection_info['default']['driver'] !== 'mysql') {
 | 
			
		||||
      $this->markTestSkipped('This test relies on overriding the mysql driver');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Use a database driver that reports a fake database version that does
 | 
			
		||||
    // not meet requirements.
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
    $connection_info['default']['driver'] = 'DriverTestMysqlDeprecatedVersion';
 | 
			
		||||
    $namespace = 'Drupal\\driver_test\\Driver\\Database\\DriverTestMysqlDeprecatedVersion';
 | 
			
		||||
    $connection_info['default']['namespace'] = $namespace;
 | 
			
		||||
    $connection_info['default']['autoload'] = \Drupal::service('extension.list.database_driver')
 | 
			
		||||
      ->get($namespace)
 | 
			
		||||
      ->getAutoloadInfo()['autoload'];
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // The parent method asserts that there are no requirements errors, but
 | 
			
		||||
    // this test expects a requirements error in the test method below.
 | 
			
		||||
    // Therefore, we override this method to suppress the parent's assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the expected requirements problem.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRequirementsProblem(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Requirements problem | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Database settings');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The database server version 10.2.31-MariaDB-1:10.2.31+maria~bionic-log is less than the minimum required version');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installation when a config_sync_directory exists and is set up.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigDirectoryTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The expected file perms of the folder.
 | 
			
		||||
   *
 | 
			
		||||
   * @var int
 | 
			
		||||
   */
 | 
			
		||||
  protected $expectedFilePerms;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    mkdir($this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/config_read_only', 0444);
 | 
			
		||||
    $this->expectedFilePerms = fileperms($this->siteDirectory . '/config_read_only');
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => $this->siteDirectory . '/config_read_only',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertEquals($this->expectedFilePerms, fileperms($this->siteDirectory . '/config_read_only'));
 | 
			
		||||
    $this->assertEquals([], glob($this->siteDirectory . '/config_read_only/*'), 'The sync directory is empty after install because it is read-only.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigExistingSettingsTest extends InstallerExistingConfigTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * Partially configures a preexisting settings.php file before invoking the
 | 
			
		||||
   * interactive installer.
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure hash salt.
 | 
			
		||||
    // Any string is valid, so simply use the class name of this test.
 | 
			
		||||
    $this->settings['settings']['hash_salt'] = (object) [
 | 
			
		||||
      'value' => __CLASS__,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Pre-configure database credentials.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Logger\RfcLogLevel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigMultilingualTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_config_install_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/multilingual';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    parent::testConfigSync();
 | 
			
		||||
 | 
			
		||||
    // Ensure no warning, error, critical, alert or emergency messages have been
 | 
			
		||||
    // logged.
 | 
			
		||||
    $count = (int) \Drupal::database()->select('watchdog', 'w')->fields('w')->condition('severity', RfcLogLevel::WARNING, '<=')->countQuery()->execute()->fetchField();
 | 
			
		||||
    $this->assertSame(0, $count);
 | 
			
		||||
 | 
			
		||||
    // Ensure the correct message is logged from locale_config_batch_finished().
 | 
			
		||||
    $count = (int) \Drupal::database()->select('watchdog', 'w')->fields('w')->condition('message', 'The configuration was successfully updated. %number configuration objects updated.')->countQuery()->execute()->fetchField();
 | 
			
		||||
    $this->assertSame(1, $count);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that profiles invalid config can not be installed.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigNoConfigTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'no_config_profile';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Final installer step: Configure site.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // There are errors therefore there is nothing to do here.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that profiles with an empty config/sync directory do not work.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Configuration validation | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
 | 
			
		||||
    $this->assertSession()->pageTextContains('This import is empty and if applied would delete all of your configuration, so has been rejected.');
 | 
			
		||||
 | 
			
		||||
    // Ensure there is no continuation button.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Save and continue');
 | 
			
		||||
    $this->assertSession()->buttonNotExists('edit-submit');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration without a profile works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigNoProfileTest extends InstallerExistingConfigTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = FALSE;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Testing installing from config without system.site.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigNoSystemSiteTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    unlink($this->siteDirectory . '/profiles/' . $this->profile . '/config/sync/system.site.yml');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function setUpSite(): void {
 | 
			
		||||
    // There are errors. Therefore, there is nothing to do here.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that profiles with no system.site do not work.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $this->htmlOutput(NULL);
 | 
			
		||||
    $this->assertSession()->titleEquals('Configuration validation | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
 | 
			
		||||
    $this->assertSession()->pageTextContains('This import does not contain system.site configuration, so has been rejected.');
 | 
			
		||||
 | 
			
		||||
    // Ensure there is no continuation button.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Save and continue');
 | 
			
		||||
    $this->assertSession()->buttonNotExists('edit-submit');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/testing_config_install';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,76 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that profiles with hook_install() can't be installed from config.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigProfileHookInstallTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'config_profile_with_hook_install';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Create an .install file with a hook_install() implementation.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/' . $this->profile;
 | 
			
		||||
    $contents = <<<EOF
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
function config_profile_with_hook_install_install() {
 | 
			
		||||
}
 | 
			
		||||
EOF;
 | 
			
		||||
    file_put_contents("$path/{$this->profile}.install", $contents);
 | 
			
		||||
    parent::visitInstaller();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Configure settings.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // There are errors therefore there is nothing to do here.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // The parent method asserts that there are no requirements errors, but
 | 
			
		||||
    // this test expects a requirements error in the test method below.
 | 
			
		||||
    // Therefore, we override this method to suppress the parent's assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Final installer step: Configure site.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // There are errors therefore there is nothing to do here.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    // We're not going to get to the config import stage so this does not
 | 
			
		||||
    // matter.
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms the installation has failed and the expected error is displayed.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Requirements problem | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->profile);
 | 
			
		||||
    $this->assertSession()->pageTextContains('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,212 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
// cSpell:ignore Anónimo Aplicar
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigSyncDirectoryMultilingualTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_config_install_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $existingSyncDirectory = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select installation profile.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile() {
 | 
			
		||||
    // Ensure the site name 'Multilingual' appears as expected in the 'Use
 | 
			
		||||
    // existing configuration' radio description.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Install Multilingual using existing configuration.');
 | 
			
		||||
    return parent::setUpProfile();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/multilingual';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Place custom local translations in the translations directory and fix up
 | 
			
		||||
    // configuration.
 | 
			
		||||
    mkdir($this->publicFilesDirectory . '/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->publicFilesDirectory . '/translations/drupal-8.0.0.es.po', $this->getPo('es'));
 | 
			
		||||
    $locale_settings = Yaml::decode(file_get_contents($this->siteDirectory . '/config/sync/locale.settings.yml'));
 | 
			
		||||
    $locale_settings['translation']['use_source'] = 'local';
 | 
			
		||||
    $locale_settings['translation']['path'] = $this->publicFilesDirectory . '/translations';
 | 
			
		||||
    file_put_contents($this->siteDirectory . '/config/sync/locale.settings.yml', Yaml::encode($locale_settings));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation installed the configuration correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $comparer = $this->configImporter()->getStorageComparer();
 | 
			
		||||
    $expected_changelist_default_collection = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // The system.mail is changed configuration because the test system
 | 
			
		||||
      // changes it to ensure that mails are not sent.
 | 
			
		||||
      'update' => ['system.mail'],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected_changelist_default_collection, $comparer->getChangelist());
 | 
			
		||||
    $expected_changelist_spanish_collection = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // The view was untranslated but the translation exists so the installer
 | 
			
		||||
      // performs the translation.
 | 
			
		||||
      'update' => ['views.view.who_s_new'],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
 | 
			
		||||
 | 
			
		||||
    // Ensure that menu blocks have been created correctly.
 | 
			
		||||
    $this->assertSession()->responseNotContains('This block is broken or missing.');
 | 
			
		||||
    $this->assertSession()->linkExists('Add content');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the Spanish translation of anonymous is the one from
 | 
			
		||||
    // configuration and not the PO file.
 | 
			
		||||
    // cspell:disable-next-line
 | 
			
		||||
    $this->assertSame('Anónimo', \Drupal::languageManager()->getLanguageConfigOverride('es', 'user.settings')->get('anonymous'));
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\locale\StringStorageInterface $locale_storage */
 | 
			
		||||
    $locale_storage = \Drupal::service('locale.storage');
 | 
			
		||||
    // If configuration contains a translation that is not in the po file then
 | 
			
		||||
    // _install_config_locale_overrides_process_batch() will create a customized
 | 
			
		||||
    // translation.
 | 
			
		||||
    $translation = $locale_storage->findTranslation(['source' => 'Anonymous', 'language' => 'es']);
 | 
			
		||||
    $this->assertSame('Anónimo', $translation->getString());
 | 
			
		||||
    $this->assertTrue((bool) $translation->customized, "A customized translation has been created for Anonymous");
 | 
			
		||||
 | 
			
		||||
    // If configuration contains a translation that is in the po file then
 | 
			
		||||
    // _install_config_locale_overrides_process_batch() will not create a
 | 
			
		||||
    // customized translation.
 | 
			
		||||
    $translation = $locale_storage->findTranslation(['source' => 'Apply', 'language' => 'es']);
 | 
			
		||||
    $this->assertSame('Aplicar', $translation->getString());
 | 
			
		||||
    $this->assertFalse((bool) $translation->customized, "A non-customized translation has been created for Apply");
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\language\Config\LanguageConfigOverride $view_config */
 | 
			
		||||
    // Ensure that views are translated as expected.
 | 
			
		||||
    $view_config = \Drupal::languageManager()->getLanguageConfigOverride('es', 'views.view.who_s_new');
 | 
			
		||||
    $this->assertSame('Aplicar', $view_config->get('display.default.display_options.exposed_form.options.submit_button'));
 | 
			
		||||
    $view_config = \Drupal::languageManager()->getLanguageConfigOverride('es', 'views.view.archive');
 | 
			
		||||
    $this->assertSame('Aplicar', $view_config->get('display.default.display_options.exposed_form.options.submit_button'));
 | 
			
		||||
 | 
			
		||||
    // Manually update the translation status so can re-run the import.
 | 
			
		||||
    $status = locale_translation_get_status();
 | 
			
		||||
    $status['drupal']['es']->type = 'local';
 | 
			
		||||
    $status['drupal']['es']->files['local']->timestamp = time();
 | 
			
		||||
    \Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
 | 
			
		||||
    // Run the translation import.
 | 
			
		||||
    $this->drupalGet('admin/reports/translations');
 | 
			
		||||
    $this->submitForm([], 'Update translations');
 | 
			
		||||
 | 
			
		||||
    // Ensure that only the config we expected to have changed has.
 | 
			
		||||
    $comparer = $this->configImporter()->getStorageComparer();
 | 
			
		||||
    $expected_changelist_spanish_collection = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // The view was untranslated but the translation exists so the installer
 | 
			
		||||
      // performs the translation.
 | 
			
		||||
      'update' => ['views.view.who_s_new'],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
 | 
			
		||||
 | 
			
		||||
    // Change a translation and ensure configuration is updated.
 | 
			
		||||
    $po = <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Anonymous"
 | 
			
		||||
msgstr "Anonymous es"
 | 
			
		||||
 | 
			
		||||
msgid "Apply"
 | 
			
		||||
msgstr "Aplicar New"
 | 
			
		||||
 | 
			
		||||
PO;
 | 
			
		||||
    file_put_contents($this->publicFilesDirectory . '/translations/drupal-8.0.0.es.po', $po);
 | 
			
		||||
 | 
			
		||||
    // Manually update the translation status so can re-run the import.
 | 
			
		||||
    $status = locale_translation_get_status();
 | 
			
		||||
    $status['drupal']['es']->type = 'local';
 | 
			
		||||
    $status['drupal']['es']->files['local']->timestamp = time();
 | 
			
		||||
    \Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
 | 
			
		||||
    // Run the translation import.
 | 
			
		||||
    $this->drupalGet('admin/reports/translations');
 | 
			
		||||
    $this->submitForm([], 'Update translations');
 | 
			
		||||
 | 
			
		||||
    $translation = \Drupal::service('locale.storage')->findTranslation(['source' => 'Apply', 'language' => 'es']);
 | 
			
		||||
    $this->assertSame('Aplicar New', $translation->getString());
 | 
			
		||||
    $this->assertFalse((bool) $translation->customized, "A non-customized translation has been created for Apply");
 | 
			
		||||
 | 
			
		||||
    // Ensure that only the config we expected to have changed has.
 | 
			
		||||
    $comparer = $this->configImporter()->getStorageComparer();
 | 
			
		||||
    $expected_changelist_spanish_collection = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // All views with 'Aplicar' will have been changed to use the new
 | 
			
		||||
      // translation.
 | 
			
		||||
      'update' => [
 | 
			
		||||
        'views.view.archive',
 | 
			
		||||
        'views.view.content_recent',
 | 
			
		||||
        'views.view.frontpage',
 | 
			
		||||
        'views.view.glossary',
 | 
			
		||||
        'views.view.who_s_new',
 | 
			
		||||
        'views.view.who_s_online',
 | 
			
		||||
      ],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Anonymous"
 | 
			
		||||
msgstr "Anonymous $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Apply"
 | 
			
		||||
msgstr "Aplicar"
 | 
			
		||||
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,96 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that profiles with hook_install() can't be installed from config.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigSyncDirectoryProfileHookInstallTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_config_install_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $existingSyncDirectory = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Create an .install file with a hook_install() implementation.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/' . $this->profile;
 | 
			
		||||
    $contents = <<<EOF
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
function testing_config_install_multilingual_install() {
 | 
			
		||||
}
 | 
			
		||||
EOF;
 | 
			
		||||
    file_put_contents("$path/{$this->profile}.install", $contents);
 | 
			
		||||
    parent::visitInstaller();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select installation profile.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This is the form we are testing so wait until the test method to do
 | 
			
		||||
    // assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Requirements problem.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Configure settings.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Final installer step: Configure site.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/multilingual';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests installing from config is not available due to hook_INSTALL().
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $this->assertSession()->titleEquals('Select an installation profile | Drupal');
 | 
			
		||||
    $this->assertSession()->responseNotContains('Use existing configuration');
 | 
			
		||||
 | 
			
		||||
    // Remove the install hook and the option to install from existing
 | 
			
		||||
    // configuration will be available.
 | 
			
		||||
    unlink("{$this->siteDirectory}/profiles/{$this->profile}/{$this->profile}.install");
 | 
			
		||||
    $this->getSession()->reload();
 | 
			
		||||
    $this->assertSession()->titleEquals('Select an installation profile | Drupal');
 | 
			
		||||
    $this->assertSession()->responseContains('Use existing configuration');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigSyncDirectoryProfileMismatchTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_config_install_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $existingSyncDirectory = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/multilingual';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Configure settings.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // Cause a profile mismatch by hacking the URL.
 | 
			
		||||
    $this->drupalGet(str_replace($this->profile, 'minimal', $this->getUrl()));
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This step will not occur because there is an error.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that profile mismatch fails to install.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    $this->htmlOutput(NULL);
 | 
			
		||||
    $this->assertSession()->titleEquals('Configuration validation | Drupal');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The selected installation profile minimal does not match the profile stored in configuration testing_config_install_multilingual.');
 | 
			
		||||
 | 
			
		||||
    // Ensure there is no continuation button.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Save and continue');
 | 
			
		||||
    $this->assertSession()->buttonNotExists('edit-submit');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore nmsgid nmsgstr enregistrer
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that installing from existing configuration works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingConfigTest extends InstallerConfigDirectoryTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.fr.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Enregistrer et continuer\"");
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function setUpSettings(): void {
 | 
			
		||||
    // The configuration is from a site installed in French.
 | 
			
		||||
    // So after selecting the profile the installer detects that the site must
 | 
			
		||||
    // be installed in French, thus we change the button translation.
 | 
			
		||||
    $this->translations['Save and continue'] = 'Enregistrer et continuer';
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getConfigLocation(): string {
 | 
			
		||||
    return __DIR__ . '/../../../fixtures/config_install/testing_config_install';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,179 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\Archiver\ArchiveTar;
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Core\Installer\Form\SelectProfileForm;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides a base class for testing installing from existing configuration.
 | 
			
		||||
 *
 | 
			
		||||
 * @deprecated in drupal:10.4.0 and is removed from drupal:12.0.0. Use
 | 
			
		||||
 *   \Drupal\FunctionalTests\Installer\InstallerConfigDirectoryTestBase
 | 
			
		||||
 *   instead.
 | 
			
		||||
 *
 | 
			
		||||
 * @see https://www.drupal.org/node/3460001
 | 
			
		||||
 */
 | 
			
		||||
abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $existingSyncDirectory = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct(string $name) {
 | 
			
		||||
    @trigger_error(__CLASS__ . ' is deprecated in drupal:10.4.0 and is removed from drupal:12.0.0. Use \Drupal\FunctionalTests\Installer\InstallerConfigDirectoryTestBase instead. See https://www.drupal.org/node/3460001');
 | 
			
		||||
    parent::__construct($name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment() {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $archiver = new ArchiveTar($this->getConfigTarball(), 'gz');
 | 
			
		||||
 | 
			
		||||
    if ($this->profile === NULL) {
 | 
			
		||||
      $core_extension = Yaml::decode($archiver->extractInString('core.extension.yml'));
 | 
			
		||||
      $this->profile = $core_extension['profile'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($this->profile !== FALSE) {
 | 
			
		||||
      // Create a profile for testing. We set core_version_requirement to '*'
 | 
			
		||||
      // for the test so that it does not need to be updated between major
 | 
			
		||||
      // versions.
 | 
			
		||||
      $info = [
 | 
			
		||||
        'type' => 'profile',
 | 
			
		||||
        'core_version_requirement' => '*',
 | 
			
		||||
        'name' => 'Configuration installation test profile (' . $this->profile . ')',
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      // File API functions are not available yet.
 | 
			
		||||
      $path = $this->siteDirectory . '/profiles/' . $this->profile;
 | 
			
		||||
 | 
			
		||||
      // Put the sync directory inside the profile.
 | 
			
		||||
      $config_sync_directory = $path . '/config/sync';
 | 
			
		||||
 | 
			
		||||
      mkdir($path, 0777, TRUE);
 | 
			
		||||
      file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info));
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // If we have no profile we must use an existing sync directory.
 | 
			
		||||
      $this->existingSyncDirectory = TRUE;
 | 
			
		||||
      $config_sync_directory = $this->siteDirectory . '/config/sync';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($this->existingSyncDirectory) {
 | 
			
		||||
      $config_sync_directory = $this->siteDirectory . '/config/sync';
 | 
			
		||||
      $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
        'value' => $config_sync_directory,
 | 
			
		||||
        'required' => TRUE,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create config/sync directory and extract tarball contents to it.
 | 
			
		||||
    mkdir($config_sync_directory, 0777, TRUE);
 | 
			
		||||
    $files = [];
 | 
			
		||||
    $list = $archiver->listContent();
 | 
			
		||||
    if (is_array($list)) {
 | 
			
		||||
      /** @var array $list */
 | 
			
		||||
      foreach ($list as $file) {
 | 
			
		||||
        $files[] = $file['filename'];
 | 
			
		||||
      }
 | 
			
		||||
      $archiver->extractList($files, $config_sync_directory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add the module that is providing the database driver to the list of
 | 
			
		||||
    // modules that can not be uninstalled in the core.extension configuration.
 | 
			
		||||
    if (file_exists($config_sync_directory . '/core.extension.yml')) {
 | 
			
		||||
      $core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
 | 
			
		||||
      $module = Database::getConnection()->getProvider();
 | 
			
		||||
      if ($module !== 'core') {
 | 
			
		||||
        $core_extension['module'][$module] = 0;
 | 
			
		||||
        $core_extension['module'] = module_config_sort($core_extension['module']);
 | 
			
		||||
      }
 | 
			
		||||
      if ($this->profile === FALSE && array_key_exists('profile', $core_extension)) {
 | 
			
		||||
        // Remove the profile.
 | 
			
		||||
        unset($core_extension['module'][$core_extension['profile']]);
 | 
			
		||||
        unset($core_extension['profile']);
 | 
			
		||||
 | 
			
		||||
        // Set a default theme to the first theme that will be installed as this
 | 
			
		||||
        // can not be retrieved from the profile.
 | 
			
		||||
        $this->defaultTheme = array_key_first($core_extension['theme']);
 | 
			
		||||
      }
 | 
			
		||||
      file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the filepath to the configuration tarball.
 | 
			
		||||
   *
 | 
			
		||||
   * The tarball will be extracted to the install profile's config/sync
 | 
			
		||||
   * directory for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The filepath to the configuration tarball.
 | 
			
		||||
   */
 | 
			
		||||
  abstract protected function getConfigTarball();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $parameters = parent::installParameters();
 | 
			
		||||
 | 
			
		||||
    // The options that change configuration are disabled when installing from
 | 
			
		||||
    // existing configuration.
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['site_name']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['site_mail']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['enable_update_status_module']);
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['enable_update_status_emails']);
 | 
			
		||||
 | 
			
		||||
    return $parameters;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation installed the configuration correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigSync(): void {
 | 
			
		||||
    // After installation there is no snapshot and nothing to import.
 | 
			
		||||
    $change_list = $this->configImporter()->getStorageComparer()->getChangelist();
 | 
			
		||||
    $expected = [
 | 
			
		||||
      'create' => [],
 | 
			
		||||
      // The system.mail is changed configuration because the test system
 | 
			
		||||
      // changes it to ensure that mails are not sent.
 | 
			
		||||
      'update' => ['system.mail'],
 | 
			
		||||
      'delete' => [],
 | 
			
		||||
      'rename' => [],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertEquals($expected, $change_list);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select installation profile.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile() {
 | 
			
		||||
    if ($this->existingSyncDirectory) {
 | 
			
		||||
      $edit = [
 | 
			
		||||
        'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
 | 
			
		||||
      ];
 | 
			
		||||
      $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      parent::setUpProfile();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installation with database information in an existing settings file.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingDatabaseSettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure database credentials in settings.php.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * @todo The database settings form is not supposed to appear if settings.php
 | 
			
		||||
   *   contains a valid database connection already (but e.g. no config
 | 
			
		||||
   *   directories yet).
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // All database settings should be pre-configured, except password.
 | 
			
		||||
    $values = $this->parameters['forms']['install_settings_form'];
 | 
			
		||||
    $driver = $values['driver'];
 | 
			
		||||
    $edit = [];
 | 
			
		||||
    if (isset($values[$driver]['password']) && $values[$driver]['password'] !== '') {
 | 
			
		||||
      $edit = $this->translatePostValues([
 | 
			
		||||
        $driver => [
 | 
			
		||||
          'password' => $values[$driver]['password'],
 | 
			
		||||
        ],
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
    $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with an existing Drupal installation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingInstallationTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that Drupal fails to install when there is an existing installation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    // Verify that Drupal can't be immediately reinstalled.
 | 
			
		||||
    $this->visitInstaller();
 | 
			
		||||
    $this->assertSession()->pageTextContains('Drupal already installed');
 | 
			
		||||
 | 
			
		||||
    // Verify that Drupal version is not displayed.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains(\Drupal::VERSION);
 | 
			
		||||
 | 
			
		||||
    // Delete settings.php and attempt to reinstall again.
 | 
			
		||||
    unlink($this->siteDirectory . '/settings.php');
 | 
			
		||||
    $this->visitInstaller();
 | 
			
		||||
    $this->setUpLanguage();
 | 
			
		||||
    $this->setUpProfile();
 | 
			
		||||
    $this->setUpRequirementsProblem();
 | 
			
		||||
    $this->setUpSettings();
 | 
			
		||||
    $this->assertSession()->pageTextContains('Drupal already installed');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\DrupalKernel;
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with an existing settings file but no install profile.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingSettingsNoProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * Configures a preexisting settings.php file without an install_profile
 | 
			
		||||
   * setting before invoking the interactive installer.
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
 | 
			
		||||
    // Pre-configure hash salt.
 | 
			
		||||
    // Any string is valid, so simply use the class name of this test.
 | 
			
		||||
    $this->settings['settings']['hash_salt'] = (object) [
 | 
			
		||||
      'value' => __CLASS__,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Pre-configure database credentials.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Pre-configure config directories.
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => DrupalKernel::findSitePath(Request::createFromGlobals()) . '/files/config_sync',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This step should not appear, since settings.php is fully configured
 | 
			
		||||
    // already.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertEquals('testing', \Drupal::installProfile());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,87 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Core\DrupalKernel;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the installer with an existing settings file.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerExistingSettingsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * Fully configures a preexisting settings.php file before invoking the
 | 
			
		||||
   * interactive installer.
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Pre-configure hash salt.
 | 
			
		||||
    // Any string is valid, so simply use the class name of this test.
 | 
			
		||||
    $this->settings['settings']['hash_salt'] = (object) [
 | 
			
		||||
      'value' => __CLASS__,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Pre-configure database credentials.
 | 
			
		||||
    $connection_info = Database::getConnectionInfo();
 | 
			
		||||
    unset($connection_info['default']['pdo']);
 | 
			
		||||
    unset($connection_info['default']['init_commands']);
 | 
			
		||||
 | 
			
		||||
    $this->settings['databases']['default'] = (object) [
 | 
			
		||||
      'value' => $connection_info,
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Use the kernel to find the site path because the site.path service should
 | 
			
		||||
    // not be available at this point in the install process.
 | 
			
		||||
    $site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
 | 
			
		||||
    // Pre-configure config directories.
 | 
			
		||||
    $this->settings['settings']['config_sync_directory'] = (object) [
 | 
			
		||||
      'value' => $site_path . '/files/config_sync',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Visits the interactive installer.
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Should redirect to the installer.
 | 
			
		||||
    $this->drupalGet($GLOBALS['base_url']);
 | 
			
		||||
    // Ensure no database tables have been created yet.
 | 
			
		||||
    $this->assertSame([], Database::getConnection()->schema()->findTables('%'));
 | 
			
		||||
    $this->assertSession()->addressEquals($GLOBALS['base_url'] . '/core/install.php');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This step should not appear, since settings.php is fully configured
 | 
			
		||||
    // already.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertEquals('testing', \Drupal::installProfile());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore nmsgid nmsgstr
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that the early installer uses the correct language direction.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerLanguageDirectionTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Overrides the language code the installer should use.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'ar';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.ar.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue Arabic\"");
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
    // After selecting a different language than English, all following screens
 | 
			
		||||
    // should be translated already.
 | 
			
		||||
    $this->assertSession()->buttonExists('Save and continue Arabic');
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue Arabic';
 | 
			
		||||
 | 
			
		||||
    // Verify that language direction is right-to-left.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '/@dir', 'rtl');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Language\LanguageManager;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore xoxo
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that the installer language list uses local and remote languages.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerLanguagePageTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select language.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    touch($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.xoxo.po');
 | 
			
		||||
 | 
			
		||||
    // Check that all predefined languages show up with their native names.
 | 
			
		||||
    $this->visitInstaller();
 | 
			
		||||
    foreach (LanguageManager::getStandardLanguageList() as $langcode => $names) {
 | 
			
		||||
      $this->assertSession()->optionExists('edit-langcode', $langcode);
 | 
			
		||||
      $this->assertSession()->responseContains('>' . $names[1] . '<');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that our custom one shows up with the file name indicated language.
 | 
			
		||||
    $this->assertSession()->optionExists('edit-langcode', 'xoxo');
 | 
			
		||||
    $this->assertSession()->responseContains('>xoxo<');
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,115 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore drupaldriver testdriverdatabasedrivertestmysql
 | 
			
		||||
// cspell:ignore testdriverdatabasedrivertestpgsql
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the interactive installer.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The name of the test database driver in use.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $testDriverName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    $driver = Database::getConnection()->driver();
 | 
			
		||||
    if (!in_array($driver, ['mysql', 'pgsql'])) {
 | 
			
		||||
      $this->markTestSkipped("This test does not support the {$driver} database driver.");
 | 
			
		||||
    }
 | 
			
		||||
    $driverNamespace = Database::getConnection()->getConnectionOptions()['namespace'];
 | 
			
		||||
    $this->testDriverName = 'DriverTest' . ucfirst($driver);
 | 
			
		||||
    $testDriverNamespace = "Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}";
 | 
			
		||||
 | 
			
		||||
    // Assert that we are using the database drivers from the driver_test
 | 
			
		||||
    // module.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupaldriver-testdriverdatabasedrivertestmysql"]', 'MySQL by the driver_test module');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupaldriver-testdriverdatabasedrivertestpgsql"]', 'PostgreSQL by the driver_test module');
 | 
			
		||||
 | 
			
		||||
    $settings = $this->parameters['forms']['install_settings_form'];
 | 
			
		||||
 | 
			
		||||
    $settings['driver'] = $testDriverNamespace;
 | 
			
		||||
    $settings[$testDriverNamespace] = $settings[$driverNamespace];
 | 
			
		||||
    unset($settings[$driverNamespace]);
 | 
			
		||||
    $edit = $this->translatePostValues($settings);
 | 
			
		||||
    $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Assert that in the settings.php the database connection array has the
 | 
			
		||||
    // correct values set.
 | 
			
		||||
    $installedDatabaseSettings = $this->getInstalledDatabaseSettings();
 | 
			
		||||
    $this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']);
 | 
			
		||||
    $this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']);
 | 
			
		||||
    $this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']);
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'mysql' => [
 | 
			
		||||
        'namespace' => 'Drupal\\mysql',
 | 
			
		||||
        'autoload' => 'core/modules/mysql/src/',
 | 
			
		||||
      ],
 | 
			
		||||
      'pgsql' => [
 | 
			
		||||
        'namespace' => 'Drupal\\pgsql',
 | 
			
		||||
        'autoload' => 'core/modules/pgsql/src/',
 | 
			
		||||
      ],
 | 
			
		||||
    ], $installedDatabaseSettings['default']['default']['dependencies']);
 | 
			
		||||
 | 
			
		||||
    // Assert that the module "driver_test" and its dependencies have been
 | 
			
		||||
    // installed.
 | 
			
		||||
    $this->drupalGet('admin/modules');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('modules[driver_test][enable]');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('modules[mysql][enable]');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('modules[pgsql][enable]');
 | 
			
		||||
 | 
			
		||||
    // The module "driver_test" can not be uninstalled, because it is providing
 | 
			
		||||
    // the database driver. Also, the "mysql" and "pgsql" modules can not be
 | 
			
		||||
    // uninstalled being dependencies of the "driver_test" module.
 | 
			
		||||
    $this->drupalGet('admin/modules/uninstall');
 | 
			
		||||
    $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-driver-test"]', "The following reason prevents Contrib database driver test from being uninstalled: The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'.");
 | 
			
		||||
    $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: driver_test");
 | 
			
		||||
    $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the databases setup from the SUT's settings.php.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array<string,mixed>
 | 
			
		||||
   *   The value of the $databases variable.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getInstalledDatabaseSettings(): array {
 | 
			
		||||
    // The $app_root and $site_path variables are required by the settings.php
 | 
			
		||||
    // file to be parsed correctly. The $databases variable is set in the
 | 
			
		||||
    // included file, we need to inform PHPStan about that since PHPStan itself
 | 
			
		||||
    // is unable to determine it.
 | 
			
		||||
    $app_root = $this->container->getParameter('app.root');
 | 
			
		||||
    $site_path = $this->siteDirectory;
 | 
			
		||||
    include $app_root . '/' . $site_path . '/settings.php';
 | 
			
		||||
    assert(isset($databases));
 | 
			
		||||
    return $databases;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,79 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installing a profile with non-English language and no locale module.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerNonEnglishProfileWithoutLocaleModuleTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The testing profile name.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  const PROFILE = 'testing_with_language_without_locale';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = self::PROFILE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
 | 
			
		||||
    // Create a self::PROFILE testing profile that depends on the 'language'
 | 
			
		||||
    // module but not on 'locale' module. We set core_version_requirement to '*'
 | 
			
		||||
    // for the test so that it does not need to be updated between major
 | 
			
		||||
    // versions.
 | 
			
		||||
    $profile_info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Test with language but without locale',
 | 
			
		||||
      'install' => ['language'],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $profile_dir = "{$this->root}/{$this->siteDirectory}/profiles/" . self::PROFILE;
 | 
			
		||||
    $profile_config_dir = "$profile_dir/config/install";
 | 
			
		||||
    mkdir($profile_config_dir, 0777, TRUE);
 | 
			
		||||
    $profile_info_file = $profile_dir . '/' . static::PROFILE . '.info.yml';
 | 
			
		||||
    file_put_contents($profile_info_file, Yaml::encode($profile_info));
 | 
			
		||||
 | 
			
		||||
    // Copy a non-English language config YAML to be installed with the profile.
 | 
			
		||||
    copy($this->root . '/core/profiles/tests/testing_multilingual/config/install/language.entity.de.yml', $profile_config_dir . '/language.entity.de.yml');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests installing a profile with non-English language and no locale module.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonEnglishProfileWithoutLocaleModule(): void {
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Verify that the confirmation message appears.
 | 
			
		||||
    require_once $this->root . '/core/includes/install.inc';
 | 
			
		||||
    $this->assertSession()->pageTextContains('Congratulations, you installed Drupal!');
 | 
			
		||||
 | 
			
		||||
    $this->assertFalse(\Drupal::service('module_handler')->moduleExists('locale'), 'The Locale module is not installed.');
 | 
			
		||||
    $this->assertTrue(\Drupal::service('module_handler')->moduleExists('language'), 'The Language module is installed.');
 | 
			
		||||
    $this->assertTrue(\Drupal::languageManager()->isMultilingual(), 'The language manager is multi-lingual.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Routing\RoutingEvents;
 | 
			
		||||
use Drupal\Core\Test\PerformanceTestRecorder;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the interactive installer.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerPerformanceTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareSettings(): void {
 | 
			
		||||
    parent::prepareSettings();
 | 
			
		||||
    PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', FALSE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that the user page is available after installation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    // Ensures that router is not rebuilt unnecessarily during the install.
 | 
			
		||||
    // Currently it is built once during the install in install_finished().
 | 
			
		||||
    $this->assertSame(1, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests re-visiting the installer after a successful installation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerPostInstallTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'minimal';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that visiting the installer does not break things post-install.
 | 
			
		||||
   */
 | 
			
		||||
  public function testVisitInstallerPostInstall(): void {
 | 
			
		||||
    \Drupal::service('module_installer')->install(['system_test']);
 | 
			
		||||
    // Clear caches to ensure that system_test's routes are available.
 | 
			
		||||
    $this->resetAll();
 | 
			
		||||
    // Confirm that the install_profile is correct.
 | 
			
		||||
    $this->drupalGet('/system-test/get-install-profile');
 | 
			
		||||
    $this->assertSession()->pageTextContains('minimal');
 | 
			
		||||
    // Make an anonymous visit to the installer
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
    $this->visitInstaller();
 | 
			
		||||
    // Ensure that the install profile is still correct.
 | 
			
		||||
    $this->drupalGet('/system-test/get-install-profile');
 | 
			
		||||
    $this->assertSession()->pageTextContains('minimal');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that an install profile can implement hook_requirements().
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerProfileRequirementsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_requirements';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // The parent method asserts that there are no requirements errors, but
 | 
			
		||||
    // this test expects a requirements error in the test method below.
 | 
			
		||||
    // Therefore, we override this method to suppress the parent's assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert that the profile failed hook_requirements().
 | 
			
		||||
   */
 | 
			
		||||
  public function testHookRequirementsFailure(): void {
 | 
			
		||||
    $this->assertSession()->pageTextContains('Testing requirements failed requirements.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Routing\RoutingEvents;
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
use Drupal\Core\Test\PerformanceTestRecorder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests router rebuilding during installation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerRouterTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'test_profile';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $info = [
 | 
			
		||||
      'type' => 'profile',
 | 
			
		||||
      'core_version_requirement' => '*',
 | 
			
		||||
      'name' => 'Router testing profile',
 | 
			
		||||
      'install' => [
 | 
			
		||||
        'router_test',
 | 
			
		||||
        'router_installer_test',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // File API functions are not available yet.
 | 
			
		||||
    $path = $this->siteDirectory . '/profiles/test_profile';
 | 
			
		||||
    mkdir($path, 0777, TRUE);
 | 
			
		||||
    file_put_contents("$path/test_profile.info.yml", Yaml::encode($info));
 | 
			
		||||
 | 
			
		||||
    $settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
 | 
			
		||||
    copy($settings_services_file, $this->siteDirectory . '/services.yml');
 | 
			
		||||
    PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Ensures that router is not rebuilt unnecessarily during the install. It
 | 
			
		||||
    // is rebuilt during:
 | 
			
		||||
    // - router_test_install()
 | 
			
		||||
    // - router_installer_test_modules_installed()
 | 
			
		||||
    // - install_finished()
 | 
			
		||||
    $this->assertSame(3, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
 | 
			
		||||
    $this->assertStringEndsWith('/core/install.php/router_installer_test/test1', \Drupal::state()->get('router_installer_test_modules_installed'));
 | 
			
		||||
    $this->assertStringEndsWith('/core/install.php/router_test/test1', \Drupal::state()->get('router_test_install'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that the installer uses the profile's site configuration.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerSiteConfigProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_site_config';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The site mail we expect to be set from the install profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @see testing_site_config_install()
 | 
			
		||||
   */
 | 
			
		||||
  const EXPECTED_SITE_MAIL = 'profile-testing-site-config@example.com';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The timezone we expect to be set from the install profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @see testing_site_config_install()
 | 
			
		||||
   */
 | 
			
		||||
  const EXPECTED_TIMEZONE = 'America/Los_Angeles';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $parameters = parent::installParameters();
 | 
			
		||||
 | 
			
		||||
    // Don't override the site email address, allowing it to default to the one
 | 
			
		||||
    // from our install profile.
 | 
			
		||||
    unset($parameters['forms']['install_configure_form']['site_mail']);
 | 
			
		||||
 | 
			
		||||
    return $parameters;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite() {
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('site_mail', self::EXPECTED_SITE_MAIL);
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('date_default_timezone', self::EXPECTED_TIMEZONE);
 | 
			
		||||
 | 
			
		||||
    return parent::setUpSite();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verify the correct site config was set.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertEquals(self::EXPECTED_SITE_MAIL, $this->config('system.site')->get('mail'));
 | 
			
		||||
    $this->assertEquals(self::EXPECTED_TIMEZONE, $this->config('system.date')->get('timezone.default'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,48 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that the installer skipped permission hardening.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerSkipPermissionHardeningTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $this->settings['settings']['skip_permissions_hardening'] = (object) ['value' => TRUE, 'required' => TRUE];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    $site_directory = $this->container->getParameter('app.root') . '/' . $this->siteDirectory;
 | 
			
		||||
    $this->assertDirectoryIsWritable($site_directory);
 | 
			
		||||
    $this->assertFileIsWritable($site_directory . '/settings.php');
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->responseContains('All necessary changes to <em class="placeholder">' . $this->siteDirectory . '</em> and <em class="placeholder">' . $this->siteDirectory . '/settings.php</em> have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="https://www.drupal.org/server-permissions">online handbook</a>.');
 | 
			
		||||
 | 
			
		||||
    parent::setUpSite();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies the expected behaviors of the installation result.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,158 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\Core\Routing\RoutingEvents;
 | 
			
		||||
use Drupal\Core\Test\PerformanceTestRecorder;
 | 
			
		||||
use Drupal\Core\Extension\ModuleUninstallValidatorException;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore drupalmysqldriverdatabasemysql drupalpgsqldriverdatabasepgsql
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the interactive installer.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that the user page is available after installation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertNotEquals('0', \Drupal::service('asset.query_string')->get(), 'The dummy query string should be set during install');
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Verify that the confirmation message appears.
 | 
			
		||||
    require_once $this->root . '/core/includes/install.inc';
 | 
			
		||||
    $this->assertSession()->pageTextContains('Congratulations, you installed Drupal!');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the timezone is correct for sites under test after installing
 | 
			
		||||
    // interactively.
 | 
			
		||||
    $this->assertEquals('Australia/Sydney', $this->config('system.date')->get('timezone.default'));
 | 
			
		||||
 | 
			
		||||
    // Ensure the profile has a weight of 1000.
 | 
			
		||||
    $module_extension_list = \Drupal::service('extension.list.module');
 | 
			
		||||
    $extensions = $module_extension_list->getList();
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('testing', $extensions);
 | 
			
		||||
    $this->assertEquals(1000, $extensions['testing']->weight);
 | 
			
		||||
    // Ensures that router is not rebuilt unnecessarily during the install.
 | 
			
		||||
    $this->assertSame(1, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select language.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Test that \Drupal\Core\Render\BareHtmlPageRenderer adds assets and
 | 
			
		||||
    // metatags as expected to the first page of the installer.
 | 
			
		||||
    $this->assertSession()->responseContains("css/components/button.css");
 | 
			
		||||
    $this->assertSession()->responseContains('<meta charset="utf-8" />');
 | 
			
		||||
 | 
			
		||||
    // Assert that the expected title is present.
 | 
			
		||||
    $this->assertEquals('Choose language', $this->cssSelect('main h2')[0]->getText());
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    $settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
 | 
			
		||||
    // Copy the testing-specific service overrides in place.
 | 
			
		||||
    copy($settings_services_file, $this->siteDirectory . '/services.yml');
 | 
			
		||||
    PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', TRUE);
 | 
			
		||||
    // Assert that the expected title is present.
 | 
			
		||||
    $this->assertEquals('Select an installation profile', $this->cssSelect('main h2')[0]->getText());
 | 
			
		||||
    // Verify that Title/Label are not displayed when '#title_display' =>
 | 
			
		||||
    // 'invisible' attribute is set.
 | 
			
		||||
    $this->assertSession()->elementsCount('xpath', "//span[contains(@class, 'visually-hidden') and contains(text(), 'Select an installation profile')]", 1);
 | 
			
		||||
 | 
			
		||||
    parent::setUpProfile();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // Assert that the expected title is present.
 | 
			
		||||
    $this->assertEquals('Database configuration', $this->cssSelect('main h2')[0]->getText());
 | 
			
		||||
 | 
			
		||||
    // Assert that we use the by core supported database drivers by default and
 | 
			
		||||
    // not the ones from the driver_test module.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupalmysqldriverdatabasemysql"]', 'MySQL, MariaDB, Percona Server, or equivalent');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupalpgsqldriverdatabasepgsql"]', 'PostgreSQL');
 | 
			
		||||
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // Assert that the expected title is present.
 | 
			
		||||
    $this->assertEquals('Configure site', $this->cssSelect('main h2')[0]->getText());
 | 
			
		||||
 | 
			
		||||
    // Test that SiteConfigureForm::buildForm() has made the site directory and
 | 
			
		||||
    // the settings file non-writable.
 | 
			
		||||
    $site_directory = $this->container->getParameter('app.root') . '/' . $this->siteDirectory;
 | 
			
		||||
    $this->assertDirectoryIsNotWritable($site_directory);
 | 
			
		||||
    $this->assertFileIsNotWritable($site_directory . '/settings.php');
 | 
			
		||||
 | 
			
		||||
    parent::setUpSite();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    parent::visitInstaller();
 | 
			
		||||
 | 
			
		||||
    // Assert the title is correct and has the title suffix.
 | 
			
		||||
    $this->assertSession()->titleEquals('Choose language | Drupal');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    $database = Database::getConnection();
 | 
			
		||||
    $module = $database->getProvider();
 | 
			
		||||
    $module_handler = \Drupal::service('module_handler');
 | 
			
		||||
    $module_extension_list = \Drupal::service('extension.list.module');
 | 
			
		||||
 | 
			
		||||
    // Ensure the Update Status module is not installed.
 | 
			
		||||
    $this->assertFalse($module_handler->moduleExists('update'), 'The Update Status module should not be installed.');
 | 
			
		||||
 | 
			
		||||
    // Assert that the module that is providing the database driver has been
 | 
			
		||||
    // installed.
 | 
			
		||||
    $this->assertTrue($module_handler->moduleExists($module));
 | 
			
		||||
 | 
			
		||||
    // The module that is providing the database driver should be uninstallable.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->container->get('module_installer')->uninstall([$module]);
 | 
			
		||||
      $this->fail("Uninstalled $module module.");
 | 
			
		||||
    }
 | 
			
		||||
    catch (ModuleUninstallValidatorException $e) {
 | 
			
		||||
      $module_name = $module_extension_list->getName($module);
 | 
			
		||||
      $driver = $database->driver();
 | 
			
		||||
      $this->assertStringContainsString("The module '$module_name' is providing the database driver '$driver'.", $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,305 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\DrupalKernel;
 | 
			
		||||
use Drupal\Core\Language\Language;
 | 
			
		||||
use Drupal\Core\Session\UserSession;
 | 
			
		||||
use Drupal\Core\Site\Settings;
 | 
			
		||||
use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
 | 
			
		||||
use Drupal\Core\Utility\PhpRequirements;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\RequirementsPageTrait;
 | 
			
		||||
use GuzzleHttp\HandlerStack;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Reference;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\RequestStack;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Session\Session;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for testing the interactive installer.
 | 
			
		||||
 */
 | 
			
		||||
abstract class InstallerTestBase extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use RequirementsPageTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Custom settings.php values to write for a test run.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   *   An array of settings to write out, in the format expected by
 | 
			
		||||
   *   SettingsEditor::rewrite().
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Site\SettingsEditor::rewrite()
 | 
			
		||||
   */
 | 
			
		||||
  protected $settings = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The language code in which to install Drupal.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'en';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The installation profile to install.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Additional parameters to use for installer screens.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   *
 | 
			
		||||
   * @see FunctionalTestSetupTrait::installParameters()
 | 
			
		||||
   */
 | 
			
		||||
  protected $parameters = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A string translation map used for translated installer screens.
 | 
			
		||||
   *
 | 
			
		||||
   * Keys are English strings, values are translated strings.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $translations = [
 | 
			
		||||
    'Save and continue' => 'Save and continue',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether the installer has completed.
 | 
			
		||||
   *
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $isInstalled = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $params = parent::installParameters();
 | 
			
		||||
    // Set the checkbox values to FALSE so that
 | 
			
		||||
    // \Drupal\Tests\BrowserTestBase::translatePostValues() does not remove
 | 
			
		||||
    // them.
 | 
			
		||||
    $params['forms']['install_configure_form']['enable_update_status_module'] = FALSE;
 | 
			
		||||
    $params['forms']['install_configure_form']['enable_update_status_emails'] = FALSE;
 | 
			
		||||
    return $params;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * We are testing the installer, so set up a minimal environment for that.
 | 
			
		||||
   */
 | 
			
		||||
  public function installDrupal() {
 | 
			
		||||
    // Define information about the user 1 account.
 | 
			
		||||
    $this->rootUser = new UserSession([
 | 
			
		||||
      'uid' => 1,
 | 
			
		||||
      'name' => 'admin',
 | 
			
		||||
      'mail' => 'admin@example.com',
 | 
			
		||||
      'pass_raw' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // If any $settings are defined for this test, copy and prepare an actual
 | 
			
		||||
    // settings.php, so as to resemble a regular installation.
 | 
			
		||||
    if (!empty($this->settings)) {
 | 
			
		||||
      // Not using File API; a potential error must trigger a PHP warning.
 | 
			
		||||
      copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
 | 
			
		||||
      $this->writeSettings($this->settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Note that FunctionalTestSetupTrait::installParameters() returns form
 | 
			
		||||
    // input values suitable for a programmed
 | 
			
		||||
    // \Drupal::formBuilder()->submitForm().
 | 
			
		||||
    // @see InstallerTestBase::translatePostValues()
 | 
			
		||||
    $this->parameters = $this->installParameters();
 | 
			
		||||
 | 
			
		||||
    // Set up a minimal container (required by BrowserTestBase). Set cookie and
 | 
			
		||||
    // server information so that XDebug works.
 | 
			
		||||
    // @see install_begin_request()
 | 
			
		||||
    $request = Request::create($GLOBALS['base_url'] . '/core/install.php', 'GET', [], $_COOKIE, [], $_SERVER);
 | 
			
		||||
    $request->setSession(new Session(new MockArraySessionStorage()));
 | 
			
		||||
    $this->container = new ContainerBuilder();
 | 
			
		||||
    $request_stack = new RequestStack();
 | 
			
		||||
    $request_stack->push($request);
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->set('request_stack', $request_stack);
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->setParameter('language.default_values', Language::$defaultValues);
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->register('language.default', 'Drupal\Core\Language\LanguageDefault')
 | 
			
		||||
      ->addArgument('%language.default_values%');
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
 | 
			
		||||
      ->addArgument(new Reference('language.default'));
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->register('http_client', 'GuzzleHttp\Client')
 | 
			
		||||
      ->setFactory('http_client_factory:fromOptions');
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->register('http_client_factory', 'Drupal\Core\Http\ClientFactory')
 | 
			
		||||
      ->setArguments([new Reference('http_handler_stack')]);
 | 
			
		||||
    $handler_stack = HandlerStack::create();
 | 
			
		||||
    $test_http_client_middleware = new TestHttpClientMiddleware();
 | 
			
		||||
    $handler_stack->push($test_http_client_middleware(), 'test.http_client.middleware');
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->set('http_handler_stack', $handler_stack);
 | 
			
		||||
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->setParameter('app.root', DRUPAL_ROOT);
 | 
			
		||||
    \Drupal::setContainer($this->container);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->visitInstaller();
 | 
			
		||||
 | 
			
		||||
    // Select language.
 | 
			
		||||
    $this->setUpLanguage();
 | 
			
		||||
 | 
			
		||||
    // Select profile.
 | 
			
		||||
    $this->setUpProfile();
 | 
			
		||||
 | 
			
		||||
    // Address the requirements problem screen, if any.
 | 
			
		||||
    $this->setUpRequirementsProblem();
 | 
			
		||||
 | 
			
		||||
    // Configure settings.
 | 
			
		||||
    $this->setUpSettings();
 | 
			
		||||
 | 
			
		||||
    // @todo Allow test classes based on this class to act on further installer
 | 
			
		||||
    //   screens.
 | 
			
		||||
 | 
			
		||||
    // Configure site.
 | 
			
		||||
    $this->setUpSite();
 | 
			
		||||
 | 
			
		||||
    if ($this->isInstalled) {
 | 
			
		||||
      // Import new settings.php written by the installer.
 | 
			
		||||
      $request = Request::createFromGlobals();
 | 
			
		||||
      $class_loader = require $this->container->getParameter('app.root') . '/autoload.php';
 | 
			
		||||
      Settings::initialize($this->container->getParameter('app.root'), DrupalKernel::findSitePath($request), $class_loader);
 | 
			
		||||
 | 
			
		||||
      // After writing settings.php, the installer removes write permissions
 | 
			
		||||
      // from the site directory. To allow drupal_generate_test_ua() to write
 | 
			
		||||
      // a file containing the private key for drupal_valid_test_ua(), the site
 | 
			
		||||
      // directory has to be writable.
 | 
			
		||||
      // BrowserTestBase::tearDown() will delete the entire test site directory.
 | 
			
		||||
      // Not using File API; a potential error must trigger a PHP warning.
 | 
			
		||||
      chmod($this->container->getParameter('app.root') . '/' . $this->siteDirectory, 0777);
 | 
			
		||||
      $this->kernel = DrupalKernel::createFromRequest($request, $class_loader, 'prod', FALSE);
 | 
			
		||||
      $this->kernel->boot();
 | 
			
		||||
      $this->kernel->preHandle($request);
 | 
			
		||||
      $this->container = $this->kernel->getContainer();
 | 
			
		||||
 | 
			
		||||
      // Manually configure the test mail collector implementation to prevent
 | 
			
		||||
      // tests from sending out emails and collect them in state instead.
 | 
			
		||||
      $this->container->get('config.factory')
 | 
			
		||||
        ->getEditable('system.mail')
 | 
			
		||||
        ->set('interface.default', 'test_mail_collector')
 | 
			
		||||
        ->set('mailer_dsn', [
 | 
			
		||||
          'scheme' => 'null',
 | 
			
		||||
          'host' => 'null',
 | 
			
		||||
          'user' => NULL,
 | 
			
		||||
          'password' => NULL,
 | 
			
		||||
          'port' => NULL,
 | 
			
		||||
          'options' => [],
 | 
			
		||||
        ])
 | 
			
		||||
        ->save();
 | 
			
		||||
 | 
			
		||||
      $this->installDefaultThemeFromClassProperty($this->container);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function initFrontPage() {
 | 
			
		||||
    // We don't want to visit the front page with the installer when
 | 
			
		||||
    // initializing Mink, so we do nothing here.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Visits the interactive installer.
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller() {
 | 
			
		||||
    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select language.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Installer\Form\SelectLanguageForm
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage() {
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'langcode' => $this->langcode,
 | 
			
		||||
    ];
 | 
			
		||||
    // The 'Select Language' step is always English.
 | 
			
		||||
    $this->submitForm($edit, 'Save and continue');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Select installation profile.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile() {
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'profile' => $this->profile,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Configure settings.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings() {
 | 
			
		||||
    $parameters = $this->parameters['forms']['install_settings_form'];
 | 
			
		||||
    $driver = $parameters['driver'];
 | 
			
		||||
    unset($parameters[$driver]['dependencies']);
 | 
			
		||||
    $edit = $this->translatePostValues($parameters);
 | 
			
		||||
    $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Installer step: Requirements problem.
 | 
			
		||||
   *
 | 
			
		||||
   * Override this method to test specific requirements warnings or errors
 | 
			
		||||
   * during the installer.
 | 
			
		||||
   *
 | 
			
		||||
   * @see system_requirements()
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem() {
 | 
			
		||||
    if (version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
 | 
			
		||||
      $this->continueOnExpectedWarnings(['PHP']);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Final installer step: Configure site.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite() {
 | 
			
		||||
    $edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']);
 | 
			
		||||
    $this->submitForm($edit, $this->translations['Save and continue']);
 | 
			
		||||
    // If we've got to this point the site is installed using the regular
 | 
			
		||||
    // installation workflow.
 | 
			
		||||
    $this->isInstalled = TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * FunctionalTestSetupTrait::refreshVariables() tries to operate on persistent
 | 
			
		||||
   * storage, which is only available after the installer completed.
 | 
			
		||||
   */
 | 
			
		||||
  protected function refreshVariables() {
 | 
			
		||||
    if ($this->isInstalled) {
 | 
			
		||||
      parent::refreshVariables();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\block\Entity\Block;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies that the installer does not generate theme blocks.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerThemesBlocksProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'testing_theme_required_blocks';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_themes_blocks';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verify that there is no automatic block generation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
 | 
			
		||||
    // Account menu is a block that testing_theme_required_blocks provides,
 | 
			
		||||
    // but not testing_theme_optional_blocks. There shouldn't be a account menu
 | 
			
		||||
    // block for testing_theme_optional_blocks after the installation.
 | 
			
		||||
    $this->assertEmpty(Block::load('testing_theme_optional_blocks_account_menu'));
 | 
			
		||||
    $this->assertNotEmpty(Block::load('testing_theme_optional_blocks_page_title'));
 | 
			
		||||
 | 
			
		||||
    // Ensure that for themes without blocks, some default blocks will be
 | 
			
		||||
    // created.
 | 
			
		||||
    $this->assertNotEmpty(Block::load('testing_theme_without_blocks_account_menu'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests translation files for multiple languages get imported during install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationExistingFileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Overrides the language code in which to install Drupal.
 | 
			
		||||
   *
 | 
			
		||||
   * Choose one of the smaller languages on ftp.drupal.org. There is no way to
 | 
			
		||||
   * avoid using ftp.drupal.org since the code being tested runs extremely early
 | 
			
		||||
   * in the installer. However, even if the call to ftp.drupal.org fails then
 | 
			
		||||
   * this test will not fail as it will end up on the requirements page.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'xx-lolspeak';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place custom local translations in the translations directory.
 | 
			
		||||
    mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    $po_contents = <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
PO;
 | 
			
		||||
    // Create a misnamed translation file that
 | 
			
		||||
    // \Drupal\Core\StringTranslation\Translator\FileTranslation::findTranslationFiles()
 | 
			
		||||
    // will not find.
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0-DEV.xx-lolspeak.po', $po_contents);
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // Do nothing, because this test only tests the language installation
 | 
			
		||||
    // step's results.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // Do nothing, because this test only tests the language installation
 | 
			
		||||
    // step's results.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // Do nothing, because this test only tests the language installation
 | 
			
		||||
    // step's results.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // Do nothing, because this test only tests the language installation
 | 
			
		||||
    // step's results.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures language selection has not failed.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstall(): void {
 | 
			
		||||
    // At this point we'll be on the profile selection or requirements screen.
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests translation files for multiple languages get imported during install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationMultipleLanguageForeignTest extends InstallerTranslationMultipleLanguageTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Overrides the language code in which to install Drupal.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'de';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue de';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that keeping English in a foreign language install works.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationMultipleLanguageKeepEnglishTest extends InstallerTranslationMultipleLanguageForeignTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Switch to the multilingual testing profile with English kept.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_multilingual_with_english';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,153 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore montag
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests translation files for multiple languages get imported during install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationMultipleLanguageNonInteractiveTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use StringTranslationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Switch to the multilingual testing profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Place custom local translations in the translations directory.
 | 
			
		||||
    mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Anonymous"
 | 
			
		||||
msgstr "Anonymous $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Language"
 | 
			
		||||
msgstr "Language $langcode"
 | 
			
		||||
 | 
			
		||||
#: Testing site name configuration during the installer.
 | 
			
		||||
msgid "Drupal"
 | 
			
		||||
msgstr "Drupal"
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $params = parent::installParameters();
 | 
			
		||||
    $params['forms']['install_configure_form']['site_name'] = 'SITE_NAME_en';
 | 
			
		||||
    return $params;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that translations ended up at the expected places.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationsLoaded(): void {
 | 
			
		||||
    $this->drupalLogin($this->createUser([], NULL, TRUE));
 | 
			
		||||
    // Ensure the title is correct.
 | 
			
		||||
    $this->assertEquals('SITE_NAME_en', \Drupal::config('system.site')->get('name'));
 | 
			
		||||
 | 
			
		||||
    // Verify German and Spanish were configured.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    $this->assertSession()->pageTextContains('German');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Spanish');
 | 
			
		||||
    // If the installer was English or we used a profile that keeps English, we
 | 
			
		||||
    // expect that configured also. Otherwise English should not be configured
 | 
			
		||||
    // on the site.
 | 
			
		||||
    $this->assertSession()->pageTextContains('English');
 | 
			
		||||
 | 
			
		||||
    // Verify the strings from the translation files were imported.
 | 
			
		||||
    $this->verifyImportedStringsTranslated();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
 | 
			
		||||
    $language_manager = \Drupal::languageManager();
 | 
			
		||||
 | 
			
		||||
    // If the site was installed in a foreign language (only tested with German
 | 
			
		||||
    // in subclasses), then the active configuration should be updated and no
 | 
			
		||||
    // override should exist in German. Otherwise the German translation should
 | 
			
		||||
    // end up in overrides the same way as Spanish (which is not used as a site
 | 
			
		||||
    // installation language). English should be available based on profile
 | 
			
		||||
    // information and should be possible to add if not yet added, making
 | 
			
		||||
    // English overrides available.
 | 
			
		||||
 | 
			
		||||
    $config = \Drupal::config('user.settings');
 | 
			
		||||
    $override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
 | 
			
		||||
    $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
 | 
			
		||||
    $override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings');
 | 
			
		||||
 | 
			
		||||
    // Active configuration should be English.
 | 
			
		||||
    $this->assertEquals('Anonymous', $config->get('anonymous'));
 | 
			
		||||
    $this->assertEquals('en', $config->get('langcode'));
 | 
			
		||||
    // There should not be an English override.
 | 
			
		||||
    $this->assertTrue($override_en->isNew());
 | 
			
		||||
    // German should be an override.
 | 
			
		||||
    $this->assertEquals('Anonymous de', $override_de->get('anonymous'));
 | 
			
		||||
 | 
			
		||||
    // Spanish is always an override (never used as installation language).
 | 
			
		||||
    $this->assertEquals('Anonymous es', $override_es->get('anonymous'));
 | 
			
		||||
 | 
			
		||||
    // Test translation from locale_test module.
 | 
			
		||||
    $this->assertEquals('Montag', $this->t('Monday', [], ['langcode' => 'de']));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper function to verify that the expected strings are translated.
 | 
			
		||||
   */
 | 
			
		||||
  protected function verifyImportedStringsTranslated(): void {
 | 
			
		||||
    $test_samples = ['Save and continue', 'Anonymous', 'Language'];
 | 
			
		||||
    $langcodes = ['de', 'es'];
 | 
			
		||||
 | 
			
		||||
    foreach ($test_samples as $sample) {
 | 
			
		||||
      foreach ($langcodes as $langcode) {
 | 
			
		||||
        $edit = [];
 | 
			
		||||
        $edit['langcode'] = $langcode;
 | 
			
		||||
        $edit['translation'] = 'translated';
 | 
			
		||||
        $edit['string'] = $sample;
 | 
			
		||||
        $this->drupalGet('admin/config/regional/translate');
 | 
			
		||||
        $this->submitForm($edit, 'Filter');
 | 
			
		||||
        $this->assertSession()->pageTextContains($sample . ' ' . $langcode);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,187 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests translation files for multiple languages get imported during install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationMultipleLanguageTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Switch to the multilingual testing profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing_multilingual';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place custom local translations in the translations directory.
 | 
			
		||||
    mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es'));
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Anonymous"
 | 
			
		||||
msgstr "Anonymous $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Language"
 | 
			
		||||
msgstr "Language $langcode"
 | 
			
		||||
 | 
			
		||||
#: Testing site name configuration during the installer.
 | 
			
		||||
msgid "Drupal"
 | 
			
		||||
msgstr "Drupal"
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $params = parent::installParameters();
 | 
			
		||||
    $params['forms']['install_configure_form']['site_name'] = 'SITE_NAME_' . $this->langcode;
 | 
			
		||||
    return $params;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that translations ended up at the expected places.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationsLoaded(): void {
 | 
			
		||||
    // Ensure the title is correct.
 | 
			
		||||
    $this->assertEquals('SITE_NAME_' . $this->langcode, \Drupal::config('system.site')->get('name'));
 | 
			
		||||
 | 
			
		||||
    // Verify German and Spanish were configured.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    $this->assertSession()->pageTextContains('German');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Spanish');
 | 
			
		||||
    // If the installer was English or we used a profile that keeps English, we
 | 
			
		||||
    // expect that configured also. Otherwise English should not be configured
 | 
			
		||||
    // on the site.
 | 
			
		||||
    if ($this->langcode == 'en' || $this->profile == 'testing_multilingual_with_english') {
 | 
			
		||||
      $this->assertSession()->pageTextContains('English');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertSession()->pageTextNotContains('English');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Verify the strings from the translation files were imported.
 | 
			
		||||
    $this->verifyImportedStringsTranslated();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
 | 
			
		||||
    $language_manager = \Drupal::languageManager();
 | 
			
		||||
 | 
			
		||||
    // If the site was installed in a foreign language (only tested with German
 | 
			
		||||
    // in subclasses), then the active configuration should be updated and no
 | 
			
		||||
    // override should exist in German. Otherwise the German translation should
 | 
			
		||||
    // end up in overrides the same way as Spanish (which is not used as a site
 | 
			
		||||
    // installation language). English should be available based on profile
 | 
			
		||||
    // information and should be possible to add if not yet added, making
 | 
			
		||||
    // English overrides available.
 | 
			
		||||
 | 
			
		||||
    $config = \Drupal::config('user.settings');
 | 
			
		||||
    $override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
 | 
			
		||||
    $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
 | 
			
		||||
    $override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings');
 | 
			
		||||
 | 
			
		||||
    if ($this->langcode == 'de') {
 | 
			
		||||
      // Active configuration should be in German and no German override should
 | 
			
		||||
      // exist.
 | 
			
		||||
      $this->assertEquals('Anonymous de', $config->get('anonymous'));
 | 
			
		||||
      $this->assertEquals('de', $config->get('langcode'));
 | 
			
		||||
      $this->assertTrue($override_de->isNew());
 | 
			
		||||
 | 
			
		||||
      if ($this->profile == 'testing_multilingual_with_english') {
 | 
			
		||||
        // English is already added in this profile. Should make the override
 | 
			
		||||
        // available.
 | 
			
		||||
        $this->assertEquals('Anonymous', $override_en->get('anonymous'));
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // English is not yet available.
 | 
			
		||||
        $this->assertTrue($override_en->isNew());
 | 
			
		||||
 | 
			
		||||
        // Adding English should make the English override available.
 | 
			
		||||
        $edit = ['predefined_langcode' => 'en'];
 | 
			
		||||
        $this->drupalGet('admin/config/regional/language/add');
 | 
			
		||||
        $this->submitForm($edit, 'Add language');
 | 
			
		||||
        $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
 | 
			
		||||
        $this->assertEquals('Anonymous', $override_en->get('anonymous'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Activate a module, to make sure that config is not overridden by module
 | 
			
		||||
      // installation.
 | 
			
		||||
      $edit = [
 | 
			
		||||
        'modules[views][enable]' => TRUE,
 | 
			
		||||
        'modules[filter][enable]' => TRUE,
 | 
			
		||||
      ];
 | 
			
		||||
      $this->drupalGet('admin/modules');
 | 
			
		||||
      $this->submitForm($edit, 'Install');
 | 
			
		||||
 | 
			
		||||
      // Verify the strings from the translation are still as expected.
 | 
			
		||||
      $this->verifyImportedStringsTranslated();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // Active configuration should be English.
 | 
			
		||||
      $this->assertEquals('Anonymous', $config->get('anonymous'));
 | 
			
		||||
      $this->assertEquals('en', $config->get('langcode'));
 | 
			
		||||
      // There should not be an English override.
 | 
			
		||||
      $this->assertTrue($override_en->isNew());
 | 
			
		||||
      // German should be an override.
 | 
			
		||||
      $this->assertEquals('Anonymous de', $override_de->get('anonymous'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Spanish is always an override (never used as installation language).
 | 
			
		||||
    $this->assertEquals('Anonymous es', $override_es->get('anonymous'));
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper function to verify that the expected strings are translated.
 | 
			
		||||
   */
 | 
			
		||||
  protected function verifyImportedStringsTranslated(): void {
 | 
			
		||||
    $test_samples = ['Save and continue', 'Anonymous', 'Language'];
 | 
			
		||||
    $langcodes = ['de', 'es'];
 | 
			
		||||
 | 
			
		||||
    foreach ($test_samples as $sample) {
 | 
			
		||||
      foreach ($langcodes as $langcode) {
 | 
			
		||||
        $edit = [];
 | 
			
		||||
        $edit['langcode'] = $langcode;
 | 
			
		||||
        $edit['translation'] = 'translated';
 | 
			
		||||
        $edit['string'] = $sample;
 | 
			
		||||
        $this->drupalGet('admin/config/regional/translate');
 | 
			
		||||
        $this->submitForm($edit, 'Filter');
 | 
			
		||||
        $this->assertSession()->pageTextContains($sample . ' ' . $langcode);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,43 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests non-standard named translation files get imported during install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationNonStandardFilenamesTest extends InstallerTranslationMultipleLanguageNonInteractiveTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    BrowserTestBase::prepareEnvironment();
 | 
			
		||||
    // Place custom local translations in the translations directory.
 | 
			
		||||
    mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal.de.po', $this->getPo('de'));
 | 
			
		||||
    file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal.es.po', $this->getPo('es'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareSettings(): void {
 | 
			
		||||
    parent::prepareSettings();
 | 
			
		||||
    $settings['config']['locale.settings']['translation']['default_filename'] = (object) [
 | 
			
		||||
      'value' => '%project.%language.po',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $settings['config']['locale.settings']['translation']['default_server_pattern'] = (object) [
 | 
			
		||||
      'value' => 'translations://%project.%language.po',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->writeSettings($settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,90 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Installs Drupal in German and checks resulting site.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\FunctionalTests\Installer\InstallerTranslationTest
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationQueryTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Overrides the language code in which to install Drupal.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'de';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function visitInstaller(): void {
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
 | 
			
		||||
    // The unrouted URL assembler does not exist at this point, so we build the
 | 
			
		||||
    // URL ourselves.
 | 
			
		||||
    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=' . $this->langcode);
 | 
			
		||||
 | 
			
		||||
    // The language should have been automatically detected, all following
 | 
			
		||||
    // screens should be translated already.
 | 
			
		||||
    $this->assertSession()->buttonExists('Save and continue de');
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue de';
 | 
			
		||||
 | 
			
		||||
    // Check the language direction.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // The language was preset by passing a query parameter in the URL, so no
 | 
			
		||||
    // explicit language selection is necessary.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies the expected behaviors of the installation result.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Verify German was configured but not English.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    // cspell:ignore deutsch
 | 
			
		||||
    $this->assertSession()->pageTextContains('Deutsch');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('English');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,194 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\user\Entity\User;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Installs Drupal in German and checks resulting site.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class InstallerTranslationTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'test_theme';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Overrides the language code in which to install Drupal.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcode = 'de';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Place a custom local translation in the translations directory.
 | 
			
		||||
    mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
 | 
			
		||||
    file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
 | 
			
		||||
    // After selecting a different language than English, all following screens
 | 
			
		||||
    // should be translated already.
 | 
			
		||||
    $this->assertSession()->buttonExists('Save and continue de');
 | 
			
		||||
    $this->translations['Save and continue'] = 'Save and continue de';
 | 
			
		||||
 | 
			
		||||
    // Check the language direction.
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // We are creating a table here to force an error in the installer because
 | 
			
		||||
    // it will try and create the drupal_install_test table as this is part of
 | 
			
		||||
    // the standard database tests performed by the installer in
 | 
			
		||||
    // Drupal\Core\Database\Install\Tasks.
 | 
			
		||||
    $spec = [
 | 
			
		||||
      'fields' => [
 | 
			
		||||
        'id' => [
 | 
			
		||||
          'type' => 'int',
 | 
			
		||||
          'not null' => TRUE,
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'primary key' => ['id'],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    Database::getConnection('default')->schema()->createTable('drupal_install_test', $spec);
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
 | 
			
		||||
    // Ensure that the error message translation is working.
 | 
			
		||||
    // cSpell:disable
 | 
			
		||||
    $this->assertSession()->responseContains('Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/docs/installing-drupal">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter.');
 | 
			
		||||
    $this->assertSession()->responseContains('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)</em> fehlgeschlagen.');
 | 
			
		||||
    // cSpell:enable
 | 
			
		||||
 | 
			
		||||
    // Now do it successfully.
 | 
			
		||||
    Database::getConnection('default')->schema()->dropTable('drupal_install_test');
 | 
			
		||||
    parent::setUpSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies the expected behaviors of the installation result.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstaller(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Verify German was configured but not English.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language');
 | 
			
		||||
    // cspell:ignore deutsch
 | 
			
		||||
    $this->assertSession()->pageTextContains('Deutsch');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('English');
 | 
			
		||||
 | 
			
		||||
    // The current container still has the english as current language, rebuild.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
    /** @var \Drupal\user\Entity\User $account */
 | 
			
		||||
    $account = User::load(0);
 | 
			
		||||
    $this->assertEquals('de', $account->language()->getId(), 'Anonymous user is German.');
 | 
			
		||||
    $account = User::load(1);
 | 
			
		||||
    $this->assertEquals('de', $account->language()->getId(), 'Administrator user is German.');
 | 
			
		||||
    $account = $this->drupalCreateUser();
 | 
			
		||||
    $this->assertEquals('de', $account->language()->getId(), 'New user is German.');
 | 
			
		||||
 | 
			
		||||
    // Ensure that we can enable basic_auth on a non-english site.
 | 
			
		||||
    $this->drupalGet('admin/modules');
 | 
			
		||||
    $this->submitForm(['modules[basic_auth][enable]' => TRUE], 'Install');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Assert that the theme CSS was added to the page.
 | 
			
		||||
    $edit = ['preprocess_css' => FALSE];
 | 
			
		||||
    $this->drupalGet('admin/config/development/performance');
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->drupalGet('<front>');
 | 
			
		||||
    $this->assertSession()->responseContains('my_theme/css/my-container-inline.css');
 | 
			
		||||
 | 
			
		||||
    // Verify the strings from the translation files were imported.
 | 
			
		||||
    $test_samples = ['Save and continue', 'Anonymous'];
 | 
			
		||||
    foreach ($test_samples as $sample) {
 | 
			
		||||
      $edit = [];
 | 
			
		||||
      $edit['langcode'] = 'de';
 | 
			
		||||
      $edit['translation'] = 'translated';
 | 
			
		||||
      $edit['string'] = $sample;
 | 
			
		||||
      $this->drupalGet('admin/config/regional/translate');
 | 
			
		||||
      $this->submitForm($edit, 'Filter');
 | 
			
		||||
      $this->assertSession()->pageTextContains($sample . ' de');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
 | 
			
		||||
    $language_manager = \Drupal::languageManager();
 | 
			
		||||
 | 
			
		||||
    // Installed in German, configuration should be in German. No German or
 | 
			
		||||
    // English overrides should be present.
 | 
			
		||||
    $config = \Drupal::config('user.settings');
 | 
			
		||||
    $override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
 | 
			
		||||
    $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
 | 
			
		||||
    $this->assertEquals('Anonymous de', $config->get('anonymous'));
 | 
			
		||||
    $this->assertEquals('de', $config->get('langcode'));
 | 
			
		||||
    $this->assertTrue($override_de->isNew());
 | 
			
		||||
    $this->assertTrue($override_en->isNew());
 | 
			
		||||
 | 
			
		||||
    // Assert that adding English makes the English override available.
 | 
			
		||||
    $edit = ['predefined_langcode' => 'en'];
 | 
			
		||||
    $this->drupalGet('admin/config/regional/language/add');
 | 
			
		||||
    $this->submitForm($edit, 'Add language');
 | 
			
		||||
    $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
 | 
			
		||||
    $this->assertFalse($override_en->isNew());
 | 
			
		||||
    $this->assertSession()->pageTextContains('English de');
 | 
			
		||||
    $this->assertEquals('Anonymous', $override_en->get('anonymous'));
 | 
			
		||||
 | 
			
		||||
    $english = ConfigurableLanguage::load('en');
 | 
			
		||||
    $this->assertEquals('de', $english->language()->getId(), 'The langcode of the english language is de.');
 | 
			
		||||
 | 
			
		||||
    // English is guaranteed to be the second language, click the second
 | 
			
		||||
    // language edit link.
 | 
			
		||||
    $this->clickLink('Edit', 1);
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('label', 'English de');
 | 
			
		||||
    $this->submitForm([], 'Save language');
 | 
			
		||||
 | 
			
		||||
    $english = ConfigurableLanguage::load('en');
 | 
			
		||||
    $this->assertEquals('de', $english->language()->getId(), 'The langcode of the english language is de.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string for the test .po file.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Contents for the test .po file.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getPo($langcode): string {
 | 
			
		||||
    return <<<PO
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Save and continue"
 | 
			
		||||
msgstr "Save and continue $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Anonymous"
 | 
			
		||||
msgstr "Anonymous $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "English"
 | 
			
		||||
msgstr "English $langcode"
 | 
			
		||||
 | 
			
		||||
msgid "Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/docs/installing-drupal">installation handbook</a>, or contact your hosting provider."
 | 
			
		||||
msgstr "Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/docs/installing-drupal">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter."
 | 
			
		||||
 | 
			
		||||
msgid "Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>"
 | 
			
		||||
msgstr "<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl %query fehlgeschlagen."
 | 
			
		||||
PO;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,35 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\AssertConfigTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the interactive installer installing the minimal profile.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class MinimalInstallerTest extends ConfigAfterInstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  use AssertConfigTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'minimal';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that the exported minimal configuration is up to date.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMinimalConfig(): void {
 | 
			
		||||
    $this->assertInstalledConfig([]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,93 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests multiple distribution profile support.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class MultipleDistributionsProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The distribution profile info.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $info;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    // Create two distributions.
 | 
			
		||||
    foreach (['distribution_one', 'distribution_two'] as $name) {
 | 
			
		||||
      $info = [
 | 
			
		||||
        'type' => 'profile',
 | 
			
		||||
        'core_version_requirement' => '*',
 | 
			
		||||
        'name' => $name . ' profile',
 | 
			
		||||
        'distribution' => [
 | 
			
		||||
          'name' => $name,
 | 
			
		||||
          'install' => [
 | 
			
		||||
            'theme' => 'claro',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ];
 | 
			
		||||
      // File API functions are not available yet.
 | 
			
		||||
      $path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/' . $name;
 | 
			
		||||
      mkdir($path, 0777, TRUE);
 | 
			
		||||
      file_put_contents("$path/$name.info.yml", Yaml::encode($info));
 | 
			
		||||
    }
 | 
			
		||||
    // Install the first distribution.
 | 
			
		||||
    $this->profile = 'distribution_one';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpLanguage(): void {
 | 
			
		||||
    // Verify that the distribution name appears.
 | 
			
		||||
    $this->assertSession()->pageTextContains('distribution_one');
 | 
			
		||||
    // Verify that the requested theme is used.
 | 
			
		||||
    $this->assertSession()->responseContains('claro');
 | 
			
		||||
    // Verify that the "Choose profile" step does not appear.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('profile');
 | 
			
		||||
 | 
			
		||||
    parent::setUpLanguage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is a distribution profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Confirm that Drupal recognizes this distribution as the current profile.
 | 
			
		||||
    $this->assertEquals('distribution_one', \Drupal::installProfile());
 | 
			
		||||
    $this->assertEquals('distribution_one', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
 | 
			
		||||
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
    $this->assertEquals('distribution_one', \Drupal::installProfile());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests installing a profile that implements InstallRequirementsInterface.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class ProfileRequirementsTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'profile_install_requirements';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSettings(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpRequirementsProblem(): void {
 | 
			
		||||
    // The parent method asserts that there are no requirements errors, but
 | 
			
		||||
    // this test expects a requirements error in the test method below.
 | 
			
		||||
    // Therefore, we override this method to suppress the parent's assertions.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpSite(): void {
 | 
			
		||||
    // This form will never be reached.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test Requirements are picked up.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRequirementsFailure(): void {
 | 
			
		||||
    $this->assertSession()->pageTextContains('Testing requirements failed requirements.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests distribution profile support.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class SingleVisibleProfileTest extends InstallerTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The installation profile to install.
 | 
			
		||||
   *
 | 
			
		||||
   * Not needed when only one is visible.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function prepareEnvironment(): void {
 | 
			
		||||
    parent::prepareEnvironment();
 | 
			
		||||
    $profiles = ['standard', 'demo_umami'];
 | 
			
		||||
    foreach ($profiles as $profile) {
 | 
			
		||||
      $info = [
 | 
			
		||||
        'type' => 'profile',
 | 
			
		||||
        'core_version_requirement' => '^8 || ^9 || ^10',
 | 
			
		||||
        'name' => 'Override ' . $profile,
 | 
			
		||||
        'hidden' => TRUE,
 | 
			
		||||
      ];
 | 
			
		||||
      // File API functions are not available yet.
 | 
			
		||||
      $path = $this->siteDirectory . '/profiles/' . $profile;
 | 
			
		||||
      mkdir($path, 0777, TRUE);
 | 
			
		||||
      file_put_contents("$path/$profile.info.yml", Yaml::encode($info));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUpProfile(): void {
 | 
			
		||||
    // This step is skipped, because there is only one visible profile.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms that the installation succeeded.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInstalled(): void {
 | 
			
		||||
    $this->assertSession()->addressEquals('user/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    // Confirm that we are logged-in after installation.
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->rootUser->getAccountName());
 | 
			
		||||
    // Confirm that the minimal profile was installed.
 | 
			
		||||
    $this->assertEquals('minimal', \Drupal::installProfile());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,46 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\FunctionalTests\Installer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that the site name can be set during a non-interactive installation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Installer
 | 
			
		||||
 */
 | 
			
		||||
class SiteNameTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The site name to be used when testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $siteName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function installParameters() {
 | 
			
		||||
    $this->siteName = $this->randomMachineName();
 | 
			
		||||
    $parameters = parent::installParameters();
 | 
			
		||||
    $parameters['forms']['install_configure_form']['site_name'] = $this->siteName;
 | 
			
		||||
    return $parameters;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the desired site name appears on the page after installation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSiteName(): void {
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->pageTextContains($this->siteName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user