first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,6 @@
name: 'Quick Edit test'
type: module
description: 'Support module for the Quick Edit module tests.'
core: 8.x
package: Testing
version: VERSION
@@ -0,0 +1,33 @@
<?php
/**
* @file
* Helper module for the Quick Edit tests.
*/
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_quickedit_render_field().
*/
function quickedit_test_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) {
$entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
return [
'#prefix' => '<div class="quickedit-test-wrapper">',
'field' => $entity->get($field_name)->view($view_mode_id),
'#suffix' => '</div>',
];
}
/**
* Implements hook_field_formatter_info_alter().
*
* @see quickedit_field_formatter_info_alter()
* @see editor_field_formatter_info_alter()
*/
function quickedit_test_field_formatter_info_alter(&$info) {
// Update \Drupal\text\Plugin\Field\FieldFormatter\TextDefaultFormatter's
// annotation to indicate that it supports the 'wysiwyg' in-place editor
// provided by this module.
$info['text_default']['quickedit'] = ['editor' => 'wysiwyg'];
}
@@ -0,0 +1,10 @@
<?php
namespace Drupal\quickedit_test;
/**
* @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0.
*/
class MockEditEntityFieldAccessCheck extends MockQuickEditEntityFieldAccessCheck {
}
@@ -0,0 +1,20 @@
<?php
namespace Drupal\quickedit_test;
use Drupal\Core\Entity\EntityInterface;
use Drupal\quickedit\Access\QuickEditEntityFieldAccessCheckInterface;
/**
* Access check for in-place editing entity fields.
*/
class MockQuickEditEntityFieldAccessCheck implements QuickEditEntityFieldAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function accessEditEntityField(EntityInterface $entity, $field_name) {
return TRUE;
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\quickedit_test\Plugin\InPlaceEditor;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\quickedit\Plugin\InPlaceEditorBase;
/**
* Defines the 'wysiwyg' in-place editor.
*
* @InPlaceEditor(
* id = "wysiwyg",
* )
*/
class WysiwygEditor extends InPlaceEditorBase {
/**
* {@inheritdoc}
*/
public function isCompatible(FieldItemListInterface $items) {
$field_definition = $items->getFieldDefinition();
// This editor is incompatible with multivalued fields.
if ($field_definition->getFieldStorageDefinition()->getCardinality() != 1) {
return FALSE;
}
// This editor is compatible with formatted ("rich") text fields; but only
// if there is a currently active text format and that text format is the
// 'full_html' text format.
return $items[0]->format === 'full_html';
}
/**
* {@inheritdoc}
*/
public function getMetadata(FieldItemListInterface $items) {
$metadata['format'] = $items[0]->format;
return $metadata;
}
/**
* {@inheritdoc}
*/
public function getAttachments() {
return [
'library' => [
'quickedit_test/not-existing-wysiwyg',
],
];
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\quickedit\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\BrowserTestBase;
/**
* Tests using a custom pipeline with Quick Edit.
*
* @group quickedit
*/
class QuickEditCustomPipelineTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'quickedit',
'quickedit_test',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that Quick Edit works with custom render pipelines.
*/
public function testCustomPipeline() {
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$node = $this->createNode(['type' => 'article']);
$editor_user = $this->drupalCreateUser([
'access content',
'create article content',
'edit any article content',
'access in-place editing',
]);
$this->drupalLogin($editor_user);
$custom_render_url = $this->buildUrl('quickedit/form/node/' . $node->id() . '/body/en/quickedit_test-custom-render-data');
$client = $this->getHttpClient();
$post = ['nocssjs' => 'true'];
$response = $client->post($custom_render_url, [
'body' => http_build_query($post),
'query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'],
'cookies' => $this->getSessionCookies(),
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'http_errors' => FALSE,
]);
$this->assertEquals(200, $response->getStatusCode());
$ajax_commands = Json::decode($response->getBody());
// Request editing to render results with the custom render pipeline.
// Prepare form values for submission. drupalPostAJAX() is not suitable for
// handling pages with JSON responses, so we need our own solution here.
$form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
$this->assertTrue($form_tokens_found, 'Form tokens found in output.');
$post = [
'form_id' => 'quickedit_field_form',
'form_token' => $token_match[1],
'form_build_id' => $build_id_match[1],
'body[0][summary]' => '',
'body[0][value]' => '<p>Fine thanks.</p>',
'body[0][format]' => 'filtered_html',
'op' => t('Save'),
];
// Assume there is another field on this page, which doesn't use a custom
// render pipeline, but the default one, and it uses the "full" view mode.
$post += ['other_view_modes[]' => 'full'];
// Submit field form and check response. Should render with the custom
// render pipeline.
$response = $client->post($custom_render_url, [
'body' => http_build_query($post),
'query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'],
'cookies' => $this->getSessionCookies(),
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'http_errors' => FALSE,
]);
$ajax_commands = Json::decode($response->getBody());
$this->assertCount(1, $ajax_commands, 'The field form HTTP request results in one AJAX command.');
$this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
$this->assertStringContainsString('Fine thanks.', $ajax_commands[0]['data'], 'Form value saved and printed back.');
$this->assertStringContainsString('<div class="quickedit-test-wrapper">', $ajax_commands[0]['data'], 'Custom render pipeline used to render the value.');
$this->assertIdentical(array_keys($ajax_commands[0]['other_view_modes']), ['full'], 'Field was also rendered in the "full" view mode.');
$this->assertStringContainsString('Fine thanks.', $ajax_commands[0]['other_view_modes']['full'], '"full" version of field contains the form value.');
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\quickedit\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\BrowserTestBase;
use GuzzleHttp\RequestOptions;
/**
* Tests accessing the Quick Edit endpoints.
*
* @group quickedit
*/
class QuickEditEndPointAccessTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'quickedit',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
}
/**
* Tests that Quick Edit endpoints are protected from anonymous requests.
*/
public function testEndPointAccess() {
// Quick Edit's JavaScript would never hit these endpoints, but we need to
// make sure that malicious users aren't able to use any of the other
// endpoints either.
$url = $this->buildUrl('/quickedit/attachments');
$post = ['editors[0]' => 'form'];
$this->assertAccessIsBlocked($url, $post);
$node = $this->createNode(['type' => 'article']);
$url = $this->buildUrl('quickedit/form/node/' . $node->id() . '/body/en/full');
$post = ['nocssjs' => 'true'];
$this->assertAccessIsBlocked($url, $post);
$edit = [];
$edit['form_id'] = 'quickedit_field_form';
$edit['form_token'] = 'xIOzMjuc-PULKsRn_KxFn7xzNk5Bx7XKXLfQfw1qOnA';
$edit['form_build_id'] = 'form-kVmovBpyX-SJfTT5kY0pjTV35TV-znor--a64dEnMR8';
$edit['body[0][summary]'] = '';
$edit['body[0][value]'] = '<p>Malicious content.</p>';
$edit['body[0][format]'] = 'filtered_html';
$edit['op'] = t('Save');
$this->assertAccessIsBlocked($url, $edit);
$post = ['nocssjs' => 'true'];
$url = $this->buildUrl('quickedit/entity/node/' . $node->id());
$this->assertAccessIsBlocked($url, $post);
}
/**
* Asserts that access to the passed URL is blocked.
*
* @param string $url
* The URL to check.
* @param array $body
* The payload to send with the request.
*/
protected function assertAccessIsBlocked($url, array $body) {
$client = $this->getHttpClient();
$message = ['message' => "The 'access in-place editing' permission is required."];
$response = $client->post($url, [
RequestOptions::BODY => http_build_query($body),
RequestOptions::QUERY => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'],
RequestOptions::COOKIES => $this->getSessionCookies(),
RequestOptions::HEADERS => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
RequestOptions::HTTP_ERRORS => FALSE,
]);
$this->assertEquals(403, $response->getStatusCode());
$response_message = Json::decode($response->getBody());
$this->assertSame($message, $response_message);
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\Tests\quickedit\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Quick Edit can be installed with Minimal.
*
* @group quickedit
*/
class QuickEditMinimalTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'minimal';
/**
* {@inheritdoc}
*/
protected static $modules = [
'quickedit',
'quickedit_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that Quick Edit works with no admin theme.
*
* @see \quickedit_library_info_alter()
*/
public function testSuccessfulInstall() {
$editor_user = $this->drupalCreateUser([
'access in-place editing',
]);
$this->drupalLogin($editor_user);
$this->assertSame('', $this->config('system.theme')->get('admin'), 'There is no admin theme set on the site.');
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Tests quickedit.
*
* @group quickedit
*/
class FieldTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'ckeditor',
'contextual',
'quickedit',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a text format and associate CKEditor.
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
]);
$filtered_html_format->save();
Editor::create([
'format' => 'filtered_html',
'editor' => 'ckeditor',
])->save();
// Create note type with body field.
$node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
$node_type->save();
node_add_body_field($node_type);
$account = $this->drupalCreateUser([
'access content',
'administer nodes',
'edit any page content',
'use text format filtered_html',
'access contextual links',
'access in-place editing',
]);
$this->drupalLogin($account);
}
/**
* Tests that quickeditor works correctly for field with CKEditor.
*/
public function testFieldWithCkeditor() {
$body_value = '<p>Sapere aude</p>';
$node = Node::create([
'type' => 'page',
'title' => 'Page node',
'body' => [['value' => $body_value, 'format' => 'filtered_html']],
]);
$node->save();
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$this->drupalGet('node/' . $node->id());
// Wait "Quick edit" button for node.
$this->assertSession()->waitForElement('css', '[data-quickedit-entity-id="node/' . $node->id() . '"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $node->id() . '"]', 'Quick edit');
// Switch to body field.
$page->find('css', '[data-quickedit-field-id="node/' . $node->id() . '/body/en/full"]')->click();
// Wait and click by "Blockquote" button from editor for body field.
$this->assertSession()->waitForElementVisible('css', '.cke_button.cke_button__blockquote')->click();
// Wait and click by "Save" button after body field was changed.
$this->assertSession()->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
// Wait until the save occurs and the editor UI disappears.
$this->assertSession()->assertNoElementAfterWait('css', '.cke_button.cke_button__blockquote');
// Ensure that the changes take effect.
$assert->responseMatches("|<blockquote>\s*$body_value\s*</blockquote>|");
}
}
@@ -0,0 +1,209 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
/**
* Tests in-place editing of autocomplete tags.
*
* @group quickedit
*/
class QuickEditAutocompleteTermTest extends WebDriverTestBase {
use EntityReferenceTestTrait;
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'taxonomy',
'quickedit',
'contextual',
'ckeditor',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Stores the node used for the tests.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* Stores the vocabulary used in the tests.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary;
/**
* Stores the first term used in the tests.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term1;
/**
* Stores the second term used in the tests.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term2;
/**
* Stores the field name for the autocomplete field.
*
* @var string
*/
protected $fieldName;
/**
* An user with permissions to access in-place editor.
*
* @var \Drupal\user\UserInterface
*/
protected $editorUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'article',
]);
$this->vocabulary = Vocabulary::create([
'name' => 'quickedit testing tags',
'vid' => 'quickedit_testing_tags',
]);
$this->vocabulary->save();
$this->fieldName = 'field_' . $this->vocabulary->id();
$handler_settings = [
'target_bundles' => [
$this->vocabulary->id() => $this->vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $this->fieldName, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'article')
->setComponent($this->fieldName, [
'type' => 'entity_reference_autocomplete_tags',
'weight' => -4,
])
->save();
\Drupal::service('entity_display.repository')->getViewDisplay('node', 'article')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
'weight' => 10,
])
->save();
\Drupal::service('entity_display.repository')->getViewDisplay('node', 'article', 'teaser')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
'weight' => 10,
])
->save();
$this->term1 = $this->createTerm();
$this->term2 = $this->createTerm();
$node = [];
$node['type'] = 'article';
$node[$this->fieldName][]['target_id'] = $this->term1->id();
$node[$this->fieldName][]['target_id'] = $this->term2->id();
$this->node = $this->drupalCreateNode($node);
$this->editorUser = $this->drupalCreateUser([
'access content',
'create article content',
'edit any article content',
'administer nodes',
'access contextual links',
'access in-place editing',
]);
}
/**
* Tests Quick Edit autocomplete term behavior.
*/
public function testAutocompleteQuickEdit() {
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$this->drupalLogin($this->editorUser);
$this->drupalGet('node/' . $this->node->id());
// Wait "Quick edit" button for node.
$assert->waitForElement('css', '[data-quickedit-entity-id="node/' . $this->node->id() . '"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $this->node->id() . '"]', 'Quick edit');
// Switch to body field.
$page->find('css', '[data-quickedit-field-id="node/' . $this->node->id() . '/' . $this->fieldName . '/' . $this->node->language()->getId() . '/full"]')->click();
// Open Quick Edit.
$quickedit_field_locator = '[name="field_quickedit_testing_tags[target_id]"]';
$tag_field = $assert->waitForElementVisible('css', $quickedit_field_locator);
$tag_field->focus();
$tags = $tag_field->getValue();
// Check existing terms.
$this->assertStringContainsString($this->term1->label(), $tags);
$this->assertStringContainsString($this->term2->label(), $tags);
// Add new term.
$new_tag = $this->randomMachineName();
$tags .= ', ' . $new_tag;
$assert->waitForElementVisible('css', $quickedit_field_locator)->setValue($tags);
$assert->waitOnAutocomplete();
// Wait and click by "Save" button after body field was changed.
$assert->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
$assert->waitOnAutocomplete();
// Reload the page and check new term.
$this->drupalGet('node/' . $this->node->id());
$link = $assert->waitForLink($new_tag);
$this->assertNotEmpty($link);
}
/**
* Returns a new term with random name and description in $this->vocabulary.
*
* @return \Drupal\Core\Entity\EntityInterface|\Drupal\taxonomy\Entity\Term
* The created taxonomy term.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createTerm() {
$filter_formats = filter_formats();
$format = array_pop($filter_formats);
$term = Term::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
// Use the first available text format.
'format' => $format->id(),
'vid' => $this->vocabulary->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$term->save();
return $term;
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* @group quickedit
*/
class QuickEditFileTest extends QuickEditJavascriptTestBase {
use FileFieldCreationTrait;
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'file',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create the Article node type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Add file field to Article node type.
$this->createFileField('field_file', 'node', 'article', ['file_extensions' => 'txt']);
// Log in as a content author who can use Quick Edit and edit Articles.
$user = $this->drupalCreateUser([
'access contextual links',
'access toolbar',
'access in-place editing',
'access content',
'create article content',
'edit any article content',
]);
$this->drupalLogin($user);
}
/**
* Tests if a file can be in-place removed with Quick Edit.
*/
public function testRemove() {
$assert_session = $this->assertSession();
// Create test file.
$this->generateFile('test', 64, 10, 'text');
$file = File::create([
'uri' => 'public://test.txt',
'filename' => 'test.txt',
]);
$file->setPermanent();
$file->save();
// Create test node.
$node = $this->drupalCreateNode([
'type' => 'article',
'title' => t('My Test Node'),
'field_file' => [
'target_id' => $file->id(),
],
]);
$this->drupalGet($node->toUrl()->toString());
// Start Quick Edit.
$this->awaitQuickEditForEntity('node', 1);
$this->startQuickEditViaToolbar('node', 1, 0);
// Click the file field.
$assert_session->waitForElementVisible('css', '[data-quickedit-field-id="node/1/field_file/en/full"]');
$this->click('[data-quickedit-field-id="node/1/field_file/en/full"]');
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="file"]');
// Remove the file.
$remove = $assert_session->waitForButton('Remove');
$remove->click();
// Wait for remove.
$assert_session->waitForElement('css', 'input[name="files[field_file_0]"]');
$this->saveQuickEdit();
// Wait for save.
$this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'closed'");
// Assert file is removed from node.
$assert_session->pageTextNotContains('test.txt');
$node = Node::load($node->id());
$this->assertEmpty($node->get('field_file')->getValue());
}
}
@@ -0,0 +1,348 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
/**
* @group quickedit
*/
class QuickEditIntegrationTest extends QuickEditJavascriptTestBase {
use EntityReferenceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'editor',
'ckeditor',
'taxonomy',
'block',
'block_content',
'hold_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* A user with permissions to edit Articles and use Quick Edit.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAuthorUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create text format, associate CKEditor.
FilterFormat::create([
'format' => 'some_format',
'name' => 'Some format',
'weight' => 0,
'filters' => [
'filter_html' => [
'status' => 1,
'settings' => [
'allowed_html' => '<h2 id> <h3> <h4> <h5> <h6> <p> <br> <strong> <a href hreflang>',
],
],
],
])->save();
Editor::create([
'format' => 'some_format',
'editor' => 'ckeditor',
])->save();
// Create the Article node type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Add "tags" vocabulary + field to the Article node type.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
]);
$vocabulary->save();
$field_name = 'field_' . $vocabulary->id();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $field_name, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
// Add formatter & widget for "tags" field.
\Drupal::entityTypeManager()
->getStorage('entity_form_display')
->load('node.article.default')
->setComponent($field_name, ['type' => 'entity_reference_autocomplete_tags'])
->save();
\Drupal::entityTypeManager()
->getStorage('entity_view_display')
->load('node.article.default')
->setComponent($field_name, ['type' => 'entity_reference_label'])
->save();
$this->drupalPlaceBlock('page_title_block');
$this->drupalPlaceBlock('system_main_block');
// Log in as a content author who can use Quick Edit and edit Articles.
$this->contentAuthorUser = $this->drupalCreateUser([
'access contextual links',
'access toolbar',
'access in-place editing',
'access content',
'create article content',
'edit any article content',
'use text format some_format',
'edit terms in tags',
'administer blocks',
]);
$this->drupalLogin($this->contentAuthorUser);
}
/**
* Tests if an article node can be in-place edited with Quick Edit.
*/
public function testArticleNode() {
$term = Term::create([
'name' => 'foo',
'vid' => 'tags',
]);
$term->save();
$node = $this->drupalCreateNode([
'type' => 'article',
'title' => t('My Test Node'),
'body' => [
'value' => '<p>Hello world!</p><p>I do not know what to say…</p><p>I wish I were eloquent.</p>',
'format' => 'some_format',
],
'field_tags' => [
['target_id' => $term->id()],
],
]);
$this->drupalGet('node/' . $node->id());
// Initial state.
$this->awaitQuickEditForEntity('node', 1);
$this->assertEntityInstanceStates([
'node/1[0]' => 'closed',
]);
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/title/en/full' => 'inactive',
'node/1/uid/en/full' => 'inactive',
'node/1/created/en/full' => 'inactive',
'node/1/body/en/full' => 'inactive',
'node/1/field_tags/en/full' => 'inactive',
]);
// Start in-place editing of the article node.
$this->startQuickEditViaToolbar('node', 1, 0);
$this->assertEntityInstanceStates([
'node/1[0]' => 'opened',
]);
$this->assertQuickEditEntityToolbar((string) $node->label(), NULL);
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/title/en/full' => 'candidate',
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'candidate',
]);
$assert_session = $this->assertSession();
// Click the title field.
$this->click('[data-quickedit-field-id="node/1/title/en/full"].quickedit-candidate');
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="title"]');
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Title');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/title/en/full' => 'active',
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'candidate',
]);
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
'node/1/title/en/full' => '[contenteditable="true"]',
]);
// Append something to the title.
$this->typeInPlainTextEditor('[data-quickedit-field-id="node/1/title/en/full"].quickedit-candidate', ' Llamas are awesome!');
$this->awaitEntityInstanceFieldState('node', 1, 0, 'title', 'en', 'changed');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/title/en/full' => 'changed',
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'candidate',
]);
// Click the body field.
hold_test_response(TRUE);
$this->click('[data-quickedit-entity-id="node/1"] .field--name-body');
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="body"]');
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Body');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/title/en/full' => 'saving',
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'active',
'node/1/field_tags/en/full' => 'candidate',
]);
hold_test_response(FALSE);
// Wait for CKEditor to load, then verify it has.
$this->assertJsCondition('CKEDITOR.status === "loaded"');
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
'node/1/body/en/full' => '.cke_editable_inline',
'node/1/field_tags/en/full' => ':not(.quickedit-editor-is-popup)',
]);
$this->assertSession()->elementExists('css', '#quickedit-entity-toolbar .quickedit-toolgroup.wysiwyg-main > .cke_chrome .cke_top[role="presentation"] .cke_toolbar[role="toolbar"] .cke_toolgroup[role="presentation"] > .cke_button[title~="Bold"][role="button"]');
// Wait for the validating & saving of the title to complete.
$this->awaitEntityInstanceFieldState('node', 1, 0, 'title', 'en', 'candidate');
// Click the tags field.
hold_test_response(TRUE);
$this->click('[data-quickedit-field-id="node/1/field_tags/en/full"]');
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="tags"]');
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Tags');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'activating',
'node/1/title/en/full' => 'candidate',
]);
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
'node/1/title/en/full' => '.quickedit-changed',
'node/1/field_tags/en/full' => '.quickedit-editor-is-popup',
]);
// Assert the "Loading…" popup appears.
$this->assertSession()->elementExists('css', '.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder');
hold_test_response(FALSE);
// Wait for the form to load.
$this->assertJsCondition('document.querySelector(\'.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder\') === null');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'active',
'node/1/title/en/full' => 'candidate',
]);
// Enter an additional tag.
$this->typeInFormEditorTextInputField('field_tags[target_id]', 'foo, bar');
$this->awaitEntityInstanceFieldState('node', 1, 0, 'field_tags', 'en', 'changed');
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'changed',
'node/1/title/en/full' => 'candidate',
]);
// Click 'Save'.
hold_test_response(TRUE);
$this->saveQuickEdit();
$this->assertEntityInstanceStates([
'node/1[0]' => 'committing',
]);
$this->assertEntityInstanceFieldStates('node', 1, 0, [
'node/1/uid/en/full' => 'candidate',
'node/1/created/en/full' => 'candidate',
'node/1/body/en/full' => 'candidate',
'node/1/field_tags/en/full' => 'saving',
'node/1/title/en/full' => 'candidate',
]);
hold_test_response(FALSE);
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
'node/1/title/en/full' => '.quickedit-changed',
'node/1/field_tags/en/full' => '.quickedit-changed',
]);
// Wait for the saving of the tags field to complete.
$this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'closed'");
$this->assertEntityInstanceStates([
'node/1[0]' => 'closed',
]);
}
/**
* Tests if a custom can be in-place edited with Quick Edit.
*/
public function testCustomBlock() {
$block_content_type = BlockContentType::create([
'id' => 'basic',
'label' => 'basic',
'revision' => FALSE,
]);
$block_content_type->save();
block_content_add_body_field($block_content_type->id());
$block_content = BlockContent::create([
'info' => 'Llama',
'type' => 'basic',
'body' => [
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
'format' => 'some_format',
],
]);
$block_content->save();
$this->drupalPlaceBlock('block_content:' . $block_content->uuid(), [
'label' => 'My custom block!',
]);
$this->drupalGet('');
// Initial state.
$this->awaitQuickEditForEntity('block_content', 1);
$this->assertEntityInstanceStates([
'block_content/1[0]' => 'closed',
]);
// Start in-place editing of the article node.
$this->startQuickEditViaToolbar('block_content', 1, 0);
$this->assertEntityInstanceStates([
'block_content/1[0]' => 'opened',
]);
$this->assertQuickEditEntityToolbar((string) $block_content->label(), 'Body');
$this->assertEntityInstanceFieldStates('block_content', 1, 0, [
'block_content/1/body/en/full' => 'highlighted',
]);
// Click the body field.
$this->click('[data-quickedit-entity-id="block_content/1"] .field--name-body');
$assert_session = $this->assertSession();
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="body"]');
$this->assertQuickEditEntityToolbar((string) $block_content->label(), 'Body');
$this->assertEntityInstanceFieldStates('block_content', 1, 0, [
'block_content/1/body/en/full' => 'active',
]);
// Wait for CKEditor to load, then verify it has.
$this->assertJsCondition('CKEDITOR.status === "loaded"');
$this->assertEntityInstanceFieldMarkup('block_content', 1, 0, [
'block_content/1/body/en/full' => '.cke_editable_inline',
]);
$this->assertSession()->elementExists('css', '#quickedit-entity-toolbar .quickedit-toolgroup.wysiwyg-main > .cke_chrome .cke_top[role="presentation"] .cke_toolbar[role="toolbar"] .cke_toolgroup[role="presentation"] > .cke_button[title~="Bold"][role="button"]');
}
}
@@ -0,0 +1,318 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use WebDriver\Key;
/**
* Base class for testing the QuickEdit.
*/
class QuickEditJavascriptTestBase extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['contextual', 'quickedit', 'toolbar'];
/**
* A user with permissions to edit Articles and use Quick Edit.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAuthorUser;
protected static $expectedFieldStateAttributes = [
'inactive' => '.quickedit-field:not(.quickedit-editable):not(.quickedit-candidate):not(.quickedit-highlighted):not(.quickedit-editing):not(.quickedit-changed)',
// A field in 'candidate' state may still have the .quickedit-changed class
// because when its changes were saved to tempstore, it'll still be changed.
// It's just not currently being edited, so that's why it is not in the
// 'changed' state.
'candidate' => '.quickedit-field.quickedit-editable.quickedit-candidate:not(.quickedit-highlighted):not(.quickedit-editing)',
'highlighted' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted:not(.quickedit-editing)',
'activating' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing:not(.quickedit-changed)',
'active' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing:not(.quickedit-changed)',
'changed' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing.quickedit-changed',
'saving' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing.quickedit-changed',
];
/**
* Starts in-place editing of the given entity instance.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
* @param int $entity_instance_id
* The entity instance ID. (Instance on the page.)
*/
protected function startQuickEditViaToolbar($entity_type_id, $entity_id, $entity_instance_id) {
$page = $this->getSession()->getPage();
$toolbar_edit_button_selector = '#toolbar-bar .contextual-toolbar-tab button';
$entity_instance_selector = '[data-quickedit-entity-id="' . $entity_type_id . '/' . $entity_id . '"][data-quickedit-entity-instance-id="' . $entity_instance_id . '"]';
$contextual_links_trigger_selector = '[data-contextual-id] > .trigger';
// Assert the original page state does not have the toolbar's "Edit" button
// pressed/activated, and hence none of the contextual link triggers should
// be visible.
$toolbar_edit_button = $page->find('css', $toolbar_edit_button_selector);
$this->assertSame('false', $toolbar_edit_button->getAttribute('aria-pressed'), 'The "Edit" button in the toolbar is not yet pressed.');
$this->assertFalse($toolbar_edit_button->hasClass('is-active'), 'The "Edit" button in the toolbar is not yet marked as active.');
foreach ($page->findAll('css', $contextual_links_trigger_selector) as $dom_node) {
/** @var \Behat\Mink\Element\NodeElement $dom_node */
$this->assertTrue($dom_node->hasClass('visually-hidden'), 'The contextual links trigger "' . $dom_node->getParent()->getAttribute('data-contextual-id') . '" is hidden.');
}
$this->assertTrue(TRUE, 'All contextual links triggers are hidden.');
// Click the "Edit" button in the toolbar.
$this->click($toolbar_edit_button_selector);
// Assert the toolbar's "Edit" button is now pressed/activated, and hence
// all of the contextual link triggers should be visible.
$this->assertSame('true', $toolbar_edit_button->getAttribute('aria-pressed'), 'The "Edit" button in the toolbar is pressed.');
$this->assertTrue($toolbar_edit_button->hasClass('is-active'), 'The "Edit" button in the toolbar is marked as active.');
foreach ($page->findAll('css', $contextual_links_trigger_selector) as $dom_node) {
/** @var \Behat\Mink\Element\NodeElement $dom_node */
$this->assertFalse($dom_node->hasClass('visually-hidden'), 'The contextual links trigger "' . $dom_node->getParent()->getAttribute('data-contextual-id') . '" is visible.');
}
$this->assertTrue(TRUE, 'All contextual links triggers are visible.');
// @todo Press tab key to verify that tabbing is now contrained to only
// contextual links triggers: https://www.drupal.org/node/2834776
// Assert that the contextual links associated with the entity's contextual
// links trigger are not visible.
/** @var \Behat\Mink\Element\NodeElement $entity_contextual_links_container */
$entity_contextual_links_container = $page->find('css', $entity_instance_selector)
->find('css', $contextual_links_trigger_selector)
->getParent();
$this->assertFalse($entity_contextual_links_container->hasClass('open'));
$this->assertTrue($entity_contextual_links_container->find('css', 'ul.contextual-links')->hasAttribute('hidden'));
// Click the contextual link trigger for the entity we want to Quick Edit.
$this->click($entity_instance_selector . ' ' . $contextual_links_trigger_selector);
$this->assertTrue($entity_contextual_links_container->hasClass('open'));
$this->assertFalse($entity_contextual_links_container->find('css', 'ul.contextual-links')->hasAttribute('hidden'));
// Click the "Quick edit" contextual link.
$this->click($entity_instance_selector . ' [data-contextual-id] ul.contextual-links li.quickedit a');
// Assert the Quick Edit internal state is correct.
$js_condition = <<<JS
Drupal.quickedit.collections.entities.where({isActive: true}).length === 1 && Drupal.quickedit.collections.entities.where({isActive: true})[0].get('entityID') === '$entity_type_id/$entity_id';
JS;
$this->assertJsCondition($js_condition);
}
/**
* Clicks the 'Save' button in the Quick Edit entity toolbar.
*/
protected function saveQuickEdit() {
$quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar');
$save_button = $quickedit_entity_toolbar->find('css', 'button.action-save');
$save_button->press();
$this->assertSame('Saving', $save_button->getText());
}
/**
* Awaits Quick Edit to be initiated for all instances of the given entity.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
*/
protected function awaitQuickEditForEntity($entity_type_id, $entity_id) {
$entity_selector = '[data-quickedit-entity-id="' . $entity_type_id . '/' . $entity_id . '"]';
$condition = "document.querySelectorAll('" . $entity_selector . "').length === document.querySelectorAll('" . $entity_selector . " .quickedit').length";
$this->assertJsCondition($condition, 10000);
}
/**
* Awaits a particular field instance to reach a particular state.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
* @param int $entity_instance_id
* The entity instance ID. (Instance on the page.)
* @param string $field_name
* The field name.
* @param string $langcode
* The language code.
* @param string $awaited_state
* One of the possible field states.
*/
protected function awaitEntityInstanceFieldState($entity_type_id, $entity_id, $entity_instance_id, $field_name, $langcode, $awaited_state) {
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
$logical_field_id = $entity_type_id . '/' . $entity_id . '/' . $field_name . '/' . $langcode;
$this->assertJsCondition("Drupal.quickedit.collections.entities.get('$entity_page_id').get('fields').findWhere({logicalFieldID: '$logical_field_id'}).get('state') === '$awaited_state';");
}
/**
* Asserts the state of the Quick Edit entity toolbar.
*
* @param string $expected_entity_label
* The expected label in the Quick Edit Entity Toolbar.
*/
protected function assertQuickEditEntityToolbar($expected_entity_label, $expected_field_label) {
$quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar');
// We cannot use ->getText() because it also returns the text of all child
// nodes. We also cannot use XPath to select text node in Selenium. So we
// use JS expression to select only the text node.
$this->assertSame($expected_entity_label, $this->getSession()->evaluateScript("return window.jQuery('#quickedit-entity-toolbar .quickedit-toolbar-label').clone().children().remove().end().text();"));
if ($expected_field_label !== NULL) {
$field_label = $quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field');
// Only try to find the text content of the element if it was actually
// found; otherwise use the returned value for assertion. This helps
// us find a more useful stack/error message from testbot instead of the
// trimmed partial exception stack.
if ($field_label) {
$field_label = $field_label->getText();
}
$this->assertSame($expected_field_label, $field_label);
}
else {
$this->assertEmpty($quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field'));
}
}
/**
* Asserts all EntityModels (entity instances) on the page.
*
* @param array $expected_entity_states
* Must describe the expected state of all in-place editable entity
* instances on the page.
*
* @see Drupal.quickedit.EntityModel
*/
protected function assertEntityInstanceStates(array $expected_entity_states) {
$js_get_all_field_states_for_entity = <<<JS
function () {
Drupal.quickedit.collections.entities.reduce(function (result, fieldModel) { result[fieldModel.get('id')] = fieldModel.get('state'); return result; }, {})
var entityCollection = Drupal.quickedit.collections.entities;
return entityCollection.reduce(function (result, entityModel) {
result[entityModel.id] = entityModel.get('state');
return result;
}, {});
}()
JS;
$this->assertSame($expected_entity_states, $this->getSession()->evaluateScript($js_get_all_field_states_for_entity));
}
/**
* Asserts all FieldModels for the given entity instance.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
* @param int $entity_instance_id
* The entity instance ID. (Instance on the page.)
* @param array $expected_field_states
* Must describe the expected state of all in-place editable fields of the
* given entity instance.
*/
protected function assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_states) {
// Get all FieldModel states for the entity instance being asserted. This
// ensures that $expected_field_states must describe the state of all fields
// of the entity instance.
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
$js_get_all_field_states_for_entity = <<<JS
function () {
var entityCollection = Drupal.quickedit.collections.entities;
var entityModel = entityCollection.get('$entity_page_id');
return entityModel.get('fields').reduce(function (result, fieldModel) {
result[fieldModel.get('fieldID')] = fieldModel.get('state');
return result;
}, {});
}()
JS;
$this->assertEquals($expected_field_states, $this->getSession()->evaluateScript($js_get_all_field_states_for_entity));
// Assert that those fields also have the appropriate DOM decorations.
$expected_field_attributes = [];
foreach ($expected_field_states as $quickedit_field_id => $expected_field_state) {
$expected_field_attributes[$quickedit_field_id] = static::$expectedFieldStateAttributes[$expected_field_state];
}
$this->assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, $expected_field_attributes);
}
/**
* Asserts all in-place editable fields with markup expectations.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
* @param int $entity_instance_id
* The entity instance ID. (Instance on the page.)
* @param array $expected_field_attributes
* Must describe the expected markup attributes for all given in-place
* editable fields.
*/
protected function assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_attributes) {
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
$expected_field_attributes_json = json_encode($expected_field_attributes);
$js_match_field_element_attributes = <<<JS
function () {
var expectations = $expected_field_attributes_json;
var entityCollection = Drupal.quickedit.collections.entities;
var entityModel = entityCollection.get('$entity_page_id');
return entityModel.get('fields').reduce(function (result, fieldModel) {
var fieldID = fieldModel.get('fieldID');
var element = fieldModel.get('el');
var matches = element.webkitMatchesSelector(expectations[fieldID]);
result[fieldID] = matches ? matches : element.outerHTML;
return result;
}, {});
}()
JS;
$result = $this->getSession()->evaluateScript($js_match_field_element_attributes);
foreach ($expected_field_attributes as $quickedit_field_id => $expectation) {
$this->assertSame(TRUE, $result[$quickedit_field_id], 'Field ' . $quickedit_field_id . ' did not match its expectation selector (' . $expectation . '), actual HTML: ' . $result[$quickedit_field_id]);
}
}
/**
* Simulates typing in a 'plain_text' in-place editor.
*
* @param string $css_selector
* The CSS selector to find the DOM element (with the 'contenteditable=true'
* attribute set), to type in.
* @param string $text
* The text to type.
*
* @see \Drupal\quickedit\Plugin\InPlaceEditor\PlainTextEditor
*/
protected function typeInPlainTextEditor($css_selector, $text) {
$field = $this->getSession()->getPage()->find('css', $css_selector);
$field->setValue(Key::END . $text);
}
/**
* Simulates typing in an input[type=text] inside a 'form' in-place editor.
*
* @param string $input_name
* The "name" attribute of the input[type=text] to type in.
* @param string $text
* The text to type.
*
* @see \Drupal\quickedit\Plugin\InPlaceEditor\FormEditor
*/
protected function typeInFormEditorTextInputField($input_name, $text) {
$input = $this->cssSelect('.quickedit-form-container > .quickedit-form[role="dialog"] form.quickedit-field-form input[type=text][name="' . $input_name . '"]')[0];
$input->setValue($text);
$js_simulate_user_typing = <<<JS
function () {
var el = document.querySelector('.quickedit-form-container > .quickedit-form[role="dialog"] form.quickedit-field-form input[name="$input_name"]');
window.jQuery(el).trigger('formUpdated');
}()
JS;
$this->getSession()->evaluateScript($js_simulate_user_typing);
}
}
@@ -0,0 +1,397 @@
<?php
namespace Drupal\Tests\quickedit\FunctionalJavascript;
use Behat\Mink\Session;
use Drupal\block_content\Entity\BlockContent;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests loading of in-place editing functionality and lazy loading of its
* in-place editors.
*
* @group quickedit
*/
class QuickEditLoadingTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'contextual',
'quickedit',
'filter',
'node',
'image',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* An user with permissions to create and edit articles.
*
* @var \Drupal\user\UserInterface
*/
protected $authorUser;
/**
* A test node.
*
* @var \Drupal\node\NodeInterface
*/
protected $testNode;
/**
* A author user with permissions to access in-place editor.
*
* @var \Drupal\user\UserInterface
*/
protected $editorUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a text format.
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => [],
]);
$filtered_html_format->save();
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
// Set the node type to initially not have revisions.
// Testing with revisions will be done later.
$node_type = NodeType::load('article');
$node_type->setNewRevision(FALSE);
$node_type->save();
// Create one node of the above node type using the above text format.
$this->testNode = $this->drupalCreateNode([
'type' => 'article',
'body' => [
0 => [
'value' => '<p>How are you?</p>',
'format' => 'filtered_html',
],
],
'revision_log' => $this->randomString(),
]);
// Create 2 users, the only difference being the ability to use in-place
// editing
$basic_permissions = [
'access content',
'create article content',
'edit any article content',
'use text format filtered_html',
'access contextual links',
];
$this->authorUser = $this->drupalCreateUser($basic_permissions);
$this->editorUser = $this->drupalCreateUser(array_merge($basic_permissions, ['access in-place editing']));
}
/**
* Test the loading of Quick Edit with different permissions.
*/
public function testUserPermissions() {
$assert = $this->assertSession();
$this->drupalLogin($this->authorUser);
$this->drupalGet('node/1');
// Library and in-place editors.
$this->assertNoRaw('core/modules/quickedit/js/quickedit.js', 'Quick Edit library not loaded.');
$this->assertNoRaw('core/modules/quickedit/js/editors/formEditor.js', "'form' in-place editor not loaded.");
// HTML annotation and title class does not exist for users without
// permission to in-place edit.
$this->assertNoRaw('data-quickedit-entity-id="node/1"');
$this->assertNoRaw('data-quickedit-field-id="node/1/body/en/full"');
$this->assertNoFieldByXPath('//h1[contains(@class, "js-quickedit-page-title")]');
$assert->linkNotExists('Quick edit');
// Tests the loading of Quick Edit when a user does have access to it.
// Also ensures lazy loading of in-place editors works.
$nid = $this->testNode->id();
// There should be only one revision so far.
$node = Node::load($nid);
$vids = \Drupal::entityTypeManager()->getStorage('node')->revisionIds($node);
$this->assertCount(1, $vids, 'The node has only one revision.');
$original_log = $node->revision_log->value;
$this->drupalLogin($this->editorUser);
$this->drupalGet('node/' . $nid);
$page = $this->getSession()->getPage();
// Wait "Quick edit" button for node.
$assert->waitForElement('css', '[data-quickedit-entity-id="node/' . $nid . '"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $nid . '"]', 'Quick edit');
// Switch to body field.
$page->find('css', '[data-quickedit-field-id="node/' . $nid . '/body/en/full"]')->click();
$assert->assertWaitOnAjaxRequest();
// Wait and update body field.
$body_field_locator = '[name="body[0][value]"]';
$body_text = 'Fine thanks.';
$assert->waitForElementVisible('css', $body_field_locator)->setValue('<p>' . $body_text . '</p>');
// Wait and click by "Save" button after body field was changed.
$assert->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
$assert->assertWaitOnAjaxRequest();
$node = Node::load($nid);
$vids = \Drupal::entityTypeManager()->getStorage('node')->revisionIds($node);
$this->assertCount(1, $vids, 'The node has only one revision.');
$this->assertSame($original_log, $node->revision_log->value, 'The revision log message is unchanged.');
// Ensure that the changes take effect.
$assert->responseMatches("|\s*$body_text\s*|");
// Reload the page and check for updated body.
$this->drupalGet('node/' . $nid);
$assert->pageTextContains($body_text);
}
/**
* Test Quick Edit does not appear for entities with pending revisions.
*/
public function testWithPendingRevision() {
$this->drupalLogin($this->editorUser);
// Verify that the preview is loaded correctly.
$this->drupalPostForm('node/add/article', ['title[0][value]' => 'foo'], 'Preview');
// Verify that quickedit is not active on preview.
$this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
$this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
$this->drupalGet('node/' . $this->testNode->id());
$this->assertRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
$this->assertRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
// Wait for the page to completely load before making any changes to the
// node. This allows Quick Edit to fetch the metadata without causing
// database locks on SQLite.
$this->assertSession()->assertWaitOnAjaxRequest();
$this->testNode->title = 'Updated node';
$this->testNode->setNewRevision(TRUE);
$this->testNode->isDefaultRevision(FALSE);
$this->testNode->save();
$this->drupalGet('node/' . $this->testNode->id());
$this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
$this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
}
/**
* Tests the loading of Quick Edit for the title base field.
*/
public function testTitleBaseField() {
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
$nid = $this->testNode->id();
$this->drupalLogin($this->editorUser);
$this->drupalGet('node/' . $nid);
// Wait "Quick edit" button for node.
$assert->waitForElement('css', '[data-quickedit-entity-id="node/' . $nid . '"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $nid . '"]', 'Quick edit');
// Switch to title field.
$page->find('css', '[data-quickedit-field-id="node/' . $nid . '/title/en/full"]')->click();
$assert->assertWaitOnAjaxRequest();
// Wait and update title field.
$field_locator = '.field--name-title';
$text_new = 'Obligatory question';
$assert->waitForElementVisible('css', $field_locator)->setValue($text_new);
// Wait and click by "Save" button after title field was changed.
$this->assertSession()->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
$assert->assertWaitOnAjaxRequest();
// Ensure that the changes take effect.
$assert->responseMatches("|\s*$text_new\s*|");
// Reload the page and check for updated title.
$this->drupalGet('node/' . $nid);
$assert->pageTextContains($text_new);
}
/**
* Tests that Quick Edit doesn't make fields rendered with display options
* editable.
*/
public function testDisplayOptions() {
$node = Node::load('1');
$display_settings = [
'label' => 'inline',
];
$build = $node->body->view($display_settings);
$output = \Drupal::service('renderer')->renderRoot($build);
$this->assertStringNotContainsString('data-quickedit-field-id', $output, 'data-quickedit-field-id attribute not added when rendering field using dynamic display options.');
}
/**
* Tests Quick Edit on a node that was concurrently edited on the full node
* form.
*/
public function testConcurrentEdit() {
$nid = $this->testNode->id();
$this->drupalLogin($this->authorUser);
// Open the edit page in the default session.
$this->drupalGet('node/' . $nid . '/edit');
// Switch to a concurrent session and save a quick edit change.
// We need to do some bookkeeping to keep track of the logged in user.
$logged_in_user = $this->loggedInUser;
$this->loggedInUser = FALSE;
// Register a session to preform concurrent editing.
$driver = $this->getDefaultDriverInstance();
$session = new Session($driver);
$this->mink->registerSession('concurrent', $session);
$this->mink->setDefaultSessionName('concurrent');
$this->initFrontPage();
$this->drupalLogin($this->editorUser);
$this->drupalGet('node/' . $nid);
$assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Wait "Quick edit" button for node.
$assert->waitForElement('css', '[data-quickedit-entity-id="node/' . $nid . '"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $nid . '"]', 'Quick edit');
// Switch to body field.
$page->find('css', '[data-quickedit-field-id="node/' . $nid . '/body/en/full"]')->click();
$assert->assertWaitOnAjaxRequest();
// Wait and update body field.
$body_field_locator = '[name="body[0][value]"]';
$body_text = 'Fine thanks.';
$assert->waitForElementVisible('css', $body_field_locator)->setValue('<p>' . $body_text . '</p>');
// Wait and click by "Save" button after body field was changed.
$assert->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
$assert->assertWaitOnAjaxRequest();
// Ensure that the changes take effect.
$assert->responseMatches("|\s*$body_text\s*|");
// Switch back to the default session.
$this->mink->setDefaultSessionName('default');
$this->loggedInUser = $logged_in_user;
// Ensure different save timestamps for field editing.
sleep(2);
$this->drupalPostForm(NULL, ['body[0][value]' => '<p>Concurrent edit!</p>'], 'Save');
$this->getSession()->getPage()->hasContent('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
}
/**
* Tests that Quick Edit's data- attributes are present for content blocks.
*/
public function testContentBlock() {
\Drupal::service('module_installer')->install(['block_content']);
// Create and place a content_block block.
$block = BlockContent::create([
'info' => $this->randomMachineName(),
'type' => 'basic',
'langcode' => 'en',
]);
$block->save();
$this->drupalPlaceBlock('block_content:' . $block->uuid());
// Check that the data- attribute is present.
$this->drupalLogin($this->editorUser);
$this->drupalGet('');
$this->assertRaw('data-quickedit-entity-id="block_content/1"');
}
/**
* Tests that Quick Edit can handle an image field.
*/
public function testImageField() {
$page = $this->getSession()->getPage();
$assert = $this->assertSession();
// Add an image field to the content type.
FieldStorageConfig::create([
'field_name' => 'field_image',
'type' => 'image',
'entity_type' => 'node',
])->save();
FieldConfig::create([
'field_name' => 'field_image',
'field_type' => 'image',
'label' => t('Image'),
'entity_type' => 'node',
'bundle' => 'article',
])->save();
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'article', 'default')
->setComponent('field_image', [
'type' => 'image_image',
])
->save();
$display = EntityViewDisplay::load('node.article.default');
$display->setComponent('field_image', [
'type' => 'image',
])->save();
// Add an image to the node.
$this->drupalLogin($this->editorUser);
$this->drupalGet('node/1/edit');
$image = $this->drupalGetTestFiles('image')[0];
$image_path = $this->container->get('file_system')->realpath($image->uri);
$page->attachFileToField('files[field_image_0]', $image_path);
$alt_field = $assert->waitForField('field_image[0][alt]');
$this->assertNotEmpty($alt_field);
$this->drupalPostForm(NULL, [
'field_image[0][alt]' => 'Vivamus aliquet elit',
], t('Save'));
// The image field form should load normally.
// Wait "Quick edit" button for node.
$assert->waitForElement('css', '[data-quickedit-entity-id="node/1"] .contextual .quickedit');
// Click by "Quick edit".
$this->clickContextualLink('[data-quickedit-entity-id="node/1"]', 'Quick edit');
// Switch to body field.
$assert->waitForElement('css', '[data-quickedit-field-id="node/1/field_image/en/full"]')->click();
$assert->assertWaitOnAjaxRequest();
$field_locator = '.field--name-field-image';
$assert->waitForElementVisible('css', $field_locator);
}
}
@@ -0,0 +1,157 @@
<?php
namespace Drupal\Tests\quickedit\Kernel;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\quickedit\EditorSelector;
/**
* Tests in-place field editor selection.
*
* @group quickedit
*/
class EditorSelectionTest extends QuickEditTestBase {
/**
* The manager for editor plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorManager;
/**
* The editor selector object to be tested.
*
* @var \Drupal\quickedit\EditorSelectorInterface
*/
protected $editorSelector;
protected function setUp() {
parent::setUp();
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->editorSelector = new EditorSelector($this->editorManager, $this->container->get('plugin.manager.field.formatter'));
}
/**
* Returns the in-place editor that Quick Edit selects.
*/
protected function getSelectedEditor($entity_id, $field_name, $view_mode = 'default') {
$storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
$storage->resetCache([$entity_id]);
$entity = $storage->load($entity_id);
$items = $entity->get($field_name);
$options = \Drupal::service('entity_display.repository')
->getViewDisplay('entity_test', 'entity_test', $view_mode)
->getComponent($field_name);
return $this->editorSelector->getEditor($options['type'], $items);
}
/**
* Tests a string (plain text) field, with cardinality 1 and >1.
*/
public function testText() {
$field_name = 'field_text';
$this->createFieldWithStorage(
$field_name, 'string', 1, 'Simple text field',
// Instance settings.
[],
// Widget type & settings.
'string_textfield',
['size' => 42],
// 'default' formatter type & settings.
'string',
[]
);
// Create an entity with values for this text field.
$entity = EntityTest::create();
$entity->{$field_name}->value = 'Hello, world!';
$entity->save();
// With cardinality 1.
$this->assertEqual('plain_text', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality 1, the 'plain_text' editor is selected.");
// With cardinality >1
$this->fields->field_text_field_storage->setCardinality(2);
$this->fields->field_text_field_storage->save();
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality >1, the 'form' editor is selected.");
}
/**
* Tests a textual field, with text filtering, with cardinality 1 and >1,
* always with an Editor plugin present that supports textual fields with text
* filtering, but with varying text format compatibility.
*/
public function testTextWysiwyg() {
// Enable edit_test module so that the 'wysiwyg' editor becomes available.
$this->enableModules(['quickedit_test']);
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->editorSelector = new EditorSelector($this->editorManager, $this->container->get('plugin.manager.field.formatter'));
$field_name = 'field_textarea';
$this->createFieldWithStorage(
$field_name, 'text', 1, 'Long text field',
// Instance settings.
[],
// Widget type & settings.
'text_textarea',
['size' => 42],
// 'default' formatter type & settings.
'text_default',
[]
);
// Create an entity with values for this text field.
$entity = EntityTest::create();
$entity->{$field_name}->value = 'Hello, world!';
$entity->{$field_name}->format = 'filtered_html';
$entity->save();
// Editor selection w/ cardinality 1, text format w/o associated text editor.
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
// Editor selection w/ cardinality 1, text format w/ associated text editor.
$entity->{$field_name}->format = 'full_html';
$entity->save();
$this->assertEqual('wysiwyg', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality 1, and the full_html text format, the 'wysiwyg' editor is selected.");
// Editor selection with text field, cardinality >1.
$this->fields->field_textarea_field_storage->setCardinality(2);
$this->fields->field_textarea_field_storage->save();
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
}
/**
* Tests a number field, with cardinality 1 and >1.
*/
public function testNumber() {
$field_name = 'field_nr';
$this->createFieldWithStorage(
$field_name, 'integer', 1, 'Simple number field',
// Instance settings.
[],
// Widget type & settings.
'number',
[],
// 'default' formatter type & settings.
'number_integer',
[]
);
// Create an entity with values for this text field.
$entity = EntityTest::create();
$entity->{$field_name}->value = 42;
$entity->save();
// Editor selection with cardinality 1.
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality 1, the 'form' editor is selected.");
// Editor selection with cardinality >1.
$this->fields->field_nr_field_storage->setCardinality(2);
$this->fields->field_nr_field_storage->save();
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $field_name), "With cardinality >1, the 'form' editor is selected.");
}
}
@@ -0,0 +1,178 @@
<?php
namespace Drupal\Tests\quickedit\Kernel;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\quickedit\EditorSelector;
use Drupal\quickedit\MetadataGenerator;
use Drupal\quickedit_test\MockQuickEditEntityFieldAccessCheck;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests in-place field editing metadata.
*
* @group quickedit
*/
class MetadataGeneratorTest extends QuickEditTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['quickedit_test'];
/**
* The manager for editor plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorManager;
/**
* The metadata generator object to be tested.
*
* @var \Drupal\quickedit\MetadataGeneratorInterface
*/
protected $metadataGenerator;
/**
* The editor selector object to be used by the metadata generator object.
*
* @var \Drupal\quickedit\EditorSelectorInterface
*/
protected $editorSelector;
/**
* The access checker object to be used by the metadata generator object.
*
* @var \Drupal\quickedit\Access\QuickEditEntityFieldAccessCheckInterface
*/
protected $accessChecker;
protected function setUp() {
parent::setUp();
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->accessChecker = new MockQuickEditEntityFieldAccessCheck();
$this->editorSelector = new EditorSelector($this->editorManager, $this->container->get('plugin.manager.field.formatter'));
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
}
/**
* Tests a simple entity type, with two different simple fields.
*/
public function testSimpleEntityType() {
$field_1_name = 'field_text';
$field_1_label = 'Plain text field';
$this->createFieldWithStorage(
$field_1_name, 'string', 1, $field_1_label,
// Instance settings.
[],
// Widget type & settings.
'string_textfield',
['size' => 42],
// 'default' formatter type & settings.
'string',
[]
);
$field_2_name = 'field_nr';
$field_2_label = 'Simple number field';
$this->createFieldWithStorage(
$field_2_name, 'integer', 1, $field_2_label,
// Instance settings.
[],
// Widget type & settings.
'number',
[],
// 'default' formatter type & settings.
'number_integer',
[]
);
// Create an entity with values for this text field.
$entity = EntityTest::create();
$entity->{$field_1_name}->value = 'Test';
$entity->{$field_2_name}->value = 42;
$entity->save();
$entity = EntityTest::load($entity->id());
// Verify metadata for field 1.
$items_1 = $entity->get($field_1_name);
$metadata_1 = $this->metadataGenerator->generateFieldMetadata($items_1, 'default');
$expected_1 = [
'access' => TRUE,
'label' => 'Plain text field',
'editor' => 'plain_text',
];
$this->assertEqual($expected_1, $metadata_1, 'The correct metadata is generated for the first field.');
// Verify metadata for field 2.
$items_2 = $entity->get($field_2_name);
$metadata_2 = $this->metadataGenerator->generateFieldMetadata($items_2, 'default');
$expected_2 = [
'access' => TRUE,
'label' => 'Simple number field',
'editor' => 'form',
];
$this->assertEqual($expected_2, $metadata_2, 'The correct metadata is generated for the second field.');
}
/**
* Tests a field whose associated in-place editor generates custom metadata.
*/
public function testEditorWithCustomMetadata() {
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->editorSelector = new EditorSelector($this->editorManager, $this->container->get('plugin.manager.field.formatter'));
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->editorSelector = new EditorSelector($this->editorManager, $this->container->get('plugin.manager.field.formatter'));
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
// Create a rich text field.
$field_name = 'field_rich';
$field_label = 'Rich text field';
$this->createFieldWithStorage(
$field_name, 'text', 1, $field_label,
// Instance settings.
[],
// Widget type & settings.
'text_textfield',
['size' => 42],
// 'default' formatter type & settings.
'text_default',
[]
);
// Create a text format.
$full_html_format = FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => [
'filter_htmlcorrector' => ['status' => 1],
],
]);
$full_html_format->save();
// Create an entity with values for this rich text field.
$entity = EntityTest::create();
$entity->{$field_name}->value = 'Test';
$entity->{$field_name}->format = 'full_html';
$entity->save();
$entity = EntityTest::load($entity->id());
// Verify metadata.
$items = $entity->get($field_name);
$metadata = $this->metadataGenerator->generateFieldMetadata($items, 'default');
$expected = [
'access' => TRUE,
'label' => 'Rich text field',
'editor' => 'wysiwyg',
'custom' => [
'format' => 'full_html',
],
];
$this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
}
}
@@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\quickedit\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests loading of in-place editing functionality and lazy loading of its
* in-place editors.
*
* @group quickedit
*/
class QuickEditLoadingTest extends KernelTestBase {
use NodeCreationTrait;
use UserCreationTrait;
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'field',
'node',
'text',
'filter',
'contextual',
'quickedit',
];
/**
* A user with permissions to access in-place editor.
*
* @var \Drupal\user\UserInterface
*/
protected $editorUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'sequences');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installConfig(['field', 'filter', 'node']);
// Create a Content type and one test node.
$this->createContentType(['type' => 'page']);
$this->createNode();
$this->editorUser = $this->createUser([
'access content',
'create page content',
'edit any page content',
'access contextual links',
'access in-place editing',
]);
}
/**
* Tests that Quick Edit doesn't make fields rendered with display options
* editable.
*/
public function testDisplayOptions() {
$node = Node::load(1);
$renderer = $this->container->get('renderer');
$this->container->get('current_user')->setAccount($this->editorUser);
$build = $node->body->view(['label' => 'inline']);
$this->setRawContent($renderer->renderRoot($build));
$elements = $this->xpath('//div[@data-quickedit-field-id]');
$this->assertFalse(!empty($elements), 'data-quickedit-field-id attribute not added when rendering field using dynamic display options.');
$build = $node->body->view('default');
$this->setRawContent($renderer->renderRoot($build));
$elements = $this->xpath('//div[@data-quickedit-field-id="node/1/body/en/default"]');
$this->assertTrue(!empty($elements), 'Body with data-quickedit-field-id attribute found.');
}
}
@@ -0,0 +1,117 @@
<?php
namespace Drupal\Tests\quickedit\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Base class for testing Quick Edit functionality.
*/
abstract class QuickEditTestBase extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'system',
'entity_test',
'field',
'field_test',
'filter',
'user',
'text',
'quickedit',
];
/**
* Bag of created fields.
*
* Allows easy access to test field names/IDs/objects via:
* - $this->fields->{$field_name}_field_storage
* - $this->fields->{$field_name}_instance
*
* @see \Drupal\quickedit\Tests\QuickEditTestBase::createFieldWithStorage()
*
* @var \ArrayObject
*/
protected $fields;
/**
* Sets the default field storage backend for fields created during tests.
*/
protected function setUp() {
parent::setUp();
$this->fields = new \ArrayObject([], \ArrayObject::ARRAY_AS_PROPS);
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
$this->installConfig(['field', 'filter']);
}
/**
* Creates a field.
*
* @param string $field_name
* The field name.
* @param string $type
* The field type.
* @param int $cardinality
* The field's cardinality.
* @param string $label
* The field's label (used everywhere: widget label, formatter label).
* @param array $field_settings
* @param string $widget_type
* The widget type.
* @param array $widget_settings
* The widget settings.
* @param string $formatter_type
* The formatter type.
* @param array $formatter_settings
* The formatter settings.
*/
protected function createFieldWithStorage($field_name, $type, $cardinality, $label, $field_settings, $widget_type, $widget_settings, $formatter_type, $formatter_settings) {
$field_storage = $field_name . '_field_storage';
$this->fields->$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => $type,
'cardinality' => $cardinality,
]);
$this->fields->$field_storage->save();
$field = $field_name . '_field';
$this->fields->$field = FieldConfig::create([
'field_storage' => $this->fields->$field_storage,
'bundle' => 'entity_test',
'label' => $label,
'description' => $label,
'weight' => mt_rand(0, 127),
'settings' => $field_settings,
]);
$this->fields->$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, [
'type' => $widget_type,
'settings' => $widget_settings,
])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test')
->setComponent($field_name, [
'label' => 'above',
'type' => $formatter_type,
'settings' => $formatter_settings,
])
->save();
}
}
@@ -0,0 +1,149 @@
<?php
namespace Drupal\Tests\quickedit\Unit\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\DependencyInjection\Container;
use Drupal\quickedit\Access\QuickEditEntityFieldAccessCheck;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Language\LanguageInterface;
/**
* @coversDefaultClass \Drupal\quickedit\Access\QuickEditEntityFieldAccessCheck
* @group Access
* @group quickedit
*/
class QuickEditEntityFieldAccessCheckTest extends UnitTestCase {
/**
* The tested access checker.
*
* @var \Drupal\quickedit\Access\QuickEditEntityFieldAccessCheck
*/
protected $editAccessCheck;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->editAccessCheck = new QuickEditEntityFieldAccessCheck();
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
$cache_contexts_manager->reveal();
$container = new Container();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
}
/**
* Provides test data for testAccess().
*
* @see \Drupal\Tests\edit\Unit\quickedit\Access\QuickEditEntityFieldAccessCheckTest::testAccess()
*/
public function providerTestAccess() {
$data = [];
$data[] = [TRUE, TRUE, AccessResult::allowed()];
$data[] = [FALSE, TRUE, AccessResult::neutral()];
$data[] = [TRUE, FALSE, AccessResult::neutral()];
$data[] = [FALSE, FALSE, AccessResult::neutral()];
return $data;
}
/**
* Tests the method for checking access to routes.
*
* @param bool $entity_is_editable
* Whether the subject entity is editable.
* @param bool $field_storage_is_accessible
* Whether the user has access to the field storage entity.
* @param \Drupal\Core\Access\AccessResult $expected_result
* The expected result of the access call.
*
* @dataProvider providerTestAccess
*/
public function testAccess($entity_is_editable, $field_storage_is_accessible, AccessResult $expected_result) {
$entity = $this->createMockEntity();
$entity->expects($this->any())
->method('access')
->willReturn(AccessResult::allowedIf($entity_is_editable)->cachePerPermissions());
$field_storage = $this->createMock('Drupal\field\FieldStorageConfigInterface');
$field_storage->expects($this->any())
->method('access')
->willReturn(AccessResult::allowedIf($field_storage_is_accessible));
$expected_result->cachePerPermissions();
$field_name = 'valid';
$entity_with_field = clone $entity;
$entity_with_field->expects($this->any())
->method('get')
->with($field_name)
->will($this->returnValue($field_storage));
$entity_with_field->expects($this->once())
->method('hasTranslation')
->with(LanguageInterface::LANGCODE_NOT_SPECIFIED)
->will($this->returnValue(TRUE));
$account = $this->createMock('Drupal\Core\Session\AccountInterface');
$access = $this->editAccessCheck->access($entity_with_field, $field_name, LanguageInterface::LANGCODE_NOT_SPECIFIED, $account);
$this->assertEquals($expected_result, $access);
}
/**
* Tests checking access to routes that result in AccessResult::isForbidden().
*
* @dataProvider providerTestAccessForbidden
*/
public function testAccessForbidden($field_name, $langcode) {
$account = $this->createMock('Drupal\Core\Session\AccountInterface');
$entity = $this->createMockEntity();
$this->assertEquals(AccessResult::forbidden(), $this->editAccessCheck->access($entity, $field_name, $langcode, $account));
}
/**
* Provides test data for testAccessForbidden.
*/
public function providerTestAccessForbidden() {
$data = [];
// Tests the access method without a field_name.
$data[] = [NULL, LanguageInterface::LANGCODE_NOT_SPECIFIED];
// Tests the access method with a non-existent field.
$data[] = ['not_valid', LanguageInterface::LANGCODE_NOT_SPECIFIED];
// Tests the access method without a langcode.
$data[] = ['valid', NULL];
// Tests the access method with an invalid langcode.
$data[] = ['valid', 'xx-lolspeak'];
return $data;
}
/**
* Returns a mock entity.
*
* @return \Drupal\Core\Entity\EntityInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected function createMockEntity() {
$entity = $this->getMockBuilder('Drupal\entity_test\Entity\EntityTest')
->disableOriginalConstructor()
->getMock();
$entity->expects($this->any())
->method('hasTranslation')
->will($this->returnValueMap([
[LanguageInterface::LANGCODE_NOT_SPECIFIED, TRUE],
['xx-lolspeak', FALSE],
]));
$entity->expects($this->any())
->method('hasField')
->will($this->returnValueMap([
['valid', TRUE],
['not_valid', FALSE],
]));
return $entity;
}
}