first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,33 @@
<?php
namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\ComposerIntegrationTrait;
/**
* @group Composer
* @requires externalCommand 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();
}
}
@@ -0,0 +1,288 @@
<?php
namespace Drupal\BuildTests\Composer\Template;
use Composer\Json\JsonFile;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Composer\Composer;
use Symfony\Component\Finder\Finder;
/**
* Demonstrate that Composer project templates are buildable 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
*
* @requires externalCommand composer
*/
class ComposerProjectTemplatesTest extends BuildTestBase {
/**
* 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.
$path_repos = Finder::create()
->files()
->name('composer.json')
->in($workspace_directory . '/composer/' . $subdir);
$data = [];
/* @var $path_repo \SplFileInfo */
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 we've accounted for all the templates.
*/
public function testVerifyTemplateTestProviderIsAccurate() {
$root = $this->getDrupalRoot();
$data = $this->provideTemplateCreateProject($root);
// Find all the templates.
$template_files = Finder::create()
->files()
->name('composer.json')
->in($root . '/composer/Template');
$this->assertEquals(count($template_files), count($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 $file \SplFileInfo */
foreach ($template_files as $file) {
$json_file = new JsonFile($file->getPathname());
$json = $json_file->read();
$this->assertArrayHasKey('name', $json);
// Does provideTemplateCreateProject() give us this template name?
$this->assertArrayHasKey($json['name'], $template_data);
}
}
/**
* @dataProvider provideTemplateCreateProject
*/
public function testTemplateCreateProject($project, $package_dir, $docroot_dir) {
$composer_version_line = exec('composer --version');
if (strpos($composer_version_line, 'Composer version 2') !== FALSE) {
// @todo Remove in https://www.drupal.org/project/drupal/issues/3128631
$this->markTestSkipped("Composer 2 not supported for this test yet.");
}
// Make a working COMPOSER_HOME directory for setting global composer config
$composer_home = $this->getWorkspaceDirectory() . '/composer-home';
mkdir($composer_home);
// 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();
// Get the Drupal core version branch. For instance, this should be
// 8.9.x-dev for the 8.9.x branch.
$core_version = Composer::drupalVersionBranch();
// 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.
$this->copyCodebase();
// 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');
$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();
}
$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, $core_version);
$autoloader = $this->getWorkspaceDirectory() . '/testproject' . $docroot_dir . '/autoload.php';
$this->assertFileNotExists($autoloader);
$this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$core_version composer create-project --no-ansi $project testproject $core_version -s dev -vv --repository $repository_path");
$this->assertCommandSuccessful();
// Ensure we used the project from our codebase.
$this->assertErrorOutputContains("Installing $project ($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 ($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);
// 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 ($core_version)");
}
else {
$this->assertErrorOutputContains("Installing $package_name ($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.
if (is_dir($full_path)) {
$packages['packages'][$name] = [
$version => [
"name" => $name,
"dist" => [
"type" => "path",
"url" => $path,
],
"version" => $version,
],
];
}
}
$json = json_encode($packages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
mkdir(dirname($repository_path));
file_put_contents($repository_path, $json);
}
}
@@ -0,0 +1,596 @@
<?php
namespace Drupal\BuildTests\Framework;
use Behat\Mink\Driver\Goutte\Client;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Mink;
use Behat\Mink\Session;
use Drupal\Component\FileSystem\FileSystem as DrupalFilesystem;
use Drupal\Tests\PhpunitCompatibilityTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\BrowserKit\Client as SymfonyClient;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
/**
* Provides a workspace to test build processes.
*
* If you need to build a file system and then run a command from the command
* line then this is the test framework for you.
*
* Tests using this interface run in separate processes.
*
* Tests can perform HTTP requests against the assembled codebase.
*
* The results of these HTTP requests can be asserted using Mink.
*
* This framework does not use the same Mink extensions as BrowserTestBase.
*
* Features:
* - Provide complete isolation between the test runner and the site under test.
* - Provide a workspace where filesystem build processes can be performed.
* - Allow for the use of PHP's build-in HTTP server to send requests to the
* site built using the filesystem.
* - Allow for commands and HTTP requests to be made to different subdirectories
* of the workspace filesystem, to facilitate comparison between different
* build results, and to support Composer builds which have an alternate
* docroot.
* - Provide as little framework as possible. Convenience methods should be
* built into the test, or abstract base classes.
* - Allow parallel testing, using random/unique port numbers for different HTTP
* servers.
* - Allow the use of PHPUnit-style (at)require annotations for external shell
* commands.
*
* We don't use UiHelperInterface because it is too tightly integrated to
* Drupal.
*/
abstract class BuildTestBase extends TestCase {
use ExternalCommandRequirementsTrait;
use PhpunitCompatibilityTrait;
/**
* The working directory where this test will manipulate files.
*
* Use getWorkspaceDirectory() to access this information.
*
* @var string
*
* @see self::getWorkspaceDirectory()
*/
private $workspaceDir;
/**
* The process that's running the HTTP server.
*
* @var \Symfony\Component\Process\Process
*
* @see self::standUpServer()
* @see self::stopServer()
*/
private $serverProcess = NULL;
/**
* Default to destroying build artifacts after a test finishes.
*
* Mainly useful for debugging.
*
* @var bool
*/
protected $destroyBuild = TRUE;
/**
* The docroot for the server process.
*
* This stores the last docroot directory used to start the server process. We
* keep this information so we can restart the server if the desired docroot
* changes.
*
* @var string
*/
private $serverDocroot = NULL;
/**
* Our native host name, used by PHP when it starts up the server.
*
* Requests should always be made to 'localhost', and not this IP address.
*
* @var string
*/
private static $hostName = '127.0.0.1';
/**
* Port that will be tested.
*
* Generated internally. Use getPortNumber().
*
* @var int
*/
private $hostPort;
/**
* A list of ports used by the test.
*
* Prevent the same process finding the same port by storing a list of ports
* already discovered. This also stores locks so they are not released until
* the test class is torn down.
*
* @var \Symfony\Component\Lock\LockInterface[]
*/
private $portLocks = [];
/**
* The Mink session manager.
*
* @var \Behat\Mink\Mink
*/
private $mink;
/**
* The most recent command process.
*
* @var \Symfony\Component\Process\Process
*
* @see ::executeCommand()
*/
private $commandProcess;
/**
* {@inheritdoc}
*/
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
static::checkClassCommandRequirements();
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
static::checkMethodCommandRequirements($this->getName());
$this->phpFinder = new PhpExecutableFinder();
// Set up the workspace directory.
// @todo Glean working directory from env vars, etc.
$fs = new SymfonyFilesystem();
$this->workspaceDir = $fs->tempnam(DrupalFilesystem::getOsTemporaryDirectory(), '/build_workspace_' . md5($this->getName() . microtime(TRUE)));
$fs->remove($this->workspaceDir);
$fs->mkdir($this->workspaceDir);
$this->initMink();
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
parent::tearDown();
$this->stopServer();
foreach ($this->portLocks as $lock) {
$lock->release();
}
$ws = $this->getWorkspaceDirectory();
$fs = new SymfonyFilesystem();
if ($this->destroyBuild && $fs->exists($ws)) {
// Filter out symlinks as chmod cannot alter them.
$finder = new Finder();
$finder->in($ws)
->directories()
->ignoreVCS(FALSE)
->ignoreDotFiles(FALSE)
// composer script is a symlink and fails chmod. Ignore it.
->notPath('/^vendor\/bin\/composer$/');
$fs->chmod($finder->getIterator(), 0775, 0000);
$fs->remove($ws);
}
}
/**
* Get the working directory within the workspace, creating if necessary.
*
* @param string $working_dir
* The path within the workspace directory.
*
* @return string
* The full path to the working directory within the workspace directory.
*/
protected function getWorkingPath($working_dir = NULL) {
$full_path = $this->getWorkspaceDirectory();
if ($working_dir) {
$full_path .= '/' . $working_dir;
}
if (!file_exists($full_path)) {
$fs = new SymfonyFilesystem();
$fs->mkdir($full_path);
}
return $full_path;
}
/**
* Set up the Mink session manager.
*
* @return \Behat\Mink\Session
*/
protected function initMink() {
// If the Symfony BrowserKit client can followMetaRefresh(), we should use
// the Goutte descendent instead of ours.
if (method_exists(SymfonyClient::class, 'followMetaRefresh')) {
$client = new Client();
}
else {
$client = new DrupalMinkClient();
}
$client->followMetaRefresh(TRUE);
$driver = new GoutteDriver($client);
$session = new Session($driver);
$this->mink = new Mink();
$this->mink->registerSession('default', $session);
$this->mink->setDefaultSessionName('default');
$session->start();
return $session;
}
/**
* Get the Mink instance.
*
* Use the Mink object to perform assertions against the content returned by a
* request.
*
* @return \Behat\Mink\Mink
* The Mink object.
*/
public function getMink() {
return $this->mink;
}
/**
* Full path to the workspace where this test can build.
*
* This is often a directory within the system's temporary directory.
*
* @return string
* Full path to the workspace where this test can build.
*/
public function getWorkspaceDirectory() {
return $this->workspaceDir;
}
/**
* Assert that text is present in the error output of the most recent command.
*
* @param string $expected
* Text we expect to find in the error output of the command.
*/
public function assertErrorOutputContains($expected) {
$this->assertStringContainsString($expected, $this->commandProcess->getErrorOutput());
}
/**
* Assert that text is present in the output of the most recent command.
*
* @param string $expected
* Text we expect to find in the output of the command.
*/
public function assertCommandOutputContains($expected) {
$this->assertStringContainsString($expected, $this->commandProcess->getOutput());
}
/**
* Asserts that the last command ran without error.
*
* This assertion checks whether the last command returned an exit code of 0.
*
* If you need to assert a different exit code, then you can use
* executeCommand() and perform a different assertion on the process object.
*/
public function assertCommandSuccessful() {
return $this->assertCommandExitCode(0);
}
/**
* Asserts that the last command returned the specified exit code.
*
* @param int $expected_code
* The expected process exit code.
*/
public function assertCommandExitCode($expected_code) {
$this->assertEquals($expected_code, $this->commandProcess->getExitCode(),
'COMMAND: ' . $this->commandProcess->getCommandLine() . "\n" .
'OUTPUT: ' . $this->commandProcess->getOutput() . "\n" .
'ERROR: ' . $this->commandProcess->getErrorOutput() . "\n"
);
}
/**
* Run a command.
*
* @param string $command_line
* A command line to run in an isolated process.
* @param string $working_dir
* (optional) A working directory relative to the workspace, within which to
* execute the command. Defaults to the workspace directory.
*
* @return \Symfony\Component\Process\Process
*/
public function executeCommand($command_line, $working_dir = NULL) {
$this->commandProcess = new Process($command_line);
$this->commandProcess->setWorkingDirectory($this->getWorkingPath($working_dir))
->setTimeout(300)
->setIdleTimeout(300);
$this->commandProcess->run();
return $this->commandProcess;
}
/**
* Helper function to assert that the last visit was a Drupal site.
*
* This method asserts that the X-Generator header shows that the site is a
* Drupal site.
*/
public function assertDrupalVisit() {
$this->getMink()->assertSession()->responseHeaderMatches('X-Generator', '/Drupal \d+ \(https:\/\/www.drupal.org\)/');
}
/**
* Visit a URI on the HTTP server.
*
* The concept here is that there could be multiple potential docroots in the
* workspace, so you can use whichever ones you want.
*
* @param string $request_uri
* (optional) The non-host part of the URL. Example: /some/path?foo=bar.
* Defaults to visiting the homepage.
* @param string $working_dir
* (optional) Relative path within the test workspace file system that will
* be the docroot for the request. Defaults to the workspace directory.
*
* @return \Behat\Mink\Mink
* The Mink object. Perform assertions against this.
*
* @throws \InvalidArgumentException
* Thrown when $request_uri does not start with a slash.
*/
public function visit($request_uri = '/', $working_dir = NULL) {
if ($request_uri[0] !== '/') {
throw new \InvalidArgumentException('URI: ' . $request_uri . ' must be relative. Example: /some/path?foo=bar');
}
// Try to make a server.
$this->standUpServer($working_dir);
$request = 'http://localhost:' . $this->getPortNumber() . $request_uri;
$this->mink->getSession()->visit($request);
return $this->mink;
}
/**
* Makes a local test server using PHP's internal HTTP server.
*
* Test authors should call visit() or assertVisit() instead.
*
* @param string|null $working_dir
* (optional) Server docroot relative to the workspace file system. Defaults
* to the workspace directory.
*/
protected function standUpServer($working_dir = NULL) {
// If the user wants to test a new docroot, we have to shut down the old
// server process and generate a new port number.
if ($working_dir !== $this->serverDocroot && !empty($this->serverProcess)) {
$this->stopServer();
}
// If there's not a server at this point, make one.
if (!$this->serverProcess || $this->serverProcess->isTerminated()) {
$this->serverProcess = $this->instantiateServer($this->getPortNumber(), $working_dir);
if ($this->serverProcess) {
$this->serverDocroot = $working_dir;
}
}
}
/**
* Do the work of making a server process.
*
* Test authors should call visit() or assertVisit() instead.
*
* When initializing the server, if '.ht.router.php' exists in the root, it is
* leveraged. If testing with a version of Drupal before 8.5.x., this file
* does not exist.
*
* @param int $port
* The port number for the server.
* @param string|null $working_dir
* (optional) Server docroot relative to the workspace filesystem. Defaults
* to the workspace directory.
*
* @return \Symfony\Component\Process\Process
* The server process.
*
* @throws \RuntimeException
* Thrown if we were unable to start a web server.
*/
protected function instantiateServer($port, $working_dir = NULL) {
$finder = new PhpExecutableFinder();
$working_path = $this->getWorkingPath($working_dir);
$server = [
$finder->find(),
'-S',
self::$hostName . ':' . $port,
'-t',
$working_path,
];
if (file_exists($working_path . DIRECTORY_SEPARATOR . '.ht.router.php')) {
$server[] = $working_path . DIRECTORY_SEPARATOR . '.ht.router.php';
}
$ps = new Process($server, $working_path);
$ps->setIdleTimeout(30)
->setTimeout(30)
->start();
// Wait until the web server has started. It is started if the port is no
// longer available.
for ($i = 0; $i < 1000; $i++) {
if (!$this->checkPortIsAvailable($port)) {
return $ps;
}
usleep(1000);
}
throw new \RuntimeException(sprintf("Unable to start the web server.\nERROR OUTPUT:\n%s", $ps->getErrorOutput()));
}
/**
* Stop the HTTP server, zero out all necessary variables.
*/
protected function stopServer() {
if (!empty($this->serverProcess)) {
$this->serverProcess->stop();
}
$this->serverProcess = NULL;
$this->serverDocroot = NULL;
$this->hostPort = NULL;
$this->initMink();
}
/**
* Discover an available port number.
*
* @return int
* The available port number that we discovered.
*
* @throws \RuntimeException
* Thrown when there are no available ports within the range.
*/
protected function findAvailablePort() {
$store = new FlockStore(DrupalFilesystem::getOsTemporaryDirectory());
$lock_factory = new Factory($store);
$counter = 100;
while ($counter--) {
// Limit to 9999 as higher ports cause random fails on DrupalCI.
$port = random_int(1024, 9999);
if (isset($this->portLocks[$port])) {
continue;
}
// Take a lock so that no other process can use the same port number even
// if the server is yet to start.
$lock = $lock_factory->createLock('drupal-build-test-port-' . $port);
if ($lock->acquire()) {
if ($this->checkPortIsAvailable($port)) {
$this->portLocks[$port] = $lock;
return $port;
}
else {
$lock->release();
}
}
}
throw new \RuntimeException('Unable to find a port available to run the web server.');
}
/**
* Checks whether a port is available.
*
* @param $port
* A number between 1024 and 65536.
*
* @return bool
*/
protected function checkPortIsAvailable($port) {
$fp = @fsockopen(self::$hostName, $port, $errno, $errstr, 1);
// If fsockopen() fails to connect, probably nothing is listening.
// It could be a firewall but that's impossible to detect, so as a
// best guess let's return it as available.
if ($fp === FALSE) {
return TRUE;
}
else {
fclose($fp);
}
return FALSE;
}
/**
* Get the port number for requests.
*
* Test should never call this. Used by standUpServer().
*
* @return int
*/
protected function getPortNumber() {
if (empty($this->hostPort)) {
$this->hostPort = $this->findAvailablePort();
}
return $this->hostPort;
}
/**
* Copy the current working codebase into a workspace.
*
* Use this method to copy the current codebase, including any patched
* changes, into the workspace.
*
* By default, the copy will exclude sites/default/settings.php,
* sites/default/files, and vendor/. Use the $iterator parameter to override
* this behavior.
*
* @param \Iterator|null $iterator
* (optional) An iterator of all the files to copy. Default behavior is to
* exclude site-specific directories and files.
* @param string|null $working_dir
* (optional) Relative path within the test workspace file system that will
* contain the copy of the codebase. Defaults to the workspace directory.
*/
public function copyCodebase(\Iterator $iterator = NULL, $working_dir = NULL) {
$working_path = $this->getWorkingPath($working_dir);
if ($iterator === NULL) {
$iterator = $this->getCodebaseFinder()->getIterator();
}
$fs = new SymfonyFilesystem();
$options = ['override' => TRUE, 'delete' => FALSE];
$fs->mirror($this->getDrupalRoot(), $working_path, $iterator, $options);
}
/**
* Get a default Finder object for a Drupal codebase.
*
* This method can be used two ways:
* - Override this method and provide your own default Finder object for
* copyCodebase().
* - Call the method to get a default Finder object which can then be
* modified for other purposes.
*
* @return \Symfony\Component\Finder\Finder
* A Finder object ready to iterate over core codebase.
*/
public function getCodebaseFinder() {
$finder = new Finder();
$finder->files()
->ignoreUnreadableDirs()
->in($this->getDrupalRoot())
->notPath('#^sites/default/files#')
->notPath('#^sites/simpletest#')
->notPath('#^vendor#')
->notPath('#^sites/default/settings\..*php#')
->ignoreDotFiles(FALSE)
->ignoreVCS(FALSE);
return $finder;
}
/**
* Get the root path of this Drupal codebase.
*
* @return string
* The full path to the root of this Drupal codebase.
*/
protected function getDrupalRoot() {
return realpath(dirname(__DIR__, 5));
}
}
@@ -0,0 +1,74 @@
<?php
namespace Drupal\BuildTests\Framework;
use Behat\Mink\Driver\Goutte\Client;
use Symfony\Component\BrowserKit\Client as SymfonyClient;
/**
* Extend the Mink client for Drupal use-cases.
*
* This is adapted from https://github.com/symfony/symfony/pull/27118.
*
* @todo Update this client when Drupal starts using Symfony 4.2.0+.
* https://www.drupal.org/project/drupal/issues/3077785
*/
class DrupalMinkClient extends Client {
/**
* Whether to follow meta redirects or not.
*
* @var bool
*
* @see \Drupal\BuildTests\Framework\DrupalMinkClient::followMetaRefresh()
*/
protected $followMetaRefresh;
/**
* Sets whether to automatically follow meta refresh redirects or not.
*
* @param bool $followMetaRefresh
* (optional) Whether to follow meta redirects. Defaults to TRUE.
*/
public function followMetaRefresh(bool $followMetaRefresh = TRUE) {
$this->followMetaRefresh = $followMetaRefresh;
}
/**
* Glean the meta refresh URL from the current page content.
*
* @return string|null
* Either the redirect URL that was found, or NULL if none was found.
*/
private function getMetaRefreshUrl() {
$metaRefresh = $this->getCrawler()->filter('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]');
foreach ($metaRefresh->extract(['content']) as $content) {
if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) {
return str_replace("\t\r\n", '', rtrim($m[1]));
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function request($method, $uri, array $parameters = [], array $files = [], array $server = [], $content = NULL, $changeHistory = TRUE) {
$this->crawler = parent::request($method, $uri, $parameters, $files, $server, $content, $changeHistory);
// Check for meta refresh redirect and follow it.
if ($this->followMetaRefresh && NULL !== $redirect = $this->getMetaRefreshUrl()) {
$this->redirect = $redirect;
// $this->redirects is private on the BrowserKit client, so we have to use
// reflection to manage the redirects stack.
$ref_redirects = new \ReflectionProperty(SymfonyClient::class, 'redirects');
$ref_redirects->setAccessible(TRUE);
$redirects = $ref_redirects->getValue($this);
$redirects[serialize($this->history->current())] = TRUE;
$ref_redirects->setValue($this, $redirects);
$this->crawler = $this->followRedirect();
}
return $this->crawler;
}
}
@@ -0,0 +1,106 @@
<?php
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'.
*/
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() {
$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) {
$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) {
// Make a list of required commands.
$required_commands = [];
foreach ($annotations as $requirement) {
if (strpos($requirement, 'externalCommand ') === 0) {
$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);
}
}
@@ -0,0 +1,188 @@
<?php
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.txt',
'.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/>',
],
],
],
'vendor' => [
'composer' => [
'composer' => [
'installed.json' => '"items": {"things"}',
],
],
],
'modules' => [
'my_module' => [
'vendor' => [
'my_vendor' => [
'composer.json' => "{\n}",
],
],
],
],
]);
// Mock BuildTestBase so that it thinks our VFS is the Drupal root.
/** @var \PHPUnit\Framework\MockObject\MockBuilder|\Drupal\BuildTests\Framework\BuildTestBase $base */
$base = $this->getMockBuilder(BuildTestBase::class)
->setMethods(['getDrupalRoot'])
->getMockForAbstractClass();
$base->expects($this->exactly(2))
->method('getDrupalRoot')
->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();
}
/**
* @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');
$ref_process->setAccessible(TRUE);
$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));
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\DrupalMinkClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\BrowserKit\Response;
/**
* Test \Drupal\BuildTests\Framework\DrupalMinkClient.
*
* This test is adapted from \Symfony\Component\BrowserKit\Tests\ClientTest.
*
* @coversDefaultClass \Drupal\BuildTests\Framework\DrupalMinkClient
*
* @group Build
*/
class DrupalMinkClientTest extends TestCase {
/**
* @dataProvider getTestsForMetaRefresh
* @covers ::getMetaRefreshUrl
*/
public function testFollowMetaRefresh(string $content, string $expectedEndingUrl, bool $followMetaRefresh = TRUE) {
$client = new TestClient();
$client->followMetaRefresh($followMetaRefresh);
$client->setNextResponse(new Response($content));
$client->request('GET', 'http://www.example.com/foo/foobar');
$this->assertEquals($expectedEndingUrl, $client->getRequest()->getUri());
}
public function getTestsForMetaRefresh() {
return [
['<html><head><meta http-equiv="Refresh" content="4" /><meta http-equiv="refresh" content="0; URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL=\'http://www.example.com/redirected\'"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content=\'0;URL="http://www.example.com/redirected"\'/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0; URL = http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL= http://www.example.com/redirected "/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;url=http://www.example.com/redirected "/></head></html>', 'http://www.example.com/redirected'],
['<html><head><noscript><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></noscript></head></head></html>', 'http://www.example.com/redirected'],
// Non-zero timeout should not result in a redirect.
['<html><head><meta http-equiv="refresh" content="4; URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/foo/foobar'],
['<html><body></body></html>', 'http://www.example.com/foo/foobar'],
// HTML 5 allows the meta tag to be placed in head or body.
['<html><body><meta http-equiv="refresh" content="0;url=http://www.example.com/redirected"/></body></html>', 'http://www.example.com/redirected'],
// Valid meta refresh should not be followed if disabled.
['<html><head><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/foo/foobar', FALSE],
'drupal-1' => ['<html><head><meta http-equiv="Refresh" content="0; URL=/update.php/start?id=2&op=do_nojs" /></body></html>', 'http://www.example.com/update.php/start?id=2&op=do_nojs'],
'drupal-2' => ['<html><head><noscript><meta http-equiv="Refresh" content="0; URL=/update.php/start?id=2&op=do_nojs" /></noscript></body></html>', 'http://www.example.com/update.php/start?id=2&op=do_nojs'],
];
}
/**
* @covers ::request
*/
public function testBackForwardMetaRefresh() {
$client = new TestClient();
$client->followMetaRefresh();
// First request.
$client->request('GET', 'http://www.example.com/first-page');
$content = '<html><head><meta http-equiv="Refresh" content="0; URL=/refreshed" /></body></html>';
$client->setNextResponse(new Response($content, 200));
$client->request('GET', 'http://www.example.com/refresh-from-here');
$this->assertEquals('http://www.example.com/refreshed', $client->getRequest()->getUri());
$client->back();
$this->assertEquals('http://www.example.com/first-page', $client->getRequest()->getUri());
$client->forward();
$this->assertEquals('http://www.example.com/refreshed', $client->getRequest()->getUri());
}
}
/**
* Special client that can return a given response on the first doRequest().
*/
class TestClient extends DrupalMinkClient {
protected $nextResponse = NULL;
public function setNextResponse(Response $response) {
$this->nextResponse = $response;
}
protected function doRequest($request) {
if (NULL === $this->nextResponse) {
return new Response();
}
$response = $this->nextResponse;
$this->nextResponse = NULL;
return $response;
}
}
@@ -0,0 +1,182 @@
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait
* @group Build
*/
class ExternalCommandRequirementTest extends TestCase {
/**
* @covers ::checkExternalCommandRequirements
*/
public function testCheckExternalCommandRequirementsNotAvailable() {
$requires = new UsesCommandRequirements();
$ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements');
$ref_check_requirements->setAccessible(TRUE);
// 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() {
$requires = new UsesCommandRequirements();
$ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements');
$ref_check_requirements->setAccessible(TRUE);
// 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() {
$requires = new ClassRequiresAvailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
$ref_check->setAccessible(TRUE);
// 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() {
$requires = new ClassRequiresUnavailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
$ref_check->setAccessible(TRUE);
// 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() {
$requires = new MethodRequires();
$ref_check = new \ReflectionMethod($requires, 'checkMethodCommandRequirements');
$ref_check->setAccessible(TRUE);
// 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() {
$requires = new MethodRequires();
$ref_check = new \ReflectionMethod($requires, 'checkMethodCommandRequirements');
$ref_check->setAccessible(TRUE);
// 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']);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\QuickStart\QuickStartTestBase;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
* @group Build
*/
class HtRouterTest extends QuickStartTestBase {
/**
* @covers ::instantiateServer
*/
public function testHtRouter() {
$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);
}
}
@@ -0,0 +1,66 @@
<?php
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();
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests Ajax callbacks on FAPI elements.
*
* @group Ajax
*/
class AjaxCallbacksTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public 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"]'));
}
}
@@ -0,0 +1,113 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the usage of form caching for AJAX forms.
*
* @group Ajax
*/
class AjaxFormCacheTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public 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->container->get('router.builder')->rebuild();
$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',
FormBuilderInterface::AJAX_FORM_REQUEST => 1,
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
]);
$this->assertUrl($url);
}
}
@@ -0,0 +1,54 @@
<?php
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}
*/
public 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');
}
}
@@ -0,0 +1,132 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on AJAX forms in cached pages.
*
* @group Ajax
*/
class AjaxFormPageCacheTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Return the build id of the current form.
*/
protected function getFormBuildId() {
$build_id_fields = $this->xpath('//input[@name="form_build_id"]');
$this->assertCount(1, $build_id_fields, 'One form build id field on the page');
return $build_id_fields[0]->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 a 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 a 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 a 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 simpletest-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 a 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('drivertext');
$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', "ul.messages__list li.messages__item em:contains('some dumb text')");
$this->assertNotNull($placeholder, 'Message structure containing input data located.');
}
}
@@ -0,0 +1,62 @@
<?php
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() {
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 a 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');
}
}
@@ -0,0 +1,205 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests AJAX responses.
*
* @group Ajax
*/
class AjaxTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testAjaxWithAdminRoute() {
\Drupal::service('theme_installer')->install(['stable', 'seven']);
$theme_config = \Drupal::configFactory()->getEditable('system.theme');
$theme_config->set('admin', 'seven');
$theme_config->set('default', 'stable');
$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: seven');
// Now click the modal, which should also use the admin theme.
$this->drupalGet('ajax-test/dialog');
$assert->pageTextNotContains('Current theme: stable');
$this->clickLink('Link 8 (ajax)');
$assert->assertWaitOnAjaxRequest();
$assert->pageTextContains('Current theme: stable');
$assert->pageTextNotContains('Current theme: seven');
}
/**
* Test 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';
$session->evaluateScript("drupalSettings.ajaxPageState.libraries = drupalSettings.ajaxPageState.libraries + ',$fake_library';");
$libraries = $session->evaluateScript('drupalSettings.ajaxPageState.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.
$libraries = $session->evaluateScript('drupalSettings.ajaxPageState.libraries');
$this->assertStringContainsString($fake_library, $libraries);
// Reload the page, this should reset the loaded libraries and remove the
// fake library.
$this->drupalGet('ajax-test/dialog');
$libraries = $session->evaluateScript('drupalSettings.ajaxPageState.libraries');
$this->assertStringNotContainsString($fake_library, $libraries);
// Click on the AJAX link again, and the libraries should still not contain
// the fake library.
$this->clickLink('Link 8 (ajax)');
$assert->assertWaitOnAjaxRequest();
$libraries = $session->evaluateScript('drupalSettings.ajaxPageState.libraries');
$this->assertStringNotContainsString($fake_library, $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"/></svg>',
'empty' => '',
];
$render_multiple_root_unwrapper = [
'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_unwrapper 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_unwrapper
);
// 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);
}
/**
* Assert insert.
*
* @param string $render_type
* Render type.
* @param string $expected
* Expected result.
* @param string $script
* Script for additional theming.
*/
public function assertInsert($render_type, $expected, $script = '') {
// 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.
*/
protected function assertWaitPageContains($expected) {
$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");
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the compatibility of the ajax.es6.js file.
*
* @group Ajax
*/
class BackwardCompatibilityTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'js_ajax_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures Drupal.Ajax.element_settings BC layer.
*/
public function testAjaxBackwardCompatibility() {
$this->drupalGet('/js_ajax_test');
$this->click('#edit-test-button');
$this->assertSession()
->waitForElement('css', '#js_ajax_test_form_element');
$elements = $this->cssSelect('#js_ajax_test_form_element');
$this->assertCount(1, $elements);
$json = $elements[0]->getText();
$data = json_decode($json, TRUE);
$this->assertEquals([
'element_settings' => 'catbro',
'elementSettings' => 'catbro',
], $data);
}
}
@@ -0,0 +1,157 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on AJAX framework commands.
*
* @group Ajax
*/
class CommandsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public 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');
// 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 'testvalue' === $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>');
}
/**
* Asserts that page contains a text after waiting.
*
* @param string $text
* A needle text.
*/
protected function assertWaitPageContains($text) {
$page = $this->getSession()->getPage();
$page->waitFor(10, function () use ($page, $text) {
return stripos($page->getContent(), $text) !== FALSE;
});
$this->assertStringContainsString($text, $page->getContent());
}
}
@@ -0,0 +1,192 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\ajax_test\Controller\AjaxTestController;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on opening and manipulating dialogs via AJAX commands.
*
* @group Ajax
*/
class DialogTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test', 'ajax_forms_test', 'contact'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* Test sending non-JS and AJAX requests to open and manipulate modals.
*/
public function testDialog() {
$this->drupalLogin($this->drupalCreateUser(['administer contact forms']));
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// Set up variables for this test.
$dialog_renderable = AjaxTestController::dialogContents();
$dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
// Check that requesting a modal dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertSession()->responseContains($dialog_contents);
// Visit the page containing the many test dialog links.
$this->drupalGet('ajax-test/dialog');
// Tests a basic modal dialog by verifying the contents of the dialog are as
// expected.
$this->getSession()->getPage()->clickLink('Link 1 (modal)');
// Clicking the link triggers a AJAX request/response.
// Opens a Dialog panel.
$link1_dialog_div = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($link1_dialog_div, 'Link was used to open a dialog ( modal )');
$link1_modal = $link1_dialog_div->find('css', '#drupal-modal');
$this->assertNotNull($link1_modal, 'Link was used to open a dialog ( non-modal )');
$this->assertSession()->responseContains($dialog_contents);
$dialog_title = $link1_dialog_div->find('css', "span.ui-dialog-title:contains('AJAX Dialog & contents')");
$this->assertNotNull($dialog_title);
$dialog_title_amp = $link1_dialog_div->find('css', "span.ui-dialog-title:contains('AJAX Dialog &amp; contents')");
$this->assertNull($dialog_title_amp);
// Close open dialog, return to the dialog links page.
$close_button = $link1_dialog_div->findButton('Close');
$this->assertNotNull($close_button);
$close_button->press();
// Tests a modal with a dialog-option.
// Link 2 is similar to Link 1, except it submits additional width
// information which must be echoed in the resulting DOM update.
$this->getSession()->getPage()->clickLink('Link 2 (modal)');
$dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($dialog, 'Link was used to open a dialog ( non-modal, with options )');
$style = $dialog->getAttribute('style');
$this->assertStringContainsString('width: 400px;', $style, new FormattableMarkup('Modal respected the dialog-options width parameter. Style = style', ['%style' => $style]));
// Reset: Return to the dialog links page.
$this->drupalGet('ajax-test/dialog');
// Test a non-modal dialog ( with target ).
$this->clickLink('Link 3 (non-modal)');
$non_modal_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($non_modal_dialog, 'Link opens a non-modal dialog.');
// Tests the dialog contains a target element specified in the AJAX request.
$non_modal_dialog->find('css', 'div#ajax-test-dialog-wrapper-1');
$this->assertSession()->responseContains($dialog_contents);
// Reset: Return to the dialog links page.
$this->drupalGet('ajax-test/dialog');
// Tests a non-modal dialog ( without target ).
$this->clickLink('Link 7 (non-modal, no target)');
$no_target_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($no_target_dialog, 'Link opens a non-modal dialog.');
$contents_no_target = $no_target_dialog->find('css', 'div.ui-dialog-content');
$this->assertNotNull($contents_no_target, 'non-modal dialog opens ( no target ). ');
$id = $contents_no_target->getAttribute('id');
$partial_match = strpos($id, 'drupal-dialog-ajax-testdialog-contents') === 0;
$this->assertTrue($partial_match, 'The non-modal ID has the expected prefix.');
$no_target_button = $no_target_dialog->findButton('Close');
$this->assertNotNull($no_target_button, 'Link dialog has a close button');
$no_target_button->press();
$this->getSession()->getPage()->findButton('Button 1 (modal)')->press();
$button1_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($button1_dialog, 'Button opens a modal dialog.');
$button1_dialog_content = $button1_dialog->find('css', 'div.ui-dialog-content');
$this->assertNotNull($button1_dialog_content, 'Button opens a modal dialog.');
// Test the HTML escaping of & character.
$button1_dialog_title = $button1_dialog->find('css', "span.ui-dialog-title:contains('AJAX Dialog & contents')");
$this->assertNotNull($button1_dialog_title);
$button1_dialog_title_amp = $button1_dialog->find('css', "span.ui-dialog-title:contains('AJAX Dialog &amp; contents')");
$this->assertNull($button1_dialog_title_amp);
// Reset: Close the dialog.
$button1_dialog->findButton('Close')->press();
// Abbreviated test for "normal" dialogs, testing only the difference.
$this->getSession()->getPage()->findButton('Button 2 (non-modal)')->press();
$button2_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog-content');
$this->assertNotNull($button2_dialog, 'Non-modal content displays as expected.');
// Use a link to close the pagnel opened by button 2.
$this->getSession()->getPage()->clickLink('Link 4 (close non-modal if open)');
// Form modal.
$this->clickLink('Link 5 (form)');
// Two links have been clicked in succession - This time wait for a change
// in the title as the previous closing dialog may temporarily be open.
$form_dialog_title = $this->assertSession()->waitForElementVisible('css', "span.ui-dialog-title:contains('Ajax Form contents')");
$this->assertNotNull($form_dialog_title, 'Dialog form has the expected title.');
// Locate the newly opened dialog.
$form_dialog = $this->getSession()->getPage()->find('css', 'div.ui-dialog');
$this->assertNotNull($form_dialog, 'Form dialog is visible');
$form_contents = $form_dialog->find('css', "p:contains('Ajax Form contents description.')");
$this->assertNotNull($form_contents, 'For has the expected text.');
$do_it = $form_dialog->findButton('Do it');
$this->assertNotNull($do_it, 'The dialog has a "Do it" button.');
$preview = $form_dialog->findButton('Preview');
$this->assertNotNull($preview, 'The dialog contains a "Preview" button.');
// When a form with submit inputs is in a dialog, the form's submit inputs
// are copied to the dialog buttonpane as buttons. The originals should have
// their styles set to display: none.
$hidden_buttons = $this->getSession()->getPage()->findAll('css', '.ajax-test-form [type="submit"]');
$this->assertCount(2, $hidden_buttons);
$hidden_button_text = [];
foreach ($hidden_buttons as $button) {
$styles = $button->getAttribute('style');
$this->assertTrue((stripos($styles, 'display: none;') !== FALSE));
$hidden_button_text[] = $button->getAttribute('value');
}
// The copied buttons should have the same text as the submit inputs they
// were copied from.
$moved_to_buttonpane_buttons = $this->getSession()->getPage()->findAll('css', '.ui-dialog-buttonpane button');
$this->assertCount(2, $moved_to_buttonpane_buttons);
foreach ($moved_to_buttonpane_buttons as $key => $button) {
$this->assertEqual($button->getText(), $hidden_button_text[$key]);
}
// Reset: close the form.
$form_dialog->findButton('Close')->press();
// Non AJAX version of Link 6.
$this->drupalGet('admin/structure/contact/add');
// Check we get a chunk of the code, we can't test the whole form as form
// build id and token with be different.
$contact_form = $this->xpath("//form[@id='contact-form-add-form']");
$this->assertTrue(!empty($contact_form), 'Non-JS entity form page present.');
// Reset: Return to the dialog links page.
$this->drupalGet('ajax-test/dialog');
$this->clickLink('Link 6 (entity form)');
$dialog_add = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($dialog_add, 'Form dialog is visible');
$form_add = $dialog_add->find('css', 'form.contact-form-add-form');
$this->assertNotNull($form_add, 'Modal dialog JSON contains entity form.');
$form_title = $dialog_add->find('css', "span.ui-dialog-title:contains('Add contact form')");
$this->assertNotNull($form_title, 'The add form title is as expected.');
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Various tests of AJAX behavior.
*
* @group Ajax
*/
class ElementValidationTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* Tries to post an Ajax change to a form that has a validated element.
*
* Drupal AJAX commands update the DOM echoing back the validated values in
* the form of messages that appear on the page.
*/
public function testAjaxElementValidation() {
$this->drupalGet('ajax_validation_test');
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
// Partially complete the form with a string.
$page->fillField('drivertext', 'some dumb text');
// Move focus away from this field to trigger AJAX.
$page->findField('spare_required_field')->focus();
// 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_text = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('some dumb text')");
$this->assertNotNull($placeholder_text, 'A callback successfully echoed back a string.');
$this->drupalGet('ajax_validation_test');
// Partially complete the form with a number.
$page->fillField('drivernumber', '12345');
$page->findField('spare_required_field')->focus();
// The AJAX request/resonse will complete successfully when a InsertCommand
// injects a message with a placeholder element into the DOM with the
// submitted number.
$placeholder_number = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('12345')");
$this->assertNotNull($placeholder_number, 'A callback successfully echoed back a number.');
}
}
@@ -0,0 +1,89 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests that form values are properly delivered to AJAX callbacks.
*
* @group Ajax
*/
class FormValuesTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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');
$session = $this->getSession();
$assertSession = $this->assertSession();
// Verify form values of a select element.
foreach (['green', 'blue', 'red'] as $item) {
// Updating the field will trigger a AJAX request/response.
$session->getPage()->selectFieldOption('select', $item);
// The AJAX command in the response will update the DOM
$select = $assertSession->waitForElement('css', "div#ajax_selected_color:contains('$item')");
$this->assertNotNull($select, "DataCommand has updated the page with a value of $item.");
}
// Verify form values of a checkbox element.
$session->getPage()->checkField('checkbox');
$div0 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('checked')");
$this->assertNotNull($div0, 'DataCommand updates the DOM as expected when a checkbox is selected');
$session->getPage()->uncheckField('checkbox');
$div1 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('unchecked')");
$this->assertNotNull($div1, 'DataCommand updates the DOM as expected when a checkbox is de-selected');
// Verify that AJAX elements with invalid callbacks return error code 500.
// Ensure the test error log is empty before these tests.
$this->assertFileNotExists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
// We don't need to check for the X-Drupal-Ajax-Token header with these
// invalid requests.
$this->assertAjaxHeader = FALSE;
foreach (['null', 'empty', 'nonexistent'] as $key) {
$element_name = 'select_' . $key . '_callback';
// Updating the field will trigger a AJAX request/response.
$session->getPage()->selectFieldOption($element_name, 'green');
// The select element is disabled as the AJAX request is issued.
$this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:disabled");
// The select element is enabled as the response is receieved.
$this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:enabled");
$this->assertFileExists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
$this->assertStringContainsString('"The specified #ajax callback is empty or not callable."', file_get_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'));
// The exceptions are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
}
// We need to reload the page to kill any unfinished AJAX calls before
// tearDown() is called.
$this->drupalGet('ajax_forms_test_get_form');
}
}
@@ -0,0 +1,129 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests adding messages via AJAX command.
*
* @group Ajax
*/
class MessageCommandTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['ajax_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test AJAX MessageCommand use in a form.
*/
public function testMessageCommand() {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('ajax-test/message');
$page->pressButton('Make Message In Default Location');
$this->waitForMessageVisible('I am a message in the default location.');
$this->assertAnnounceContains('I am a message in the default location.');
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
$page->pressButton('Make Message In Alternate Location');
$this->waitForMessageVisible('I am a message in an alternate location.', '#alternate-message-container');
$assert_session->pageTextContains('I am a message in the default location.');
$this->assertAnnounceContains('I am a message in an alternate location.');
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
$assert_session->elementsCount('css', '#alternate-message-container .messages', 1);
$page->pressButton('Make Warning Message');
$this->waitForMessageVisible('I am a warning message in the default location.', NULL, 'warning');
$assert_session->pageTextNotContains('I am a message in the default location.');
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
$assert_session->elementsCount('css', '#alternate-message-container .messages', 1);
$this->drupalGet('ajax-test/message');
// Test that by default, previous messages in a location are removed.
for ($i = 0; $i < 6; $i++) {
$page->pressButton('Make Message In Default Location');
$this->waitForMessageVisible('I am a message in the default location.');
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
$page->pressButton('Make Warning Message');
$this->waitForMessageVisible('I am a warning message in the default location.', NULL, 'warning');
// Test that setting MessageCommand::$option['announce'] => '' supresses
// screen reader announcement.
$this->assertAnnounceNotContains('I am a warning message in the default location.');
$this->waitForMessageRemoved('I am a message in the default location.');
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
}
// Test that if MessageCommand::clearPrevious is FALSE, messages will not
// be cleared.
$this->drupalGet('ajax-test/message');
for ($i = 1; $i < 7; $i++) {
$page->pressButton('Make Message In Alternate Location');
$expected_count = $page->waitFor(10, function () use ($i, $page) {
return count($page->findAll('css', '#alternate-message-container .messages')) === $i;
});
$this->assertTrue($expected_count);
$this->assertAnnounceContains('I am a message in an alternate location.');
}
}
/**
* Asserts that a message of the expected type appears.
*
* @param string $message
* The expected message.
* @param string $selector
* The selector for the element in which to check for the expected message.
* @param string $type
* The expected type.
*/
protected function waitForMessageVisible($message, $selector = '[data-drupal-messages]', $type = 'status') {
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', $selector . ' .messages--' . $type . ':contains("' . $message . '")'));
}
/**
* Asserts that a message of the expected type is removed.
*
* @param string $message
* The expected message.
* @param string $selector
* The selector for the element in which to check for the expected message.
* @param string $type
* The expected type.
*/
protected function waitForMessageRemoved($message, $selector = '[data-drupal-messages]', $type = 'status') {
$this->assertNotEmpty($this->assertSession()->waitForElementRemoved('css', $selector . ' .messages--' . $type . ':contains("' . $message . '")'));
}
/**
* Checks for inclusion of text in #drupal-live-announce.
*
* @param string $expected_message
* The text expected to be present in #drupal-live-announce.
*/
protected function assertAnnounceContains($expected_message) {
$assert_session = $this->assertSession();
$this->assertNotEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')"));
}
/**
* Checks for absence of the given text from #drupal-live-announce.
*
* @param string $expected_message
* The text expected to be absent from #drupal-live-announce.
*/
protected function assertAnnounceNotContains($expected_message) {
$assert_session = $this->assertSession();
$this->assertEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')", 1000));
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests that AJAX-enabled forms work when multiple instances of the same form
* are on a page.
*
* @group Ajax
*/
class MultiFormTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'form_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
// Create a multi-valued field for 'page' nodes to use for Ajax testing.
$field_name = 'field_ajax_test';
FieldStorageConfig::create([
'entity_type' => 'node',
'field_name' => $field_name,
'type' => 'text',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => 'page',
])->save();
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'page', 'default')
->setComponent($field_name, ['type' => 'text_textfield'])
->save();
// Log in a user who can create 'page' nodes.
$this->drupalLogin($this->drupalCreateUser(['create page content']));
}
/**
* Tests that pages with the 'node_page_form' included twice work correctly.
*/
public function testMultiForm() {
// HTML IDs for elements within the field are potentially modified with
// each Ajax submission, but these variables are stable and help target the
// desired elements.
$field_name = 'field_ajax_test';
$form_xpath = '//form[starts-with(@id, "node-page-form")]';
$field_xpath = '//div[contains(@class, "field--name-field-ajax-test")]';
$button_name = $field_name . '_add_more';
$button_value = t('Add another item');
$button_xpath_suffix = '//input[@name="' . $button_name . '"]';
$field_items_xpath_suffix = '//input[@type="text"]';
// Ensure the initial page contains both node forms and the correct number
// of field items and "add more" button for the multi-valued field within
// each form.
$this->drupalGet('form-test/two-instances-of-same-form');
// Wait for javascript on the page to prepare the form attributes.
$this->assertSession()->assertWaitOnAjaxRequest();
$session = $this->getSession();
$page = $session->getPage();
$fields = $page->findAll('xpath', $form_xpath . $field_xpath);
$this->assertCount(2, $fields);
foreach ($fields as $field) {
$this->assertCount(1, $field->findAll('xpath', '.' . $field_items_xpath_suffix), 'Found the correct number of field items on the initial page.');
$this->assertFieldsByValue($field->find('xpath', '.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
}
$this->assertNoDuplicateIds();
// Submit the "add more" button of each form twice. After each corresponding
// page update, ensure the same as above.
for ($i = 0; $i < 2; $i++) {
$forms = $page->find('xpath', $form_xpath);
foreach ($forms as $offset => $form) {
$button = $form->findButton($button_value);
$this->assertNotNull($button, 'Add Another Item button exists');
$button->press();
// Wait for page update.
$this->assertSession()->assertWaitOnAjaxRequest();
// After AJAX request and response page will update.
$page_updated = $session->getPage();
$field = $page_updated->findAll('xpath', '.' . $field_xpath);
$this->assertCount($i + 2, $field[0]->find('xpath', '.' . $field_items_xpath_suffix), 'Found the correct number of field items after an AJAX submission.');
$this->assertFieldsByValue($field[0]->find('xpath', '.' . $button_xpath_suffix), NULL, 'Found the "add more" button after an AJAX submission.');
$this->assertNoDuplicateIds();
}
}
}
/**
* Asserts that each HTML ID is used for just a single element on the page.
*
* @param string $message
* (optional) A message to display with the assertion.
*/
protected function assertNoDuplicateIds($message = '') {
$args = ['@url' => $this->getUrl()];
if (!$elements = $this->xpath('//*[@id]')) {
$this->fail(new FormattableMarkup('The page @url contains no HTML IDs.', $args));
return;
}
$message = $message ?: new FormattableMarkup('The page @url does not contain duplicate HTML IDs', $args);
$seen_ids = [];
foreach ($elements as $element) {
$id = $element->getAttribute('id');
if (isset($seen_ids[$id])) {
$this->fail($message);
return;
}
$seen_ids[$id] = TRUE;
}
$this->assertTrue(TRUE, $message);
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the throbber.
*
* @group Ajax
*/
class ThrobberTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'views',
'views_ui',
'views_ui_test_field',
'hold_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer views',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests theming throbber element.
*/
public function testThemingThrobberElement() {
$session = $this->getSession();
$web_assert = $this->assertSession();
$page = $session->getPage();
$custom_ajax_progress_indicator_fullscreen = <<<JS
Drupal.theme.ajaxProgressIndicatorFullscreen = function () {
return '<div class="custom-ajax-progress-fullscreen"></div>';
};
JS;
$custom_ajax_progress_throbber = <<<JS
Drupal.theme.ajaxProgressThrobber = function (message) {
return '<div class="custom-ajax-progress-throbber"></div>';
};
JS;
$custom_ajax_progress_message = <<<JS
Drupal.theme.ajaxProgressMessage = function (message) {
return '<div class="custom-ajax-progress-message">Hold door!</div>';
};
JS;
$this->drupalGet('admin/structure/views/view/content');
$web_assert->assertNoElementAfterWait('css', '.ajax-progress-fullscreen');
// Test theming fullscreen throbber.
$session->executeScript($custom_ajax_progress_indicator_fullscreen);
hold_test_response(TRUE);
$page->clickLink('Content: Published (grouped)');
$this->assertNotNull($web_assert->waitForElement('css', '.custom-ajax-progress-fullscreen'), 'Custom ajaxProgressIndicatorFullscreen.');
hold_test_response(FALSE);
$web_assert->assertNoElementAfterWait('css', '.custom-ajax-progress-fullscreen');
// Test theming throbber message.
$web_assert->waitForElementVisible('css', '[data-drupal-selector="edit-options-group-info-add-group"]');
$session->executeScript($custom_ajax_progress_message);
hold_test_response(TRUE);
$page->pressButton('Add another item');
$this->assertNotNull($web_assert->waitForElement('css', '.ajax-progress-throbber .custom-ajax-progress-message'), 'Custom ajaxProgressMessage.');
hold_test_response(FALSE);
$web_assert->assertNoElementAfterWait('css', '.ajax-progress-throbber');
// Test theming throbber.
$web_assert->waitForElementVisible('css', '[data-drupal-selector="edit-options-group-info-group-items-3-title"]');
$session->executeScript($custom_ajax_progress_throbber);
hold_test_response(TRUE);
$page->pressButton('Add another item');
$this->assertNotNull($web_assert->waitForElement('css', '.custom-ajax-progress-throbber'), 'Custom ajaxProgressThrobber.');
hold_test_response(FALSE);
$web_assert->assertNoElementAfterWait('css', '.custom-ajax-progress-throbber');
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Driver\GoutteDriver;
use PHPUnit\Framework\AssertionFailedError;
/**
* Tests if we can execute JavaScript in the browser.
*
* @group javascript
*/
class BrowserWithJavascriptTest extends WebDriverTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testJavascript() {
$this->drupalGet('<front>');
$session = $this->getSession();
$session->resizeWindow(400, 300);
$javascript = <<<JS
(function(){
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
x = w.innerWidth || e.clientWidth || g.clientWidth,
y = w.innerHeight || e.clientHeight|| g.clientHeight;
return x == 400 && y == 300;
}());
JS;
$this->assertJsCondition($javascript);
}
public function testAssertJsCondition() {
$this->drupalGet('<front>');
$session = $this->getSession();
$session->resizeWindow(500, 300);
$javascript = <<<JS
(function(){
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
x = w.innerWidth || e.clientWidth || g.clientWidth,
y = w.innerHeight || e.clientHeight|| g.clientHeight;
return x == 400 && y == 300;
}());
JS;
// We expected the following assertion to fail because the window has been
// re-sized to have a width of 500 not 400.
$this->expectException(AssertionFailedError::class);
$this->assertJsCondition($javascript, 100);
}
/**
* Tests creating screenshots.
*/
public function testCreateScreenshot() {
$this->drupalGet('<front>');
$this->createScreenshot('public://screenshot.jpg');
$this->assertFileExists('public://screenshot.jpg');
}
/**
* Tests assertEscaped() and assertUnescaped().
*
* @see \Drupal\Tests\WebAssert::assertNoEscaped()
* @see \Drupal\Tests\WebAssert::assertEscaped()
*/
public function testEscapingAssertions() {
$assert = $this->assertSession();
$this->drupalGet('test-escaped-characters');
$assert->assertNoEscaped('<div class="escaped">');
$assert->responseContains('<div class="escaped">');
$assert->assertEscaped('Escaped: <"\'&>');
$this->drupalGet('test-escaped-script');
$assert->assertNoEscaped('<div class="escaped">');
$assert->responseContains('<div class="escaped">');
$assert->assertEscaped("<script>alert('XSS');alert(\"XSS\");</script>");
$this->drupalGetWithAlert('test-unescaped-script');
$assert->assertNoEscaped('<div class="unescaped">');
$assert->responseContains('<div class="unescaped">');
$assert->responseContains("<script>alert('Marked safe');alert(\"Marked safe\");</script>");
$assert->assertNoEscaped("<script>alert('Marked safe');alert(\"Marked safe\");</script>");
}
/**
* Retrieves a Drupal path or an absolute path.
*
* @param string|\Drupal\Core\Url $path
* Drupal path or URL to load into Mink controlled browser.
* @param array $options
* (optional) Options to be forwarded to the url generator.
* @param string[] $headers
* An array containing additional HTTP request headers, the array keys are
* the header names and the array values the header values. This is useful
* to set for example the "Accept-Language" header for requesting the page
* in a different language. Note that not all headers are supported, for
* example the "Accept" header is always overridden by the browser. For
* testing REST APIs it is recommended to obtain a separate HTTP client
* using getHttpClient() and performing requests that way.
*
* @return string
* The retrieved HTML string, also available as $this->getRawContent()
*
* @see \Drupal\Tests\BrowserTestBase::getHttpClient()
*/
protected function drupalGetWithAlert($path, array $options = [], array $headers = []) {
$options['absolute'] = TRUE;
$url = $this->buildUrl($path, $options);
$session = $this->getSession();
$this->prepareRequest();
foreach ($headers as $header_name => $header_value) {
$session->setRequestHeader($header_name, $header_value);
}
$session->visit($url);
// There are 2 alerts to accept before we can get the content of the page.
$session->getDriver()->getWebdriverSession()->accept_alert();
$session->getDriver()->getWebdriverSession()->accept_alert();
$out = $session->getPage()->getContent();
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
// Replace original page output with new output from redirected page(s).
if ($new = $this->checkForMetaRefresh()) {
$out = $new;
// We are finished with all meta refresh redirects, so reset the counter.
$this->metaRefreshCount = 0;
}
// Log only for JavascriptTestBase tests because for Goutte we log with
// ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
$html_output = 'GET request to: ' . $url .
'<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
$html_output .= '<hr />' . $out;
$html_output .= $this->getHtmlOutputHeaders();
$this->htmlOutput($html_output);
}
return $out;
}
}
@@ -0,0 +1,61 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Test race condition for CSRF tokens for simultaneous requests.
*
* @group Session
*/
class CsrfTokenRaceTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['csrf_race_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test race condition for CSRF tokens for simultaneous requests.
*/
public function testCsrfRace() {
$user = $this->createUser(['access content']);
$this->drupalLogin($user);
$this->drupalGet('/csrf_race/test');
$script = '';
// Delay the request processing of the first request by one second through
// the request parameter, which will simulate the concurrent processing
// of both requests.
foreach ([1, 0] as $i) {
$script .= <<<EOT
jQuery.ajax({
url: "$this->baseUrl/csrf_race/get_csrf_token/$i",
method: "GET",
headers: {
"Content-Type": "application/json"
},
success: function(response) {
jQuery('body').append("<p class='csrf$i'></p>");
jQuery('.csrf$i').html(response);
},
error: function() {
jQuery('body').append('Nothing');
}
});
EOT;
}
$this->getSession()->getDriver()->executeScript($script);
$token0 = $this->assertSession()->waitForElement('css', '.csrf0')->getHtml();
$token1 = $this->assertSession()->waitForElement('css', '.csrf1')->getHtml();
$this->assertNotNull($token0);
$this->assertNotNull($token1);
$this->assertEqual($token0, $token1);
}
}
@@ -0,0 +1,138 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core\Form;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests for form grouping elements.
*
* @group form
*/
class FormGroupingElementsTest extends WebDriverTestBase {
/**
* Required modules.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
}
/**
* Tests that vertical tab children become visible.
*
* Makes sure that a child element of a vertical tab that is not visible,
* becomes visible when the tab is clicked, a fragment link to the child is
* clicked or when the URI fragment pointing to that child changes.
*/
public function testVerticalTabChildVisibility() {
$session = $this->getSession();
$web_assert = $this->assertSession();
// Request the group vertical tabs testing page with a fragment identifier
// to the second element.
$this->drupalGet('form-test/group-vertical-tabs', ['fragment' => 'edit-element-2']);
$page = $session->getPage();
$tab_link_1 = $page->find('css', '.vertical-tabs__menu-item > a');
$child_1_selector = '#edit-element';
$child_1 = $page->find('css', $child_1_selector);
$child_2_selector = '#edit-element-2';
$child_2 = $page->find('css', $child_2_selector);
// Assert that the child in the second vertical tab becomes visible.
// It should be visible after initial load due to the fragment in the URI.
$this->assertTrue($child_2->isVisible(), 'Child 2 is visible due to a URI fragment');
// Click on a fragment link pointing to an invisible child inside an
// inactive vertical tab.
$session->executeScript("jQuery('<a href=\"$child_1_selector\"></a>').insertAfter('h1')[0].click()");
// Assert that the child in the first vertical tab becomes visible.
$web_assert->waitForElementVisible('css', $child_1_selector, 50);
// Trigger a URI fragment change (hashchange) to show the second vertical
// tab again.
$session->executeScript("location.replace('$child_2_selector')");
// Assert that the child in the second vertical tab becomes visible again.
$web_assert->waitForElementVisible('css', $child_2_selector, 50);
$tab_link_1->click();
// Assert that the child in the first vertical tab is visible again after
// a click on the first tab.
$this->assertTrue($child_1->isVisible(), 'Child 1 is visible after clicking the parent tab');
}
/**
* Tests that details element children become visible.
*
* Makes sure that a child element of a details element that is not visible,
* becomes visible when a fragment link to the child is clicked or when the
* URI fragment pointing to that child changes.
*/
public function testDetailsChildVisibility() {
$session = $this->getSession();
$web_assert = $this->assertSession();
// Store reusable JavaScript code to remove the current URI fragment and
// close all details.
$reset_js = "location.replace('#'); jQuery('details').removeAttr('open')";
// Request the group details testing page.
$this->drupalGet('form-test/group-details');
$page = $session->getPage();
$session->executeScript($reset_js);
$child_selector = '#edit-element';
$child = $page->find('css', $child_selector);
// Assert that the child is not visible.
$this->assertFalse($child->isVisible(), 'Child is not visible');
// Trigger a URI fragment change (hashchange) to open all parent details
// elements of the child.
$session->executeScript("location.replace('$child_selector')");
// Assert that the child becomes visible again after a hash change.
$web_assert->waitForElementVisible('css', $child_selector, 50);
$session->executeScript($reset_js);
// Click on a fragment link pointing to an invisible child inside a closed
// details element.
$session->executeScript("jQuery('<a href=\"$child_selector\"></a>').insertAfter('h1')[0].click()");
// Assert that the child is visible again after a fragment link click.
$web_assert->waitForElementVisible('css', $child_selector, 50);
// Find the summary belonging to the closest details element.
$summary = $page->find('css', '#edit-meta > summary');
// Assert that both aria-expanded and aria-pressed are true.
$this->assertEquals('true', $summary->getAttribute('aria-expanded'));
$this->assertEquals('true', $summary->getAttribute('aria-pressed'));
}
}
@@ -0,0 +1,132 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core\Installer\Form;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use GuzzleHttp\HandlerStack;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Tests the select profile form.
*
* @group Installer
*/
class SelectProfileFormTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public function setUp() {
$this->setupBaseUrl();
$this->prepareDatabasePrefix();
// Install Drupal test site.
$this->prepareEnvironment();
// 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);
$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
->set('app.root', DRUPAL_ROOT);
\Drupal::setContainer($this->container);
// Setup Mink.
$this->initMink();
}
/**
* {@inheritdoc}
*/
protected function initMink() {
// The temporary files directory doesn't exist yet, as install_base_system()
// has not run. We need to create the template cache directory recursively.
$path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache';
if (!file_exists($path)) {
mkdir($path, 0777, TRUE);
}
parent::initMink();
}
/**
* {@inheritdoc}
*
* BrowserTestBase::refreshVariables() tries to operate on persistent storage,
* which is only available after the installer completed.
*/
protected function refreshVariables() {
// Intentionally empty as the site is not yet installed.
}
/**
* Tests a warning message is displayed when the Umami profile is selected.
*/
public function testUmamiProfileWarningMessage() {
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php');
$edit = [
'langcode' => 'en',
];
$this->drupalPostForm(NULL, $edit, 'Save and continue');
$page = $this->getSession()->getPage();
$warning_message = $page->find('css', '.description .messages--warning');
$this->assertFalse($warning_message->isVisible());
$page->selectFieldOption('profile', 'demo_umami');
$this->assertTrue($warning_message->isVisible());
}
}
@@ -0,0 +1,124 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\js_message_test\Controller\JSMessageTestController;
/**
* Tests core/drupal.message library.
*
* @group Javascript
*/
class JsMessageTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['js_message_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Enable the theme.
\Drupal::service('theme_installer')->install(['test_messages']);
$theme_config = \Drupal::configFactory()->getEditable('system.theme');
$theme_config->set('default', 'test_messages');
$theme_config->save();
}
/**
* Test click on links to show messages and remove messages.
*/
public function testAddRemoveMessages() {
$web_assert = $this->assertSession();
$this->drupalGet('js_message_test_link');
$current_messages = [];
foreach (JSMessageTestController::getMessagesSelectors() as $messagesSelector) {
$web_assert->elementExists('css', $messagesSelector);
foreach (JSMessageTestController::getTypes() as $type) {
$this->click('[id="add-' . $messagesSelector . '-' . $type . '"]');
$selector = "$messagesSelector .messages.messages--$type";
$msg_element = $web_assert->waitForElementVisible('css', $selector);
$this->assertNotEmpty($msg_element, "Message element visible: $selector");
$web_assert->elementContains('css', $selector, "This is a message of the type, $type. You be the judge of its importance.");
$current_messages[$selector] = "This is a message of the type, $type. You be the judge of its importance.";
$this->assertCurrentMessages($current_messages, $messagesSelector);
}
// Remove messages 1 by 1 and confirm the messages are expected.
foreach (JSMessageTestController::getTypes() as $type) {
$this->click('[id="remove-' . $messagesSelector . '-' . $type . '"]');
$selector = "$messagesSelector .messages.messages--$type";
// The message for this selector should not be on the page.
unset($current_messages[$selector]);
$this->assertCurrentMessages($current_messages, $messagesSelector);
}
}
$messagesSelector = JSMessageTestController::getMessagesSelectors()[0];
$current_messages = [];
$types = JSMessageTestController::getTypes();
$nb_messages = count($types) * 2;
for ($i = 0; $i < $nb_messages; $i++) {
$current_messages[] = "This is message number $i of the type, {$types[$i % count($types)]}. You be the judge of its importance.";
}
// Test adding multiple messages at once.
// @see processMessages()
$this->click('[id="add-multiple"]');
$this->assertCurrentMessages($current_messages, $messagesSelector);
$this->click('[id="remove-multiple"]');
$this->assertCurrentMessages([], $messagesSelector);
$current_messages = [];
for ($i = 0; $i < $nb_messages; $i++) {
$current_messages[] = "Msg-$i";
}
// The last message is of a different type and shouldn't get cleared.
$last_message = 'Msg-' . count($current_messages);
$current_messages[] = $last_message;
$this->click('[id="add-multiple-error"]');
$this->assertCurrentMessages($current_messages, $messagesSelector);
$this->click('[id="remove-type"]');
$this->assertCurrentMessages([$last_message], $messagesSelector);
$this->click('[id="clear-all"]');
$this->assertCurrentMessages([], $messagesSelector);
// Confirm that when adding a message with an "id" specified but no status
// that it receives the default status.
$this->click('[id="id-no-status"]');
$no_status_msg = 'Msg-id-no-status';
$this->assertCurrentMessages([$no_status_msg], $messagesSelector);
$web_assert->elementTextContains('css', "$messagesSelector .messages--status[data-drupal-message-id=\"my-special-id\"]", $no_status_msg);
}
/**
* Asserts that currently shown messages match expected messages.
*
* @param array $expected_messages
* Expected messages.
* @param string $messagesSelector
* The css selector for the containing messages element.
*/
protected function assertCurrentMessages(array $expected_messages, $messagesSelector) {
$expected_messages = array_values($expected_messages);
$current_messages = [];
if ($message_divs = $this->getSession()->getPage()->findAll('css', "$messagesSelector .messages")) {
foreach ($message_divs as $message_div) {
/** @var \Behat\Mink\Element\NodeElement $message_div */
$current_messages[] = $message_div->getText();
}
}
$this->assertEquals($expected_messages, $current_messages);
}
}
@@ -0,0 +1,155 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests for the machine name field.
*
* @group field
*/
class MachineNameTest extends WebDriverTestBase {
/**
* Required modules.
*
* Node is required because the machine name callback checks for
* access_content.
*
* @var array
*/
public static $modules = ['node', 'form_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$account = $this->drupalCreateUser([
'access content',
]);
$this->drupalLogin($account);
}
/**
* Tests that machine name field functions.
*
* Makes sure that the machine name field automatically provides a valid
* machine name and that the manual editing mode functions.
*/
public function testMachineName() {
// Visit the machine name test page which contains two machine name fields.
$this->drupalGet('form-test/machine-name');
// Test values for conversion.
$test_values = [
[
'input' => 'Test value !0-9@',
'message' => 'A title that should be transliterated must be equal to the php generated machine name',
'expected' => 'test_value_0_9_',
],
[
'input' => 'Test value',
'message' => 'A title that should not be transliterated must be equal to the php generated machine name',
'expected' => 'test_value',
],
];
// Get page and session.
$page = $this->getSession()->getPage();
// Get elements from the page.
$title_1 = $page->findField('machine_name_1_label');
$machine_name_1_field = $page->findField('machine_name_1');
$machine_name_2_field = $page->findField('machine_name_2');
$machine_name_1_wrapper = $machine_name_1_field->getParent();
$machine_name_2_wrapper = $machine_name_2_field->getParent();
$machine_name_1_value = $page->find('css', '#edit-machine-name-1-label-machine-name-suffix .machine-name-value');
$machine_name_2_value = $page->find('css', '#edit-machine-name-2-label-machine-name-suffix .machine-name-value');
$machine_name_3_value = $page->find('css', '#edit-machine-name-3-label-machine-name-suffix .machine-name-value');
$button_1 = $page->find('css', '#edit-machine-name-1-label-machine-name-suffix button.link');
// Assert all fields are initialized correctly.
$this->assertNotEmpty($machine_name_1_value, 'Machine name field 1 must be initialized');
$this->assertNotEmpty($machine_name_2_value, 'Machine name field 2 must be initialized');
$this->assertNotEmpty($machine_name_3_value, 'Machine name field 3 must be initialized');
// Assert that a machine name based on a default value is initialized.
$this->assertJsCondition('jQuery("#edit-machine-name-3-label-machine-name-suffix .machine-name-value").html() == "yet_another_machine_name"');
// Field must be present for the rest of the test to work.
if (empty($machine_name_1_value)) {
$this->fail('Cannot finish test, missing machine name field');
}
// Test each value for conversion to a machine name.
foreach ($test_values as $test_info) {
// Set the value for the field, triggering the machine name update.
$title_1->setValue($test_info['input']);
// Wait the set timeout for fetching the machine name.
$this->assertJsCondition('jQuery("#edit-machine-name-1-label-machine-name-suffix .machine-name-value").html() == "' . $test_info['expected'] . '"');
// Validate the generated machine name.
$this->assertEquals($test_info['expected'], $machine_name_1_value->getHtml(), $test_info['message']);
// Validate the second machine name field is empty.
$this->assertEmpty($machine_name_2_value->getHtml(), 'The second machine name field should still be empty');
}
// Validate the machine name field is hidden. Elements are visually hidden
// using positioning, isVisible() will therefore not work.
$this->assertEquals(TRUE, $machine_name_1_wrapper->hasClass('visually-hidden'), 'The ID field must not be visible');
$this->assertEquals(TRUE, $machine_name_2_wrapper->hasClass('visually-hidden'), 'The ID field must not be visible');
// Test switching back to the manual editing mode by clicking the edit link.
$button_1->click();
// Validate the visibility of the machine name field.
$this->assertEquals(FALSE, $machine_name_1_wrapper->hasClass('visually-hidden'), 'The ID field must now be visible');
// Validate the visibility of the second machine name field.
$this->assertEquals(TRUE, $machine_name_2_wrapper->hasClass('visually-hidden'), 'The ID field must not be visible');
// Validate if the element contains the correct value.
$this->assertEquals($test_values[1]['expected'], $machine_name_1_field->getValue(), 'The ID field value must be equal to the php generated machine name');
$assert = $this->assertSession();
$this->drupalGet('/form-test/form-test-machine-name-validation');
// Test errors after with no AJAX.
$assert->buttonExists('Save')->press();
$assert->pageTextContains('Machine-readable name field is required.');
// Ensure only the first machine name field has an error.
$this->assertTrue($assert->fieldExists('id')->hasClass('error'));
$this->assertFalse($assert->fieldExists('id2')->hasClass('error'));
// Test a successful submit after using AJAX.
$assert->fieldExists('Name')->setValue('test 1');
$assert->fieldExists('id')->setValue('test_1');
$assert->selectExists('snack')->selectOption('apple');
$assert->assertWaitOnAjaxRequest();
$assert->buttonExists('Save')->press();
$assert->pageTextContains('The form_test_machine_name_validation_form form has been submitted successfully.');
// Test errors after using AJAX.
$assert->fieldExists('Name')->setValue('duplicate');
$this->assertJsCondition('document.forms[0].id.value === "duplicate"');
$assert->fieldExists('id2')->setValue('duplicate2');
$assert->selectExists('snack')->selectOption('potato');
$assert->assertWaitOnAjaxRequest();
$assert->buttonExists('Save')->press();
$assert->pageTextContains('The machine-readable name is already in use. It must be unique.');
// Ensure both machine name fields both have errors.
$this->assertTrue($assert->fieldExists('id')->hasClass('error'));
$this->assertTrue($assert->fieldExists('id2')->hasClass('error'));
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Core\Session;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests that sessions don't expire.
*
* @group session
*/
class SessionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
$menu_link_content = MenuLinkContent::create([
'title' => 'Link to front page',
'menu_name' => 'tools',
'link' => ['uri' => 'route:<front>'],
]);
$menu_link_content->save();
$this->drupalPlaceBlock('system_menu_block:tools');
}
/**
* Tests that the session doesn't expire.
*
* Makes sure that drupal_valid_test_ua() works for multiple requests
* performed by the Mink browser. The SIMPLETEST_USER_AGENT cookie must always
* be valid.
*/
public function testSessionExpiration() {
// Visit the front page and click the link back to the front page a large
// number of times.
$this->drupalGet('<front>');
$page = $this->getSession()->getPage();
for ($i = 0; $i < 25; $i++) {
$page->clickLink('Link to front page');
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Dialog;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript functionality of the dialog position.
*
* @group dialog
*/
class DialogPositionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if the dialog UI works properly with block layout page.
*/
public function testDialogOpenAndClose() {
$admin_user = $this->drupalCreateUser(['administer blocks']);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/structure/block');
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Open the dialog using the place block link.
$placeBlockLink = $page->findLink('Place block');
$this->assertTrue($placeBlockLink->isVisible(), 'Place block button exists.');
$placeBlockLink->click();
$assert_session->assertWaitOnAjaxRequest();
$dialog = $page->find('css', '.ui-dialog');
$this->assertTrue($dialog->isVisible(), 'Dialog is opened after clicking the Place block button.');
// Close the dialog again.
$closeButton = $page->find('css', '.ui-dialog-titlebar-close');
$closeButton->click();
$assert_session->assertWaitOnAjaxRequest();
$dialog = $page->find('css', '.ui-dialog');
$this->assertNull($dialog, 'Dialog is closed after clicking the close button.');
// Resize the window. The test should pass after waiting for Javascript to
// finish as no Javascript errors should have been triggered. If there were
// javascript errors the test will fail on that.
$session->resizeWindow(625, 625);
$assert_session->assertWaitOnAjaxRequest();
}
}
@@ -0,0 +1,111 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Exception\DriverException;
use WebDriver\Exception\UnknownError;
use WebDriver\ServiceFactory;
/**
* Provides a driver for Selenium testing.
*/
class DrupalSelenium2Driver extends Selenium2Driver {
/**
* {@inheritdoc}
*/
public function __construct($browserName = 'firefox', $desiredCapabilities = NULL, $wdHost = 'http://localhost:4444/wd/hub') {
parent::__construct($browserName, $desiredCapabilities, $wdHost);
ServiceFactory::getInstance()->setServiceClass('service.curl', WebDriverCurlService::class);
}
/**
* {@inheritdoc}
*/
public function setCookie($name, $value = NULL) {
if ($value === NULL) {
$this->getWebDriverSession()->deleteCookie($name);
return;
}
$cookieArray = [
'name' => $name,
'value' => urlencode($value),
'secure' => FALSE,
// Unlike \Behat\Mink\Driver\Selenium2Driver::setCookie we set a domain
// and an expire date, as otherwise cookies leak from one test site into
// another.
'domain' => parse_url($this->getWebDriverSession()->url(), PHP_URL_HOST),
'expires' => time() + 80000,
];
$this->getWebDriverSession()->setCookie($cookieArray);
}
/**
* Uploads a file to the Selenium instance and returns the remote path.
*
* \Behat\Mink\Driver\Selenium2Driver::uploadFile() is a private method so
* that can't be used inside a test, but we need the remote path that is
* generated when uploading to make sure the file reference exists on the
* container running selenium.
*
* @param string $path
* The path to the file to upload.
*
* @return string
* The remote path.
*
* @throws \Behat\Mink\Exception\DriverException
* When PHP is compiled without zip support, or the file doesn't exist.
* @throws \WebDriver\Exception\UnknownError
* When an unknown error occurred during file upload.
* @throws \Exception
* When a known error occurred during file upload.
*/
public function uploadFileAndGetRemoteFilePath($path) {
if (!is_file($path)) {
throw new DriverException('File does not exist locally and cannot be uploaded to the remote instance.');
}
if (!class_exists('ZipArchive')) {
throw new DriverException('Could not compress file, PHP is compiled without zip support.');
}
// Selenium only accepts uploads that are compressed as a Zip archive.
$tempFilename = tempnam('', 'WebDriverZip');
$archive = new \ZipArchive();
$result = $archive->open($tempFilename, \ZipArchive::CREATE);
if (!$result) {
throw new DriverException('Zip archive could not be created. Error ' . $result);
}
$result = $archive->addFile($path, basename($path));
if (!$result) {
throw new DriverException('File could not be added to zip archive.');
}
$result = $archive->close();
if (!$result) {
throw new DriverException('Zip archive could not be closed.');
}
try {
$remotePath = $this->getWebDriverSession()->file(['file' => base64_encode(file_get_contents($tempFilename))]);
// If no path is returned the file upload failed silently.
if (empty($remotePath)) {
throw new UnknownError();
}
}
catch (\Exception $e) {
throw $e;
}
finally {
unlink($tempFilename);
}
return $remotePath;
}
}
@@ -0,0 +1,166 @@
<?php
namespace Drupal\FunctionalJavascriptTests\EntityReference;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
/**
* Tests the output of entity reference autocomplete widgets.
*
* @group entity_reference
*/
class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
use ContentTypeCreationTrait;
use EntityReferenceTestTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a Content type and two test nodes.
$this->createContentType(['type' => 'page']);
$this->createNode(['title' => 'Test page']);
$this->createNode(['title' => 'Page test']);
$user = $this->drupalCreateUser([
'access content',
'create page content',
]);
$this->drupalLogin($user);
}
/**
* Tests that the default autocomplete widget return the correct results.
*/
public function testEntityReferenceAutocompleteWidget() {
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Create an entity reference field and use the default 'CONTAINS' match
// operator.
$field_name = 'field_test';
$this->createEntityReferenceField('node', 'page', $field_name, $field_name, 'node', 'default', ['target_bundles' => ['page'], 'sort' => ['field' => 'title', 'direction' => 'DESC']]);
$form_display = $display_repository->getFormDisplay('node', 'page');
$form_display->setComponent($field_name, [
'type' => 'entity_reference_autocomplete',
'settings' => [
'match_operator' => 'CONTAINS',
],
]);
// To satisfy config schema, the size setting must be an integer, not just
// a numeric value. See https://www.drupal.org/node/2885441.
$this->assertIsInt($form_display->getComponent($field_name)['settings']['size']);
$form_display->save();
$this->assertIsInt($form_display->getComponent($field_name)['settings']['size']);
// Visit the node add page.
$this->drupalGet('node/add/page');
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$autocomplete_field = $assert_session->waitForElement('css', '[name="' . $field_name . '[0][target_id]"].ui-autocomplete-input');
$autocomplete_field->setValue('Test');
$this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
$assert_session->waitOnAutocomplete();
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(2, $results);
$assert_session->pageTextContains('Test page');
$assert_session->pageTextContains('Page test');
// Now switch the autocomplete widget to the 'STARTS_WITH' match operator.
$display_repository->getFormDisplay('node', 'page')
->setComponent($field_name, [
'type' => 'entity_reference_autocomplete',
'settings' => [
'match_operator' => 'STARTS_WITH',
],
])
->save();
$this->drupalGet('node/add/page');
$this->doAutocomplete($field_name);
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(1, $results);
$assert_session->pageTextContains('Test page');
$assert_session->pageTextNotContains('Page test');
// Change the size of the result set.
$display_repository->getFormDisplay('node', 'page')
->setComponent($field_name, [
'type' => 'entity_reference_autocomplete',
'settings' => [
'match_limit' => 1,
],
])
->save();
$this->drupalGet('node/add/page');
$this->doAutocomplete($field_name);
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(1, $results);
$assert_session->pageTextContains('Test page');
$assert_session->pageTextNotContains('Page test');
// Change the size of the result set via the UI.
$this->drupalLogin($this->createUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'create page content',
]
));
$this->drupalGet('/admin/structure/types/manage/page/form-display');
$assert_session->pageTextContains('Autocomplete suggestion list size: 1');
// Click on the widget settings button to open the widget settings form.
$this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
$this->assertSession()->waitForElement('css', sprintf('[name="fields[%s][settings_edit_form][settings][match_limit]"]', $field_name));
$page->fillField('Number of results', 2);
$page->pressButton('Save');
$assert_session->pageTextContains('Your settings have been saved.');
$assert_session->pageTextContains('Autocomplete suggestion list size: 2');
$this->drupalGet('node/add/page');
$this->doAutocomplete($field_name);
$this->assertCount(2, $page->findAll('css', '.ui-autocomplete li'));
}
/**
* Executes an autocomplete on a given field and waits for it to finish.
*
* @param string $field_name
* The field name.
*/
protected function doAutocomplete($field_name) {
$autocomplete_field = $this->getSession()->getPage()->findField($field_name . '[0][target_id]');
$autocomplete_field->setValue('Test');
$this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
$this->assertSession()->waitOnAutocomplete();
}
}
@@ -0,0 +1,492 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementHtmlException;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Drupal\Tests\WebAssert;
/**
* Defines a class with methods for asserting presence of elements during tests.
*/
class JSWebAssert extends WebAssert {
/**
* Waits for AJAX request to be completed.
*
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
* @param string $message
* (optional) A message for exception.
*
* @throws \RuntimeException
* When the request is not completed. If left blank, a default message will
* be displayed.
*/
public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to complete AJAX request.') {
$condition = <<<JS
(function() {
function isAjaxing(instance) {
return instance && instance.ajaxing === true;
}
return (
// Assert no AJAX request is running (via jQuery or Drupal) and no
// animation is running.
(typeof jQuery === 'undefined' || (jQuery.active === 0 && jQuery(':animated').length === 0)) &&
(typeof Drupal === 'undefined' || typeof Drupal.ajax === 'undefined' || !Drupal.ajax.instances.some(isAjaxing))
);
}());
JS;
$result = $this->session->wait($timeout, $condition);
if (!$result) {
throw new \RuntimeException($message);
}
}
/**
* Waits for the specified selector and returns it when available.
*
* @param string $selector
* The selector engine name. See ElementInterface::findAll() for the
* supported selectors.
* @param string|array $locator
* The selector locator.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found, NULL if not.
*
* @see \Behat\Mink\Element\ElementInterface::findAll()
*/
public function waitForElement($selector, $locator, $timeout = 10000) {
$page = $this->session->getPage();
$result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
return $page->find($selector, $locator);
});
return $result;
}
/**
* Looks for the specified selector and returns TRUE when it is unavailable.
*
* @param string $selector
* The selector engine name. See ElementInterface::findAll() for the
* supported selectors.
* @param string|array $locator
* The selector locator.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return bool
* TRUE if not found, FALSE if found.
*
* @see \Behat\Mink\Element\ElementInterface::findAll()
*/
public function waitForElementRemoved($selector, $locator, $timeout = 10000) {
$page = $this->session->getPage();
$result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
return !$page->find($selector, $locator);
});
return $result;
}
/**
* Waits for the specified selector and returns it when available and visible.
*
* @param string $selector
* The selector engine name. See ElementInterface::findAll() for the
* supported selectors.
* @param string|array $locator
* The selector locator.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found and visible, NULL if not.
*
* @see \Behat\Mink\Element\ElementInterface::findAll()
*/
public function waitForElementVisible($selector, $locator, $timeout = 10000) {
$page = $this->session->getPage();
$result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
$element = $page->find($selector, $locator);
if (!empty($element) && $element->isVisible()) {
return $element;
}
return NULL;
});
return $result;
}
/**
* Waits for the specified text and returns its element when available.
*
* @param string $text
* The text to wait for.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found and visible, NULL if not.
*/
public function waitForText($text, $timeout = 10000) {
$page = $this->session->getPage();
return $page->waitFor($timeout / 1000, function () use ($page, $text) {
$actual = preg_replace('/\s+/u', ' ', $page->getText());
$regex = '/' . preg_quote($text, '/') . '/ui';
return (bool) preg_match($regex, $actual);
});
}
/**
* Waits for a button (input[type=submit|image|button|reset], button) with
* specified locator and returns it.
*
* @param string $locator
* The button ID, value or alt string.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found, NULL if not.
*/
public function waitForButton($locator, $timeout = 10000) {
return $this->waitForElement('named', ['button', $locator], $timeout);
}
/**
* Waits for a link with specified locator and returns it when available.
*
* @param string $locator
* The link ID, title, text or image alt.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found, NULL if not.
*/
public function waitForLink($locator, $timeout = 10000) {
return $this->waitForElement('named', ['link', $locator], $timeout);
}
/**
* Waits for a field with specified locator and returns it when available.
*
* @param string $locator
* The input ID, name or label for the field (input, textarea, select).
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found, NULL if not.
*/
public function waitForField($locator, $timeout = 10000) {
return $this->waitForElement('named', ['field', $locator], $timeout);
}
/**
* Waits for an element by its id and returns it when available.
*
* @param string $id
* The element ID.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The page element node if found, NULL if not.
*/
public function waitForId($id, $timeout = 10000) {
return $this->waitForElement('named', ['id', $id], $timeout);
}
/**
* Waits for the jQuery autocomplete delay duration.
*
* @see https://api.jqueryui.com/autocomplete/#option-delay
*/
public function waitOnAutocomplete() {
// Wait for the autocomplete to be visible.
return $this->waitForElementVisible('css', '.ui-autocomplete li');
}
/**
* Test that a node, or its specific corner, is visible in the viewport.
*
* Note: Always set the viewport size. This can be done with a PhantomJS
* startup parameter or in your test with \Behat\Mink\Session->resizeWindow().
* Drupal CI Javascript tests by default use a viewport of 1024x768px.
*
* @param string $selector_type
* The element selector type (CSS, XPath).
* @param string|array $selector
* The element selector. Note: the first found element is used.
* @param bool|string $corner
* (Optional) The corner to test:
* topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
* @param string $message
* (optional) A message for the exception.
*
* @throws \Behat\Mink\Exception\ElementHtmlException
* When the element doesn't exist.
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element is not visible in the viewport.
*/
public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') {
$node = $this->session->getPage()->find($selector_type, $selector);
if ($node === NULL) {
if (is_array($selector)) {
$selector = implode(' ', $selector);
}
throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
}
// Check if the node is visible on the page, which is a prerequisite of
// being visible in the viewport.
if (!$node->isVisible()) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
$result = $this->checkNodeVisibilityInViewport($node, $corner);
if (!$result) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
}
/**
* Test that a node, or its specific corner, is not visible in the viewport.
*
* Note: the node should exist in the page, otherwise this assertion fails.
*
* @param string $selector_type
* The element selector type (CSS, XPath).
* @param string|array $selector
* The element selector. Note: the first found element is used.
* @param bool|string $corner
* (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
* @param string $message
* (optional) A message for the exception.
*
* @throws \Behat\Mink\Exception\ElementHtmlException
* When the element doesn't exist.
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element is not visible in the viewport.
*
* @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport()
*/
public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') {
$node = $this->session->getPage()->find($selector_type, $selector);
if ($node === NULL) {
if (is_array($selector)) {
$selector = implode(' ', $selector);
}
throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
}
$result = $this->checkNodeVisibilityInViewport($node, $corner);
if ($result) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
}
/**
* Check the visibility of a node, or its specific corner.
*
* @param \Behat\Mink\Element\NodeElement $node
* A valid node.
* @param bool|string $corner
* (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
*
* @return bool
* Returns TRUE if the node is visible in the viewport, FALSE otherwise.
*
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* When an invalid corner specification is given.
*/
private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) {
$xpath = $node->getXpath();
// Build the Javascript to test if the complete element or a specific corner
// is in the viewport.
switch ($corner) {
case 'topLeft':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.top <= ly &&
r.left >= 0 &&
r.left <= lx
)
}
JS;
break;
case 'topRight':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.top <= ly &&
r.right >= 0 &&
r.right <= lx
);
}
JS;
break;
case 'bottomRight':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.bottom >= 0 &&
r.bottom <= ly &&
r.right >= 0 &&
r.right <= lx
);
}
JS;
break;
case 'bottomLeft':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.bottom >= 0 &&
r.bottom <= ly &&
r.left >= 0 &&
r.left <= lx
);
}
JS;
break;
case FALSE:
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.left >= 0 &&
r.bottom <= ly &&
r.right <= lx
);
}
JS;
break;
// Throw an exception if an invalid corner parameter is given.
default:
throw new UnsupportedDriverActionException($corner, $this->session->getDriver());
}
// Build the full Javascript test. The shared logic gets the corner
// specific test logic injected.
$full_javascript_visibility_test = <<<JS
(function(t){
var w = window,
d = document,
e = d.documentElement,
n = d.evaluate("$xpath", d, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
r = n.getBoundingClientRect(),
lx = (w.innerWidth || e.clientWidth),
ly = (w.innerHeight || e.clientHeight);
return t(r, lx, ly);
}($test_javascript_function));
JS;
// Check the visibility by injecting and executing the full Javascript test
// script in the page.
return $this->session->evaluateScript($full_javascript_visibility_test);
}
/**
* Passes if the raw text IS NOT found escaped on the loaded page.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*/
public function assertNoEscaped($raw) {
$this->responseNotContains($this->escapeHtml($raw));
}
/**
* Passes if the raw text IS found escaped on the loaded page.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*/
public function assertEscaped($raw) {
$this->responseContains($this->escapeHtml($raw));
}
/**
* Escapes HTML for testing.
*
* Drupal's Html::escape() uses the ENT_QUOTES flag with htmlspecialchars() to
* escape both single and double quotes. With JavascriptTestBase testing the
* browser is automatically converting &quot; and &#039; to double and single
* quotes respectively therefore we can not escape them when testing for
* escaped HTML.
*
* @param $raw
* The raw string to escape.
*
* @return string
* The string with escaped HTML.
*
* @see Drupal\Component\Utility\Html::escape()
*/
protected function escapeHtml($raw) {
return htmlspecialchars($raw, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* Asserts that no matching element exists on the page after a wait.
*
* @param string $selector_type
* The element selector type (css, xpath).
* @param string|array $selector
* The element selector.
* @param int $timeout
* (optional) Timeout in milliseconds, defaults to 10000.
* @param string $message
* (optional) The exception message.
*
* @throws \Behat\Mink\Exception\ElementHtmlException
* When an element still exists on the page.
*/
public function assertNoElementAfterWait($selector_type, $selector, $timeout = 10000, $message = 'Element exists on the page.') {
$start = microtime(TRUE);
$end = $start + ($timeout / 1000);
$page = $this->session->getPage();
do {
$node = $page->find($selector_type, $selector);
if (empty($node)) {
return;
}
usleep(100000);
} while (microtime(TRUE) < $end);
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Tests Javascript deprecation notices.
*
* @group javascript
* @group legacy
*/
class JavascriptDeprecationTest extends WebDriverTestBase {
public static $modules = ['js_deprecation_test'];
/**
* @expectedDeprecation Javascript Deprecation: This function is deprecated for testing purposes.
* @expectedDeprecation Javascript Deprecation: This property is deprecated for testing purposes.
*/
public function testJavascriptDeprecation() {
$this->drupalGet('js_deprecation_test');
// Ensure that deprecation message from previous page loads will be
// detected.
$this->drupalGet('user');
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Tests Drupal settings retrieval in JavascriptTestBase tests.
*
* @group javascript
*/
class JavascriptGetDrupalSettingsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests retrieval of Drupal settings.
*
* @see \Drupal\FunctionalJavascriptTests\WebDriverTestBase::getDrupalSettings()
*/
public function testGetDrupalSettings() {
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet('test-page');
// Check that we can read the JS settings.
$js_settings = $this->getDrupalSettings();
$this->assertSame('azAZ09();.,\\\/-_{}', $js_settings['test-setting']);
// Dynamically change the setting using Javascript.
$script = <<<EndOfScript
(function () {
drupalSettings['test-setting'] = 'foo';
})();
EndOfScript;
$this->getSession()->evaluateScript($script);
// Check that the setting has been changed.
$js_settings = $this->getDrupalSettings();
$this->assertSame('foo', $js_settings['test-setting']);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
@trigger_error('The ' . __NAMESPACE__ . '\JavascriptTestBase is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Instead, use ' . __NAMESPACE__ . '\WebDriverTestBase. See https://www.drupal.org/node/2945059', E_USER_DEPRECATED);
use Zumba\Mink\Driver\PhantomJSDriver;
/**
* Runs a browser test using PhantomJS.
*
* Base class for testing browser interaction implemented in JavaScript.
*
* @deprecated in drupal:8.6.0 and is removed from drupal:9.0.0.
* Use \Drupal\FunctionalJavascriptTests\WebDriverTestBase instead
*
* @see https://www.drupal.org/node/2945059
*
* @ingroup testing
*/
abstract class JavascriptTestBase extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $minkDefaultDriverClass = PhantomJSDriver::class;
/**
* {@inheritdoc}
*/
public function assertSession($name = NULL) {
// Return a WebAssert that supports status code and header assertions.
return new JSWebAssert($this->getSession($name), $this->baseUrl);
}
}
@@ -0,0 +1,15 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Runs a browser test using PhantomJS.
*
* @deprecated in drupal:8.6.0 and is removed from drupal:9.0.0.
* Use \Drupal\FunctionalJavascriptTests\WebDriverTestBase instead
*
* BC layer for testing browser interaction implemented in JavaScript.
*/
abstract class LegacyJavascriptTestBase extends JavascriptTestBase {
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Provides functions for simulating sort changes.
*
* Selenium uses ChromeDriver for FunctionalJavascript tests, but it does not
* currently support HTML5 drag and drop. These methods manipulate the DOM.
* This trait should be deprecated when the Chromium bug is fixed.
*
* @see https://www.drupal.org/project/drupal/issues/3078152
*/
trait SortableTestTrait {
/**
* Define to provide any necessary callback following layout change.
*
* @param string $item
* The HTML selector for the element to be moved.
* @param string $from
* The HTML selector for the previous container element.
* @param null|string $to
* The HTML selector for the target container.
*/
abstract protected function sortableUpdate($item, $from, $to = NULL);
/**
* Simulates a drag on an element from one container to another.
*
* @param string $item
* The HTML selector for the element to be moved.
* @param string $from
* The HTML selector for the previous container element.
* @param null|string $to
* The HTML selector for the target container.
*/
protected function sortableTo($item, $from, $to) {
$item = addslashes($item);
$from = addslashes($from);
$to = addslashes($to);
$script = <<<JS
(function (src, to) {
var sourceElement = document.querySelector(src);
var toElement = document.querySelector(to);
toElement.insertBefore(sourceElement, toElement.firstChild);
})('{$item}', '{$to}')
JS;
$options = [
'script' => $script,
'args' => [],
];
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
$this->sortableUpdate($item, $from, $to);
}
/**
* Simulates a drag moving an element after its sibling in the same container.
*
* @param string $item
* The HTML selector for the element to be moved.
* @param string $target
* The HTML selector for the sibling element.
* @param string $from
* The HTML selector for the element container.
*/
protected function sortableAfter($item, $target, $from) {
$item = addslashes($item);
$target = addslashes($target);
$from = addslashes($from);
$script = <<<JS
(function (src, to) {
var sourceElement = document.querySelector(src);
var toElement = document.querySelector(to);
toElement.insertAdjacentElement('afterend', sourceElement);
})('{$item}', '{$target}')
JS;
$options = [
'script' => $script,
'args' => [],
];
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
$this->sortableUpdate($item, $from);
}
}
@@ -0,0 +1,473 @@
<?php
namespace Drupal\FunctionalJavascriptTests\TableDrag;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ExpectationException;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests draggable table.
*
* @group javascript
*/
class TableDragTest extends WebDriverTestBase {
/**
* Class used to verify that dragging operations are in execution.
*/
const DRAGGING_CSS_CLASS = 'tabledrag-test-dragging';
/**
* {@inheritdoc}
*/
protected static $modules = ['tabledrag_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->state = $this->container->get('state');
}
/**
* Tests row weight switch.
*/
public function testRowWeightSwitch() {
$this->state->set('tabledrag_test_table', array_flip(range(1, 3)));
$this->drupalGet('tabledrag_test');
$session = $this->getSession();
$page = $session->getPage();
$weight_select1 = $page->findField("table[1][weight]");
$weight_select2 = $page->findField("table[2][weight]");
$weight_select3 = $page->findField("table[3][weight]");
// Check that rows weight selects are hidden.
$this->assertFalse($weight_select1->isVisible());
$this->assertFalse($weight_select2->isVisible());
$this->assertFalse($weight_select3->isVisible());
// Toggle row weight selects as visible.
$this->findWeightsToggle('Show row weights')->click();
// Check that rows weight selects are visible.
$this->assertTrue($weight_select1->isVisible());
$this->assertTrue($weight_select2->isVisible());
$this->assertTrue($weight_select3->isVisible());
// Toggle row weight selects back to hidden.
$this->findWeightsToggle('Hide row weights')->click();
// Check that rows weight selects are hidden again.
$this->assertFalse($weight_select1->isVisible());
$this->assertFalse($weight_select2->isVisible());
$this->assertFalse($weight_select3->isVisible());
}
/**
* Tests draggable table drag'n'drop.
*/
public function testDragAndDrop() {
$this->state->set('tabledrag_test_table', array_flip(range(1, 3)));
$this->drupalGet('tabledrag_test');
$session = $this->getSession();
$page = $session->getPage();
$weight_select1 = $page->findField("table[1][weight]");
$weight_select2 = $page->findField("table[2][weight]");
$weight_select3 = $page->findField("table[3][weight]");
// Check that initially the rows are in the correct order.
$this->assertOrder(['Row with id 1', 'Row with id 2', 'Row with id 3']);
// Check that the 'unsaved changes' text is not present in the message area.
$this->assertSession()->pageTextNotContains('You have unsaved changes.');
$row1 = $this->findRowById(1)->find('css', 'a.tabledrag-handle');
$row2 = $this->findRowById(2)->find('css', 'a.tabledrag-handle');
$row3 = $this->findRowById(3)->find('css', 'a.tabledrag-handle');
// Drag row1 over row2.
$row1->dragTo($row2);
// Check that the 'unsaved changes' text was added in the message area.
$this->assertSession()->waitForText('You have unsaved changes.');
// Check that row1 and row2 were swapped.
$this->assertOrder(['Row with id 2', 'Row with id 1', 'Row with id 3']);
// Check that weights were changed.
$this->assertGreaterThan($weight_select2->getValue(), $weight_select1->getValue());
$this->assertGreaterThan($weight_select2->getValue(), $weight_select3->getValue());
$this->assertGreaterThan($weight_select1->getValue(), $weight_select3->getValue());
// Now move the last row (row3) in the second position. row1 should go last.
$row3->dragTo($row1);
// Check that the order is: row2, row3 and row1.
$this->assertOrder(['Row with id 2', 'Row with id 3', 'Row with id 1']);
}
/**
* Tests accessibility through keyboard of the tabledrag functionality.
*/
public function testKeyboardAccessibility() {
$this->state->set('tabledrag_test_table', array_flip(range(1, 5)));
$expected_table = [
['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 2, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 3, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
];
$this->drupalGet('tabledrag_test');
$this->assertDraggableTable($expected_table);
// Nest the row with id 2 as child of row 1.
$this->moveRowWithKeyboard($this->findRowById(2), 'right');
$expected_table[1] = ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Nest the row with id 3 as child of row 1.
$this->moveRowWithKeyboard($this->findRowById(3), 'right');
$expected_table[2] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Nest the row with id 3 as child of row 2.
$this->moveRowWithKeyboard($this->findRowById(3), 'right');
$expected_table[2] = ['id' => 3, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Nesting should be allowed to maximum level 2.
$this->moveRowWithKeyboard($this->findRowById(4), 'right', 4);
$expected_table[3] = ['id' => 4, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Re-order children of row 1.
$this->moveRowWithKeyboard($this->findRowById(4), 'up');
$expected_table[2] = ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
$expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Move back the row 3 to the 1st level.
$this->moveRowWithKeyboard($this->findRowById(3), 'left');
$expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
$this->moveRowWithKeyboard($this->findRowById(3), 'left');
$expected_table[0] = ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
$expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
$expected_table[4] = ['id' => 5, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
$this->assertDraggableTable($expected_table);
// Move row 3 to the last position.
$this->moveRowWithKeyboard($this->findRowById(3), 'down');
$expected_table[3] = ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
$expected_table[4] = ['id' => 3, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// Nothing happens when trying to move the last row further down.
$this->moveRowWithKeyboard($this->findRowById(3), 'down');
$this->assertDraggableTable($expected_table);
// Nest row 3 under 5. The max depth allowed should be 1.
$this->moveRowWithKeyboard($this->findRowById(3), 'right', 3);
$expected_table[4] = ['id' => 3, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
// The first row of the table cannot be nested.
$this->moveRowWithKeyboard($this->findRowById(1), 'right');
$this->assertDraggableTable($expected_table);
// Move a row which has nested children. The children should move with it,
// with nesting preserved. Swap the order of the top-level rows by moving
// row 1 to after row 3.
$this->moveRowWithKeyboard($this->findRowById(1), 'down', 2);
$expected_table[0] = ['id' => 5, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
$expected_table[3] = $expected_table[1];
$expected_table[1] = $expected_table[4];
$expected_table[4] = $expected_table[2];
$expected_table[2] = ['id' => 1, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
}
/**
* Tests the root and leaf behaviors for rows.
*/
public function testRootLeafDraggableRowsWithKeyboard() {
$this->state->set('tabledrag_test_table', [
1 => [],
2 => ['parent' => 1, 'depth' => 1, 'classes' => ['tabledrag-leaf']],
3 => ['parent' => 1, 'depth' => 1],
4 => [],
5 => ['classes' => ['tabledrag-root']],
]);
$this->drupalGet('tabledrag_test');
$expected_table = [
['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE],
['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE],
['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
];
$this->assertDraggableTable($expected_table);
// Rows marked as root cannot be moved as children of another row.
$this->moveRowWithKeyboard($this->findRowById(5), 'right');
$this->assertDraggableTable($expected_table);
// Rows marked as leaf cannot have children. Trying to move the row #3
// as child of #2 should have no results.
$this->moveRowWithKeyboard($this->findRowById(3), 'right');
$this->assertDraggableTable($expected_table);
// Leaf can be still swapped and moved to first level.
$this->moveRowWithKeyboard($this->findRowById(2), 'down');
$this->moveRowWithKeyboard($this->findRowById(2), 'left');
$expected_table[0]['weight'] = -10;
$expected_table[1]['id'] = 3;
$expected_table[1]['weight'] = -10;
$expected_table[2] = ['id' => 2, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
$expected_table[3]['weight'] = -8;
$expected_table[4]['weight'] = -7;
$this->assertDraggableTable($expected_table);
// Root rows can have children.
$this->moveRowWithKeyboard($this->findRowById(4), 'down');
$this->moveRowWithKeyboard($this->findRowById(4), 'right');
$expected_table[3]['id'] = 5;
$expected_table[4] = ['id' => 4, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE];
$this->assertDraggableTable($expected_table);
}
/**
* Tests the warning that appears upon making changes to a tabledrag table.
*/
public function testTableDragChangedWarning() {
$this->drupalGet('tabledrag_test');
// By default no text is visible.
$this->assertSession()->pageTextNotContains('You have unsaved changes.');
// Try to make a non-allowed action, like moving further down the last row.
// No changes happen, so no message should be shown.
$this->moveRowWithKeyboard($this->findRowById(5), 'down');
$this->assertSession()->pageTextNotContains('You have unsaved changes.');
// Make a change. The message will appear.
$this->moveRowWithKeyboard($this->findRowById(5), 'right');
$this->assertSession()->pageTextContainsOnce('You have unsaved changes.');
// Make another change, the text will stay visible and appear only once.
$this->moveRowWithKeyboard($this->findRowById(2), 'up');
$this->assertSession()->pageTextContainsOnce('You have unsaved changes.');
}
/**
* Asserts that several pieces of markup are in a given order in the page.
*
* @param string[] $items
* An ordered list of strings.
*
* @throws \Behat\Mink\Exception\ExpectationException
* When any of the given string is not found.
*
* @todo Remove this and use the WebAssert method when #2817657 is done.
*/
protected function assertOrder(array $items) {
$session = $this->getSession();
$text = $session->getPage()->getHtml();
$strings = [];
foreach ($items as $item) {
if (($pos = strpos($text, $item)) === FALSE) {
throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver());
}
$strings[$pos] = $item;
}
ksort($strings);
$this->assertSame($items, array_values($strings), "Strings found on the page but incorrectly ordered.");
}
/**
* Asserts the whole structure of the draggable test table.
*
* @param array $structure
* The table structure. Each entry represents a row and consists of:
* - id: the expected value for the ID hidden field.
* - weight: the expected row weight.
* - parent: the expected parent ID for the row.
* - indentation: how many indents the row should have.
* - changed: whether or not the row should have been marked as changed.
*/
protected function assertDraggableTable(array $structure) {
$rows = $this->getSession()->getPage()->findAll('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr');
$this->assertSession()->elementsCount('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr', count($structure));
foreach ($structure as $delta => $expected) {
$this->assertTableRow($rows[$delta], $expected['id'], $expected['weight'], $expected['parent'], $expected['indentation'], $expected['changed']);
}
}
/**
* Asserts the values of a draggable row.
*
* @param \Behat\Mink\Element\NodeElement $row
* The row element to assert.
* @param string $id
* The expected value for the ID hidden input of the row.
* @param int $weight
* The expected weight of the row.
* @param string $parent
* The expected parent ID.
* @param int $indentation
* The expected indentation of the row.
* @param bool $changed
* Whether or not the row should have been marked as changed.
*/
protected function assertTableRow(NodeElement $row, $id, $weight, $parent = '', $indentation = 0, $changed = FALSE) {
// Assert that the row position is correct by checking that the id
// corresponds.
$this->assertSession()->hiddenFieldValueEquals("table[$id][id]", $id, $row);
$this->assertSession()->hiddenFieldValueEquals("table[$id][parent]", $parent, $row);
$this->assertSession()->fieldValueEquals("table[$id][weight]", $weight, $row);
$this->assertSession()->elementsCount('css', '.js-indentation.indentation', $indentation, $row);
// A row is marked as changed when the related markup is present.
$this->assertSession()->elementsCount('css', 'abbr.tabledrag-changed', (int) $changed, $row);
}
/**
* Finds a row in the test table by the row ID.
*
* @param string $id
* The ID of the row.
*
* @return \Behat\Mink\Element\NodeElement
* The row element.
*/
protected function findRowById($id) {
$xpath = "//table[@id='tabledrag-test-table']/tbody/tr[.//input[@name='table[$id][id]']]";
$row = $this->getSession()->getPage()->find('xpath', $xpath);
$this->assertNotEmpty($row);
return $row;
}
/**
* Finds the show/hide weight toggle element.
*
* @param string $expected_text
* The expected text on the element.
*
* @return \Behat\Mink\Element\NodeElement
* The toggle element.
*/
protected function findWeightsToggle($expected_text) {
$toggle = $this->getSession()->getPage()->findButton($expected_text);
$this->assertNotEmpty($toggle);
return $toggle;
}
/**
* Moves a row through the keyboard.
*
* @param \Behat\Mink\Element\NodeElement $row
* The row to move.
* @param string $arrow
* The arrow button to use to move the row. Either one of 'left', 'right',
* 'up' or 'down'.
* @param int $repeat
* (optional) How many times to press the arrow button. Defaults to 1.
*/
protected function moveRowWithKeyboard(NodeElement $row, $arrow, $repeat = 1) {
$keys = [
'left' => 37,
'right' => 39,
'up' => 38,
'down' => 40,
];
if (!isset($keys[$arrow])) {
throw new \InvalidArgumentException('The arrow parameter must be one of "left", "right", "up" or "down".');
}
$key = $keys[$arrow];
$handle = $row->find('css', 'a.tabledrag-handle');
$handle->focus();
for ($i = 0; $i < $repeat; $i++) {
$this->markRowHandleForDragging($handle);
$handle->keyDown($key);
$handle->keyUp($key);
$this->waitUntilDraggingCompleted($handle);
}
$handle->blur();
}
/**
* Marks a row handle for dragging.
*
* The handle is marked by adding a css class that is removed by an helper
* js library once the dragging is over.
*
* @param \Behat\Mink\Element\NodeElement $handle
* The draggable row handle element.
*
* @throws \Exception
* Thrown when the class is not added successfully to the handle.
*/
protected function markRowHandleForDragging(NodeElement $handle) {
$class = self::DRAGGING_CSS_CLASS;
$script = <<<JS
document.evaluate("{$handle->getXpath()}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue.classList.add('{$class}');
JS;
$this->getSession()->executeScript($script);
$has_class = $this->getSession()->getPage()->waitFor(1, function () use ($handle, $class) {
return $handle->hasClass($class);
});
if (!$has_class) {
throw new \Exception(sprintf('Dragging css class was not added on handle "%s".', $handle->getXpath()));
}
}
/**
* Waits until the dragging operations are finished on a row handle.
*
* @param \Behat\Mink\Element\NodeElement $handle
* The draggable row handle element.
*
* @throws \Exception
* Thrown when the dragging operations are not completed on time.
*/
protected function waitUntilDraggingCompleted(NodeElement $handle) {
$class_removed = $this->getSession()->getPage()->waitFor(1, function () use ($handle) {
return !$handle->hasClass($this::DRAGGING_CSS_CLASS);
});
if (!$class_removed) {
throw new \Exception(sprintf('Dragging operations did not complete on time on handle %s', $handle->getXpath()));
}
}
}
@@ -0,0 +1,73 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Tests;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests the DrupalSelenium2Driver methods.
*
* @coversDefaultClass \Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver
* @group javascript
*/
class DrupalSelenium2DriverTest extends WebDriverTestBase {
use TestFileCreationTrait;
use FileFieldCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['file', 'field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$storage_settings = ['cardinality' => 3];
$this->createFileField('field_file', 'entity_test', 'entity_test', $storage_settings);
$this->drupalLogin($this->drupalCreateUser([
'administer entity_test content',
'access content',
]));
}
/**
* Tests uploading remote files.
*/
public function testGetRemoteFilePath() {
$web_driver = $this->getSession()->getDriver();
$file_system = \Drupal::service('file_system');
$entity = EntityTest::create();
$entity->save();
$files = array_slice($this->getTestFiles('text'), 0, 3);
$real_paths = [];
foreach ($files as $file) {
$real_paths[] = $file_system->realpath($file->uri);
}
$remote_paths = [];
foreach ($real_paths as $path) {
$remote_paths[] = $web_driver->uploadFileAndGetRemoteFilePath($path);
}
// Tests that uploading multiple remote files works with remote path.
$this->drupalGet($entity->toUrl('edit-form'));
$multiple_field = $this->xpath('//input[@multiple]')[0];
$multiple_field->setValue(implode("\n", $remote_paths));
$this->assertSession()->assertWaitOnAjaxRequest();
$this->getSession()->getPage()->findButton('Save')->click();
$entity = EntityTest::load($entity->id());
$this->assertCount(3, $entity->field_file);
}
}
@@ -0,0 +1,111 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Tests;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementHtmlException;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests for the JSWebAssert class.
*
* @group javascript
*/
class JSWebAssertTest extends WebDriverTestBase {
/**
* Required modules.
*
* @var array
*/
public static $modules = ['js_webassert_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that JSWebAssert assertions work correctly.
*/
public function testJsWebAssert() {
$this->drupalGet('js_webassert_test_form');
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
$assert_session->elementExists('css', '[data-drupal-selector="edit-test-assert-no-element-after-wait-pass"]');
$page->findButton('Test assertNoElementAfterWait: pass')->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-test-assert-no-element-after-wait-pass"]', 1000);
$assert_session->elementExists('css', '[data-drupal-selector="edit-test-assert-no-element-after-wait-fail"]');
$page->findButton('Test assertNoElementAfterWait: fail')->press();
try {
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-test-assert-no-element-after-wait-fail"]', 500, 'Element exists on page after too short wait.');
$this->fail('Element not exists on page after too short wait.');
}
catch (ElementHtmlException $e) {
$this->assertSame('Element exists on page after too short wait.', $e->getMessage());
}
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-test-assert-no-element-after-wait-fail"]', 2500, 'Element remove after another wait.ss');
$test_button = $page->findButton('Add button');
$test_link = $page->findButton('Add link');
$test_field = $page->findButton('Add field');
$test_id = $page->findButton('Add ID');
$test_wait_on_ajax = $page->findButton('Test assertWaitOnAjaxRequest');
$test_wait_on_element_visible = $page->findButton('Test waitForElementVisible');
// Test the wait...() methods by first checking the fields aren't available
// and then are available after the wait method.
$result = $page->findButton('Added button');
$this->assertEmpty($result);
$test_button->click();
$result = $assert_session->waitForButton('Added button');
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
$result = $page->findLink('Added link');
$this->assertEmpty($result);
$test_link->click();
$result = $assert_session->waitForLink('Added link');
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
$result = $page->findField('added_field');
$this->assertEmpty($result);
$test_field->click();
$result = $assert_session->waitForField('added_field');
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
$result = $page->findById('js_webassert_test_field_id');
$this->assertEmpty($result);
$test_id->click();
$result = $assert_session->waitForId('js_webassert_test_field_id');
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
// Test waitOnAjaxRequest. Verify the element is available after the wait
// and the behaviors have run on completing by checking the value.
$result = $page->findField('test_assert_wait_on_ajax_input');
$this->assertEmpty($result);
$test_wait_on_ajax->click();
$assert_session->assertWaitOnAjaxRequest();
$result = $page->findField('test_assert_wait_on_ajax_input');
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
$this->assertEquals('js_webassert_test', $result->getValue());
$result = $page->findButton('Added WaitForElementVisible');
$this->assertEmpty($result);
$test_wait_on_element_visible->click();
$result = $assert_session->waitForElementVisible('named', ['button', 'Added WaitForElementVisible']);
$this->assertNotEmpty($result);
$this->assertInstanceOf(NodeElement::class, $result);
$this->assertEquals(TRUE, $result->isVisible());
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Tests;
use Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver;
/**
* Tests for the JSWebAssert class using webdriver.
*
* @group javascript
*/
class JSWebWithWebDriverAssertTest extends JSWebAssertTest {
/**
* {@inheritdoc}
*/
protected $minkDefaultDriverClass = DrupalSelenium2Driver::class;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\block\FunctionalJavascript\BlockFilterTest;
/**
* Runs BlockFilterTest in Claro.
*
* @group block
*
* @see \Drupal\Tests\block\FunctionalJavascript\BlockFilterTest.
*/
class ClaroBlockFilterTest extends BlockFilterTest {
/**
* Modules to enable.
*
* Install the shortcut module so that claro.settings has its schema checked.
* There's currently no way for Claro to provide a default and have valid
* configuration as themes cannot react to a module install.
*
* @var string[]
*/
public static $modules = ['shortcut'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container->get('theme_installer')->install(['claro']);
$this->config('system.theme')->set('default', 'claro')->save();
}
}
@@ -0,0 +1,116 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest;
/**
* Runs EntityDisplayTest in Claro.
*
* @group claro
*
* @see \Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest.
*/
class ClaroEntityDisplayTest extends EntityDisplayTest {
/**
* Modules to enable.
*
* Install the shortcut module so that claro.settings has its schema checked.
* There's currently no way for Claro to provide a default and have valid
* configuration as themes cannot react to a module install.
*
* @var string[]
*/
public static $modules = ['shortcut'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container->get('theme_installer')->install(['claro']);
$this->config('system.theme')->set('default', 'claro')->save();
}
/**
* Copied from parent.
*
* This is Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest::testEntityForm()
* with a line changed to reflect row weight toggle being a link instead
* of a button.
*/
public function testEntityForm() {
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldExists('field_test_text[0][value]');
$this->drupalGet('entity_test/structure/entity_test/form-display');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->getSession()->getPage()->clickLink('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldNotExists('field_test_text[0][value]');
}
/**
* Copied from parent.
*
* This is Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest::testEntityView()
* with a line changed to reflect row weight toggle being a link instead
* of a button.
*/
public function testEntityView() {
$this->drupalGet('entity_test/1');
$this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->getSession()->getPage()->clickLink('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->drupalGet('entity_test/1');
$this->assertSession()->elementExists('css', '.field--name-field-test-text');
}
/**
* Copied from parent.
*
* This is Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest::testExtraFields()
* with a line changed to reflect Claro's tabledrag selector.
*/
public function testExtraFields() {
entity_test_create_bundle('bundle_with_extra_fields');
$this->drupalGet('entity_test/structure/bundle_with_extra_fields/display');
$this->assertSession()->waitForElement('css', '.tabledrag-handle');
$id = $this->getSession()->getPage()->find('css', '[name="form_build_id"]')->getValue();
$extra_field_row = $this->getSession()->getPage()->find('css', '#display-extra-field');
$disabled_region_row = $this->getSession()->getPage()->find('css', '.region-hidden-title');
$extra_field_row->find('css', '.js-tabledrag-handle')->dragTo($disabled_region_row);
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()
->waitForElement('css', "[name='form_build_id']:not([value='$id'])");
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
}
}
@@ -0,0 +1,41 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\menu_ui\FunctionalJavascript\MenuUiJavascriptTest;
/**
* Runs MenuUiJavascriptTest in Claro.
*
* @group claro
*
* @see \Drupal\Tests\menu_ui\FunctionalJavascript\MenuUiJavascriptTest;
*/
class ClaroMenuUiJavascriptTest extends MenuUiJavascriptTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'shortcut',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container->get('theme_installer')->install(['claro']);
$this->config('system.theme')->set('default', 'claro')->save();
}
/**
* Intentionally empty method.
*
* Contextual links do not work in admin themes, so this is empty to prevent
* this test running in the parent class.
*/
public function testBlockContextualLinks() {
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\TableDrag\TableDragTest;
/**
* Runs TableDragTest in Claro.
*
* @group claro
*
* @see \Drupal\FunctionalJavascriptTests\TableDrag\TableDragTest
*/
class ClaroTableDragTest extends TableDragTest {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'claro';
/**
* {@inheritdoc}
*/
protected function findWeightsToggle($expected_text) {
$toggle = $this->getSession()->getPage()->findLink($expected_text);
$this->assertNotEmpty($toggle);
return $toggle;
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Runs tests on Views UI using Claro.
*
* @group claro
*/
class ClaroViewsUiTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'claro';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Disable automatic live preview to make the sequence of calls clearer.
$this->config('views.settings')->set('ui.always_live_preview', FALSE)->save();
// Create the test user and log in.
$admin_user = $this->drupalCreateUser([
'administer views',
'access administration pages',
'view the administration theme',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests Views UI display menu tabs CSS classes.
*
* Ensures that the CSS classes added to display menu tabs are preserved when
* Views UI is updated with AJAX.
*/
public function testViewsUiTabsCssClasses() {
$this->drupalGet('admin/structure/views/view/who_s_online');
$assert_session = $this->assertSession();
$assert_session->elementExists('css', '#views-display-menu-tabs.views-tabs.views-tabs--secondary');
// Click on the Display name and wait for the Views UI dialog.
$assert_session->elementExists('css', '#edit-display-settings-top .views-display-setting a')->click();
$this->assertNotNull($this->assertSession()->waitForElement('css', '.js-views-ui-dialog'));
// Click the Apply button of the dialog.
$assert_session->elementExists('css', '.js-views-ui-dialog .ui-dialog-buttonpane')->findButton('Apply')->press();
// Wait for AJAX to finish.
$assert_session->assertWaitOnAjaxRequest();
// Check that the display menu tabs list still has the expected CSS classes.
$assert_session->elementExists('css', '#views-display-menu-tabs.views-tabs.views-tabs--secondary');
}
/**
* Tests Views UI dropbutton CSS classes.
*
* Ensures that the CSS classes added to the Views UI extra actions dropbutton
* in .views-display-top are preserved when Views UI is refreshed with AJAX.
*/
public function testViewsUiDropButtonCssClasses() {
$this->drupalGet('admin/structure/views/view/who_s_online');
$assert_session = $this->assertSession();
$extra_actions_dropbutton_list = $assert_session->elementExists('css', '#views-display-extra-actions.dropbutton--small');
$list_item_selectors = ['li:first-child', 'li:last-child'];
// Test list item CSS classes.
foreach ($list_item_selectors as $list_item_selector) {
$this->assertNotNull($extra_actions_dropbutton_list->find('css', "$list_item_selector.dropbutton__item.dropbutton__item--small"));
}
// Click on the Display name and wait for the Views UI dialog.
$assert_session->elementExists('css', '#edit-display-settings-top .views-display-setting a')->click();
$this->assertNotNull($this->assertSession()->waitForElement('css', '.js-views-ui-dialog'));
// Click the Apply button of the dialog.
$assert_session->elementExists('css', '.js-views-ui-dialog .ui-dialog-buttonpane')->findButton('Apply')->press();
// Wait for AJAX to finish.
$this->assertSession()->assertWaitOnAjaxRequest();
// Check that the drop button list still has the expected CSS classes.
$this->assertTrue($extra_actions_dropbutton_list->hasClass('dropbutton--small'));
// Check list item CSS classes.
foreach ($list_item_selectors as $list_item_selector) {
$this->assertNotNull($extra_actions_dropbutton_list->find('css', "$list_item_selector.dropbutton__item.dropbutton__item--small"));
}
}
}
@@ -0,0 +1,120 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use WebDriver\Service\CurlService;
use WebDriver\Exception\CurlExec;
use WebDriver\Exception as WebDriverException;
/**
* Provides a curl service to interact with Selenium driver.
*
* Extends WebDriver\Service\CurlService to solve problem with race conditions,
* when multiple processes requests.
*/
class WebDriverCurlService extends CurlService {
/**
* {@inheritdoc}
*/
public function execute($requestMethod, $url, $parameters = NULL, $extraOptions = []) {
$extraOptions += [
CURLOPT_FAILONERROR => TRUE,
];
$retries = 0;
while ($retries < 10) {
try {
$customHeaders = [
'Content-Type: application/json;charset=UTF-8',
'Accept: application/json;charset=UTF-8',
];
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
switch ($requestMethod) {
case 'GET':
break;
case 'POST':
if ($parameters && is_array($parameters)) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($parameters));
}
else {
$customHeaders[] = 'Content-Length: 0';
// Suppress "Transfer-Encoding: chunked" header automatically
// added by cURL that causes a 400 bad request (bad
// content-length).
$customHeaders[] = 'Transfer-Encoding:';
}
// Suppress "Expect: 100-continue" header automatically added by
// cURL that causes a 1 second delay if the remote server does not
// support Expect.
$customHeaders[] = 'Expect:';
curl_setopt($curl, CURLOPT_POST, TRUE);
break;
case 'DELETE':
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'PUT':
if ($parameters && is_array($parameters)) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($parameters));
}
else {
$customHeaders[] = 'Content-Length: 0';
// Suppress "Transfer-Encoding: chunked" header automatically
// added by cURL that causes a 400 bad request (bad
// content-length).
$customHeaders[] = 'Transfer-Encoding:';
}
// Suppress "Expect: 100-continue" header automatically added by
// cURL that causes a 1 second delay if the remote server does not
// support Expect.
$customHeaders[] = 'Expect:';
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
break;
}
foreach ($extraOptions as $option => $value) {
curl_setopt($curl, $option, $value);
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $customHeaders);
$rawResult = trim(curl_exec($curl));
$info = curl_getinfo($curl);
$info['request_method'] = $requestMethod;
if (array_key_exists(CURLOPT_FAILONERROR, $extraOptions) && $extraOptions[CURLOPT_FAILONERROR] && CURLE_GOT_NOTHING !== ($errno = curl_errno($curl)) && $error = curl_error($curl)) {
curl_close($curl);
throw WebDriverException::factory(WebDriverException::CURL_EXEC, sprintf("Curl error thrown for http %s to %s%s\n\n%s", $requestMethod, $url, $parameters && is_array($parameters) ? ' with params: ' . json_encode($parameters) : '', $error));
}
curl_close($curl);
$result = json_decode($rawResult, TRUE);
if (isset($result['status']) && $result['status'] === WebDriverException::STALE_ELEMENT_REFERENCE) {
usleep(100000);
$retries++;
continue;
}
return [$rawResult, $info];
}
catch (CurlExec $exception) {
$retries++;
}
}
throw WebDriverException::factory(WebDriverException::CURL_EXEC, sprintf("Curl error thrown for http %s to %s%s\n\n%s", $requestMethod, $url, $parameters && is_array($parameters) ? ' with params: ' . json_encode($parameters) : '', $error));
}
}
@@ -0,0 +1,252 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Exception\DriverException;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Zumba\GastonJS\Exception\DeadClient;
use Zumba\Mink\Driver\PhantomJSDriver;
/**
* Runs a browser test using a driver that supports Javascript.
*
* Base class for testing browser interaction implemented in JavaScript.
*
* @ingroup testing
*/
abstract class WebDriverTestBase extends BrowserTestBase {
/**
* Disables CSS animations in tests for more reliable testing.
*
* CSS animations are disabled by installing the css_disable_transitions_test
* module. Set to FALSE to test CSS animations.
*
* @var bool
*/
protected $disableCssAnimations = TRUE;
/**
* {@inheritdoc}
*
* To use a legacy phantomjs based approach, please use PhantomJSDriver::class.
*/
protected $minkDefaultDriverClass = DrupalSelenium2Driver::class;
/**
* {@inheritdoc}
*/
protected function initMink() {
if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) {
$this->minkDefaultDriverArgs = ['chrome', NULL, 'http://localhost:4444'];
}
elseif ($this->minkDefaultDriverClass === PhantomJSDriver::class) {
// Set up the template cache used by the PhantomJS mink driver.
$path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache';
$this->minkDefaultDriverArgs = [
'http://127.0.0.1:8510',
$path,
];
if (!file_exists($path)) {
mkdir($path);
}
}
try {
return parent::initMink();
}
catch (DeadClient $e) {
$this->markTestSkipped('PhantomJS is either not installed or not running. Start it via phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768&');
}
catch (DriverException $e) {
if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) {
$this->markTestSkipped("The test wasn't able to connect to your webdriver instance. For more information read core/tests/README.md.\n\nThe original message while starting Mink: {$e->getMessage()}");
}
else {
throw $e;
}
}
catch (\Exception $e) {
$this->markTestSkipped('An unexpected error occurred while starting Mink: ' . $e->getMessage());
}
}
/**
* {@inheritdoc}
*/
protected function installModulesFromClassProperty(ContainerInterface $container) {
self::$modules = ['js_deprecation_log_test'];
if ($this->disableCssAnimations) {
self::$modules[] = 'css_disable_transitions_test';
}
parent::installModulesFromClassProperty($container);
}
/**
* {@inheritdoc}
*/
protected function initFrontPage() {
parent::initFrontPage();
// Set a standard window size so that all javascript tests start with the
// same viewport.
$this->getSession()->resizeWindow(1024, 768);
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
if ($this->mink) {
// Wait for all requests to finish. It is possible that an AJAX request is
// still on-going.
$result = $this->getSession()->wait(5000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))');
if (!$result) {
// If the wait is unsuccessful, there may still be an AJAX request in
// progress. If we tear down now, then this AJAX request may fail with
// missing database tables, because tear down will have removed them.
// Rather than allow it to fail, throw an explicit exception now
// explaining what the problem is.
throw new \RuntimeException('Unfinished AJAX requests while tearing down a test');
}
$warnings = $this->getSession()->evaluateScript("JSON.parse(sessionStorage.getItem('js_deprecation_log_test.warnings') || JSON.stringify([]))");
foreach ($warnings as $warning) {
if (strpos($warning, '[Deprecation]') === 0) {
@trigger_error('Javascript Deprecation:' . substr($warning, 13), E_USER_DEPRECATED);
}
}
}
parent::tearDown();
}
/**
* {@inheritdoc}
*/
protected function getMinkDriverArgs() {
if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) {
return getenv('MINK_DRIVER_ARGS_WEBDRIVER') ?: getenv('MINK_DRIVER_ARGS_PHANTOMJS') ?: parent::getMinkDriverArgs();
}
elseif ($this->minkDefaultDriverClass === PhantomJSDriver::class) {
return getenv('MINK_DRIVER_ARGS_PHANTOMJS') ?: parent::getMinkDriverArgs();
}
return parent::getMinkDriverArgs();
}
/**
* Asserts that the element with the given CSS selector is visible.
*
* @param string $css_selector
* The CSS selector identifying the element to check.
* @param string $message
* Optional message to show alongside the assertion.
*
* @deprecated in drupal:8.1.0 and is removed from drupal:9.0.0. Use
* \Behat\Mink\Element\NodeElement::isVisible() instead.
*/
protected function assertElementVisible($css_selector, $message = '') {
$this->assertTrue($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath($css_selector)), $message);
@trigger_error('The ' . __METHOD__ . ' method is deprecated since version 8.1.0 and will be removed in 9.0.0. Use \Behat\Mink\Element\NodeElement::isVisible() instead.', E_USER_DEPRECATED);
}
/**
* Asserts that the element with the given CSS selector is not visible.
*
* @param string $css_selector
* The CSS selector identifying the element to check.
* @param string $message
* Optional message to show alongside the assertion.
*
* @deprecated in drupal:8.1.0 and is removed from drupal:9.0.0. Use
* \Behat\Mink\Element\NodeElement::isVisible() instead.
*/
protected function assertElementNotVisible($css_selector, $message = '') {
$this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath($css_selector)), $message);
@trigger_error('The ' . __METHOD__ . ' method is deprecated since version 8.1.0 and will be removed in 9.0.0. Use \Behat\Mink\Element\NodeElement::isVisible() instead.', E_USER_DEPRECATED);
}
/**
* Waits for the given time or until the given JS condition becomes TRUE.
*
* @param string $condition
* JS condition to wait until it becomes TRUE.
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
* @param string $message
* (optional) A message to display with the assertion. If left blank, a
* default message will be displayed.
*
* @throws \PHPUnit\Framework\AssertionFailedError
*
* @see \Behat\Mink\Driver\DriverInterface::evaluateScript()
*/
protected function assertJsCondition($condition, $timeout = 10000, $message = '') {
$message = $message ?: "Javascript condition met:\n" . $condition;
$result = $this->getSession()->getDriver()->wait($timeout, $condition);
$this->assertTrue($result, $message);
}
/**
* Creates a screenshot.
*
* @param string $filename
* The file name of the resulting screenshot. If using the default phantomjs
* driver then this should be a JPG filename.
* @param bool $set_background_color
* (optional) By default this method will set the background color to white.
* Set to FALSE to override this behavior.
*
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* When operation not supported by the driver.
* @throws \Behat\Mink\Exception\DriverException
* When the operation cannot be done.
*/
protected function createScreenshot($filename, $set_background_color = TRUE) {
$session = $this->getSession();
if ($set_background_color) {
$session->executeScript("document.body.style.backgroundColor = 'white';");
}
$image = $session->getScreenshot();
file_put_contents($filename, $image);
}
/**
* {@inheritdoc}
*/
public function assertSession($name = NULL) {
return new WebDriverWebAssert($this->getSession($name), $this->baseUrl);
}
/**
* Gets the current Drupal javascript settings and parses into an array.
*
* Unlike BrowserTestBase::getDrupalSettings(), this implementation reads the
* current values of drupalSettings, capturing all changes made via javascript
* after the page was loaded.
*
* @return array
* The Drupal javascript settings array.
*
* @see \Drupal\Tests\BrowserTestBase::getDrupalSettings()
*/
protected function getDrupalSettings() {
$script = <<<EndOfScript
(function () {
if (typeof drupalSettings !== 'undefined') {
return drupalSettings;
}
})();
EndOfScript;
return $this->getSession()->evaluateScript($script) ?: [];
}
/**
* {@inheritdoc}
*/
protected function getHtmlOutputHeaders() {
// The webdriver API does not support fetching headers.
return '';
}
}
@@ -0,0 +1,110 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Defines a JSWebAssert with no support for status code and header assertions.
*/
class WebDriverWebAssert extends JSWebAssert {
/**
* The use of statusCodeEquals() is not available.
*
* @param int $code
* The status code.
*/
public function statusCodeEquals($code) {
@trigger_error('Support for statusCodeEquals is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::statusCodeEquals($code);
}
/**
* The use of statusCodeNotEquals() is not available.
*
* @param int $code
* The status code.
*/
public function statusCodeNotEquals($code) {
@trigger_error('Support for statusCodeNotEquals is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::statusCodeNotEquals($code);
}
/**
* The use of responseHeaderEquals() is not available.
*
* @param string $name
* The name of the header.
* @param string $value
* The value to check the header against.
*/
public function responseHeaderEquals($name, $value) {
@trigger_error('Support for responseHeaderEquals is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderEquals($name, $value);
}
/**
* The use of responseHeaderNotEquals() is not available.
*
* @param string $name
* The name of the header.
* @param string $value
* The value to check the header against.
*/
public function responseHeaderNotEquals($name, $value) {
@trigger_error('Support for responseHeaderNotEquals is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderNotEquals($name, $value);
}
/**
* The use of responseHeaderContains() is not available.
*
* @param string $name
* The name of the header.
* @param string $value
* The value to check the header against.
*/
public function responseHeaderContains($name, $value) {
@trigger_error('Support for responseHeaderContains is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderContains($name, $value);
}
/**
* The use of responseHeaderNotContains() is not available.
*
* @param string $name
* The name of the header.
* @param string $value
* The value to check the header against.
*/
public function responseHeaderNotContains($name, $value) {
@trigger_error('Support for responseHeaderNotContains is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderNotContains($name, $value);
}
/**
* The use of responseHeaderMatches() is not available.
*
* @param string $name
* The name of the header.
* @param string $regex
* The value to check the header against.
*/
public function responseHeaderMatches($name, $regex) {
@trigger_error('Support for responseHeaderMatches is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderMatches($name, $regex);
}
/**
* The use of responseHeaderNotMatches() is not available.
*
* @param string $name
* The name of the header.
* @param string $regex
* The value to check the header against.
*/
public function responseHeaderNotMatches($name, $regex) {
@trigger_error('Support for responseHeaderNotMatches is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.', E_USER_DEPRECATED);
parent::responseHeaderNotMatches($name, $regex);
}
}
@@ -0,0 +1,840 @@
<?php
// @codingStandardsIgnoreFile
namespace Drupal\FunctionalTests;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ExpectationException;
use Behat\Mink\Selector\Xpath\Escaper;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\KernelTests\AssertLegacyTrait as BaseAssertLegacyTrait;
/**
* Provides convenience methods for assertions in browser tests.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* the methods on \Drupal\Tests\WebAssert instead, for example
* @code
* $this->assertSession()->statusCodeEquals(200);
* @endcode
*
* @todo https://www.drupal.org/project/drupal/issues/3114617 Note that
* deprecations in this file do not use the @ symbol in Drupal 8 because this
* will be removed in Drupal 10.0.0. Adding the @ back should re-enable coding
* standards checks.
*/
trait AssertLegacyTrait {
use BaseAssertLegacyTrait;
/**
* Asserts that the element with the given CSS selector is present.
*
* @param string $css_selector
* The CSS selector identifying the element to check.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->elementExists() instead.
*/
protected function assertElementPresent($css_selector) {
$this->assertSession()->elementExists('css', $css_selector);
}
/**
* Asserts that the element with the given CSS selector is not present.
*
* @param string $css_selector
* The CSS selector identifying the element to check.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->elementNotExists() instead.
*/
protected function assertElementNotPresent($css_selector) {
$this->assertSession()->elementNotExists('css', $css_selector);
}
/**
* Passes if the page (with HTML stripped) contains the text.
*
* Note that stripping HTML tags also removes their attributes, such as
* the values of text fields.
*
* @param string $text
* Plain text to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* - $this->assertSession()->responseContains() for non-HTML responses,
* like XML or Json.
* - $this->assertSession()->pageTextContains() for HTML responses. Unlike
* the deprecated assertText(), the passed text should be HTML decoded,
* exactly as a human sees it in the browser.
*/
protected function assertText($text) {
// Cast MarkupInterface to string.
$text = (string) $text;
$content_type = $this->getSession()->getResponseHeader('Content-type');
// In case of a Non-HTML response (example: XML) check the original
// response.
if (strpos($content_type, 'html') === FALSE) {
$this->assertSession()->responseContains($text);
}
else {
$this->assertTextHelper($text, FALSE);
}
}
/**
* Passes if the page (with HTML stripped) does not contains the text.
*
* Note that stripping HTML tags also removes their attributes, such as
* the values of text fields.
*
* @param string $text
* Plain text to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* - $this->assertSession()->responseNotContains() for non-HTML responses,
* like XML or Json.
* - $this->assertSession()->pageTextNotContains() for HTML responses.
* Unlike the deprecated assertNoText(), the passed text should be HTML
* decoded, exactly as a human sees it in the browser.
*/
protected function assertNoText($text) {
// Cast MarkupInterface to string.
$text = (string) $text;
$content_type = $this->getSession()->getResponseHeader('Content-type');
// In case of a Non-HTML response (example: XML) check the original
// response.
if (strpos($content_type, 'html') === FALSE) {
$this->assertSession()->responseNotContains($text);
}
else {
$this->assertTextHelper($text);
}
}
/**
* Helper for assertText and assertNoText.
*
* @param string $text
* Plain text to look for.
* @param bool $not_exists
* (optional) TRUE if this text should not exist, FALSE if it should.
* Defaults to TRUE.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertTextHelper($text, $not_exists = TRUE) {
$args = ['@text' => $text];
$message = $not_exists ? new FormattableMarkup('"@text" not found', $args) : new FormattableMarkup('"@text" found', $args);
$raw_content = $this->getSession()->getPage()->getContent();
// Trying to simulate what the user sees, given that it removes all text
// inside the head tags, removes inline Javascript, fix all HTML entities,
// removes dangerous protocols and filtering out all HTML tags, as they are
// not visible in a normal browser.
$raw_content = preg_replace('@<head>(.+?)</head>@si', '', $raw_content);
$page_text = Xss::filter($raw_content, []);
$actual = $not_exists == (strpos($page_text, (string) $text) === FALSE);
$this->assertTrue($actual, $message);
return $actual;
}
/**
* Passes if the text is found ONLY ONCE on the text version of the page.
*
* The text version is the equivalent of what a user would see when viewing
* through a web browser. In other words the HTML has been filtered out of
* the contents.
*
* @param string|\Drupal\Component\Render\MarkupInterface $text
* Plain text to look for.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t(). If left blank, a default message will be displayed.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->getSession()->getPage()->getText() and substr_count() instead.
*/
protected function assertUniqueText($text, $message = NULL) {
// Cast MarkupInterface objects to string.
$text = (string) $text;
$message = $message ?: "'$text' found only once on the page";
$page_text = $this->getSession()->getPage()->getText();
$nr_found = substr_count($page_text, $text);
$this->assertSame(1, $nr_found, $message);
}
/**
* Passes if the text is found MORE THAN ONCE on the text version of the page.
*
* The text version is the equivalent of what a user would see when viewing
* through a web browser. In other words the HTML has been filtered out of
* the contents.
*
* @param string|\Drupal\Component\Render\MarkupInterface $text
* Plain text to look for.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t(). If left blank, a default message will be displayed.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->getSession()->getPage()->getText() and substr_count() instead.
*/
protected function assertNoUniqueText($text, $message = '') {
// Cast MarkupInterface objects to string.
$text = (string) $text;
$message = $message ?: "'$text' found more than once on the page";
$page_text = $this->getSession()->getPage()->getText();
$nr_found = substr_count($page_text, $text);
$this->assertGreaterThan(1, $nr_found, $message);
}
/**
* Asserts the page responds with the specified response code.
*
* @param int $code
* Response code. For example 200 is a successful page request. For a list
* of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->statusCodeEquals() instead.
*/
protected function assertResponse($code) {
$this->assertSession()->statusCodeEquals($code);
}
/**
* Asserts that a field exists with the given name and value.
*
* @param string $name
* Name of field to assert.
* @param string $value
* (optional) Value of the field to assert. You may pass in NULL (default)
* to skip checking the actual value, while still checking that the field
* exists.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldByName($name, $value = NULL) {
$this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
* Asserts that a field does not exist with the given name and value.
*
* @param string $name
* Name of field to assert.
* @param string $value
* (optional) Value for the field, to assert that the field's value on the
* page does not match it. You may pass in NULL to skip checking the
* value, while still checking that the field does not exist. However, the
* default value ('') asserts that the field value is not an empty string.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldByName($name, $value = '') {
$this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
* Asserts that a field exists with the given ID and value.
*
* @param string $id
* ID of field to assert.
* @param string|\Drupal\Component\Render\MarkupInterface $value
* (optional) Value for the field to assert. You may pass in NULL to skip
* checking the value, while still checking that the field exists.
* However, the default value ('') asserts that the field value is an empty
* string.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldById($id, $value = '') {
$this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
* Asserts that a field exists with the given name or ID.
*
* @param string $field
* Name or ID of field to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() instead.
*/
protected function assertField($field) {
$this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
* Asserts that a field does NOT exist with the given name or ID.
*
* @param string $field
* Name or ID of field to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() instead.
*/
protected function assertNoField($field) {
$this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
* Passes if the raw text IS found on the loaded page, fail otherwise.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseContains() instead.
*/
protected function assertRaw($raw) {
$this->assertSession()->responseContains($raw);
}
/**
* Passes if the raw text IS not found on the loaded page, fail otherwise.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseNotContains() instead.
*/
protected function assertNoRaw($raw) {
$this->assertSession()->responseNotContains($raw);
}
/**
* Pass if the page title is the given string.
*
* @param string $expected_title
* The string the page title should be.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->titleEquals() instead.
*/
protected function assertTitle($expected_title) {
// Cast MarkupInterface to string.
$expected_title = (string) $expected_title;
return $this->assertSession()->titleEquals($expected_title);
}
/**
* Passes if a link with the specified label is found.
*
* An optional link index may be passed.
*
* @param string|\Drupal\Component\Render\MarkupInterface $label
* Text between the anchor tags.
* @param int $index
* Link position counting from zero.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->linkExists() instead.
*/
protected function assertLink($label, $index = 0) {
return $this->assertSession()->linkExists($label, $index);
}
/**
* Passes if a link with the specified label is not found.
*
* @param string|\Drupal\Component\Render\MarkupInterface $label
* Text between the anchor tags.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->linkNotExists() instead.
*/
protected function assertNoLink($label) {
return $this->assertSession()->linkNotExists($label);
}
/**
* Passes if a link containing a given href (part) is found.
*
* @param string $href
* The full or partial value of the 'href' attribute of the anchor tag.
* @param int $index
* Link position counting from zero.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->linkByHrefExists() instead.
*/
protected function assertLinkByHref($href, $index = 0) {
$this->assertSession()->linkByHrefExists($href, $index);
}
/**
* Passes if a link containing a given href (part) is not found.
*
* @param string $href
* The full or partial value of the 'href' attribute of the anchor tag.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->linkByHrefNotExists() instead.
*/
protected function assertNoLinkByHref($href) {
$this->assertSession()->linkByHrefNotExists($href);
}
/**
* Asserts that a field does not exist with the given ID and value.
*
* @param string $id
* ID of field to assert.
* @param string $value
* (optional) Value for the field, to assert that the field's value on the
* page doesn't match it. You may pass in NULL to skip checking the value,
* while still checking that the field doesn't exist. However, the default
* value ('') asserts that the field value is not an empty string.
*
* @throws \Behat\Mink\Exception\ExpectationException
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldById($id, $value = '') {
$this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
* Passes if the internal browser's URL matches the given path.
*
* @param \Drupal\Core\Url|string $path
* The expected system path or URL.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->addressEquals() instead.
*/
protected function assertUrl($path) {
$this->assertSession()->addressEquals($path);
}
/**
* Asserts that a select option in the current page exists.
*
* @param string $id
* ID of select field to assert.
* @param string $option
* Option to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->optionExists() instead.
*/
protected function assertOption($id, $option) {
return $this->assertSession()->optionExists($id, $option);
}
/**
* Asserts that a select option with the visible text exists.
*
* @param string $id
* The ID of the select field to assert.
* @param string $text
* The text for the option tag to assert.
*
* Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->optionExists() instead.
*/
protected function assertOptionByText($id, $text) {
return $this->assertSession()->optionExists($id, $text);
}
/**
* Asserts that a select option does NOT exist in the current page.
*
* @param string $id
* ID of select field to assert.
* @param string $option
* Option to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->optionNotExists() instead.
*/
protected function assertNoOption($id, $option) {
return $this->assertSession()->optionNotExists($id, $option);
}
/**
* Asserts that a select option in the current page is checked.
*
* @param string $id
* ID of select field to assert.
* @param string $option
* Option to assert.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t(). If left blank, a default message will be displayed.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->optionExists() instead and check the
* "selected" attribute yourself.
*/
protected function assertOptionSelected($id, $option, $message = NULL) {
$option_field = $this->assertSession()->optionExists($id, $option);
$message = $message ?: "Option $option for field $id is selected.";
$this->assertTrue($option_field->hasAttribute('selected'), $message);
}
/**
* Asserts that a checkbox field in the current page is checked.
*
* @param string $id
* ID of field to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->checkboxChecked() instead.
*/
protected function assertFieldChecked($id) {
$this->assertSession()->checkboxChecked($id);
}
/**
* Asserts that a checkbox field in the current page is not checked.
*
* @param string $id
* ID of field to assert.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->checkboxNotChecked() instead.
*/
protected function assertNoFieldChecked($id) {
$this->assertSession()->checkboxNotChecked($id);
}
/**
* Asserts that a field exists in the current page by the given XPath.
*
* @param string $xpath
* XPath used to find the field.
* @param string $value
* (optional) Value of the field to assert. You may pass in NULL (default)
* to skip checking the actual value, while still checking that the field
* exists.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t().
*
* Deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use
* $this->xpath() instead and check the values directly in the test.
*/
protected function assertFieldByXPath($xpath, $value = NULL, $message = '') {
$fields = $this->xpath($xpath);
$this->assertFieldsByValue($fields, $value, $message);
}
/**
* Asserts that a field does not exist or its value does not match, by XPath.
*
* @param string $xpath
* XPath used to find the field.
* @param string $value
* (optional) Value of the field, to assert that the field's value on the
* page does not match it.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t().
*
* @throws \Behat\Mink\Exception\ExpectationException
*
* Deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use
* $this->xpath() instead and assert that the result is empty.
*/
protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '') {
$fields = $this->xpath($xpath);
if (!empty($fields)) {
if (isset($value)) {
$found = FALSE;
try {
$this->assertFieldsByValue($fields, $value);
$found = TRUE;
}
catch (\Exception $e) {
}
if ($found) {
throw new ExpectationException(sprintf('The field resulting from %s was found with the provided value %s.', $xpath, $value), $this->getSession()->getDriver());
}
}
else {
throw new ExpectationException(sprintf('The field resulting from %s was found.', $xpath), $this->getSession()->getDriver());
}
}
}
/**
* Asserts that a field exists in the current page with a given Xpath result.
*
* @param \Behat\Mink\Element\NodeElement[] $fields
* Xml elements.
* @param string $value
* (optional) Value of the field to assert. You may pass in NULL (default) to skip
* checking the actual value, while still checking that the field exists.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages with t().
*
* Deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use
* iteration over the fields yourself instead and directly check the values
* in the test.
*/
protected function assertFieldsByValue($fields, $value = NULL, $message = '') {
// If value specified then check array for match.
$found = TRUE;
if (isset($value)) {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
if ($field->getAttribute('type') == 'checkbox') {
if (is_bool($value)) {
$found = $field->isChecked() == $value;
}
else {
$found = TRUE;
}
}
elseif ($field->getAttribute('value') == $value) {
// Input element with correct value.
$found = TRUE;
}
elseif ($field->find('xpath', '//option[@value = ' . (new Escaper())->escapeLiteral($value) . ' and @selected = "selected"]')) {
// Select element with an option.
$found = TRUE;
}
elseif ($field->getTagName() === 'textarea' && $field->getValue() == $value) {
// Text area with correct text. Use getValue() here because
// getText() would remove any newlines in the value.
$found = TRUE;
}
elseif ($field->getTagName() !== 'input' && $field->getText() == $value) {
$found = TRUE;
}
}
}
}
$this->assertTrue($fields && $found, $message);
}
/**
* Passes if the raw text IS found escaped on the loaded page, fail otherwise.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->assertEscaped() instead.
*/
protected function assertEscaped($raw) {
$this->assertSession()->assertEscaped($raw);
}
/**
* Passes if the raw text is not found escaped on the loaded page.
*
* Raw text refers to the raw HTML that the page generated.
*
* @param string $raw
* Raw (HTML) string to look for.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->assertNoEscaped() instead.
*/
protected function assertNoEscaped($raw) {
$this->assertSession()->assertNoEscaped($raw);
}
/**
* Triggers a pass if the Perl regex pattern is found in the raw content.
*
* @param string $pattern
* Perl regex to look for including the regex delimiters.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseMatches() instead.
*/
protected function assertPattern($pattern) {
$this->assertSession()->responseMatches($pattern);
}
/**
* Triggers a pass if the Perl regex pattern is not found in the raw content.
*
* @param string $pattern
* Perl regex to look for including the regex delimiters.
*
* Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseNotMatches() instead.
*
* @see https://www.drupal.org/node/3129738
*/
protected function assertNoPattern($pattern) {
@trigger_error('AssertLegacyTrait::assertNoPattern() is deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseNotMatches() instead. See https://www.drupal.org/node/3129738', E_USER_DEPRECATED);
$this->assertSession()->responseNotMatches($pattern);
}
/**
* Asserts whether an expected cache tag was present in the last response.
*
* @param string $expected_cache_tag
* The expected cache tag.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseHeaderContains() instead.
*/
protected function assertCacheTag($expected_cache_tag) {
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tag);
}
/**
* Asserts whether an expected cache tag was absent in the last response.
*
* @param string $cache_tag
* The cache tag to check.
*
* Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseHeaderNotContains() instead.
*
* @see https://www.drupal.org/node/3129738
*/
protected function assertNoCacheTag($cache_tag) {
@trigger_error('AssertLegacyTrait::assertNoCacheTag() is deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseHeaderNotContains() instead. See https://www.drupal.org/node/3129738', E_USER_DEPRECATED);
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', $cache_tag);
}
/**
* Checks that current response header equals value.
*
* @param string $name
* Name of header to assert.
* @param string $value
* Value of the header to assert
*
* Deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->responseHeaderEquals() instead.
*/
protected function assertHeader($name, $value) {
$this->assertSession()->responseHeaderEquals($name, $value);
}
/**
* Returns WebAssert object.
*
* @param string $name
* (optional) Name of the session. Defaults to the active session.
*
* @return \Drupal\Tests\WebAssert
* A new web-assert option for asserting the presence of elements with.
*/
abstract public function assertSession($name = NULL);
/**
* Builds an XPath query.
*
* Builds an XPath query by replacing placeholders in the query by the value
* of the arguments.
*
* XPath 1.0 (the version supported by libxml2, the underlying XML library
* used by PHP) doesn't support any form of quotation. This function
* simplifies the building of XPath expression.
*
* @param string $xpath
* An XPath query, possibly with placeholders in the form ':name'.
* @param array $args
* An array of arguments with keys in the form ':name' matching the
* placeholders in the query. The values may be either strings or numeric
* values.
*
* @return string
* An XPath query with arguments replaced.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->assertSession()->buildXPathQuery() instead.
*/
protected function buildXPathQuery($xpath, array $args = []) {
return $this->assertSession()->buildXPathQuery($xpath, $args);
}
/**
* Helper: Constructs an XPath for the given set of attributes and value.
*
* @param string $attribute
* Field attributes.
* @param string $value
* Value of field.
*
* @return string
* XPath for specified values.
*
* Deprecated in drupal:8.5.0 and is removed from drupal:10.0.0. Use
* $this->getSession()->getPage()->findField() instead.
*/
protected function constructFieldXpath($attribute, $value) {
$xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]';
return $this->buildXPathQuery($xpath, [':value' => $value]);
}
/**
* Gets the current raw content.
*
* Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use
* $this->getSession()->getPage()->getContent() instead.
*
* @see https://www.drupal.org/node/3129738
*/
protected function getRawContent() {
@trigger_error('AssertLegacyTrait::getRawContent() is deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->getSession()->getPage()->getContent() instead. See https://www.drupal.org/node/3129738', E_USER_DEPRECATED);
return $this->getSession()->getPage()->getContent();
}
/**
* Get all option elements, including nested options, in a select.
*
* @param \Behat\Mink\Element\NodeElement $element
* The element for which to get the options.
*
* @return \Behat\Mink\Element\NodeElement[]
* Option elements in select.
*
* Deprecated in drupal:8.5.0 and is removed from drupal:10.0.0. Use
* $element->findAll('xpath', 'option') instead.
*
* @see https://www.drupal.org/node/3129738
*/
protected function getAllOptions(NodeElement $element) {
@trigger_error('AssertLegacyTrait::getAllOptions() is deprecated in drupal:8.5.0 and is removed from drupal:10.0.0. Use $element->findAll(\'xpath\', \'option\') instead. See https://www.drupal.org/node/3129738', E_USER_DEPRECATED);
return $element->findAll('xpath', '//option');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\FunctionalTests\Bootstrap;
use Drupal\Core\DependencyInjection\Container;
/**
* Container base class which triggers an error.
*/
class ErrorContainer extends Container {
/**
* {@inheritdoc}
*/
public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) {
if ($id === 'http_kernel') {
// Enforce a recoverable error.
$callable = function (ErrorContainer $container) {
};
$callable(1);
}
else {
return parent::get($id, $invalidBehavior);
}
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Bootstrap;
use Drupal\Core\DependencyInjection\Container;
/**
* Base container which throws an exception.
*/
class ExceptionContainer extends Container {
/**
* {@inheritdoc}
*/
public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) {
if ($id === 'http_kernel') {
throw new \Exception('Thrown exception during Container::get');
}
else {
return parent::get($id, $invalidBehavior);
}
}
}
@@ -0,0 +1,399 @@
<?php
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 {
/**
* Last cURL response.
*
* @var string
*/
protected $response = '';
/**
* Last cURL info.
*
* @var array
*/
protected $info = [];
/**
* Exceptions thrown by site under test that contain this text are ignored.
*
* @var string
*/
protected $expectedExceptionMessage;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['error_service_test', 'error_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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";
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() {
$this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.';
\Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_HIDE)
->save();
$settings = [];
$settings['config']['system.logging']['error_level'] = (object) [
'value' => ERROR_REPORTING_HIDE,
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertNoText($this->expectedExceptionMessage);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_DISPLAY_ALL)
->save();
$settings = [];
$settings['config']['system.logging']['error_level'] = (object) [
'value' => ERROR_REPORTING_DISPLAY_ALL,
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertText($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests displaying an uncaught fatal error.
*/
public function testUncaughtFatalError() {
$fatal_error = [
'%type' => 'TypeError',
'@message' => 'Argument 1 passed to Drupal\error_test\Controller\ErrorTestController::Drupal\error_test\Controller\{closure}() must be of the type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 62',
'%function' => 'Drupal\error_test\Controller\ErrorTestController->Drupal\error_test\Controller\{closure}()',
];
$this->drupalGet('error-test/generate-fatals');
$this->assertResponse(500);
$message = new FormattableMarkup('%type: @message in %function (line ', $fatal_error);
$this->assertRaw((string) $message);
$this->assertRaw('<pre class="backtrace">');
// Ensure we are escaping but not double escaping.
$this->assertRaw('&#039;');
$this->assertNoRaw('&amp;#039;');
}
/**
* Tests uncaught exception handling with custom exception handler.
*/
public function testUncaughtExceptionCustomExceptionHandler() {
$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->assertResponse(418);
$this->assertNoText('The website encountered an unexpected error. Please try again later.');
$this->assertNoText('Oh oh, bananas in the instruments');
$this->assertText('Oh oh, flying teapots');
}
/**
* Tests a missing dependency on a service.
*/
public function testMissingDependency() {
if (version_compare(PHP_VERSION, '7.1') < 0) {
$this->expectedExceptionMessage = 'Argument 1 passed to Drupal\error_service_test\LonelyMonkeyClass::__construct() must be an instance of Drupal\Core\Database\Connection, non';
}
else {
$this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed';
}
$this->drupalGet('broken-service-class');
$this->assertResponse(500);
$this->assertRaw('The website encountered an unexpected error.');
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests a missing dependency on a service with a custom error handler.
*/
public function testMissingDependencyCustomErrorHandler() {
$settings_filename = $this->siteDirectory . '/settings.php';
chmod($settings_filename, 0777);
$settings_php = file_get_contents($settings_filename);
$settings_php .= "\n";
$settings_php .= "set_error_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 .= " exit();\n";
$settings_php .= "});\n";
$settings_php .= "\$settings['teapots'] = TRUE;\n";
file_put_contents($settings_filename, $settings_php);
$this->drupalGet('broken-service-class');
$this->assertResponse(418);
$this->assertSame('Oh oh, flying teapots', $this->response);
}
/**
* Tests a container which has an error.
*/
public function testErrorContainer() {
$settings = [];
$settings['settings']['container_base_class'] = (object) [
'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer',
'required' => TRUE,
];
$this->writeSettings($settings);
\Drupal::service('kernel')->invalidateContainer();
$this->expectedExceptionMessage = 'Argument 1 passed to Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closur';
$this->drupalGet('');
$this->assertResponse(500);
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests a container which has an exception really early.
*/
public function testExceptionContainer() {
$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->assertResponse(500);
$this->assertRaw('The website encountered an unexpected error');
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests the case when the database connection is gone.
*/
public function testLostDatabaseConnection() {
$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->pass('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.');
return;
}
// 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->assertResponse(500);
$this->assertRaw('DatabaseAccessDeniedException');
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests fallback to PHP error log when an exception is thrown while logging.
*/
public function testLoggerException() {
// 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->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertRaw($this->expectedExceptionMessage);
// Find fatal error logged to the error.log
$errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
$this->assertCount(8, $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 = 59;
$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\simpletest\TestBase::prepareEnvironment()
* @see \Drupal\Core\DrupalKernel::bootConfiguration()
*/
protected function assertErrorLogged($error_message) {
$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 (strpos($content, $error_message) !== FALSE) {
$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\simpletest\TestBase::prepareEnvironment()
* @see \Drupal\Core\DrupalKernel::bootConfiguration()
*/
protected function assertNoErrorsLogged() {
// Since PHP only creates the error.log file when an actual error is
// triggered, it is sufficient to check whether the file exists.
$this->assertFileNotExists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
}
/**
* Retrieves a Drupal path or an absolute path.
*
* Executes a cURL request for processing errors and exceptions.
*
* @param string|\Drupal\Core\Url $path
* Request path.
* @param array $extra_options
* (optional) Curl options to pass to curl_setopt()
* @param array $headers
* (optional) Not used.
*/
protected function drupalGet($path, array $extra_options = [], array $headers = []) {
$url = $this->buildUrl($path, ['absolute' => TRUE]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
$this->response = curl_exec($ch);
$this->info = curl_getinfo($ch);
curl_close($ch);
}
/**
* {@inheritdoc}
*/
protected function assertResponse($code) {
$this->assertSame($code, $this->info['http_code']);
}
/**
* {@inheritdoc}
*/
protected function assertText($text) {
$this->assertStringContainsString($text, $this->response);
}
/**
* {@inheritdoc}
*/
protected function assertNoText($text) {
$this->assertStringNotContainsString($text, $this->response);
}
/**
* {@inheritdoc}
*/
protected function assertRaw($text) {
$this->assertText($text);
}
/**
* {@inheritdoc}
*/
protected function assertNoRaw($text) {
$this->assertNoText($text);
}
}
@@ -0,0 +1,62 @@
<?php
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}
*/
public static $modules = ['system', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that different 404s don't create unnecessary cache entries.
*/
public function testBreadcrumbOn404Pages() {
$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;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\FunctionalTests;
use Drupal\Tests\BrowserTestBase;
/**
* Tests BrowserTestBase legacy functionality.
*
* @group browsertestbase
* @group legacy
*/
class BrowserTestBaseLegacyTest extends BrowserTestBase {
/**
* Test ::drupalGetHeaders().
*
* @expectedDeprecation Drupal\Tests\BrowserTestBase::drupalGetHeaders() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use $this->getSession()->getResponseHeaders() instead. See https://www.drupal.org/node/3067207
*/
public function testDrupalGetHeaders() {
$this->assertSame(
$this->getSession()->getResponseHeaders(),
$this->drupalGetHeaders()
);
}
}
@@ -0,0 +1,792 @@
<?php
namespace Drupal\FunctionalTests;
use Behat\Mink\Exception\ExpectationException;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\CronRunTrait;
use PHPUnit\Framework\ExpectationFailedException;
/**
* Tests BrowserTestBase functionality.
*
* @group browsertestbase
*/
class BrowserTestBaseTest extends BrowserTestBase {
use CronRunTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'test_page_test',
'form_test',
'system_test',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* Tests basic page test.
*/
public function testGoTo() {
$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);
// Response includes cache tags that we can assert.
$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',
]);
$returned_header = $this->getSession()->getResponseHeader('Test-Header');
$this->assertSame('header value', $returned_header);
}
/**
* Tests drupalGet().
*/
public function testDrupalGet() {
$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/');
}
/**
* Tests basic form functionality.
*/
public function testForm() {
// 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('redstrawberryhiddenfield');
$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 drupalPostForm().
$edit = ['bananas' => 'red'];
// Submit the form using the button label.
$result = $this->drupalPostForm('form-test/object-builder', $edit, 'Save');
$this->assertSame($this->getSession()->getPage()->getContent(), $result);
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('red', $value);
$this->drupalPostForm('form-test/object-builder', NULL, 'Save');
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('', $value);
// Submit the form using the button id.
$edit = ['bananas' => 'blue'];
$result = $this->drupalPostForm('form-test/object-builder', $edit, 'edit-submit');
$this->assertSame($this->getSession()->getPage()->getContent(), $result);
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('blue', $value);
// Submit the form using the button name.
$edit = ['bananas' => 'purple'];
$result = $this->drupalPostForm('form-test/object-builder', $edit, 'op');
$this->assertSame($this->getSession()->getPage()->getContent(), $result);
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('purple', $value);
// Test drupalPostForm() with no-html response.
$values = Json::decode($this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit')));
$this->assertSame(1000, $values['beer']);
// Test drupalPostForm() 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->drupalPostForm(NULL, [], 'Save', [], 'node-page-form--2');
$this->assertSession()->pageTextContains('Page form2 has been created.');
}
/**
* Tests clickLink() functionality.
*/
public function testClickLink() {
$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() {
$this->expectException('\Exception');
$this->expectExceptionMessage('User notice: foo');
$this->drupalGet('test-error');
}
/**
* Tests linkExists() with pipe character (|) in locator.
*
* @see \Drupal\Tests\WebAssert::linkExists()
*/
public function testPipeCharInLocator() {
$this->drupalGet('test-pipe-char');
$this->assertSession()->linkExists('foo|bar|baz');
}
/**
* Tests linkExistsExact() functionality.
*
* @see \Drupal\Tests\WebAssert::linkExistsExact()
*/
public function testLinkExistsExact() {
$this->drupalGet('test-pipe-char');
$this->assertSession()->linkExistsExact('foo|bar|baz');
}
/**
* Tests linkExistsExact() functionality fail.
*
* @see \Drupal\Tests\WebAssert::linkExistsExact()
*/
public function testInvalidLinkExistsExact() {
$this->drupalGet('test-pipe-char');
$this->expectException(ExpectationException::class);
$this->expectExceptionMessage('Link with label foo|bar found');
$this->assertSession()->linkExistsExact('foo|bar');
}
/**
* Tests linkNotExistsExact() functionality.
*
* @see \Drupal\Tests\WebAssert::linkNotExistsExact()
*/
public function testLinkNotExistsExact() {
$this->drupalGet('test-pipe-char');
$this->assertSession()->linkNotExistsExact('foo|bar');
}
/**
* Tests linkNotExistsExact() functionality fail.
*
* @see \Drupal\Tests\WebAssert::linkNotExistsExact()
*/
public function testInvalidLinkNotExistsExact() {
$this->drupalGet('test-pipe-char');
$this->expectException(ExpectationException::class);
$this->expectExceptionMessage('Link with label foo|bar|baz not found');
$this->assertSession()->linkNotExistsExact('foo|bar|baz');
}
/**
* Tests legacy text asserts.
*/
public function testTextAsserts() {
$this->drupalGet('test-encoded');
$dangerous = 'Bad html <script>alert(123);</script>';
$sanitized = Html::escape($dangerous);
$this->assertNoText($dangerous);
$this->assertText($sanitized);
}
/**
* Tests legacy getRawContent().
*
* @group legacy
* @expectedDeprecation AssertLegacyTrait::getRawContent() is deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->getSession()->getPage()->getContent() instead. See https://www.drupal.org/node/3129738
*/
public function testGetRawContent() {
$this->drupalGet('test-encoded');
$this->assertSame($this->getSession()->getPage()->getContent(), $this->getRawContent());
}
/**
* Tests legacy field asserts which use xpath directly.
*/
public function testXpathAsserts() {
$this->drupalGet('test-field-xpath');
$this->assertFieldsByValue($this->xpath("//h1[@class = 'page-title']"), NULL);
$this->assertFieldsByValue($this->xpath('//table/tbody/tr[2]/td[1]'), 'one');
$this->assertFieldByXPath('//table/tbody/tr[2]/td[1]', 'one');
$this->assertFieldsByValue($this->xpath("//input[@id = 'edit-name']"), 'Test name');
$this->assertFieldByXPath("//input[@id = 'edit-name']", 'Test name');
$this->assertFieldsByValue($this->xpath("//select[@id = 'edit-options']"), '2');
$this->assertFieldByXPath("//select[@id = 'edit-options']", '2');
$this->assertNoFieldByXPath('//notexisting');
$this->assertNoFieldByXPath("//input[@id = 'edit-name']", 'wrong value');
// Test that the assertion fails correctly.
try {
$this->assertFieldByXPath("//input[@id = 'notexisting']");
$this->fail('The "notexisting" field was found.');
}
catch (ExpectationFailedException $e) {
$this->pass('assertFieldByXPath correctly failed. The "notexisting" field was not found.');
}
try {
$this->assertNoFieldByXPath("//input[@id = 'edit-name']");
$this->fail('The "edit-name" field was not found.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldByXPath correctly failed. The "edit-name" field was found.');
}
try {
$this->assertFieldsByValue($this->xpath("//input[@id = 'edit-name']"), 'not the value');
$this->fail('The "edit-name" field is found with the value "not the value".');
}
catch (ExpectationFailedException $e) {
$this->pass('The "edit-name" field is not found with the value "not the value".');
}
}
/**
* Tests legacy field asserts using textfields.
*/
public function testFieldAssertsForTextfields() {
$this->drupalGet('test-field-xpath');
// *** 1. assertNoField().
$this->assertNoField('invalid_name_and_id');
// Test that the assertion fails correctly when searching by name.
try {
$this->assertNoField('name');
$this->fail('The "name" field was not found based on name.');
}
catch (ExpectationException $e) {
$this->pass('assertNoField correctly failed. The "name" field was found by name.');
}
// Test that the assertion fails correctly when searching by id.
try {
$this->assertNoField('edit-name');
$this->fail('The "name" field was not found based on id.');
}
catch (ExpectationException $e) {
$this->pass('assertNoField correctly failed. The "name" field was found by id.');
}
// *** 2. assertField().
$this->assertField('name');
$this->assertField('edit-name');
// Test that the assertion fails correctly if the field does not exist.
try {
$this->assertField('invalid_name_and_id');
$this->fail('The "invalid_name_and_id" field was found.');
}
catch (ExpectationFailedException $e) {
$this->pass('assertField correctly failed. The "invalid_name_and_id" field was not found.');
}
// *** 3. assertNoFieldById().
$this->assertNoFieldById('name');
$this->assertNoFieldById('name', 'not the value');
$this->assertNoFieldById('notexisting');
$this->assertNoFieldById('notexisting', NULL);
// Test that the assertion fails correctly if no value is passed in.
try {
$this->assertNoFieldById('edit-description');
$this->fail('The "description" field, with no value was not found.');
}
catch (ExpectationException $e) {
$this->pass('The "description" field, with no value was found.');
}
// Test that the assertion fails correctly if a NULL value is passed in.
try {
$this->assertNoFieldById('edit-name', NULL);
$this->fail('The "name" field was not found.');
}
catch (ExpectationException $e) {
$this->pass('The "name" field was found.');
}
// *** 4. assertFieldById().
$this->assertFieldById('edit-name', NULL);
$this->assertFieldById('edit-name', 'Test name');
$this->assertFieldById('edit-description', NULL);
$this->assertFieldById('edit-description');
// Test that the assertion fails correctly if no value is passed in.
try {
$this->assertFieldById('edit-name');
$this->fail('The "edit-name" field with no value was found.');
}
catch (ExpectationFailedException $e) {
$this->pass('The "edit-name" field with no value was not found.');
}
// Test that the assertion fails correctly if the wrong value is passed in.
try {
$this->assertFieldById('edit-name', 'not the value');
$this->fail('The "name" field was found, using the wrong value.');
}
catch (ExpectationFailedException $e) {
$this->pass('The "name" field was not found, using the wrong value.');
}
// *** 5. assertNoFieldByName().
$this->assertNoFieldByName('name');
$this->assertNoFieldByName('name', 'not the value');
$this->assertNoFieldByName('notexisting');
$this->assertNoFieldByName('notexisting', NULL);
// Test that the assertion fails correctly if no value is passed in.
try {
$this->assertNoFieldByName('description');
$this->fail('The "description" field, with no value was not found.');
}
catch (ExpectationException $e) {
$this->pass('The "description" field, with no value was found.');
}
// Test that the assertion fails correctly if a NULL value is passed in.
try {
$this->assertNoFieldByName('name', NULL);
$this->fail('The "name" field was not found.');
}
catch (ExpectationException $e) {
$this->pass('The "name" field was found.');
}
// *** 6. assertFieldByName().
$this->assertFieldByName('name');
$this->assertFieldByName('name', NULL);
$this->assertFieldByName('name', 'Test name');
$this->assertFieldByName('description');
$this->assertFieldByName('description', '');
$this->assertFieldByName('description', NULL);
// Test that the assertion fails correctly if given the wrong name.
try {
$this->assertFieldByName('non-existing-name');
$this->fail('The "non-existing-name" field was found.');
}
catch (ExpectationFailedException $e) {
$this->pass('The "non-existing-name" field was not found');
}
// Test that the assertion fails correctly if given the wrong value.
try {
$this->assertFieldByName('name', 'not the value');
$this->fail('The "name" field with incorrect value was found.');
}
catch (ExpectationFailedException $e) {
$this->pass('assertFieldByName correctly failed. The "name" field with incorrect value was not found.');
}
// Test that text areas can contain new lines.
$this->assertFieldsByValue($this->xpath("//textarea[@id = 'edit-test-textarea-with-newline']"), "Test text with\nnewline");
}
/**
* Tests legacy field asserts for options field type.
*/
public function testFieldAssertsForOptions() {
$this->drupalGet('test-field-xpath');
// Option field type.
$this->assertOptionByText('options', 'one');
try {
$this->assertOptionByText('options', 'four');
$this->fail('The select option "four" was found.');
}
catch (ExpectationException $e) {
$this->pass($e->getMessage());
}
$this->assertOption('options', 1);
try {
$this->assertOption('options', 4);
$this->fail('The select option "4" was found.');
}
catch (ExpectationException $e) {
$this->pass($e->getMessage());
}
$this->assertNoOption('options', 'non-existing');
try {
$this->assertNoOption('options', 'one');
$this->fail('The select option "one" was not found.');
}
catch (ExpectationException $e) {
$this->pass($e->getMessage());
}
$this->assertOptionSelected('options', 2);
try {
$this->assertOptionSelected('options', 4);
$this->fail('The select option "4" was selected.');
}
catch (ExpectationException $e) {
$this->pass($e->getMessage());
}
try {
$this->assertOptionSelected('options', 1);
$this->fail('The select option "1" was selected.');
}
catch (ExpectationFailedException $e) {
$this->pass($e->getMessage());
}
}
/**
* Tests legacy field asserts for button field type.
*/
public function testFieldAssertsForButton() {
$this->drupalGet('test-field-xpath');
$this->assertFieldById('edit-save', NULL);
// Test that the assertion fails correctly if the field value is passed in
// rather than the id.
try {
$this->assertFieldById('Save', NULL);
$this->fail('The field with id of "Save" was found.');
}
catch (ExpectationFailedException $e) {
$this->pass($e->getMessage());
}
$this->assertNoFieldById('Save', NULL);
// Test that the assertion fails correctly if the id of an actual field is
// passed in.
try {
$this->assertNoFieldById('edit-save', NULL);
$this->fail('The field with id of "edit-save" was not found.');
}
catch (ExpectationException $e) {
$this->pass($e->getMessage());
}
// Test that multiple fields with the same name are validated correctly.
$this->assertFieldByName('duplicate_button', 'Duplicate button 1');
$this->assertFieldByName('duplicate_button', 'Duplicate button 2');
$this->assertNoFieldByName('duplicate_button', 'Rabbit');
try {
$this->assertNoFieldByName('duplicate_button', 'Duplicate button 2');
$this->fail('The "duplicate_button" field with the value Duplicate button 2 was not found.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldByName correctly failed. The "duplicate_button" field with the value Duplicate button 2 was found.');
}
}
/**
* Tests legacy field asserts for checkbox field type.
*/
public function testFieldAssertsForCheckbox() {
$this->drupalGet('test-field-xpath');
// Part 1 - Test by name.
// Test that checkboxes are found/not found correctly by name, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldByName('checkbox_enabled', TRUE);
$this->assertFieldByName('checkbox_disabled', FALSE);
$this->assertNoFieldByName('checkbox_enabled', FALSE);
$this->assertNoFieldByName('checkbox_disabled', TRUE);
// Test that checkboxes are found by name when using NULL to ignore the
// 'checked' state.
$this->assertFieldByName('checkbox_enabled', NULL);
$this->assertFieldByName('checkbox_disabled', NULL);
// Test that checkboxes are found by name when passing no second parameter.
$this->assertFieldByName('checkbox_enabled');
$this->assertFieldByName('checkbox_disabled');
// Test that we have legacy support.
$this->assertFieldByName('checkbox_enabled', '1');
$this->assertFieldByName('checkbox_disabled', '');
// Test that the assertion fails correctly when using NULL to ignore state.
try {
$this->assertNoFieldByName('checkbox_enabled', NULL);
$this->fail('The "checkbox_enabled" field was not found by name, using NULL value.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.');
}
// Part 2 - Test by ID.
// Test that checkboxes are found/not found correctly by ID, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', TRUE);
$this->assertFieldById('edit-checkbox-disabled', FALSE);
$this->assertNoFieldById('edit-checkbox-enabled', FALSE);
$this->assertNoFieldById('edit-checkbox-disabled', TRUE);
// Test that checkboxes are found by ID, when using NULL to ignore the
// 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', NULL);
$this->assertFieldById('edit-checkbox-disabled', NULL);
// Test that checkboxes are found by ID when passing no second parameter.
$this->assertFieldById('edit-checkbox-enabled');
$this->assertFieldById('edit-checkbox-disabled');
// Test that we have legacy support.
$this->assertFieldById('edit-checkbox-enabled', '1');
$this->assertFieldById('edit-checkbox-disabled', '');
// Test that the assertion fails correctly when using NULL to ignore state.
try {
$this->assertNoFieldById('edit-checkbox-disabled', NULL);
$this->fail('The "edit-checkbox-disabled" field was not found by ID, using NULL value.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldById failed correctly. The "edit-checkbox-disabled" field was found by ID using NULL value.');
}
// Part 3 - Test the specific 'checked' assertions.
$this->assertFieldChecked('edit-checkbox-enabled');
$this->assertNoFieldChecked('edit-checkbox-disabled');
// Test that the assertion fails correctly with non-existent field id.
try {
$this->assertNoFieldChecked('incorrect_checkbox_id');
$this->fail('The "incorrect_checkbox_id" field was found');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldChecked correctly failed. The "incorrect_checkbox_id" field was not found.');
}
// Test that the assertion fails correctly for a checkbox that is checked.
try {
$this->assertNoFieldChecked('edit-checkbox-enabled');
$this->fail('The "edit-checkbox-enabled" field was not found in a checked state.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldChecked correctly failed. The "edit-checkbox-enabled" field was found in a checked state.');
}
// Test that the assertion fails correctly for a checkbox that is not
// checked.
try {
$this->assertFieldChecked('edit-checkbox-disabled');
$this->fail('The "edit-checkbox-disabled" field was found and checked.');
}
catch (ExpectationException $e) {
$this->pass('assertFieldChecked correctly failed. The "edit-checkbox-disabled" field was not found in a checked state.');
}
}
/**
* Tests the ::cronRun() method.
*/
public function testCronRun() {
$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() {
$htaccess_filename = $this->tempFilesDirectory . '/.htaccess';
$this->assertFileExists($htaccess_filename);
}
/**
* Tests the assumption that local time is in 'Australia/Sydney'.
*/
public function testLocalTimeZone() {
$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() {
// 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() {
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() {
$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');
}
/**
* Test the protections provided by .htkey.
*/
public function testHtkey() {
// 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 assertEscaped() and assertUnescaped().
*
* @see \Drupal\Tests\WebAssert::assertNoEscaped()
* @see \Drupal\Tests\WebAssert::assertEscaped()
*/
public function testEscapingAssertions() {
$assert = $this->assertSession();
$this->drupalGet('test-escaped-characters');
$assert->assertNoEscaped('<div class="escaped">');
$assert->responseContains('<div class="escaped">');
$assert->assertEscaped('Escaped: <"\'&>');
$this->drupalGet('test-escaped-script');
$assert->assertNoEscaped('<div class="escaped">');
$assert->responseContains('<div class="escaped">');
$assert->assertEscaped("<script>alert('XSS');alert(\"XSS\");</script>");
$this->drupalGet('test-unescaped-script');
$assert->assertNoEscaped('<div class="unescaped">');
$assert->responseContains('<div class="unescaped">');
$assert->responseContains("<script>alert('Marked safe');alert(\"Marked safe\");</script>");
$assert->assertNoEscaped("<script>alert('Marked safe');alert(\"Marked safe\");</script>");
}
/**
* Tests that deprecation headers do not get duplicated.
*
* @group legacy
*
* @see \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware::__invoke()
*/
public function testDeprecationHeaders() {
$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) {
// Call \Drupal\simpletest\WebTestBase::error() with the parameters from
// the header.
$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);
}
}
@@ -0,0 +1,73 @@
<?php
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;
/**
* Test validation of the User-Agent header we use to perform test requests.
*/
public function testUserAgentValidation() {
$assert_session = $this->assertSession();
$system_path = $this->buildUrl(drupal_get_path('module', 'system'));
$http_path = $system_path . '/tests/http.php/user/login';
$https_path = $system_path . '/tests/https.php/user/login';
// Generate a valid simpletest 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() {
$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,27 @@
<?php
namespace Drupal\FunctionalTests\Core\Config;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\Config\SchemaConfigListenerTestTrait;
/**
* Tests the functionality of ConfigSchemaChecker in KernelTestBase tests.
*
* @group config
*/
class SchemaConfigListenerTest extends BrowserTestBase {
use SchemaConfigListenerTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['config_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\FunctionalTests\Core\Test;
use Drupal\Tests\BrowserTestBase;
/**
* Tests deprecated AssertLegacyTrait functionality.
*
* @group browsertestbase
* @group legacy
*/
class AssertLegacyTraitDeprecatedTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['form_test'];
/**
* Tests getAllOptions().
*
* @expectedDeprecation AssertLegacyTrait::getAllOptions() is deprecated in drupal:8.5.0 and is removed from drupal:10.0.0. Use $element->findAll('xpath', 'option') instead. See https://www.drupal.org/node/3129738
*/
public function testGetAllOptions() {
$this->drupalGet('/form-test/select');
$this->assertCount(6, $this->getAllOptions($this->cssSelect('select[name="opt_groups"]')[0]));
}
}
@@ -0,0 +1,42 @@
<?php
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
*
* @see \Drupal\simpletest\Tests\SimpleTestInstallBatchTest
*/
class ModuleInstallBatchTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public 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() {
foreach ([1, 2] as $id) {
$this->assertNotNull(EntityTest::load($id), 'Successfully loaded entity ' . $id);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\FunctionalTests\Core\Test;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Drupal's integration with Symfony PHPUnit Bridge.
*
* @group Test
* @group legacy
*/
class PhpUnitBridgeTest extends BrowserTestBase {
protected static $modules = ['deprecation_test'];
/**
* @expectedDeprecation This is the deprecation message for deprecation_test_function().
*/
public function testSilencedError() {
$this->assertEquals('known_return_value', deprecation_test_function());
}
/**
* @expectedDeprecation This is the deprecation message for deprecation_test_function().
*/
public function testErrorOnSiteUnderTest() {
$this->drupalGet(Url::fromRoute('deprecation_test.route'));
}
}
@@ -0,0 +1,127 @@
<?php
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}
*/
public static $modules = ['entity_test', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
$this->drupalGet('entity_test/structure/entity_test/display');
$edit = [
'fields[field_timestamp][region]' => 'content',
'fields[field_timestamp][type]' => 'timestamp_ago',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->drupalPostForm(NULL, [], '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->drupalPostForm(NULL, $edit, 'Update');
$this->drupalPostForm(NULL, [], 'Save');
$this->assertSession()->pageTextContains('ends in 1 year');
$this->assertSession()->pageTextContains('started 1 year ago');
}
}
@@ -0,0 +1,154 @@
<?php
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 to pass 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}
*/
public static $modules = ['node', 'entity_test', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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,
]);
$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() {
// 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 "datetime_timestamp" widget is on the page.
$fields = $this->xpath('//div[contains(@class, "field--widget-datetime-timestamp") and @id="edit-field-timestamp-wrapper"]');
$this->assertCount(1, $fields);
// 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->drupalPostForm(NULL, $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));
}
}
@@ -0,0 +1,123 @@
<?php
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}
*/
public static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
/** @var ContentEntityStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId);
/** @var 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->drupalPostForm(NULL, $edit, t('Add another item'));
$this->drupalPostForm(NULL, [], t('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($entity->get($this->fieldName)->getValue(), [
['shape' => 'circle', 'color' => 'blue'],
['shape' => 'rectangle', 'color' => 'green'],
]);
}
}
@@ -0,0 +1,169 @@
<?php
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}
*/
public static $modules = ['entity_test', 'field_test', 'file', 'image'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
$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.form-type-textfield input', 1);
$this->drupalPostForm(NULL, [], 'Add another item');
$assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.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->drupalPostForm(NULL, $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 throws a validation error.
$field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $this->fieldNameMultiple);
$field_config->setRequired(TRUE);
$field_config->save();
$this->drupalPostForm($this->entityTypeId . '/add', [], 'Add another item');
$assert_session->pageTextContains('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->drupalPostForm(NULL, [], 'Save');
$assert_session->pageTextContains('Test single field is required.');
$assert_session->pageTextContains('Test multiple (value 1) field is required.');
}
}
@@ -0,0 +1,162 @@
<?php
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;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['entity_test', 'user', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
ConfigurableLanguage::create(['id' => 'es'])->save();
ConfigurableLanguage::create(['id' => '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', '.page-title', '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 > div.item-list > ul';
$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() {
$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', '.page-title', 'Are you sure you want to delete these test entity - revisions entities?');
$list_selector = '#entity-test-rev-delete-multiple-confirm-form > div.item-list > ul';
$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
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;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['cache_test', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
// 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,33 @@
<?php
namespace Drupal\FunctionalTests;
use Drupal\Tests\BrowserTestBase;
/**
* This test will check BrowserTestBase's treatment of hook_install during
* setUp.
* Image module is used for test.
*
* @group browsertestbase
*/
class FolderTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['image'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testFolderSetup() {
$directory = 'public://styles';
$this->assertTrue(\Drupal::service('file_system')->prepareDirectory($directory, FALSE), 'Directory created.');
}
}
@@ -0,0 +1,36 @@
<?php
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() {
$method_caller = $this->getTestMethodCaller();
$expected = [
'file' => __FILE__,
'line' => 23,
'function' => __CLASS__ . '->' . __FUNCTION__ . '()',
'class' => BrowserTestBase::class,
'object' => $this,
'type' => '->',
'args' => [],
];
$this->assertEquals($expected, $method_caller);
}
}
@@ -0,0 +1,36 @@
<?php
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() {
$method_caller = $this->getTestMethodCaller();
$expected = [
'file' => __FILE__,
'line' => 23,
'function' => __CLASS__ . '->' . __FUNCTION__ . '()',
'class' => BrowserTestBase::class,
'object' => $this,
'type' => '->',
'args' => [],
];
$this->assertEquals($expected, $method_caller);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonAnonTest extends BaseFieldOverrideResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonBasicAuthTest extends BaseFieldOverrideResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonCookieTest extends BaseFieldOverrideResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonAnonTest extends DateFormatResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonBasicAuthTest extends DateFormatResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonCookieTest extends DateFormatResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonAnonTest extends EntityFormDisplayResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonBasicAuthTest extends EntityFormDisplayResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonCookieTest extends EntityFormDisplayResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonAnonTest extends EntityFormModeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonBasicAuthTest extends EntityFormModeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonCookieTest extends EntityFormModeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonAnonTest extends EntityViewDisplayResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,29 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonBasicAuthTest extends EntityViewDisplayHalJsonAnonTest {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonCookieTest extends EntityViewDisplayHalJsonAnonTest {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonAnonTest extends EntityViewModeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonBasicAuthTest extends EntityViewModeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonCookieTest extends EntityViewModeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,96 @@
<?php
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}
*/
public static $modules = ['system', 'test_page_test', 'page_cache'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testCrossSiteRequest() {
// Test default parameters.
$cors_config = $this->container->getParameter('cors.config');
$this->assertSame(FALSE, $cors_config['enabled']);
$this->assertSame([], $cors_config['allowedHeaders']);
$this->assertSame([], $cors_config['allowedMethods']);
$this->assertSame(['*'], $cors_config['allowedOrigins']);
$this->assertSame(FALSE, $cors_config['exposedHeaders']);
$this->assertSame(FALSE, $cors_config['maxAge']);
$this->assertSame(FALSE, $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', 'http://example.com');
// 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', 'http://example.com');
// 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', 'http://example.org');
// Configure the CORS stack to allow a specific set of origins.
$cors_config['allowedOrigins'] = ['http://example.com'];
$this->setContainerParameter('cors.config', $cors_config);
$this->rebuildContainer();
// Fire a request from an origin that isn't allowed.
/** @var \Symfony\Component\HttpFoundation\Response $response */
$this->drupalGet('/test-page', [], ['Origin' => 'http://non-valid.com']);
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->pageTextContains('Not allowed.');
// 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');
// 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');
$response = $httpClient->request('POST', $url->setAbsolute()->toString(), [
'headers' => [
'Origin' => $origin,
],
]);
$this->assertEquals(200, $response->getStatusCode());
}
}
@@ -0,0 +1,79 @@
<?php
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;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['system', 'image_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Test Image toolkit setup form.
*/
public function testToolkitSetupForm() {
// Get form.
$this->drupalGet('admin/config/media/image-toolkit');
// Test that default toolkit is GD.
$this->assertFieldByName('image_toolkit', 'gd', 'The default image toolkit is GD.');
// Test changing the jpeg image quality.
$edit = ['gd[image_jpeg_quality]' => '70'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image.gd')->get('jpeg_quality'), '70');
// Test changing the toolkit.
$edit = ['image_toolkit' => 'test'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image')->get('toolkit'), 'test');
$this->assertFieldByName('test[test_parameter]', '10');
// Test changing the test toolkit parameter.
$edit = ['test[test_parameter]' => '0'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertText(t('Test parameter should be different from 0.'), 'Validation error displayed.');
$edit = ['test[test_parameter]' => '20'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image.test_toolkit')->get('test_parameter'), '20');
// 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);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Drupal\FunctionalTests\Image;
/**
* Tests image toolkit functions.
*
* @group Image
*/
class ToolkitTest extends ToolkitTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Check that ImageToolkitManager::getAvailableToolkits() only returns
* available toolkits.
*/
public function testGetAvailableToolkits() {
$manager = $this->container->get('image.toolkit.manager');
$toolkits = $manager->getAvailableToolkits();
$this->assertTrue(isset($toolkits['test']), 'The working toolkit was returned.');
$this->assertTrue(isset($toolkits['test:derived_toolkit']), 'The derived toolkit was returned.');
$this->assertFalse(isset($toolkits['broken']), 'The toolkit marked unavailable was not returned');
$this->assertToolkitOperationsCalled([]);
}
/**
* Tests Image's methods.
*/
public function testLoad() {
$image = $this->getImage();
$this->assertIsObject($image);
$this->assertEqual($image->getToolkitId(), 'test', 'Image had toolkit set.');
$this->assertToolkitOperationsCalled(['parseFile']);
}
/**
* Test the image_save() function.
*/
public function testSave() {
$this->assertFalse($this->image->save(), 'Function returned the expected value.');
$this->assertToolkitOperationsCalled(['save']);
}
/**
* Test the image_apply() function.
*/
public function testApply() {
$data = ['p1' => 1, 'p2' => TRUE, 'p3' => 'text'];
$this->assertTrue($this->image->apply('my_operation', $data), 'Function returned the expected value.');
// Check that apply was called and with the correct parameters.
$this->assertToolkitOperationsCalled(['apply']);
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['apply'][0][0], 'my_operation', "'my_operation' was passed correctly as operation");
$this->assertEqual($calls['apply'][0][1]['p1'], 1, 'integer parameter p1 was passed correctly');
$this->assertEqual($calls['apply'][0][1]['p2'], TRUE, 'boolean parameter p2 was passed correctly');
$this->assertEqual($calls['apply'][0][1]['p3'], 'text', 'string parameter p3 was passed correctly');
}
/**
* Test the image_apply() function.
*/
public function testApplyNoParameters() {
$this->assertTrue($this->image->apply('my_operation'), 'Function returned the expected value.');
// Check that apply was called and with the correct parameters.
$this->assertToolkitOperationsCalled(['apply']);
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['apply'][0][0], 'my_operation', "'my_operation' was passed correctly as operation");
$this->assertEqual($calls['apply'][0][1], [], 'passing no parameters was handled correctly');
}
/**
* Tests image toolkit operations inheritance by derivative toolkits.
*/
public function testDerivative() {
$toolkit_manager = $this->container->get('image.toolkit.manager');
$operation_manager = $this->container->get('image.toolkit.operation.manager');
$toolkit = $toolkit_manager->createInstance('test:derived_toolkit');
// Load an overwritten and an inherited operation.
$blur = $operation_manager->getToolkitOperation($toolkit, 'blur');
$invert = $operation_manager->getToolkitOperation($toolkit, 'invert');
$this->assertIdentical('foo_derived', $blur->getPluginId(), "'Blur' operation overwritten by derivative.");
$this->assertIdentical('bar', $invert->getPluginId(), '"Invert" operation inherited from base plugin.');
}
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\FunctionalTests\Image;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Base class for image manipulation testing.
*/
abstract class ToolkitTestBase extends BrowserTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['image_test'];
/**
* The URI for the file.
*
* @var string
*/
protected $file;
/**
* The image factory service.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* The image object for the test file.
*
* @var \Drupal\Core\Image\ImageInterface
*/
protected $image;
protected function setUp() {
parent::setUp();
// Set the image factory service.
$this->imageFactory = $this->container->get('image.factory');
// Pick a file for testing.
$file = current($this->drupalGetTestFiles('image'));
$this->file = $file->uri;
// Setup a dummy image to work with.
$this->image = $this->getImage();
// Clear out any hook calls.
$this->imageTestReset();
}
/**
* Sets up an image with the custom toolkit.
*
* @return \Drupal\Core\Image\ImageInterface
* The image object.
*/
protected function getImage() {
$image = $this->imageFactory->get($this->file, 'test');
$this->assertTrue($image->isValid(), 'Image file was parsed.');
return $image;
}
/**
* Assert that all of the specified image toolkit operations were called
* exactly once once, other values result in failure.
*
* @param $expected
* Array with string containing with the operation name, e.g. 'load',
* 'save', 'crop', etc.
*/
public function assertToolkitOperationsCalled(array $expected) {
// If one of the image operations is expected, apply should be expected as
// well.
$operations = [
'resize',
'rotate',
'crop',
'desaturate',
'create_new',
'scale',
'scale_and_crop',
'my_operation',
'convert',
];
if (count(array_intersect($expected, $operations)) > 0 && !in_array('apply', $expected)) {
$expected[] = 'apply';
}
// Determine which operations were called.
$actual = array_keys(array_filter($this->imageTestGetAllCalls()));
// Determine if there were any expected that were not called.
$uncalled = array_diff($expected, $actual);
if (count($uncalled)) {
$this->assertTrue(FALSE, new FormattableMarkup('Expected operations %expected to be called but %uncalled was not called.', ['%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)]));
}
else {
$this->assertTrue(TRUE, new FormattableMarkup('All the expected operations were called: %expected', ['%expected' => implode(', ', $expected)]));
}
// Determine if there were any unexpected calls.
// If all unexpected calls are operations and apply was expected, we do not
// count it as an error.
$unexpected = array_diff($actual, $expected);
if (count($unexpected) && (!in_array('apply', $expected) || count(array_intersect($unexpected, $operations)) !== count($unexpected))) {
$this->assertTrue(FALSE, new FormattableMarkup('Unexpected operations were called: %unexpected.', ['%unexpected' => implode(', ', $unexpected)]));
}
else {
$this->assertTrue(TRUE, 'No unexpected operations were called.');
}
}
/**
* Resets/initializes the history of calls to the test toolkit functions.
*/
protected function imageTestReset() {
// Keep track of calls to these operations
$results = [
'parseFile' => [],
'save' => [],
'settings' => [],
'apply' => [],
'resize' => [],
'rotate' => [],
'crop' => [],
'desaturate' => [],
'create_new' => [],
'scale' => [],
'scale_and_crop' => [],
'convert' => [],
];
\Drupal::state()->set('image_test.results', $results);
}
/**
* Gets an array of calls to the test toolkit.
*
* @return array
* An array keyed by operation name ('parseFile', 'save', 'settings',
* 'resize', 'rotate', 'crop', 'desaturate') with values being arrays of
* parameters passed to each call.
*/
protected function imageTestGetAllCalls() {
return \Drupal::state()->get('image_test.results') ?: [];
}
}
@@ -0,0 +1,39 @@
<?php
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,133 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
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() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.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() {
// 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->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getAccountName());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
$this->assertArrayNotHasKey('install_profile', Settings::getAll(), 'The install profile has not been written to settings.php.');
$this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.');
$this->rebuildContainer();
$this->pass('Container can be rebuilt even though distribution is not written to settings.php.');
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
}
}
@@ -0,0 +1,84 @@
<?php
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() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'bartik',
'finish_url' => '/myrootuser',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
file_put_contents("$path/mydistro.install", "<?php function mydistro_install() {\Drupal::entityTypeManager()->getStorage('path_alias')->create(['path' => '/user/1', 'alias' => '/myrootuser'])->save();}");
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the distribution name is used in the site title.
$this->assertTitle('Choose language | ' . $this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('myrootuser');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getAccountName());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
$this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.');
}
}
@@ -0,0 +1,144 @@
<?php
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() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'langcode' => $this->langcode,
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.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() {
// 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() {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// The language should have been automatically detected, all following
// screens should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), '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->assertEqual($direction, 'ltr');
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('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) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
ENDPO;
}
}
@@ -0,0 +1,135 @@
<?php
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() {
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' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.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() {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// The language should have been automatically detected, all following
// screens should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = current($this->xpath('/@dir'))->getText();
$this->assertEqual($direction, 'ltr');
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('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) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
ENDPO;
}
}

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