diff --git a/core/modules/file/src/Tests/FileFieldTestBase.php b/core/modules/file/src/Tests/FileFieldTestBase.php index ba34c8f42b..12ab12390e 100644 --- a/core/modules/file/src/Tests/FileFieldTestBase.php +++ b/core/modules/file/src/Tests/FileFieldTestBase.php @@ -2,6 +2,8 @@ namespace Drupal\file\Tests; +@trigger_error('The ' . __NAMESPACE__ . '\FileFieldTestBase is scheduled for removal in Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileFieldTestBase', E_USER_DEPRECATED); + use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; use Drupal\file\FileInterface; diff --git a/core/modules/file/src/Tests/FileManagedTestBase.php b/core/modules/file/src/Tests/FileManagedTestBase.php index 879e1c4e94..fde651dca6 100644 --- a/core/modules/file/src/Tests/FileManagedTestBase.php +++ b/core/modules/file/src/Tests/FileManagedTestBase.php @@ -2,6 +2,8 @@ namespace Drupal\file\Tests; +@trigger_error('The ' . __NAMESPACE__ . '\FileManagedTestBase is scheduled for removal in Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileManagedTestBase', E_USER_DEPRECATED); + use Drupal\file\Entity\File; use Drupal\file\FileInterface; use Drupal\simpletest\WebTestBase; diff --git a/core/modules/file/src/Tests/DownloadTest.php b/core/modules/file/tests/src/Functional/DownloadTest.php similarity index 85% rename from core/modules/file/src/Tests/DownloadTest.php rename to core/modules/file/tests/src/Functional/DownloadTest.php index 8f85f09f96..704c9db190 100644 --- a/core/modules/file/src/Tests/DownloadTest.php +++ b/core/modules/file/tests/src/Functional/DownloadTest.php @@ -1,6 +1,9 @@ getViaScheme('public')->getDirectoryPath() . '/' . rawurlencode($file->getFilename()); $this->assertEqual($filename, $url, 'Correctly generated a URL for a created file.'); - $this->drupalHead($url); - $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the created file.'); - + $http_client = \Drupal::httpClient(); + $response = $http_client->head($url); + $this->assertEquals(200, $response->getStatusCode(), 'Confirmed that the generated URL is correct by downloading the created file.'); // Test generating a URL to a shipped file (i.e. a file that is part of // Drupal core, a module or a theme, for example a JavaScript file). $filepath = 'core/assets/vendor/jquery/jquery.min.js'; $url = file_create_url($filepath); $this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, 'Correctly generated a URL for a shipped file.'); - $this->drupalHead($url); - $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); + $response = $http_client->head($url); + $this->assertEquals(200, $response->getStatusCode(), 'Confirmed that the generated URL is correct by downloading the shipped file.'); } /** @@ -70,17 +73,19 @@ protected function doPrivateFileTransferTest() { $this->assertResponse(200, 'Correctly allowed access to a file when file_test provides headers.'); // Test that the file transferred correctly. - $this->assertEqual($contents, $this->content, 'Contents of the file are correct.'); + $this->assertSame($contents, $this->getSession()->getPage()->getContent(), 'Contents of the file are correct.'); + + $http_client = $this->getSession()->getDriver()->getClient()->getClient(); // Deny access to all downloads via a -1 header. file_test_set_return('download', -1); - $this->drupalHead($url); - $this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.'); + $response = $http_client->head($url, ['http_errors' => FALSE]); + $this->assertSame(403, $response->getStatusCode(), 'Correctly denied access to a file when file_test sets the header to -1.'); // Try non-existent file. $url = file_create_url('private://' . $this->randomMachineName()); - $this->drupalHead($url); - $this->assertResponse(404, 'Correctly returned 404 response for a non-existent file.'); + $response = $http_client->head($url, ['http_errors' => FALSE]); + $this->assertSame(404, $response->getStatusCode(), 'Correctly returned 404 response for a non-existent file.'); } /** diff --git a/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php b/core/modules/file/tests/src/Functional/FileFieldAnonymousSubmissionTest.php similarity index 99% rename from core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php rename to core/modules/file/tests/src/Functional/FileFieldAnonymousSubmissionTest.php index 66d87cd7e8..9f004abba7 100644 --- a/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldAnonymousSubmissionTest.php @@ -1,6 +1,6 @@ realpath($test_file->getFileUri()); + $edit_upload[$name] = \Drupal::service('file_system')->realpath($test_file->getFileUri()); + $this->drupalPostForm('node/' . $nid . '/edit', $edit_upload, t('Upload')); // Uncheck the display checkboxes and go to the preview. $edit[$field_name . '[0][display]'] = FALSE; $edit[$field_name . '[1][display]'] = FALSE; - $this->drupalPostForm("node/$nid/edit", $edit, t('Preview')); + $this->drupalPostForm(NULL, $edit, t('Preview')); $this->clickLink(t('Back to content editing')); $this->assertRaw($field_name . '[0][display]', 'First file appears as expected.'); $this->assertRaw($field_name . '[1][display]', 'Second file appears as expected.'); + $this->assertSession()->responseContains($field_name . '[1][description]', 'Description of second file appears as expected.'); } /** diff --git a/core/modules/file/src/Tests/FileFieldFormatterAccessTest.php b/core/modules/file/tests/src/Functional/FileFieldFormatterAccessTest.php similarity index 95% rename from core/modules/file/src/Tests/FileFieldFormatterAccessTest.php rename to core/modules/file/tests/src/Functional/FileFieldFormatterAccessTest.php index 68bb0473ef..5fbbdf14ff 100644 --- a/core/modules/file/src/Tests/FileFieldFormatterAccessTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldFormatterAccessTest.php @@ -1,6 +1,6 @@ drupalGet('rss.xml'); $uploaded_filename = str_replace('public://', '', $node_file->getFileUri()); $selector = sprintf( - 'enclosure[url="%s"][length="%s"][type="%s"]', + 'enclosure[@url="%s"][@length="%s"][@type="%s"]', file_create_url("public://$uploaded_filename", ['absolute' => TRUE]), $node_file->getSize(), $node_file->getMimeType() ); - $this->assertTrue(!empty($this->cssSelect($selector)), 'File field RSS enclosure is displayed when viewing the RSS feed.'); + $this->assertNotNull($this->getSession()->getDriver()->find('xpath', $selector), 'File field RSS enclosure is displayed when viewing the RSS feed.'); } } diff --git a/core/modules/file/src/Tests/FileFieldRevisionTest.php b/core/modules/file/tests/src/Functional/FileFieldRevisionTest.php similarity index 99% rename from core/modules/file/src/Tests/FileFieldRevisionTest.php rename to core/modules/file/tests/src/Functional/FileFieldRevisionTest.php index 04fbcb8767..6f05136cca 100644 --- a/core/modules/file/src/Tests/FileFieldRevisionTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldRevisionTest.php @@ -1,6 +1,6 @@ $this->randomMachineName(), 'revision' => (string) (int) $new_revision, @@ -156,7 +166,13 @@ public function uploadNodeFiles(array $files, $field_name, $nid_or_type, $new_re $edit[$name][] = $file_path; } } - $this->drupalPostForm("node/$nid/edit", $edit, t('Save and keep published')); + + if ($is_invalid) { + $this->drupalPostFormWithInvalidOptions("node/$nid/edit", $edit, t('Save')); + } + else { + $this->drupalPostForm("node/$nid/edit", $edit, t('Save')); + } return $nid; } @@ -172,7 +188,7 @@ public function removeNodeFile($nid, $new_revision = TRUE) { ]; $this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove')); - $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $this->drupalPostForm(NULL, $edit, t('Save')); } /** @@ -185,7 +201,7 @@ public function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) ]; $this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove')); - $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $this->drupalPostForm(NULL, $edit, t('Save')); } /** diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php similarity index 99% rename from core/modules/file/src/Tests/FileFieldValidateTest.php rename to core/modules/file/tests/src/Functional/FileFieldValidateTest.php index be96f3c587..40c7771113 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php @@ -1,6 +1,6 @@ sumUsages($file_usage->listUsage($file)); $this->assertRaw('admin/content/files/usage/' . $file->id() . '">' . $usage); - $result = $this->xpath("//td[contains(@class, 'views-field-status') and contains(text(), :value)]", [':value' => t('Temporary')]); + $result = $this->xpath("//td[contains(@class, 'views-field-status') and contains(text(), :value)]", [':value' => 'Temporary']); $this->assertEqual(1, count($result), 'Unused file marked as temporary.'); // Test file usage page. diff --git a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php b/core/modules/file/tests/src/Functional/FileOnTranslatedEntityTest.php similarity index 99% rename from core/modules/file/src/Tests/FileOnTranslatedEntityTest.php rename to core/modules/file/tests/src/Functional/FileOnTranslatedEntityTest.php index e72f6d87eb..2e22a69c35 100644 --- a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php +++ b/core/modules/file/tests/src/Functional/FileOnTranslatedEntityTest.php @@ -1,6 +1,6 @@ drupalPostForm('node/add/' . $type_name, $edit, t('Save')); $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']); $edit[$field_name . '[0][fids]'] = $node_file->id(); - $this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save')); + $this->drupalPostFormWithInvalidOptions('node/' . $new_node->id() . '/edit', $edit, t('Save')); // Make sure the form submit failed - we stayed on the edit form. $this->assertUrl('node/' . $new_node->id() . '/edit'); // Check that we got the expected constraint form error. @@ -93,7 +93,7 @@ public function testPrivateFile() { $edit = []; $edit['title[0][value]'] = $this->randomMachineName(); $edit[$field_name . '[0][fids]'] = $node_file->id(); - $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save')); + $this->drupalPostFormWithInvalidOptions('node/add/' . $type_name, $edit, t('Save')); $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']); $this->assertTrue(empty($new_node), 'Node was not created.'); $this->assertUrl('node/add/' . $type_name); @@ -144,9 +144,7 @@ public function testPrivateFile() { $this->drupalGet($file_url); $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the temporary file.'); // Close the prior connection and remove the session cookie. - $this->curlClose(); - $this->curlCookies = []; - $this->cookies = []; + $this->getSession()->reset(); $this->drupalGet($file_url); $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the temporary file.'); @@ -174,9 +172,7 @@ public function testPrivateFile() { $this->drupalGet($file_url); $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the file whose references were removed.'); // Close the prior connection and remove the session cookie. - $this->curlClose(); - $this->curlCookies = []; - $this->cookies = []; + $this->getSession()->reset(); $this->drupalGet($file_url); $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the file whose references were removed.'); @@ -197,9 +193,7 @@ public function testPrivateFile() { $this->drupalGet($file_url); $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the permanent file that is referenced by a published node.'); // Close the prior connection and remove the session cookie. - $this->curlClose(); - $this->curlCookies = []; - $this->cookies = []; + $this->getSession()->reset(); $this->drupalGet($file_url); $this->assertResponse(200, 'Confirmed that another anonymous user also has access to the permanent file that is referenced by a published node.'); @@ -224,9 +218,7 @@ public function testPrivateFile() { $this->drupalGet($file_url); $this->assertResponse(403, 'Confirmed that the anonymous uploader cannot access the permanent file when it is referenced by an unpublished node.'); // Close the prior connection and remove the session cookie. - $this->curlClose(); - $this->curlCookies = []; - $this->cookies = []; + $this->getSession()->reset(); $this->drupalGet($file_url); $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.'); } diff --git a/core/modules/file/src/Tests/FileTokenReplaceTest.php b/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php similarity index 99% rename from core/modules/file/src/Tests/FileTokenReplaceTest.php rename to core/modules/file/tests/src/Functional/FileTokenReplaceTest.php index 5bfa07b9ab..fd955913b9 100644 --- a/core/modules/file/src/Tests/FileTokenReplaceTest.php +++ b/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php @@ -1,6 +1,6 @@ 'jpeg', ]; - $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit')); + $this->drupalPostFormWithInvalidOptions('file-test/save_upload_from_form_test', $edit, t('Submit')); $this->assertResponse(200, 'Received a 200 response for posted test file.'); $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.'); diff --git a/core/modules/file/src/Tests/SaveUploadTest.php b/core/modules/file/tests/src/Functional/SaveUploadTest.php similarity index 98% rename from core/modules/file/src/Tests/SaveUploadTest.php rename to core/modules/file/tests/src/Functional/SaveUploadTest.php index a6e6f9a46a..40f560eec9 100644 --- a/core/modules/file/src/Tests/SaveUploadTest.php +++ b/core/modules/file/tests/src/Functional/SaveUploadTest.php @@ -1,8 +1,9 @@ admin = $this->drupalCreateUser([], 'admin user', TRUE); + $this->user = $this->drupalCreateUser(['access content']); + + // Create Basic page and Article node types. + $this->drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Page', + ]); + + // Create test node instance. + $this->drupalCreateNode([ + 'type' => 'page', + ]); + + $this->drupalLogin($this->admin); + } + + /** + * Tests post form request with valid values. + */ + public function testParameters() { + // Check post request with set field value. + $edit = ['title[0][value]' => 'Test1']; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Save')); + $this->assertSession()->pageTextContains('Page Test1 has been updated'); + + // Check post request without edit values. + $this->drupalGet('node/1'); + $this->assertSession()->pageTextNotContains('Page Test1 has been updated'); + $this->drupalPostFormWithInvalidOptions('node/1/edit'); + $this->assertSession()->pageTextContains('Page Test1 has been updated'); + + // Check post request without any arguments. + $this->drupalGet('node/1'); + $this->assertSession()->pageTextNotContains('Page Test1 has been updated'); + $this->drupalGet('node/1/edit'); + $this->drupalPostFormWithInvalidOptions(); + $this->assertSession()->pageTextContains('Page Test1 has been updated'); + + // Check post request with options. + $options = ['query' => ['destination' => 'node/add/page']]; + $this->drupalPostFormWithInvalidOptions('node/1/edit', [], t('Save'), $options); + $this->assertSession()->pageTextContains('Page Test1 has been updated'); + $this->assertSession()->pageTextContains('Create Page'); + + // Check post request with set header value. + try { + $headers = ['HTTP_HOST' => 'invalid-host']; + $this->drupalPostFormWithInvalidOptions('node/1/edit', [], t('Save'), [], $headers); + $this->assertSession()->pageTextContains('The requested URL was not found on this server.'); + } + catch (ConnectException $e) { + $this->assertContains('cURL error 6: Could not resolve host: invalid-host', $e->getMessage()); + } + // Check post request with with find form by ID. + $edit = ['title[0][value]' => 'Test by ID']; + $form_html_id = 'node-page-edit-form'; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, NULL, [], [], $form_html_id); + $this->assertSession()->pageTextContains('Page Test by ID has been updated'); + + // Check that raw arguments have more priority than $edit. + $edit = ['title[0][value]' => 'Test2']; + $params = ['title' => [['value' => 'Test3']]]; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Save'), [], [], NULL, $params); + $this->assertSession()->pageTextContains('Page Test3 has been updated'); + } + + /** + * Tests post form requests with non-existent fields. + */ + public function testNonExistentField() { + // Check that edit params can contain values of non-existent fields. + $edit = [ + 'title[0][value]' => 'Chell', + 'cake' => 'is a lie', + ]; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Save')); + $this->assertSession()->pageTextContains('Page Chell has been updated'); + + // Check that drupalPostForm() cannot do this request. + try { + $this->drupalPostForm('node/1/edit', $edit, t('Save')); + $this->fail('drupalPostForm() does not work with nonexistent field'); + } + catch (ElementNotFoundException $e) { + $this->assertSame('Form field with id|name|label|value "cake" not found.', $e->getMessage()); + } + } + + /** + * Tests post form requests with hidden fields. + */ + public function testHiddenField() { + // Check post request with changed hidden field. + $edit = ['changed' => 1234567890]; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Save')); + $this->assertSession()->pageTextContains('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.'); + + // Check that drupalPostForm() cannot do this request. + try { + $this->drupalPostForm('node/1/edit', $edit, t('Save')); + $this->fail('drupalPostForm() does not work with hidden field'); + } + catch (ElementNotFoundException $e) { + $this->assertSame('Form field with id|name|label|value "changed" not found.', $e->getMessage()); + } + } + + /** + * Tests post form requests with invalid fields. + */ + public function testInvalidFileField() { + $file = $this->getTestFile('text'); + $path = \Drupal::service('file_system')->realpath($file->getFileUri()); + + // Check post request with invalid options. + $edit = [ + 'files[file_test_upload][]' => [ + $path, + $path, + ], + 'allow_all_extensions' => FALSE, + 'is_image_file' => TRUE, + 'extensions' => 'jpeg', + ]; + $this->drupalPostFormWithInvalidOptions('file-test/save_upload_from_form_test', $edit, t('Submit')); + $this->assertSession()->pageTextContains('Epic upload FAIL!'); + + // Check that drupalPostForm() cannot do this request. + try { + $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit')); + $this->fail('drupalPostForm() does not work with array instead of string'); + } + catch (\PHPUnit_Framework_Error_Warning $e) { + $this->assertSame('is_readable() expects parameter 1 to be a valid path, array given', $e->getMessage()); + } + } + + /** + * Tests post form requests with multi file fields. + */ + public function testMultiFileFieldWithLimit() { + $this->createFileField('field_file_multi', 'node', 'page', ['cardinality' => 2]); + + $file = $this->getTestFile('text'); + $path = \Drupal::service('file_system')->realpath($file->getFileUri()); + + // Check post request with more files than the limit set on the field. + $edit = ['files[field_file_multi_0][]' => [$path, $path, $path]]; + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Save')); + $this->assertSession()->pageTextContains('Field field_file_multi can only hold 2 values but there were 3 uploaded.'); + + // Check that drupalPostForm() cannot do this request. + try { + $this->drupalPostForm('node/1/edit', $edit, t('Save')); + $this->fail('drupalPostForm() does not work with more files that cardinality of field'); + } + catch (ElementNotFoundException $e) { + $this->assertSame('Form field with id|name|label|value "files[field_file_multi_0][]" not found.', $e->getMessage()); + } + } + + /** + * Tests post form requests when a field already contains a file. + */ + public function testAddValuesForFieldWithExistValues() { + $field_storage_settings = [ + 'display_field' => '1', + 'display_default' => '0', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ]; + $field_settings = [ + 'description_field' => '1', + ]; + $this->createFileField('field_file_multi', 'node', 'page', $field_storage_settings, $field_settings); + // Add 1 file to field. + $test_file = $this->getTestFile('text'); + $test_file_path = \Drupal::service('file_system')->realpath($test_file->getFileUri()); + $this->uploadNodeFile($test_file, 'field_file_multi', 1); + + $assert = $this->assertSession(); + + // Edit first value. + $edit['field_file_multi[0][description]'] = 'description0'; + $edit['field_file_multi[0][display]'] = '1'; + // Add second value. + $edit['files[field_file_multi_1][]'] = $test_file_path; + $edit['field_file_multi[1][description]'] = 'description1'; + $edit['field_file_multi[1][display]'] = '1'; + + // Test 'Preview' request without pre-save values. + $this->drupalPostFormWithInvalidOptions('node/1/edit', $edit, t('Preview')); + $assert->linkExistsExact('description0'); + $assert->linkExistsExact('description1'); + + $this->clickLink(t('Back to content editing')); + + // Ensure that existing field values are changed. + $this->assertSame('description0', $assert->fieldExists('field_file_multi[0][description]')->getValue()); + $this->assertSame('1', $assert->fieldExists('field_file_multi[0][display]')->getAttribute('value')); + // Ensure that non-existing field values are lost. + $assert->fieldNotExists('field_file_multi[1][description]'); + $assert->fieldNotExists('field_file_multi[1][display]'); + + // Test 'Upload' request. + $this->drupalPostFormWithInvalidOptions(NULL, $edit, t('Upload')); + $this->assertSame('description0', $assert->fieldExists('field_file_multi[0][description]')->getValue()); + $this->assertSame('description1', $assert->fieldExists('field_file_multi[1][description]')->getValue()); + + // Test 'Preview' request with pre-save values (after upload new file). + $edit['field_file_multi[0][description]'] = 'description0-1'; + $edit['field_file_multi[1][description]'] = 'description1-1'; + $this->drupalPostFormWithInvalidOptions(NULL, $edit, t('Preview')); + $assert->linkExistsExact('description0-1'); + $assert->linkExistsExact('description1-1'); + $this->clickLink(t('Back to content editing')); + // Ensure that both values are changed. + $this->assertSame('description0-1', $assert->fieldExists('field_file_multi[0][description]')->getValue()); + $this->assertSame('description1-1', $assert->fieldExists('field_file_multi[1][description]')->getValue()); + } + +} diff --git a/core/tests/Drupal/Tests/Traits/DrupalPostFormWithInvalidOptionsTrait.php b/core/tests/Drupal/Tests/Traits/DrupalPostFormWithInvalidOptionsTrait.php new file mode 100644 index 0000000000..a4339154b3 --- /dev/null +++ b/core/tests/Drupal/Tests/Traits/DrupalPostFormWithInvalidOptionsTrait.php @@ -0,0 +1,228 @@ + $parameters + * 'form_multipart' -> $files + * Example: + * [ + * 'form_params' => ['title' => [['value' => 'Test']]], + * 'form_multipart' => [['files[field]' => 'path']], + * ]; + * Use this to avoid processing, such as $edit parameter. + */ + protected function drupalPostFormWithInvalidOptions($path = NULL, array $edit = [], $submit = NULL, array $options = [], array $headers = [], $form_html_id = NULL, $raw_params = [], $raw_files=[]) { + $form = $this->getCrawlerForm($path, $options, $submit, $form_html_id); + $form_params = $form->getPhpValues(); + $form_files = $form->getPhpFiles(); + + $edit_params = $this->prepareEditValues($edit); + $edit_files = []; + if (isset($edit_params['files'])) { + $edit_files['files'] = $edit_params['files']; + unset($edit_params['files']); + } + + $params = array_replace_recursive($form_params, $edit_params, $raw_params); + $files = array_replace_recursive($form_files, $edit_files, $raw_files); + + $method = $form->getMethod() ?: 'POST'; + $action = $form->getUri() ?: $this->getUrl(); + + $this->drupalRequest($method, $action, $params, $files, $headers); + } + + /** + * Perform a request with update browser page. + * + * For send the form use: + * drupalPostForm() with valid data + * drupalPostFormWithInvalidOptions() with invalid data + * For navigation use drupalGet(). + * For more flexible request use http client. + */ + protected function drupalRequest($method, $action, $params = [], $multipart = [], $headers = []) { + $this->prepareRequest(); + $client = $this->getSession()->getDriver()->getClient(); + $client->request($method, $action, $params, $multipart, $headers); + $this->refreshVariables(); + // Check if there are any meta refresh redirects (like Batch API pages). + if ($this->checkForMetaRefresh()) { + // We are finished with all meta refresh redirects. + $this->metaRefreshCount = 0; + } + } + + /** + * Gets the crawler form by html ID or button name on the page. + * + * @return \Symfony\Component\DomCrawler\Form + * Crawler form. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + */ + protected function getCrawlerForm($path = NULL, $options = NULL, $submit = NULL, $form_html_id = NULL) { + if ($path !== NULL) { + $this->drupalGet($path, $options); + } + + if (is_object($submit)) { + $submit = (string) $submit; + } + + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + if ($form_html_id !== NULL) { + $form_element = $assert->elementExists('xpath', "//form[@id='$form_html_id']"); + $submit_button = ($submit !== NULL) ? $form_element->findField($submit) : $form_element->find('css', '[type="submit"]'); + } + elseif ($submit !== NULL) { + $submit_button = $assert->buttonExists($submit); + $form_element = $assert->elementExists('xpath', './ancestor::form', $submit_button); + } + else { + $form_element = $assert->elementExists('xpath', '//form'); + $submit_button = $form_element->find('css', '[type="submit"]'); + } + + $element = $submit_button ?: $form_element; + $crawler = $this->getSession()->getDriver()->getClient()->getCrawler(); + $form = $crawler->filterXPath($element->getXpath())->form(); + + return $form; + } + + /** + * Prepare edit values to request. + * + * @return array + * An array with prepared values. + */ + protected function prepareEditValues(array $edit = []) { + $edit = $this->castSafeStrings($edit); + $edit = $this->convertMultipleValues($edit); + $edit = $this->getPhpValues($edit); + return $edit; + } + + /** + * Convert item with array values to items with string values. + * + * Example: + * [ + * 'key1' => 'value1', + * 'key2[]' => ['value2', 'value3'] + * ] + * will be converted to + * [ + * 'key1' => 'value1', + * 'key2[0]' => 'value2', + * 'key2[1]' => 'value3', + * ] + * + * @param array $values + * List of files to prepare, each item as 'field_name' => value. + * + * @return array + * Prepared array of values after converting. + */ + protected function convertMultipleValues(array $values) { + foreach ($values as $key => $item) { + if (is_array($item)) { + foreach ($item as $i => $value) { + $new_key = str_replace('[]', "[$i]", $key); + $values[$new_key] = $value; + } + unset($values[$key]); + } + } + return $values; + } + + /** + * Gets the field values as PHP. + * + * Copied from \Symfony\Component\DomCrawler\Form::getPhpValues() + * + * This method converts fields with the array notation. + * Example: + * [ + * 'foo[0][bar]' => 'value', + * ] + * will be converted to + * [ + * 'foo' => [ + * [ + * 'bar' => 'value', + * ], + * ], + * ] + * + * @param array $post + * An array of field values with the string notation. + * + * @return array + * An array of field values with the array notation. + */ + protected function getPhpValues($post) { + $values = []; + foreach ($post as $name => $value) { + $qs = http_build_query([$name => $value], '', '&'); + if (!empty($qs)) { + parse_str($qs, $expandedValue); + $values = array_replace_recursive($values, $expandedValue); + } + } + return $values; + } + +}