From 59ee80c47486351a98ae63cad01485a4e34c2d1e Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Tue, 21 Jun 2022 16:09:51 +0000
Subject: [PATCH] Issue #3264849 by tedbow, phenaproxima: Show next minor or
 current minor updates in Update form

---
 automatic_updates.module                      |  12 +-
 src/Form/UpdaterForm.php                      | 269 ++++++++++++------
 tests/src/Build/CoreUpdateTest.php            |   2 +-
 .../Functional/AvailableUpdatesReportTest.php |   4 +-
 tests/src/Functional/UpdaterFormTest.php      | 119 ++++++--
 5 files changed, 295 insertions(+), 111 deletions(-)

diff --git a/automatic_updates.module b/automatic_updates.module
index 17b39d9db6..bfb47fef89 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -197,10 +197,16 @@ function automatic_updates_preprocess_update_project_status(array &$variables) {
     return;
   }
   $updater = \Drupal::service('automatic_updates.updater');
+  $supported_target_versions = [];
   /** @var \Drupal\automatic_updates\ReleaseChooser $recommender */
   $recommender = \Drupal::service('automatic_updates.release_chooser');
   try {
-    $supported_update_release = $recommender->getLatestInInstalledMinor($updater) ?? $recommender->getLatestInNextMinor($updater);
+    if ($installed_minor_release = $recommender->getLatestInInstalledMinor($updater)) {
+      $supported_target_versions[] = $installed_minor_release->getVersion();
+    }
+    if ($next_minor_release = $recommender->getLatestInNextMinor($updater)) {
+      $supported_target_versions[] = $next_minor_release->getVersion();
+    }
   }
   catch (RuntimeException $exception) {
     // If for some reason we are not able to get the update recommendations
@@ -211,7 +217,7 @@ function automatic_updates_preprocess_update_project_status(array &$variables) {
   $variables['#attached']['library'][] = 'automatic_updates/update_status';
 
   $status = &$variables['status'];
-  if ($supported_update_release && $status['label']) {
+  if ($supported_target_versions && $status['label']) {
     $status['label'] = [
       '#markup' => t(
         '@label <a href=":update-form">Update now</a>', [
@@ -226,7 +232,7 @@ function automatic_updates_preprocess_update_project_status(array &$variables) {
   }
   foreach ($variables['versions'] as &$themed_version) {
     $version_info = &$themed_version['#version'];
-    if ($supported_update_release && $version_info['version'] === $supported_update_release->getVersion()) {
+    if ($supported_target_versions && in_array($version_info['version'], $supported_target_versions, TRUE)) {
       $version_info['download_link'] = Url::fromRoute('automatic_updates.report_update')->setAbsolute()->toString();
     }
     else {
diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php
index 2074fe1153..95c8e96398 100644
--- a/src/Form/UpdaterForm.php
+++ b/src/Form/UpdaterForm.php
@@ -8,13 +8,15 @@ use Drupal\automatic_updates\ProjectInfo;
 use Drupal\automatic_updates\ReleaseChooser;
 use Drupal\automatic_updates\Updater;
 use Drupal\automatic_updates\Validation\ReadinessTrait;
+use Drupal\automatic_updates_9_3_shim\ProjectRelease;
 use Drupal\Core\Batch\BatchBuilder;
+use Drupal\Core\Extension\ExtensionVersion;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Drupal\package_manager\Exception\StageException;
 use Drupal\package_manager\Exception\StageOwnershipException;
@@ -28,9 +30,9 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  * Defines a form to update Drupal core.
  *
  * @internal
- *   Form classes are internal.
+ *   Form classes are internal and the form structure may change at any time.
  */
-class UpdaterForm extends FormBase {
+final class UpdaterForm extends FormBase {
 
   use ReadinessTrait {
     formatResult as traitFormatResult;
@@ -154,10 +156,8 @@ class UpdaterForm extends FormBase {
       //   one release on the form. First, try to show the latest release in the
       //   currently installed minor. Failing that, try to show the latest
       //   release in the next minor.
-      $recommended_release = $this->releaseChooser->getLatestInInstalledMinor($this->updater);
-      if (!$recommended_release) {
-        $recommended_release = $this->releaseChooser->getLatestInNextMinor($this->updater);
-      }
+      $installed_minor_release = $this->releaseChooser->getLatestInInstalledMinor($this->updater);
+      $next_minor_release = $this->releaseChooser->getLatestInNextMinor($this->updater);
     }
     catch (\RuntimeException $e) {
       $form['message'] = [
@@ -170,7 +170,7 @@ class UpdaterForm extends FormBase {
     $form['#attached']['library'][] = 'update/drupal.update.admin';
 
     $project = $project_info->getProjectInfo();
-    if ($recommended_release === NULL) {
+    if ($installed_minor_release === NULL && $next_minor_release === NULL) {
       if ($project['status'] === UpdateManagerInterface::CURRENT) {
         $this->messenger()->addMessage($this->t('No update available'));
       }
@@ -186,92 +186,91 @@ class UpdaterForm extends FormBase {
       return $form;
     }
 
-    $form['target_version'] = [
-      '#type' => 'value',
-      '#value' => [
-        'drupal' => $recommended_release->getVersion(),
-      ],
-    ];
-
     if (empty($project['title']) || empty($project['link'])) {
       throw new \UnexpectedValueException('Expected project data to have a title and link.');
     }
-    $title = Link::fromTextAndUrl($project['title'], Url::fromUri($project['link']))->toRenderable();
+
+    $form['title'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'h2',
+      '#value' => $this->t(
+        'Update <a href=":url">Drupal core</a>',
+        [':url' => $project['link']],
+      ),
+    ];
+    $form['current'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'p',
+      '#value' => $this->t(
+        'Currently installed: @version (@status)',
+        [
+          '@version' => $project_info->getInstalledVersion(),
+          '@status' => $this->getUpdateStatus($project['status']),
+        ]
+      ),
+    ];
 
     switch ($project['status']) {
       case UpdateManagerInterface::NOT_SECURE:
       case UpdateManagerInterface::REVOKED:
-        $title['#suffix'] = ' ' . $this->t('(Security update)');
+        $release_status = $this->t('Security update');
         $type = 'update-security';
         break;
 
-      case UpdateManagerInterface::NOT_SUPPORTED:
-        $title['#suffix'] = ' ' . $this->t('(Unsupported)');
-        $type = 'unsupported';
-        break;
-
       default:
-        $type = 'recommended';
-        break;
+        $release_status = $this->t('Available update');
+        $type = 'update-recommended';
     }
-
-    // Create an entry for this project.
-    $entry = [
-      'title' => [
-        'data' => $title,
-      ],
-      'installed_version' => $project_info->getInstalledVersion(),
-      'recommended_version' => [
-        'data' => [
-          // @todo Is an inline template the right tool here? Is there an Update
-          // module template we should use instead?
-          '#type' => 'inline_template',
-          '#template' => '{{ release_version }} (<a href="{{ release_link }}" title="{{ project_title }}">{{ release_notes }}</a>)',
-          '#context' => [
-            'release_version' => $recommended_release->getVersion(),
-            'release_link' => $recommended_release->getReleaseUrl(),
-            'project_title' => $this->t('Release notes for @project_title', ['@project_title' => $project['title']]),
-            'release_notes' => $this->t('Release notes'),
-          ],
-        ],
-      ],
-    ];
-
-    $form['projects'] = [
-      '#type' => 'table',
-      '#header' => [
-        'title' => [
-          'data' => $this->t('Name'),
-          'class' => ['update-project-name'],
-        ],
-        'installed_version' => $this->t('Installed version'),
-        'recommended_version' => [
-          'data' => $this->t('Recommended version'),
-        ],
-      ],
-      '#rows' => [
-        'drupal' => [
-          'class' => "update-$type",
-          'data' => $entry,
-        ],
-      ],
-    ];
-
-    $form['backup'] = [
-      '#markup' => $this->t('It\'s a good idea to <a href=":url">back up your database</a> before you begin.', [':url' => 'https://www.drupal.org/node/22281#s-backing-up-the-database']),
-    ];
-
     if ($form_state->getUserInput()) {
       $results = [];
     }
     else {
-      $event = new ReadinessCheckEvent($this->updater, [
-        'drupal' => $recommended_release->getVersion(),
-      ]);
+      $event = new ReadinessCheckEvent($this->updater);
       $this->eventDispatcher->dispatch($event);
       $results = $event->getResults();
     }
     $this->displayResults($results, $this->messenger(), $this->renderer);
+    $create_update_buttons = !$stage_exists && $this->getOverallSeverity($results) !== SystemManager::REQUIREMENT_ERROR;
+    if ($installed_minor_release) {
+      $installed_version = ExtensionVersion::createFromVersionString($project_info->getInstalledVersion());
+      $form['installed_minor'] = $this->createReleaseTable(
+        $installed_minor_release,
+        $release_status,
+        $this->t('Latest version of Drupal @major.@minor (currently installed):', [
+          '@major' => $installed_version->getMajorVersion(),
+          '@minor' => $installed_version->getMinorVersion(),
+        ]),
+        $type,
+        $create_update_buttons,
+        // Any update in the current minor should be the primary update.
+        TRUE,
+      );
+    }
+    if ($next_minor_release) {
+      // If there is no update in the current minor make the button for the next
+      // minor primary unless the project status is 'CURRENT' or 'NOT_CURRENT'.
+      // 'NOT_CURRENT' does not denote that installed version is not a valid
+      // only that there is newer version available.
+      $is_primary = !$installed_minor_release && !($project['status'] === UpdateManagerInterface::CURRENT || $project['status'] === UpdateManagerInterface::NOT_CURRENT);
+      $next_minor_version = ExtensionVersion::createFromVersionString($next_minor_release->getVersion());
+      // @todo Add documentation to explain what is different about a minor
+      //   update in https://www.drupal.org/i/3291730.
+      $form['next_minor'] = $this->createReleaseTable(
+        $next_minor_release,
+        $installed_minor_release ? $this->t('Minor update') : $release_status,
+        $this->t('Latest version of Drupal @major.@minor (next minor):', [
+          '@major' => $next_minor_version->getMajorVersion(),
+          '@minor' => $next_minor_version->getMinorVersion(),
+        ]),
+        $installed_minor_release ? 'update-optional' : $type,
+        $create_update_buttons,
+        $is_primary
+      );
+    }
+
+    $form['backup'] = [
+      '#markup' => $this->t('It\'s a good idea to <a href=":url">back up your database</a> before you begin.', [':url' => 'https://www.drupal.org/node/22281#s-backing-up-the-database']),
+    ];
 
     if ($stage_exists) {
       // If the form has been submitted, do not display this error message
@@ -286,13 +285,6 @@ class UpdaterForm extends FormBase {
         '#submit' => ['::deleteExistingUpdate'],
       ];
     }
-    // If there were no errors, allow the user to proceed with the update.
-    elseif ($this->getOverallSeverity($results) !== SystemManager::REQUIREMENT_ERROR) {
-      $form['actions']['submit'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Update'),
-      ];
-    }
     $form['actions']['#type'] = 'actions';
 
     return $form;
@@ -315,12 +307,13 @@ class UpdaterForm extends FormBase {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
     $batch = (new BatchBuilder())
       ->setTitle($this->t('Downloading updates'))
       ->setInitMessage($this->t('Preparing to download updates'))
       ->addOperation(
         [BatchProcessor::class, 'begin'],
-        [$form_state->getValue('target_version')]
+        [['drupal' => $button['#target_version']]]
       )
       ->addOperation([BatchProcessor::class, 'stage'])
       ->setFinishCallback([BatchProcessor::class, 'finishStage'])
@@ -345,4 +338,116 @@ class UpdaterForm extends FormBase {
     return $this->traitFormatResult($result);
   }
 
+  /**
+   * Gets the update table for a specific release.
+   *
+   * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease $release
+   *   The project release.
+   * @param string $release_description
+   *   The release description.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $caption
+   *   The table caption, if any.
+   * @param string $update_type
+   *   The update type.
+   * @param bool $create_update_button
+   *   Whether the update button should be created.
+   * @param bool $is_primary
+   *   Whether update button should be a primary button.
+   *
+   * @return array
+   *   The table render array.
+   */
+  private function createReleaseTable(ProjectRelease $release, string $release_description, ?TranslatableMarkup $caption, string $update_type, bool $create_update_button, bool $is_primary): array {
+    $release_section = ['#type' => 'container'];
+    $release_section['table'] = [
+      '#type' => 'table',
+      '#description' => $this->t('more'),
+      '#header' => [
+        'title' => [
+          'data' => $this->t('Update type'),
+          'class' => ['update-project-name'],
+        ],
+        'target_version' => [
+          'data' => $this->t('Version'),
+        ],
+      ],
+    ];
+    if ($caption) {
+      $release_section['table']['#caption'] = $caption;
+    }
+    $release_section['table'][$release->getVersion()] = [
+      'title' => [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $release_description,
+      ],
+      'target_version' => [
+        'data' => [
+          // @todo Is an inline template the right tool here? Is there an Update
+          // module template we should use instead?
+          '#type' => 'inline_template',
+          '#template' => '{{ release_version }} (<a href="{{ release_link }}" title="{{ project_title }}">{{ release_notes }}</a>)',
+          '#context' => [
+            'release_version' => $release->getVersion(),
+            'release_link' => $release->getReleaseUrl(),
+            'project_title' => $this->t(
+              'Release notes for @project_title @version',
+              [
+                '@project_title' => 'Drupal core',
+                '@version' => $release->getVersion(),
+              ]
+            ),
+            'release_notes' => $this->t('Release notes'),
+          ],
+        ],
+      ],
+      '#attributes' => ['class' => ['update-' . $update_type]],
+    ];
+    if ($create_update_button) {
+      $release_section['submit'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Update to @version', ['@version' => $release->getVersion()]),
+        '#target_version' => $release->getVersion(),
+      ];
+      if ($is_primary) {
+        $release_section['submit']['#button_type'] = 'primary';
+      }
+    }
+    $release_section['#suffix'] = '<br />';
+    return $release_section;
+
+  }
+
+  /**
+   * Gets the human-readable project status.
+   *
+   * @param int $status
+   *   The project status, one of \Drupal\update\UpdateManagerInterface
+   *   constants.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The human-readable status.
+   */
+  private function getUpdateStatus(int $status): TranslatableMarkup {
+    switch ($status) {
+      case UpdateManagerInterface::NOT_SECURE:
+        return $this->t('Security update required!');
+
+      case UpdateManagerInterface::REVOKED:
+        return $this->t('Revoked!');
+
+      case UpdateManagerInterface::NOT_SUPPORTED:
+        return $this->t('Not supported!');
+
+      case UpdateManagerInterface::NOT_CURRENT:
+        return $this->t('Update available');
+
+      case UpdateManagerInterface::CURRENT:
+        return $this->t('Up to date');
+
+      default:
+        return $this->t('Unknown status');
+    }
+  }
+
 }
diff --git a/tests/src/Build/CoreUpdateTest.php b/tests/src/Build/CoreUpdateTest.php
index 7e32c651fb..61e18bbe85 100644
--- a/tests/src/Build/CoreUpdateTest.php
+++ b/tests/src/Build/CoreUpdateTest.php
@@ -109,7 +109,7 @@ class CoreUpdateTest extends UpdateTestBase {
     $session->reload();
 
     $assert_session->pageTextNotContains('There is a security update available for your version of Drupal.');
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->waitForBatchJob();
     $assert_session->pageTextContains('Ready to update');
     $page->pressButton('Continue');
diff --git a/tests/src/Functional/AvailableUpdatesReportTest.php b/tests/src/Functional/AvailableUpdatesReportTest.php
index 08ae40ee7a..2052512d4b 100644
--- a/tests/src/Functional/AvailableUpdatesReportTest.php
+++ b/tests/src/Functional/AvailableUpdatesReportTest.php
@@ -68,7 +68,7 @@ class AvailableUpdatesReportTest extends AutomaticUpdatesFunctionalTestBase {
 
     $assert->elementAttributeContains('named', ['link', 'Update now'], 'href', $form_url);
     // Releases that will available on the form should link to the form.
-    $this->assertVersionLink('9.8.2', 'http://example.com/drupal-9-8-2-release');
+    $this->assertVersionLink('9.8.2', $form_url);
     $this->assertVersionLink('9.7.1', $form_url);
     // Releases that will not be available in the form should link to the
     // project release page.
@@ -78,7 +78,7 @@ class AvailableUpdatesReportTest extends AutomaticUpdatesFunctionalTestBase {
     $this->checkForUpdates();
     $assert->pageTextContains('Update available Update now');
     $assert->elementAttributeContains('named', ['link', 'Update now'], 'href', $form_url);
-    $this->assertVersionLink('9.8.2', 'http://example.com/drupal-9-8-2-release');
+    $this->assertVersionLink('9.8.2', $form_url);
   }
 
   /**
diff --git a/tests/src/Functional/UpdaterFormTest.php b/tests/src/Functional/UpdaterFormTest.php
index 7bbb2254bc..dc485c4fd9 100644
--- a/tests/src/Functional/UpdaterFormTest.php
+++ b/tests/src/Functional/UpdaterFormTest.php
@@ -117,7 +117,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
 
     $assert_session = $this->assertSession();
     $assert_session->pageTextContains('No update available');
-    $assert_session->buttonNotExists('Update');
+    $this->assertNoUpdateButtons();
   }
 
   /**
@@ -131,6 +131,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
    * @dataProvider providerTableLooksCorrect
    */
   public function testTableLooksCorrect(string $access_page): void {
+    $page = $this->getSession()->getPage();
     $this->drupalPlaceBlock('local_tasks_block', ['primary' => TRUE]);
     $assert_session = $this->assertSession();
     $this->setCoreVersion('9.8.0');
@@ -148,17 +149,82 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
       $this->clickLink('Available updates');
     }
     $this->clickLink('Update');
+
+    // Check the form when there is an updates in the next minor only.
+    $assert_session->pageTextContainsOnce('Currently installed: 9.8.0 (Security update required!)');
+    $this->checkReleaseTable('#edit-installed-minor', '.update-update-security', '9.8.1', TRUE, 'Latest version of Drupal 9.8 (currently installed):');
+    $assert_session->elementNotExists('css', '#edit-next-minor');
+
+    // Check the form when there is an updates in the next minor only.
+    $this->config('automatic_updates.settings')->set('allow_core_minor_updates', TRUE)->save();
+    $this->setCoreVersion('9.7.0');
+    $page->clickLink('Check manually');
+    $this->checkForMetaRefresh();
+    $this->checkReleaseTable('#edit-next-minor', '.update-update-recommended', '9.8.1', TRUE, 'Latest version of Drupal 9.8 (next minor):');
+    $assert_session->pageTextContainsOnce('Currently installed: 9.7.0 (Not supported!)');
+    $assert_session->elementNotExists('css', '#edit-installed-minor');
+
+    // Check the form when there are updates in the current and next minors but
+    // the site does not support minor updates.
+    $this->config('automatic_updates.settings')->set('allow_core_minor_updates', FALSE)->save();
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.2.xml');
+    $page->clickLink('Check manually');
+    $this->checkForMetaRefresh();
+    $assert_session->pageTextContainsOnce('Currently installed: 9.7.0 (Update available)');
+    $this->checkReleaseTable('#edit-installed-minor', '.update-update-recommended', '9.7.1', TRUE, 'Latest version of Drupal 9.7 (currently installed):');
+    $assert_session->elementNotExists('css', '#edit-next-minor');
+
+    // Check that if minor updates are enabled the update in the next minor will
+    // be visible.
+    $this->config('automatic_updates.settings')->set('allow_core_minor_updates', TRUE)->save();
+    $this->getSession()->reload();
+    $this->checkReleaseTable('#edit-installed-minor', '.update-update-recommended', '9.7.1', TRUE, 'Latest version of Drupal 9.7 (currently installed):');
+    $this->checkReleaseTable('#edit-next-minor', '.update-update-optional', '9.8.2', FALSE, 'Latest version of Drupal 9.8 (next minor):');
+
+    $this->setCoreVersion('9.7.1');
+    $page->clickLink('Check manually');
+    $this->checkForMetaRefresh();
+    $assert_session->pageTextContainsOnce('Currently installed: 9.7.1 (Update available)');
+    $assert_session->elementNotExists('css', '#edit-installed-minor');
+    $this->checkReleaseTable('#edit-next-minor', '.update-update-recommended', '9.8.2', FALSE, 'Latest version of Drupal 9.8 (next minor):');
+
+    $this->assertUpdateStagedTimes(0);
+  }
+
+  /**
+   * Checks the table for a release on the form.
+   *
+   * @param string $container_locator
+   *   The CSS locator for the element with contains the table.
+   * @param string $row_class
+   *   The row class for the update.
+   * @param string $version
+   *   The release version number.
+   * @param bool $is_primary
+   *   Whether update button should be a primary button.
+   * @param string|null $table_caption
+   *   The table caption or NULL if none expected.
+   */
+  private function checkReleaseTable(string $container_locator, string $row_class, string $version, bool $is_primary, ?string $table_caption = NULL): void {
+    $assert_session = $this->assertSession();
     $assert_session->pageTextNotContains('There is a security update available for your version of Drupal.');
-    $cells = $assert_session->elementExists('css', '#edit-projects .update-update-security')
+    $assert_session->linkExists('Drupal core');
+    $container = $assert_session->elementExists('css', $container_locator);
+    if ($table_caption) {
+      $this->assertSame($table_caption, $assert_session->elementExists('css', 'caption', $container)->getText());
+    }
+    else {
+      $assert_session->elementNotExists('css', 'caption', $container);
+    }
+
+    $cells = $assert_session->elementExists('css', $row_class, $container)
       ->findAll('css', 'td');
-    $this->assertCount(3, $cells);
-    $assert_session->elementExists('named', ['link', 'Drupal'], $cells[0]);
-    $this->assertSame('9.8.0', $cells[1]->getText());
-    $this->assertSame('9.8.1 (Release notes)', $cells[2]->getText());
-    $release_notes = $assert_session->elementExists('named', ['link', 'Release notes'], $cells[2]);
-    $this->assertSame('Release notes for Drupal', $release_notes->getAttribute('title'));
-    $assert_session->buttonExists('Update');
-    $this->assertUpdateStagedTimes(0);
+    $this->assertCount(2, $cells);
+    $this->assertSame("$version (Release notes)", $cells[1]->getText());
+    $release_notes = $assert_session->elementExists('named', ['link', 'Release notes'], $cells[1]);
+    $this->assertSame("Release notes for Drupal core $version", $release_notes->getAttribute('title'));
+    $button = $assert_session->buttonExists("Update to $version", $container);
+    $this->assertSame($is_primary, $button->hasClass('button--primary'));
   }
 
   /**
@@ -186,7 +252,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     // If a validator raises an error during readiness checking, the form should
     // not have a submit button.
     $this->drupalGet('/admin/modules/automatic-update');
-    $assert_session->buttonNotExists('Update');
+    $this->assertNoUpdateButtons();
     // Since this is an administrative page, the error message should be visible
     // thanks to automatic_updates_page_top(). The readiness checks were re-run
     // during the form build, which means the new error should be cached and
@@ -206,7 +272,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $assert_session->pageTextNotContains(static::$errorsExplanation);
     $assert_session->pageTextNotContains(static::$warningsExplanation);
     $assert_session->pageTextNotContains($cached_message);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(0);
     $assert_session->pageTextContainsOnce('An error has occurred.');
@@ -225,7 +291,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     // If a validator flags an error, but doesn't throw, the update should still
     // be halted.
     TestSubscriber1::setTestResult($expected_results, PreCreateEvent::class);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(0);
     $assert_session->pageTextContainsOnce('An error has occurred.');
@@ -254,7 +320,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $assert_session->pageTextContains('Updates were found, but they must be performed manually. See the list of available updates for more information.');
     $this->clickLink('the list of available updates');
     $assert_session->elementExists('css', 'table.update');
-    $assert_session->buttonNotExists('Update');
+    $this->assertNoUpdateButtons();
   }
 
   /**
@@ -271,7 +337,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
 
     $this->drupalGet('/admin/modules/automatic-update');
     FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1');
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
 
@@ -290,7 +356,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $assert_session->pageTextContains($cancelled_message);
     $assert_session->pageTextNotContains($conflict_message);
     // Ensure we can start another update after deleting the existing one.
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
 
     // Confirm we are on the confirmation page.
@@ -304,12 +370,12 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $this->drupalLogin($account);
     $this->drupalGet('/admin/reports/updates/automatic-update');
     $assert_session->pageTextContains($conflict_message);
-    $assert_session->buttonNotExists('Update');
+    $this->assertNoUpdateButtons();
     // We should be able to delete the previous update, then start a new one.
     $page->pressButton('Delete existing update');
     $assert_session->pageTextContains('Staged update deleted');
     $assert_session->pageTextNotContains($conflict_message);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateReady('9.8.1');
 
@@ -355,7 +421,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $this->createTestValidationResults();
     $results = $this->testResults['checker_1']['1 error'];
     TestSubscriber1::setTestResult($results, PreApplyEvent::class);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateReady('9.8.1');
     $page->pressButton('Continue');
@@ -409,7 +475,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $assert_session = $this->assertSession();
     $assert_session->pageTextContains(reset($messages));
     $assert_session->pageTextNotContains($cached_message);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
     $this->assertUpdateReady('9.8.1');
@@ -506,7 +572,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $this->drupalGet($update_form_url);
     FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1');
     $assert_session->pageTextNotContains($cached_message);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
     $this->assertUpdateReady('9.8.1');
@@ -534,7 +600,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $page = $this->getSession()->getPage();
     $this->drupalGet('/admin/modules/automatic-update');
     FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1');
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
     $this->assertUpdateReady('9.8.1');
@@ -574,7 +640,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     TestSubscriber1::setException($error, PostRequireEvent::class);
     $assert_session->pageTextNotContains(static::$errorsExplanation);
     $assert_session->pageTextNotContains(static::$warningsExplanation);
-    $page->pressButton('Update');
+    $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
     $assert_session->pageTextContainsOnce('An error has occurred.');
@@ -610,4 +676,11 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     return $message;
   }
 
+  /**
+   * Asserts that no update buttons exist.
+   */
+  private function assertNoUpdateButtons(): void {
+    $this->assertSession()->elementNotExists('css', "input[value*='Update']");
+  }
+
 }
-- 
GitLab