Commit 38144b9e authored by catch's avatar catch

Issue #3176655 by alexpott, jungle, longwave, ayushmishra206, Gábor Hojtsy,...

Issue #3176655 by alexpott, jungle, longwave, ayushmishra206, Gábor Hojtsy, andypost: Deprecate GoutteDriver use in core and use Behat\Mink\Driver\BrowserKitDriver directly instead
parent 99b763a1
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1387b767469672675eafc323b5e0b55a",
"content-hash": "460aef4d3073d9f5e8e6a2dae54468d6",
"packages": [
{
"name": "asm89/stack-cors",
......
......@@ -8,6 +8,7 @@
},
"require": {
"behat/mink": "^1.8",
"behat/mink-browserkit-driver": "^1.3",
"behat/mink-goutte-driver": "^1.2",
"behat/mink-selenium2-driver": "^1.4",
"composer/composer": "^2.0.2",
......
......@@ -2,11 +2,11 @@
namespace Drupal\BuildTests\Framework;
use Behat\Mink\Driver\Goutte\Client;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Driver\BrowserKitDriver;
use Behat\Mink\Mink;
use Behat\Mink\Session;
use Drupal\Component\FileSystem\FileSystem as DrupalFilesystem;
use Drupal\Tests\DrupalTestBrowser;
use Drupal\Tests\PhpUnitCompatibilityTrait;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
......@@ -219,9 +219,9 @@ protected function getWorkingPath($working_dir = NULL) {
* @return \Behat\Mink\Session
*/
protected function initMink() {
$client = new Client();
$client = new DrupalTestBrowser();
$client->followMetaRefresh(TRUE);
$driver = new GoutteDriver($client);
$driver = new BrowserKitDriver($client);
$session = new Session($driver);
$this->mink = new Mink();
$this->mink->registerSession('default', $session);
......
......@@ -2,7 +2,6 @@
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Driver\GoutteDriver;
use PHPUnit\Framework\AssertionFailedError;
/**
......@@ -41,6 +40,10 @@ public function testJavascript() {
}());
JS;
$this->assertJsCondition($javascript);
// Ensure that \Drupal\Tests\UiHelperTrait::isTestUsingGuzzleClient() works
// as expected.
$this->assertFalse($this->isTestUsingGuzzleClient());
}
public function testAssertJsCondition() {
......@@ -151,9 +154,9 @@ protected function drupalGetWithAlert($path, array $options = [], array $headers
$this->metaRefreshCount = 0;
}
// Log only for WebDriverTestBase tests because for Goutte we log with
// ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
// Log only for WebDriverTestBase tests because for DrupalTestBrowser we log
// with ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !$this->isTestUsingGuzzleClient()) {
$html_output = 'GET request to: ' . $url .
'<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
$html_output .= '<hr />' . $out;
......
......@@ -78,6 +78,10 @@ public function testGoTo() {
]);
$this->assertSession()->responseHeaderExists('Test-Header');
$this->assertSession()->responseHeaderEquals('Test-Header', 'header value');
// Ensure that \Drupal\Tests\UiHelperTrait::isTestUsingGuzzleClient() works
// as expected.
$this->assertTrue($this->isTestUsingGuzzleClient());
}
/**
......
<?php
namespace Drupal\FunctionalTests;
use Behat\Mink\Driver\GoutteDriver;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests legacy support for GoutteDriver
*
* @group browsertestbase
*/
class GoutteDriverTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $minkDefaultDriverClass = GoutteDriver::class;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests basic page test.
*
* @group legacy
*/
public function testGoTo() {
$this->expectDeprecation('Using \Behat\Mink\Driver\GoutteDriver is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. The dependencies behat/mink-goutte-driver and fabpot/goutte will be removed. See https://www.drupal.org/node/3177235');
$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()->responseHeaderExists('X-Drupal-Cache-Tags');
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'http_response rendered');
// Test that we can read the JS settings.
$js_settings = $this->getDrupalSettings();
$this->assertSame('azAZ09();.,\\\/-_{}', $js_settings['test-setting']);
// Test drupalGet with a url object.
$url = Url::fromRoute('test_page_test.render_title');
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(200);
// Test page contains some text.
$this->assertSession()->pageTextContains('Hello Drupal');
// Ensure that \Drupal\Tests\UiHelperTrait::isTestUsingGuzzleClient() works
// as expected.
$this->assertTrue($this->isTestUsingGuzzleClient());
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\Tests;
use Behat\Mink\Driver\BrowserKitDriver;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Element\Element;
use Behat\Mink\Mink;
......@@ -145,7 +146,7 @@ abstract class BrowserTestBase extends TestCase {
*
* @var string
*/
protected $minkDefaultDriverClass = GoutteDriver::class;
protected $minkDefaultDriverClass = BrowserKitDriver::class;
/*
* Mink default driver params.
......@@ -220,8 +221,11 @@ abstract class BrowserTestBase extends TestCase {
*/
protected function initMink() {
$driver = $this->getDefaultDriverInstance();
if ($driver instanceof GoutteDriver) {
@trigger_error('Using \Behat\Mink\Driver\GoutteDriver is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. The dependencies behat/mink-goutte-driver and fabpot/goutte will be removed. See https://www.drupal.org/node/3177235', E_USER_DEPRECATED);
}
if ($driver instanceof BrowserKitDriver) {
// Turn off curl timeout. Having a timeout is not a problem in a normal
// test running, but it is a problem when debugging. Also, disable SSL
// peer verification so that testing under HTTPS always works.
......@@ -306,7 +310,10 @@ protected function getDefaultDriverInstance() {
}
}
if (is_array($this->minkDefaultDriverArgs)) {
if ($this->minkDefaultDriverClass === BrowserKitDriver::class) {
$driver = new $this->minkDefaultDriverClass(new DrupalTestBrowser());
}
elseif (is_array($this->minkDefaultDriverArgs)) {
// Use ReflectionClass to instantiate class with received params.
$reflector = new \ReflectionClass($this->minkDefaultDriverClass);
$driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
......@@ -539,7 +546,7 @@ protected function getSessionCookies() {
protected function getHttpClient() {
/* @var $mink_driver \Behat\Mink\Driver\DriverInterface */
$mink_driver = $this->getSession()->getDriver();
if ($mink_driver instanceof GoutteDriver) {
if ($this->isTestUsingGuzzleClient()) {
return $mink_driver->getClient()->getClient();
}
throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().');
......
......@@ -2,9 +2,11 @@
namespace Drupal\Tests\Core\Test;
use Behat\Mink\Driver\GoutteDriver;
use Drupal\Tests\DrupalTestBrowser;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\BrowserTestBase;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Driver\BrowserKitDriver;
use Behat\Mink\Session;
use Goutte\Client;
......@@ -19,7 +21,7 @@ protected function mockBrowserTestBaseWithDriver($driver) {
->disableOriginalConstructor()
->setMethods(['getDriver'])
->getMock();
$session->expects($this->once())
$session->expects($this->any())
->method('getDriver')
->willReturn($driver);
......@@ -27,7 +29,7 @@ protected function mockBrowserTestBaseWithDriver($driver) {
->disableOriginalConstructor()
->setMethods(['getSession'])
->getMockForAbstractClass();
$btb->expects($this->once())
$btb->expects($this->any())
->method('getSession')
->willReturn($session);
......@@ -41,21 +43,41 @@ public function testGetHttpClient() {
// Our stand-in for the Guzzle client object.
$expected = new \stdClass();
$browserkit_client = $this->getMockBuilder(Client::class)
$browserkit_client = $this->getMockBuilder(DrupalTestBrowser::class)
->setMethods(['getClient'])
->getMockForAbstractClass();
$browserkit_client->expects($this->once())
->method('getClient')
->willReturn($expected);
// Because the driver is a GoutteDriver, we'll get back a client.
$driver = $this->getMockBuilder(GoutteDriver::class)
// Because the driver is a BrowserKitDriver, we'll get back a client.
$driver = new BrowserKitDriver($browserkit_client);
$btb = $this->mockBrowserTestBaseWithDriver($driver);
$ref_gethttpclient = new \ReflectionMethod($btb, 'getHttpClient');
$ref_gethttpclient->setAccessible(TRUE);
$this->assertSame(get_class($expected), get_class($ref_gethttpclient->invoke($btb)));
}
/**
* @covers ::getHttpClient
*
* @group legacy
*/
public function testGetHttpClientGoutte() {
// Our stand-in for the Guzzle client object.
$expected = new \stdClass();
$browserkit_client = $this->getMockBuilder(Client::class)
->setMethods(['getClient'])
->getMock();
$driver->expects($this->once())
->getMockForAbstractClass();
$browserkit_client->expects($this->once())
->method('getClient')
->willReturn($browserkit_client);
->willReturn($expected);
// Because the driver is a GoutteDriver, we'll get back a client.
$driver = new GoutteDriver($browserkit_client);
$btb = $this->mockBrowserTestBaseWithDriver($driver);
$ref_gethttpclient = new \ReflectionMethod($btb, 'getHttpClient');
......@@ -68,7 +90,7 @@ public function testGetHttpClient() {
* @covers ::getHttpClient
*/
public function testGetHttpClientException() {
// A driver type that isn't GoutteDriver. This should cause a
// A driver type that isn't BrowserKitDriver. This should cause a
// RuntimeException.
$btb = $this->mockBrowserTestBaseWithDriver(new \stdClass());
......
<?php
namespace Drupal\Tests;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Response;
/**
* Enables a BrowserKitDriver mink driver to use a Guzzle client.
*
* This code is heavily based on the following projects:
* - https://github.com/FriendsOfPHP/Goutte
* - https://github.com/minkphp/MinkGoutteDriver
*/
class DrupalTestBrowser extends AbstractBrowser {
/**
* The Guzzle client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $client;
/**
* Sets the Guzzle client.
*
* @param \GuzzleHttp\ClientInterface $client
* The Guzzle client.
*
* @return $this
*/
public function setClient(ClientInterface $client) {
$this->client = $client;
if ($this->getServerParameter('HTTP_HOST', NULL) !== NULL || $base_uri = $client->getConfig('base_uri') === NULL) {
return $this;
}
$path = $base_uri->getPath();
if ($path !== '' && $path !== '/') {
throw new \InvalidArgumentException('Setting a path in the Guzzle "base_uri" config option is not supported by DrupalTestBrowser.');
}
if ($this->getServerParameter('HTTPS', NULL) === NULL && $base_uri->getScheme() === 'https') {
$this->setServerParameter('HTTPS', 'on');
}
$host = $base_uri->getHost();
if (($port = $base_uri->getPort()) !== NULL) {
$host .= ":$port";
}
$this->setServerParameter('HTTP_HOST', $host);
return $this;
}
/**
* Gets the Guzzle client.
*
* @return \GuzzleHttp\ClientInterface
* The Guzzle client.
*/
public function getClient() {
if (!$this->client) {
$this->client = new Client([
'allow_redirects' => FALSE,
'cookies' => TRUE,
]);
}
return $this->client;
}
/**
* {@inheritdoc}
*/
protected function doRequest($request) {
$headers = [];
foreach ($request->getServer() as $key => $val) {
$key = strtolower(str_replace('_', '-', $key));
$content_headers = [
'content-length' => TRUE,
'content-md5' => TRUE,
'content-type' => TRUE,
];
if (strpos($key, 'http-') === 0) {
$headers[substr($key, 5)] = $val;
}
// CONTENT_* are not prefixed with HTTP_
elseif (isset($content_headers[$key])) {
$headers[$key] = $val;
}
}
$cookies = CookieJar::fromArray(
$this->getCookieJar()->allRawValues($request->getUri()),
parse_url($request->getUri(), PHP_URL_HOST)
);
$request_options = [
'cookies' => $cookies,
'allow_redirects' => FALSE,
];
if (!\in_array($request->getMethod(), ['GET', 'HEAD'], TRUE)) {
if (NULL !== $content = $request->getContent()) {
$request_options['body'] = $content;
}
else {
if ($files = $request->getFiles()) {
$request_options['multipart'] = [];
$this->addPostFields($request->getParameters(), $request_options['multipart']);
$this->addPostFiles($files, $request_options['multipart']);
}
else {
$request_options['form_params'] = $request->getParameters();
}
}
}
if (!empty($headers)) {
$request_options['headers'] = $headers;
}
$method = $request->getMethod();
$uri = $request->getUri();
// Let BrowserKit handle redirects
try {
$response = $this->getClient()->request($method, $uri, $request_options);
}
catch (RequestException $e) {
$response = $e->getResponse();
if (NULL === $response) {
throw $e;
}
}
return $this->createResponse($response);
}
/**
* Adds files to the $multipart array.
*
* @param array $files
* The files.
* @param array $multipart
* A reference to the multipart array to add the files to.
* @param string $array_name
* Internal parameter used by recursive calls.
*/
protected function addPostFiles(array $files, array &$multipart, $array_name = '') {
if (empty($files)) {
return;
}
foreach ($files as $name => $info) {
if (!empty($array_name)) {
$name = $array_name . '[' . $name . ']';
}
$file = [
'name' => $name,
];
if (\is_array($info)) {
if (isset($info['tmp_name'])) {
if ($info['tmp_name'] !== '') {
$file['contents'] = fopen($info['tmp_name'], 'r');
if (isset($info['name'])) {
$file['filename'] = $info['name'];
}
}
else {
continue;
}
}
else {
$this->addPostFiles($info, $multipart, $name);
continue;
}
}
else {
$file['contents'] = fopen($info, 'r');
}
$multipart[] = $file;
}
}
/**
* Adds form parameters to the $multipart array.
*
* @param array $formParams
* The form parameters.
* @param array $multipart
* A reference to the multipart array to add the form parameters to.
* @param string $array_name
* Internal parameter used by recursive calls.
*/
public function addPostFields(array $formParams, array &$multipart, $array_name = '') {
foreach ($formParams as $name => $value) {
if (!empty($array_name)) {
$name = $array_name . '[' . $name . ']';
}
if (\is_array($value)) {
$this->addPostFields($value, $multipart, $name);
}
else {
$multipart[] = [
'name' => $name,
'contents' => $value,
];
}
}
}
/**
* Converts a Guzzle response to a BrowserKit response.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The Guzzle response.
*
* @return \Symfony\Component\BrowserKit\Response
* A BrowserKit Response instance.
*/
protected function createResponse(ResponseInterface $response) {
return new Response((string) $response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
/**
* Reads response meta tags to guess content-type charset.
*
* @param \Symfony\Component\BrowserKit\Response $response
* The origin response to filter.
*
* @return \Symfony\Component\BrowserKit\Response
* A BrowserKit Response instance.
*/
protected function filterResponse($response) {
$content_type = $response->getHeader('Content-Type');
if (!$content_type || strpos($content_type, 'charset=') === FALSE) {
if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $response->getContent(), $matches)) {
$headers = $response->getHeaders();
$headers['Content-Type'] = $content_type . ';charset=' . $matches[1];
$response = new Response($response->getContent(), $response->getStatus(), $headers);
}
}
return parent::filterResponse($response);
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\Tests;
use Behat\Mink\Driver\BrowserKitDriver;
use Behat\Mink\Driver\GoutteDriver;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
......@@ -107,9 +108,9 @@ protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
$this->metaRefreshCount = 0;
}
// Log only for WebDriverTestBase tests because for Goutte we log with
// ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
// Log only for WebDriverTestBase tests because for tests using
// DrupalTestBrowser we log with ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !$this->isTestUsingGuzzleClient()) {
$out = $this->getSession()->getPage()->getContent();
$html_output = 'POST request to: ' . $action .
'<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
......@@ -339,9 +340,9 @@ protected function drupalGet($path, array $options = [], array $headers = []) {
$this->metaRefreshCount = 0;
}
// Log only for WebDriverTestBase tests because for Goutte we log with
// ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
// Log only for WebDriverTestBase tests because for BrowserKitDriver we log
// with ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !$this->isTestUsingGuzzleClient()) {
$html_output = 'GET request to: ' . $url .
'<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
$html_output .= '<hr />' . $out;
......@@ -465,9 +466,9 @@ protected function drupalUserIsLoggedIn(AccountInterface $account) {
protected function click($css_selector) {
$starting_url = $this->getSession()->getCurrentUrl();
$this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector));
// Log only for WebDriverTestBase tests because for Goutte we log with
// ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
// Log only for WebDriverTestBase tests because for BrowserKitDriver we log
// with ::getResponseLogHandler.
if ($this->htmlOutputEnabled && !$this->isTestUsingGuzzleClient()) {
$out = $this->getSession()->getPage()->getContent();
$html_output =
'Clicked element with CSS selector: ' . $css_selector .
......@@ -553,4 +554,22 @@ protected function cssSelect($selector) {
return $this->getSession()->getPage()->findAll('css', $selector);
}
/**
* Determines if test is using DrupalTestBrowser.
*
* @return bool
* TRUE if test is using DrupalTestBrowser.
*/