.../QuickEditImageEditorTestTrait.php | 76 +++++ .../FunctionalJavascript/QuickEditImageTest.php | 118 +++++--- .../QuickEditIntegrationTest.php | 316 +++++++++++++++++++ .../QuickEditJavaScriptTestBase.php | 334 +++++++++++++++++++++ 4 files changed, 801 insertions(+), 43 deletions(-) diff --git a/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageEditorTestTrait.php b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageEditorTestTrait.php new file mode 100644 index 0000000..7b0d211 --- /dev/null +++ b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageEditorTestTrait.php @@ -0,0 +1,76 @@ +assertJsCondition('document.querySelector(".quickedit-image-field-info") !== null', 10000); + + $quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar'); + $this->assertNotNull($quickedit_entity_toolbar->find('css', 'form.quickedit-image-field-info input[name="alt"]')); + } + + /** + * Simulates typing in the 'image' in-place editor 'alt' attribute text input. + * + * @param string $text + * The text to type. + */ + protected function typeInImageEditorAltTextInput($text) { + $quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar'); + $input = $quickedit_entity_toolbar->find('css', 'form.quickedit-image-field-info input[name="alt"]'); + $input->setValue($text); + } + + /** + * Simulates dragging and dropping an image on the 'image' in-place editor. + * + * @param string $file_uri + * The URI of the image file to drag and drop. + */ + protected function dropImageOnImageEditor($file_uri) { + // Our headless browser can't drag+drop files, but we can mock the event. + // Append a hidden upload element to the DOM. + $script = 'jQuery("").appendTo("body")'; + $this->getSession()->executeScript($script); + + // Find the element, and set its value to our new image. + $input = $this->assertSession()->elementExists('css', '#quickedit-image-test-input'); + $filepath = $this->container->get('file_system')->realpath($file_uri); + $input->attachFile($filepath); + + // Trigger the upload logic with a mock "drop" event. + $script = 'var e = jQuery.Event("drop");' + . 'e.originalEvent = {dataTransfer: {files: jQuery("#quickedit-image-test-input").get(0).files}};' + . 'e.preventDefault = e.stopPropagation = function () {};' + . 'jQuery(".quickedit-image-dropzone").trigger(e);'; + $this->getSession()->executeScript($script); + + // Wait for the dropzone element to be removed (i.e. loading is done). + $js_condition = <<assertJsCondition($js_condition, 20000); + + } + +} \ No newline at end of file diff --git a/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php index 12e43e7..2c43b12 100644 --- a/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php @@ -5,22 +5,23 @@ use Drupal\file\Entity\File; use Drupal\FunctionalJavascriptTests\JavascriptTestBase; use Drupal\Tests\image\Kernel\ImageFieldCreationTrait; +use Drupal\Tests\quickedit\FunctionalJavascript\QuickEditJavaScriptTestBase; use Drupal\Tests\TestFileCreationTrait; /** - * Tests the JavaScript functionality of the "image" in-place editor. - * + * @coversDefaultClass \Drupal\image\Plugin\InPlaceEditor\Image * @group image */ -class QuickEditImageTest extends JavascriptTestBase { +class QuickEditImageTest extends QuickEditJavaScriptTestBase { use ImageFieldCreationTrait; use TestFileCreationTrait; + use QuickEditImageEditorTestTrait; /** * {@inheritdoc} */ - public static $modules = ['node', 'image', 'field_ui', 'contextual', 'quickedit', 'toolbar']; + public static $modules = ['node', 'image', 'field_ui']; /** * A user with permissions to edit Articles and use Quick Edit. @@ -52,9 +53,10 @@ protected function setUp() { } /** - * Tests if an image can be uploaded inline with Quick Edit. + * @covers ::isCompatible + * @covers ::getAttachments */ - public function testUpload() { + public function testImageInPlaceEditor() { // Create a field with a basic filetype restriction. $field_name = strtolower($this->randomMachineName()); $field_settings = [ @@ -114,52 +116,82 @@ public function testUpload() { // Assert that the initial image is present. $this->assertSession()->elementExists('css', $entity_selector . ' ' . $field_selector . ' ' . $original_image_selector); - // Wait until Quick Edit loads. - $condition = "jQuery('" . $entity_selector . " .quickedit').length > 0"; - $this->assertJsCondition($condition, 10000); - - // Initiate Quick Editing. - $this->click('.contextual-toolbar-tab button'); - $this->click($entity_selector . ' [data-contextual-id] > button'); - $this->click($entity_selector . ' [data-contextual-id] .quickedit > a'); - $this->click($field_selector); + // 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_name . '/en/full' => 'inactive', + ]); - // Wait for the field info to load and set new alt text. - $condition = "jQuery('.quickedit-image-field-info').length > 0"; - $this->assertJsCondition($condition, 10000); - $input = $this->assertSession()->elementExists('css', '.quickedit-image-field-info input[name="alt"]'); - $input->setValue('New text'); + // 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_name . '/en/full' => 'candidate', + ]); - // Check that our Dropzone element exists. + // Click the image field. + $this->click($field_selector); + $this->awaitImageEditor(); $this->assertSession()->elementExists('css', $field_selector . ' .quickedit-image-dropzone'); + $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_name . '/en/full' => 'active', + ]); - // Our headless browser can't drag+drop files, but we can mock the event. - // Append a hidden upload element to the DOM. - $script = 'jQuery("").appendTo("body")'; - $this->getSession()->executeScript($script); - - // Find the element, and set its value to our new image. - $input = $this->assertSession()->elementExists('css', '#quickedit-image-test-input'); - $filepath = $this->container->get('file_system')->realpath($valid_images[1]->uri); - $input->attachFile($filepath); - - // Trigger the upload logic with a mock "drop" event. - $script = 'var e = jQuery.Event("drop");' - . 'e.originalEvent = {dataTransfer: {files: jQuery("#quickedit-image-test-input").get(0).files}};' - . 'e.preventDefault = e.stopPropagation = function () {};' - . 'jQuery(".quickedit-image-dropzone").trigger(e);'; - $this->getSession()->executeScript($script); + // Type new 'alt' text. + $this->typeInImageEditorAltTextInput('New text'); + $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_name . '/en/full' => 'changed', + ]); - // Wait for the dropzone element to be removed (i.e. loading is done). - $condition = "jQuery('" . $field_selector . " .quickedit-image-dropzone').length == 0"; - $this->assertJsCondition($condition, 20000); + // Drag and drop an image. + $this->dropImageOnImageEditor($valid_images[1]->uri); // To prevent 403s on save, we re-set our request (cookie) state. $this->prepareRequest(); - // Save the change. - $this->click('.quickedit-button.action-save'); - $this->assertSession()->assertWaitOnAjaxRequest(); + // Click 'Save'. + $this->saveQuickEdit(); + $this->assertEntityInstanceStates([ + 'node/1[0]' => 'committing', + ]); + $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_name . '/en/full' => 'saving', + ]); + $this->assertEntityInstanceFieldMarkup('node', 1, 0, [ + 'node/1/' . $field_name . '/en/full' => '.quickedit-changed', + ]); + + // Wait for the saving of the image field to complete. + $this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'closed'"); + $this->assertEntityInstanceStates([ + 'node/1[0]' => 'closed', + ]); // Re-visit the page to make sure the edit worked. $this->drupalGet('node/' . $node->id()); diff --git a/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditIntegrationTest.php b/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditIntegrationTest.php new file mode 100644 index 0000000..9841503 --- /dev/null +++ b/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditIntegrationTest.php @@ -0,0 +1,316 @@ + 'some_format', + 'name' => 'Some format', + 'weight' => 0, + 'filters' => [ + 'filter_html' => [ + 'status' => 1, + 'settings' => [ + 'allowed_html' => '


', + ], + ], + ], + ])->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. + entity_get_form_display('node', 'article', 'default') + ->setComponent($field_name, [ + 'type' => 'entity_reference_autocomplete_tags', + ]) + ->save(); + entity_get_display('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' => '

Hello world!

I do not know what to say…

I wish I were eloquent.

', + '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', + ]); + + // Click the title field. + $this->click('h1.js-quickedit-page-title > span'); + $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('h1.js-quickedit-page-title > span', $node->label() . ' Llamas are awesome!'); + $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. + $this->click('[data-quickedit-entity-id="node/1"] .field--name-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', + ]); + + // 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 tags field to complete. + $this->awaitEntityInstanceFieldState('node', 1, 0, 'field_tags', 'en', 'candidate'); + + // Click the tags field. + $this->click('.field--name-field-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', + ]); + $this->awaitFormEditor(); + $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->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'. + $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', + ]); + $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(array( + '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(), NULL); + $this->assertEntityInstanceFieldStates('block_content', 1, 0, [ + 'block_content/1/body/en/full' => 'candidate', + ]); + + // Click the body field. + $this->click('[data-quickedit-entity-id="block_content/1"] .field--name-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"]'); + } + +} diff --git a/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditJavaScriptTestBase.php b/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditJavaScriptTestBase.php new file mode 100644 index 0000000..0504c0d --- /dev/null +++ b/core/modules/quickedit/tests/src/FunctionalJavaScript/QuickEditJavaScriptTestBase.php @@ -0,0 +1,334 @@ + '.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)', + '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 $entity_type_id + * The entity type ID. + * @param $entity_id + * The entity ID. + * @param $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 + // allof 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. + + // 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 = <<assertJsCondition($js_condition); + } + + // @todo startViaMouseOver + + /** + * 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 $entity_type_id + * The entity type ID. + * @param $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 string $entity_id + * The entity ID. + * @param string $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. So we use XPath to select only the text node. + $this->assertSame($expected_entity_label, $quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label')->find('xpath', 'text()')->getText()); + if ($expected_field_label !== NULL) { + $this->assertSame($expected_field_label, $quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field')->getText()); + } + else { + $this->assertFalse($quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field')); + } + } + + /** + * Asserts all EntityModels (entity instances) on the page. + * + * @see Drupal.quickedit.EntityModel + * + * @param array $expected_entity_states + * Must describe the expected state of all in-place editable entity + * instances on the page. + */ + protected function assertEntityInstanceStates(array $expected_entity_states) { + $js_get_all_field_states_for_entity = <<assertSame($expected_entity_states, $this->getSession()->evaluateScript($js_get_all_field_states_for_entity)); + } + + /** + * Asserts all FieldModels for the given entity instance. + * + * @param $entity_type_id + * The entity type ID. + * @param $entity_id + * The entity ID. + * @param $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 = <<assertSame($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 $entity_type_id + * The entity type ID. + * @param $entity_id + * The entity ID. + * @param $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 = <<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 $text + * The text to type. + * + * @see \Drupal\quickedit\Plugin\InPlaceEditor\PlainTextEditor + */ + protected function typeInPlainTextEditor($css_selector, $text) { + // @todo Clean this up once PhantomJS is fixed, see http://sticksnglue.com/wordpress/phantomjs-1-9-and-keyboardevent/. + $js_simulate_user_typing = <<getSession()->evaluateScript($js_simulate_user_typing); + } + + /** + * Awaits the 'form' in-place editor. + */ + protected function awaitFormEditor() { + // Assert the "Loading…" popup appears. + $this->assertSession()->elementExists('css', '.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder'); + // Wait for the form to load. + $this->assertJsCondition('document.querySelector(\'.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder\') === null'); + } + + /** + * 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 $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 = << .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); + } + +}