diff --git a/core/drupalci.yml b/core/drupalci.yml
index 2085b9737b..e91e84a13e 100644
--- a/core/drupalci.yml
+++ b/core/drupalci.yml
@@ -3,48 +3,11 @@
 # https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing
 build:
   assessment:
-    validate_codebase:
-      phplint:
-      csslint:
-        halt-on-fail: false
-      eslint:
-        # A test must pass eslinting standards check in order to continue processing.
-        halt-on-fail: false
-      phpcs:
-        # phpcs will use core's specified version of Coder.
-        sniff-all-files: false
-        halt-on-fail: false
     testing:
-      # run_tests task is executed several times in order of performance speeds.
-      # halt-on-fail can be set on the run_tests tasks in order to fail fast.
-      # suppress-deprecations is false in order to be alerted to usages of
-      # deprecated code.
-      run_tests.phpunit:
-        types: 'PHPUnit-Unit'
-        testgroups: '--all'
-        suppress-deprecations: false
-        halt-on-fail: false
-      run_tests.kernel:
-        types: 'PHPUnit-Kernel'
-        testgroups: '--all'
-        suppress-deprecations: false
-        halt-on-fail: false
-      run_tests.simpletest:
-         types: 'Simpletest'
-         testgroups: '--all'
-         suppress-deprecations: false
-         halt-on-fail: false
-      run_tests.functional:
-        types: 'PHPUnit-Functional'
-        testgroups: '--all'
-        suppress-deprecations: false
-        halt-on-fail: false
       run_tests.javascript:
         concurrency: 15
         types: 'PHPUnit-FunctionalJavascript'
-        testgroups: '--all'
+        testgroups: '--class "Drupal\Tests\media_library\FunctionalJavascript\UploadFail"'
         suppress-deprecations: false
         halt-on-fail: false
-      # Run nightwatch testing.
-      # @see https://www.drupal.org/project/drupal/issues/2869825
-      nightwatchjs:
+        repeat: 30
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/UploadFail.php b/core/modules/media_library/tests/src/FunctionalJavascript/UploadFail.php
new file mode 100644
index 0000000000..11c80fc9ca
--- /dev/null
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/UploadFail.php
@@ -0,0 +1,242 @@
+<?php
+
+namespace Drupal\Tests\media_library\FunctionalJavascript;
+
+use Behat\Mink\Exception\ElementNotFoundException;
+use Drupal\Core\File\Exception\FileNotExistsException;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\media\Entity\Media;
+use Drupal\Tests\TestFileCreationTrait;
+
+/**
+ * Checks if uploads fail after several attempts.
+ *
+ * @group media_library
+ */
+class UploadFail extends WebDriverTestBase {
+
+  use TestFileCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'media_library_test',
+    'node',
+  ];
+
+  /**
+   * Tests that repeated uploads to Media Library continue working.
+   */
+  public function testManyConsecutiveUploads() {
+    $page = $this->getSession()->getPage();
+
+    foreach ($this->getTestFiles('image') as $image) {
+      $extension = pathinfo($image->filename, PATHINFO_EXTENSION);
+      if ($extension === 'jpg') {
+        $jpg_image = $image;
+      }
+    }
+
+    if (!isset($jpg_image)) {
+      $this->fail('Expected test files not present.');
+    }
+
+    // Create a user that can only add media of type four.
+    $user = $this->drupalCreateUser([
+      'access administration pages',
+      'access content',
+      'create basic_page content',
+      'create type_one media',
+      'create type_four media',
+      'view media',
+    ]);
+    $this->drupalLogin($user);
+
+    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+    $file_system = $this->container->get('file_system');
+    // Visit a node create page and open the media library.
+    $this->drupalGet('node/add/basic_page');
+
+
+    $count = 0;
+    // Upload then remove an image 100x to see if/when it fails.
+    while ($count < 100) {
+      $this->assertElementExistsAfterWait('css', '.media-library-open-button[name^="field_twin_media"]', 10000, "Twin media open button not found on iteration $count")->click();
+      $this->assertElementExistsAfterWait('css', '.media-library-menu', 10000, "Media library menu did not appear on iteration $count");
+      $this->waitForText('Add or select media', 10000, "Text 'Add or select media' not appear on iteration $count");
+      $this->clickTypeTab('Four');
+      $jpg_uri_2 = $this->copiedFileUri($jpg_image->uri);
+      $this->waitForFieldExists('Add files', 10000, "Did not find 'Add files' on iteration $count");
+      $page->attachFileToField('Add files', $file_system->realpath($jpg_uri_2));
+      $this->waitForText('Alternative text', 10000, "Alternative text field not found on iteration $count");
+      $field = $page->findField('Alternative text');
+      $this->assertTrue(!empty($field), "Alt text label found but not the field on iteration $count");
+      $field->setValue('alt text!');
+      $hidden_field_locator = '[name="media[0][fields][field_media_test_image][0][width]"]';
+      $hidden_field = $page->find('css', $hidden_field_locator);
+      $this->assertNotEmpty($hidden_field, "Width field not found on iteration $count");
+      $this->assertNotEmpty($hidden_field->getAttribute('value'), "Width field empty on iteration $count");
+      $this->assertElementExistsAfterWait('css', '.ui-dialog-buttonpane', 10000, "Save and select button not found on iteration $count")->pressButton('Save and select');
+      $this->waitForNoText('Save and select', 10000, "Save and select visible when it shouldn't be on iteration $count");
+      $this->assertElementExistsAfterWait('css', '.ui-dialog-buttonpane', 10000, "Insert selected button not found on iteration $count")->pressButton('Insert selected');
+      $this->waitForText($file_system->basename($jpg_uri_2), 10000, "$jpg_uri_2 not found on iteration $count");
+      $this->waitForText('One media item remaining', 10000, "'One media item remaining' not found on iteration $count");
+      $this->assertElementExistsAfterWait('css', '[name="field_twin_media-0-media-library-remove-button"]', 10000, "Media remove button not found on iteration $count")->click();
+      $this->waitForText('2 media items remaining', 10000, "'2 media items remaining' not found on iteration $count");
+      $count++;
+      usleep(10000);
+    }
+
+  }
+
+  /**
+   * Asserts that text does not appear on page after a wait.
+   *
+   * @todo replace with whatever gets added in
+   *   https://www.drupal.org/node/3061852
+   */
+  protected function waitForNoText($text, $timeout = 10000, $message = 'text still present on page') {
+    $page = $this->getSession()->getPage();
+    $result = $page->waitFor($timeout / 1000, function () use ($page, $text) {
+      $actual = preg_replace('/\s+/u', ' ', $page->getText());
+      $regex = '/' . preg_quote($text, '/') . '/ui';
+      return (bool) !preg_match($regex, $actual);
+    });
+    if (empty($result)) {
+      $this->htmlOutput();
+      $filename = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '-waitNoText.jpg';
+      $this->createScreenshot($filename);
+    }
+    $this->assertNotEmpty($result, $message);
+  }
+
+  /**
+   * Asserts that text appears on page after a wait.
+   *
+   * @todo replace with whatever gets added in
+   *   https://www.drupal.org/node/3061852
+   */
+  protected function waitForText($text, $timeout = 10000, $message = 'text not found') {
+    $assert_session = $this->assertSession();
+    $result = $assert_session->waitForText($text, $timeout);
+    if (empty($result)) {
+      $this->htmlOutput();
+      $filename = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '-waitForText.jpg';
+      $this->createScreenshot($filename);
+    }
+    $this->assertNotEmpty($result, $message);
+  }
+
+  /**
+   * Waits for the specified selector and returns it if not empty.
+   *
+   * @param string $selector
+   *   The selector engine name. See ElementInterface::findAll() for the
+   *   supported selectors.
+   * @param string|array $locator
+   *   The selector locator.
+   * @param int $timeout
+   *   Timeout in milliseconds, defaults to 10000.
+   * @param string $message
+   *   Error message.
+   *
+   * @return \Behat\Mink\Element\NodeElement
+   *   The page element node if found. If not found, the test fails.
+   *
+   * @todo replace with whatever gets added in
+   *   https://www.drupal.org/node/3061852
+   */
+  protected function assertElementExistsAfterWait($selector, $locator, $timeout = 10000, $message = 'element not found after wait') {
+    $assert_session = $this->assertSession();
+    $element = $assert_session->waitForElement($selector, $locator, $timeout);
+    if (empty($element)) {
+      $this->htmlOutput();
+      $filename = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '-asertElExisAftWait.jpg';      $this->createScreenshot($filename);
+    }
+    $this->assertNotEmpty($element, $message);
+    return $element;
+  }
+
+  /**
+   * Clicks a media type tab and waits for it to appear.
+   */
+  protected function clickTypeTab($type) {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+    $lowercase_type = strtolower($type);
+
+    try {
+      $assert_session->elementExists('css', ".media-library-menu-type-$lowercase_type .active-tab");
+      // There is nothing to do as the type is already active.
+      return;
+    }
+    catch (ElementNotFoundException $e) {
+    }
+
+    $page->clickLink($type);
+    $this->assertElementExistsAfterWait('css', ".media-library-menu-type-$lowercase_type .active-tab");
+    $assert_session->waitForElementVisible('css', "[action='/admin/content/media-widget/type_$lowercase_type']");
+  }
+
+  /**
+   * Checks for the existence of a field on page after wait.
+   *
+   * @param string $field
+   *   The field to find.
+   * @param int $timeout
+   *   (Optional) Timeout in milliseconds, defaults to 10000.
+   * @param string $message
+   *   Error message.
+   *
+   * @return \Behat\Mink\Element\NodeElement|null
+   *   The element if found, otherwise null.
+   *
+   * @todo replace with whatever gets added in
+   *   https://www.drupal.org/node/3061852
+   */
+  protected function waitForFieldExists($field, $timeout = 10000, $message = 'did not find field') {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $start = microtime(TRUE);
+    $end = $start + ($timeout / 1000);
+    do {
+      $node = $page->findField($field);
+      if (!is_null($node)) {
+        return $node;
+      }
+      usleep(100000);
+    } while (microtime(TRUE) < $end);
+
+    $this->htmlOutput();
+    $filename = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '-waitFieExis.jpg';    $this->createScreenshot($filename);
+
+    $this->assertNotEmpty($node, $message);
+  }
+
+  /**
+   * Confirms the integrity of a copied file.
+   *
+   * @param string $uri
+   *   Uri of source file.
+   *
+   * @return string
+   *   The Uri of the copied file.
+   *
+   * @throws FileNotExistsException
+   *   If the copy does not match.
+   */
+  protected function copiedFileUri($uri) {
+    $file_system = $this->container->get('file_system');
+
+    $new_uri = $file_system->copy($uri, 'public://');
+    $original_hash = sha1_file($uri);
+    $new_hash = sha1_file($new_uri);
+    if ($original_hash === $new_hash) {
+      return $new_uri;
+    }
+    throw new FileNotExistsException("The copy of file $uri was not identical");
+  }
+
+}
