Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,44 @@
<?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'];
/**
* 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"]'));
}
}

View file

@ -0,0 +1,108 @@
<?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'];
/**
* 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->assertEqual(0, count($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->assertEqual(0, count($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);
}
}

View file

@ -0,0 +1,42 @@
<?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'];
/**
* Tests image buttons can be operated with the keyboard ENTER key.
*/
public function testAjaxImageButton() {
// Get a Field UI manage-display page.
$this->drupalGet('ajax_forms_image_button_form');
$assertSession = $this->assertSession();
$session = $this->getSession();
$enter_key_event = <<<JS
jQuery('#edit-image-button')
.trigger(jQuery.Event('keypress', {
which: 13
}));
JS;
// PhantomJS driver has buggy behavior with key events, we send a JavaScript
// key event instead.
// @todo: use WebDriver event when we remove PhantomJS driver.
$session->executeScript($enter_key_event);
$this->assertNotEmpty($assertSession->waitForElementVisible('css', '#ajax-1-more-div'), 'Page updated after image button pressed');
}
}

View file

@ -2,14 +2,14 @@
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Performs tests on AJAX forms in cached pages.
*
* @group Ajax
*/
class AjaxFormPageCacheTest extends JavascriptTestBase {
class AjaxFormPageCacheTest extends WebDriverTestBase {
/**
* {@inheritdoc}
@ -41,7 +41,6 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
*/
public function testSimpleAJAXFormValue() {
$this->drupalGet('ajax_forms_test_get_form');
$this->assertEquals($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
$build_id_initial = $this->getFormBuildId();
// Changing the value of a select input element, triggers a AJAX
@ -55,8 +54,8 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
// Wait for the DOM to update. The HtmlCommand will update
// #ajax_selected_color to reflect the color change.
$green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')");
$this->assertNotNull($green_div, 'DOM update: The selected color DIV is green.');
$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();
@ -67,8 +66,8 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
$session->getPage()->selectFieldOption('select', 'red');
// Wait for the DOM to update.
$red_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')");
$this->assertNotNull($red_div, 'DOM update: The selected color DIV is red.');
$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();
@ -77,7 +76,6 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
// Emulate a push of the reload button and then repeat the test sequence
// this time with a page loaded from the cache.
$session->reload();
$this->assertEquals($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
$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');
@ -86,8 +84,8 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
$session->getPage()->selectFieldOption('select', 'green');
// Wait for the DOM to update.
$green_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')");
$this->assertNotNull($green_div2, 'DOM update: After reload - the selected color DIV is green.');
$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');
@ -98,8 +96,8 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
$session->getPage()->selectFieldOption('select', 'red');
// Wait for the DOM to update.
$red_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')");
$this->assertNotNull($red_div2, 'DOM update: After reload - the selected color DIV is red.');
$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');
@ -115,7 +113,9 @@ class AjaxFormPageCacheTest extends JavascriptTestBase {
$this->drupalGet('ajax_validation_test');
// Changing the value of the textfield will trigger an AJAX
// request/response.
$this->getSession()->getPage()->fillField('drivertext', 'some dumb text');
$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

View file

@ -0,0 +1,57 @@
<?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 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');
}
}

View file

@ -2,14 +2,14 @@
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests AJAX responses.
*
* @group Ajax
*/
class AjaxTest extends JavascriptTestBase {
class AjaxTest extends WebDriverTestBase {
/**
* {@inheritdoc}
@ -82,4 +82,119 @@ class AjaxTest extends JavascriptTestBase {
$this->assertNotContains($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");
}
}

View file

@ -0,0 +1,40 @@
<?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',
];
/**
* 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);
}
}

View file

@ -0,0 +1,130 @@
<?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'];
/**
* 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.
$test_alert_command = <<<JS
window.alert = function() {
document.body.innerHTML += '<div class="alert-command">Alert</div>';
};
JS;
$session->executeScript($test_alert_command);
$page->pressButton("AJAX 'Alert': Click to alert");
$this->assertWaitPageContains('<div class="alert-command">Alert</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->assertContains($text, $page->getContent());
}
}

View file

@ -0,0 +1,167 @@
<?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'];
/**
* Test sending non-JS and AJAX requests to open and manipulate modals.
*/
public function testDialog() {
$this->drupalLogin($this->drupalCreateUser(['administer contact forms']));
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// Set up variables for this test.
$dialog_renderable = AjaxTestController::dialogContents();
$dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
// Check that requesting a modal dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertSession()->responseContains($dialog_contents);
// Visit the page containing the many test dialog links.
$this->drupalGet('ajax-test/dialog');
// Tests a basic modal dialog by verifying the contents of the dialog are as
// expected.
$this->getSession()->getPage()->clickLink('Link 1 (modal)');
// Clicking the link triggers a AJAX request/response.
// Opens a Dialog panel.
$link1_dialog_div = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($link1_dialog_div, 'Link was used to open a dialog ( modal )');
$link1_modal = $link1_dialog_div->find('css', '#drupal-modal');
$this->assertNotNull($link1_modal, 'Link was used to open a dialog ( non-modal )');
$this->assertSession()->responseContains($dialog_contents);
$dialog_title = $link1_dialog_div->find('css', "span.ui-dialog-title:contains('AJAX Dialog & contents')");
$this->assertNotNull($dialog_title);
$dialog_title_amp = $link1_dialog_div->find('css', "span.ui-dialog-title:contains('AJAX Dialog &amp; contents')");
$this->assertNull($dialog_title_amp);
// Close open dialog, return to the dialog links page.
$close_button = $link1_dialog_div->findButton('Close');
$this->assertNotNull($close_button);
$close_button->press();
// Tests a modal with a dialog-option.
// Link 2 is similar to Link 1, except it submits additional width
// information which must be echoed in the resulting DOM update.
$this->getSession()->getPage()->clickLink('Link 2 (modal)');
$dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($dialog, 'Link was used to open a dialog ( non-modal, with options )');
$style = $dialog->getAttribute('style');
$this->assertContains('width: 400px;', $style, new FormattableMarkup('Modal respected the dialog-options width parameter. Style = style', ['%style' => $style]));
// Reset: Return to the dialog links page.
$this->drupalGet('ajax-test/dialog');
// Test a non-modal dialog ( with target ).
$this->clickLink('Link 3 (non-modal)');
$non_modal_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($non_modal_dialog, 'Link opens a non-modal dialog.');
// Tests the dialog contains a target element specified in the AJAX request.
$non_modal_dialog->find('css', 'div#ajax-test-dialog-wrapper-1');
$this->assertSession()->responseContains($dialog_contents);
// Reset: Return to the dialog links page.
$this->drupalGet('ajax-test/dialog');
// Tests a non-modal dialog ( without target ).
$this->clickLink('Link 7 (non-modal, no target)');
$no_target_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($no_target_dialog, 'Link opens a non-modal dialog.');
$contents_no_target = $no_target_dialog->find('css', 'div.ui-dialog-content');
$this->assertNotNull($contents_no_target, 'non-modal dialog opens ( no target ). ');
$id = $contents_no_target->getAttribute('id');
$partial_match = strpos($id, 'drupal-dialog-ajax-testdialog-contents') === 0;
$this->assertTrue($partial_match, 'The non-modal ID has the expected prefix.');
$no_target_button = $no_target_dialog->findButton('Close');
$this->assertNotNull($no_target_button, 'Link dialog has a close button');
$no_target_button->press();
$this->getSession()->getPage()->findButton('Button 1 (modal)')->press();
$button1_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog');
$this->assertNotNull($button1_dialog, 'Button opens a modal dialog.');
$button1_dialog_content = $button1_dialog->find('css', 'div.ui-dialog-content');
$this->assertNotNull($button1_dialog_content, 'Button opens a modal dialog.');
// Test the HTML escaping of & character.
$button1_dialog_title = $button1_dialog->find('css', "span.ui-dialog-title:contains('AJAX Dialog & contents')");
$this->assertNotNull($button1_dialog_title);
$button1_dialog_title_amp = $button1_dialog->find('css', "span.ui-dialog-title:contains('AJAX Dialog &amp; contents')");
$this->assertNull($button1_dialog_title_amp);
// Reset: Close the dialog.
$button1_dialog->findButton('Close')->press();
// Abbreviated test for "normal" dialogs, testing only the difference.
$this->getSession()->getPage()->findButton('Button 2 (non-modal)')->press();
$button2_dialog = $this->assertSession()->waitForElementVisible('css', 'div.ui-dialog-content');
$this->assertNotNull($button2_dialog, 'Non-modal content displays as expected.');
// Use a link to close the pagnel opened by button 2.
$this->getSession()->getPage()->clickLink('Link 4 (close non-modal if open)');
// Form modal.
$this->clickLink('Link 5 (form)');
// Two links have been clicked in succession - This time wait for a change
// in the title as the previous closing dialog may temporarily be open.
$form_dialog_title = $this->assertSession()->waitForElementVisible('css', "span.ui-dialog-title:contains('Ajax Form contents')");
$this->assertNotNull($form_dialog_title, 'Dialog form has the expected title.');
// Locate the newly opened dialog.
$form_dialog = $this->getSession()->getPage()->find('css', 'div.ui-dialog');
$this->assertNotNull($form_dialog, 'Form dialog is visible');
$form_contents = $form_dialog->find('css', "p:contains('Ajax Form contents description.')");
$this->assertNotNull($form_contents, 'For has the expected text.');
$do_it = $form_dialog->findButton('Do it');
$this->assertNotNull($do_it, 'The dialog has a "Do it" button.');
$preview = $form_dialog->findButton('Preview');
$this->assertNotNull($preview, 'The dialog contains a "Preview" button.');
// 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.');
}
}

View file

@ -0,0 +1,139 @@
<?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 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();
entity_get_form_display('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->assertEqual(count($fields), 2);
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->assertEqual(count($field[0]->find('xpath', '.' . $field_items_xpath_suffix)), $i + 2, '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);
}
}

View file

@ -0,0 +1,106 @@
<?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}
*/
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');
$this->waitForNoElement('.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);
$this->waitForNoElement('.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);
$this->waitForNoElement('.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);
$this->waitForNoElement('.custom-ajax-progress-throbber');
}
/**
* Waits for an element to be removed from the page.
*
* @param string $selector
* CSS selector.
* @param int $timeout
* (optional) Timeout in milliseconds, defaults to 10000.
*
* @todo Remove in https://www.drupal.org/node/2892440.
*/
protected function waitForNoElement($selector, $timeout = 10000) {
$condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)";
$this->assertJsCondition($condition, $timeout);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Tests if we can execute JavaScript in the browser.
*
* @group javascript
*/
class BrowserWithJavascriptTest extends WebDriverTestBase {
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->setExpectedException(\PHPUnit_Framework_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');
}
}

View file

@ -0,0 +1,133 @@
<?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 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->assertTrue($summary->getAttribute('aria-expanded'));
$this->assertTrue($summary->getAttribute('aria-pressed'));
}
}

View file

@ -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());
}
}

View file

@ -2,14 +2,14 @@
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests for the machine name field.
*
* @group field
*/
class MachineNameTest extends JavascriptTestBase {
class MachineNameTest extends WebDriverTestBase {
/**
* Required modules.
@ -59,7 +59,6 @@ class MachineNameTest extends JavascriptTestBase {
// Get page and session.
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Get elements from the page.
$title_1 = $page->findField('machine_name_1_label');
@ -111,6 +110,36 @@ class MachineNameTest extends JavascriptTestBase {
// 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'));
}
}

View file

@ -2,7 +2,7 @@
namespace Drupal\FunctionalJavascriptTests\Core\Session;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
@ -10,7 +10,7 @@ use Drupal\menu_link_content\Entity\MenuLinkContent;
*
* @group session
*/
class SessionTest extends JavascriptTestBase {
class SessionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
@ -48,13 +48,10 @@ class SessionTest extends JavascriptTestBase {
// number of times.
$this->drupalGet('<front>');
$session_assert = $this->assertSession();
$page = $this->getSession()->getPage();
for ($i = 0; $i < 25; $i++) {
$page->clickLink('Link to front page');
$session_assert->statusCodeEquals(200);
}
}

View file

@ -2,14 +2,14 @@
namespace Drupal\FunctionalJavascriptTests\Dialog;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript functionality of the dialog position.
*
* @group dialog
*/
class DialogPositionTest extends JavascriptTestBase {
class DialogPositionTest extends WebDriverTestBase {
/**
* {@inheritdoc}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Driver\Selenium2Driver;
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);
}
}

View file

@ -2,17 +2,17 @@
namespace Drupal\FunctionalJavascriptTests\EntityReference;
use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\simpletest\ContentTypeCreationTrait;
use Drupal\simpletest\NodeCreationTrait;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
/**
* Tests the output of entity reference autocomplete widgets.
*
* @group entity_reference
*/
class EntityReferenceAutocompleteWidgetTest extends JavascriptTestBase {
class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
use ContentTypeCreationTrait;
use EntityReferenceTestTrait;
@ -63,7 +63,7 @@ class EntityReferenceAutocompleteWidgetTest extends JavascriptTestBase {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$autocomplete_field = $page->findField($field_name . '[0][target_id]');
$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();
@ -87,7 +87,7 @@ class EntityReferenceAutocompleteWidgetTest extends JavascriptTestBase {
$this->drupalGet('node/add/page');
$page = $this->getSession()->getPage();
$autocomplete_field = $page->findField($field_name . '[0][target_id]');
$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();

View file

@ -64,7 +64,7 @@ JS;
public function waitForElement($selector, $locator, $timeout = 10000) {
$page = $this->session->getPage();
$result = $page->waitFor($timeout / 1000, function() use ($page, $selector, $locator) {
$result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
return $page->find($selector, $locator);
});
@ -90,7 +90,7 @@ JS;
public function waitForElementVisible($selector, $locator, $timeout = 10000) {
$page = $this->session->getPage();
$result = $page->waitFor($timeout / 1000, function() use ($page, $selector, $locator) {
$result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
$element = $page->find($selector, $locator);
if (!empty($element) && $element->isVisible()) {
return $element;
@ -100,6 +100,7 @@ JS;
return $result;
}
/**
* Waits for a button (input[type=submit|image|button|reset], button) with
* specified locator and returns it.
@ -172,7 +173,7 @@ JS;
}
/**
* Test that a node, or it's specific corner, is visible in the viewport.
* 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().
@ -255,7 +256,7 @@ JS;
}
/**
* Check the visibility of a node, or it's specific corner.
* Check the visibility of a node, or its specific corner.
*
* @param \Behat\Mink\Element\NodeElement $node
* A valid node.

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Tests Drupal settings retrieval in JavascriptTestBase tests.
*
* @group javascript
*/
class JavascriptGetDrupalSettingsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['test_page_test'];
/**
* 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']);
}
}

View file

@ -2,171 +2,33 @@
namespace Drupal\FunctionalJavascriptTests;
use Drupal\Tests\BrowserTestBase;
use Zumba\GastonJS\Exception\DeadClient;
@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.x, will be removed before Drupal 9.0.0.
* Use \Drupal\FunctionalJavascriptTests\WebDriverTestBase instead
*
* @see https://www.drupal.org/node/2945059
*/
abstract class JavascriptTestBase extends BrowserTestBase {
abstract class JavascriptTestBase extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $minkDefaultDriverClass = PhantomJSDriver::class;
/**
* {@inheritdoc}
*/
protected function initMink() {
// 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 (\Exception $e) {
$this->markTestSkipped('An unexpected error occurred while starting Mink: ' . $e->getMessage());
}
}
/**
* {@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');
}
}
parent::tearDown();
}
/**
* 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.x, will be removed before Drupal 8.3.x. Use
* \Behat\Mink\Element\NodeElement::isVisible() instead.
*/
protected function assertElementVisible($css_selector, $message = '') {
$this->assertTrue($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath($css_selector)), $message);
}
/**
* 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.x, will be removed before Drupal 8.3.x. Use
* \Behat\Mink\Element\NodeElement::isVisible() instead.
*/
protected function assertElementNotVisible($css_selector, $message = '') {
$this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath($css_selector)), $message);
}
/**
* 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 behaviour.
*
* @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 a WebAssert that supports status code and header assertions.
return new JSWebAssert($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) ?: [];
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Runs a browser test using PhantomJS.
*
* @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0.
* Use \Drupal\FunctionalJavascriptTests\WebDriverTestBase instead
*
* BC layer for testing browser interaction implemented in JavaScript.
*/
abstract class LegacyJavascriptTestBase extends JavascriptTestBase {
}

View file

@ -3,14 +3,14 @@
namespace Drupal\FunctionalJavascriptTests\Tests;
use Behat\Mink\Element\NodeElement;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests for the JSWebAssert class.
*
* @group javascript
*/
class JSWebAssertTest extends JavascriptTestBase {
class JSWebAssertTest extends WebDriverTestBase {
/**
* Required modules.

View file

@ -0,0 +1,19 @@
<?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;
}

View file

@ -0,0 +1,110 @@
<?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 "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 "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));
}
}

View file

@ -0,0 +1,211 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Exception\DriverException;
use Drupal\Tests\BrowserTestBase;
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.
*/
abstract class WebDriverTestBase extends BrowserTestBase {
/**
* {@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 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');
}
}
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, will be removed before 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, will be removed before 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 behaviour.
*
* @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 '';
}
}

View file

@ -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.');
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.');
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.');
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.');
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.');
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.');
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.');
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.');
parent::responseHeaderNotMatches($name, $regex);
}
}

View file

@ -2,7 +2,7 @@
namespace Drupal\FunctionalTests;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ExpectationException;
use Behat\Mink\Selector\Xpath\Escaper;
use Drupal\Component\Render\FormattableMarkup;
@ -220,13 +220,11 @@ trait AssertLegacyTrait {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldByName($name, $value = NULL) {
$this->assertSession()->fieldExists($name);
if ($value !== NULL) {
$this->assertSession()->fieldValueEquals($name, (string) $value);
}
$this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
@ -242,15 +240,11 @@ trait AssertLegacyTrait {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldByName($name, $value = '') {
if ($this->getSession()->getPage()->findField($name) && isset($value)) {
$this->assertSession()->fieldValueNotEquals($name, (string) $value);
}
else {
$this->assertSession()->fieldNotExists($name);
}
$this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
@ -268,19 +262,11 @@ trait AssertLegacyTrait {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldById($id, $value = '') {
$xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]);
$field = $this->getSession()->getPage()->find('xpath', $xpath);
if (empty($field)) {
throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'id', $field);
}
if ($value !== NULL) {
$this->assertEquals($value, $field->getValue());
}
$this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
@ -290,23 +276,25 @@ trait AssertLegacyTrait {
* Name or ID of field to assert.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldExists() instead.
* Use $this->assertSession()->fieldExists() or
* $this->assertSession()->buttonExists() instead.
*/
protected function assertField($field) {
$this->assertSession()->fieldExists($field);
$this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
* Asserts that a field exists with the given name or ID does NOT exist.
* Asserts that a field does NOT exist with the given name or ID.
*
* @param string $field
* Name or ID of field to assert.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldNotExists() instead.
* Use $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() instead.
*/
protected function assertNoField($field) {
$this->assertSession()->fieldNotExists($field);
$this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
@ -427,23 +415,11 @@ trait AssertLegacyTrait {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldNotExists() or
* $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldById($id, $value = '') {
$xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]);
$field = $this->getSession()->getPage()->find('xpath', $xpath);
// Return early if the field could not be found as expected.
if ($field === NULL) {
return;
}
if (!isset($value)) {
throw new ExpectationException(sprintf('Id "%s" appears on this page, but it should not.', $id), $this->getSession()->getDriver());
}
elseif ($value === $field->getValue()) {
throw new ExpectationException(sprintf('Failed asserting that %s is not equal to %s', $field->getValue(), $value), $this->getSession()->getDriver());
}
$this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
@ -585,25 +561,32 @@ trait AssertLegacyTrait {
* (optional) A message to display with the assertion. Do not translate
* messages with t().
*
* @throws \Behat\Mink\Exception\ExpectationException
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->xpath() instead and assert that the result is empty.
*/
protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '') {
$fields = $this->xpath($xpath);
// If value specified then check array for match.
$found = TRUE;
if (isset($value)) {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
if ($field->getAttribute('value') == $value) {
$found = TRUE;
}
if (!empty($fields)) {
if (isset($value)) {
$found = FALSE;
try {
$this->assertFieldsByValue($fields, $value);
$found = TRUE;
}
catch (\Exception $e) {
}
if ($found) {
throw new ExpectationException(sprintf('The field resulting from %s was found with the provided value %s.', $xpath, $value), $this->getSession()->getDriver());
}
}
else {
throw new ExpectationException(sprintf('The field resulting from %s was found.', $xpath), $this->getSession()->getDriver());
}
}
return $this->assertFalse($fields && $found, $message);
}
/**
@ -629,7 +612,15 @@ trait AssertLegacyTrait {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
if ($field->getAttribute('value') == $value) {
if ($field->getAttribute('type') == 'checkbox') {
if (is_bool($value)) {
$found = $field->isChecked() == $value;
}
else {
$found = TRUE;
}
}
elseif ($field->getAttribute('value') == $value) {
// Input element with correct value.
$found = TRUE;
}
@ -637,8 +628,12 @@ trait AssertLegacyTrait {
// Select element with an option.
$found = TRUE;
}
elseif ($field->getText() == $value) {
// Text area with correct text.
elseif ($field->getTagName() === 'textarea' && $field->getValue() == $value) {
// Text area with correct text. Use getValue() here because
// getText() would remove any newlines in the value.
$found = TRUE;
}
elseif ($field->getTagName() !== 'input' && $field->getText() == $value) {
$found = TRUE;
}
}
@ -719,6 +714,22 @@ trait AssertLegacyTrait {
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tag);
}
/**
* Asserts whether an expected cache tag was absent in the last response.
*
* @param string $cache_tag
* The cache tag to check.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->responseHeaderNotContains() instead.
*
* @see https://www.drupal.org/node/2864029
*/
protected function assertNoCacheTag($cache_tag) {
@trigger_error('assertNoCacheTag() is deprecated and scheduled for removal in Drupal 9.0.0. Use $this->assertSession()->responseHeaderNotContains() instead. See https://www.drupal.org/node/2864029.', E_USER_DEPRECATED);
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', $cache_tag);
}
/**
* Checks that current response header equals value.
*
@ -772,6 +783,25 @@ trait AssertLegacyTrait {
return $this->assertSession()->buildXPathQuery($xpath, $args);
}
/**
* Helper: Constructs an XPath for the given set of attributes and value.
*
* @param string $attribute
* Field attributes.
* @param string $value
* Value of field.
*
* @return string
* XPath for specified values.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->getSession()->getPage()->findField() instead.
*/
protected function constructFieldXpath($attribute, $value) {
$xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]';
return $this->buildXPathQuery($xpath, [':value' => $value]);
}
/**
* Gets the current raw content.
*
@ -783,4 +813,21 @@ trait AssertLegacyTrait {
return $this->getSession()->getPage()->getContent();
}
/**
* Get all option elements, including nested options, in a select.
*
* @param \Behat\Mink\Element\NodeElement $element
* The element for which to get the options.
*
* @return \Behat\Mink\Element\NodeElement[]
* Option elements in select.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $element->findAll('xpath', 'option') instead.
*/
protected function getAllOptions(NodeElement $element) {
@trigger_error('AssertLegacyTrait::getAllOptions() is scheduled for removal in Drupal 9.0.0. Use $element->findAll(\'xpath\', \'option\') instead.', E_USER_DEPRECATED);
return $element->findAll('xpath', '//option');
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\FunctionalTests\Bootstrap;
use Drupal\Core\DependencyInjection\Container;
/**
* Container base class which triggers an error.
*/
class ErrorContainer extends Container {
/**
* {@inheritdoc}
*/
public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) {
if ($id === 'http_kernel') {
// Enforce a recoverable error.
$callable = function (ErrorContainer $container) {
};
$callable(1);
}
else {
return parent::get($id, $invalidBehavior);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Bootstrap;
use Drupal\Core\DependencyInjection\Container;
/**
* Base container which throws an exception.
*/
class ExceptionContainer extends Container {
/**
* {@inheritdoc}
*/
public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) {
if ($id === 'http_kernel') {
throw new \Exception('Thrown exception during Container::get');
}
else {
return parent::get($id, $invalidBehavior);
}
}
}

View file

@ -0,0 +1,375 @@
<?php
namespace Drupal\FunctionalTests\Bootstrap;
use Drupal\Tests\BrowserTestBase;
/**
* Tests kernel panic when things are really messed up.
*
* @group system
*/
class UncaughtExceptionTest extends BrowserTestBase {
/**
* Last cURL response.
*
* @var string
*/
protected $response = '';
/**
* Last cURL info.
*
* @var array
*/
protected $info = [];
/**
* Exceptions thrown by site under test that contain this text are ignored.
*
* @var string
*/
protected $expectedExceptionMessage;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['error_service_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$settings_filename = $this->siteDirectory . '/settings.php';
chmod($settings_filename, 0777);
$settings_php = file_get_contents($settings_filename);
$settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ErrorContainer.php';\n";
$settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ExceptionContainer.php';\n";
file_put_contents($settings_filename, $settings_php);
$settings = [];
$settings['config']['system.logging']['error_level'] = (object) [
'value' => ERROR_REPORTING_DISPLAY_VERBOSE,
'required' => TRUE,
];
$this->writeSettings($settings);
}
/**
* Tests uncaught exception handling when system is in a bad state.
*/
public function testUncaughtException() {
$this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.';
\Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_HIDE)
->save();
$settings = [];
$settings['config']['system.logging']['error_level'] = (object) [
'value' => ERROR_REPORTING_HIDE,
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertNoText($this->expectedExceptionMessage);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_DISPLAY_ALL)
->save();
$settings = [];
$settings['config']['system.logging']['error_level'] = (object) [
'value' => ERROR_REPORTING_DISPLAY_ALL,
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertText($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests uncaught exception handling with custom exception handler.
*/
public function testUncaughtExceptionCustomExceptionHandler() {
$settings_filename = $this->siteDirectory . '/settings.php';
chmod($settings_filename, 0777);
$settings_php = file_get_contents($settings_filename);
$settings_php .= "\n";
$settings_php .= "set_exception_handler(function() {\n";
$settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
$settings_php .= " print('Oh oh, flying teapots');\n";
$settings_php .= "});\n";
file_put_contents($settings_filename, $settings_php);
\Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
$this->drupalGet('');
$this->assertResponse(418);
$this->assertNoText('The website encountered an unexpected error. Please try again later.');
$this->assertNoText('Oh oh, bananas in the instruments');
$this->assertText('Oh oh, flying teapots');
}
/**
* Tests a missing dependency on a service.
*/
public function testMissingDependency() {
if (version_compare(PHP_VERSION, '7.1') < 0) {
$this->expectedExceptionMessage = 'Argument 1 passed to Drupal\error_service_test\LonelyMonkeyClass::__construct() must be an instance of Drupal\Core\Database\Connection, non';
}
else {
$this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed';
}
$this->drupalGet('broken-service-class');
$this->assertResponse(500);
$this->assertRaw('The website encountered an unexpected error.');
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests a missing dependency on a service with a custom error handler.
*/
public function testMissingDependencyCustomErrorHandler() {
$settings_filename = $this->siteDirectory . '/settings.php';
chmod($settings_filename, 0777);
$settings_php = file_get_contents($settings_filename);
$settings_php .= "\n";
$settings_php .= "set_error_handler(function() {\n";
$settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
$settings_php .= " print('Oh oh, flying teapots');\n";
$settings_php .= " exit();\n";
$settings_php .= "});\n";
$settings_php .= "\$settings['teapots'] = TRUE;\n";
file_put_contents($settings_filename, $settings_php);
$this->drupalGet('broken-service-class');
$this->assertResponse(418);
$this->assertSame('Oh oh, flying teapots', $this->response);
}
/**
* Tests a container which has an error.
*/
public function testErrorContainer() {
$settings = [];
$settings['settings']['container_base_class'] = (object) [
'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer',
'required' => TRUE,
];
$this->writeSettings($settings);
\Drupal::service('kernel')->invalidateContainer();
$this->expectedExceptionMessage = 'Argument 1 passed to Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closur';
$this->drupalGet('');
$this->assertResponse(500);
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests a container which has an exception really early.
*/
public function testExceptionContainer() {
$settings = [];
$settings['settings']['container_base_class'] = (object) [
'value' => '\Drupal\FunctionalTests\Bootstrap\ExceptionContainer',
'required' => TRUE,
];
$this->writeSettings($settings);
\Drupal::service('kernel')->invalidateContainer();
$this->expectedExceptionMessage = 'Thrown exception during Container::get';
$this->drupalGet('');
$this->assertResponse(500);
$this->assertRaw('The website encountered an unexpected error');
$this->assertRaw($this->expectedExceptionMessage);
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests the case when the database connection is gone.
*/
public function testLostDatabaseConnection() {
$incorrect_username = $this->randomMachineName(16);
switch ($this->container->get('database')->driver()) {
case 'pgsql':
case 'mysql':
$this->expectedExceptionMessage = $incorrect_username;
break;
default:
// We can not carry out this test.
$this->pass('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.');
return;
}
// We simulate a broken database connection by rewrite settings.php to no
// longer have the proper data.
$settings['databases']['default']['default']['username'] = (object) [
'value' => $incorrect_username,
'required' => TRUE,
];
$settings['databases']['default']['default']['password'] = (object) [
'value' => $this->randomMachineName(16),
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertRaw('DatabaseAccessDeniedException');
$this->assertErrorLogged($this->expectedExceptionMessage);
}
/**
* Tests fallback to PHP error log when an exception is thrown while logging.
*/
public function testLoggerException() {
// Ensure the test error log is empty before these tests.
$this->assertNoErrorsLogged();
$this->expectedExceptionMessage = 'Deforestation';
\Drupal::state()->set('error_service_test.break_logger', TRUE);
$this->drupalGet('');
$this->assertResponse(500);
$this->assertText('The website encountered an unexpected error. Please try again later.');
$this->assertRaw($this->expectedExceptionMessage);
// Find fatal error logged to the error.log
$errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
$this->assertIdentical(count($errors), 8, 'The error + the error that the logging service is broken has been written to the error log.');
$this->assertTrue(strpos($errors[0], 'Failed to log error') !== FALSE, 'The error handling logs when an error could not be logged to the logger.');
$expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php';
$expected_line = 59;
$expected_entry = "Failed to log error: Exception: Deforestation in Drupal\\error_service_test\\MonkeysInTheControlRoom->handle() (line ${expected_line} of ${expected_path})";
$this->assert(strpos($errors[0], $expected_entry) !== FALSE, 'Original error logged to the PHP error log when an exception is thrown by a logger');
// The exception is expected. Do not interpret it as a test failure. Not
// using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
}
/**
* Asserts that a specific error has been logged to the PHP error log.
*
* @param string $error_message
* The expected error message.
*
* @see \Drupal\simpletest\TestBase::prepareEnvironment()
* @see \Drupal\Core\DrupalKernel::bootConfiguration()
*/
protected function assertErrorLogged($error_message) {
$error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log';
if (!file_exists($error_log_filename)) {
$this->fail('No error logged yet.');
}
$content = file_get_contents($error_log_filename);
$rows = explode(PHP_EOL, $content);
// We iterate over the rows in order to be able to remove the logged error
// afterwards.
$found = FALSE;
foreach ($rows as $row_index => $row) {
if (strpos($content, $error_message) !== FALSE) {
$found = TRUE;
unset($rows[$row_index]);
}
}
file_put_contents($error_log_filename, implode("\n", $rows));
$this->assertTrue($found, sprintf('The %s error message was logged.', $error_message));
}
/**
* Asserts that no errors have been logged to the PHP error.log thus far.
*
* @see \Drupal\simpletest\TestBase::prepareEnvironment()
* @see \Drupal\Core\DrupalKernel::bootConfiguration()
*/
protected function assertNoErrorsLogged() {
// Since PHP only creates the error.log file when an actual error is
// triggered, it is sufficient to check whether the file exists.
$this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.');
}
/**
* Retrieves a Drupal path or an absolute path.
*
* Executes a cURL request for processing errors and exceptions.
*
* @param string|\Drupal\Core\Url $path
* Request path.
* @param array $extra_options
* (optional) Curl options to pass to curl_setopt()
* @param array $headers
* (optional) Not used.
*/
protected function drupalGet($path, array $extra_options = [], array $headers = []) {
$url = $this->buildUrl($path, ['absolute' => TRUE]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
$this->response = curl_exec($ch);
$this->info = curl_getinfo($ch);
curl_close($ch);
}
/**
* {@inheritdoc}
*/
protected function assertResponse($code) {
$this->assertSame($code, $this->info['http_code']);
}
/**
* {@inheritdoc}
*/
protected function assertText($text) {
$this->assertContains($text, $this->response);
}
/**
* {@inheritdoc}
*/
protected function assertNoText($text) {
$this->assertNotContains($text, $this->response);
}
/**
* {@inheritdoc}
*/
protected function assertRaw($text) {
$this->assertText($text);
}
/**
* {@inheritdoc}
*/
protected function assertNoRaw($text) {
$this->assertNoText($text);
}
}

View file

@ -3,6 +3,7 @@
namespace Drupal\FunctionalTests;
use Behat\Mink\Exception\ExpectationException;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
@ -22,7 +23,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
*
* @var array
*/
public static $modules = ['test_page_test', 'form_test', 'system_test'];
public static $modules = ['test_page_test', 'form_test', 'system_test', 'node'];
/**
* Tests basic page test.
@ -66,6 +67,21 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertSame('header value', $returned_header);
}
/**
* Tests drupalGet().
*/
public function testDrupalGet() {
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('test-page');
$this->drupalGet('/test-page');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('test-page');
$this->drupalGet('/test-page/');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('/test-page/');
}
/**
* Tests basic form functionality.
*/
@ -98,13 +114,27 @@ class BrowserTestBaseTest extends BrowserTestBase {
// Test drupalPostForm().
$edit = ['bananas' => 'red'];
$this->drupalPostForm('form-test/object-builder', $edit, 'Save');
$result = $this->drupalPostForm('form-test/object-builder', $edit, 'Save');
$this->assertSame($this->getSession()->getPage()->getContent(), $result);
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('red', $value);
$this->drupalPostForm('form-test/object-builder', NULL, 'Save');
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('', $value);
// Test drupalPostForm() with no-html response.
$values = Json::decode($this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit')));
$this->assertTrue(1000, $values['beer']);
// Test drupalPostForm() with form by HTML id.
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalLogin($this->drupalCreateUser(['create page content']));
$this->drupalGet('form-test/two-instances-of-same-form');
$this->getSession()->getPage()->fillField('edit-title-0-value', 'form1');
$this->getSession()->getPage()->fillField('edit-title-0-value--2', 'form2');
$this->drupalPostForm(NULL, [], 'Save', [], 'node-page-form--2');
$this->assertSession()->pageTextContains('Page form2 has been created.');
}
/**
@ -137,10 +167,52 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertSession()->linkExists('foo|bar|baz');
}
/**
* Tests linkExistsExact() functionality.
*
* @see \Drupal\Tests\WebAssert::linkExistsExact()
*/
public function testLinkExistsExact() {
$this->drupalGet('test-pipe-char');
$this->assertSession()->linkExistsExact('foo|bar|baz');
}
/**
* Tests linkExistsExact() functionality fail.
*
* @see \Drupal\Tests\WebAssert::linkExistsExact()
*/
public function testInvalidLinkExistsExact() {
$this->drupalGet('test-pipe-char');
$this->setExpectedException(ExpectationException::class, 'Link with label foo|bar found');
$this->assertSession()->linkExistsExact('foo|bar');
}
/**
* Tests linkNotExistsExact() functionality.
*
* @see \Drupal\Tests\WebAssert::linkNotExistsExact()
*/
public function testLinkNotExistsExact() {
$this->drupalGet('test-pipe-char');
$this->assertSession()->linkNotExistsExact('foo|bar');
}
/**
* Tests linkNotExistsExact() functionality fail.
*
* @see \Drupal\Tests\WebAssert::linkNotExistsExact()
*/
public function testInvalidLinkNotExistsExact() {
$this->drupalGet('test-pipe-char');
$this->setExpectedException(ExpectationException::class, 'Link with label foo|bar|baz not found');
$this->assertSession()->linkNotExistsExact('foo|bar|baz');
}
/**
* Tests legacy text asserts.
*/
public function testLegacyTextAsserts() {
public function testTextAsserts() {
$this->drupalGet('test-encoded');
$dangerous = 'Bad html <script>alert(123);</script>';
$sanitized = Html::escape($dangerous);
@ -148,13 +220,13 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertText($sanitized);
// Test getRawContent().
$this->assertSame($this->getSession()->getPage()->getContent(), $this->getRawContent());
$this->assertSame($this->getSession()->getPage()->getContent(), $this->getSession()->getPage()->getContent());
}
/**
* Tests legacy field asserts which use xpath directly.
*/
public function testLegacyXpathAsserts() {
public function testXpathAsserts() {
$this->drupalGet('test-field-xpath');
$this->assertFieldsByValue($this->xpath("//h1[@class = 'page-title']"), NULL);
$this->assertFieldsByValue($this->xpath('//table/tbody/tr[2]/td[1]'), 'one');
@ -181,7 +253,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertNoFieldByXPath("//input[@id = 'edit-name']");
$this->fail('The "edit-name" field was not found.');
}
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
catch (ExpectationException $e) {
$this->pass('assertNoFieldByXPath correctly failed. The "edit-name" field was found.');
}
@ -197,7 +269,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
/**
* Tests legacy field asserts using textfields.
*/
public function testLegacyFieldAssertsWithTextfields() {
public function testFieldAssertsForTextfields() {
$this->drupalGet('test-field-xpath');
// *** 1. assertNoField().
@ -230,7 +302,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertField('invalid_name_and_id');
$this->fail('The "invalid_name_and_id" field was found.');
}
catch (ExpectationException $e) {
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('assertField correctly failed. The "invalid_name_and_id" field was not found.');
}
@ -319,7 +391,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertFieldByName('non-existing-name');
$this->fail('The "non-existing-name" field was found.');
}
catch (ExpectationException $e) {
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('The "non-existing-name" field was not found');
}
@ -328,15 +400,18 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertFieldByName('name', 'not the value');
$this->fail('The "name" field with incorrect value was found.');
}
catch (ExpectationException $e) {
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('assertFieldByName correctly failed. The "name" field with incorrect value was not found.');
}
// Test that text areas can contain new lines.
$this->assertFieldsByValue($this->xpath("//textarea[@id = 'edit-test-textarea-with-newline']"), "Test text with\nnewline");
}
/**
* Tests legacy field asserts on other types of field.
* Tests legacy field asserts for options field type.
*/
public function testLegacyFieldAssertsWithNonTextfields() {
public function testFieldAssertsForOptions() {
$this->drupalGet('test-field-xpath');
// Option field type.
@ -384,7 +459,17 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->pass($e->getMessage());
}
// Button field type.
// Test \Drupal\FunctionalTests\AssertLegacyTrait::getAllOptions.
$this->drupalGet('/form-test/select');
$this->assertCount(6, $this->getAllOptions($this->cssSelect('select[name="opt_groups"]')[0]));
}
/**
* Tests legacy field asserts for button field type.
*/
public function testFieldAssertsForButton() {
$this->drupalGet('test-field-xpath');
$this->assertFieldById('edit-save', NULL);
// Test that the assertion fails correctly if the field value is passed in
// rather than the id.
@ -392,7 +477,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertFieldById('Save', NULL);
$this->fail('The field with id of "Save" was found.');
}
catch (ExpectationException $e) {
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass($e->getMessage());
}
@ -407,7 +492,27 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->pass($e->getMessage());
}
// Checkbox field type.
// Test that multiple fields with the same name are validated correctly.
$this->assertFieldByName('duplicate_button', 'Duplicate button 1');
$this->assertFieldByName('duplicate_button', 'Duplicate button 2');
$this->assertNoFieldByName('duplicate_button', 'Rabbit');
try {
$this->assertNoFieldByName('duplicate_button', 'Duplicate button 2');
$this->fail('The "duplicate_button" field with the value Duplicate button 2 was not found.');
}
catch (ExpectationException $e) {
$this->pass('assertNoFieldByName correctly failed. The "duplicate_button" field with the value Duplicate button 2 was found.');
}
}
/**
* Tests legacy field asserts for checkbox field type.
*/
public function testFieldAssertsForCheckbox() {
$this->drupalGet('test-field-xpath');
// Part 1 - Test by name.
// Test that checkboxes are found/not found correctly by name, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldByName('checkbox_enabled', TRUE);
@ -420,17 +525,13 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertFieldByName('checkbox_enabled', NULL);
$this->assertFieldByName('checkbox_disabled', NULL);
// Test that checkboxes are found/not found correctly by ID, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', TRUE);
$this->assertFieldById('edit-checkbox-disabled', FALSE);
$this->assertNoFieldById('edit-checkbox-enabled', FALSE);
$this->assertNoFieldById('edit-checkbox-disabled', TRUE);
// Test that checkboxes are found by name when passing no second parameter.
$this->assertFieldByName('checkbox_enabled');
$this->assertFieldByName('checkbox_disabled');
// Test that checkboxes are found by by ID, when using NULL to ignore the
// 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', NULL);
$this->assertFieldById('edit-checkbox-disabled', NULL);
// Test that we have legacy support.
$this->assertFieldByName('checkbox_enabled', '1');
$this->assertFieldByName('checkbox_disabled', '');
// Test that the assertion fails correctly when using NULL to ignore state.
try {
@ -441,6 +542,27 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.');
}
// Part 2 - Test by ID.
// Test that checkboxes are found/not found correctly by ID, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', TRUE);
$this->assertFieldById('edit-checkbox-disabled', FALSE);
$this->assertNoFieldById('edit-checkbox-enabled', FALSE);
$this->assertNoFieldById('edit-checkbox-disabled', TRUE);
// Test that checkboxes are found by ID, when using NULL to ignore the
// 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', NULL);
$this->assertFieldById('edit-checkbox-disabled', NULL);
// Test that checkboxes are found by ID when passing no second parameter.
$this->assertFieldById('edit-checkbox-enabled');
$this->assertFieldById('edit-checkbox-disabled');
// Test that we have legacy support.
$this->assertFieldById('edit-checkbox-enabled', '1');
$this->assertFieldById('edit-checkbox-disabled', '');
// Test that the assertion fails correctly when using NULL to ignore state.
try {
$this->assertNoFieldById('edit-checkbox-disabled', NULL);
@ -450,11 +572,11 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->pass('assertNoFieldById failed correctly. The "edit-checkbox-disabled" field was found by ID using NULL value.');
}
// Test the specific 'checked' assertions.
// Part 3 - Test the specific 'checked' assertions.
$this->assertFieldChecked('edit-checkbox-enabled');
$this->assertNoFieldChecked('edit-checkbox-disabled');
// Test that the assertion fails correctly with non-existant field id.
// Test that the assertion fails correctly with non-existent field id.
try {
$this->assertNoFieldChecked('incorrect_checkbox_id');
$this->fail('The "incorrect_checkbox_id" field was found');
@ -503,4 +625,51 @@ class BrowserTestBaseTest extends BrowserTestBase {
$this->assertTrue(file_exists($htaccess_filename), "$htaccess_filename exists");
}
/**
* Tests the assumption that local time is in 'Australia/Sydney'.
*/
public function testLocalTimeZone() {
// The 'Australia/Sydney' time zone is set in core/tests/bootstrap.php
$this->assertEquals('Australia/Sydney', date_default_timezone_get());
// The 'Australia/Sydney' time zone is also set in
// FunctionalTestSetupTrait::initConfig().
$config_factory = $this->container->get('config.factory');
$value = $config_factory->get('system.date')->get('timezone.default');
$this->assertEquals('Australia/Sydney', $value);
}
/**
* Tests the ::checkForMetaRefresh() method.
*/
public function testCheckForMetaRefresh() {
// Disable following redirects in the client.
$this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
// Set the maximumMetaRefreshCount to zero to make sure the redirect doesn't
// happen when doing a drupalGet.
$this->maximumMetaRefreshCount = 0;
$this->drupalGet('test-meta-refresh');
$this->assertNotEmpty($this->cssSelect('meta[http-equiv="refresh"]'));
// Allow one redirect to happen.
$this->maximumMetaRefreshCount = 1;
$this->checkForMetaRefresh();
// Check that we are now on the test page.
$this->assertSession()->pageTextContains('Test page text.');
}
public function testGetDefaultDriveInstance() {
putenv('MINK_DRIVER_ARGS=' . json_encode([NULL, ['key1' => ['key2' => ['key3' => 3, 'key3.1' => 3.1]]]]));
$this->getDefaultDriverInstance();
$this->assertEquals([NULL, ['key1' => ['key2' => ['key3' => 3, 'key3.1' => 3.1]]]], $this->minkDefaultDriverArgs);
}
/**
* Ensures we can't access modules we shouldn't be able to after install.
*/
public function testProfileModules() {
$this->setExpectedException(\InvalidArgumentException::class, 'The module demo_umami_content does not exist.');
$this->assertFileExists('core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.info.yml');
\Drupal::service('extension.list.module')->getPathname('demo_umami_content');
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\FunctionalTests\Core\Test;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Drupal's integration with Symfony PHPUnit Bridge.
*
* @group Test
* @group legacy
*/
class PhpUnitBridgeTest extends BrowserTestBase {
protected static $modules = ['deprecation_test'];
/**
* @expectedDeprecation This is the deprecation message for deprecation_test_function().
*/
public function testSilencedError() {
$this->assertEquals('known_return_value', deprecation_test_function());
}
/**
* @expectedDeprecation This is the deprecation message for deprecation_test_function().
*/
public function testErrorOnSiteUnderTest() {
$this->drupalGet(Url::fromRoute('deprecation_test.route'));
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Drupal\FunctionalTests\Datetime;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the functionality of TimestampAgoFormatter core field formatter.
*
* @group field
*/
class TimestampAgoFormatterTest extends BrowserTestBase {
/**
* An array of display options to pass to entity_get_display().
*
* @var array
*/
protected $displayOptions;
/**
* A field storage to use in this test class.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field used in this test class.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test', 'field_ui'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser([
'access administration pages',
'view test entity',
'administer entity_test content',
'administer entity_test fields',
'administer entity_test display',
'administer entity_test form display',
'view the administration theme',
]);
$this->drupalLogin($web_user);
$field_name = 'field_timestamp';
$type = 'timestamp';
$widget_type = 'datetime_timestamp';
$formatter_type = 'timestamp_ago';
$this->fieldStorage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => $type,
]);
$this->fieldStorage->save();
$this->field = FieldConfig::create([
'field_storage' => $this->fieldStorage,
'bundle' => 'entity_test',
'required' => TRUE,
]);
$this->field->save();
EntityFormDisplay::load('entity_test.entity_test.default')
->setComponent($field_name, ['type' => $widget_type])
->save();
$this->displayOptions = [
'type' => $formatter_type,
'label' => 'hidden',
];
EntityViewDisplay::create([
'targetEntityType' => $this->field->getTargetEntityTypeId(),
'bundle' => $this->field->getTargetBundle(),
'mode' => 'full',
'status' => TRUE,
])->setComponent($field_name, $this->displayOptions)
->save();
}
/**
* Tests the formatter settings.
*/
public function testSettings() {
$this->drupalGet('entity_test/structure/entity_test/display');
$edit = [
'fields[field_timestamp][region]' => 'content',
'fields[field_timestamp][type]' => 'timestamp_ago',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->drupalPostForm(NULL, [], 'field_timestamp_settings_edit');
$edit = [
'fields[field_timestamp][settings_edit_form][settings][future_format]' => 'ends in @interval',
'fields[field_timestamp][settings_edit_form][settings][past_format]' => 'started @interval ago',
'fields[field_timestamp][settings_edit_form][settings][granularity]' => 3,
];
$this->drupalPostForm(NULL, $edit, 'Update');
$this->drupalPostForm(NULL, [], 'Save');
$this->assertSession()->pageTextContains('ends in 1 year 1 month 1 week');
$this->assertSession()->pageTextContains('started 1 year 1 month 1 week ago');
}
}

View file

@ -75,10 +75,12 @@ class ContentEntityFormCorrectUserInputMappingOnFieldDeltaElementsTest extends B
$storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId);
/** @var ContentEntityInterface $entity */
$entity = $storage->create([$this->fieldName => [
['shape' => 'rectangle', 'color' => 'green'],
['shape' => 'circle', 'color' => 'blue'],
]]);
$entity = $storage->create([
$this->fieldName => [
['shape' => 'rectangle', 'color' => 'green'],
['shape' => 'circle', 'color' => 'blue'],
],
]);
$entity->save();
$this->drupalGet($this->entityTypeId . '/manage/' . $entity->id() . '/edit');

View file

@ -0,0 +1,163 @@
<?php
namespace Drupal\FunctionalTests\Entity;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests field validation filtering on content entity forms.
*
* @group Entity
*/
class ContentEntityFormFieldValidationFilteringTest extends BrowserTestBase {
use TestFileCreationTrait;
/**
* The ID of the type of the entity under test.
*
* @var string
*/
protected $entityTypeId;
/**
* The single-valued field name being tested with the entity type.
*
* @var string
*/
protected $fieldNameSingle;
/**
* The multi-valued field name being tested with the entity type.
*
* @var string
*/
protected $fieldNameMultiple;
/**
* The name of the file field being tested with the entity type.
*
* @var string
*/
protected $fieldNameFile;
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test', 'field_test', 'file', 'image'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(['administer entity_test content']);
$this->drupalLogin($web_user);
// Create two fields of field type "test_field", one with single cardinality
// and one with unlimited cardinality on the entity type "entity_test". It
// is important to use this field type because its default widget has a
// custom \Drupal\Core\Field\WidgetInterface::errorElement() implementation.
$this->entityTypeId = 'entity_test';
$this->fieldNameSingle = 'test_single';
$this->fieldNameMultiple = 'test_multiple';
$this->fieldNameFile = 'test_file';
FieldStorageConfig::create([
'field_name' => $this->fieldNameSingle,
'entity_type' => $this->entityTypeId,
'type' => 'test_field',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => $this->entityTypeId,
'field_name' => $this->fieldNameSingle,
'bundle' => $this->entityTypeId,
'label' => 'Test single',
'required' => TRUE,
'translatable' => FALSE,
])->save();
FieldStorageConfig::create([
'field_name' => $this->fieldNameMultiple,
'entity_type' => $this->entityTypeId,
'type' => 'test_field',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
])->save();
FieldConfig::create([
'entity_type' => $this->entityTypeId,
'field_name' => $this->fieldNameMultiple,
'bundle' => $this->entityTypeId,
'label' => 'Test multiple',
'translatable' => FALSE,
])->save();
// Also create a file field to test its '#limit_validation_errors'
// implementation.
FieldStorageConfig::create([
'field_name' => $this->fieldNameFile,
'entity_type' => $this->entityTypeId,
'type' => 'file',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => $this->entityTypeId,
'field_name' => $this->fieldNameFile,
'bundle' => $this->entityTypeId,
'label' => 'Test file',
'translatable' => FALSE,
])->save();
entity_get_form_display($this->entityTypeId, $this->entityTypeId, 'default')
->setComponent($this->fieldNameSingle, ['type' => 'test_field_widget'])
->setComponent($this->fieldNameMultiple, ['type' => 'test_field_widget'])
->setComponent($this->fieldNameFile, ['type' => 'file_generic'])
->save();
}
/**
* Tests field widgets with #limit_validation_errors.
*/
public function testFieldWidgetsWithLimitedValidationErrors() {
$assert_session = $this->assertSession();
$this->drupalGet($this->entityTypeId . '/add');
// The 'Test multiple' field is the only multi-valued field in the form, so
// try to add a new item for it. This tests the '#limit_validation_errors'
// property set by \Drupal\Core\Field\WidgetBase::formMultipleElements().
$assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.form-type-textfield input', 1);
$this->drupalPostForm(NULL, [], 'Add another item');
$assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.form-type-textfield input', 2);
// Now try to upload a file. This tests the '#limit_validation_errors'
// property set by
// \Drupal\file\Plugin\Field\FieldWidget\FileWidget::process().
$text_file = current($this->getTestFiles('text'));
$edit = [
'files[test_file_0]' => \Drupal::service('file_system')->realpath($text_file->uri),
];
$assert_session->elementNotExists('css', 'input#edit-test-file-0-remove-button');
$this->drupalPostForm(NULL, $edit, 'Upload');
$assert_session->elementExists('css', 'input#edit-test-file-0-remove-button');
// Make the 'Test multiple' field required and check that adding another
// item throws a validation error.
$field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $this->fieldNameMultiple);
$field_config->setRequired(TRUE);
$field_config->save();
$this->drupalPostForm($this->entityTypeId . '/add', [], 'Add another item');
$assert_session->pageTextContains('Test multiple (value 1) field is required.');
// Check that saving the form without entering any value for the required
// field still throws the proper validation errors.
$this->drupalPostForm(NULL, [], 'Save');
$assert_session->pageTextContains('Test single field is required.');
$assert_session->pageTextContains('Test multiple (value 1) field is required.');
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace Drupal\FunctionalTests\Entity;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the delete multiple confirmation form.
*
* @group Entity
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class DeleteMultipleFormTest extends BrowserTestBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['entity_test', 'user', 'language'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
EntityTestBundle::create([
'id' => 'default',
'label' => 'Default',
])->save();
$this->account = $this->drupalCreateUser(['administer entity_test content']);
$this->drupalLogin($this->account);
}
/**
* Tests the delete form for translatable entities.
*/
public function testTranslatableEntities() {
ConfigurableLanguage::create(['id' => 'es'])->save();
ConfigurableLanguage::create(['id' => 'fr'])->save();
$selection = [];
$entity1 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity1']);
$entity1->addTranslation('es', ['name' => 'entity1 spanish']);
$entity1->addTranslation('fr', ['name' => 'entity1 french']);
$entity1->save();
$selection[$entity1->id()]['en'] = 'en';
$entity2 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity2']);
$entity2->addTranslation('es', ['name' => 'entity2 spanish']);
$entity2->addTranslation('fr', ['name' => 'entity2 french']);
$entity2->save();
$selection[$entity2->id()]['es'] = 'es';
$selection[$entity2->id()]['fr'] = 'fr';
$entity3 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity3']);
$entity3->addTranslation('es', ['name' => 'entity3 spanish']);
$entity3->addTranslation('fr', ['name' => 'entity3 french']);
$entity3->save();
$selection[$entity3->id()]['fr'] = 'fr';
// This entity will be inaccessible because of
// Drupal\entity_test\EntityTestAccessControlHandler.
$entity4 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'forbid_access']);
$entity4->save();
$selection[$entity4->id()]['en'] = 'en';
// Add the selection to the tempstore just like DeleteAction would.
$tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm');
$tempstore->set($this->account->id() . ':entity_test_mulrevpub', $selection);
$this->drupalGet('/entity_test/delete');
$assert = $this->assertSession();
$assert->statusCodeEquals(200);
$assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these test entity - revisions, data table, and published interface entities?');
$list_selector = '#entity-test-mulrevpub-delete-multiple-confirm-form > div.item-list > ul';
$assert->elementTextContains('css', $list_selector, 'entity1 (Original translation) - The following test entity - revisions, data table, and published interface translations will be deleted:');
$assert->elementTextContains('css', $list_selector, 'entity2 spanish');
$assert->elementTextContains('css', $list_selector, 'entity2 french');
$assert->elementTextNotContains('css', $list_selector, 'entity3 spanish');
$assert->elementTextContains('css', $list_selector, 'entity3 french');
$delete_button = $this->getSession()->getPage()->findButton('Delete');
$delete_button->click();
$assert = $this->assertSession();
$assert->addressEquals('/user/' . $this->account->id());
$assert->responseContains('Deleted 6 items.');
$assert->responseContains('1 item has not been deleted because you do not have the necessary permissions.');
\Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache();
$remaining_entities = EntityTestMulRevPub::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]);
$this->assertCount(3, $remaining_entities);
}
/**
* Tests the delete form for untranslatable entities.
*/
public function testUntranslatableEntities() {
$selection = [];
$entity1 = EntityTestRev::create(['type' => 'default', 'name' => 'entity1']);
$entity1->save();
$selection[$entity1->id()]['en'] = 'en';
$entity2 = EntityTestRev::create(['type' => 'default', 'name' => 'entity2']);
$entity2->save();
$selection[$entity2->id()]['en'] = 'en';
// This entity will be inaccessible because of
// Drupal\entity_test\EntityTestAccessControlHandler.
$entity3 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']);
$entity3->save();
$selection[$entity3->id()]['en'] = 'en';
// This entity will be inaccessible because of
// Drupal\entity_test\EntityTestAccessControlHandler.
$entity4 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']);
$entity4->save();
$selection[$entity4->id()]['en'] = 'en';
// Add the selection to the tempstore just like DeleteAction would.
$tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm');
$tempstore->set($this->account->id() . ':entity_test_rev', $selection);
$this->drupalGet('/entity_test_rev/delete_multiple');
$assert = $this->assertSession();
$assert->statusCodeEquals(200);
$assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these test entity - revisions entities?');
$list_selector = '#entity-test-rev-delete-multiple-confirm-form > div.item-list > ul';
$assert->elementTextContains('css', $list_selector, 'entity1');
$assert->elementTextContains('css', $list_selector, 'entity2');
$delete_button = $this->getSession()->getPage()->findButton('Delete');
$delete_button->click();
$assert = $this->assertSession();
$assert->addressEquals('/user/' . $this->account->id());
$assert->responseContains('Deleted 2 items.');
$assert->responseContains('2 items have not been deleted because you do not have the necessary permissions.');
\Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache();
$remaining_entities = EntityTestRev::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]);
$this->assertCount(2, $remaining_entities);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonAnonTest extends BaseFieldOverrideResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonBasicAuthTest extends BaseFieldOverrideResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\BaseFieldOverrideResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class BaseFieldOverrideHalJsonCookieTest extends BaseFieldOverrideResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonAnonTest extends DateFormatResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonBasicAuthTest extends DateFormatResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\DateFormatResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class DateFormatHalJsonCookieTest extends DateFormatResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonAnonTest extends EntityFormDisplayResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonBasicAuthTest extends EntityFormDisplayResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityFormDisplayHalJsonCookieTest extends EntityFormDisplayResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonAnonTest extends EntityFormModeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonBasicAuthTest extends EntityFormModeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityFormModeResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityFormModeHalJsonCookieTest extends EntityFormModeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewDisplayResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonAnonTest extends EntityViewDisplayResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonBasicAuthTest extends EntityViewDisplayHalJsonAnonTest {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityViewDisplayHalJsonCookieTest extends EntityViewDisplayHalJsonAnonTest {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonAnonTest extends EntityViewModeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonBasicAuthTest extends EntityViewModeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\EntityViewModeResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EntityViewModeHalJsonCookieTest extends EntityViewModeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -2,6 +2,7 @@
namespace Drupal\FunctionalTests\HttpKernel;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
@ -72,6 +73,19 @@ class CorsIntegrationTest extends BrowserTestBase {
$this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.com');
// Verify POST still functions with 'Origin' header set to site's domain.
$origin = \Drupal::request()->getSchemeAndHttpHost();
/** @var \GuzzleHttp\ClientInterface $httpClient */
$httpClient = $this->getSession()->getDriver()->getClient()->getClient();
$url = Url::fromUri('base:/test-page');
$response = $httpClient->request('POST', $url->setAbsolute()->toString(), [
'headers' => [
'Origin' => $origin,
],
]);
$this->assertEquals(200, $response->getStatusCode());
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\FunctionalTests\Image;
use Drupal\Tests\BrowserTestBase;
/**
* Tests image toolkit setup form.
*
* @group Image
*/
class ToolkitSetupFormTest extends BrowserTestBase {
/**
* Admin user account.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['system', 'image_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Test Image toolkit setup form.
*/
public function testToolkitSetupForm() {
// Get form.
$this->drupalGet('admin/config/media/image-toolkit');
// Test that default toolkit is GD.
$this->assertFieldByName('image_toolkit', 'gd', 'The default image toolkit is GD.');
// Test changing the jpeg image quality.
$edit = ['gd[image_jpeg_quality]' => '70'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image.gd')->get('jpeg_quality'), '70');
// Test changing the toolkit.
$edit = ['image_toolkit' => 'test'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image')->get('toolkit'), 'test');
$this->assertFieldByName('test[test_parameter]', '10');
// Test changing the test toolkit parameter.
$edit = ['test[test_parameter]' => '0'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertText(t('Test parameter should be different from 0.'), 'Validation error displayed.');
$edit = ['test[test_parameter]' => '20'];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->assertEqual($this->config('system.image.test_toolkit')->get('test_parameter'), '20');
// Test access without the permission 'administer site configuration'.
$this->drupalLogin($this->drupalCreateUser(['access administration pages']));
$this->drupalGet('admin/config/media/image-toolkit');
$this->assertResponse(403);
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\FunctionalTests\Image;
/**
* Tests image toolkit functions.
*
* @group Image
*/
class ToolkitTest extends ToolkitTestBase {
/**
* Check that ImageToolkitManager::getAvailableToolkits() only returns
* available toolkits.
*/
public function testGetAvailableToolkits() {
$manager = $this->container->get('image.toolkit.manager');
$toolkits = $manager->getAvailableToolkits();
$this->assertTrue(isset($toolkits['test']), 'The working toolkit was returned.');
$this->assertTrue(isset($toolkits['test:derived_toolkit']), 'The derived toolkit was returned.');
$this->assertFalse(isset($toolkits['broken']), 'The toolkit marked unavailable was not returned');
$this->assertToolkitOperationsCalled([]);
}
/**
* Tests Image's methods.
*/
public function testLoad() {
$image = $this->getImage();
$this->assertTrue(is_object($image), 'Returned an object.');
$this->assertEqual($image->getToolkitId(), 'test', 'Image had toolkit set.');
$this->assertToolkitOperationsCalled(['parseFile']);
}
/**
* Test the image_save() function.
*/
public function testSave() {
$this->assertFalse($this->image->save(), 'Function returned the expected value.');
$this->assertToolkitOperationsCalled(['save']);
}
/**
* Test the image_apply() function.
*/
public function testApply() {
$data = ['p1' => 1, 'p2' => TRUE, 'p3' => 'text'];
$this->assertTrue($this->image->apply('my_operation', $data), 'Function returned the expected value.');
// Check that apply was called and with the correct parameters.
$this->assertToolkitOperationsCalled(['apply']);
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['apply'][0][0], 'my_operation', "'my_operation' was passed correctly as operation");
$this->assertEqual($calls['apply'][0][1]['p1'], 1, 'integer parameter p1 was passed correctly');
$this->assertEqual($calls['apply'][0][1]['p2'], TRUE, 'boolean parameter p2 was passed correctly');
$this->assertEqual($calls['apply'][0][1]['p3'], 'text', 'string parameter p3 was passed correctly');
}
/**
* Test the image_apply() function.
*/
public function testApplyNoParameters() {
$this->assertTrue($this->image->apply('my_operation'), 'Function returned the expected value.');
// Check that apply was called and with the correct parameters.
$this->assertToolkitOperationsCalled(['apply']);
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['apply'][0][0], 'my_operation', "'my_operation' was passed correctly as operation");
$this->assertEqual($calls['apply'][0][1], [], 'passing no parameters was handled correctly');
}
/**
* Tests image toolkit operations inheritance by derivative toolkits.
*/
public function testDerivative() {
$toolkit_manager = $this->container->get('image.toolkit.manager');
$operation_manager = $this->container->get('image.toolkit.operation.manager');
$toolkit = $toolkit_manager->createInstance('test:derived_toolkit');
// Load an overwritten and an inherited operation.
$blur = $operation_manager->getToolkitOperation($toolkit, 'blur');
$invert = $operation_manager->getToolkitOperation($toolkit, 'invert');
$this->assertIdentical('foo_derived', $blur->getPluginId(), "'Blur' operation overwritten by derivative.");
$this->assertIdentical('bar', $invert->getPluginId(), '"Invert" operation inherited from base plugin.');
}
}

View file

@ -0,0 +1,158 @@
<?php
namespace Drupal\FunctionalTests\Image;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Base class for image manipulation testing.
*/
abstract class ToolkitTestBase extends BrowserTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['image_test'];
/**
* The URI for the file.
*
* @var string
*/
protected $file;
/**
* The image factory service.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* The image object for the test file.
*
* @var \Drupal\Core\Image\ImageInterface
*/
protected $image;
protected function setUp() {
parent::setUp();
// Set the image factory service.
$this->imageFactory = $this->container->get('image.factory');
// Pick a file for testing.
$file = current($this->drupalGetTestFiles('image'));
$this->file = $file->uri;
// Setup a dummy image to work with.
$this->image = $this->getImage();
// Clear out any hook calls.
$this->imageTestReset();
}
/**
* Sets up an image with the custom toolkit.
*
* @return \Drupal\Core\Image\ImageInterface
* The image object.
*/
protected function getImage() {
$image = $this->imageFactory->get($this->file, 'test');
$this->assertTrue($image->isValid(), 'Image file was parsed.');
return $image;
}
/**
* Assert that all of the specified image toolkit operations were called
* exactly once once, other values result in failure.
*
* @param $expected
* Array with string containing with the operation name, e.g. 'load',
* 'save', 'crop', etc.
*/
public function assertToolkitOperationsCalled(array $expected) {
// If one of the image operations is expected, apply should be expected as
// well.
$operations = [
'resize',
'rotate',
'crop',
'desaturate',
'create_new',
'scale',
'scale_and_crop',
'my_operation',
'convert',
];
if (count(array_intersect($expected, $operations)) > 0 && !in_array('apply', $expected)) {
$expected[] = 'apply';
}
// Determine which operations were called.
$actual = array_keys(array_filter($this->imageTestGetAllCalls()));
// Determine if there were any expected that were not called.
$uncalled = array_diff($expected, $actual);
if (count($uncalled)) {
$this->assertTrue(FALSE, new FormattableMarkup('Expected operations %expected to be called but %uncalled was not called.', ['%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)]));
}
else {
$this->assertTrue(TRUE, new FormattableMarkup('All the expected operations were called: %expected', ['%expected' => implode(', ', $expected)]));
}
// Determine if there were any unexpected calls.
// If all unexpected calls are operations and apply was expected, we do not
// count it as an error.
$unexpected = array_diff($actual, $expected);
if (count($unexpected) && (!in_array('apply', $expected) || count(array_intersect($unexpected, $operations)) !== count($unexpected))) {
$this->assertTrue(FALSE, new FormattableMarkup('Unexpected operations were called: %unexpected.', ['%unexpected' => implode(', ', $unexpected)]));
}
else {
$this->assertTrue(TRUE, 'No unexpected operations were called.');
}
}
/**
* Resets/initializes the history of calls to the test toolkit functions.
*/
protected function imageTestReset() {
// Keep track of calls to these operations
$results = [
'parseFile' => [],
'save' => [],
'settings' => [],
'apply' => [],
'resize' => [],
'rotate' => [],
'crop' => [],
'desaturate' => [],
'create_new' => [],
'scale' => [],
'scale_and_crop' => [],
'convert' => [],
];
\Drupal::state()->set('image_test.results', $results);
}
/**
* Gets an array of calls to the test toolkit.
*
* @return array
* An array keyed by operation name ('parseFile', 'save', 'settings',
* 'resize', 'rotate', 'crop', 'desaturate') with values being arrays of
* parameters passed to each call.
*/
protected function imageTestGetAllCalls() {
return \Drupal::state()->get('image_test.results') ?: [];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\KernelTests\AssertConfigTrait;
/**
* Provides a class for install profiles to check their installed config.
*/
abstract class ConfigAfterInstallerTestBase extends InstallerTestBase {
use AssertConfigTrait;
/**
* Ensures that all the installed config looks like the exported one.
*
* @param array $skipped_config
* An array of skipped config.
*/
protected function assertInstalledConfig(array $skipped_config) {
$this->addToAssertionCount(1);
/** @var \Drupal\Core\Config\StorageInterface $active_config_storage */
$active_config_storage = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = $this->container->get('config.manager');
$default_install_path = 'core/profiles/' . $this->profile . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
$profile_config_storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION);
foreach ($profile_config_storage->listAll() as $config_name) {
$result = $config_manager->diff($profile_config_storage, $active_config_storage, $config_name);
try {
$this->assertConfigDiff($result, $config_name, $skipped_config);
}
catch (\Exception $e) {
$this->fail($e->getMessage());
}
}
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests distribution profile support with existing settings.
*
* @group Installer
*/
class DistributionProfileExistingSettingsTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Use the kernel to find the site path because the site.path service should
// not be available at this point in the install process.
$site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
// Pre-configure config directories.
$this->settings['config_directories'] = [
CONFIG_SYNC_DIRECTORY => (object) [
'value' => $site_path . '/files/config_staging',
'required' => TRUE,
],
];
mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Make settings file not writable.
$filename = $this->siteDirectory . '/settings.php';
// Make the settings file read-only.
// Not using File API; a potential error must trigger a PHP warning.
chmod($filename, 0444);
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getUsername());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
$this->assertArrayNotHasKey('install_profile', Settings::getAll(), 'The install profile has not been written to settings.php.');
$this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.');
$this->rebuildContainer();
$this->pass('Container can be rebuilt even though distribution is not written to settings.php.');
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support.
*
* @group Installer
*/
class DistributionProfileTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'bartik',
'finish_url' => '/myrootuser',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
file_put_contents("$path/mydistro.install", "<?php function mydistro_install() {\Drupal::service('path.alias_storage')->save('/user/1', '/myrootuser');}");
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the distribution name is used in the site title.
$this->assertTitle('Choose language | ' . $this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('myrootuser');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getUsername());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEqual(\Drupal::installProfile(), 'mydistro');
$this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.');
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support with a 'langcode' query string.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\DistributionProfileTranslationTest
*/
class DistributionProfileTranslationQueryTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $langcode = 'de';
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'langcode' => $this->langcode,
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.fr.po', $this->getPo('fr'));
}
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Pass a different language code than the one set in the distribution
// profile. This distribution language should still be used.
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=fr');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// The language should have been automatically detected, all following
// screens should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = $this->getSession()->getPage()->find('xpath', '/@dir')->getText();
$this->assertEqual($direction, 'ltr');
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
ENDPO;
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\DistributionProfileTest
*/
class DistributionProfileTranslationTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $langcode = 'de';
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'langcode' => $this->langcode,
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/mydistro';
mkdir($path, 0777, TRUE);
file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// The language should have been automatically detected, all following
// screens should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = current($this->xpath('/@dir'))->getText();
$this->assertEqual($direction, 'ltr');
// Verify that the distribution name appears.
$this->assertRaw($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertRaw($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
ENDPO;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile with only dependencies works as expected.
*
* @group Installer
* @group legacy
*/
class InstallProfileDependenciesBcTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_install_profile_dependencies_bc';
/**
* Tests that the install profile BC layer for dependencies key works.
*
* @expectedDeprecation The install profile core/profiles/testing_install_profile_dependencies_bc/testing_install_profile_dependencies_bc.info.yml only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.
*/
public function testUninstallingModules() {
$user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($user);
$this->drupalGet('admin/modules/uninstall');
$this->getSession()->getPage()->checkField('uninstall[ban]');
$this->getSession()->getPage()->checkField('uninstall[dblog]');
$this->click('#edit-submit');
// Click the confirm button.
$this->click('#edit-submit');
$this->assertSession()->responseContains('The selected modules have been uninstalled.');
$this->assertSession()->responseContains('No modules are available to uninstall.');
// We've uninstalled modules therefore we need to rebuild the container in
// the test runner.
$this->rebuildContainer();
$module_handler = $this->container->get('module_handler');
$this->assertFalse($module_handler->moduleExists('ban'));
$this->assertFalse($module_handler->moduleExists('dblog'));
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Extension\ModuleUninstallValidatorException;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile can require modules.
*
* @group Installer
*/
class InstallProfileDependenciesTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_install_profile_dependencies';
/**
* Tests that an install profile can require modules.
*/
public function testUninstallingModules() {
$user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($user);
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->fieldDisabled('uninstall[dblog]');
$this->getSession()->getPage()->checkField('uninstall[ban]');
$this->click('#edit-submit');
// Click the confirm button.
$this->click('#edit-submit');
$this->assertSession()->responseContains('The selected modules have been uninstalled.');
// We've uninstalled a module therefore we need to rebuild the container in
// the test runner.
$this->rebuildContainer();
$this->assertFalse($this->container->get('module_handler')->moduleExists('ban'));
try {
$this->container->get('module_installer')->uninstall(['dblog']);
$this->fail('Uninstalled dblog module.');
}
catch (ModuleUninstallValidatorException $e) {
$this->assertContains('The Testing install profile dependencies module is required', $e->getMessage());
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Utility\Crypt;
/**
* Tests the installer when a config_directory set up but does not exist.
*
* @group Installer
*/
class InstallerConfigDirectorySetNoDirectoryErrorTest extends InstallerTestBase {
/**
* The directory where the sync directory should be created during install.
*
* @var string
*/
protected $configDirectory;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->configDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64();
$this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $this->configDirectory . '/sync',
'required' => TRUE,
];
// Create the files directory early so we can test the error case.
mkdir($this->publicFilesDirectory);
// Create a file so the directory can not be created.
file_put_contents($this->configDirectory, 'Test');
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// This step should not appear as we had a failure prior to the settings
// screen.
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
// This step should not appear as we had a failure prior to the settings
// screen.
}
/**
* Verifies that installation failed.
*/
public function testError() {
$this->assertText("An automated attempt to create the directory {$this->configDirectory}/sync failed, possibly due to a permissions problem.");
$this->assertFalse(file_exists($this->configDirectory . '/sync') && is_dir($this->configDirectory . '/sync'), "The directory {$this->configDirectory}/sync does not exist.");
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Utility\Crypt;
/**
* Tests the installer when a config_directory set up but does not exist.
*
* @group Installer
*/
class InstallerConfigDirectorySetNoDirectoryTest extends InstallerTestBase {
/**
* The sync directory created during the install.
*
* @var string
*/
protected $syncDirectory;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->syncDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64() . '/sync';
$this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $this->syncDirectory,
'required' => TRUE,
];
// Other directories will be created too.
$this->settings['config_directories']['custom'] = (object) [
'value' => $this->publicFilesDirectory . '/config_custom',
'required' => TRUE,
];
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
$this->assertTrue(file_exists($this->syncDirectory) && is_dir($this->syncDirectory), "The directory {$this->syncDirectory} exists.");
$this->assertTrue(file_exists($this->publicFilesDirectory . '/config_custom') && is_dir($this->publicFilesDirectory . '/config_custom'), "The directory {$this->publicFilesDirectory}/custom_config exists.");
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with database errors.
*
* @group Installer
*/
class InstallerDatabaseErrorMessagesTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// We are creating a table here to force an error in the installer because
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
parent::setUpSettings();
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
// This step should not appear as we had a failure on the settings screen.
}
/**
* Verifies that the error message in the settings step is correct.
*/
public function testSetUpSettingsErrorMessage() {
$this->assertRaw('<ul><li>Failed to <strong>CREATE</strong> a test table');
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the installer with empty settings file.
*
* @group Installer
*/
class InstallerEmptySettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Create an empty settings.php file.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory;
file_put_contents($path . '/settings.php', '');
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the installer when a config_directory has already been set up.
*
* @group Installer
*/
class InstallerExistingConfigDirectoryTest extends InstallerTestBase {
/**
* The expected file perms of the folder.
*
* @var int
*/
protected $expectedFilePerms;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
mkdir($this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/config_read_only', 0444);
$this->expectedFilePerms = fileperms($this->siteDirectory . '/config_read_only');
$this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $this->siteDirectory . '/config_read_only',
'required' => TRUE,
];
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
$this->assertEqual($this->expectedFilePerms, fileperms($this->siteDirectory . '/config_read_only'));
$this->assertEqual([], glob($this->siteDirectory . '/config_read_only/*'), 'The sync directory is empty after install because it is read-only.');
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigMultilingualTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles invalid config can not be installed.
*
* @group Installer
*/
class InstallerExistingConfigNoConfigTest extends InstallerExistingConfigTestBase {
protected $profile = 'no_config_profile';
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// There are errors therefore there is nothing to do here.
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz';
}
/**
* Tests that profiles with an empty config/sync directory do not work.
*/
public function testConfigSync() {
$this->assertTitle('Configuration validation | Drupal');
$this->assertText('The configuration synchronization failed validation.');
$this->assertText('This import is empty and if applied would delete all of your configuration, so has been rejected.');
// Ensure there is no continuation button.
$this->assertNoText('Save and continue');
$this->assertNoFieldById('edit-submit');
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Testing installing from config without system.site.
*
* @group Installer
*/
class InstallerExistingConfigNoSystemSiteTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// File API functions are not available yet.
unlink($this->siteDirectory . '/profiles/' . $this->profile . '/config/sync/system.site.yml');
}
/**
* {@inheritdoc}
*/
public function setUpSite() {
return;
}
/**
* Tests that profiles with no system.site do not work.
*/
public function testConfigSync() {
$this->htmlOutput(NULL);
$this->assertTitle('Configuration validation | Drupal');
$this->assertText('The configuration synchronization failed validation.');
$this->assertText('This import does not contain system.site configuration, so has been rejected.');
// Ensure there is no continuation button.
$this->assertNoText('Save and continue');
$this->assertNoFieldById('edit-submit');
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz';
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigProfileHookInstall extends InstallerExistingConfigTestBase {
protected $profile = 'config_profile_with_hook_install';
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function config_profile_with_hook_install_install() {
}
EOF;
file_put_contents("$path/{$this->profile}.install", $contents);
parent::visitInstaller();
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// There are errors therefore there is nothing to do here.
return;
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// There are errors therefore there is nothing to do here.
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
// We're not going to get to the config import stage so this does not
// matter.
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz';
}
/**
* Confirms the installation has failed and the expected error is displayed.
*/
public function testConfigSync() {
$this->assertTitle('Requirements problem | Drupal');
$this->assertText($this->profile);
$this->assertText('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.');
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryMultilingualTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
// Ensure the site name 'Multilingual' appears as expected in the 'Use
// existing configuration' radio description.
$this->assertSession()->pageTextContains('Install Multilingual using existing configuration.');
return parent::setUpProfile();
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
/**
* Confirms that the installation installed the configuration correctly.
*/
public function testConfigSync() {
parent::testConfigSync();
// Ensure that menu blocks have been created correctly.
$this->assertSession()->responseNotContains('This block is broken or missing.');
$this->assertSession()->linkExists('Add content');
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryProfileHookInstall extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function testing_config_install_multilingual_install() {
}
EOF;
file_put_contents("$path/{$this->profile}.install", $contents);
parent::visitInstaller();
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
// This is the form we are testing so wait until the test method to do
// assertions.
return;
}
/**
* Installer step: Requirements problem.
*/
protected function setUpRequirementsProblem() {
// This form will never be reached.
return;
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// This form will never be reached.
return;
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// This form will never be reached.
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
/**
* Tests installing from config is not available due to hook_INSTALL().
*/
public function testConfigSync() {
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseNotContains('Use existing configuration');
// Remove the install hook and the option to install from existing
// configuration will be available.
unlink("{$this->siteDirectory}/profiles/{$this->profile}/{$this->profile}.install");
$this->getSession()->reload();
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseContains('Use existing configuration');
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDriectoryProfileMismatchTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// Cause a profile mismatch by hacking the URL.
$this->drupalGet(str_replace($this->profile, 'minimal', $this->getUrl()));
parent::setUpSettings();
}
protected function setUpSite() {
// This step will not occur because there is an error.
return;
}
/**
* Tests that profile mismatch fails to install.
*/
public function testConfigSync() {
$this->htmlOutput(NULL);
$this->assertTitle('Configuration validation | Drupal');
$this->assertText('The configuration synchronization failed validation.');
$this->assertText('The selected installation profile minimal does not match the profile stored in configuration testing_config_install_multilingual.');
// Ensure there is no continuation button.
$this->assertNoText('Save and continue');
$this->assertNoFieldById('edit-submit');
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
public function setUpSite() {
// The configuration is from a site installed in French.
// So after selecting the profile the installer detects that the site must
// be installed in French, thus we change the button translation.
$this->translations['Save and continue'] = 'Enregistrer et continuer';
parent::setUpSite();
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz';
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
* Provides a base class for testing installing from existing configuration.
*/
abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
/**
* This is set by the profile in the core.extension extracted.
*/
protected $profile = NULL;
/**
* @todo
*/
protected $existingSyncDirectory = FALSE;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$archiver = new ArchiveTar($this->getConfigTarball(), 'gz');
if ($this->profile === NULL) {
$core_extension = Yaml::decode($archiver->extractInString('core.extension.yml'));
$this->profile = $core_extension['profile'];
}
// Create a profile for testing.
$info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Configuration installation test profile (' . $this->profile . ')',
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
if ($this->existingSyncDirectory) {
$config_sync_directory = $this->siteDirectory . '/config/sync';
$this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $config_sync_directory,
'required' => TRUE,
];
}
else {
// Put the sync directory inside the profile.
$config_sync_directory = $path . '/config/sync';
}
mkdir($path, 0777, TRUE);
file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info));
// Create config/sync directory and extract tarball contents to it.
mkdir($config_sync_directory, 0777, TRUE);
$files = [];
$list = $archiver->listContent();
if (is_array($list)) {
/** @var array $list */
foreach ($list as $file) {
$files[] = $file['filename'];
}
$archiver->extractList($files, $config_sync_directory);
}
}
/**
* Gets the filepath to the configuration tarball.
*
* The tarball will be extracted to the install profile's config/sync
* directory for testing.
*
* @return string
* The filepath to the configuration tarball.
*/
abstract protected function getConfigTarball();
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// The options that change configuration are disabled when installing from
// existing configuration.
unset($parameters['forms']['install_configure_form']['site_name']);
unset($parameters['forms']['install_configure_form']['site_mail']);
unset($parameters['forms']['install_configure_form']['update_status_module']);
return $parameters;
}
/**
* Confirms that the installation installed the configuration correctly.
*/
public function testConfigSync() {
// After installation there is no snapshot and nothing to import.
$change_list = $this->configImporter()->getStorageComparer()->getChangelist();
$expected = [
'create' => [],
// The system.mail is changed configuration because the test system
// changes it to ensure that mails are not sent.
'update' => ['system.mail'],
'delete' => [],
'rename' => [],
];
$this->assertEqual($expected, $change_list);
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
if ($this->existingSyncDirectory) {
$edit = [
'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
];
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
else {
parent::setUpProfile();
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with an existing settings file with database connection
* info.
*
* @group Installer
*/
class InstallerExistingDatabaseSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Pre-configure database credentials in settings.php.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
/**
* {@inheritdoc}
*
* @todo The database settings form is not supposed to appear if settings.php
* contains a valid database connection already (but e.g. no config
* directories yet).
*/
protected function setUpSettings() {
// All database settings should be pre-configured, except password.
$values = $this->parameters['forms']['install_settings_form'];
$driver = $values['driver'];
$edit = [];
if (isset($values[$driver]['password']) && $values[$driver]['password'] !== '') {
$edit = $this->translatePostValues([
$driver => [
'password' => $values[$driver]['password'],
],
]);
}
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the installer with an existing Drupal installation.
*
* @group Installer
*/
class InstallerExistingInstallationTest extends InstallerTestBase {
/**
* Verifies that Drupal can't be reinstalled while an existing installation is
* available.
*/
public function testInstaller() {
// Verify that Drupal can't be immediately reinstalled.
$this->visitInstaller();
$this->assertRaw('Drupal already installed');
// Delete settings.php and attempt to reinstall again.
unlink($this->siteDirectory . '/settings.php');
$this->visitInstaller();
$this->setUpLanguage();
$this->setUpProfile();
$this->setUpRequirementsProblem();
$this->setUpSettings();
$this->assertRaw('Drupal already installed');
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Drupal\Core\Database\Database;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests install with existing settings.php and a mismatching install profile.
*
* @group Installer
* @group legacy
*/
class InstallerExistingSettingsMismatchProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*
* Configures a preexisting settings.php file without an install_profile
* setting before invoking the interactive installer.
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// During interactive install we'll change this to a different profile and
// this test will ensure that the new value is written to settings.php.
$this->settings['settings']['install_profile'] = (object) [
'value' => 'minimal',
'required' => TRUE,
];
// Pre-configure config directories.
$this->settings['config_directories'] = [
CONFIG_SYNC_DIRECTORY => (object) [
'value' => DrupalKernel::findSitePath(Request::createFromGlobals()) . '/files/config_sync',
'required' => TRUE,
],
];
mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Provide profile and language in query string to skip these pages.
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// This step is skipped, because there is a langcode as a query param.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a profile as a query param.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*
* @expectedDeprecation To access the install profile in Drupal 8 use \Drupal::installProfile() or inject the install_profile container parameter into your service. See https://www.drupal.org/node/2538996
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
$this->assertEqual('testing', \Drupal::installProfile());
$this->assertEqual('testing', Settings::get('install_profile'), 'Profile was correctly changed to testing in Settings.php');
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the installer with an existing settings file but no install profile.
*
* @group Installer
*/
class InstallerExistingSettingsNoProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*
* Configures a preexisting settings.php file without an install_profile
* setting before invoking the interactive installer.
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Pre-configure config directories.
$this->settings['config_directories'] = [
CONFIG_SYNC_DIRECTORY => (object) [
'value' => DrupalKernel::findSitePath(Request::createFromGlobals()) . '/files/config_sync',
'required' => TRUE,
],
];
mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
$this->assertEqual('testing', \Drupal::installProfile());
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests installer breaks with a profile mismatch and a read-only settings.php.
*
* @group Installer
* @group legacy
*/
class InstallerExistingSettingsReadOnlyMismatchProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*
* Configures a preexisting settings.php file without an install_profile
* setting before invoking the interactive installer.
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// During interactive install we'll change this to a different profile and
// this test will ensure that the new value is written to settings.php.
$this->settings['settings']['install_profile'] = (object) [
'value' => 'minimal',
'required' => TRUE,
];
// Pre-configure config directories.
$site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
$this->settings['config_directories'] = [
CONFIG_SYNC_DIRECTORY => (object) [
'value' => $site_path . '/files/config_staging',
'required' => TRUE,
],
];
mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Make settings file not writable. This will break the installer.
$filename = $this->siteDirectory . '/settings.php';
// Make the settings file read-only.
// Not using File API; a potential error must trigger a PHP warning.
chmod($filename, 0444);
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// This step is skipped, because there is a lagcode as a query param.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a profile as a query param.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*
* @expectedDeprecation To access the install profile in Drupal 8 use \Drupal::installProfile() or inject the install_profile container parameter into your service. See https://www.drupal.org/node/2538996
*/
public function testInstalled() {
$this->initBrowserOutputFile();
$this->htmlOutput(NULL);
$this->assertEquals('testing', \Drupal::installProfile());
$this->assertEquals('minimal', Settings::get('install_profile'));
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains("Drupal 8 no longer uses the \$settings['install_profile'] value in settings.php and it can be removed.");
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the installer with an existing settings file.
*
* @group Installer
*/
class InstallerExistingSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*
* Fully configures a preexisting settings.php file before invoking the
* interactive installer.
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Use the kernel to find the site path because the site.path service should
// not be available at this point in the install process.
$site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
// Pre-configure config directories.
$this->settings['config_directories'] = [
CONFIG_SYNC_DIRECTORY => (object) [
'value' => $site_path . '/files/config_sync',
'required' => TRUE,
],
];
mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
$this->assertEqual('testing', \Drupal::installProfile());
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that the early installer uses the correct language direction.
*
* @group Installer
*/
class InstallerLanguageDirectionTest extends InstallerTestBase {
/**
* Overrides the language code the installer should use.
*
* @var string
*/
protected $langcode = 'ar';
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.ar.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue Arabic\"");
parent::setUpLanguage();
// After selecting a different language than English, all following screens
// should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue Arabic');
$this->translations['Save and continue'] = 'Save and continue Arabic';
// Verify that language direction is right-to-left.
$direction = current($this->xpath('/@dir'))->getText();
$this->assertEqual($direction, 'rtl');
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Language\LanguageManager;
/**
* Verifies that the installer language list combines local and remote languages.
*
* @group Installer
*/
class InstallerLanguagePageTest extends InstallerTestBase {
/**
* Installer step: Select language.
*/
protected function setUpLanguage() {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
touch($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.xoxo.po');
// Check that all predefined languages show up with their native names.
$this->visitInstaller();
foreach (LanguageManager::getStandardLanguageList() as $langcode => $names) {
$this->assertOption('edit-langcode', $langcode);
$this->assertRaw('>' . $names[1] . '<');
}
// Check that our custom one shows up with the file name indicated language.
$this->assertOption('edit-langcode', 'xoxo');
$this->assertRaw('>xoxo<');
parent::setUpLanguage();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests that an install profile can implement hook_requirements().
*
* @group Installer
*/
class InstallerProfileRequirementsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_requirements';
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
// This form will never be reached.
}
/**
* Assert that the profile failed hook_requirements().
*/
public function testHookRequirementsFailure() {
$this->assertSession()->pageTextContains('Testing requirements failed requirements.');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that the installer skipped permission hardening.
*
* @group Installer
*/
class InstallerSkipPermissionHardeningTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$this->settings['settings']['skip_permissions_hardening'] = (object) ['value' => TRUE, 'required' => TRUE];
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
$site_directory = $this->container->get('app.root') . '/' . $this->siteDirectory;
$this->assertTrue(is_writable($site_directory));
$this->assertTrue(is_writable($site_directory . '/settings.php'));
$this->assertSession()->responseContains('All necessary changes to <em class="placeholder">' . $this->siteDirectory . '</em> and <em class="placeholder">' . $this->siteDirectory . '/settings.php</em> have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="https://www.drupal.org/server-permissions">online handbook</a>.');
parent::setUpSite();
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstalled() {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the interactive installer.
*
* @group Installer
*/
class InstallerTest extends InstallerTestBase {
/**
* Ensures that the user page is available after installation.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getUsername());
// Verify that the confirmation message appears.
require_once $this->root . '/core/includes/install.inc';
$this->assertRaw(t('Congratulations, you installed @drupal!', [
'@drupal' => drupal_install_profile_distribution_name(),
]));
// Ensure that the timezone is correct for sites under test after installing
// interactively.
$this->assertEqual($this->config('system.date')->get('timezone.default'), 'Australia/Sydney');
}
/**
* Installer step: Select language.
*/
protected function setUpLanguage() {
// Test that \Drupal\Core\Render\BareHtmlPageRenderer adds assets and
// metatags as expected to the first page of the installer.
$this->assertRaw("core/themes/seven/css/components/buttons.css");
$this->assertRaw('<meta charset="utf-8" />');
// Assert that the expected title is present.
$this->assertEqual('Choose language', $this->cssSelect('main h2')[0]->getText());
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// Assert that the expected title is present.
$this->assertEqual('Select an installation profile', $this->cssSelect('main h2')[0]->getText());
$result = $this->xpath('//span[contains(@class, :class) and contains(text(), :text)]', [':class' => 'visually-hidden', ':text' => 'Select an installation profile']);
$this->assertEqual(count($result), 1, "Title/Label not displayed when '#title_display' => 'invisible' attribute is set");
parent::setUpProfile();
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// Assert that the expected title is present.
$this->assertEqual('Database configuration', $this->cssSelect('main h2')[0]->getText());
parent::setUpSettings();
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
// Assert that the expected title is present.
$this->assertEqual('Configure site', $this->cssSelect('main h2')[0]->getText());
// Test that SiteConfigureForm::buildForm() has made the site directory and
// the settings file non-writable.
$site_directory = $this->container->get('app.root') . '/' . $this->siteDirectory;
$this->assertFalse(is_writable($site_directory));
$this->assertFalse(is_writable($site_directory . '/settings.php'));
parent::setUpSite();
}
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
parent::visitInstaller();
// Assert the title is correct and has the title suffix.
$this->assertTitle('Choose language | Drupal');
}
}

View file

@ -0,0 +1,319 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
use Drupal\Tests\BrowserTestBase;
use GuzzleHttp\HandlerStack;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Base class for testing the interactive installer.
*/
abstract class InstallerTestBase extends BrowserTestBase {
/**
* Custom settings.php values to write for a test run.
*
* @var array
* An array of settings to write out, in the format expected by
* drupal_rewrite_settings().
*/
protected $settings = [];
/**
* The language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'en';
/**
* The installation profile to install.
*
* @var string
*/
protected $profile = 'testing';
/**
* Additional parameters to use for installer screens.
*
* @see FunctionalTestSetupTrait::installParameters()
*
* @var array
*/
protected $parameters = [];
/**
* A string translation map used for translated installer screens.
*
* Keys are English strings, values are translated strings.
*
* @var array
*/
protected $translations = [
'Save and continue' => 'Save and continue',
];
/**
* Whether the installer has completed.
*
* @var bool
*/
protected $isInstalled = FALSE;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->isInstalled = FALSE;
$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();
// Set up the browser test output file.
$this->initBrowserOutputFile();
$this->visitInstaller();
// Select language.
$this->setUpLanguage();
// Select profile.
$this->setUpProfile();
// Address the requirements problem screen, if any.
$this->setUpRequirementsProblem();
// Configure settings.
$this->setUpSettings();
// @todo Allow test classes based on this class to act on further installer
// screens.
// Configure site.
$this->setUpSite();
if ($this->isInstalled) {
// Import new settings.php written by the installer.
$request = Request::createFromGlobals();
$class_loader = require $this->container->get('app.root') . '/autoload.php';
Settings::initialize($this->container->get('app.root'), DrupalKernel::findSitePath($request), $class_loader);
foreach ($GLOBALS['config_directories'] as $type => $path) {
$this->configDirectories[$type] = $path;
}
// After writing settings.php, the installer removes write permissions
// from the site directory. To allow drupal_generate_test_ua() to write
// a file containing the private key for drupal_valid_test_ua(), the site
// directory has to be writable.
// BrowserTestBase::tearDown() will delete the entire test site directory.
// Not using File API; a potential error must trigger a PHP warning.
chmod($this->container->get('app.root') . '/' . $this->siteDirectory, 0777);
$this->kernel = DrupalKernel::createFromRequest($request, $class_loader, 'prod', FALSE);
$this->kernel->prepareLegacyRequest($request);
$this->container = $this->kernel->getContainer();
// Manually configure the test mail collector implementation to prevent
// tests from sending out emails and collect them in state instead.
$this->container->get('config.factory')
->getEditable('system.mail')
->set('interface.default', 'test_mail_collector')
->save();
}
}
/**
* {@inheritdoc}
*/
protected function initFrontPage() {
// We don't want to visit the front page with the installer when
// initializing Mink, so we do nothing here.
}
/**
* Visits the interactive installer.
*/
protected function visitInstaller() {
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php');
}
/**
* Installer step: Select language.
*/
protected function setUpLanguage() {
$edit = [
'langcode' => $this->langcode,
];
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
$edit = [
'profile' => $this->profile,
];
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
$edit = $this->translatePostValues($this->parameters['forms']['install_settings_form']);
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
/**
* Installer step: Requirements problem.
*
* Override this method to test specific requirements warnings or errors
* during the installer.
*
* @see system_requirements()
*/
protected function setUpRequirementsProblem() {
// By default, skip the "recommended PHP version" warning on older test
// environments. This allows the installer to be tested consistently on
// both recommended PHP versions and older (but still supported) versions.
if (version_compare(phpversion(), '7.0') < 0) {
$this->continueOnExpectedWarnings(['PHP']);
}
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
$edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']);
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
// If we've got to this point the site is installed using the regular
// installation workflow.
$this->isInstalled = TRUE;
}
/**
* {@inheritdoc}
*
* FunctionalTestSetupTrait::refreshVariables() tries to operate on persistent
* storage, which is only available after the installer completed.
*/
protected function refreshVariables() {
if ($this->isInstalled) {
parent::refreshVariables();
}
}
/**
* Continues installation when an expected warning is found.
*
* @param string[] $expected_warnings
* A list of warning summaries to expect on the requirements screen (e.g.
* 'PHP', 'PHP OPcode caching', etc.). If only the expected warnings
* are found, the test will click the "continue anyway" link to go to the
* next screen of the installer. If an expected warning is not found, or if
* a warning not in the list is present, a fail is raised.
*/
protected function continueOnExpectedWarnings($expected_warnings = []) {
// Don't try to continue if there are errors.
if (strpos($this->getTextContent(), 'Errors found') !== FALSE) {
return;
}
// Allow only details elements that are directly after the warning header
// or each other. There is no guaranteed wrapper we can rely on across
// distributions. When there are multiple warnings, the selectors will be:
// - h3#warning+details summary
// - h3#warning+details+details summary
// - etc.
// We add one more selector than expected warnings to confirm that there
// isn't any other warning before clicking the link.
// @todo Make this more reliable in
// https://www.drupal.org/project/drupal/issues/2927345.
$selectors = [];
for ($i = 0; $i <= count($expected_warnings); $i++) {
$selectors[] = 'h3#warning' . implode('', array_fill(0, $i + 1, '+details')) . ' summary';
}
$warning_elements = $this->cssSelect(implode(', ', $selectors));
// Confirm that there are only the expected warnings.
$warnings = [];
foreach ($warning_elements as $warning) {
$warnings[] = trim($warning->getText());
}
$this->assertEquals($expected_warnings, $warnings);
$this->clickLink('continue anyway');
$this->checkForMetaRefresh();
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageForeignTest extends InstallerTranslationMultipleLanguageTest {
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
parent::setUpLanguage();
$this->translations['Save and continue'] = 'Save and continue de';
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests that keeping English in a foreign language install works.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageKeepEnglishTest extends InstallerTranslationMultipleLanguageForeignTest {
/**
* Switch to the multilingual testing profile with English kept.
*
* @var string
*/
protected $profile = 'testing_multilingual_with_english';
}

View file

@ -0,0 +1,176 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageTest extends InstallerTestBase {
/**
* Switch to the multilingual testing profile.
*
* @var string
*/
protected $profile = 'testing_multilingual';
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Place custom local translations in the translations directory.
mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es'));
parent::setUpLanguage();
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "Language"
msgstr "Language $langcode"
#: Testing site name configuration during the installer.
msgid "Drupal"
msgstr "Drupal"
ENDPO;
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$params = parent::installParameters();
$params['forms']['install_configure_form']['site_name'] = 'SITE_NAME_' . $this->langcode;
return $params;
}
/**
* Tests that translations ended up at the expected places.
*/
public function testTranslationsLoaded() {
// Ensure the title is correct.
$this->assertEqual('SITE_NAME_' . $this->langcode, \Drupal::config('system.site')->get('name'));
// Verify German and Spanish were configured.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertText('Spanish');
// If the installer was English or we used a profile that keeps English, we
// expect that configured also. Otherwise English should not be configured
// on the site.
if ($this->langcode == 'en' || $this->profile == 'testing_multilingual_with_english') {
$this->assertText('English');
}
else {
$this->assertNoText('English');
}
// Verify the strings from the translation files were imported.
$this->verifyImportedStringsTranslated();
/** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
$language_manager = \Drupal::languageManager();
// If the site was installed in a foreign language (only tested with German
// in subclasses), then the active configuration should be updated and no
// override should exist in German. Otherwise the German translation should
// end up in overrides the same way as Spanish (which is not used as a site
// installation language). English should be available based on profile
// information and should be possible to add if not yet added, making
// English overrides available.
$config = \Drupal::config('user.settings');
$override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings');
if ($this->langcode == 'de') {
// Active configuration should be in German and no German override should
// exist.
$this->assertEqual($config->get('anonymous'), 'Anonymous de');
$this->assertEqual($config->get('langcode'), 'de');
$this->assertTrue($override_de->isNew());
if ($this->profile == 'testing_multilingual_with_english') {
// English is already added in this profile. Should make the override
// available.
$this->assertEqual($override_en->get('anonymous'), 'Anonymous');
}
else {
// English is not yet available.
$this->assertTrue($override_en->isNew());
// Adding English should make the English override available.
$edit = ['predefined_langcode' => 'en'];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertEqual($override_en->get('anonymous'), 'Anonymous');
}
// Activate a module, to make sure that config is not overridden by module
// installation.
$edit = [
'modules[views][enable]' => TRUE,
'modules[filter][enable]' => TRUE,
];
$this->drupalPostForm('admin/modules', $edit, t('Install'));
// Verify the strings from the translation are still as expected.
$this->verifyImportedStringsTranslated();
}
else {
// Active configuration should be English.
$this->assertEqual($config->get('anonymous'), 'Anonymous');
$this->assertEqual($config->get('langcode'), 'en');
// There should not be an English override.
$this->assertTrue($override_en->isNew());
// German should be an override.
$this->assertEqual($override_de->get('anonymous'), 'Anonymous de');
}
// Spanish is always an override (never used as installation language).
$this->assertEqual($override_es->get('anonymous'), 'Anonymous es');
}
/**
* Helper function to verify that the expected strings are translated.
*/
protected function verifyImportedStringsTranslated() {
$test_samples = ['Save and continue', 'Anonymous', 'Language'];
$langcodes = ['de', 'es'];
foreach ($test_samples as $sample) {
foreach ($langcodes as $langcode) {
$edit = [];
$edit['langcode'] = $langcode;
$edit['translation'] = 'translated';
$edit['string'] = $sample;
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Filter'));
$this->assertText($sample . ' ' . $langcode);
}
}
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Installs Drupal in German and checks resulting site.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\InstallerTranslationTest
*/
class InstallerTranslationQueryTest extends InstallerTestBase {
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=' . $this->langcode);
// The language should have been automatically detected, all following
// screens should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = current($this->xpath('/@dir'))->getText();
$this->assertEqual($direction, 'ltr');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// The language was was preset by passing a query parameter in the URL, so
// no explicit language selection is necessary.
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
ENDPO;
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
use Drupal\user\Entity\User;
/**
* Installs Drupal in German and checks resulting site.
*
* @group Installer
*/
class InstallerTranslationTest extends InstallerTestBase {
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
parent::setUpLanguage();
// After selecting a different language than English, all following screens
// should be translated already.
$elements = $this->xpath('//input[@type="submit"]/@value');
$this->assertEqual(current($elements)->getText(), 'Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = current($this->xpath('/@dir'))->getText();
$this->assertEqual($direction, 'ltr');
}
/**
* {@inheritdoc}
*/
protected function setUpSettings() {
// We are creating a table here to force an error in the installer because
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
parent::setUpSettings();
// Ensure that the error message translation is working.
$this->assertRaw('Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/getting-started/install">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter.');
$this->assertRaw('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NULL)</em> fehlgeschlagen.');
// Now do it successfully.
Database::getConnection('default')->query('DROP TABLE {drupal_install_test}');
parent::setUpSettings();
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstaller() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
$this->assertText('German');
$this->assertNoText('English');
// The current container still has the english as current language, rebuild.
$this->rebuildContainer();
/** @var \Drupal\user\Entity\User $account */
$account = User::load(0);
$this->assertEqual($account->language()->getId(), 'en', 'Anonymous user is English.');
$account = User::load(1);
$this->assertEqual($account->language()->getId(), 'en', 'Administrator user is English.');
$account = $this->drupalCreateUser();
$this->assertEqual($account->language()->getId(), 'de', 'New user is German.');
// Ensure that we can enable basic_auth on a non-english site.
$this->drupalPostForm('admin/modules', ['modules[basic_auth][enable]' => TRUE], t('Install'));
$this->assertResponse(200);
// Assert that the theme CSS was added to the page.
$edit = ['preprocess_css' => FALSE];
$this->drupalPostForm('admin/config/development/performance', $edit, t('Save configuration'));
$this->drupalGet('<front>');
$this->assertRaw('classy/css/components/action-links.css');
// Verify the strings from the translation files were imported.
$test_samples = ['Save and continue', 'Anonymous'];
foreach ($test_samples as $sample) {
$edit = [];
$edit['langcode'] = 'de';
$edit['translation'] = 'translated';
$edit['string'] = $sample;
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Filter'));
$this->assertText($sample . ' de');
}
/** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
$language_manager = \Drupal::languageManager();
// Installed in German, configuration should be in German. No German or
// English overrides should be present.
$config = \Drupal::config('user.settings');
$override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertEqual($config->get('anonymous'), 'Anonymous de');
$this->assertEqual($config->get('langcode'), 'de');
$this->assertTrue($override_de->isNew());
$this->assertTrue($override_en->isNew());
// Assert that adding English makes the English override available.
$edit = ['predefined_langcode' => 'en'];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertFalse($override_en->isNew());
$this->assertEqual($override_en->get('anonymous'), 'Anonymous');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode) {
return <<<ENDPO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider."
msgstr "Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/getting-started/install">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter."
msgid "Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>"
msgstr "<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl %query fehlgeschlagen."
ENDPO;
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\KernelTests\AssertConfigTrait;
/**
* Tests the interactive installer installing the minimal profile.
*
* @group Installer
*/
class MinimalInstallerTest extends ConfigAfterInstallerTestBase {
use AssertConfigTrait;
/**
* {@inheritdoc}
*/
protected $profile = 'minimal';
/**
* Ensures that the exported minimal configuration is up to date.
*/
public function testMinimalConfig() {
$this->assertInstalledConfig([]);
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
/**
* Tests multiple distribution profile support.
*
* @group Installer
*/
class MultipleDistributionsProfileTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// Create two distributions.
foreach (['distribution_one', 'distribution_two'] as $name) {
$info = [
'type' => 'profile',
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => $name . ' profile',
'distribution' => [
'name' => $name,
'install' => [
'theme' => 'bartik',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/' . $name;
mkdir($path, 0777, TRUE);
file_put_contents("$path/$name.info.yml", Yaml::encode($info));
}
// Install the first distribution.
$this->profile = 'distribution_one';
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage() {
// Verify that the distribution name appears.
$this->assertRaw('distribution_one');
// Verify that the requested theme is used.
$this->assertRaw('bartik');
// Verify that the "Choose profile" step does not appear.
$this->assertNoText('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile() {
// This step is skipped, because there is a distribution profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
$this->assertUrl('user/1');
$this->assertResponse(200);
// Confirm that we are logged-in after installation.
$this->assertText($this->rootUser->getUsername());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEqual(\Drupal::installProfile(), 'distribution_one');
$this->assertEqual($this->config('core.extension')->get('profile'), 'distribution_one', 'The install profile has been written to core.extension configuration.');
$this->rebuildContainer();
$this->pass('Container can be rebuilt as distribution is written to configuration.');
$this->assertEqual(\Drupal::installProfile(), 'distribution_one');
}
}

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