Commit c7bf7415 authored by Carlos Javier's avatar Carlos Javier
Browse files

Delete unused files and assets

parent d7b2c6e6
Pipeline #1005 failed with stages
in 0 seconds

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
use Symfony\Component\Finder\Finder;
/**
* Try to install dependencies per component, using Composer.
*
* @group #slow
* @group Composer
* @group Component
*
* @coversNothing
*/
class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
/**
* Provides an array with relative paths to the component paths.
*
* @return array
* An array with relative paths to the component paths.
*/
public function provideComponentPaths(): array {
$data = [];
// During the dataProvider phase, there is not a workspace directory yet.
// So we will find relative paths and assemble them with the workspace
// path later.
$drupal_root = $this->getDrupalRoot();
$composer_json_finder = $this->getComponentPathsFinder($drupal_root);
/** @var \Symfony\Component\Finder\SplFileInfo $path */
foreach ($composer_json_finder->getIterator() as $path) {
$data[$path->getRelativePath()] = ['/' . $path->getRelativePath()];
}
return $data;
}
/**
* Test whether components' composer.json can be installed in isolation.
*
* @dataProvider provideComponentPaths
*/
public function testComponentComposerJson(string $component_path): void {
// Only copy the components. Copy all of them because some of them depend on
// each other.
$finder = new Finder();
$finder->files()
->ignoreUnreadableDirs()
->in($this->getDrupalRoot() . static::$componentsPath)
->ignoreDotFiles(FALSE)
->ignoreVCS(FALSE);
$this->copyCodebase($finder->getIterator());
$working_dir = $this->getWorkingPath() . static::$componentsPath . $component_path;
// We add path repositories so we can wire internal dependencies together.
$this->addExpectedRepositories($working_dir);
// Perform the installation.
$this->executeCommand("composer install --working-dir=$working_dir --no-interaction --no-progress");
$this->assertCommandSuccessful();
}
/**
* Adds expected repositories as path repositories to package under test.
*
* @param string $working_dir
* The working directory.
*/
protected function addExpectedRepositories(string $working_dir): void {
foreach ($this->provideComponentPaths() as $path) {
$path = $path[0];
$package_name = 'drupal/core' . strtolower(preg_replace('/[A-Z]/', '-$0', substr($path, 1)));
$path_repo = $this->getWorkingPath() . static::$componentsPath . $path;
$repo_name = strtolower($path);
// Add path repositories with the current version number to the current
// package under test.
$drupal_version = Composer::drupalVersionBranch();
$this->executeCommand("composer config repositories.$repo_name " .
"'{\"type\": \"path\",\"url\": \"$path_repo\",\"options\": {\"versions\": {\"$package_name\": \"$drupal_version\"}}}' --working-dir=$working_dir");
}
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
/**
* Demonstrate that the Component generator responds to release tagging.
*
* @group #slow
* @group Composer
* @group Component
*
* @coversNothing
*/
class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
/**
* Highly arbitrary version and constraint expectations.
*
* @return array
* - First element is the tag that should be applied to \Drupal::version.
* - Second element is the resulting constraint which should be present in
* the component core dependencies.
*/
public function providerVersionConstraint(): array {
return [
// [Tag, constraint]
'1.0.x-dev' => ['1.0.x-dev', '1.0.x-dev'],
'1.0.0-beta1' => ['1.0.0-beta1', '1.0.0-beta1'],
'1.0.0-rc1' => ['1.0.0-rc1', '1.0.0-rc1'],
'1.0.0' => ['1.0.0', '^1.0'],
];
}
/**
* Validate release tagging and regeneration of dependencies.
*
* @dataProvider providerVersionConstraint
*/
public function testReleaseTagging(string $tag, string $constraint): void {
$this->copyCodebase();
$drupal_root = $this->getWorkspaceDirectory();
// Set the core version.
Composer::setDrupalVersion($drupal_root, $tag);
$this->assertDrupalVersion($tag, $drupal_root);
// Emulate the release script.
// @see https://github.com/xjm/drupal_core_release/blob/main/tag.sh
$this->executeCommand("COMPOSER_ROOT_VERSION=\"$tag\" composer update drupal/core*");
$this->assertCommandSuccessful();
$this->assertErrorOutputContains('generateComponentPackages');
// Find all the components.
$component_finder = $this->getComponentPathsFinder($drupal_root);
// Loop through all the component packages.
/** @var \Symfony\Component\Finder\SplFileInfo $composer_json */
foreach ($component_finder->getIterator() as $composer_json) {
$composer_json_data = json_decode(file_get_contents($composer_json->getPathname()), TRUE);
$requires = array_merge(
$composer_json_data['require'] ?? [],
$composer_json_data['require-dev'] ?? []
);
// Required packages from drupal/core-* should have our constraint.
foreach ($requires as $package => $req_constraint) {
if (str_contains($package, 'drupal/core-')) {
$this->assertEquals($constraint, $req_constraint);
}
}
}
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
use Symfony\Component\Finder\Finder;
/**
* Base class for Composer build tests.
*
* @coversNothing
*/
abstract class ComposerBuildTestBase extends BuildTestBase {
/**
* Relative path from Drupal root to the Components directory.
*
* @var string
*/
protected static $componentsPath = '/core/lib/Drupal/Component';
/**
* Assert that the VERSION constant in Drupal.php is the expected value.
*
* @param string $expectedVersion
* The expected version.
* @param string $dir
* The path to the site root.
*
* @internal
*/
protected function assertDrupalVersion(string $expectedVersion, string $dir): void {
$drupal_php_path = $dir . '/core/lib/Drupal.php';
$this->assertFileExists($drupal_php_path);
// Read back the Drupal version that was set and assert it matches
// expectations
$this->executeCommand("php -r 'include \"$drupal_php_path\"; print \Drupal::VERSION;'");
$this->assertCommandSuccessful();
$this->assertCommandOutputContains($expectedVersion);
}
/**
* Find all the composer.json files for components.
*
* @param string $drupal_root
* The Drupal root directory.
*
* @return \Symfony\Component\Finder\Finder
* A Finder object with all the composer.json files for components.
*/
protected function getComponentPathsFinder(string $drupal_root): Finder {
$finder = new Finder();
$finder->name('composer.json')
->in($drupal_root . static::$componentsPath)
->ignoreUnreadableDirs()
->depth(1);
return $finder;
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\ComposerIntegrationTrait;
/**
* @group Composer
*/
class ComposerValidateTest extends BuildTestBase {
use ComposerIntegrationTrait;
public function provideComposerJson() {
$data = [];
$composer_json_finder = $this->getComposerJsonFinder($this->getDrupalRoot());
foreach ($composer_json_finder->getIterator() as $composer_json) {
$data[] = [$composer_json->getPathname()];
}
return $data;
}
/**
* @dataProvider provideComposerJson
*/
public function testValidateComposer($path) {
$this->executeCommand('composer validate --strict --no-check-all ' . $path);
$this->assertCommandSuccessful();
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Composer\Template;
use Composer\Json\JsonFile;
use Composer\Semver\VersionParser;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
/**
* Demonstrate that Composer project templates can be built as patched.
*
* We have to use the packages.json fixture so that Composer will use the
* in-codebase version of the project template.
*
* We also have to add path repositories to the in-codebase project template or
* else Composer will try to use packagist to resolve dependencies we'd prefer
* it to find locally.
*
* This is because Composer only uses the packages.json file to resolve the
* project template and not any other dependencies.
*
* @group #slow
* @group Template
*/
class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
/**
* The minimum stability requirement for dependencies.
*
* @see https://getcomposer.org/doc/04-schema.md#minimum-stability
*/
protected const MINIMUM_STABILITY = 'stable';
/**
* The order of stability strings from least stable to most stable.
*
* This only includes normalized stability strings: i.e., ones that are
* returned by \Composer\Semver\VersionParser::parseStability().
*/
protected const STABILITY_ORDER = ['dev', 'alpha', 'beta', 'RC', 'stable'];
/**
* Get Composer items that we want to be path repos, from within a directory.
*
* @param string $workspace_directory
* The full path to the workspace directory.
* @param string $subdir
* The subdirectory to search under composer/.
*
* @return string[]
* Array of paths, indexed by package name.
*/
public function getPathReposForType($workspace_directory, $subdir) {
// Find the Composer items that we want to be path repos.
/** @var \SplFileInfo[] $path_repos */
$path_repos = Composer::composerSubprojectPaths($workspace_directory, $subdir);
$data = [];
foreach ($path_repos as $path_repo) {
$json_file = new JsonFile($path_repo->getPathname());
$json = $json_file->read();
$data[$json['name']] = $path_repo->getPath();
}
return $data;
}
public function provideTemplateCreateProject() {
return [
'recommended-project' => [
'drupal/recommended-project',
'composer/Template/RecommendedProject',
'/web',
],
'legacy-project' => [
'drupal/legacy-project',
'composer/Template/LegacyProject',
'',
],
];
}
/**
* Make sure that static::MINIMUM_STABILITY is sufficiently strict.
*/
public function testMinimumStabilityStrictness() {
// Ensure that static::MINIMUM_STABILITY is not less stable than the
// current core stability. For example, if we've already released a beta on
// the branch, ensure that we no longer allow alpha dependencies.
$this->assertGreaterThanOrEqual(array_search($this->getCoreStability(), static::STABILITY_ORDER), array_search(static::MINIMUM_STABILITY, static::STABILITY_ORDER));
// Ensure that static::MINIMUM_STABILITY is the same as the least stable
// dependency.
// - We can't set it stricter than our least stable dependency.
// - We don't want to set it looser than we need to, because we don't want
// to in the future accidentally commit a dependency that regresses our
// actual stability requirement without us explicitly changing this
// constant.
$root = $this->getDrupalRoot();
$process = $this->executeCommand("composer --working-dir=$root info --format=json");
$this->assertCommandSuccessful();
$installed = json_decode($process->getOutput(), TRUE);
// A lookup of the numerical position of each of the stability terms.
$stability_order_indexes = array_flip(static::STABILITY_ORDER);
$minimum_stability_order_index = $stability_order_indexes[static::MINIMUM_STABILITY];
$exclude = [
'drupal/core',
'drupal/core-project-message',
'drupal/core-vendor-hardening',
];
foreach ($installed['installed'] as $project) {
// Exclude dependencies that are required with "self.version", since
// those stabilities will automatically match the corresponding Drupal
// release.
if (in_array($project['name'], $exclude, TRUE)) {
continue;
}
$project_stability = VersionParser::parseStability($project['version']);
$project_stability_order_index = $stability_order_indexes[$project_stability];
$project_stabilities[$project['name']] = $project_stability;
$this->assertGreaterThanOrEqual($minimum_stability_order_index, $project_stability_order_index, sprintf(
"Dependency %s with stability %s does not meet minimum stability %s.",
$project['name'],
$project_stability,
static::MINIMUM_STABILITY,
));
}
// At least one project should be at the minimum stability.
$this->assertContains(static::MINIMUM_STABILITY, $project_stabilities);
}
/**
* Make sure we've accounted for all the templates.
*/
public function testVerifyTemplateTestProviderIsAccurate() {
$root = $this->getDrupalRoot();
$data = $this->provideTemplateCreateProject();
// Find all the templates.
$template_files = Composer::composerSubprojectPaths($root, 'Template');
$this->assertSameSize($template_files, $data);
// We could have the same number of templates but different names.
$template_data = [];
foreach ($data as $data_name => $data_value) {
$template_data[$data_value[0]] = $data_name;
}
/** @var \SplFileInfo $file */
foreach ($template_files as $file) {
$json_file = new JsonFile($file->getPathname());
$json = $json_file->read();
$this->assertArrayHasKey('name', $json);
// Assert that the template name is in the project created
// from the template.
$this->assertArrayHasKey($json['name'], $template_data);
}
}
/**
* @dataProvider provideTemplateCreateProject
*/
public function testTemplateCreateProject($project, $package_dir, $docroot_dir) {
// Make a working COMPOSER_HOME directory for setting global composer config
$composer_home = $this->getWorkspaceDirectory() . '/composer-home';
mkdir($composer_home);
// Create an empty global composer.json file, just to avoid warnings.
file_put_contents("$composer_home/composer.json", '{}');
// Disable packagist globally (but only in our own custom COMPOSER_HOME).
// It is necessary to do this globally rather than in our SUT composer.json
// in order to ensure that Packagist is disabled during the
// `composer create-project` command.
$this->executeCommand("COMPOSER_HOME=$composer_home composer config --no-interaction --global repo.packagist false");
$this->assertCommandSuccessful();
// Create a "Composer"-type repository containing one entry for every
// package in the vendor directory.
$vendor_packages_path = $this->getWorkspaceDirectory() . '/vendor_packages/packages.json';
$this->makeVendorPackage($vendor_packages_path);
// Make a copy of the code to alter in the workspace directory.
$this->copyCodebase();
// Tests are typically run on "-dev" versions, but we want to simulate
// running them on a tagged release at the same stability as specified in
// static::MINIMUM_STABILITY, in order to verify that everything will work
// if/when we make such a release.
$simulated_core_version = \Drupal::VERSION;
$simulated_core_version_suffix = (static::MINIMUM_STABILITY === 'stable' ? '' : '-' . static::MINIMUM_STABILITY . '99');
$simulated_core_version = str_replace('-dev', $simulated_core_version_suffix, $simulated_core_version);
Composer::setDrupalVersion($this->getWorkspaceDirectory(), $simulated_core_version);
$this->assertDrupalVersion($simulated_core_version, $this->getWorkspaceDirectory());
// Remove the packages.drupal.org entry (and any other custom repository)
// from the SUT's repositories section. There is no way to do this via
// `composer config --unset`, so we read and rewrite composer.json.
$composer_json_path = $this->getWorkspaceDirectory() . "/$package_dir/composer.json";
$composer_json = json_decode(file_get_contents($composer_json_path), TRUE);
unset($composer_json['repositories']);
$json = json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($composer_json_path, $json);
// Set up the template to use our path repos. Inclusion of metapackages is
// reported differently, so we load up a separate set for them.
$metapackage_path_repos = $this->getPathReposForType($this->getWorkspaceDirectory(), 'Metapackage');
$this->assertArrayHasKey('drupal/core-recommended', $metapackage_path_repos);
$path_repos = array_merge($metapackage_path_repos, $this->getPathReposForType($this->getWorkspaceDirectory(), 'Plugin'));
// Always add drupal/core as a path repo.
$path_repos['drupal/core'] = $this->getWorkspaceDirectory() . '/core';
foreach ($path_repos as $name => $path) {
$this->executeCommand("composer config --no-interaction repositories.$name path $path", $package_dir);
$this->assertCommandSuccessful();
}
// Change drupal/core-recommended to require the simulated version of
// drupal/core.
$core_recommended_dir = 'composer/Metapackage/CoreRecommended';
$this->executeCommand("composer remove --no-interaction drupal/core --no-update", $core_recommended_dir);
$this->assertCommandSuccessful();
$this->executeCommand("composer require --no-interaction drupal/core:^$simulated_core_version --no-update", $core_recommended_dir);
$this->assertCommandSuccessful();
// Add our vendor package repository to our SUT's repositories section.
// Call it "local" (although the name does not matter).
$this->executeCommand("composer config --no-interaction repositories.local composer file://" . $vendor_packages_path, $package_dir);
$this->assertCommandSuccessful();
$repository_path = $this->getWorkspaceDirectory() . '/test_repository/packages.json';
$this->makeTestPackage($repository_path, $simulated_core_version);
$installed_composer_json = $this->getWorkspaceDirectory() . '/test_project/composer.json';
$autoloader = $this->getWorkspaceDirectory() . '/test_project' . $docroot_dir . '/autoload.php';
$this->assertFileDoesNotExist($autoloader);
$this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$simulated_core_version composer create-project --no-ansi $project test_project $simulated_core_version -vvv --repository $repository_path");
$this->assertCommandSuccessful();
// Check the output of the project creation for the absence of warnings
// about any non-allowed composer plugins.
// Note: There are different warnings for disallowed composer plugins
// depending on running in non-interactive mode or not. It seems the Drupal
// CI environment always forces composer commands to run in the
// non-interactive mode. The only thing these messages have in common is the
// following string.
$this->assertErrorOutputNotContains('See https://getcomposer.org/allow-plugins');
// Ensure we used the project from our codebase.
$this->assertErrorOutputContains("Installing $project ($simulated_core_version): Symlinking from $package_dir");
// Ensure that we used drupal/core from our codebase. This probably means
// that drupal/core-recommended was added successfully by the project.
$this->assertErrorOutputContains("Installing drupal/core ($simulated_core_version): Symlinking from");
// Verify that there is an autoloader. This is written by the scaffold
// plugin, so its existence assures us that scaffolding happened.
$this->assertFileExists($autoloader);
// Verify that the minimum stability in the installed composer.json file
// matches the stability of the simulated core version.
$this->assertFileExists($installed_composer_json);
$composer_json_contents = file_get_contents($installed_composer_json);
$this->assertStringContainsString('"minimum-stability": "' . static::MINIMUM_STABILITY . '"', $composer_json_contents);
// In order to verify that Composer used the path repos for our project, we
// have to get the requirements from the project composer.json so we can
// reconcile our expectations.
$template_json_file = $this->getWorkspaceDirectory() . '/' . $package_dir . '/composer.json';
$this->assertFileExists($template_json_file);
$json_file = new JsonFile($template_json_file);
$template_json = $json_file->read();
// Get the require and require-dev information, and ensure that our
// requirements are not erroneously empty.
$this->assertNotEmpty(
$require = array_merge($template_json['require'] ?? [], $template_json['require-dev'] ?? [])
);
// Verify that path repo packages were installed.
$path_repos = array_keys($path_repos);
foreach (array_keys($require) as $package_name) {
if (in_array($package_name, $path_repos)) {
// Metapackages do not report that they were installed as symlinks, but
// we still must check that their installed version matches
// COMPOSER_CORE_VERSION.
if (array_key_exists($package_name, $metapackage_path_repos)) {
$this->assertErrorOutputContains("Installing $package_name ($simulated_core_version)");
}
else {
$this->assertErrorOutputContains("Installing $package_name ($simulated_core_version): Symlinking from");
}
}
}
}
/**
* Creates a test package that points to the templates.
*
* @param string $repository_path
* The path where to create the test package.
* @param string $version
* The version under test.
*/
protected function makeTestPackage($repository_path, $version) {
$json = <<<JSON
{
"packages": {
"drupal/recommended-project": {
"$version": {
"name": "drupal/recommended-project",
"dist": {
"type": "path",
"url": "composer/Template/RecommendedProject"
},
"type": "project",
"version": "$version"
}
},
"drupal/legacy-project": {
"$version": {
"name": "drupal/legacy-project",
"dist": {
"type": "path",
"url": "composer/Template/LegacyProject"
},
"type": "project",
"version": "$version"
}
}
}
}
JSON;
mkdir(dirname($repository_path));
file_put_contents($repository_path, $json);
}
/**
* Creates a test package that points to all the projects in vendor.
*
* @param string $repository_path
* The path where to create the test package.
*/
protected function makeVendorPackage($repository_path) {
$root = $this->getDrupalRoot();
$process = $this->executeCommand("composer --working-dir=$root info --format=json");
$this->assertCommandSuccessful();
$installed = json_decode($process->getOutput(), TRUE);
// Build out package definitions for everything installed in
// the vendor directory.
$packages = [];
foreach ($installed['installed'] as $project) {
$name = $project['name'];
$version = $project['version'];
$path = "vendor/$name";
$full_path = "$root/$path";
// We are building a set of path repositories to projects in the vendor
// directory, so we will skip any project that does not exist in vendor.
// Also skip the projects that are symlinked in vendor. These are in our
// metapackage. They will be represented as path repositories in the test
// project's composer.json.
if (is_dir($full_path) && !is_link($full_path)) {
$packages['packages'][$name] = [
$version => [
"name" => $name,
"dist" => [
"type" => "path",
"url" => $full_path,
],
"version" => $version,
],
];
// Ensure composer plugins are registered correctly.
$package_json = json_decode(file_get_contents($full_path . '/composer.json'), TRUE);
if (isset($package_json['type']) && $package_json['type'] === 'composer-plugin') {
$packages['packages'][$name][$version]['type'] = $package_json['type'];
$packages['packages'][$name][$version]['require'] = $package_json['require'];
$packages['packages'][$name][$version]['extra'] = $package_json['extra'];
if (isset($package_json['autoload'])) {
$packages['packages'][$name][$version]['autoload'] = $package_json['autoload'];
}
}
}
}
$json = json_encode($packages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
mkdir(dirname($repository_path));
file_put_contents($repository_path, $json);
}
/**
* Returns the stability of the current core version.
*
* If the current core version is a tagged release (not a "-dev" version),
* this returns the stability of that version.
*
* If the current core version is a "-dev" version, but not a "x.y.0-dev"
* version, this returns "stable", because it means that the corresponding
* "x.y.0" has already been released, and only stable changes are now
* permitted on the branch.
*
* If the current core version is a "x.y.0-dev" version, then this returns
* the stability of the latest tag that matches "x.y.0-*". For example, if
* we've already released "x.y.0-alpha1" but have not yet released
* "x.y.0-beta1", then the current stability is "alpha". If there aren't any
* matching tags, this returns "dev", because it means that an "alpha1" has
* not yet been released.
*
* @return string
* One of: "dev", "alpha", "beta", "RC", "stable".
*/
protected function getCoreStability() {
$version = \Drupal::VERSION;
$stability = VersionParser::parseStability($version);
if ($stability === 'dev') {
// Strip off "-dev";
$version_towards = substr($version, 0, -4);
if (substr($version_towards, -2) !== '.0') {
// If the current version is developing towards an x.y.z release where
// z is not 0, it means that the x.y.0 has already been released, and
// only stable changes are permitted on the branch.
$stability = 'stable';
}
else {
// If the current version is developing towards an x.y.0 release, there
// might be tagged pre-releases. "git describe" identifies the latest
// one.
$root = $this->getDrupalRoot();
$process = $this->executeCommand("git -C \"$root\" describe --abbrev=0 --match=\"$version_towards-*\"");
// If there aren't any tagged pre-releases for this version yet, return
// 'dev'. Ensure that any other error from "git describe" causes a test
// failure.
if (!$process->isSuccessful()) {
$this->assertErrorOutputContains('No names found, cannot describe anything.');
return 'dev';
}
// We expect a pre-release, because:
// - A tag should not be of "dev" stability.
// - After a "stable" release is made, \Drupal::VERSION is incremented,
// so there should not be a stable release on that new version.
$stability = VersionParser::parseStability(trim($process->getOutput()));
$this->assertContains($stability, ['alpha', 'beta', 'RC']);
}
}
return $stability;
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Framework;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Util\Test;
use Symfony\Component\Process\ExecutableFinder;
/**
* Allows test classes to require external command line applications.
*
* Use annotation such as '(at)requires externalCommand git'.
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* Drupal\\TestTools\\Extension\\RequiresComposerTrait instead.
*
* @see https://www.drupal.org/node/3362239
*/
trait ExternalCommandRequirementsTrait {
/**
* A list of existing external commands we've already discovered.
*
* @var string[]
*/
private static $existingCommands = [];
/**
* Checks whether required external commands are available per test class.
*
* @throws \PHPUnit\Framework\SkippedTestError
* Thrown when the requirements are not met, and this test should be
* skipped. Callers should not catch this exception.
*/
private static function checkClassCommandRequirements() {
@trigger_error(__METHOD__ . "() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239", E_USER_DEPRECATED);
$annotations = Test::parseTestMethodAnnotations(static::class);
if (!empty($annotations['class']['requires'])) {
static::checkExternalCommandRequirements($annotations['class']['requires']);
}
}
/**
* Checks whether required external commands are available per method.
*
* @throws \PHPUnit\Framework\SkippedTestError
* Thrown when the requirements are not met, and this test should be
* skipped. Callers should not catch this exception.
*/
private static function checkMethodCommandRequirements($name) {
@trigger_error(__METHOD__ . "() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239", E_USER_DEPRECATED);
$annotations = Test::parseTestMethodAnnotations(static::class, $name);
if (!empty($annotations['method']['requires'])) {
static::checkExternalCommandRequirements($annotations['method']['requires']);
}
}
/**
* Checks missing external command requirements.
*
* @param string[] $annotations
* A list of requires annotations from either a method or class annotation.
*
* @throws \PHPUnit\Framework\SkippedTestError
* Thrown when the requirements are not met, and this test should be
* skipped. Callers should not catch this exception.
*/
private static function checkExternalCommandRequirements(array $annotations) {
@trigger_error(__METHOD__ . "() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239", E_USER_DEPRECATED);
// Make a list of required commands.
$required_commands = [];
foreach ($annotations as $requirement) {
if (str_starts_with($requirement, 'externalCommand ')) {
@trigger_error("The '@require externalCommand' annotation for tests is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239", E_USER_DEPRECATED);
$command = trim(str_replace('externalCommand ', '', $requirement));
// Use named keys to avoid duplicates.
$required_commands[$command] = $command;
}
}
// Figure out which commands are not available.
$unavailable = [];
foreach ($required_commands as $required_command) {
if (!in_array($required_command, self::$existingCommands)) {
if (static::externalCommandIsAvailable($required_command)) {
// Cache existing commands so we don't have to ask again.
self::$existingCommands[] = $required_command;
}
else {
$unavailable[] = $required_command;
}
}
}
// Skip the test if there were some we couldn't find.
if (!empty($unavailable)) {
throw new SkippedTestError('Required external commands: ' . implode(', ', $unavailable));
}
}
/**
* Determine if an external command is available.
*
* @param $command
* The external command.
*
* @return bool
* TRUE if external command is available, else FALSE.
*/
private static function externalCommandIsAvailable($command) {
$finder = new ExecutableFinder();
return (bool) $finder->find($command);
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\BuildTestBase;
use org\bovigo\vfs\vfsStream;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
* @group Build
*/
class BuildTestTest extends BuildTestBase {
/**
* Ensure that workspaces work.
*/
public function testWorkspace() {
$test_directory = 'test_directory';
// Execute an empty command through the shell to build out a working
// directory.
$process = $this->executeCommand('', $test_directory);
$this->assertCommandSuccessful();
// Assert that our working directory exists and is in use by the process.
$workspace = $this->getWorkspaceDirectory();
$working_path = $workspace . '/' . $test_directory;
$this->assertDirectoryExists($working_path);
$this->assertEquals($working_path, $process->getWorkingDirectory());
}
/**
* @covers ::copyCodebase
*/
public function testCopyCodebase() {
$test_directory = 'copied_codebase';
$this->copyCodebase(NULL, $test_directory);
$full_path = $this->getWorkspaceDirectory() . '/' . $test_directory;
$files = [
'autoload.php',
'composer.json',
'index.php',
'README.md',
'.git',
'.ht.router.php',
];
foreach ($files as $file) {
$this->assertFileExists($full_path . '/' . $file);
}
}
/**
* Ensure we're not copying directories we wish to exclude.
*
* @covers ::copyCodebase
*/
public function testCopyCodebaseExclude() {
// Create a virtual file system containing items that should be
// excluded. Exception being modules directory.
vfsStream::setup('drupal', NULL, [
'sites' => [
'default' => [
'files' => [
'a_file.txt' => 'some file.',
],
'settings.php' => '<?php $settings = stuff;',
'settings.local.php' => '<?php $settings = override;',
],
'simpletest' => [
'simpletest_hash' => [
'some_results.xml' => '<xml/>',
],
],
],
'modules' => [
'my_module' => [
'vendor' => [
'my_vendor' => [
'composer.json' => "{\n}",
],
],
],
],
]);
// Mock BuildTestBase so that it thinks our VFS is the Composer and Drupal
// roots.
/** @var \PHPUnit\Framework\MockObject\MockBuilder|\Drupal\BuildTests\Framework\BuildTestBase $base */
$base = $this->getMockBuilder(BuildTestBase::class)
->onlyMethods(['getDrupalRoot', 'getComposerRoot'])
->getMockForAbstractClass();
$base->expects($this->exactly(1))
->method('getDrupalRoot')
->willReturn(vfsStream::url('drupal'));
$base->expects($this->exactly(3))
->method('getComposerRoot')
->willReturn(vfsStream::url('drupal'));
$base->setUp();
// Perform the copy.
$test_directory = 'copied_codebase';
$base->copyCodebase(NULL, $test_directory);
$full_path = $base->getWorkspaceDirectory() . '/' . $test_directory;
$this->assertDirectoryExists($full_path);
// Verify nested vendor directory was not excluded. Then remove it for next
// validation.
$this->assertFileExists($full_path . DIRECTORY_SEPARATOR . 'modules/my_module/vendor/my_vendor/composer.json');
$file_system = new Filesystem();
$file_system->remove($full_path . DIRECTORY_SEPARATOR . 'modules');
// Use scandir() to determine if our target directory is empty. It should
// only contain the system dot directories.
$this->assertTrue(
($files = @scandir($full_path)) && count($files) <= 2,
'Directory is not empty: ' . implode(', ', $files)
);
$base->tearDown();
}
/**
* Tests copying codebase when Drupal and Composer roots are different.
*
* @covers ::copyCodebase
*/
public function testCopyCodebaseDocRoot() {
// Create a virtual file system containing items that should be
// excluded. Exception being modules directory.
vfsStream::setup('drupal', NULL, [
'docroot' => [
'sites' => [
'default' => [
'files' => [
'a_file.txt' => 'some file.',
],
'settings.php' => '<?php $settings = "stuff";',
'settings.local.php' => '<?php $settings = "override";',
'default.settings.php' => '<?php $settings = "default";',
],
'simpletest' => [
'simpletest_hash' => [
'some_results.xml' => '<xml/>',
],
],
],
'modules' => [
'my_module' => [
'vendor' => [
'my_vendor' => [
'composer.json' => "{\n}",
],
],
],
],
],
'vendor' => [
'test.txt' => 'File exists',
],
]);
// Mock BuildTestBase so that it thinks our VFS is the Composer and Drupal
// roots.
/** @var \PHPUnit\Framework\MockObject\MockBuilder|\Drupal\BuildTests\Framework\BuildTestBase $base */
$base = $this->getMockBuilder(BuildTestBase::class)
->onlyMethods(['getDrupalRoot', 'getComposerRoot'])
->getMockForAbstractClass();
$base->expects($this->exactly(3))
->method('getDrupalRoot')
->willReturn(vfsStream::url('drupal/docroot'));
$base->expects($this->exactly(5))
->method('getComposerRoot')
->willReturn(vfsStream::url('drupal'));
$base->setUp();
// Perform the copy.
$base->copyCodebase();
$full_path = $base->getWorkspaceDirectory();
$this->assertDirectoryExists($full_path . '/docroot');
// Verify expected files exist.
$this->assertFileExists($full_path . DIRECTORY_SEPARATOR . 'docroot/modules/my_module/vendor/my_vendor/composer.json');
$this->assertFileExists($full_path . DIRECTORY_SEPARATOR . 'docroot/sites/default/default.settings.php');
$this->assertFileExists($full_path . DIRECTORY_SEPARATOR . 'vendor');
// Verify expected files do not exist
$this->assertFileDoesNotExist($full_path . DIRECTORY_SEPARATOR . 'docroot/sites/default/settings.php');
$this->assertFileDoesNotExist($full_path . DIRECTORY_SEPARATOR . 'docroot/sites/default/settings.local.php');
$this->assertFileDoesNotExist($full_path . DIRECTORY_SEPARATOR . 'docroot/sites/default/files');
// Ensure that the workspace Drupal root is calculated correctly.
$this->assertSame($full_path . '/docroot/', $base->getWorkspaceDrupalRoot());
$this->assertSame('docroot/', $base->getWorkingPathDrupalRoot());
$base->tearDown();
}
/**
* @covers ::findAvailablePort
*/
public function testPortMany() {
$iterator = (new Finder())->in($this->getDrupalRoot())
->ignoreDotFiles(FALSE)
->exclude(['sites/simpletest'])
->path('/^.ht.router.php$/')
->getIterator();
$this->copyCodebase($iterator);
/** @var \Symfony\Component\Process\Process[] $processes */
$processes = [];
$count = 15;
for ($i = 0; $i <= $count; $i++) {
$port = $this->findAvailablePort();
$this->assertArrayNotHasKey($port, $processes, 'Port ' . $port . ' was already in use by a process.');
$processes[$port] = $this->instantiateServer($port);
$this->assertNotEmpty($processes[$port]);
$this->assertTrue($processes[$port]->isRunning(), 'Process on port ' . $port . ' is not still running.');
$this->assertFalse($this->checkPortIsAvailable($port));
}
// Clean up after ourselves.
foreach ($processes as $process) {
$process->stop();
}
}
/**
* @covers ::standUpServer
*/
public function testStandUpServer() {
// Stand up a server with working directory 'first'.
$this->standUpServer('first');
// Get the process object for the server.
$ref_process = new \ReflectionProperty(parent::class, 'serverProcess');
$first_process = $ref_process->getValue($this);
// Standing up the server again should not change the server process.
$this->standUpServer('first');
$this->assertSame($first_process, $ref_process->getValue($this));
// Standing up the server with working directory 'second' should give us a
// new server process.
$this->standUpServer('second');
$this->assertNotSame(
$first_process,
$second_process = $ref_process->getValue($this)
);
// And even with the original working directory name, we should get a new
// server process.
$this->standUpServer('first');
$this->assertNotSame($first_process, $ref_process->getValue($this));
$this->assertNotSame($second_process, $ref_process->getValue($this));
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait
* @group Build
* @group legacy
*/
class ExternalCommandRequirementTest extends TestCase {
use ExpectDeprecationTrait;
/**
* @covers ::checkExternalCommandRequirements
*/
public function testCheckExternalCommandRequirementsNotAvailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkExternalCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$this->expectDeprecation('The \'@require externalCommand\' annotation for tests is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new UsesCommandRequirements();
$ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$ref_check_requirements->invokeArgs($requires, [
['externalCommand not_available', 'externalCommand available_command'],
]);
$this->fail('Unavailable external command requirement should throw a skipped test error exception.');
}
catch (SkippedTestError $exception) {
$this->assertEquals('Required external commands: not_available', $exception->getMessage());
}
}
/**
* @covers ::checkExternalCommandRequirements
*/
public function testCheckExternalCommandRequirementsAvailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkExternalCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$this->expectDeprecation('The \'@require externalCommand\' annotation for tests is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new UsesCommandRequirements();
$ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull(
$ref_check_requirements->invokeArgs($requires, [['externalCommand available_command']])
);
}
catch (SkippedTestError $exception) {
$this->fail(sprintf('The external command should be available: %s', $exception->getMessage()));
}
}
/**
* @covers ::checkClassCommandRequirements
*/
public function testClassRequiresAvailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkClassCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new ClassRequiresAvailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires));
}
catch (SkippedTestError $exception) {
$this->fail(sprintf('The external command should be available: %s', $exception->getMessage()));
}
}
/**
* @covers ::checkClassCommandRequirements
*/
public function testClassRequiresUnavailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkClassCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new ClassRequiresUnavailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires));
$this->fail('Unavailable external command requirement should throw a skipped test error exception.');
}
catch (SkippedTestError $exception) {
$this->assertEquals('Required external commands: unavailable_command', $exception->getMessage());
}
}
/**
* @covers ::checkMethodCommandRequirements
*/
public function testMethodRequiresAvailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkMethodCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new MethodRequires();
$ref_check = new \ReflectionMethod($requires, 'checkMethodCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires, 'testRequiresAvailable'));
}
catch (SkippedTestError $exception) {
$this->fail(sprintf('The external command should be available: %s', $exception->getMessage()));
}
}
/**
* @covers ::checkMethodCommandRequirements
*/
public function testMethodRequiresUnavailable() {
$this->expectDeprecation('Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait::checkMethodCommandRequirements() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\\TestTools\\Extension\\RequiresComposerTrait instead. See https://www.drupal.org/node/3362239');
$requires = new MethodRequires();
$ref_check = new \ReflectionMethod($requires, 'checkMethodCommandRequirements');
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires, 'testRequiresUnavailable'));
$this->fail('Unavailable external command requirement should throw a skipped test error exception.');
}
catch (SkippedTestError $exception) {
$this->assertEquals('Required external commands: unavailable_command', $exception->getMessage());
}
}
}
class UsesCommandRequirements {
use ExternalCommandRequirementsTrait;
protected static function externalCommandIsAvailable($command) {
return in_array($command, ['available_command']);
}
}
/**
* @requires externalCommand available_command
*/
class ClassRequiresAvailable {
use ExternalCommandRequirementsTrait;
protected static function externalCommandIsAvailable($command) {
return in_array($command, ['available_command']);
}
}
/**
* @requires externalCommand unavailable_command
*/
class ClassRequiresUnavailable {
use ExternalCommandRequirementsTrait;
}
class MethodRequires {
use ExternalCommandRequirementsTrait;
/**
* @requires externalCommand available_command
*/
public function testRequiresAvailable() {
}
/**
* @requires externalCommand unavailable_command
*/
public function testRequiresUnavailable() {
}
protected static function externalCommandIsAvailable($command) {
return in_array($command, ['available_command']);
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
* @group Build
* @requires extension pdo_sqlite
*/
class HtRouterTest extends QuickStartTestBase {
/**
* @covers ::instantiateServer
*/
public function testHtRouter() {
$sqlite = (new \PDO('sqlite::memory:'))->query('select sqlite_version()')->fetch()[0];
if (version_compare($sqlite, Tasks::SQLITE_MINIMUM_VERSION) < 0) {
$this->markTestSkipped();
}
$this->copyCodebase();
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
$this->installQuickStart('minimal');
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->visit('/.well-known/change-password');
$this->assertDrupalVisit();
$url = $this->getMink()->getSession()->getCurrentUrl();
$this->assertEquals('http://localhost:' . $this->getPortNumber() . '/user/1/edit', $url);
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\QuickStart;
use Drupal\BuildTests\Framework\BuildTestBase;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* Helper methods for using the quickstart feature of Drupal.
*/
abstract class QuickStartTestBase extends BuildTestBase {
/**
* User name of the admin account generated during install.
*
* @var string
*/
protected $adminUsername;
/**
* Password of the admin account generated during install.
*
* @var string
*/
protected $adminPassword;
/**
* Install a Drupal site using the quick start feature.
*
* @param string $profile
* Drupal profile to install.
* @param string $working_dir
* (optional) A working directory relative to the workspace, within which to
* execute the command. Defaults to the workspace directory.
*/
public function installQuickStart($profile, $working_dir = NULL) {
$php_finder = new PhpExecutableFinder();
$install_process = $this->executeCommand($php_finder->find() . ' ./core/scripts/drupal install ' . $profile, $working_dir);
$this->assertCommandOutputContains('Username:');
preg_match('/Username: (.+)\vPassword: (.+)/', $install_process->getOutput(), $matches);
$this->assertNotEmpty($this->adminUsername = $matches[1]);
$this->assertNotEmpty($this->adminPassword = $matches[2]);
}
/**
* Helper that uses Drupal's user/login form to log in.
*
* @param string $username
* Username.
* @param string $password
* Password.
* @param string $working_dir
* (optional) A working directory within which to login. Defaults to the
* workspace directory.
*/
public function formLogin($username, $password, $working_dir = NULL) {
$this->visit('/user/login', $working_dir);
$assert = $this->getMink()->assertSession();
$assert->statusCodeEquals(200);
$assert->fieldExists('edit-name')->setValue($username);
$assert->fieldExists('edit-pass')->setValue($password);
$session = $this->getMink()->getSession();
$session->getPage()->findButton('Log in')->submit();
}
}
<?php
declare(strict_types=1);
namespace Drupal\BuildTests\TestSiteApplication;
use Drupal\BuildTests\Framework\BuildTestBase;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* @group Build
* @group TestSiteApplication
*/
class InstallTest extends BuildTestBase {
public function testInstall() {
$this->copyCodebase();
$fs = new Filesystem();
$fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
// Composer tells you stuff in error output.
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
// We have to stand up the server first so we can know the port number to
// pass along to the install command.
$this->standUpServer();
$php_finder = new PhpExecutableFinder();
$install_command = [
$php_finder->find(),
'./core/scripts/test-site.php',
'install',
'--base-url=http://localhost:' . $this->getPortNumber(),
'--db-url=sqlite://localhost/foo.sqlite',
'--install-profile=minimal',
'--json',
];
$this->assertNotEmpty($output_json = $this->executeCommand(implode(' ', $install_command))->getOutput());
$this->assertCommandSuccessful();
$connection_details = json_decode($output_json, TRUE);
foreach (['db_prefix', 'user_agent', 'site_path'] as $key) {
$this->assertArrayHasKey($key, $connection_details);
}
// Visit paths with expectations.
$this->visit();
$this->assertDrupalVisit();
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests Ajax callbacks on FAPI elements.
*
* @group Ajax
*/
class AjaxCallbacksTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if Ajax callback works on date element.
*/
public function testDateAjaxCallback() {
// Test Ajax callback when date changes.
$this->drupalGet('ajax_forms_test_ajax_element_form');
$this->assertNotEmpty($this->getSession()->getPage()->find('xpath', '//div[@id="ajax_date_value"][text()="No date yet selected"]'));
$this->getSession()->executeScript('jQuery("[data-drupal-selector=edit-date]").val("2016-01-01").trigger("change");');
$this->assertNotEmpty($this->assertSession()->waitForElement('xpath', '//div[@id="ajax_date_value"]/div[text()="2016-01-01"]'));
}
/**
* Tests if Ajax callback works on datetime element.
*/
public function testDateTimeAjaxCallback() {
// Test Ajax callback when datetime changes.
$this->drupalGet('ajax_forms_test_ajax_element_form');
$this->assertNotEmpty($this->getSession()->getPage()->find('xpath', '//div[@id="ajax_datetime_value"][text()="No datetime selected."]'));
$this->getSession()->executeScript('jQuery("[data-drupal-selector=edit-datetime-date]").val("2016-01-01");');
$this->getSession()->executeScript('jQuery("[data-drupal-selector=edit-datetime-time]").val("12:00:00").trigger("change");');
$this->assertNotEmpty($this->assertSession()->waitForElement('xpath', '//div[@id="ajax_datetime_value"]/div[text()="2016-01-01 12:00:00"]'));
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the usage of form caching for AJAX forms.
*
* @group Ajax
*/
class AjaxFormCacheTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the usage of form cache for AJAX forms.
*/
public function testFormCacheUsage() {
/** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
$key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
$this->drupalLogin($this->rootUser);
// Ensure that the cache is empty.
$this->assertCount(0, $key_value_expirable->getAll());
// Visit an AJAX form that is not cached, 3 times.
$uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form');
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
// The number of cache entries should not have changed.
$this->assertCount(0, $key_value_expirable->getAll());
}
/**
* Tests AJAX forms in blocks.
*/
public function testBlockForms() {
$this->container->get('module_installer')->install(['block', 'search']);
$this->rebuildContainer();
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('search_form_block', ['weight' => -5]);
$this->drupalPlaceBlock('ajax_forms_test_block');
$this->drupalGet('');
$session = $this->getSession();
// Select first option and trigger ajax update.
$session->getPage()->selectFieldOption('edit-test1', 'option1');
// DOM update: The InsertCommand in the AJAX response changes the text
// in the option element to 'Option1!!!'.
$opt1_selector = $this->assertSession()->waitForElement('css', "select[data-drupal-selector='edit-test1'] option:contains('Option 1!!!')");
$this->assertNotEmpty($opt1_selector);
$this->assertTrue($opt1_selector->isSelected());
// Confirm option 3 exists.
$page = $session->getPage();
$opt3_selector = $page->find('xpath', '//select[@data-drupal-selector="edit-test1"]//option[@value="option3"]');
$this->assertNotEmpty($opt3_selector);
// Confirm success message appears after a submit.
$page->findButton('edit-submit')->click();
$this->assertSession()->waitForButton('edit-submit');
$updated_page = $session->getPage();
$updated_page->hasContent('Submission successful.');
}
/**
* Tests AJAX forms on pages with a query string.
*/
public function testQueryString() {
$this->container->get('module_installer')->install(['block']);
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('ajax_forms_test_block');
$url = Url::fromRoute('entity.user.canonical', ['user' => $this->rootUser->id()], ['query' => ['foo' => 'bar']]);
$this->drupalGet($url);
$session = $this->getSession();
// Select first option and trigger ajax update.
$session->getPage()->selectFieldOption('edit-test1', 'option1');
// DOM update: The InsertCommand in the AJAX response changes the text
// in the option element to 'Option1!!!'.
$opt1_selector = $this->assertSession()->waitForElement('css', "option:contains('Option 1!!!')");
$this->assertNotEmpty($opt1_selector);
$url->setOption('query', [
'foo' => 'bar',
]);
$this->assertSession()->addressEquals($url);
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the Ajax image buttons work with key press events.
*
* @group Ajax
*/
class AjaxFormImageButtonTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests image buttons can be operated with the keyboard ENTER key.
*/
public function testAjaxImageButtonKeypressEnter() {
// Get a Field UI manage-display page.
$this->drupalGet('ajax_forms_image_button_form');
$assertSession = $this->assertSession();
$session = $this->getSession();
$button = $session->getPage()->findButton('Edit');
$button->keyPress(13);
$this->assertNotEmpty($assertSession->waitForElementVisible('css', '#ajax-1-more-div'), 'Page updated after image button pressed');
}
/**
* Tests image buttons can be operated with the keyboard SPACE key.
*/
public function testAjaxImageButtonKeypressSpace() {
// Get a Field UI manage-display page.
$this->drupalGet('ajax_forms_image_button_form');
$assertSession = $this->assertSession();
$session = $this->getSession();
$button = $session->getPage()->findButton('Edit');
$button->keyPress(32);
$this->assertNotEmpty($assertSession->waitForElementVisible('css', '#ajax-1-more-div'), 'Page updated after image button pressed');
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on AJAX forms in cached pages.
*
* @group Ajax
*/
class AjaxFormPageCacheTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Return the build id of the current form.
*/
protected function getFormBuildId() {
// Ensure the hidden 'form_build_id' field is unique.
$this->assertSession()->elementsCount('xpath', '//input[@name="form_build_id"]', 1);
return $this->assertSession()->hiddenFieldExists('form_build_id')->getValue();
}
/**
* Create a simple form, then submit the form via AJAX to change to it.
*/
public function testSimpleAJAXFormValue() {
$this->drupalGet('ajax_forms_test_get_form');
$build_id_initial = $this->getFormBuildId();
// Changing the value of a select input element, triggers an AJAX
// request/response. The callback on the form responds with three AJAX
// commands:
// - UpdateBuildIdCommand
// - HtmlCommand
// - DataCommand
$session = $this->getSession();
$session->getPage()->selectFieldOption('select', 'green');
// Wait for the DOM to update. The HtmlCommand will update
// #ajax_selected_color to reflect the color change.
$green_span = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('green')");
$this->assertNotNull($green_span, 'DOM update: The selected color SPAN is green.');
// Confirm the operation of the UpdateBuildIdCommand.
$build_id_first_ajax = $this->getFormBuildId();
$this->assertNotEquals($build_id_initial, $build_id_first_ajax, 'Build id is changed in the form_build_id element on first AJAX submission');
// Changing the value of a select input element, triggers an AJAX
// request/response.
$session->getPage()->selectFieldOption('select', 'red');
// Wait for the DOM to update.
$red_span = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('red')");
$this->assertNotNull($red_span, 'DOM update: The selected color SPAN is red.');
// Confirm the operation of the UpdateBuildIdCommand.
$build_id_second_ajax = $this->getFormBuildId();
$this->assertNotEquals($build_id_first_ajax, $build_id_second_ajax, 'Build id changes on subsequent AJAX submissions');
// Emulate a push of the reload button and then repeat the test sequence
// this time with a page loaded from the cache.
$session->reload();
$build_id_from_cache_initial = $this->getFormBuildId();
$this->assertEquals($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request');
// Changing the value of a select input element, triggers an AJAX
// request/response.
$session->getPage()->selectFieldOption('select', 'green');
// Wait for the DOM to update.
$green_span2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('green')");
$this->assertNotNull($green_span2, 'DOM update: After reload - the selected color SPAN is green.');
$build_id_from_cache_first_ajax = $this->getFormBuildId();
$this->assertNotEquals($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the DOM on first AJAX submission');
$this->assertNotEquals($build_id_first_ajax, $build_id_from_cache_first_ajax, 'Build id from first user is not reused');
// Changing the value of a select input element, triggers an AJAX
// request/response.
$session->getPage()->selectFieldOption('select', 'red');
// Wait for the DOM to update.
$red_span2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('red')");
$this->assertNotNull($red_span2, 'DOM update: After reload - the selected color SPAN is red.');
$build_id_from_cache_second_ajax = $this->getFormBuildId();
$this->assertNotEquals($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions');
}
/**
* Tests that updating the text field trigger an AJAX request/response.
*
* @see \Drupal\system\Tests\Ajax\ElementValidationTest::testAjaxElementValidation()
*/
public function testAjaxElementValidation() {
$this->drupalGet('ajax_validation_test');
// Changing the value of the textfield will trigger an AJAX
// request/response.
$field = $this->getSession()->getPage()->findField('driver_text');
$field->setValue('some dumb text');
$field->blur();
// When the AJAX command updates the DOM a <ul> unsorted list
// "message__list" structure will appear on the page echoing back the
// "some dumb text" message.
$placeholder = $this->assertSession()->waitForElement('css', "[aria-label='Status message'] > ul > li > em:contains('some dumb text')");
$this->assertNotNull($placeholder, 'Message structure containing input data located.');
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests that form elements in groups work correctly with AJAX.
*
* @group Ajax
*/
class AjaxInGroupTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(['access content']));
}
/**
* Submits forms with select and checkbox elements via Ajax.
*/
public function testSimpleAjaxFormValue() {
$this->drupalGet('/ajax_forms_test_get_form');
$assert_session = $this->assertSession();
$assert_session->responseContains('Test group');
$assert_session->responseContains('AJAX checkbox in a group');
$session = $this->getSession();
$checkbox_original = $session->getPage()->findField('checkbox_in_group');
$this->assertNotNull($checkbox_original, 'The checkbox_in_group is on the page.');
$original_id = $checkbox_original->getAttribute('id');
// Triggers an AJAX request/response.
$checkbox_original->check();
// The response contains a new nested "test group" form element, similar
// to the one already in the DOM except for a change in the form build id.
$checkbox_new = $assert_session->waitForElement('xpath', "//input[@name='checkbox_in_group' and not(@id='$original_id')]");
$this->assertNotNull($checkbox_new, 'DOM update: clicking the checkbox refreshed the checkbox_in_group structure');
$assert_session->responseContains('Test group');
$assert_session->responseContains('AJAX checkbox in a group');
$assert_session->responseContains('AJAX checkbox in a nested group');
$assert_session->responseContains('Another AJAX checkbox in a nested group');
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests maintenance message during an AJAX call.
*
* @group Ajax
*/
class AjaxMaintenanceModeTest extends WebDriverTestBase {
use FieldUiTestTrait;
use FileFieldCreationTrait;
use TestFileCreationTrait;
/**
* An user with administration permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'administer site configuration',
'access site in maintenance mode',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests maintenance message only appears once on an AJAX call.
*/
public function testAjaxCallMaintenanceMode(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
\Drupal::state()->set('system.maintenance_mode', TRUE);
$this->drupalGet('ajax-test/insert-inline-wrapper');
$assert_session->pageTextContains('Target inline');
$page->clickLink('Link html pre-wrapped-div');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->pageTextContainsOnce('Operating in maintenance mode');
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Component\Utility\UrlHelper;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests AJAX responses.
*
* @group Ajax
*/
class AjaxTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testAjaxWithAdminRoute() {
\Drupal::service('theme_installer')->install(['stable9', 'claro']);
$theme_config = \Drupal::configFactory()->getEditable('system.theme');
$theme_config->set('admin', 'claro');
$theme_config->set('default', 'stable9');
$theme_config->save();
$account = $this->drupalCreateUser(['view the administration theme']);
$this->drupalLogin($account);
// First visit the site directly via the URL. This should render it in the
// admin theme.
$this->drupalGet('admin/ajax-test/theme');
$assert = $this->assertSession();
$assert->pageTextContains('Current theme: claro');
// Now click the modal, which should use the front-end theme.
$this->drupalGet('ajax-test/dialog');
$assert->pageTextNotContains('Current theme: stable9');
$this->clickLink('Link 8 (ajax)');
$assert->assertWaitOnAjaxRequest();
$assert->pageTextContains('Current theme: stable9');
$assert->pageTextNotContains('Current theme: claro');
}
/**
* Tests that AJAX loaded libraries are not retained between requests.
*
* @see https://www.drupal.org/node/2647916
*/
public function testDrupalSettingsCachingRegression() {
$this->drupalGet('ajax-test/dialog');
$assert = $this->assertSession();
$session = $this->getSession();
// Insert a fake library into the already loaded library settings.
$fake_library = 'fakeLibrary/fakeLibrary';
$libraries = $session->evaluateScript("drupalSettings.ajaxPageState.libraries");
$libraries = UrlHelper::compressQueryParameter(UrlHelper::uncompressQueryParameter($libraries) . ',' . $fake_library);
$session->evaluateScript("drupalSettings.ajaxPageState.libraries = '$libraries';");
$ajax_page_state = $session->evaluateScript("drupalSettings.ajaxPageState");
$libraries = UrlHelper::uncompressQueryParameter($ajax_page_state['libraries']);
// Test that the fake library is set.
$this->assertStringContainsString($fake_library, $libraries);
// Click on the AJAX link.
$this->clickLink('Link 8 (ajax)');
$assert->assertWaitOnAjaxRequest();
// Test that the fake library is still set after the AJAX call.
$ajax_page_state = $session->evaluateScript("drupalSettings.ajaxPageState");
// Test that the fake library is set.
$this->assertStringContainsString($fake_library, UrlHelper::uncompressQueryParameter($ajax_page_state['libraries']));
// Reload the page, this should reset the loaded libraries and remove the
// fake library.
$this->drupalGet('ajax-test/dialog');
$ajax_page_state = $session->evaluateScript("drupalSettings.ajaxPageState");
$this->assertStringNotContainsString($fake_library, UrlHelper::uncompressQueryParameter($ajax_page_state['libraries']));
// Click on the AJAX link again, and the libraries should still not contain
// the fake library.
$this->clickLink('Link 8 (ajax)');
$assert->assertWaitOnAjaxRequest();
$ajax_page_state = $session->evaluateScript("drupalSettings.ajaxPageState");
$this->assertStringNotContainsString($fake_library, UrlHelper::uncompressQueryParameter($ajax_page_state['libraries']));
}
/**
* Tests that various AJAX responses with DOM elements are correctly inserted.
*
* After inserting DOM elements, Drupal JavaScript behaviors should be
* reattached and all top-level elements of type Node.ELEMENT_NODE need to be
* part of the context.
*/
public function testInsertAjaxResponse() {
$render_single_root = [
'pre-wrapped-div' => '<div class="pre-wrapped">pre-wrapped<script> var test;</script></div>',
'pre-wrapped-span' => '<span class="pre-wrapped">pre-wrapped<script> var test;</script></span>',
'pre-wrapped-whitespace' => ' <div class="pre-wrapped-whitespace">pre-wrapped-whitespace</div>' . "\n",
'not-wrapped' => 'not-wrapped',
'comment-string-not-wrapped' => '<!-- COMMENT -->comment-string-not-wrapped',
'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>',
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"></rect></svg>',
'empty' => '',
];
$render_multiple_root_unwrap = [
'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>',
'top-level-only' => '<div>element #1</div><div>element #2</div>',
'top-level-only-pre-whitespace' => ' <div>element #1</div><div>element #2</div> ',
'top-level-only-middle-whitespace-span' => '<span>element #1</span> <span>element #2</span>',
'top-level-only-middle-whitespace-div' => '<div>element #1</div> <div>element #2</div>',
];
// This is temporary behavior for BC reason.
$render_multiple_root_wrapper = [];
foreach ($render_multiple_root_unwrap as $key => $render) {
$render_multiple_root_wrapper["$key--effect"] = '<div>' . $render . '</div>';
}
$expected_renders = array_merge(
$render_single_root,
$render_multiple_root_wrapper,
$render_multiple_root_unwrap
);
// Checking default process of wrapping Ajax content.
foreach ($expected_renders as $render_type => $expected) {
$this->assertInsert($render_type, $expected);
}
// Checking custom ajaxWrapperMultipleRootElements wrapping.
$custom_wrapper_multiple_root = <<<JS
(function($, Drupal){
Drupal.theme.ajaxWrapperMultipleRootElements = function (elements) {
return $('<div class="my-favorite-div"></div>').append(elements);
};
}(jQuery, Drupal));
JS;
$expected = '<div class="my-favorite-div"><span>element #1</span> <span>element #2</span></div>';
$this->assertInsert('top-level-only-middle-whitespace-span--effect', $expected, $custom_wrapper_multiple_root);
// Checking custom ajaxWrapperNewContent wrapping.
$custom_wrapper_new_content = <<<JS
(function($, Drupal){
Drupal.theme.ajaxWrapperNewContent = function (elements) {
return $('<div class="div-wrapper-forever"></div>').append(elements);
};
}(jQuery, Drupal));
JS;
$expected = '<div class="div-wrapper-forever"></div>';
$this->assertInsert('empty', $expected, $custom_wrapper_new_content);
}
/**
* Tests that jQuery's global Ajax events are triggered at the correct time.
*/
public function testGlobalEvents() {
$session = $this->getSession();
$assert = $this->assertSession();
$expected_event_order = implode('', ['ajaxSuccess', 'ajaxComplete', 'ajaxStop']);
$this->drupalGet('ajax-test/global-events');
// Ensure that a non-Drupal Ajax request triggers the expected events, in
// the correct order, a single time.
$session->executeScript('jQuery.get(Drupal.url("core/COPYRIGHT.txt"))');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', $expected_event_order);
$assert->elementTextEquals('css', '#test_global_events_log2', $expected_event_order);
// Ensure that an Ajax request to a Drupal Ajax response, but that was not
// initiated with Drupal.Ajax(), triggers the expected events, in the
// correct order, a single time. We expect $expected_event_order to appear
// twice in each log element, because Drupal Ajax response commands (such
// as the one to clear the log element) are only executed for requests
// initiated with Drupal.Ajax(), and these elements already contain the
// text that was added above.
$session->executeScript('jQuery.get(Drupal.url("ajax-test/global-events/clear-log"))');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', str_repeat($expected_event_order, 2));
$assert->elementTextEquals('css', '#test_global_events_log2', str_repeat($expected_event_order, 2));
// Ensure that a Drupal Ajax request triggers the expected events, in the
// correct order, a single time.
// - We expect the first log element to list the events exactly once,
// because the Ajax response clears it, and we expect the events to be
// triggered after the commands are executed.
// - We expect the second log element to list the events exactly three
// times, because it already contains the two from the code that was
// already executed above. This additional log element that isn't cleared
// by the response's command ensures that the events weren't triggered
// additional times before the response commands were executed.
$this->click('#test_global_events_drupal_ajax_link');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', $expected_event_order);
$assert->elementTextEquals('css', '#test_global_events_log2', str_repeat($expected_event_order, 3));
}
/**
* Assert insert.
*
* @param string $render_type
* Render type.
* @param string $expected
* Expected result.
* @param string $script
* Script for additional theming.
*
* @internal
*/
public function assertInsert(string $render_type, string $expected, string $script = ''): void {
// Check insert to block element.
$this->drupalGet('ajax-test/insert-block-wrapper');
$this->getSession()->executeScript($script);
$this->clickLink("Link html $render_type");
$this->assertWaitPageContains('<div class="ajax-target-wrapper"><div id="ajax-target">' . $expected . '</div></div>');
$this->drupalGet('ajax-test/insert-block-wrapper');
$this->getSession()->executeScript($script);
$this->clickLink("Link replaceWith $render_type");
$this->assertWaitPageContains('<div class="ajax-target-wrapper">' . $expected . '</div>');
// Check insert to inline element.
$this->drupalGet('ajax-test/insert-inline-wrapper');
$this->getSession()->executeScript($script);
$this->clickLink("Link html $render_type");
$this->assertWaitPageContains('<div class="ajax-target-wrapper"><span id="ajax-target-inline">' . $expected . '</span></div>');
$this->drupalGet('ajax-test/insert-inline-wrapper');
$this->getSession()->executeScript($script);
$this->clickLink("Link replaceWith $render_type");
$this->assertWaitPageContains('<div class="ajax-target-wrapper">' . $expected . '</div>');
}
/**
* Asserts that page contains an expected value after waiting.
*
* @param string $expected
* A needle text.
*
* @internal
*/
protected function assertWaitPageContains(string $expected): void {
$page = $this->getSession()->getPage();
$this->assertTrue($page->waitFor(10, function () use ($page, $expected) {
// Clear content from empty styles and "processed" classes after effect.
$content = str_replace([' class="processed"', ' processed', ' style=""'], '', $page->getContent());
return stripos($content, $expected) !== FALSE;
}), "Page contains expected value: $expected");
}
/**
* Tests that Ajax errors are visible in the UI.
*/
public function testUiAjaxException() {
$themes = [
'olivero',
'claro',
'stark',
];
\Drupal::service('theme_installer')->install($themes);
foreach ($themes as $theme) {
$theme_config = \Drupal::configFactory()->getEditable('system.theme');
$theme_config->set('default', $theme);
$theme_config->save();
\Drupal::service('router.builder')->rebuildIfNeeded();
$this->drupalGet('ajax-test/exception-link');
$page = $this->getSession()->getPage();
// We don't want the test to error out because of an expected Javascript
// console error.
$this->failOnJavascriptConsoleErrors = FALSE;
// Click on the AJAX link.
$this->clickLink('Ajax Exception');
$this->assertSession()
->statusMessageContainsAfterWait("Oops, something went wrong. Check your browser's developer console for more details.", 'error');
if ($theme === 'olivero') {
// Check that the message can be closed.
$this->click('.messages__close');
$this->assertTrue($page->find('css', '.messages--error')
->hasClass('hidden'));
}
}
// This is needed to avoid an unfinished AJAX request error from tearDown()
// because this test intentionally does not complete all AJAX requests.
$this->getSession()->executeScript("delete window.drupalActiveXhrCount");
}
/**
* Tests ajax focus handling.
*/
public function testAjaxFocus() {
$this->drupalGet('/ajax_forms_test_get_form');
$this->assertNotNull($select = $this->assertSession()->elementExists('css', '#edit-select'));
$select->setValue('green');
$this->assertSession()->assertWaitOnAjaxRequest();
$has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
$this->assertEquals('edit-select', $has_focus_id);
$this->assertNotNull($checkbox = $this->assertSession()->elementExists('css', '#edit-checkbox'));
$checkbox->check();
$this->assertSession()->assertWaitOnAjaxRequest();
$has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
$this->assertEquals('edit-checkbox', $has_focus_id);
$this->assertNotNull($textfield1 = $this->assertSession()->elementExists('css', '#edit-textfield'));
$this->assertNotNull($textfield2 = $this->assertSession()->elementExists('css', '#edit-textfield-2'));
$this->assertNotNull($textfield3 = $this->assertSession()->elementExists('css', '#edit-textfield-3'));
// Test textfield with 'blur' event listener.
$textfield1->setValue('Kittens say purr');
$textfield2->focus();
$this->assertSession()->assertWaitOnAjaxRequest();
$has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
$this->assertEquals('edit-textfield-2', $has_focus_id);
// Test textfield with 'change' event.
$textfield3->focus();
$textfield3->setValue('Wasps buzz');
$textfield3->blur();
$this->assertSession()->assertWaitOnAjaxRequest();
$has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
$this->assertEquals('edit-textfield-3', $has_focus_id);
}
}
<?php
declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on AJAX framework commands.
*
* @group Ajax
*/
class CommandsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the various Ajax Commands.
*/
public function testAjaxCommands() {
$session = $this->getSession();
$page = $this->getSession()->getPage();
$form_path = 'ajax_forms_test_ajax_commands_form';
$web_user = $this->drupalCreateUser(['access content']);
$this->drupalLogin($web_user);
$this->drupalGet($form_path);
// Tests the 'add_css' command.
$page->pressButton("AJAX 'add_css' command");
$this->assertWaitPageContains('my/file.css');
$this->assertSession()->elementExists('css', 'link[href="my/file.css"]');
$this->assertSession()->elementExists('css', 'link[href="https://example.com/css?family=Open+Sans"]');
// Tests the 'after' command.
$page->pressButton("AJAX 'After': Click to put something after the div");
$this->assertWaitPageContains('<div id="after_div">Something can be inserted after this</div>This will be placed after');
// Tests the 'alert' command.
$page->pressButton("AJAX 'Alert': Click to alert");
// Wait for the alert to appear.
$page->waitFor(10, function () use ($session) {
try {
$session->getDriver()->getWebDriverSession()->getAlert_text();
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
});
$alert_text = $this->getSession()->getDriver()->getWebDriverSession()->getAlert_text();
$this->assertEquals('Alert', $alert_text);
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
$this->drupalGet($form_path);
$page->pressButton("AJAX 'Announce': Click to announce");
$this->assertWaitPageContains('<div id="drupal-live-announce" class="visually-hidden" aria-live="polite" aria-busy="false">Default announcement.</div>');
$this->drupalGet($form_path);
$page->pressButton("AJAX 'Announce': Click to announce with 'polite' priority");
$this->assertWaitPageContains('<div id="drupal-live-announce" class="visually-hidden" aria-live="polite" aria-busy="false">Polite announcement.</div>');
$this->drupalGet($form_path);
$page->pressButton("AJAX 'Announce': Click to announce with 'assertive' priority");
$this->assertWaitPageContains('<div id="drupal-live-announce" class="visually-hidden" aria-live="assertive" aria-busy="false">Assertive announcement.</div>');
$this->drupalGet($form_path);
$page->pressButton("AJAX 'Announce': Click to announce twice");
$this->assertWaitPageContains('<div id="drupal-live-announce" class="visually-hidden" aria-live="assertive" aria-busy="false">Assertive announcement.' . "\nAnother announcement.</div>");
// Tests the 'append' command.
$page->pressButton("AJAX 'Append': Click to append something");
$this->assertWaitPageContains('<div id="append_div">Append inside this divAppended text</div>');
// Tests the 'before' command.
$page->pressButton("AJAX 'before': Click to put something before the div");
$this->assertWaitPageContains('Before text<div id="before_div">Insert something before this.</div>');
// Tests the 'changed' command.
$page->pressButton("AJAX changed: Click to mark div changed.");
$this->assertWaitPageContains('<div id="changed_div" class="ajax-changed">');
// Tests the 'changed' command using the second argument.
// Refresh page for testing 'changed' command to same element again.
$this->drupalGet($form_path);
$page->pressButton("AJAX changed: Click to mark div changed with asterisk.");
$this->assertWaitPageContains('<div id="changed_div" class="ajax-changed"> <div id="changed_div_mark_this">This div can be marked as changed or not. <abbr class="ajax-changed" title="Changed">*</abbr> </div></div>');
// Tests the 'css' command.
$page->pressButton("Set the '#box' div to be blue.");
$this->assertWaitPageContains('<div id="css_div" style="background-color: blue;">');
// Tests the 'data' command.
$page->pressButton("AJAX data command: Issue command.");
$this->assertTrue($page->waitFor(10, function () use ($session) {
return 'test_value' === $session->evaluateScript('window.jQuery("#data_div").data("testkey")');
}));
// Tests the 'html' command.
$page->pressButton("AJAX html: Replace the HTML in a selector.");
$this->assertWaitPageContains('<div id="html_div">replacement text</div>');
// Tests the 'insert' command.
$page->pressButton("AJAX insert: Let client insert based on #ajax['method'].");
$this->assertWaitPageContains('<div id="insert_div">insert replacement textOriginal contents</div>');
// Tests the 'invoke' command.
$page->pressButton("AJAX invoke command: Invoke addClass() method.");
$this->assertWaitPageContains('<div id="invoke_div" class="error">Original contents</div>');
// Tests the 'prepend' command.
$page->pressButton("AJAX 'prepend': Click to prepend something");
$this->assertWaitPageContains('<div id="prepend_div">prepended textSomething will be prepended to this div. </div>');
// Tests the 'remove' command.
$page->pressButton("AJAX 'remove': Click to remove text");
$this->assertWaitPageContains('<div id="remove_div"></div>');
// Tests the 'restripe' command.
$page->pressButton("AJAX 'restripe' command");
$this->assertWaitPageContains('<tr id="table-first" class="odd"><td>first row</td></tr>');
$this->assertWaitPageContains('<tr class="even"><td>second row</td></tr>');
// Tests the 'settings' command.
$test_settings_command = <<<JS
Drupal.behaviors.testSettingsCommand = {
attach: function (context, settings) {
window.jQuery('body').append('<div class="test-settings-command">' + settings.ajax_forms_test.foo + '</div>');
}
};
JS;
$session->executeScript($test_settings_command);
// @todo: Replace after https://www.drupal.org/project/drupal/issues/2616184
$session->executeScript('window.jQuery("#edit-settings-command-example").mousedown();');
$this->assertWaitPageContains('<div class="test-settings-command">42</div>');
}
/**
* Tests the various Ajax Commands with legacy parameters.
* @group legacy
*/
public function testLegacyAjaxCommands() {
$session = $this->getSession();
$page = $this->getSession()->getPage();
$form_path = 'ajax_forms_test_ajax_commands_form';
$web_user = $this->drupalCreateUser(['access content']);
$this->drupalLogin($web_user);
$this->drupalGet($form_path);
// Tests the 'add_css' command with legacy string value.
$this->expectDeprecation('Javascript Deprecation: Passing a string to the Drupal.ajax.add_css() method is deprecated in 10.1.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3154948.');
$page->pressButton("AJAX 'add_css' legacy command");
$this->assertWaitPageContains('my/file.css');
}
/**
* Asserts that page contains a text after waiting.
*
* @param string $text
* A needle text.
*
* @internal
*/
protected function assertWaitPageContains(string $text): void {
$page = $this->getSession()->getPage();
$page->waitFor(10, function () use ($page, $text) {
return stripos($page->getContent(), $text) !== FALSE;
});
$this->assertStringContainsString($text, $page->getContent());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment