61af1f52b632dd3f59b9c21ecc5026a5c4fe638a
[yaffs-website] / web / core / modules / quickedit / src / Tests / QuickEditLoadingTest.php
1 <?php
2
3 namespace Drupal\quickedit\Tests;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\block_content\Entity\BlockContent;
7 use Drupal\field\Entity\FieldConfig;
8 use Drupal\field\Entity\FieldStorageConfig;
9 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
10 use Drupal\Core\Url;
11 use Drupal\node\Entity\Node;
12 use Drupal\node\Entity\NodeType;
13 use Drupal\simpletest\WebTestBase;
14 use Drupal\filter\Entity\FilterFormat;
15
16 /**
17  * Tests loading of in-place editing functionality and lazy loading of its
18  * in-place editors.
19  *
20  * @group quickedit
21  */
22 class QuickEditLoadingTest extends WebTestBase {
23
24   /**
25    * Modules to enable.
26    *
27    * @var array
28    */
29   public static $modules = [
30     'contextual',
31     'quickedit',
32     'filter',
33     'node',
34     'image',
35   ];
36
37   /**
38    * An user with permissions to create and edit articles.
39    *
40    * @var \Drupal\user\UserInterface
41    */
42   protected $authorUser;
43
44   /**
45    * A test node.
46    *
47    * @var \Drupal\node\NodeInterface
48    */
49   protected $testNode;
50
51   /**
52    * A author user with permissions to access in-place editor.
53    *
54    * @var \Drupal\user\UserInterface
55    */
56   protected $editorUser;
57
58   protected function setUp() {
59     parent::setUp();
60
61     // Create a text format.
62     $filtered_html_format = FilterFormat::create([
63       'format' => 'filtered_html',
64       'name' => 'Filtered HTML',
65       'weight' => 0,
66       'filters' => [],
67     ]);
68     $filtered_html_format->save();
69
70     // Create a node type.
71     $this->drupalCreateContentType([
72       'type' => 'article',
73       'name' => 'Article',
74     ]);
75
76     // Set the node type to initially not have revisions.
77     // Testing with revisions will be done later.
78     $node_type = NodeType::load('article');
79     $node_type->setNewRevision(FALSE);
80     $node_type->save();
81
82     // Create one node of the above node type using the above text format.
83     $this->testNode = $this->drupalCreateNode([
84       'type' => 'article',
85       'body' => [
86         0 => [
87           'value' => '<p>How are you?</p>',
88           'format' => 'filtered_html',
89         ],
90       ],
91       'revision_log' => $this->randomString(),
92     ]);
93
94     // Create 2 users, the only difference being the ability to use in-place
95     // editing
96     $basic_permissions = ['access content', 'create article content', 'edit any article content', 'use text format filtered_html', 'access contextual links'];
97     $this->authorUser = $this->drupalCreateUser($basic_permissions);
98     $this->editorUser = $this->drupalCreateUser(array_merge($basic_permissions, ['access in-place editing']));
99   }
100
101   /**
102    * Test the loading of Quick Edit when a user doesn't have access to it.
103    */
104   public function testUserWithoutPermission() {
105     $this->drupalLogin($this->authorUser);
106     $this->drupalGet('node/1');
107
108     // Library and in-place editors.
109     $this->assertNoRaw('core/modules/quickedit/js/quickedit.js', 'Quick Edit library not loaded.');
110     $this->assertNoRaw('core/modules/quickedit/js/editors/formEditor.js', "'form' in-place editor not loaded.");
111
112     // HTML annotation and title class does not exist for users without
113     // permission to in-place edit.
114     $this->assertNoRaw('data-quickedit-entity-id="node/1"');
115     $this->assertNoRaw('data-quickedit-field-id="node/1/body/en/full"');
116     $this->assertNoFieldByXPath('//h1[contains(@class, "js-quickedit-page-title")]');
117
118     // Retrieving the metadata should result in an empty 403 response.
119     $post = ['fields[0]' => 'node/1/body/en/full'];
120     $response = $this->drupalPostWithFormat(Url::fromRoute('quickedit.metadata'), 'json', $post);
121     $this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
122     $this->assertResponse(403);
123
124     // Quick Edit's JavaScript would never hit these endpoints if the metadata
125     // was empty as above, but we need to make sure that malicious users aren't
126     // able to use any of the other endpoints either.
127     $post = ['editors[0]' => 'form'] + $this->getAjaxPageStatePostData();
128     $response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
129     $message = Json::encode(['message' => "The 'access in-place editing' permission is required."]);
130     $this->assertIdentical($message, $response);
131     $this->assertResponse(403);
132     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
133     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
134     $this->assertIdentical($message, $response);
135     $this->assertResponse(403);
136     $edit = [];
137     $edit['form_id'] = 'quickedit_field_form';
138     $edit['form_token'] = 'xIOzMjuc-PULKsRn_KxFn7xzNk5Bx7XKXLfQfw1qOnA';
139     $edit['form_build_id'] = 'form-kVmovBpyX-SJfTT5kY0pjTV35TV-znor--a64dEnMR8';
140     $edit['body[0][summary]'] = '';
141     $edit['body[0][value]'] = '<p>Malicious content.</p>';
142     $edit['body[0][format]'] = 'filtered_html';
143     $edit['op'] = t('Save');
144     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $edit, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
145     $this->assertIdentical($message, $response);
146     $this->assertResponse(403);
147     $post = ['nocssjs' => 'true'];
148     $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
149     $this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
150     $this->assertResponse(403);
151   }
152
153   /**
154    * Tests the loading of Quick Edit when a user does have access to it.
155    *
156    * Also ensures lazy loading of in-place editors works.
157    */
158   public function testUserWithPermission() {
159     $this->drupalLogin($this->editorUser);
160     $this->drupalGet('node/1');
161
162     // Library and in-place editors.
163     $settings = $this->getDrupalSettings();
164     $libraries = explode(',', $settings['ajaxPageState']['libraries']);
165     $this->assertTrue(in_array('quickedit/quickedit', $libraries), 'Quick Edit library loaded.');
166     $this->assertFalse(in_array('quickedit/quickedit.inPlaceEditor.form', $libraries), "'form' in-place editor not loaded.");
167
168     // HTML annotation and title class must always exist (to not break the
169     // render cache).
170     $this->assertRaw('data-quickedit-entity-id="node/1"');
171     $this->assertRaw('data-quickedit-field-id="node/1/body/en/full"');
172     $this->assertFieldByXPath('//h1[contains(@class, "js-quickedit-page-title")]');
173
174     // There should be only one revision so far.
175     $node = Node::load(1);
176     $vids = \Drupal::entityManager()->getStorage('node')->revisionIds($node);
177     $this->assertIdentical(1, count($vids), 'The node has only one revision.');
178     $original_log = $node->revision_log->value;
179
180     // Retrieving the metadata should result in a 200 JSON response.
181     $htmlPageDrupalSettings = $this->drupalSettings;
182     $post = ['fields[0]' => 'node/1/body/en/full'];
183     $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post);
184     $this->assertResponse(200);
185     $expected = [
186       'node/1/body/en/full' => [
187         'label' => 'Body',
188         'access' => TRUE,
189         'editor' => 'form',
190       ],
191     ];
192     $this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
193     // Restore drupalSettings to build the next requests; simpletest wipes them
194     // after a JSON response.
195     $this->drupalSettings = $htmlPageDrupalSettings;
196
197     // Retrieving the attachments should result in a 200 response, containing:
198     //  1. a settings command with useless metadata: AjaxController is dumb
199     //  2. an insert command that loads the required in-place editors
200     $post = ['editors[0]' => 'form'] + $this->getAjaxPageStatePostData();
201     $response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
202     $ajax_commands = Json::decode($response);
203     $this->assertIdentical(2, count($ajax_commands), 'The attachments HTTP request results in two AJAX commands.');
204     // First command: settings.
205     $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.');
206     // Second command: insert libraries into DOM.
207     $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.');
208     $this->assertTrue(in_array('quickedit/quickedit.inPlaceEditor.form', explode(',', $ajax_commands[0]['settings']['ajaxPageState']['libraries'])), 'The quickedit.inPlaceEditor.form library is loaded.');
209
210     // Retrieving the form for this field should result in a 200 response,
211     // containing only a quickeditFieldForm command.
212     $post = ['nocssjs' => 'true', 'reset' => 'true'] + $this->getAjaxPageStatePostData();
213     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
214     $this->assertResponse(200);
215     $ajax_commands = Json::decode($response);
216     $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
217     $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
218     $this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
219
220     // Prepare form values for submission. drupalPostAjaxForm() is not suitable
221     // for handling pages with JSON responses, so we need our own solution here.
222     $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);
223     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
224
225     if ($form_tokens_found) {
226       $edit = [
227         'body[0][summary]' => '',
228         'body[0][value]' => '<p>Fine thanks.</p>',
229         'body[0][format]' => 'filtered_html',
230         'op' => t('Save'),
231       ];
232       $post = [
233         'form_id' => 'quickedit_field_form',
234         'form_token' => $token_match[1],
235         'form_build_id' => $build_id_match[1],
236       ];
237       $post += $edit + $this->getAjaxPageStatePostData();
238
239       // Submit field form and check response. This should store the updated
240       // entity in PrivateTempStore on the server.
241       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
242       $this->assertResponse(200);
243       $ajax_commands = Json::decode($response);
244       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
245       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
246       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Fine thanks.'), 'Form value saved and printed back.');
247       $this->assertIdentical($ajax_commands[0]['other_view_modes'], [], 'Field was not rendered in any other view mode.');
248
249       // Ensure the text on the original node did not change yet.
250       $this->drupalGet('node/1');
251       $this->assertText('How are you?');
252
253       // Save the entity by moving the PrivateTempStore values to entity storage.
254       $post = ['nocssjs' => 'true'];
255       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
256       $this->assertResponse(200);
257       $ajax_commands = Json::decode($response);
258       $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
259       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditEntitySaved command.');
260       $this->assertIdentical($ajax_commands[0]['data']['entity_type'], 'node', 'Saved entity is of type node.');
261       $this->assertIdentical($ajax_commands[0]['data']['entity_id'], '1', 'Entity id is 1.');
262
263       // Ensure the text on the original node did change.
264       $this->drupalGet('node/1');
265       $this->assertText('Fine thanks.');
266
267       // Ensure no new revision was created and the log message is unchanged.
268       $node = Node::load(1);
269       $vids = \Drupal::entityManager()->getStorage('node')->revisionIds($node);
270       $this->assertIdentical(1, count($vids), 'The node has only one revision.');
271       $this->assertIdentical($original_log, $node->revision_log->value, 'The revision log message is unchanged.');
272
273       // Now configure this node type to create new revisions automatically,
274       // then again retrieve the field form, fill it, submit it (so it ends up
275       // in PrivateTempStore) and then save the entity. Now there should be two
276       // revisions.
277       $node_type = NodeType::load('article');
278       $node_type->setNewRevision(TRUE);
279       $node_type->save();
280
281       // Retrieve field form.
282       $post = ['nocssjs' => 'true', 'reset' => 'true'];
283       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
284       $this->assertResponse(200);
285       $ajax_commands = Json::decode($response);
286       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
287       $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
288       $this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
289
290       // Submit field form.
291       preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match);
292       preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
293       $edit['body[0][value]'] = '<p>kthxbye</p>';
294       $post = [
295         'form_id' => 'quickedit_field_form',
296         'form_token' => $token_match[1],
297         'form_build_id' => $build_id_match[1],
298       ];
299       $post += $edit + $this->getAjaxPageStatePostData();
300       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
301       $this->assertResponse(200);
302       $ajax_commands = Json::decode($response);
303       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
304       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is an quickeditFieldFormSaved command.');
305       $this->assertTrue(strpos($ajax_commands[0]['data'], 'kthxbye'), 'Form value saved and printed back.');
306
307       // Save the entity.
308       $post = ['nocssjs' => 'true'];
309       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
310       $this->assertResponse(200);
311       $ajax_commands = Json::decode($response);
312       $this->assertIdentical(1, count($ajax_commands));
313       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is an quickeditEntitySaved command.');
314       $this->assertEqual($ajax_commands[0]['data'], ['entity_type' => 'node', 'entity_id' => 1], 'Updated entity type and ID returned');
315
316       // Test that a revision was created with the correct log message.
317       $vids = \Drupal::entityManager()->getStorage('node')->revisionIds(Node::load(1));
318       $this->assertIdentical(2, count($vids), 'The node has two revisions.');
319       $revision = node_revision_load($vids[0]);
320       $this->assertIdentical($original_log, $revision->revision_log->value, 'The first revision log message is unchanged.');
321       $revision = node_revision_load($vids[1]);
322       $this->assertIdentical('Updated the <em class="placeholder">Body</em> field through in-place editing.', $revision->revision_log->value, 'The second revision log message was correctly generated by Quick Edit module.');
323     }
324   }
325
326   /**
327    * Test quickedit does not appear for entities with pending revisions.
328    */
329   public function testWithPendingRevision() {
330     $this->drupalLogin($this->editorUser);
331
332     // Verify that the preview is loaded correctly.
333     $this->drupalPostForm('node/add/article', ['title[0][value]' => 'foo'], 'Preview');
334     $this->assertResponse(200);
335     // Verify that quickedit is not active on preview.
336     $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
337     $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
338
339     $this->drupalGet('node/' . $this->testNode->id());
340     $this->assertRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
341     $this->assertRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
342
343     $this->testNode->title = 'Updated node';
344     $this->testNode->setNewRevision(TRUE);
345     $this->testNode->isDefaultRevision(FALSE);
346     $this->testNode->save();
347
348     $this->drupalGet('node/' . $this->testNode->id());
349     $this->assertResponse(200);
350     $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
351     $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
352   }
353
354   /**
355    * Tests the loading of Quick Edit for the title base field.
356    */
357   public function testTitleBaseField() {
358     $this->drupalLogin($this->editorUser);
359     $this->drupalGet('node/1');
360
361     // Ensure that the full page title is actually in-place editable
362     $node = Node::load(1);
363     $elements = $this->xpath('//h1/span[@data-quickedit-field-id="node/1/title/en/full" and normalize-space(text())=:title]', [':title' => $node->label()]);
364     $this->assertTrue(!empty($elements), 'Title with data-quickedit-field-id attribute found.');
365
366     // Retrieving the metadata should result in a 200 JSON response.
367     $htmlPageDrupalSettings = $this->drupalSettings;
368     $post = ['fields[0]' => 'node/1/title/en/full'];
369     $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post);
370     $this->assertResponse(200);
371     $expected = [
372       'node/1/title/en/full' => [
373         'label' => 'Title',
374         'access' => TRUE,
375         'editor' => 'plain_text',
376       ],
377     ];
378     $this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
379     // Restore drupalSettings to build the next requests; simpletest wipes them
380     // after a JSON response.
381     $this->drupalSettings = $htmlPageDrupalSettings;
382
383     // Retrieving the form for this field should result in a 200 response,
384     // containing only a quickeditFieldForm command.
385     $post = ['nocssjs' => 'true', 'reset' => 'true'] + $this->getAjaxPageStatePostData();
386     $response = $this->drupalPost('quickedit/form/' . 'node/1/title/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
387     $this->assertResponse(200);
388     $ajax_commands = Json::decode($response);
389     $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
390     $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
391     $this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
392
393     // Prepare form values for submission. drupalPostAjaxForm() is not suitable
394     // for handling pages with JSON responses, so we need our own solution
395     // here.
396     $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);
397     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
398
399     if ($form_tokens_found) {
400       $edit = [
401         'title[0][value]' => 'Obligatory question',
402         'op' => t('Save'),
403       ];
404       $post = [
405         'form_id' => 'quickedit_field_form',
406         'form_token' => $token_match[1],
407         'form_build_id' => $build_id_match[1],
408       ];
409       $post += $edit + $this->getAjaxPageStatePostData();
410
411       // Submit field form and check response. This should store the
412       // updated entity in PrivateTempStore on the server.
413       $response = $this->drupalPost('quickedit/form/' . 'node/1/title/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
414       $this->assertResponse(200);
415       $ajax_commands = Json::decode($response);
416       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
417       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
418       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Obligatory question'), 'Form value saved and printed back.');
419
420       // Ensure the text on the original node did not change yet.
421       $this->drupalGet('node/1');
422       $this->assertNoText('Obligatory question');
423
424       // Save the entity by moving the PrivateTempStore values to entity storage.
425       $post = ['nocssjs' => 'true'];
426       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
427       $this->assertResponse(200);
428       $ajax_commands = Json::decode($response);
429       $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
430       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is n quickeditEntitySaved command.');
431       $this->assertIdentical($ajax_commands[0]['data']['entity_type'], 'node', 'Saved entity is of type node.');
432       $this->assertIdentical($ajax_commands[0]['data']['entity_id'], '1', 'Entity id is 1.');
433
434       // Ensure the text on the original node did change.
435       $this->drupalGet('node/1');
436       $this->assertText('Obligatory question');
437     }
438   }
439
440   /**
441    * Tests that Quick Edit doesn't make fields rendered with display options
442    * editable.
443    */
444   public function testDisplayOptions() {
445     $node = Node::load('1');
446     $display_settings = [
447       'label' => 'inline',
448     ];
449     $build = $node->body->view($display_settings);
450     $output = \Drupal::service('renderer')->renderRoot($build);
451     $this->assertFalse(strpos($output, 'data-quickedit-field-id'), 'data-quickedit-field-id attribute not added when rendering field using dynamic display options.');
452   }
453
454   /**
455    * Tests that Quick Edit works with custom render pipelines.
456    */
457   public function testCustomPipeline() {
458     \Drupal::service('module_installer')->install(['quickedit_test']);
459
460     $custom_render_url = 'quickedit/form/node/1/body/en/quickedit_test-custom-render-data';
461     $this->drupalLogin($this->editorUser);
462
463     // Request editing to render results with the custom render pipeline.
464     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
465     $response = $this->drupalPost($custom_render_url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
466     $ajax_commands = Json::decode($response);
467
468     // Prepare form values for submission. drupalPostAJAX() is not suitable for
469     // handling pages with JSON responses, so we need our own solution here.
470     $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);
471     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
472
473     if ($form_tokens_found) {
474       $post = [
475         'form_id' => 'quickedit_field_form',
476         'form_token' => $token_match[1],
477         'form_build_id' => $build_id_match[1],
478         'body[0][summary]' => '',
479         'body[0][value]' => '<p>Fine thanks.</p>',
480         'body[0][format]' => 'filtered_html',
481         'op' => t('Save'),
482       ];
483       // Assume there is another field on this page, which doesn't use a custom
484       // render pipeline, but the default one, and it uses the "full" view mode.
485       $post += ['other_view_modes[]' => 'full'];
486
487       // Submit field form and check response. Should render with the custom
488       // render pipeline.
489       $response = $this->drupalPost($custom_render_url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
490       $this->assertResponse(200);
491       $ajax_commands = Json::decode($response);
492       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
493       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
494       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Fine thanks.'), 'Form value saved and printed back.');
495       $this->assertTrue(strpos($ajax_commands[0]['data'], '<div class="quickedit-test-wrapper">') !== FALSE, 'Custom render pipeline used to render the value.');
496       $this->assertIdentical(array_keys($ajax_commands[0]['other_view_modes']), ['full'], 'Field was also rendered in the "full" view mode.');
497       $this->assertTrue(strpos($ajax_commands[0]['other_view_modes']['full'], 'Fine thanks.'), '"full" version of field contains the form value.');
498     }
499   }
500
501   /**
502    * Tests Quick Edit on a node that was concurrently edited on the full node
503    * form.
504    */
505   public function testConcurrentEdit() {
506     $this->drupalLogin($this->editorUser);
507
508     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
509     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
510     $this->assertResponse(200);
511     $ajax_commands = Json::decode($response);
512
513     // Prepare form values for submission. drupalPostAJAX() is not suitable for
514     // handling pages with JSON responses, so we need our own solution here.
515     $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);
516     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
517
518     if ($form_tokens_found) {
519       $post = [
520         'nocssjs' => 'true',
521         'form_id' => 'quickedit_field_form',
522         'form_token' => $token_match[1],
523         'form_build_id' => $build_id_match[1],
524         'body[0][summary]' => '',
525         'body[0][value]' => '<p>Fine thanks.</p>',
526         'body[0][format]' => 'filtered_html',
527         'op' => t('Save'),
528       ];
529
530       // Save the node on the regular node edit form.
531       $this->drupalPostForm('node/1/edit', [], t('Save'));
532       // Ensure different save timestamps for field editing.
533       sleep(2);
534
535       // Submit field form and check response. Should throw a validation error
536       // because the node was changed in the meantime.
537       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
538       $this->assertResponse(200);
539       $ajax_commands = Json::decode($response);
540       $this->assertIdentical(2, count($ajax_commands), 'The field form HTTP request results in two AJAX commands.');
541       $this->assertIdentical('quickeditFieldFormValidationErrors', $ajax_commands[1]['command'], 'The second AJAX command is a quickeditFieldFormValidationErrors command.');
542       $this->assertTrue(strpos($ajax_commands[1]['data'], 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.'), 'Error message returned to user.');
543     }
544   }
545
546   /**
547    * Tests that Quick Edit's data- attributes are present for content blocks.
548    */
549   public function testContentBlock() {
550     \Drupal::service('module_installer')->install(['block_content']);
551
552     // Create and place a content_block block.
553     $block = BlockContent::create([
554       'info' => $this->randomMachineName(),
555       'type' => 'basic',
556       'langcode' => 'en',
557     ]);
558     $block->save();
559     $this->drupalPlaceBlock('block_content:' . $block->uuid());
560
561     // Check that the data- attribute is present.
562     $this->drupalLogin($this->editorUser);
563     $this->drupalGet('');
564     $this->assertRaw('data-quickedit-entity-id="block_content/1"');
565   }
566
567   /**
568    * Tests that Quick Edit can handle an image field.
569    */
570   public function testImageField() {
571     // Add an image field to the content type.
572     FieldStorageConfig::create([
573       'field_name' => 'field_image',
574       'type' => 'image',
575       'entity_type' => 'node',
576     ])->save();
577     FieldConfig::create([
578       'field_name' => 'field_image',
579       'field_type' => 'image',
580       'label' => t('Image'),
581       'entity_type' => 'node',
582       'bundle' => 'article',
583     ])->save();
584     entity_get_form_display('node', 'article', 'default')
585       ->setComponent('field_image', [
586         'type' => 'image_image',
587       ])
588       ->save();
589
590     // Add an image to the node.
591     $this->drupalLogin($this->editorUser);
592     $image = $this->drupalGetTestFiles('image')[0];
593     $this->drupalPostForm('node/1/edit', [
594       'files[field_image_0]' => $image->uri,
595     ], t('Upload'));
596     $this->drupalPostForm(NULL, [
597       'field_image[0][alt]' => 'Vivamus aliquet elit',
598     ], t('Save'));
599
600     // The image field form should load normally.
601     $response = $this->drupalPost('quickedit/form/node/1/field_image/en/full', '', ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData(), ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
602     $this->assertResponse(200);
603     $ajax_commands = Json::decode($response);
604     $this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
605   }
606
607 }