 .../Drupal/block/Tests/Views/DisplayBlockTest.php  |   12 +-
 .../Tests/ContextualDynamicContextTest.php         |   22 +-
 .../edit/lib/Drupal/edit/Tests/EditLoadingTest.php |  227 +++-----------------
 .../editor/Tests/EditIntegrationLoadingTest.php    |   47 +---
 .../menu/lib/Drupal/menu/Tests/MenuTest.php        |   12 +-
 .../lib/Drupal/simpletest/WebTestBase.php          |  107 +++++++--
 .../lib/Drupal/views_ui/Tests/DisplayTest.php      |   12 +-
 7 files changed, 124 insertions(+), 315 deletions(-)

diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
index 65a9ed9..3e192b1 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -162,16 +162,8 @@ public function testBlockContextualLinks() {
 
     // Get server-rendered contextual links.
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
-    $post = urlencode('ids[0]') . '=' . urlencode($id);
-    $response = $this->curlExec(array(
-      CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-page'))),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post,
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
+    $post = array('ids[0]' => $id);
+    $response = $this->drupalPostUrl('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
     $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-ui-edit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
index 3c35e48..82c7e73 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
@@ -147,30 +147,10 @@ protected function assertNoContextualLinkPlaceHolder($id) {
    *   The response body.
    */
   protected function renderContextualLinks($ids, $current_path) {
-    // Build POST values.
     $post = array();
     for ($i = 0; $i < count($ids); $i++) {
       $post['ids[' . $i . ']'] = $ids[$i];
     }
-
-    // Serialize POST values.
-    foreach ($post as $key => $value) {
-      // Encode according to application/x-www-form-urlencoded
-      // Both names and values needs to be urlencoded, according to
-      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
-      $post[$key] = urlencode($key) . '=' . urlencode($value);
-    }
-    $post = implode('&', $post);
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => $current_path))),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post,
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
+    return $this->drupalPostUrl('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path)));
   }
 }
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php
index 7b01ade..6bc7d27 100644
--- a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php
+++ b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php
@@ -86,18 +86,21 @@ function testUserWithoutPermission() {
     $this->assertRaw('data-edit-id="node/1/body/und/full"');
 
     // Retrieving the metadata should result in an empty 403 response.
-    $response = $this->retrieveMetadata(array('node/1/body/und/full'));
+    $post = array('fields[0]' => 'node/1/body/und/full');
+    $response = $this->drupalPostUrl('edit/metadata', 'application/json', $post);
     $this->assertIdentical('{}', $response);
     $this->assertResponse(403);
 
     // Edit's JavaScript would never hit these endpoints if the metadata was
     // empty as above, but we need to make sure that malicious users aren't able
     // to use any of the other endpoints either.
-    $response = $this->retrieveAttachments(array('form'));
+    $post = array('editors[0]' => 'form') + $this->getAjaxPageStatePostData();
+    $response = $this->drupalPostUrl('edit/attachments', 'application/vnd.drupal-ajax', $post);
     // @todo Uncomment the below once https://drupal.org/node/2063303 is fixed.
     // $this->assertIdentical('[]', $response);
     $this->assertResponse(403);
-    $response = $this->retrieveFieldForm('node/1/body/und/full');
+    $post = array('nocssjs' => 'true') + $this->getAjaxPageStatePostData();
+    $response = $this->drupalPostUrl('edit/form/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', $post);
     // @todo Uncomment the below once https://drupal.org/node/2063303 is fixed.
     // $this->assertIdentical('[]', $response);
     $this->assertResponse(403);
@@ -109,11 +112,12 @@ function testUserWithoutPermission() {
     $edit['body[und][0][value]'] = '<p>Malicious content.</p>';
     $edit['body[und][0][format]'] = 'filtered_html';
     $edit['op'] = t('Save');
-    $response = $this->submitFieldForm('node/1/body/und/full', $edit);
+    $response = $this->drupalPostUrl('edit/form/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', $post);
     // @todo Uncomment the below once https://drupal.org/node/2063303 is fixed.
     // $this->assertIdentical('[]', $response);
     $this->assertResponse(403);
-    $response = $this->saveEntity('node/1');
+    $post = array('nocssjs' => 'true');
+    $response = $this->drupalPostUrl('edit/entity/' . 'node/1', 'application/json', $post);
     // @todo Uncomment the below once https://drupal.org/node/2063303 is fixed.
     // $this->assertIdentical('[]', $response);
     $this->assertResponse(403);
@@ -140,7 +144,8 @@ function testUserWithPermission() {
 
     // Retrieving the metadata should result in a 200 JSON response.
     $htmlPageDrupalSettings = $this->drupalSettings;
-    $response = $this->retrieveMetadata(array('node/1/body/und/full'));
+    $post = array('fields[0]' => 'node/1/body/und/full');
+    $response = $this->drupalPostUrl('edit/metadata', 'application/json', $post);
     $this->assertResponse(200);
     $expected = array(
       'node/1/body/und/full' => array(
@@ -158,7 +163,8 @@ function testUserWithPermission() {
     // Retrieving the attachments should result in a 200 response, containing:
     //  1. a settings command with useless metadata: AjaxController is dumb
     //  2. an insert command that loads the required in-place editors
-    $response = $this->retrieveAttachments(array('form'));
+    $post = array('editors[0]' => 'form') + $this->getAjaxPageStatePostData();
+    $response = $this->drupalPostUrl('edit/attachments', 'application/vnd.drupal-ajax', $post);
     $ajax_commands = drupal_json_decode($response);
     $this->assertIdentical(2, count($ajax_commands), 'The attachments HTTP request results in two AJAX commands.');
     // First command: settings.
@@ -170,7 +176,8 @@ function testUserWithPermission() {
 
     // Retrieving the form for this field should result in a 200 response,
     // containing only an editFieldForm command.
-    $response = $this->retrieveFieldForm('node/1/body/und/full');
+    $post = array('nocssjs' => 'true') + $this->getAjaxPageStatePostData();
+    $response = $this->drupalPostUrl('edit/form/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', $post);
     $this->assertResponse(200);
     $ajax_commands = drupal_json_decode($response);
     $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
@@ -184,18 +191,20 @@ function testUserWithPermission() {
     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
 
     if ($form_tokens_found) {
-      $edit = array();
-      $edit['form_id'] = 'edit_field_form';
-      $edit['form_token'] = $token_match[1];
-      $edit['form_build_id'] = $build_id_match[1];
-      $edit['body[und][0][summary]'] = '';
-      $edit['body[und][0][value]'] = '<p>Fine thanks.</p>';
-      $edit['body[und][0][format]'] = 'filtered_html';
-      $edit['op'] = t('Save');
+      $post = array(
+        'form_id' => 'edit_field_form',
+        'form_token' => $token_match[1],
+        'form_build_id' => $build_id_match[1],
+        'body[und][0][summary]' => '',
+        'body[und][0][value]' => '<p>Fine thanks.</p>',
+        'body[und][0][format]' => 'filtered_html',
+        'op' => t('Save'),
+      );
+      $post += $this->getAjaxPageStatePostData();
 
       // Submit field form and check response. This should store the
       // updated entity in TempStore on the server.
-      $response = $this->submitFieldForm('node/1/body/und/full', $edit);
+      $response = $this->drupalPostUrl('edit/form/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', $post);
       $this->assertResponse(200);
       $ajax_commands = drupal_json_decode($response);
       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
@@ -207,7 +216,8 @@ function testUserWithPermission() {
       $this->assertText('How are you?');
 
       // Save the entity by moving the TempStore values to entity storage.
-      $response = $this->saveEntity('node/1');
+      $post = array('nocssjs' => 'true');
+      $response = $this->drupalPostUrl('edit/entity/' . 'node/1', 'application/json', $post);
       $this->assertResponse(200);
       $ajax_commands = drupal_json_decode($response);
       $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
@@ -221,185 +231,4 @@ function testUserWithPermission() {
     }
   }
 
-  /**
-   * Retrieve Edit metadata from the server. May also result in additional
-   * JavaScript settings and CSS/JS being loaded.
-   *
-   * @param array $ids
-   *   An array of edit ids.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function retrieveMetadata($ids) {
-    // Build POST values.
-    $post = array();
-    for ($i = 0; $i < count($ids); $i++) {
-      $post['fields[' . $i . ']'] = $ids[$i];
-    }
-
-    // Serialize POST values.
-    foreach ($post as $key => $value) {
-      // Encode according to application/x-www-form-urlencoded
-      // Both names and values needs to be urlencoded, according to
-      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
-      $post[$key] = urlencode($key) . '=' . urlencode($value);
-    }
-    $post = implode('&', $post);
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('edit/metadata', array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Retrieves AJAX commands to load attachments for the given in-place editors.
-   *
-   * @param array $editors
-   *   An array of in-place editor ids.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function retrieveAttachments($editors) {
-    // Build POST values.
-    $post = array();
-    for ($i = 0; $i < count($editors); $i++) {
-      $post['editors[' . $i . ']'] = $editors[$i];
-    }
-
-    // Serialize POST values.
-    foreach ($post as $key => $value) {
-      // Encode according to application/x-www-form-urlencoded
-      // Both names and values needs to be urlencoded, according to
-      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
-      $post[$key] = urlencode($key) . '=' . urlencode($value);
-    }
-    $post = implode('&', $post);
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('edit/attachments', array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/vnd.drupal-ajax',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Submit field form data to the server.
-   *
-   * @param string $field_id
-   *   An Edit field ID.
-   * @param array $post
-   *   An array of post data to send.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function submitFieldForm($field_id, $post) {
-    // Serialize POST values.
-    foreach ($post as $key => $value) {
-      // Encode according to application/x-www-form-urlencoded
-      // Both names and values needs to be urlencoded, according to
-      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
-      $post[$key] = urlencode($key) . '=' . urlencode($value);
-    }
-    $post = implode('&', $post);
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('edit/form/' . $field_id, array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Retrieve field form from the server. May also result in additional
-   * JavaScript settings and CSS/JS being loaded.
-   *
-   * @param string $field_id
-   *   An Edit field ID.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function retrieveFieldForm($field_id) {
-    // Build & serialize POST value.
-    $post = urlencode('nocssjs') . '=' . urlencode('true');
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('edit/form/' . $field_id, array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/vnd.drupal-ajax',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Save entity edits on the server.
-   *
-   * @param string $entity_type_id
-   *   The edit type and ID URI portion.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function saveEntity($entity_type_id) {
-    // Build & serialize POST value.
-    $post = urlencode('nocssjs') . '=' . urlencode('true');
-
-    // Perform HTTP request.
-    return $this->curlExec(array(
-      CURLOPT_URL => url('edit/entity/' . $entity_type_id, array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Get extra information to the POST data as ajax.js does.
-   *
-   * @return string
-   *   Additional post data.
-   */
-  protected function getAjaxPageStatePostData() {
-    $extra_post = '';
-    $drupal_settings = $this->drupalSettings;
-    if (isset($drupal_settings['ajaxPageState'])) {
-      $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']);
-      $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']);
-      foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1';
-      }
-      foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1';
-      }
-    }
-    return $extra_post;
-  }
-
 }
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationLoadingTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationLoadingTest.php
index 247678e..113719b 100644
--- a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationLoadingTest.php
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationLoadingTest.php
@@ -93,7 +93,7 @@ function testUsersWithoutPermission() {
       $this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
 
       // Retrieving the untransformed text should result in an empty 403 response.
-      $response = $this->retrieveUntransformedText('node/1/body/und/full');
+      $response = $this->drupalPostUrl('editor/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', array());
       $this->assertResponse(403);
       // @todo Uncomment the below once https://drupal.org/node/2063303 is fixed.
       // $this->assertIdentical('[]', $response);
@@ -111,7 +111,7 @@ function testUserWithPermission() {
     // Ensure the text is transformed.
     $this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
 
-    $response = $this->retrieveUntransformedText('node/1/body/und/full');
+    $response = $this->drupalPostUrl('editor/' . 'node/1/body/und/full', 'application/vnd.drupal-ajax', array());
     $this->assertResponse(200);
     $ajax_commands = drupal_json_decode($response);
     $this->assertIdentical(1, count($ajax_commands), 'The untransformed text POST request results in one AJAX command.');
@@ -119,47 +119,4 @@ function testUserWithPermission() {
     $this->assertIdentical('<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />', $ajax_commands[0]['data'], 'The editorGetUntransformedText command contains the expected data.');
   }
 
-  /**
-   * Retrieve untransformed text from the server.
-   *
-   * @param string $field_id
-   *   An Edit field ID.
-   *
-   * @return string
-   *   The response body.
-   */
-  protected function retrieveUntransformedText($field_id) {
-    return $this->curlExec(array(
-      CURLOPT_URL => url('editor/' . $field_id, array('absolute' => TRUE)),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $this->getAjaxPageStatePostData(),
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/vnd.drupal-ajax',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
-  }
-
-  /**
-   * Get extra information to the POST data as ajax.js does.
-   *
-   * @return string
-   *   Additional post data.
-   */
-  protected function getAjaxPageStatePostData() {
-    $extra_post = '';
-    $drupal_settings = $this->drupalSettings;
-    if (isset($drupal_settings['ajaxPageState'])) {
-      $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']);
-      $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']);
-      foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1';
-      }
-      foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1';
-      }
-    }
-    return $extra_post;
-  }
-
 }
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index a4b3e9e..695837e 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -416,16 +416,8 @@ public function testBlockContextualLinks() {
 
     // Get server-rendered contextual links.
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
-    $post = urlencode('ids[0]') . '=' . urlencode($id);
-    $response = $this->curlExec(array(
-      CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-page'))),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post,
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
+    $post = array('ids[0]' => $id);
+    $response =  $this->drupalPostUrl('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
     $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="menu-edit even last"><a href="' . base_path() . 'admin/structure/menu/manage/tools?destination=test-page">Edit menu</a></li></ul>');
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 7b9302a..750fe94 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1438,13 +1438,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
             }
           }
           else {
-            foreach ($post as $key => $value) {
-              // Encode according to application/x-www-form-urlencoded
-              // Both names and values needs to be urlencoded, according to
-              // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
-              $post[$key] = urlencode($key) . '=' . urlencode($value);
-            }
-            $post = implode('&', $post) . $extra_post;
+            $post = $this->serializePostValues($post) . $extra_post;
           }
           $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers));
           // Ensure that any changes to variables in the other thread are picked
@@ -1486,6 +1480,8 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
    * This executes a POST as ajax.js does. It uses the returned JSON data, an
    * array of commands, to update $this->content using equivalent DOM
    * manipulation as is used by ajax.js. It also returns the array of commands.
+   * It does not apply custom AJAX commands though, because emulation is only
+   * implemented for the AJAX commands that ship with Drupal core.
    *
    * @param $path
    *   Location of the form containing the Ajax enabled element to test. Can be
@@ -1550,10 +1546,10 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
     }
 
     // Add extra information to the POST data as ajax.js does.
-    $extra_post = '';
+    $extra_post = array();
     if (isset($ajax_settings['submit'])) {
       foreach ($ajax_settings['submit'] as $key => $value) {
-        $extra_post .= '&' . urlencode($key) . '=' . urlencode($value);
+        $extra_post[$key] = $value;
       }
     }
     $ajax_html_ids = array();
@@ -1561,18 +1557,11 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
       $ajax_html_ids[] = (string) $element['id'];
     }
     if (!empty($ajax_html_ids)) {
-      $extra_post .= '&' . urlencode('ajax_html_ids') . '=' . urlencode(implode(' ', $ajax_html_ids));
-    }
-    if (isset($drupal_settings['ajaxPageState'])) {
-      $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']);
-      $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']);
-      foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1';
-      }
-      foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) {
-        $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1';
-      }
+      $extra_post['ajax_html_ids'] = implode(' ', $ajax_html_ids);
     }
+    $extra_post += $this->getAjaxPageStatePostData();
+    // Now serialize all the $extra_post values, and prepend it with an '&'.
+    $extra_post = '&' . $this->serializePostValues($extra_post);
 
     // Unless a particular path is specified, use the one specified by the
     // Ajax settings, or else 'system/ajax'.
@@ -1677,6 +1666,84 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
   }
 
   /**
+   * Perform a POST HTTP request in a non-form context.
+   *
+   * @param string $path
+   *   Drupal path where the request should be POSTed to. Will be transformed
+   *   into an absolute path automatically.
+   * @param string $accept
+   *   The value for the "Accept" header. Usually either 'application/json' or
+   *   'application/vnd.drupal-ajax'.
+   * @param array $post
+   *   The POST data. When making a 'application/vnd.drupal-ajax' request, the
+   *   Ajax page state data should be included. Use getAjaxPageStatePostData()
+   *   for that.
+   * @param array $options
+   *   (optional) Options to be forwarded to the url generator. The 'absolute'
+   *   option will automatically be enabled.
+   *
+   * @return
+   *   The content returned from the call to curl_exec().
+   *
+   * @see WebTestBase::getAjaxPageStatePostData()
+   * @see WebTestBase::curlExec()
+   * @see url()
+   */
+  protected function drupalPostUrl($path, $accept, array $post, $options = array()) {
+    return $this->curlExec(array(
+      CURLOPT_URL => url($path, $options + array('absolute' => TRUE)),
+      CURLOPT_POST => TRUE,
+      CURLOPT_POSTFIELDS => $this->serializePostValues($post),
+      CURLOPT_HTTPHEADER => array(
+        'Accept: ' . $accept,
+        'Content-Type: application/x-www-form-urlencoded',
+      ),
+    ));
+  }
+
+  /**
+   * Get the Ajax page state from drupalSettings and prepare it for POSTing.
+   *
+   * @return array
+   *   The Ajax page state POST data.
+   */
+  protected function getAjaxPageStatePostData() {
+    $post = array();
+    $drupal_settings = $this->drupalSettings;
+    if (isset($drupal_settings['ajaxPageState'])) {
+      $post['ajax_page_state[theme]'] = $drupal_settings['ajaxPageState']['theme'];
+      $post['ajax_page_state[theme_token]'] = $drupal_settings['ajaxPageState']['theme_token'];
+      foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) {
+        $post["ajax_page_state[css][$key]"] = 1;
+      }
+      foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) {
+        $post["ajax_page_state[js][$key]"] = 1;
+      }
+    }
+    return $post;
+  }
+
+  /**
+   * Serialize POST HTTP request values.
+   *
+   * Encode according to application/x-www-form-urlencoded. Both names and
+   * values needs to be urlencoded, according to
+   * http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+   *
+   * @param array $post
+   *   The array of values to be POSTed.
+   *
+   * @return string
+   *   The serialized result.
+   */
+  protected function serializePostValues($post = array()) {
+    foreach ($post as $key => $value) {
+      $post[$key] = urlencode($key) . '=' . urlencode($value);
+    }
+    return implode('&', $post);
+  }
+
+  /**
    * Runs cron in the Drupal installed by Simpletest.
    */
   protected function cronRun() {
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
index 7d827f9..bbb07c6 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
@@ -294,16 +294,8 @@ public function testPageContextualLinks() {
 
     // Get server-rendered contextual links.
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
-    $post = urlencode('ids[0]') . '=' . urlencode($id);
-    $response = $this->curlExec(array(
-      CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-display'))),
-      CURLOPT_POST => TRUE,
-      CURLOPT_POSTFIELDS => $post,
-      CURLOPT_HTTPHEADER => array(
-        'Accept: application/json',
-        'Content-Type: application/x-www-form-urlencoded',
-      ),
-    ));
+    $post = array('ids[0]' => $id);
+    $response = $this->drupalPostUrl('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
     $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-ui-edit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
