first commit
This commit is contained in:
@@ -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 & 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 & 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);
|
||||
}
|
||||
|
||||
}
|
||||
+138
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
+132
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+166
@@ -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 " and ' 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());
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user