From 22a56817e0ad425fde7b50ae4d4909d79f268ad5 Mon Sep 17 00:00:00 2001 From: catch Date: Fri, 2 Dec 2022 10:53:59 +0000 Subject: [PATCH] =?UTF-8?q?Issue=20#3294914=20by=20Spokje,=20quietone,=20b?= =?UTF-8?q?brala,=20longwave,=20G=C3=A1bor=20Hojtsy,=20benjifisher,=20dww,?= =?UTF-8?q?=20xjm,=20rkoller:=20Create=20dedicated=20error=20section=20for?= =?UTF-8?q?=20missing=20removed=20core=20modules/themes=20on=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/modules/system/system.install | 127 ++++++- .../UpdateSystem/UpdateScriptTest.php | 342 ++++++++++++++---- 2 files changed, 382 insertions(+), 87 deletions(-) diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 36dea438509..4a99b7a5d48 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -28,6 +28,30 @@ use Drupal\Core\Url; use GuzzleHttp\Exception\TransferException; use Symfony\Component\HttpFoundation\Request; +// cspell:ignore quickedit + +/** + * An array of machine names of modules that were removed from Drupal core. + */ +const DRUPAL_CORE_REMOVED_MODULE_LIST = [ + 'aggregator' => 'Aggregator', + 'ckeditor' => 'CKEditor', + 'color' => 'Color', + 'hal' => 'HAL', + 'quickedit' => 'Quick Edit', + 'rdf' => 'RDF', +]; + +/** + * An array of machine names of themes that were removed from Drupal core. + */ +const DRUPAL_CORE_REMOVED_THEME_LIST = [ + 'bartik' => 'Bartik', + 'classy' => 'Classy', + 'seven' => 'Seven', + 'stable' => 'Stable', +]; + /** * Implements hook_requirements(). */ @@ -989,13 +1013,16 @@ function system_requirements($phase) { // Display an error if a newly introduced dependency in a module is not resolved. if ($phase === 'update' || $phase === 'runtime') { - $create_extension_incompatibility_list = function ($extension_names, $description, $title) { + $create_extension_incompatibility_list = function (array $extension_names, PluralTranslatableMarkup $description, PluralTranslatableMarkup $title, TranslatableMarkup|string $message = '', TranslatableMarkup|string $additional_description = '') { + if ($message === '') { + $message = new TranslatableMarkup('Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.', [':url' => 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates']); + } // Use an inline twig template to: - // - Concatenate two MarkupInterface objects and preserve safeness. + // - Concatenate MarkupInterface objects and preserve safeness. // - Use the item_list theme for the extension list. $template = [ '#type' => 'inline_template', - '#template' => '{{ description }}{{ extensions }}', + '#template' => '{{ description }}{{ extensions }}{{ additional_description }}
', '#context' => [ 'extensions' => [ '#theme' => 'item_list', @@ -1004,15 +1031,13 @@ function system_requirements($phase) { ]; $template['#context']['extensions']['#items'] = $extension_names; $template['#context']['description'] = $description; + $template['#context']['additional_description'] = $additional_description; return [ 'title' => $title, 'value' => [ 'list' => $template, 'handbook_link' => [ - '#markup' => t( - 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.', - [':url' => 'https://www.drupal.org/docs/8/update/troubleshooting-database-updates'] - ), + '#markup' => $message, ], ], 'severity' => REQUIREMENT_ERROR, @@ -1134,13 +1159,89 @@ function system_requirements($phase) { ); } - // Look for invalid modules. $extension_config = \Drupal::configFactory()->get('core.extension'); - $is_missing_extension = function ($extension_name) use (&$module_extension_list) { - return !$module_extension_list->exists($extension_name); - }; - $invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_extension); + // Look for removed core modules. + $is_removed_module = function ($extension_name) use ($module_extension_list) { + return !$module_extension_list->exists($extension_name) + && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_MODULE_LIST); + }; + $removed_modules = array_filter(array_keys($extension_config->get('module')), $is_removed_module); + if (!empty($removed_modules)) { + $list = []; + foreach ($removed_modules as $removed_module) { + $list[] = t('@module', [ + ':url' => "https://www.drupal.org/project/$removed_module", + '@module' => DRUPAL_CORE_REMOVED_MODULE_LIST[$removed_module], + ]); + } + $requirements['removed_module'] = $create_extension_incompatibility_list( + $list, + new PluralTranslatableMarkup( + count($removed_modules), + 'You must add the following contributed module and reload this page.', + 'You must add the following contributed modules and reload this page.' + ), + new PluralTranslatableMarkup( + count($removed_modules), + 'Removed core module', + 'Removed core modules' + ), + new TranslatableMarkup( + 'For more information read the documentation on deprecated modules.', + [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'] + ), + new PluralTranslatableMarkup( + count($removed_modules), + 'This module is installed on your site but is no longer provided by Core.', + 'These modules are installed on your site but are no longer provided by Core.' + ), + ); + } + + // Look for removed core themes. + $is_removed_theme = function ($extension_name) use ($theme_extension_list) { + return !$theme_extension_list->exists($extension_name) + && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_THEME_LIST); + }; + $removed_themes = array_filter(array_keys($extension_config->get('theme')), $is_removed_theme); + if (!empty($removed_themes)) { + $list = []; + foreach ($removed_themes as $removed_theme) { + $list[] = t('@theme', [ + ':url' => "https://www.drupal.org/project/$removed_theme", + '@theme' => DRUPAL_CORE_REMOVED_THEME_LIST[$removed_theme], + ]); + } + $requirements['removed_theme'] = $create_extension_incompatibility_list( + $list, + new PluralTranslatableMarkup( + count($removed_themes), + 'You must add the following contributed theme and reload this page.', + 'You must add the following contributed themes and reload this page.' + ), + new PluralTranslatableMarkup( + count($removed_themes), + 'Removed core theme', + 'Removed core themes' + ), + new TranslatableMarkup( + 'For more information read the documentation on deprecated themes.', + [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-themes'] + ), + new PluralTranslatableMarkup( + count($removed_themes), + 'This theme is installed on your site but is no longer provided by Core.', + 'These themes are installed on your site but are no longer provided by Core.' + ), + ); + } + + // Look for missing modules. + $is_missing_module = function ($extension_name) use ($module_extension_list) { + return !$module_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_MODULE_LIST), TRUE); + }; + $invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_module); if (!empty($invalid_modules)) { $requirements['invalid_module'] = $create_extension_incompatibility_list( @@ -1160,7 +1261,7 @@ function system_requirements($phase) { // Look for invalid themes. $is_missing_theme = function ($extension_name) use (&$theme_extension_list) { - return !$theme_extension_list->exists($extension_name); + return !$theme_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_THEME_LIST), TRUE); }; $invalid_themes = array_filter(array_keys($extension_config->get('theme')), $is_missing_theme); if (!empty($invalid_themes)) { diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php index b1ff588ea5a..9a71aba4f2f 100644 --- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php @@ -224,7 +224,7 @@ class UpdateScriptTest extends BrowserTestBase { * * @dataProvider providerExtensionCompatibilityChange */ - public function testExtensionCompatibilityChange(array $correct_info, array $breaking_info, $expected_error) { + public function testExtensionCompatibilityChange(array $correct_info, array $breaking_info, string $expected_error): void { $extension_type = $correct_info['type']; $this->drupalLogin( $this->drupalCreateUser( @@ -236,8 +236,9 @@ class UpdateScriptTest extends BrowserTestBase { ) ); - $extension_machine_name = "changing_extension"; - $extension_name = "$extension_machine_name name"; + $extension_machine_names = ['changing_extension']; + $extension_name = "$extension_machine_names[0] name"; + $test_error_urls = ['https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates']; $test_error_text = "Incompatible $extension_type " . $expected_error @@ -247,25 +248,28 @@ class UpdateScriptTest extends BrowserTestBase { if ($extension_type === 'theme') { $base_info['base theme'] = FALSE; } - $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$extension_type}s/$extension_machine_name"; - $file_path = "$folder_path/$extension_machine_name.info.yml"; + $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$extension_type}s/$extension_machine_names[0]"; + $file_path = "$folder_path/$extension_machine_names[0].info.yml"; mkdir($folder_path, 0777, TRUE); file_put_contents($file_path, Yaml::encode($base_info + $correct_info)); - $this->enableExtension($extension_type, $extension_machine_name, $extension_name); - $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + $this->enableExtensions($extension_type, $extension_machine_names, [$extension_name]); + $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); // If there are no requirements warnings or errors, we expect to be able to // go through the update process uninterrupted. - $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name); + $this->drupalGet($this->statusReportUrl); + $this->assertUpdateWithNoErrors([$test_error_text], $extension_type, $extension_machine_names); // Change the values in the info.yml and confirm updating is not possible. file_put_contents($file_path, Yaml::encode($base_info + $breaking_info)); - $this->assertErrorOnUpdate($test_error_text, $extension_type, $extension_machine_name); + $this->drupalGet($this->statusReportUrl); + $this->assertErrorOnUpdates([$test_error_text], $extension_type, $extension_machine_names, $test_error_urls); // Fix the values in the info.yml file and confirm updating is possible // again. file_put_contents($file_path, Yaml::encode($base_info + $correct_info)); - $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name); + $this->drupalGet($this->statusReportUrl); + $this->assertUpdateWithNoErrors([$test_error_text], $extension_type, $extension_machine_names); } /** @@ -329,53 +333,165 @@ class UpdateScriptTest extends BrowserTestBase { /** * Tests that a missing extension prevents updates. * - * @param string $extension_type - * The extension type, either 'module' or 'theme'. + * @param array $core + * An array keyed by 'module' and 'theme' where each sub array contains + * a list of extension machine names. + * @param array $contrib + * An array keyed by 'module' and 'theme' where each sub array contains + * a list of extension machine names. * * @dataProvider providerMissingExtension */ - public function testMissingExtension($extension_type) { + public function testMissingExtension(array $core, array $contrib): void { $this->drupalLogin( $this->drupalCreateUser( [ 'administer software updates', 'administer site configuration', - $extension_type === 'module' ? 'administer modules' : 'administer themes', + 'administer modules', + 'administer themes', ] ) ); - $extension_machine_name = "disappearing_$extension_type"; - $extension_name = 'The magically disappearing extension'; - $test_error_text = "Missing or invalid $extension_type " - . "The following $extension_type is marked as installed in the core.extension configuration, but it is missing:" - . $extension_machine_name - . static::HANDBOOK_MESSAGE; - $extension_info = [ - 'name' => $extension_name, - 'type' => $extension_type, + + $all_extensions_info = []; + $file_paths = []; + $test_error_texts = []; + $test_error_urls = []; + $extension_base_info = [ + 'version' => 'VERSION', 'core_version_requirement' => '^8 || ^9 || ^10', ]; - if ($extension_type === 'theme') { - $extension_info['base theme'] = FALSE; + + // For each core extension create and error of info.yml information and + // the expected error message. + foreach ($core as $type => $extensions) { + $removed_list = []; + $error_url = 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'; + $extension_base_info += ['package' => 'Core']; + if ($type === 'module') { + $removed_core_list = \DRUPAL_CORE_REMOVED_MODULE_LIST; + } + else { + $removed_core_list = \DRUPAL_CORE_REMOVED_THEME_LIST; + } + + foreach ($extensions as $extension) { + $extension_info = $extension_base_info + + [ + 'name' => "The magically disappearing core $type $extension", + 'type' => $type, + ]; + if ($type === 'theme') { + $extension_info['base theme'] = FALSE; + } + $all_extensions_info[$extension] = $extension_info; + $removed_list[] = $removed_core_list[$extension]; + } + + // Create the requirements test message. + if (!empty($extensions)) { + $handbook_message = "For more information read the documentation on deprecated {$type}s."; + if (count($removed_list) === 1) { + $test_error_texts[$type][] = "Removed core {$type} " + . "You must add the following contributed $type and reload this page." + . implode($removed_list) + . "This $type is installed on your site but is no longer provided by Core." + . $handbook_message; + } + else { + $test_error_texts[$type][] = "Removed core {$type}s " + . "You must add the following contributed {$type}s and reload this page." + . implode($removed_list) + . "These {$type}s are installed on your site but are no longer provided by Core." + . $handbook_message; + } + $test_error_urls[$type][] = $error_url; + } + } + + // For each contrib extension create and error of info.yml information and + // the expected error message. + foreach ($contrib as $type => $extensions) { + unset($extension_base_info['package']); + $handbook_message = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.'; + $error_url = 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates'; + foreach ($extensions as $extension) { + $extension_info = $extension_base_info + + [ + 'name' => "The magically disappearing contrib $type $extension", + 'type' => $type, + ]; + if ($type === 'theme') { + $extension_info['base theme'] = FALSE; + } + $all_extensions_info[$extension] = $extension_info; + } + + // Create the requirements test message. + if (!empty($extensions)) { + if (count($extensions) === 1) { + $test_error_texts[$type][] = "Missing or invalid {$type} " + . "The following {$type} is marked as installed in the core.extension configuration, but it is missing:" + . implode($extensions) + . $handbook_message; + } + else { + $test_error_texts[$type][] = "Missing or invalid {$type}s " + . "The following {$type}s are marked as installed in the core.extension configuration, but they are missing:" + . implode($extensions) + . $handbook_message; + } + $test_error_urls[$type][] = $error_url; + } + } + + // Create the info.yml files for each extension. + foreach ($all_extensions_info as $machine_name => $extension_info) { + $type = $extension_info['type']; + $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$type}s/contrib/$machine_name"; + $file_path = "$folder_path/$machine_name.info.yml"; + mkdir($folder_path, 0777, TRUE); + file_put_contents($file_path, Yaml::encode($extension_info)); + $file_paths[$machine_name] = $file_path; + } + + // Enable all the extensions. + foreach ($all_extensions_info as $machine_name => $extension_info) { + $extension_machine_names = [$machine_name]; + $extension_names = [$extension_info['name']]; + $this->enableExtensions($extension_info['type'], $extension_machine_names, $extension_names); } - $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$extension_type}s/$extension_machine_name"; - $file_path = "$folder_path/$extension_machine_name.info.yml"; - mkdir($folder_path, 0777, TRUE); - file_put_contents($file_path, Yaml::encode($extension_info)); - $this->enableExtension($extension_type, $extension_machine_name, $extension_name); // If there are no requirements warnings or errors, we expect to be able to // go through the update process uninterrupted. - $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name); + $this->drupalGet($this->statusReportUrl); + $types = ['module', 'theme']; + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); + } - // Delete the info.yml and confirm updates are prevented. - unlink($file_path); - $this->assertErrorOnUpdate($test_error_text, $extension_type, $extension_machine_name); + // Delete the info.yml(s) and confirm updates are prevented. + foreach ($file_paths as $file_path) { + unlink($file_path); + } + $this->drupalGet($this->statusReportUrl); + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertErrorOnUpdates($test_error_texts[$type], $type, $all, $test_error_urls[$type]); + } - // Add the info.yml file back and confirm we are able to go through the + // Add the info.yml file(s) back and confirm we are able to go through the // update process uninterrupted. - file_put_contents($file_path, Yaml::encode($extension_info)); - $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name); + foreach ($all_extensions_info as $machine_name => $extension_info) { + file_put_contents($file_paths[$machine_name], Yaml::encode($extension_info)); + } + $this->drupalGet($this->statusReportUrl); + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); + } } /** @@ -427,12 +543,44 @@ class UpdateScriptTest extends BrowserTestBase { } /** - * Data provider for testMissingExtension(). + * Data provider for ::testMissingExtension(). + * + * @return array[] + * Set of testcases to pass to the test method. */ - public function providerMissingExtension() { + public function providerMissingExtension(): array { return [ - 'module' => ['module'], - 'theme' => ['theme'], + 'core only' => [ + 'core' => [ + 'module' => ['aggregator'], + 'theme' => ['seven'], + ], + 'contrib' => [ + 'module' => [], + 'theme' => [], + ], + ], + 'contrib only' => [ + 'core' => [ + 'module' => [], + 'theme' => [], + ], + 'contrib' => [ + 'module' => ['module'], + 'theme' => ['theme'], + ], + ], + 'core and contrib' => + [ + 'core' => [ + 'module' => ['aggregator', 'rdf'], + 'theme' => ['seven'], + ], + 'contrib' => [ + 'module' => ['module_a', 'module_b'], + 'theme' => ['theme_a', 'theme_b'], + ], + ], ]; } @@ -441,22 +589,55 @@ class UpdateScriptTest extends BrowserTestBase { * * @param string $extension_type * The extension type. - * @param string $extension_machine_name - * The extension machine name. - * @param string $extension_name - * The extension name. + * @param array $extension_machine_names + * An array of the extension machine names. + * @param array $extension_names + * An array of extension names. */ - protected function enableExtension($extension_type, $extension_machine_name, $extension_name) { + protected function enableExtensions(string $extension_type, array $extension_machine_names, array $extension_names): void { if ($extension_type === 'module') { - $edit = [ - "modules[$extension_machine_name][enable]" => $extension_machine_name, - ]; + $edit = []; + foreach ($extension_machine_names as $extension_machine_name) { + $edit["modules[$extension_machine_name][enable]"] = $extension_machine_name; + } $this->drupalGet('admin/modules'); $this->submitForm($edit, 'Install'); } elseif ($extension_type === 'theme') { $this->drupalGet('admin/appearance'); - $this->click("a[title~=\"$extension_name\"]"); + foreach ($extension_names as $extension_name) { + $this->click("a[title~=\"$extension_name\"]"); + } + } + } + + /** + * Enables extensions the UI. + * + * @param array $extension_info + * An array of extension information arrays. The array is keyed by 'module' + * and 'theme'. + */ + protected function enableMissingExtensions(array $extension_info): void { + $edit = []; + foreach ($extension_info as $info) { + if ($info['type'] === 'module') { + $machine_name = $info['machine_name']; + $edit["modules[$machine_name][enable]"] = $machine_name; + } + if (!empty($edit)) { + $this->drupalGet('admin/modules'); + $this->submitForm($edit, 'Install'); + } + } + + if (isset($extension_info['theme'])) { + $this->drupalGet('admin/appearance'); + foreach ($extension_info as $info) { + if ($info['type' === 'theme']) { + $this->click('a[title~="' . $info['name'] . '"]'); + } + } } } @@ -779,70 +960,83 @@ class UpdateScriptTest extends BrowserTestBase { * * @param string $extension_type * The extension type, either 'module' or 'theme'. - * @param string $extension_machine_name - * The extension machine name. + * @param array $extension_machine_names + * An array of the extension machine names. * * @internal */ - protected function assertInstalledExtensionConfig(string $extension_type, string $extension_machine_name): void { + protected function assertInstalledExtensionsConfig(string $extension_type, array $extension_machine_names): void { $extension_config = $this->container->get('config.factory')->getEditable('core.extension'); - $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name")); + foreach ($extension_machine_names as $extension_machine_name) { + $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name")); + } } /** - * Asserts a particular error is not shown on update and status report pages. + * Asserts particular errors are not shown on update and status report pages. * - * @param string $unexpected_error_text - * The error text that should not be shown. + * @param array $unexpected_error_texts + * An array of the error texts that should not be shown. * @param string $extension_type * The extension type, either 'module' or 'theme'. - * @param string $extension_machine_name - * The extension machine name. + * @param array $extension_machine_names + * An array of the extension machine names. * * @throws \Behat\Mink\Exception\ResponseTextException * * @internal */ - protected function assertUpdateWithNoError(string $unexpected_error_text, string $extension_type, string $extension_machine_name): void { + protected function assertUpdateWithNoErrors(array $unexpected_error_texts, string $extension_type, array $extension_machine_names): void { $assert_session = $this->assertSession(); - $this->drupalGet($this->statusReportUrl); - $this->assertSession()->pageTextNotContains($unexpected_error_text); + foreach ($unexpected_error_texts as $unexpected_error_text) { + $this->assertSession()->pageTextNotContains($unexpected_error_text); + } $this->drupalGet($this->updateUrl, ['external' => TRUE]); - $this->assertSession()->pageTextNotContains($unexpected_error_text); + foreach ($unexpected_error_texts as $unexpected_error_text) { + $this->assertSession()->pageTextNotContains($unexpected_error_text); + } $this->updateRequirementsProblem(); $this->clickLink('Continue'); $assert_session->pageTextContains('No pending updates.'); - $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); } /** - * Asserts an error is shown on the update and status report pages. + * Asserts errors are shown on the update and status report pages. * - * @param string $expected_error_text - * The expected error text. + * @param array $expected_error_texts + * The expected error texts. * @param string $extension_type * The extension type, either 'module' or 'theme'. - * @param string $extension_machine_name - * The extension machine name. + * @param array $extension_machine_names + * The extension machine names. + * @param array $test_error_urls + * The URLs in the error texts. * * @throws \Behat\Mink\Exception\ExpectationException * @throws \Behat\Mink\Exception\ResponseTextException * * @internal */ - protected function assertErrorOnUpdate(string $expected_error_text, string $extension_type, string $extension_machine_name): void { + protected function assertErrorOnUpdates(array $expected_error_texts, string $extension_type, array $extension_machine_names, array $test_error_urls): void { $assert_session = $this->assertSession(); - $this->drupalGet($this->statusReportUrl); - $this->assertSession()->pageTextContains($expected_error_text); + foreach ($expected_error_texts as $expected_error_text) { + $this->assertSession()->pageTextContains($expected_error_text); + } + foreach ($test_error_urls as $test_error_url) { + $this->assertSession()->linkByHrefExists($test_error_url); + } // Reload the update page to ensure the extension with the breaking values // has not been uninstalled or otherwise affected. for ($reload = 0; $reload <= 1; $reload++) { $this->drupalGet($this->updateUrl, ['external' => TRUE]); - $this->assertSession()->pageTextContains($expected_error_text); + foreach ($expected_error_texts as $expected_error_text) { + $this->assertSession()->pageTextContains($expected_error_text); + } $assert_session->linkNotExists('Continue'); } - $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); } }