From 57f1f1a480e1276b6ce23b1f832c406d8bf95ff1 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Thu, 31 Jan 2019 01:02:35 +0000
Subject: [PATCH] Issue #2215857 by michielnugter, Lendude, gmercer,
 tim.plunkett, cferthorney, marabak, olli, ericmulder1980, TwoD, sanduhrs,
 stella, dww, nod_: Behaviors get attached to removed forms

---
 core/misc/ajax.es6.js                         |  9 +++--
 core/misc/ajax.js                             |  6 +--
 .../FilterCriteriaTest.php                    | 40 +++++++++++++++----
 .../Ajax/CommandsTest.php                     | 20 ++++++----
 4 files changed, 54 insertions(+), 21 deletions(-)

diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 440e5906dcd7..39784c403c2a 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -774,7 +774,7 @@
     // when there is a form such that this.$form.ajaxSubmit() is used instead of
     // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
     // isn't called, but don't rely on that: explicitly check this.$form.
-    if (this.$form) {
+    if (this.$form && document.contains(this.$form.get(0))) {
       const settings = this.settings || drupalSettings;
       Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
     }
@@ -1020,7 +1020,7 @@
     // attachBehaviors() called on the new content from processing the response
     // commands is not sufficient, because behaviors from the entire form need
     // to be reattached.
-    if (this.$form) {
+    if (this.$form && document.contains(this.$form.get(0))) {
       const settings = this.settings || drupalSettings;
       Drupal.attachBehaviors(this.$form.get(0), settings);
     }
@@ -1088,8 +1088,9 @@
     $(this.wrapper).show();
     // Re-enable the element.
     $(this.element).prop('disabled', false);
-    // Reattach behaviors, if they were detached in beforeSerialize().
-    if (this.$form) {
+    // Reattach behaviors, if they were detached in beforeSerialize(), and the
+    // form is still part of the document.
+    if (this.$form && document.contains(this.$form.get(0))) {
       const settings = this.settings || drupalSettings;
       Drupal.attachBehaviors(this.$form.get(0), settings);
     }
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 73b4dcca69e4..79aac05e8520 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -339,7 +339,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
   };
 
   Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
-    if (this.$form) {
+    if (this.$form && document.contains(this.$form.get(0))) {
       var settings = this.settings || drupalSettings;
       Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
     }
@@ -451,7 +451,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
       }
     }
 
-    if (this.$form) {
+    if (this.$form && document.contains(this.$form.get(0))) {
       var settings = this.settings || drupalSettings;
       Drupal.attachBehaviors(this.$form.get(0), settings);
     }
@@ -493,7 +493,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
 
     $(this.element).prop('disabled', false);
 
-    if (this.$form) {
+    if (this.$form && document.contains(this.$form.get(0))) {
       var settings = this.settings || drupalSettings;
       Drupal.attachBehaviors(this.$form.get(0), settings);
     }
diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php
index 9e303dfb9222..a9ec2475b6b5 100644
--- a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php
+++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php
@@ -42,13 +42,7 @@ public function testFilterCriteriaDialog() {
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
 
-    // Use the 'And/Or Rearrange' link for fields to open a dialog.
-    $dropbutton = $page->find('css', '.views-ui-display-tab-bucket.filter .dropbutton-toggle button');
-    $dropbutton->click();
-    $add_link = $page->findById('views-rearrange-filter');
-    $this->assertTrue($add_link->isVisible(), 'And/Or Rearrange button found.');
-    $add_link->click();
-    $assert_session->assertWaitOnAjaxRequest();
+    $this->openFilterDialog();
 
     // Add a new filter group.
     $create_new_filter_group = $page->findById('views-add-group-link');
@@ -71,6 +65,38 @@ public function testFilterCriteriaDialog() {
     $this->drupalGet('admin/structure/views/view/who_s_online');
     $page = $this->getSession()->getPage();
     $this->assertNotNull($page->findLink('User: Last access (>= -15 minutes)'));
+
+    // Add group again to test drag-n-drop.
+    $this->openFilterDialog();
+
+    $this->assertSession()->waitForLink('Create new filter group', 20000);
+    $create_new_filter_group = $page->findLink('Create new filter group');
+    $this->assertTrue($create_new_filter_group->isVisible(), 'Add group link found.');
+    $create_new_filter_group->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Validate dragging works correctly and the new group will contain the new
+    // filter.
+    $dragged = $page->find('css', ".tabledrag-handle");
+    $target = $page->find('css', '.filter-group-operator-row');
+    $dragged->dragTo($target);
+
+    $remove_link = $page->findLink('Remove group');
+    $this->assertFalse($remove_link->isVisible(), 'Remove group should be invisible after drag.');
+  }
+
+  /**
+   * Uses the 'And/Or Rearrange' link for filters to open a dialog.
+   */
+  protected function openFilterDialog() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $dropbutton = $page->find('css', '.views-ui-display-tab-bucket.filter .dropbutton-toggle button');
+    $dropbutton->click();
+    $add_link = $page->findById('views-rearrange-filter');
+    $this->assertTrue($add_link->isVisible(), 'And/Or Rearrange button found.');
+    $add_link->click();
+    $assert_session->assertWaitOnAjaxRequest();
   }
 
 }
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
index 0db5e5f74e4d..f2e957704823 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
@@ -37,14 +37,20 @@ public function testAjaxCommands() {
     $this->assertWaitPageContains('<div id="after_div">Something can be inserted after this</div>This will be placed after');
 
     // Tests the 'alert' command.
-    $test_alert_command = <<<JS
-window.alert = function() {
-  document.body.innerHTML += '<div class="alert-command">Alert</div>';
-};
-JS;
-    $session->executeScript($test_alert_command);
     $page->pressButton("AJAX 'Alert': Click to alert");
-    $this->assertWaitPageContains('<div class="alert-command">Alert</div>');
+    // Wait for the alert to appear.
+    $page->waitFor(10, function () use ($session) {
+      try {
+        $session->getDriver()->getWebDriverSession()->getAlert_text();
+        return TRUE;
+      }
+      catch (\Exception $e) {
+        return FALSE;
+      }
+    });
+    $alert_text = $this->getSession()->getDriver()->getWebDriverSession()->getAlert_text();
+    $this->assertEquals('Alert', $alert_text);
+    $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
 
     // Tests the 'append' command.
     $page->pressButton("AJAX 'Append': Click to append something");
-- 
GitLab