summaryrefslogtreecommitdiffstatshomepage
path: root/core/tests/Drupal
diff options
context:
space:
mode:
Diffstat (limited to 'core/tests/Drupal')
-rw-r--r--core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php8
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php14
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php14
-rw-r--r--core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php4
-rw-r--r--core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php9
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php (renamed from core/tests/Drupal/Tests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php)2
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php11
-rw-r--r--core/tests/Drupal/BuildTests/Framework/BuildTestBase.php4
-rw-r--r--core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php17
-rw-r--r--core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php13
-rw-r--r--core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php6
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxCallbacksTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormImageButtonTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxMaintenanceModeTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php5
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/FocusFirstCommandTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php8
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/AjaxWaitTest.php8
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/BrowserWithJavascriptTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Components/ComponentRenderTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/CsrfTokenRaceTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffViewsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/Form/FormGroupingElementsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php6
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/Session/SessionTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogDeprecationsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php8
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php8
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsSuppressionTest.php5
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/JavascriptGetDrupalSettingsTest.php5
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/MachineName/MachineNameTransliterationTest.php11
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php2
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Tests/DrupalSelenium2DriverTest.php8
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebAssertTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroBlockFilterTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroEntityDisplayTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroMenuUiJavascriptTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroModalDisplayTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroPasswordConfirmWidgetTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroTableDragTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsBulkOperationsTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsUiTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php4
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php4
-rw-r--r--core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php1
-rw-r--r--core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php23
-rw-r--r--core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php36
-rw-r--r--core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php37
-rw-r--r--core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php55
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php10
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php38
-rw-r--r--core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php1
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php22
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php46
-rw-r--r--core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php31
-rw-r--r--core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php7
-rw-r--r--core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php15
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php10
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php8
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php22
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php1278
-rw-r--r--core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php109
-rw-r--r--core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php4
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php85
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php1036
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php980
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php86
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php75
-rw-r--r--core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php8
-rw-r--r--core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php9
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php80
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php30
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php6
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php27
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php25
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php85
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php3
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php71
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php108
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php43
-rw-r--r--core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php5
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php74
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php44
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBase.php2
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php2
-rw-r--r--core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php2
-rw-r--r--core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js77
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php8
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php2
-rw-r--r--core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php34
-rw-r--r--core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php6
-rw-r--r--core/tests/Drupal/Tests/BrowserTestBase.php16
-rw-r--r--core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php85
-rw-r--r--core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php57
-rw-r--r--core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php38
-rw-r--r--core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php161
-rw-r--r--core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php36
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php130
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css1
-rw-r--r--core/tests/Drupal/Tests/Core/Database/ConnectionTest.php6
-rw-r--r--core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php4
-rw-r--r--core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php28
-rw-r--r--core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php59
-rw-r--r--core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php88
-rw-r--r--core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php26
-rw-r--r--core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php80
-rw-r--r--core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php104
-rw-r--r--core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php51
-rw-r--r--core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc5
-rw-r--r--core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php7
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php47
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php205
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php24
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php26
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php60
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php5
-rw-r--r--core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Render/RendererTest.php64
-rw-r--r--core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php43
-rw-r--r--core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php6
-rw-r--r--core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php18
-rw-r--r--core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php40
-rw-r--r--core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php371
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php23
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php3
-rw-r--r--core/tests/Drupal/Tests/RequirementsPageTrait.php10
-rw-r--r--core/tests/Drupal/Tests/SchemaCheckTestTrait.php4
-rw-r--r--core/tests/Drupal/Tests/UnitTestCase.php29
-rw-r--r--core/tests/Drupal/Tests/UpdatePathTestTrait.php6
160 files changed, 5743 insertions, 1564 deletions
diff --git a/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php b/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
index ee21d5bedf52..e943a257ee26 100644
--- a/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
+++ b/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
@@ -8,6 +8,8 @@ use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\Core\Command\GenerateTheme;
use Drupal\Core\Serialization\Yaml;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful;
use Symfony\Component\Process\PhpExecutableFinder;
@@ -15,11 +17,9 @@ use Symfony\Component\Process\Process;
/**
* Tests the generate-theme commands.
- *
- * @requires extension pdo_sqlite
- *
- * @group Command
*/
+#[Group('Command')]
+#[RequiresPhpExtension('pdo_sqlite')]
class GenerateThemeTest extends QuickStartTestBase {
/**
diff --git a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
index 0042c55e1604..8be768ed98c8 100644
--- a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
@@ -6,16 +6,17 @@ namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\CoversNothing;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Finder\Finder;
/**
* Try to install dependencies per component, using Composer.
- *
- * @group Composer
- * @group Component
- *
- * @coversNothing
*/
+#[CoversNothing]
+#[Group('Composer')]
+#[Group('Component')]
class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
/**
@@ -41,9 +42,8 @@ class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
/**
* Test whether components' composer.json can be installed in isolation.
- *
- * @dataProvider provideComponentPaths
*/
+ #[DataProvider('provideComponentPaths')]
public function testComponentComposerJson(string $component_path): void {
// Only copy the components. Copy all of them because some of them depend on
// each other.
diff --git a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
index 269edf126bef..b50aa973c59e 100644
--- a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
@@ -6,15 +6,16 @@ namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\CoversNothing;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Demonstrate that the Component generator responds to release tagging.
- *
- * @group Composer
- * @group Component
- *
- * @coversNothing
*/
+#[CoversNothing]
+#[Group('Composer')]
+#[Group('Component')]
class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
/**
@@ -37,9 +38,8 @@ class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
/**
* Validate release tagging and regeneration of dependencies.
- *
- * @dataProvider providerVersionConstraint
*/
+ #[DataProvider('providerVersionConstraint')]
public function testReleaseTagging(string $tag, string $constraint): void {
$this->copyCodebase();
$drupal_root = $this->getWorkspaceDirectory();
diff --git a/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php b/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
index 7bdba7e4f4f2..5c563016f52d 100644
--- a/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
+++ b/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
+use PHPUnit\Framework\Attributes\CoversNothing;
use Symfony\Component\Finder\Finder;
/**
* Base class for Composer build tests.
- *
- * @coversNothing
*/
+#[CoversNothing]
abstract class ComposerBuildTestBase extends BuildTestBase {
/**
diff --git a/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php b/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
index 661d159bf806..50c343354e18 100644
--- a/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
@@ -6,10 +6,13 @@ namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\ComposerIntegrationTrait;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
- * @group Composer
+ * Tests.
*/
+#[Group('Composer')]
class ComposerValidateTest extends BuildTestBase {
use ComposerIntegrationTrait;
@@ -23,9 +26,7 @@ class ComposerValidateTest extends BuildTestBase {
return $data;
}
- /**
- * @dataProvider provideComposerJson
- */
+ #[DataProvider('provideComposerJson')]
public function testValidateComposer($path): void {
$this->executeCommand('composer validate --strict --no-check-all ' . $path);
$this->assertCommandSuccessful();
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php b/core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php
index b74349256892..1397a78cf694 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Drupal\Tests\Composer\Plugin\Unpack\Functional;
+namespace Drupal\BuildTests\Composer\Plugin\Unpack\Functional;
use Composer\InstalledVersions;
use Composer\Util\Filesystem;
diff --git a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
index 6051afb1ae9b..f7155bbac018 100644
--- a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
@@ -8,6 +8,8 @@ use Composer\Json\JsonFile;
use Composer\Semver\VersionParser;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Demonstrate that Composer project templates can be built as patched.
@@ -21,9 +23,8 @@ use Drupal\Composer\Composer;
*
* This is because Composer only uses the packages.json file to resolve the
* project template and not any other dependencies.
- *
- * @group Template
*/
+#[Group('Template')]
class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
/**
@@ -31,7 +32,7 @@ class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
*
* @see https://getcomposer.org/doc/04-schema.md#minimum-stability
*/
- protected const MINIMUM_STABILITY = 'beta';
+ protected const MINIMUM_STABILITY = 'stable';
/**
* The order of stability strings from least stable to most stable.
@@ -171,9 +172,7 @@ class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
}
}
- /**
- * @dataProvider provideTemplateCreateProject
- */
+ #[DataProvider('provideTemplateCreateProject')]
public function testTemplateCreateProject($project, $package_dir, $docroot_dir): void {
// Make a working COMPOSER_HOME directory for setting global composer config
$composer_home = $this->getWorkspaceDirectory() . '/composer-home';
diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
index e92c75468843..0e0912109e83 100644
--- a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -333,8 +333,8 @@ abstract class BuildTestBase extends TestCase {
public function executeCommand($command_line, $working_dir = NULL) {
$this->commandProcess = Process::fromShellCommandline($command_line);
$this->commandProcess->setWorkingDirectory($this->getWorkingPath($working_dir))
- ->setTimeout(300)
- ->setIdleTimeout(300);
+ ->setTimeout(360)
+ ->setIdleTimeout(360);
$this->commandProcess->run();
return $this->commandProcess;
}
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
index 6e29abe18ee0..7f97eb530274 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
@@ -6,13 +6,16 @@ namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\BuildTestBase;
use org\bovigo\vfs\vfsStream;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
- * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
- * @group Build
+ * Tests Drupal\BuildTests\Framework\BuildTestBase.
*/
+#[CoversClass(BuildTestBase::class)]
+#[Group('Build')]
class BuildTestTest extends BuildTestBase {
/**
@@ -34,7 +37,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebase(): void {
$test_directory = 'copied_codebase';
@@ -56,7 +59,7 @@ class BuildTestTest extends BuildTestBase {
/**
* Ensure we're not copying directories we wish to exclude.
*
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebaseExclude(): void {
// Create a virtual file system containing items that should be
@@ -129,7 +132,7 @@ class BuildTestTest extends BuildTestBase {
/**
* Tests copying codebase when Drupal and Composer roots are different.
*
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebaseDocRoot(): void {
// Create a virtual file system containing items that should be
@@ -206,7 +209,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::findAvailablePort
+ * @legacy-covers ::findAvailablePort
*/
public function testPortMany(): void {
$iterator = (new Finder())->in($this->getDrupalRoot())
@@ -234,7 +237,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::standUpServer
+ * @legacy-covers ::standUpServer
*/
public function testStandUpServer(): void {
// Stand up a server with working directory 'first'.
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
index f34f5e7f896c..825cc6eaa468 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
@@ -4,18 +4,23 @@ declare(strict_types=1);
namespace Drupal\BuildTests\Framework\Tests;
+use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
/**
- * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
- * @group Build
- * @requires extension pdo_sqlite
+ * Tests Drupal\BuildTests\Framework\BuildTestBase.
*/
+#[CoversClass(BuildTestBase::class)]
+#[Group('Build')]
+#[RequiresPhpExtension('pdo_sqlite')]
class HtRouterTest extends QuickStartTestBase {
/**
- * @covers ::instantiateServer
+ * @legacy-covers ::instantiateServer
*/
public function testHtRouter(): void {
$sqlite = (new \PDO('sqlite::memory:'))->query('select sqlite_version()')->fetch()[0];
diff --git a/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
index bbcce3d2ca2a..59622ab504f9 100644
--- a/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
+++ b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
@@ -6,13 +6,15 @@ namespace Drupal\BuildTests\TestSiteApplication;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
/**
- * @group Build
- * @group TestSiteApplication
+ * Tests.
*/
+#[Group('Build')]
+#[Group('TestSiteApplication')]
class InstallTest extends BuildTestBase {
public function testInstall(): void {
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxCallbacksTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxCallbacksTest.php
index d1c07c20124e..1211cfb351b7 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxCallbacksTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxCallbacksTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests Ajax callbacks on FAPI elements.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxCallbacksTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
index b2ae9386bd0a..1eff2c19bbee 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
@@ -6,12 +6,12 @@ namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the usage of form caching for AJAX forms.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxFormCacheTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormImageButtonTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormImageButtonTest.php
index c9a4b3ef2722..347e8ac86f1a 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormImageButtonTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormImageButtonTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the Ajax image buttons work with key press events.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxFormImageButtonTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
index a80a627a89cc..c11430fdebff 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Performs tests on AJAX forms in cached pages.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxFormPageCacheTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php
index a8282b2b6cfe..6618ec6acede 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that form elements in groups work correctly with AJAX.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxInGroupTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxMaintenanceModeTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxMaintenanceModeTest.php
index 180381e45c1b..3f4543996f1f 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxMaintenanceModeTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxMaintenanceModeTest.php
@@ -8,12 +8,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests maintenance message during an AJAX call.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxMaintenanceModeTest extends WebDriverTestBase {
use FieldUiTestTrait;
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
index 03bd477123c1..284a3e35fa9b 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -6,12 +6,12 @@ namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\Component\Utility\UrlHelper;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests AJAX responses.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class AjaxTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
index cc81d142d453..877f255a6591 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Performs tests on AJAX framework commands.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class CommandsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
index be434633e5ef..beab4c6a242c 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
@@ -7,14 +7,13 @@ namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\ajax_test\Controller\AjaxTestController;
use Drupal\Core\Ajax\OpenModalDialogWithUrl;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
// cspell:ignore testdialog
-
/**
* Performs tests on opening and manipulating dialogs via AJAX commands.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class DialogTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
index 60fd1f40b95f..5040430abae4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Various tests of AJAX behavior.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class ElementValidationTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FocusFirstCommandTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FocusFirstCommandTest.php
index 7a9b91c52b90..2a27a09a987a 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FocusFirstCommandTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FocusFirstCommandTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests setting focus via AJAX command.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class FocusFirstCommandTest extends WebDriverTestBase {
/**
* {@inheritdoc}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
index 2ee6b8ddb2ce..68a9683a4906 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
@@ -5,12 +5,13 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that form values are properly delivered to AJAX callbacks.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class FormValuesTest extends WebDriverTestBase {
/**
@@ -33,9 +34,8 @@ class FormValuesTest extends WebDriverTestBase {
/**
* Submits forms with select and checkbox elements via Ajax.
- *
- * @dataProvider formModeProvider
*/
+ #[DataProvider('formModeProvider')]
public function testSimpleAjaxFormValue($form_mode): void {
$this->drupalGet('ajax_forms_test_get_form');
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
index 69b49916163e..f18bf953dd82 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\ExpectationFailedException;
/**
* Tests adding messages via AJAX command.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class MessageCommandTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
index 6a0026976a90..40ed7ac76704 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
@@ -8,12 +8,12 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests AJAX-enabled forms when multiple instances of the form are on a page.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class MultiFormTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php
index 6b2a17d4c6eb..92905a0c08ba 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php
@@ -6,12 +6,12 @@ namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\hold_test\HoldTestHelper;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the throbber.
- *
- * @group Ajax
*/
+#[Group('Ajax')]
class ThrobberTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/AjaxWaitTest.php b/core/tests/Drupal/FunctionalJavascriptTests/AjaxWaitTest.php
index 0a61c5f03cbe..adade6bc4208 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/AjaxWaitTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/AjaxWaitTest.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
/**
* Tests that unnecessary or untracked XHRs will cause a test failure.
- *
- * @group javascript
- * @group legacy
*/
+#[Group('javascript')]
+#[IgnoreDeprecations]
class AjaxWaitTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/BrowserWithJavascriptTest.php b/core/tests/Drupal/FunctionalJavascriptTests/BrowserWithJavascriptTest.php
index 2f11f7525eed..74c88ac48eed 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/BrowserWithJavascriptTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/BrowserWithJavascriptTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests if we can execute JavaScript in the browser.
- *
- * @group javascript
*/
+#[Group('javascript')]
class BrowserWithJavascriptTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Components/ComponentRenderTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Components/ComponentRenderTest.php
index 5b77de188aa2..95847a5d2290 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Components/ComponentRenderTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Components/ComponentRenderTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Components;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the correct rendering of components.
- *
- * @group sdc
*/
+#[Group('sdc')]
class ComponentRenderTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/CsrfTokenRaceTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/CsrfTokenRaceTest.php
index f16b300441de..3684ec3c4976 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/CsrfTokenRaceTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/CsrfTokenRaceTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Test race condition for CSRF tokens for simultaneous requests.
- *
- * @group Session
*/
+#[Group('Session')]
class CsrfTokenRaceTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffTest.php
index 53b2c5dcb7a5..6e615f4b4f91 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffTest.php
@@ -10,12 +10,12 @@ use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the 'timestamp' formatter when is used with time difference setting.
- *
- * @group Field
*/
+#[Group('Field')]
class TimestampFormatterWithTimeDiffTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffViewsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffViewsTest.php
index 0b5176182390..f35b5c72171b 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffViewsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/TimestampFormatterWithTimeDiffViewsTest.php
@@ -7,12 +7,12 @@ namespace Drupal\FunctionalJavascriptTests\Core\Field;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\views\Tests\ViewTestData;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the timestamp formatter used with time difference setting in views.
- *
- * @group Field
*/
+#[Group('Field')]
class TimestampFormatterWithTimeDiffViewsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/FormGroupingElementsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/FormGroupingElementsTest.php
index 7c8af15a2e31..f538690daf99 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/FormGroupingElementsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/FormGroupingElementsTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Core\Form;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests for form grouping elements.
- *
- * @group form
*/
+#[Group('form')]
class FormGroupingElementsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php
index 189af6e49e61..ac41f01653fe 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php
@@ -6,6 +6,7 @@ namespace Drupal\FunctionalJavascriptTests\Core\Form;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the state of elements based on another elements.
@@ -14,9 +15,8 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
* module under 'system' (core/modules/system/tests/module/form_test).
*
* @see Drupal\form_test\Form\JavascriptStatesForm
- *
- * @group javascript
*/
+#[Group('javascript')]
class JavascriptStatesTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
index ce4cf583cd8c..4acec8e11102 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
@@ -6,12 +6,12 @@ namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\js_message_test\Controller\JSMessageTestController;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests core/drupal.message library.
- *
- * @group Javascript
*/
+#[Group('Javascript')]
class JsMessageTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
index 31f044175860..b990be12549c 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Core;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests for the machine name field.
- *
- * @group field
*/
+#[Group('field')]
class MachineNameTest extends WebDriverTestBase {
/**
@@ -196,7 +196,7 @@ class MachineNameTest extends WebDriverTestBase {
$assert->fieldExists('Name')->setValue('test 1');
$machine_name_value = $page->find('css', '#edit-name-machine-name-suffix .machine-name-value');
$this->assertNotEmpty($machine_name_value, 'Machine name field must be initialized');
- $this->assertJsCondition('jQuery("#edit-name-machine-name-suffix .machine-name-value").html() == "' . 'test_1' . '"');
+ $this->assertJsCondition('jQuery("#edit-name-machine-name-suffix .machine-name-value").html() == "test_1"');
// Ensure that machine name generation still occurs after a non-HTML 5
// validation failure.
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Session/SessionTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Session/SessionTest.php
index 8adf7a5685a5..94387c04633e 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Session/SessionTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Session/SessionTest.php
@@ -6,12 +6,12 @@ namespace Drupal\FunctionalJavascriptTests\Core\Session;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that sessions don't expire.
- *
- * @group session
*/
+#[Group('session')]
class SessionTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogDeprecationsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogDeprecationsTest.php
index abc6fd4c5ca4..2085b5518419 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogDeprecationsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogDeprecationsTest.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Dialog;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
* Tests jQuery events deprecations.
- *
- * @group dialog
*/
+#[Group('dialog')]
class DialogDeprecationsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php
index ed73ac1ff706..abda27ed3b70 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Dialog;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the JavaScript functionality of the dialog position.
- *
- * @group dialog
*/
+#[Group('dialog')]
class DialogPositionTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
index 971af632a993..18fe44d8c4c3 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
@@ -4,18 +4,18 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\EntityReference;
-use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
-use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the output of entity reference autocomplete widgets.
- *
- * @group entity_reference
*/
+#[Group('entity_reference')]
class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
use ContentTypeCreationTrait;
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php
index e45026d8960a..cb2fce7a36c1 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
/**
* Tests Javascript deprecation notices.
- *
- * @group javascript
- * @group legacy
*/
+#[Group('javascript')]
+#[IgnoreDeprecations]
class JavascriptDeprecationTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsSuppressionTest.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsSuppressionTest.php
index a310d4c9e6e4..c67e27551c07 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsSuppressionTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsSuppressionTest.php
@@ -4,11 +4,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
+use PHPUnit\Framework\Attributes\Group;
+
/**
* Tests that Drupal.throwError can be suppressed to allow a test to pass.
- *
- * @group javascript
*/
+#[Group('javascript')]
class JavascriptErrorsSuppressionTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsTest.php
index 3fb355523406..5f96e95dc2bb 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptErrorsTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that Drupal.throwError will cause a test failure.
- *
- * @group javascript
*/
+#[Group('javascript')]
class JavascriptErrorsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptGetDrupalSettingsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptGetDrupalSettingsTest.php
index 31ec8c91675b..8054a7395997 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptGetDrupalSettingsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptGetDrupalSettingsTest.php
@@ -4,11 +4,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests;
+use PHPUnit\Framework\Attributes\Group;
+
/**
* Tests Drupal settings retrieval in WebDriverTestBase tests.
- *
- * @group javascript
*/
+#[Group('javascript')]
class JavascriptGetDrupalSettingsTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/MachineName/MachineNameTransliterationTest.php b/core/tests/Drupal/FunctionalJavascriptTests/MachineName/MachineNameTransliterationTest.php
index ad9ba4f89d8e..19cc4bf3a401 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/MachineName/MachineNameTransliterationTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/MachineName/MachineNameTransliterationTest.php
@@ -5,15 +5,15 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\MachineName;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
-
use Drupal\language\Entity\ConfigurableLanguage;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the machine name transliteration functionality.
- *
- * @group javascript
- * @group #slow
*/
+#[Group('javascript')]
+#[Group('#slow')]
class MachineNameTransliterationTest extends WebDriverTestBase {
/**
@@ -46,9 +46,8 @@ class MachineNameTransliterationTest extends WebDriverTestBase {
/**
* Test for machine name transliteration functionality.
- *
- * @dataProvider machineNameInputOutput
*/
+ #[DataProvider('machineNameInputOutput')]
public function testMachineNameTransliterations($langcode, $input, $output): void {
$page = $this->getSession()->getPage();
if ($langcode !== 'en') {
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
index d624e03c00d9..60f7b44fe0f4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
@@ -13,7 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @ingroup testing
*/
-class PerformanceTestBase extends WebDriverTestBase {
+abstract class PerformanceTestBase extends WebDriverTestBase {
use PerformanceTestTrait;
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php
index a9350a1976e6..0a14594369b6 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php
@@ -7,12 +7,12 @@ namespace Drupal\FunctionalJavascriptTests\TableDrag;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ExpectationException;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests draggable table.
- *
- * @group javascript
*/
+#[Group('javascript')]
class TableDragTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/DrupalSelenium2DriverTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/DrupalSelenium2DriverTest.php
index 1ab9d65b3be7..e598f5cc04a4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Tests/DrupalSelenium2DriverTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/DrupalSelenium2DriverTest.php
@@ -6,16 +6,18 @@ namespace Drupal\FunctionalJavascriptTests\Tests;
use Behat\Mink\Driver\Selenium2Driver;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the DrupalSelenium2Driver methods.
- *
- * @coversDefaultClass \Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver
- * @group javascript
*/
+#[CoversClass(DrupalSelenium2Driver::class)]
+#[Group('javascript')]
class DrupalSelenium2DriverTest extends WebDriverTestBase {
use TestFileCreationTrait;
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php
index 866f04e3a72a..a8018c7b013a 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Tests;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
use WebDriver\Exception;
/**
* Tests fault tolerant interactions.
- *
- * @group javascript
*/
+#[Group('javascript')]
class JSInteractionTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebAssertTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebAssertTest.php
index f200ed28ac98..6388bd0844e1 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebAssertTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebAssertTest.php
@@ -8,12 +8,12 @@ use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementHtmlException;
use Drupal\Component\Utility\Timer;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests for the JSWebAssert class.
- *
- * @group javascript
*/
+#[Group('javascript')]
class JSWebAssertTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroBlockFilterTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroBlockFilterTest.php
index dbc7874c735f..62ae39c4db49 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroBlockFilterTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroBlockFilterTest.php
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\block\FunctionalJavascript\BlockFilterTest;
+use PHPUnit\Framework\Attributes\Group;
/**
* Runs BlockFilterTest in Claro.
*
- * @group block
- *
* @see \Drupal\Tests\block\FunctionalJavascript\BlockFilterTest.
*/
+#[Group('block')]
class ClaroBlockFilterTest extends BlockFilterTest {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroEntityDisplayTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroEntityDisplayTest.php
index 85476468d815..c8e018ebfbe5 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroEntityDisplayTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroEntityDisplayTest.php
@@ -6,14 +6,14 @@ namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\entity_test\EntityTestHelper;
use Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest;
+use PHPUnit\Framework\Attributes\Group;
/**
* Runs EntityDisplayTest in Claro.
*
- * @group claro
- *
* @see \Drupal\Tests\field_ui\FunctionalJavascript\EntityDisplayTest.
*/
+#[Group('claro')]
class ClaroEntityDisplayTest extends EntityDisplayTest {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroMenuUiJavascriptTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroMenuUiJavascriptTest.php
index 32f6256396b3..930c7f963a7b 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroMenuUiJavascriptTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroMenuUiJavascriptTest.php
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\menu_ui\FunctionalJavascript\MenuUiJavascriptTest;
+use PHPUnit\Framework\Attributes\Group;
/**
* Runs MenuUiJavascriptTest in Claro.
*
- * @group claro
- *
* @see \Drupal\Tests\menu_ui\FunctionalJavascript\MenuUiJavascriptTest;
*/
+#[Group('claro')]
class ClaroMenuUiJavascriptTest extends MenuUiJavascriptTest {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroModalDisplayTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroModalDisplayTest.php
index c78ff99c80a3..7f110c1a4282 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroModalDisplayTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroModalDisplayTest.php
@@ -9,12 +9,12 @@ use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\media_library\FunctionalJavascript\MediaLibraryTestBase;
use Drupal\Tests\TestFileCreationTrait;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that buttons in modals are not in their button pane.
- *
- * @group claro
*/
+#[Group('claro')]
class ClaroModalDisplayTest extends MediaLibraryTestBase {
use TestFileCreationTrait;
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroPasswordConfirmWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroPasswordConfirmWidgetTest.php
index af01db8d2a8f..c090f658dc56 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroPasswordConfirmWidgetTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroPasswordConfirmWidgetTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\Tests\user\FunctionalJavascript\PasswordConfirmWidgetTest;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the password confirm widget with Claro theme.
- *
- * @group claro
*/
+#[Group('claro')]
class ClaroPasswordConfirmWidgetTest extends PasswordConfirmWidgetTest {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroTableDragTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroTableDragTest.php
index 9938ad22d095..7dda74a5348e 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroTableDragTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroTableDragTest.php
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\TableDrag\TableDragTest;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests draggable tables with Claro theme.
*
- * @group claro
- *
* @see \Drupal\FunctionalJavascriptTests\TableDrag\TableDragTest
*/
+#[Group('claro')]
class ClaroTableDragTest extends TableDragTest {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsBulkOperationsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsBulkOperationsTest.php
index e752b79a65af..3f6c21ad8755 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsBulkOperationsTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsBulkOperationsTest.php
@@ -7,12 +7,12 @@ namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests Claro's Views Bulk Operations form.
- *
- * @group claro
*/
+#[Group('claro')]
class ClaroViewsBulkOperationsTest extends WebDriverTestBase {
use ContentTypeCreationTrait;
use NodeCreationTrait;
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsUiTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsUiTest.php
index 28b3c46a9996..67474ad08a60 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsUiTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/ClaroViewsUiTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Runs tests on Views UI using Claro.
- *
- * @group claro
*/
+#[Group('claro')]
class ClaroViewsUiTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php
index ee1261a354b0..24d3ca85af26 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests usage of localStorage.
- *
- * @group olivero
*/
+#[Group('olivero')]
final class OliveroAvoidStorageUsingTest extends WebDriverTestBase {
/**
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php
index 6b0472702c0c..74278efdf4bc 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php
@@ -6,14 +6,14 @@ namespace Drupal\FunctionalJavascriptTests\Theme;
use Drupal\FunctionalJavascriptTests\Core\JsMessageTest;
use Drupal\js_message_test\Controller\JSMessageTestController;
+use PHPUnit\Framework\Attributes\Group;
/**
* Runs OliveroMessagesTest in Olivero.
*
- * @group olivero
- *
* @see \Drupal\FunctionalJavascriptTests\Core\JsMessageTest.
*/
+#[Group('olivero')]
class OliveroMessagesTest extends JsMessageTest {
/**
diff --git a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
index 80fd287751c5..aac21dcff875 100644
--- a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
+++ b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
@@ -203,6 +203,7 @@ class UncaughtExceptionTest extends BrowserTestBase {
switch ($this->container->get('database')->driver()) {
case 'pgsql':
case 'mysql':
+ case 'mysqli':
$this->expectedExceptionMessage = $incorrect_username;
break;
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
index e7509aa85281..04e771f8a654 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
@@ -46,7 +46,7 @@ class StandardRecipeInstallTest extends InstallerTestBase {
*/
protected function visitInstaller(): void {
// Use a URL to install from a recipe.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?profile=&recipe=core/recipes/standard');
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?profile=&recipe=core/recipes/standard');
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
index 4bf46e8d4911..68a793f8771f 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Core\Recipe;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\shortcut\Entity\Shortcut;
use Drupal\Tests\standard\Functional\StandardTest;
use Drupal\user\RoleInterface;
@@ -35,7 +36,12 @@ class StandardRecipeTest extends StandardTest {
$theme_installer->uninstall(['claro', 'olivero']);
// Determine which modules to uninstall.
- $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), ['user', 'system', 'path_alias', \Drupal::database()->getProvider()]);
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(\Drupal::database()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = \Drupal::database()->getProvider();
+ $keep = array_merge(['user', 'system', 'path_alias'], $database_modules);
+ $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), $keep);
foreach (['shortcut', 'field_config', 'filter_format', 'field_storage_config'] as $entity_type) {
$storage = \Drupal::entityTypeManager()->getStorage($entity_type);
$storage->delete($storage->loadMultiple());
diff --git a/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php b/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php
index f7509fd72775..f280ad493a57 100644
--- a/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php
+++ b/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php
@@ -35,6 +35,7 @@ use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
use Drupal\user\UserInterface;
+use Drupal\workspaces\Entity\Workspace;
use Psr\Log\LogLevel;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -72,6 +73,7 @@ class ContentImportTest extends BrowserTestBase {
'system',
'taxonomy',
'user',
+ 'workspaces',
];
/**
@@ -180,6 +182,22 @@ class ContentImportTest extends BrowserTestBase {
);
};
$this->assertTrue($logger->hasRecordThatPasses($predicate, LogLevel::WARNING));
+
+ // Visit a page that is published in a non-live workspace; we should not be
+ // able to see it, because we don't have permission.
+ $node_in_workspace = $this->container->get(EntityRepositoryInterface::class)
+ ->loadEntityByUuid('node', '48475954-e878-439c-9d3d-226724a44269');
+ $this->assertInstanceOf(NodeInterface::class, $node_in_workspace);
+ $node_url = $node_in_workspace->toUrl();
+ $this->drupalGet($node_url);
+ $assert_session = $this->assertSession();
+ $assert_session->statusCodeEquals(403);
+ // If we log in with administrative privileges (i.e., we can look at any
+ // workspace), we should be able to see it.
+ $this->drupalLogin($this->adminAccount);
+ $this->drupalGet($node_url);
+ $assert_session->statusCodeEquals(200);
+ $assert_session->pageTextContains($node_in_workspace->label());
}
/**
@@ -303,6 +321,11 @@ class ContentImportTest extends BrowserTestBase {
$this->assertInstanceOf(Section::class, $section);
$this->assertCount(2, $section->getComponents());
$this->assertSame('system_powered_by_block', $section->getComponent('03b45f14-cf74-469a-8398-edf3383ce7fa')->getPluginId());
+
+ // Workspaces should have been imported with their parent references intact.
+ $workspaces = Workspace::loadMultiple();
+ $this->assertArrayHasKey('test_workspace', $workspaces);
+ $this->assertSame('test_workspace', $workspaces['inner_test']?->parent->entity->id());
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php b/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
index e775b358550a..e5901e52fa41 100644
--- a/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
+++ b/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Entity;
-use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\entity_test\Entity\EntityTestRevPub;
@@ -247,7 +246,7 @@ class RevisionDeleteFormTest extends BrowserTestBase {
*
* @covers ::submitForm
*/
- protected function doTestSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, int $totalRevisions, string $expectedLog, string $expectedMessage, $expectedDestination): void {
+ protected function doTestSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, int $totalRevisions, array $expectedLog, string $expectedMessage, $expectedDestination): void {
if (count($permissions) > 0) {
$this->drupalLogin($this->createUser($permissions));
}
@@ -297,7 +296,9 @@ class RevisionDeleteFormTest extends BrowserTestBase {
// Logger log.
$logs = $this->getLogs($entity->getEntityType()->getProvider());
- $this->assertEquals([0 => $expectedLog], $logs);
+ $this->assertCount(1, $logs);
+ $this->assertEquals("@type: deleted %title revision %revision.", $logs[0]->message);
+ $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
// Messenger message.
$this->assertSession()->pageTextContains($expectedMessage);
\Drupal::database()->delete('watchdog')->execute();
@@ -314,7 +315,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_rev',
'view all revisions, delete revision',
2,
- 'entity_test_rev: deleted <em class="placeholder">view all revisions, delete revision</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view all revisions, delete revision',
+ '%revision' => '1',
+ ],
'Revision of Entity Test Bundle view all revisions, delete revision has been deleted.',
'/entity_test_rev/1/revisions',
];
@@ -324,7 +329,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_rev',
'view, view all revisions, delete revision',
2,
- 'entity_test_rev: deleted <em class="placeholder">view, view all revisions, delete revision</em> revision <em class="placeholder">3</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, view all revisions, delete revision',
+ '%revision' => '3',
+ ],
'Revision of Entity Test Bundle view, view all revisions, delete revision has been deleted.',
'/entity_test_rev/2/revisions',
];
@@ -334,7 +343,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_revlog',
'view all revisions, delete revision',
2,
- 'entity_test_revlog: deleted <em class="placeholder">view all revisions, delete revision</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view all revisions, delete revision',
+ '%revision' => '1',
+ ],
'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view all revisions, delete revision has been deleted.',
'/entity_test_revlog/1/revisions',
];
@@ -344,7 +357,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_revlog',
'view, view all revisions, delete revision',
2,
- 'entity_test_revlog: deleted <em class="placeholder">view, view all revisions, delete revision</em> revision <em class="placeholder">3</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, view all revisions, delete revision',
+ '%revision' => '3',
+ ],
'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view, view all revisions, delete revision has been deleted.',
'/entity_test_revlog/2/revisions',
];
@@ -362,14 +379,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
* Watchdog entries.
*/
protected function getLogs(string $channel): array {
- $logs = \Drupal::database()->select('watchdog')
+ return \Drupal::database()->select('watchdog')
->fields('watchdog')
->condition('type', $channel)
->execute()
->fetchAll();
- return array_map(function (object $log) {
- return (string) new FormattableMarkup($log->message, unserialize($log->variables));
- }, $logs);
}
}
diff --git a/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php b/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
index c6ff4629fd9b..8ca59b6100e4 100644
--- a/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
+++ b/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Entity;
-use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\entity_test\Entity\EntityTestRevPub;
@@ -192,7 +191,7 @@ class RevisionRevertFormTest extends BrowserTestBase {
* @covers ::submitForm
* @dataProvider providerSubmitForm
*/
- public function testSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, string $expectedLog, string $expectedMessage, string $expectedDestination): void {
+ public function testSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, array $expectedLog, string $expectedMessage, string $expectedDestination): void {
if (count($permissions) > 0) {
$this->drupalLogin($this->createUser($permissions));
}
@@ -231,7 +230,10 @@ class RevisionRevertFormTest extends BrowserTestBase {
// Logger log.
$logs = $this->getLogs($entity->getEntityType()->getProvider());
- $this->assertEquals([0 => $expectedLog], $logs);
+ $this->assertCount(1, $logs);
+ $this->assertEquals('@type: reverted %title revision %revision.', $logs[0]->message);
+ $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
+
// Messenger message.
$this->assertSession()->pageTextContains($expectedMessage);
}
@@ -246,7 +248,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
['view test entity'],
'entity_test_rev',
'view, revert',
- 'entity_test_rev: reverted <em class="placeholder">view, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, revert',
+ '%revision' => '1',
+ ],
'Entity Test Bundle view, revert has been reverted.',
'/entity_test_rev/manage/1',
];
@@ -255,7 +261,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
['view test entity'],
'entity_test_rev',
'view, view all revisions, revert',
- 'entity_test_rev: reverted <em class="placeholder">view, view all revisions, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, view all revisions, revert',
+ '%revision' => '1',
+ ],
'Entity Test Bundle view, view all revisions, revert has been reverted.',
'/entity_test_rev/1/revisions',
];
@@ -264,7 +274,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
[],
'entity_test_revlog',
'view, revert',
- 'entity_test_revlog: reverted <em class="placeholder">view, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, revert',
+ '%revision' => '1',
+ ],
'Test entity - revisions log view, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
'/entity_test_revlog/manage/1',
];
@@ -273,7 +287,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
[],
'entity_test_revlog',
'view, view all revisions, revert',
- 'entity_test_revlog: reverted <em class="placeholder">view, view all revisions, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, view all revisions, revert',
+ '%revision' => '1',
+ ],
'Test entity - revisions log view, view all revisions, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
'/entity_test_revlog/1/revisions',
];
@@ -347,14 +365,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
* Watchdog entries.
*/
protected function getLogs(string $channel): array {
- $logs = \Drupal::database()->select('watchdog')
+ return \Drupal::database()->select('watchdog')
->fields('watchdog')
->condition('type', $channel)
->execute()
->fetchAll();
- return array_map(function (object $log) {
- return (string) new FormattableMarkup($log->message, unserialize($log->variables));
- }, $logs);
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
index 01c694b4ef4b..fd7bfeecd38c 100644
--- a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
+++ b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
@@ -26,7 +26,7 @@ class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTes
$driver = Database::getConnection()->driver();
if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) {
- $this->markTestSkipped("This test does not support the {$driver} database driver.");
+ $this->markTestSkipped("This test is only relevant for database drivers that were available in Drupal prior to database drivers becoming part of modules. The {$driver} database driver is not qualifying.");
}
$filename = $this->siteDirectory . '/settings.php';
diff --git a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
index 764e9d15d65d..67bdc2bb70a1 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
@@ -67,7 +67,7 @@ class DistributionProfileTranslationQueryTest extends InstallerTestBase {
// profile. This distribution language should still be used.
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=fr');
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=fr');
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
index 2ac3ae778a4b..f7c949637547 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
@@ -7,6 +7,7 @@ namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
@@ -99,8 +100,11 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
// modules that can not be uninstalled in the core.extension configuration.
if (file_exists($config_sync_directory . '/core.extension.yml')) {
$core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
- $module = Database::getConnection()->getProvider();
- if ($module !== 'core') {
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(Database::getConnection()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = Database::getConnection()->getProvider();
+ foreach ($database_modules as $module) {
$core_extension['module'][$module] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
}
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
index 3bd0fe48af73..994ac39d2707 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
@@ -63,25 +63,20 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
// Assert that in the settings.php the database connection array has the
// correct values set.
- $contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php');
- $this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents);
-
- $dependencies = "'dependencies' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'mysql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\mysql'," . PHP_EOL .
- " 'autoload' => 'core/modules/mysql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " 'pgsql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\pgsql'," . PHP_EOL .
- " 'autoload' => 'core/modules/pgsql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " )," . PHP_EOL;
- $this->assertStringContainsString($dependencies, $contents);
+ $installedDatabaseSettings = $this->getInstalledDatabaseSettings();
+ $this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']);
+ $this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']);
+ $this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']);
+ $this->assertEquals([
+ 'mysql' => [
+ 'namespace' => 'Drupal\\mysql',
+ 'autoload' => 'core/modules/mysql/src/',
+ ],
+ 'pgsql' => [
+ 'namespace' => 'Drupal\\pgsql',
+ 'autoload' => 'core/modules/pgsql/src/',
+ ],
+ ], $installedDatabaseSettings['default']['default']['dependencies']);
// Assert that the module "driver_test" and its dependencies have been
// installed.
@@ -95,8 +90,26 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
// uninstalled being dependencies of the "driver_test" module.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-driver-test"]', "The following reason prevents Contrib database driver test from being uninstalled: The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'.");
- $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: driver_test");
- $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test");
+ $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: Contrib database driver test (driver_test)");
+ $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: Contrib database driver test (driver_test)");
+ }
+
+ /**
+ * Returns the databases setup from the SUT's settings.php.
+ *
+ * @return array<string,mixed>
+ * The value of the $databases variable.
+ */
+ protected function getInstalledDatabaseSettings(): array {
+ // The $app_root and $site_path variables are required by the settings.php
+ // file to be parsed correctly. The $databases variable is set in the
+ // included file, we need to inform PHPStan about that since PHPStan itself
+ // is unable to determine it.
+ $app_root = $this->container->getParameter('app.root');
+ $site_path = $this->siteDirectory;
+ include $app_root . '/' . $site_path . '/settings.php';
+ assert(isset($databases));
+ return $databases;
}
}
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
index c1694932a0f8..f1d2d507eeed 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
@@ -271,7 +271,7 @@ abstract class InstallerTestBase extends BrowserTestBase {
* Override this method to test specific requirements warnings or errors
* during the installer.
*
- * @see system_requirements()
+ * @see \Drupal\system\Install\SystemRequirements
*/
protected function setUpRequirementsProblem() {
if (version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
index 532c23a814ee..97976121601d 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
@@ -35,7 +35,7 @@ class InstallerTranslationQueryTest extends InstallerTestBase {
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=' . $this->langcode);
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=' . $this->langcode);
// The language should have been automatically detected, all following
// screens should be translated already.
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index 8b2c4b493e4b..3ee78e553654 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -137,6 +137,16 @@ abstract class UpdatePathTestBase extends BrowserTestBase {
// Load the database(s).
foreach ($this->databaseDumpFiles as $file) {
+ // Determine the version of the database dump if specified.
+ $matches = [];
+ $dumpVersion = preg_match('/drupal-(\d+\.\d+\.\d+)\./', $file, $matches) === 1 ? $matches[1] : NULL;
+
+ // If the db driver is mysqli, we do not need to run the update tests for
+ // db dumps prior to 11.2 when the module was introduced.
+ if (Database::getConnection()->getProvider() === 'mysqli' && $dumpVersion && version_compare($dumpVersion, '11.2.0', '<')) {
+ $this->markTestSkipped("The mysqli driver was introduced in Drupal 11.2, skip update tests from database at version {$dumpVersion}");
+ }
+
if (str_ends_with($file, '.gz')) {
$file = "compress.zlib://$file";
}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php
new file mode 100644
index 000000000000..1b012c9f7262
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Components;
+
+/**
+ * Tests discovery of components in a theme being installed or uninstalled.
+ *
+ * @group sdc
+ */
+class ComponentPluginManagerCachedDiscoveryTest extends ComponentKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $themes = ['stark'];
+
+ /**
+ * Tests cached component plugin discovery on theme install and uninstall.
+ */
+ public function testComponentDiscoveryOnThemeInstall(): void {
+ // Component in sdc_theme should not be found without sdc_theme installed.
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
+
+ // Component in sdc_theme should be found once sdc_theme is installed.
+ \Drupal::service('theme_installer')->install(['sdc_theme_test']);
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayHasKey('sdc_theme_test:bar', $definitions);
+
+ // Component in sdc_theme should not be found once sdc_theme is uninstalled.
+ \Drupal::service('theme_installer')->uninstall(['sdc_theme_test']);
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
index 3ca62dcced59..f375f6f3dd69 100644
--- a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
+++ b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
@@ -377,6 +377,7 @@ class MappingTest extends KernelTestBase {
'field.value.decimal' => ['value'],
'field.value.float' => ['value'],
'field.value.timestamp' => ['value'],
+ 'field.value.language' => ['value'],
'field.value.comment' => [
'status',
'cid',
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
index 6dd535e88f95..809e54737b7c 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
@@ -467,4 +467,26 @@ class AttachedAssetsTest extends KernelTestBase {
$this->assertStringContainsString('<script src="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.');
}
+ /**
+ * Test settings can be loaded even when libraries are not.
+ */
+ public function testAttachedSettingsWithoutLibraries(): void {
+ $assets = new AttachedAssets();
+
+ // First test with no libraries will return no settings.
+ $assets->setSettings(['test' => 'foo']);
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertArrayNotHasKey('drupalSettings', $js);
+
+ // Second test with a warm cache.
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertArrayNotHasKey('drupalSettings', $js);
+
+ // Now test with different settings when drupalSettings is already loaded.
+ $assets->setSettings(['test' => 'bar']);
+ $assets->setAlreadyLoadedLibraries(['core/drupalSettings']);
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertSame('bar', $js['drupalSettings']['data']['test']);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
index 340dcc286494..15c97bea71f4 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
@@ -99,6 +99,20 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
protected function setUp(): void {
parent::setUp();
+ // Install all core themes.
+ sort($this->allThemes);
+ $this->container->get('theme_installer')->install($this->allThemes);
+
+ $this->themeHandler = $this->container->get('theme_handler');
+ $this->themeInitialization = $this->container->get('theme.initialization');
+ $this->themeManager = $this->container->get('theme.manager');
+ $this->libraryDiscovery = $this->container->get('library.discovery');
+ }
+
+ /**
+ * Ensures that all core module and theme library files exist.
+ */
+ public function testCoreLibraryCompleteness(): void {
// Enable all core modules.
$all_modules = $this->container->get('extension.list.module')->getList();
$all_modules = array_filter($all_modules, function ($module) {
@@ -141,21 +155,37 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
}
sort($this->allModules);
$this->container->get('module_installer')->install($this->allModules);
+ // Get a library discovery from the new container.
+ $this->libraryDiscovery = $this->container->get('library.discovery');
- // Install all core themes.
- sort($this->allThemes);
- $this->container->get('theme_installer')->install($this->allThemes);
+ $this->assertLibraries();
+ }
- $this->themeHandler = $this->container->get('theme_handler');
- $this->themeInitialization = $this->container->get('theme.initialization');
- $this->themeManager = $this->container->get('theme.manager');
+ /**
+ * Ensures that module and theme library files exist for a deprecated modules.
+ *
+ * @group legacy
+ */
+ public function testCoreLibraryCompletenessDeprecated(): void {
+ // Find and install deprecated modules to test.
+ $all_modules = $this->container->get('extension.list.module')->getList();
+ $deprecated_modules_to_test = array_filter($all_modules, function ($module) {
+ if ($module->origin == 'core'
+ && $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
+ return TRUE;
+ }
+ });
+ $this->container->get('module_installer')->install(array_keys($deprecated_modules_to_test));
$this->libraryDiscovery = $this->container->get('library.discovery');
+ $this->allModules = array_keys(\Drupal::moduleHandler()->getModuleList());
+
+ $this->assertLibraries();
}
/**
- * Ensures that all core module and theme library files exist.
+ * Asserts the libraries for modules and themes exist.
*/
- public function testCoreLibraryCompleteness(): void {
+ public function assertLibraries(): void {
// First verify all libraries with no active theme.
$this->verifyLibraryFilesExist($this->getAllLibraries());
diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
index c2bff1232364..c1ff7e24406a 100644
--- a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheTagsPurgeInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
@@ -62,4 +63,34 @@ class DatabaseBackendTagTest extends KernelTestBase {
$this->assertEquals($invalidations_before + 1, $invalidations_after, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.');
}
+ /**
+ * Test cache tag purging.
+ */
+ public function testTagsPurge(): void {
+ $tags = ['test_tag:1', 'test_tag:2', 'test_tag:3'];
+ /** @var \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_invalidator */
+ $checksum_invalidator = \Drupal::service('cache_tags.invalidator.checksum');
+ // Assert that initial current tag checksum is 0. This also ensures that the
+ // 'cachetags' table is created, which at this point does not exist yet.
+ $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
+
+ /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */
+ $invalidator = \Drupal::service('cache_tags.invalidator');
+ $invalidator->invalidateTags($tags);
+ // Checksum should be incremented by 1 by the invalidation for each tag.
+ $this->assertEquals(3, $checksum_invalidator->getCurrentChecksum($tags));
+
+ // After purging, confirm checksum is 0 and the 'cachetags' table is empty.
+ $this->assertInstanceOf(CacheTagsPurgeInterface::class, $invalidator);
+ $invalidator->purge();
+ $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
+
+ $rows = Database::getConnection()->select('cachetags')
+ ->fields('cachetags')
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ $this->assertEmpty($rows, 'cachetags table is empty.');
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
index bcdc76cc71d7..3d3ee862cef6 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
@@ -28,6 +28,7 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase {
*
* @testWith [{}, "system.site", "system.site"]
* [{"prefix": "system."}, "site", "system.site"]
+ * [{"prefix": "system.[%parent.reference]."}, "admin", "system.menu.admin"]
*/
public function testValidation(array $constraint_options, string $value, string $expected_config_name): void {
// Create a data definition that specifies the value must be a string with
@@ -37,7 +38,11 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase {
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
- $data = $typed_data->create($definition, $value);
+
+ // Create a data definition for the parent data.
+ $parent_data_definition = $typed_data->createDataDefinition('map');
+ $parent_data = $typed_data->create($parent_data_definition, ['reference' => 'menu']);
+ $data = $typed_data->create($definition, $value, 'data_name', $parent_data);
$violations = $data->validate();
$this->assertCount(1, $violations);
diff --git a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php
index 91cc24234e1a..9882d3d9ef0f 100644
--- a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Controller;
+use Drupal\dblog\Logger\DbLog;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system_test\Controller\BrokenSystemTestController;
+use Drupal\system_test\Controller\OptionalServiceSystemTestController;
use Drupal\system_test\Controller\SystemTestController;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
@@ -52,4 +54,17 @@ class ControllerBaseTest extends KernelTestBase {
$this->container->get('class_resolver')->getInstanceFromDefinition(BrokenSystemTestController::class);
}
+ /**
+ * @covers ::create
+ */
+ public function testCreateOptional(): void {
+ $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class);
+ $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service);
+ $this->assertNull($service->dbLog);
+ $this->container->get('module_installer')->install(['dblog']);
+ $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class);
+ $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service);
+ $this->assertInstanceOf(DbLog::class, $service->dbLog);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
index 079009dc7a56..8725f647e8d1 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
@@ -63,18 +63,6 @@ class BasicSyntaxTest extends DatabaseTestBase {
}
/**
- * Tests string concatenation with separator, with field values.
- */
- public function testConcatWsFields(): void {
- $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
- ':a1' => 'name',
- ':a2' => 'age',
- ':age' => 25,
- ]);
- $this->assertSame('name-John-age-25', $result->fetchField());
- }
-
- /**
* Tests escaping of LIKE wildcards.
*/
public function testLikeEscape(): void {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
index ca5fb32936b5..a3b46ab67e12 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
@@ -272,6 +272,16 @@ abstract class DriverSpecificSchemaTestBase extends DriverSpecificKernelTestBase
// Test the primary key columns.
$this->assertSame(['test_serial', 'test_composite_primary_key'], $method->invoke($this->schema, 'test_table'));
+ // Test adding and removing JSON column.
+ $this->schema->addField('test_table', 'test_json', [
+ 'description' => 'I heard you liked JSON.',
+ 'type' => 'json',
+ 'pgsql_type' => 'jsonb',
+ 'mysql_type' => 'json',
+ 'sqlite_type' => 'json',
+ ]);
+ $this->schema->dropField('test_table', 'test_json');
+
// Test renaming of keys and constraints.
$this->schema->dropTable('test_table');
$table_specification = [
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
index 7723d872cc12..e18336205398 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
@@ -43,4 +43,16 @@ abstract class DriverSpecificSyntaxTestBase extends DriverSpecificDatabaseTestBa
$this->assertSame('[square]', $result->fetchField());
}
+ /**
+ * Tests string concatenation with separator, with field values.
+ */
+ public function testConcatWsFields(): void {
+ $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
+ ':a1' => 'name',
+ ':a2' => 'age',
+ ':age' => 25,
+ ]);
+ $this->assertSame('name-John-age-25', $result->fetchField());
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
index c361c7af9596..b7e31e06a59a 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
@@ -38,7 +38,7 @@ use Drupal\Core\Database\TransactionOutOfOrderException;
* is active, and mysqli does not fail when rolling back and no transaction
* active.
*/
-class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
+abstract class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Keeps track of the post-transaction callback action executed.
@@ -432,9 +432,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Tests rollback after a DDL statement when no transactional DDL supported.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
@@ -919,9 +916,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* transaction including DDL statements is not possible, since a commit
* happened already. We cannot decide what should be the status of the
* callback, an exception is thrown.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
index 6e28787f764c..3bfaa18889a9 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
@@ -20,9 +20,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchObject(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_OBJ]);
foreach ($result as $record) {
@@ -39,9 +39,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchArray(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_ASSOC]);
foreach ($result as $record) {
@@ -59,9 +59,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchNum(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_NUM]);
foreach ($result as $record) {
@@ -79,7 +79,7 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchAllColumn(): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$query = $this->connection->select('test');
$query->addField('test', 'name');
$query->orderBy('name');
@@ -94,7 +94,7 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchAllAssoc(): void {
- $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$expected_result = [
"Singer" => [
"id" => "2",
diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
new file mode 100644
index 000000000000..f40a977f6f49
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
@@ -0,0 +1,1278 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Transaction;
+use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
+use Drupal\Core\Database\Transaction\StackItem;
+use Drupal\Core\Database\Transaction\StackItemType;
+use Drupal\Core\Database\Transaction\TransactionManagerBase;
+use Drupal\Core\Database\TransactionNameNonUniqueException;
+use Drupal\Core\Database\TransactionOutOfOrderException;
+
+// cspell:ignore Tinky Winky Dipsy
+
+/**
+ * Tests the transactions, using the explicit ::commitOrRelease method.
+ *
+ * We test nesting by having two transaction layers, an outer and inner. The
+ * outer layer encapsulates the inner layer. Our transaction nesting abstraction
+ * should allow the outer layer function to call any function it wants,
+ * especially the inner layer that starts its own transaction, and be
+ * confident that, when the function it calls returns, its own transaction
+ * is still "alive."
+ *
+ * Call structure:
+ * transactionOuterLayer()
+ * Start transaction "A"
+ * transactionInnerLayer()
+ * Start transaction "B" (does nothing in database)
+ * [Maybe decide to roll back "B"]
+ * Do more stuff
+ * Should still be in transaction "A"
+ *
+ * These method can be overridden by non-core database driver if their
+ * transaction behavior is different from core. For example, both oci8 (Oracle)
+ * and mysqli (MySql) clients do not have a solution to check if a transaction
+ * is active, and mysqli does not fail when rolling back and no transaction
+ * active.
+ *
+ * @group Database
+ */
+class TransactionTest extends DatabaseTestBase {
+
+ /**
+ * Keeps track of the post-transaction callback action executed.
+ */
+ protected ?string $postTransactionCallbackAction = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Set the transaction manager to trigger warnings when appropriate.
+ $this->connection->transactionManager()->triggerWarningWhenUnpilingOnVoidTransaction = TRUE;
+ }
+
+ /**
+ * Create a root Drupal transaction.
+ */
+ protected function createRootTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+
+ // Start root transaction. Corresponds to 'BEGIN TRANSACTION' on the
+ // database.
+ $transaction = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('David');
+ $this->assertRowPresent('David');
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Create a Drupal savepoint transaction after root.
+ */
+ protected function createFirstSavepointTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_1'
+ // on the database. The name can be changed by the $name argument.
+ $savepoint = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('Roger');
+ $this->assertRowPresent('Roger');
+ }
+
+ return $savepoint;
+ }
+
+ /**
+ * Encapsulates a transaction's "inner layer" with an "outer layer".
+ *
+ * This "outer layer" transaction starts and then encapsulates the "inner
+ * layer" transaction. This nesting is used to evaluate whether the database
+ * transaction API properly supports nesting. By "properly supports," we mean
+ * the outer transaction continues to exist regardless of what functions are
+ * called and whether those functions start their own transactions.
+ *
+ * In contrast, a typical database would commit the outer transaction, start
+ * a new transaction for the inner layer, commit the inner layer transaction,
+ * and then be confused when the outer layer transaction tries to commit its
+ * transaction (which was already committed when the inner transaction
+ * started).
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionOuterLayer(string $suffix): void {
+ $txn = $this->connection->startTransaction();
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David' . $suffix,
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction before calling nested transaction.');
+
+ // We're already in a transaction, but we call ->transactionInnerLayer
+ // to nest another transaction inside the current one.
+ $this->transactionInnerLayer($suffix);
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction after calling nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Creates an "inner layer" transaction.
+ *
+ * This "inner layer" transaction is either used alone or nested inside of the
+ * "outer layer" transaction.
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionInnerLayer(string $suffix): void {
+ $depth = $this->connection->transactionManager()->stackDepth();
+ // Start a transaction. If we're being called from ->transactionOuterLayer,
+ // then we're already in a transaction. Normally, that would make starting
+ // a transaction here dangerous, but the database API handles this problem
+ // for us by tracking the nesting and avoiding the danger.
+ $txn = $this->connection->startTransaction();
+
+ $depth2 = $this->connection->transactionManager()->stackDepth();
+ $this->assertSame($depth + 1, $depth2, 'Transaction depth has increased with new transaction.');
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'Daniel' . $suffix,
+ 'age' => '19',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction inside nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Tests root transaction rollback.
+ */
+ public function testRollbackRoot(): void {
+ $transaction = $this->createRootTransaction();
+
+ // Rollback. Since we are at the root, the transaction is closed.
+ // Corresponds to 'ROLLBACK' on the database.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback after savepoint rollback.
+ */
+ public function testRollbackRootAfterSavepointRollback(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Try to rollback root. No savepoint is active, this should succeed.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback failure when savepoint is open.
+ */
+ public function testRollbackRootWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Try to rollback root. Since a savepoint is active, this should fail.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\drupal_transaction\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1/");
+ $transaction->rollBack();
+ }
+
+ /**
+ * Tests savepoint transaction rollback.
+ */
+ public function testRollbackSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction commit after rollback.
+ */
+ public function testCommitAfterRollbackSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try releasing savepoint. Should fail since it was released already.
+ try {
+ $savepoint->commitOrRelease();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting commit of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback after commit.
+ */
+ public function testRollbackAfterCommitSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Release savepoint. Corresponds to 'RELEASE savepoint_1' on the database.
+ $savepoint->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try rolling back savepoint. Should fail since it was released already.
+ try {
+ $savepoint->rollback();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction duplicated rollback.
+ */
+ public function testRollbackTwiceSameSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Rollback savepoint again. Should fail since it was released already.
+ try {
+ $savepoint->rollBack();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback failure when later savepoints exist.
+ */
+ public function testRollbackSavepointWithLaterSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction();
+
+ // Starts another savepoint transaction. Corresponds to 'SAVEPOINT
+ // savepoint_2' on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+
+ // Try to rollback to savepoint 1. Out of order.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1 > .*\\\\savepoint_2/");
+ $savepoint1->rollBack();
+ }
+
+ /**
+ * Tests commit does not fail when committing after DDL.
+ *
+ * In core, SQLite and PostgreSql databases support transactional DDL, MySql
+ * does not.
+ */
+ public function testCommitAfterDdl(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ $this->executeDDLStatement();
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $savepoint->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $savepoint->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ /**
+ * Tests a committed transaction.
+ *
+ * The behavior of this test should be identical for connections that support
+ * transactions and those that do not.
+ */
+ public function testCommittedTransaction(): void {
+ // Create two nested transactions. The changes should be committed.
+ $this->transactionOuterLayer('A');
+
+ // Because we committed, both of the inserted rows should be present.
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DavidA'])->fetchField();
+ $this->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.');
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DanielA'])->fetchField();
+ $this->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.');
+ }
+
+ /**
+ * Tests the compatibility of transactions with DDL statements.
+ */
+ public function testTransactionWithDdlStatement(): void {
+ // First, test that a commit works normally, even with DDL statements.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even in different order.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $this->insertRow('row');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // A transaction after a DDL statement should still work the same.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->rollBack();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowAbsent('row');
+
+ // The behavior of a rollback depends on the type of database server.
+ if ($this->connection->supportsTransactionalDDL()) {
+ // For database servers that support transactional DDL, a rollback
+ // of a transaction including DDL statements should be possible.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+
+ // Including with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $transaction2->commitOrRelease();
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+ $this->assertRowPresent('row');
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+ }
+ }
+
+ /**
+ * Tests rollback after a DDL statement when no transactional DDL supported.
+ */
+ public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ // For database servers that do not support transactional DDL,
+ // the DDL statement should commit the transaction stack.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $reflectionMethod = new \ReflectionMethod(get_class($this->connection->transactionManager()), 'getConnectionTransactionState');
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Active, $reflectionMethod->invoke($this->connection->transactionManager()));
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Voided, $reflectionMethod->invoke($this->connection->transactionManager()));
+
+ // Try to rollback the root transaction. Since the DDL already committed
+ // it, it should fail.
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, the attempted rollback made the overall state to
+ // ClientConnectionTransactionState::RollbackFailed.
+ }
+
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Inserts a single row into the testing table.
+ */
+ protected function insertRow(string $name): void {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => $name,
+ ])
+ ->execute();
+ }
+
+ /**
+ * Executes a DDL statement.
+ */
+ protected function executeDDLStatement(): void {
+ static $count = 0;
+ $table = [
+ 'fields' => [
+ 'id' => [
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ ],
+ 'primary key' => ['id'],
+ ];
+ $this->connection->schema()->createTable('database_test_' . ++$count, $table);
+ }
+
+ /**
+ * Starts over for a new test.
+ */
+ protected function cleanUp(): void {
+ $this->connection->truncate('test')
+ ->execute();
+ $this->postTransactionCallbackAction = NULL;
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Asserts that a given row is present in the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowPresent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist.");
+ }
+
+ /**
+ * Asserts that a given row is absent from the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowAbsent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists.");
+ }
+
+ /**
+ * Tests transaction stacking, commit, and rollback.
+ */
+ public function testTransactionStacking(): void {
+ // Standard case: pop the inner transaction before the outer transaction.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Pop the inner transaction.
+ $transaction2->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the inner transaction');
+ // Pop the outer transaction.
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the outer transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowPresent('inner');
+
+ // Rollback the inner transaction.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Now rollback the inner transaction.
+ $transaction2->rollBack();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the outer transaction');
+ // Pop the outer transaction, it should commit.
+ $this->insertRow('outer-after-inner-rollback');
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the inner transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowAbsent('inner');
+ $this->assertRowPresent('outer-after-inner-rollback');
+ }
+
+ /**
+ * Tests that transactions can continue to be used if a query fails.
+ */
+ public function testQueryFailureInTransaction(): void {
+ $transaction = $this->createRootTransaction('test_transaction', FALSE);
+ $this->connection->schema()->dropTable('test');
+
+ // Test a failed query using the query() method.
+ try {
+ $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->fail('Using the query method should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed select query.
+ try {
+ $this->connection->select('test')
+ ->fields('test', ['name'])
+ ->execute();
+
+ $this->fail('Select query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed insert query.
+ try {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->fail('Insert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed update query.
+ try {
+ $this->connection->update('test')
+ ->fields(['name' => 'Tiffany'])
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Update query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed delete query.
+ try {
+ $this->connection->delete('test')
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Delete query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed merge query.
+ try {
+ $this->connection->merge('test')
+ ->key('job', 'Presenter')
+ ->fields([
+ 'age' => '31',
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Merge query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed upsert query.
+ try {
+ $this->connection->upsert('test')
+ ->key('job')
+ ->fields(['job', 'age', 'name'])
+ ->values([
+ 'job' => 'Presenter',
+ 'age' => 31,
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Upsert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Create the missing schema and insert a row.
+ $this->installSchema('database_test', ['test']);
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ // Commit the transaction.
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->assertEquals('24', $saved_age);
+ }
+
+ /**
+ * Tests releasing a savepoint before last is safe.
+ */
+ public function testReleaseIntermediateSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_3'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint3 = $this->connection->startTransaction();
+ $this->assertSame(4, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_4'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint4 = $this->connection->startTransaction();
+ $this->assertSame(5, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Release savepoint transaction. Corresponds to 'RELEASE SAVEPOINT
+ // savepoint_2' on the database.
+ $savepoint2->commitOrRelease();
+ // Since we have committed an intermediate savepoint Transaction object,
+ // the savepoints created later have been dropped by the database already.
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ $this->assertRowPresent('row');
+
+ // Commit the remaining Transaction objects. The client transaction is
+ // eventually committed.
+ $savepoint1->commitOrRelease();
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests committing a transaction while savepoints are active.
+ */
+ public function testCommitWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Commit the root transaction.
+ $transaction->commitOrRelease();
+ // Since we have committed the outer (root) Transaction object, the inner
+ // (savepoint) ones have been dropped by the database already, and we are
+ // no longer in an active transaction state.
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ // Trying to release the inner (savepoint) Transaction object, throws an
+ // exception since it was dropped by the database already, and removed from
+ // our transaction stack.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting commit of .*\\\\savepoint_2\\. Active stack: .* empty/");
+ $savepoint2->commitOrRelease();
+ }
+
+ /**
+ * Tests for transaction names.
+ */
+ public function testTransactionName(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->assertSame('savepoint_1', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("savepoint_1 is already in use.");
+ $this->connection->startTransaction('savepoint_1');
+ }
+
+ /**
+ * Tests for arbitrary transaction names.
+ */
+ public function testArbitraryTransactionNames(): void {
+ $transaction = $this->createRootTransaction('TinkyWinky', FALSE);
+ // Despite setting a name, the root transaction is always named
+ // 'drupal_transaction'.
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('Dipsy', FALSE);
+ $this->assertSame('Dipsy', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("Dipsy is already in use.");
+ $this->connection->startTransaction('Dipsy');
+ }
+
+ /**
+ * Tests that adding a post-transaction callback fails with no transaction.
+ */
+ public function testRootTransactionEndCallbackAddedWithoutTransaction(): void {
+ $this->expectException(\LogicException::class);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction commit.
+ */
+ public function testRootTransactionEndCallbackCalledOnCommit(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a commit is not sufficient by itself.
+ $transaction->commitOrRelease();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowAbsent('rtcCommit');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowPresent('rtcCommit');
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction rollback.
+ */
+ public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction callback executes after a DDL statement.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback executes after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is possible.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase(): void {
+ if (!$this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database supporting transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing the rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback failure after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is not possible, since a commit
+ * happened already. We cannot decide what should be the status of the
+ * callback, an exception is thrown.
+ */
+ public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * A post-transaction callback for testing purposes.
+ */
+ public function rootTransactionCallback(bool $success): void {
+ $this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback';
+ $this->insertRow($this->postTransactionCallbackAction);
+ }
+
+ /**
+ * Tests TransactionManager failure.
+ */
+ public function testTransactionManagerFailureOnPendingStackItems(): void {
+ $connectionInfo = Database::getConnectionInfo();
+ Database::addConnectionInfo('default', 'test_fail', $connectionInfo['default']);
+ $testConnection = Database::getConnection('test_fail');
+
+ // Add a fake item to the stack.
+ $manager = $testConnection->transactionManager();
+ $reflectionMethod = new \ReflectionMethod($manager, 'addStackItem');
+ $reflectionMethod->invoke($manager, 'bar', new StackItem('qux', StackItemType::Root));
+ // Ensure transaction state can be determined during object destruction.
+ // This is necessary for the test to pass when xdebug.mode has the 'develop'
+ // option enabled.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'connectionTransactionState');
+ $reflectionProperty->setValue($manager, ClientConnectionTransactionState::Active);
+
+ // Ensure that __destruct() results in an assertion error. Note that this
+ // will normally be called by PHP during the object's destruction but Drupal
+ // will commit all transactions when a database is closed thereby making
+ // this impossible to test unless it is called directly.
+ try {
+ $manager->__destruct();
+ $this->fail("Expected AssertionError error not thrown");
+ }
+ catch (\AssertionError $e) {
+ $this->assertStringStartsWith('Transaction $stack was not empty. Active stack: bar\\qux', $e->getMessage());
+ }
+
+ // Clean up.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'stack');
+ $reflectionProperty->setValue($manager, []);
+ unset($testConnection);
+ Database::closeConnection('test_fail');
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php
new file mode 100644
index 000000000000..e1542a30c381
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php
@@ -0,0 +1,109 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Datetime;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * Tests DrupalDateTime functionality.
+ *
+ * @group Datetime
+ */
+class DrupalDateTimeTest extends KernelTestBase {
+
+ use UserCreationTrait;
+
+ /**
+ * Set up required modules.
+ *
+ * @var string[]
+ */
+ protected static $modules = [
+ 'system',
+ 'user',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->installConfig(['system']);
+ $this->installEntitySchema('user');
+ }
+
+ /**
+ * Tests that DrupalDateTime can detect the right timezone to use.
+ *
+ * Test with a variety of less commonly used timezone names to
+ * help ensure that the system timezone will be different than the
+ * stated timezones.
+ */
+ public function testDateTimezone(): void {
+ $date_string = '2007-01-31 21:00:00';
+
+ // Make sure no site timezone has been set.
+ $this->config('system.date')
+ ->set('timezone.user.configurable', 0)
+ ->set('timezone.default', NULL)
+ ->save();
+
+ // Detect the system timezone.
+ $system_timezone = date_default_timezone_get();
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the system timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame($system_timezone, $timezone, 'DrupalDateTime uses the system timezone when there is no site timezone.');
+
+ // Create a date object with a specified timezone.
+ $date = new DrupalDateTime($date_string, 'America/Yellowknife');
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('America/Yellowknife', $timezone, 'DrupalDateTime uses the specified timezone if provided.');
+
+ // Set a site timezone.
+ $this->config('system.date')->set('timezone.default', 'Europe/Warsaw')->save();
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the site timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('Europe/Warsaw', $timezone, 'DrupalDateTime uses the site timezone if provided.');
+
+ // Create user.
+ $this->config('system.date')->set('timezone.user.configurable', 1)->save();
+ $this->setUpCurrentUser([
+ 'timezone' => 'Asia/Manila',
+ ]);
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the user timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('Asia/Manila', $timezone, 'DrupalDateTime uses the user timezone, if configurable timezones are used and it is set.');
+ }
+
+ /**
+ * Tests the ability to override the time zone in the format method.
+ */
+ public function testTimezoneFormat(): void {
+ // Create a date in UTC
+ $date = DrupalDateTime::createFromTimestamp(87654321, 'UTC');
+
+ // Verify that the date format method displays the default time zone.
+ $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date has default UTC time zone and correct date/time.');
+
+ // Verify that the format method can override the time zone.
+ $this->assertEquals('1972/10/11 08:25:21 America/New_York', $date->format('Y/m/d H:i:s e', ['timezone' => 'America/New_York']), 'Date displayed overridden time zone and correct date/time');
+
+ // Verify that the date format method still displays the default time zone
+ // for the date object.
+ $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date still has default UTC time zone and correct date/time');
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
index 3be9b023b2f7..54d72e9fbff5 100644
--- a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
+++ b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
@@ -7,7 +7,6 @@ namespace Drupal\KernelTests\Core\DrupalKernel;
use Composer\Autoload\ClassLoader;
use Drupal\Core\DrupalKernel;
use Drupal\Core\DrupalKernelInterface;
-use Drupal\Core\Utility\Error;
use Drupal\KernelTests\KernelTestBase;
use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
@@ -27,8 +26,7 @@ class DrupalKernelTest extends KernelTestBase {
* {@inheritdoc}
*/
protected function tearDown(): void {
- $currentErrorHandler = Error::currentErrorHandler();
- if (is_string($currentErrorHandler) && $currentErrorHandler === '_drupal_error_handler') {
+ if (get_error_handler() === '_drupal_error_handler') {
restore_error_handler();
}
parent::tearDown();
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php
new file mode 100644
index 000000000000..419d8bff4d43
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\entity_test\Entity\EntityTestNoBundleWithLabel;
+use Drupal\entity_test\Entity\EntityTestWithBundle;
+
+/**
+ * Tests the getBundleEntity() method.
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
+ *
+ * @group Entity
+ */
+class EntityBundleEntityTest extends EntityKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['entity_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->installEntitySchema('entity_test');
+ $this->installEntitySchema('entity_test_with_bundle');
+ $this->installEntitySchema('entity_test_no_bundle_with_label');
+ }
+
+ /**
+ * Tests an entity type with config entities for bundles.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithConfigBundleEntity(): void {
+ $bundleEntity = EntityTestBundle::create([
+ 'id' => 'bundle_alpha',
+ 'label' => 'Alpha',
+ ]);
+ $bundleEntity->save();
+
+ $entity = EntityTestWithBundle::create([
+ 'type' => 'bundle_alpha',
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertEquals($bundleEntity->id(), $entity->getBundleEntity()->id());
+ }
+
+ /**
+ * Tests an entity type without config entities for bundles.
+ *
+ * EntityTest doesn't have bundles, but does have the bundle entity key.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithoutBundleEntity(): void {
+ $entity = EntityTest::create([
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertNull($entity->getBundleEntity());
+ }
+
+ /**
+ * Tests an entity type without the bundle entity key.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithBundleKeyEntity(): void {
+ $entity = EntityTestNoBundleWithLabel::create([
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertNull($entity->getBundleEntity());
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
new file mode 100644
index 000000000000..a914752e9f17
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
@@ -0,0 +1,1036 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
+use Drupal\Core\Entity\ContentEntityType;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeEvents;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldException;
+use Drupal\Core\Field\FieldStorageDefinitionEvents;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\entity_test\EntityTestHelper;
+use Drupal\entity_test\FieldStorageDefinition;
+use Drupal\entity_test_update\Entity\EntityTestUpdate;
+use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
+
+/**
+ * Tests EntityDefinitionUpdateManager functionality.
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager
+ *
+ * @group Entity
+ * @group #slow
+ */
+class EntityDefinitionUpdateMultipleTypesTest extends EntityKernelTestBase {
+
+ use EntityDefinitionTestTrait;
+
+ /**
+ * The entity definition update manager.
+ *
+ * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+ */
+ protected $entityDefinitionUpdateManager;
+
+ /**
+ * The entity field manager.
+ *
+ * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+ */
+ protected $entityFieldManager;
+
+ /**
+ * The database connection.
+ *
+ * @var \Drupal\Core\Database\Connection
+ */
+ protected $database;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['entity_test_update', 'language'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
+ $this->entityFieldManager = $this->container->get('entity_field.manager');
+ $this->database = $this->container->get('database');
+
+ // Install every entity type's schema that wasn't installed in the parent
+ // method.
+ foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) {
+ $this->installEntitySchema($entity_type_id);
+ }
+ }
+
+ /**
+ * Tests when no definition update is needed.
+ */
+ public function testNoUpdates(): void {
+ // Ensure that the definition update manager reports no updates.
+ $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
+ $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.');
+ $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.');
+ }
+
+ /**
+ * Tests updating entity schema when there are no existing entities.
+ */
+ public function testEntityTypeUpdateWithoutData(): void {
+ // The 'entity_test_update' entity type starts out non-revisionable, so
+ // ensure the revision table hasn't been created during setUp().
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
+
+ // Update it to be revisionable and ensure the definition update manager
+ // reports that an update is needed.
+ $this->updateEntityTypeToRevisionable();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ // The revision key is now defined, so the revision field needs to be
+ // created.
+ 'The Revision ID field needs to be installed.',
+ 'The Default revision field needs to be installed.',
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the revision table is created.
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
+ }
+
+ /**
+ * Tests updating entity schema when there are entity storage changes.
+ */
+ public function testEntityTypeUpdateWithEntityStorageChange(): void {
+ // Update the entity type to be revisionable and try to apply the update.
+ // It's expected to throw an exception.
+ $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE);
+ try {
+ $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
+ $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.');
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests creating a fieldable entity type that doesn't exist in code anymore.
+ *
+ * @covers ::installFieldableEntityType
+ */
+ public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+ $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Install the entity type and check that its tables have been created.
+ $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions);
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.');
+ }
+
+ /**
+ * Tests updating an entity type that doesn't exist in code anymore.
+ *
+ * @covers ::updateEntityType
+ */
+ public function testUpdateEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Add an entity index, update the entity type and check that the index has
+ // been created.
+ $this->addEntityIndex();
+ $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
+
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+ }
+
+ /**
+ * Tests updating a fieldable entity type that doesn't exist in code anymore.
+ *
+ * @covers ::updateFieldableEntityType
+ */
+ public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+ $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Rename the base table, update the fieldable entity type and check that
+ // the table has been renamed.
+ $entity_type->set('base_table', 'entity_test_update_new');
+ $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
+
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.');
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.');
+ }
+
+ /**
+ * Tests uninstalling an entity type that doesn't exist in code anymore.
+ *
+ * @covers ::uninstallEntityType
+ */
+ public function testUninstallEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Now uninstall it and check that the tables have been removed.
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
+ $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
+ }
+
+ /**
+ * Tests uninstalling a revisionable entity type that doesn't exist in code.
+ *
+ * @covers ::uninstallEntityType
+ */
+ public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void {
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Now uninstall it and check that the tables have been removed.
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
+ $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
+ }
+
+ /**
+ * Tests creating, updating, and deleting a base field if no entities exist.
+ */
+ public function testBaseFieldCreateUpdateDeleteWithoutData(): void {
+ // Add a base field, ensure the update manager reports it, and the update
+ // creates its schema.
+ $this->addBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+
+ // Add an index on the base field, ensure the update manager reports it,
+ // and the update creates it.
+ $this->addBaseFieldIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
+
+ // Remove the above index, ensure the update manager reports it, and the
+ // update deletes it.
+ $this->removeBaseFieldIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
+
+ // Update the type of the base field from 'string' to 'text', ensure the
+ // update manager reports it, and the update adjusts the schema
+ // accordingly.
+ $this->modifyBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
+
+ // Remove the base field, ensure the update manager reports it, and the
+ // update deletes the schema.
+ $this->removeBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
+ }
+
+ /**
+ * Tests creating, updating, and deleting a base field with no label set.
+ *
+ * See testBaseFieldCreateUpdateDeleteWithoutData() for more details
+ */
+ public function testBaseFieldWithoutLabelCreateUpdateDelete(): void {
+ // Add a base field, ensure the update manager reports it with the
+ // field id.
+ $this->addBaseField('string', 'entity_test_update', FALSE, FALSE);
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+
+ // Add an index on the base field, ensure the update manager reports it with
+ // the field id.
+ $this->addBaseFieldIndex();
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+
+ // Remove the base field, ensure the update manager reports it with the
+ // field id.
+ $this->removeBaseField();
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ }
+
+ /**
+ * Tests creating, updating, and deleting a bundle field if no entities exist.
+ */
+ public function testBundleFieldCreateUpdateDeleteWithoutData(): void {
+ // Add a bundle field, ensure the update manager reports it, and the update
+ // creates its schema.
+ $this->addBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+
+ // Update the type of the base field from 'string' to 'text', ensure the
+ // update manager reports it, and the update adjusts the schema
+ // accordingly.
+ $this->modifyBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
+
+ // Remove the bundle field, ensure the update manager reports it, and the
+ // update deletes the schema.
+ $this->removeBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+ }
+
+ /**
+ * Tests creating and deleting a base field if entities exist.
+ *
+ * This tests deletion when there are existing entities, but non-existent data
+ * for the field being deleted.
+ *
+ * @see testBaseFieldDeleteWithExistingData()
+ */
+ public function testBaseFieldCreateDeleteWithExistingEntities(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Add a base field and run the update. Ensure the base field's column is
+ // created and the prior saved entity data is still there.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+ $schema_handler = $this->database->schema();
+ $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
+
+ // Remove the base field and run the update. Ensure the base field's column
+ // is deleted and the prior saved entity data is still there.
+ $this->removeBaseField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
+
+ // Add a base field with a required property and run the update. Ensure
+ // 'not null' is not applied and thus no exception is thrown.
+ $this->addBaseField('shape_required');
+ $this->applyEntityUpdates();
+ $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
+ $this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
+
+ // Recreate the field after emptying the base table and check that its
+ // columns are not 'not null'.
+ // @todo Revisit this test when allowing for required storage field
+ // definitions. See https://www.drupal.org/node/2390495.
+ $entity->delete();
+ $this->removeBaseField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.');
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.');
+ $this->addBaseField('shape_required');
+ $this->applyEntityUpdates();
+ $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
+ $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+ }
+
+ /**
+ * Tests creating and deleting a bundle field if entities exist.
+ *
+ * This tests deletion when there are existing entities, but non-existent data
+ * for the field being deleted.
+ *
+ * @see testBundleFieldDeleteWithExistingData()
+ */
+ public function testBundleFieldCreateDeleteWithExistingEntities(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Add a bundle field and run the update. Ensure the bundle field's table
+ // is created and the prior saved entity data is still there.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+ $schema_handler = $this->database->schema();
+ $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
+
+ // Remove the base field and run the update. Ensure the bundle field's
+ // table is deleted and the prior saved entity data is still there.
+ $this->removeBundleField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
+
+ // Test that required columns are created as 'not null'.
+ $this->addBundleField('shape_required');
+ $this->applyEntityUpdates();
+ $message = 'The new_bundle_field_shape column is not nullable.';
+ $values = [
+ 'bundle' => $entity->bundle(),
+ 'deleted' => 0,
+ 'entity_id' => $entity->id(),
+ 'revision_id' => $entity->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ 'delta' => 0,
+ 'new_bundle_field_color' => $this->randomString(),
+ ];
+ try {
+ // Try to insert a record without providing a value for the 'not null'
+ // column. This should fail.
+ $this->database->insert('entity_test_update__new_bundle_field')
+ ->fields($values)
+ ->execute();
+ $this->fail($message);
+ }
+ catch (IntegrityConstraintViolationException) {
+ // Now provide a value for the 'not null' column. This is expected to
+ // succeed.
+ $values['new_bundle_field_shape'] = $this->randomString();
+ $this->database->insert('entity_test_update__new_bundle_field')
+ ->fields($values)
+ ->execute();
+ }
+ }
+
+ /**
+ * Tests deleting a bundle field when it has existing data.
+ */
+ public function testBundleFieldDeleteWithExistingData(): void {
+ /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $schema_handler = $this->database->schema();
+
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+ $table_mapping = $storage->getTableMapping();
+ $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field'];
+
+ // Check that the bundle field has a dedicated table.
+ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+ $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.');
+
+ // Save an entity with the bundle field populated.
+ EntityTestHelper::createBundle('custom');
+ $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']);
+ $entity->save();
+
+ // Remove the bundle field and apply updates.
+ $this->removeBundleField();
+ $this->applyEntityUpdates();
+
+ // Check that the table of the bundle field has been renamed to use a
+ // 'deleted' table name.
+ $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.');
+
+ $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+ $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.');
+
+ // Check that the deleted field's data is preserved in the dedicated
+ // 'deleted' table.
+ $result = $this->database->select($dedicated_deleted_table_name, 't')
+ ->fields('t')
+ ->execute()
+ ->fetchAll();
+ $this->assertCount(1, $result);
+
+ $expected = [
+ 'bundle' => $entity->bundle(),
+ 'deleted' => '1',
+ 'entity_id' => $entity->id(),
+ 'revision_id' => $entity->id(),
+ 'langcode' => $entity->language()->getId(),
+ 'delta' => '0',
+ 'new_bundle_field_value' => $entity->new_bundle_field->value,
+ ];
+ // Use assertEquals and not assertSame here to prevent that a different
+ // sequence of the columns in the table will affect the check.
+ $this->assertEquals($expected, (array) $result[0]);
+
+ // Check that the field definition is marked for purging.
+ $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+ $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.');
+
+ // Check that the field storage definition is marked for purging.
+ $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+ $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.');
+
+ // Purge field data, and check that the storage definition has been
+ // completely removed once the data is purged.
+ field_purge_batch(10);
+ $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+ $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.');
+ $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+ $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.');
+ $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.');
+ }
+
+ /**
+ * Tests updating a base field when it has existing data.
+ */
+ public function testBaseFieldUpdateWithExistingData(): void {
+ // Add the base field and run the update.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the base field populated.
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
+
+ // Change the field's field type and apply updates. It's expected to
+ // throw an exception.
+ $this->modifyBaseField();
+ try {
+ $this->applyEntityUpdates();
+ $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+ }
+ catch (FieldStorageDefinitionUpdateForbiddenException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests updating a bundle field when it has existing data.
+ */
+ public function testBundleFieldUpdateWithExistingData(): void {
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the bundle field populated.
+ EntityTestHelper::createBundle('custom');
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
+
+ // Change the field's field type and apply updates. It's expected to
+ // throw an exception.
+ $this->modifyBundleField();
+ try {
+ $this->applyEntityUpdates();
+ $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+ }
+ catch (FieldStorageDefinitionUpdateForbiddenException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests updating a bundle field when the entity type schema has changed.
+ */
+ public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void {
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ // Update the entity type schema to revisionable but don't run the updates
+ // yet.
+ $this->updateEntityTypeToRevisionable();
+
+ // Perform a no-op update on the bundle field, which should work because
+ // both the storage and the storage schema are using the last installed
+ // entity type definition.
+ $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+ $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update'));
+ }
+
+ /**
+ * Tests creating and deleting a multi-field index when there are no existing entities.
+ */
+ public function testEntityIndexCreateDeleteWithoutData(): void {
+ // Add an entity index and ensure the update manager reports that as an
+ // update to the entity type.
+ $this->addEntityIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the new index is created.
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+
+ // Remove the index and ensure the update manager reports that as an
+ // update to the entity type.
+ $this->removeEntityIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the index is deleted.
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
+
+ // Test that composite indexes are handled correctly when dropping and
+ // re-creating one of their columns.
+ $this->addEntityIndex();
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+
+ $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update');
+ $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+ $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.');
+ }
+
+ /**
+ * Tests creating a multi-field index when there are existing entities.
+ */
+ public function testEntityIndexCreateWithData(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]);
+ $entity->save();
+
+ // Add an entity index, run the update. Ensure that the index is created
+ // despite having data.
+ $this->addEntityIndex();
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.');
+ }
+
+ /**
+ * Tests applying single updates.
+ */
+ public function testSingleActionCalls(): void {
+ $db_schema = $this->database->schema();
+
+ // Ensure that a non-existing entity type cannot be installed.
+ $message = 'A non-existing entity type cannot be installed';
+ try {
+ $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo']));
+ $this->fail($message);
+ }
+ catch (PluginNotFoundException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Ensure that a field cannot be installed on non-existing entity type.
+ $message = 'A field cannot be installed on a non-existing entity type';
+ try {
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition);
+ $this->fail($message);
+ }
+ catch (PluginNotFoundException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Ensure that installing an existing entity type is a no-op.
+ $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
+ $this->entityDefinitionUpdateManager->installEntityType($entity_type);
+ $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op');
+
+ // Create a new base field.
+ $this->addRevisionableBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+
+ // Ensure that installing an existing field is a no-op.
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op');
+
+ // Update an existing field schema.
+ $this->modifyBaseField();
+ $storage_definition = BaseFieldDefinition::create('text')
+ ->setName('new_base_field')
+ ->setTargetEntityTypeId('entity_test_update')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists.");
+ $this->assertTrue(
+ $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
+ "New schema for 'new_base_field' has been created."
+ );
+
+ // Drop an existing field schema.
+ $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
+ $this->assertFalse(
+ $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
+ "The schema for 'new_base_field' has been dropped."
+ );
+
+ // Make the entity type revisionable.
+ $this->updateEntityTypeToRevisionable();
+ $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update.");
+
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
+ }
+
+ /**
+ * Ensures that a new field and index on a shared table are created.
+ *
+ * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
+ */
+ public function testCreateFieldAndIndexOnSharedTable(): void {
+ $this->addBaseField();
+ $this->addBaseFieldIndex();
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
+ // Check index size in for MySQL.
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
+ $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
+ $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
+ }
+ }
+
+ /**
+ * Ensures that a new entity level index is created when data exists.
+ *
+ * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate
+ */
+ public function testCreateIndexUsingEntityStorageSchemaWithData(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Create an index.
+ $indexes = [
+ 'entity_test_update__type_index' => ['type'],
+ ];
+ $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
+ // Check index size in for MySQL.
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
+ $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
+ $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
+ }
+ }
+
+ /**
+ * Tests updating a base field when it has existing data.
+ */
+ public function testBaseFieldEntityKeyUpdateWithExistingData(): void {
+ // Add the base field and run the update.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the base field populated.
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
+
+ // Save an entity with the base field not populated.
+ /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->create();
+ $entity->save();
+
+ // Promote the base field to an entity key. This will trigger the addition
+ // of a NOT NULL constraint.
+ $this->makeBaseFieldEntityKey();
+
+ // Field storage CRUD operations use the last installed entity type
+ // definition so we need to update it before doing any other field storage
+ // updates.
+ $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
+
+ // Try to apply the update and verify they fail since we have a NULL value.
+ $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
+ try {
+ $this->applyEntityUpdates();
+ $this->fail($message);
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Check that the update is correctly applied when no NULL data is left.
+ $entity->set('new_base_field', $this->randomString());
+ $entity->save();
+ $this->applyEntityUpdates();
+
+ // Check that the update actually applied a NOT NULL constraint.
+ $entity->set('new_base_field', NULL);
+ $message = 'The NOT NULL constraint was correctly applied.';
+ try {
+ $entity->save();
+ $this->fail($message);
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Check that field schema is correctly handled with long-named fields.
+ */
+ public function testLongNameFieldIndexes(): void {
+ $this->addLongNameBaseField();
+ $entity_type_id = 'entity_test_update';
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+ $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type);
+ $name = 'new_long_named_entity_reference_base_field';
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]);
+ $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.');
+ }
+
+ /**
+ * Tests adding a base field with initial values.
+ */
+ public function testInitialValue(): void {
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+ $db_schema = $this->database->schema();
+
+ // Create two entities before adding the base field.
+ $storage->create()->save();
+ $storage->create()->save();
+
+ // Add a base field with an initial value.
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new base field')
+ ->setInitialValue('test value');
+
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+
+ // Check that the initial values have been applied.
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+ $entities = $storage->loadMultiple();
+ $this->assertEquals('test value', $entities[1]->get('new_base_field')->value);
+ $this->assertEquals('test value', $entities[2]->get('new_base_field')->value);
+ }
+
+ /**
+ * Tests entity type and field storage definition events.
+ */
+ public function testDefinitionEvents(): void {
+ /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */
+ $event_subscriber = $this->container->get('entity_test.definition.subscriber');
+ $event_subscriber->enableEventTracking();
+ $event_subscriber->enableLiveDefinitionUpdates();
+
+ // Test field storage definition events.
+ $storage_definition = FieldStorageDefinition::create('string')
+ ->setName('field_storage_test')
+ ->setLabel(new TranslatableMarkup('Field storage test'))
+ ->setTargetEntityTypeId('entity_test_rev');
+
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.');
+
+ // Check that the newly added field can be retrieved from the live field
+ // storage definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertArrayHasKey('field_storage_test', $field_storage_definitions);
+
+ $updated_storage_definition = clone $storage_definition;
+ $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test'));
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.');
+
+ // Check that the updated field can be retrieved from the live field storage
+ // definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel());
+
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.');
+
+ // Check that the deleted field can no longer be retrieved from the live
+ // field storage definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions);
+
+ // Test entity type events.
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
+
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.');
+
+ $updated_entity_type = clone $entity_type;
+ $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev'));
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.');
+
+ // Check that the updated definition can be retrieved from the live entity
+ // type definitions.
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
+ $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel());
+
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.');
+
+ // Check that the deleted entity type can no longer be retrieved from the
+ // live entity type definitions.
+ $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE));
+ }
+
+ /**
+ * Tests the error handling when using initial values from another field.
+ */
+ public function testInitialValueFromFieldErrorHandling(): void {
+ // Check that setting invalid values for 'initial value from field' doesn't
+ // work.
+ try {
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('field_that_does_not_exist');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->fail('Using a non-existent field as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage());
+ }
+
+ try {
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('integer')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('name');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->fail('Using a field of a different type as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage());
+ }
+
+ try {
+ // Add a base field that will not be stored in the shared tables.
+ $initial_field = BaseFieldDefinition::create('string')
+ ->setName('initial_field')
+ ->setLabel('An initial field')
+ ->setCardinality(2);
+ $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field);
+
+ // Now add the base field which will try to use the previously added field
+ // as the source of its initial values.
+ $new_base_field = BaseFieldDefinition::create('string')
+ ->setName('new_base_field')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('initial_field');
+ $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field);
+ $this->fail('Using a field that is not stored in the shared tables as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage());
+ }
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
index 6437e594ee8b..d184892a976a 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
@@ -4,21 +4,6 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Entity;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\IntegrityConstraintViolationException;
-use Drupal\Core\Entity\ContentEntityType;
-use Drupal\Core\Entity\EntityStorageException;
-use Drupal\Core\Entity\EntityTypeEvents;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldException;
-use Drupal\Core\Field\FieldStorageDefinitionEvents;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\entity_test\EntityTestHelper;
-use Drupal\entity_test\FieldStorageDefinition;
-use Drupal\entity_test_update\Entity\EntityTestUpdate;
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
/**
@@ -27,7 +12,6 @@ use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
* @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager
*
* @group Entity
- * @group #slow
*/
class EntityDefinitionUpdateTest extends EntityKernelTestBase {
@@ -67,12 +51,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
$this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
$this->entityFieldManager = $this->container->get('entity_field.manager');
$this->database = $this->container->get('database');
-
- // Install every entity type's schema that wasn't installed in the parent
- // method.
- foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) {
- $this->installEntitySchema($entity_type_id);
- }
}
/**
@@ -96,61 +74,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
}
/**
- * Tests when no definition update is needed.
- */
- public function testNoUpdates(): void {
- // Ensure that the definition update manager reports no updates.
- $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
- $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.');
- $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.');
- }
-
- /**
- * Tests updating entity schema when there are no existing entities.
- */
- public function testEntityTypeUpdateWithoutData(): void {
- // The 'entity_test_update' entity type starts out non-revisionable, so
- // ensure the revision table hasn't been created during setUp().
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
-
- // Update it to be revisionable and ensure the definition update manager
- // reports that an update is needed.
- $this->updateEntityTypeToRevisionable();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- // The revision key is now defined, so the revision field needs to be
- // created.
- 'The Revision ID field needs to be installed.',
- 'The Default revision field needs to be installed.',
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the revision table is created.
- $this->updateEntityTypeToRevisionable(TRUE);
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
- }
-
- /**
- * Tests updating entity schema when there are entity storage changes.
- */
- public function testEntityTypeUpdateWithEntityStorageChange(): void {
- // Update the entity type to be revisionable and try to apply the update.
- // It's expected to throw an exception.
- $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE);
- try {
- $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
- $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.');
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
* Tests installing an additional base field while installing an entity type.
*
* @covers ::installFieldableEntityType
@@ -173,909 +96,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
}
/**
- * Tests creating a fieldable entity type that doesn't exist in code anymore.
- *
- * @covers ::installFieldableEntityType
- */
- public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
- $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Install the entity type and check that its tables have been created.
- $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions);
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.');
- }
-
- /**
- * Tests updating an entity type that doesn't exist in code anymore.
- *
- * @covers ::updateEntityType
- */
- public function testUpdateEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Add an entity index, update the entity type and check that the index has
- // been created.
- $this->addEntityIndex();
- $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
-
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
- }
-
- /**
- * Tests updating a fieldable entity type that doesn't exist in code anymore.
- *
- * @covers ::updateFieldableEntityType
- */
- public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
- $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Rename the base table, update the fieldable entity type and check that
- // the table has been renamed.
- $entity_type->set('base_table', 'entity_test_update_new');
- $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
-
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.');
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.');
- }
-
- /**
- * Tests uninstalling an entity type that doesn't exist in code anymore.
- *
- * @covers ::uninstallEntityType
- */
- public function testUninstallEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Now uninstall it and check that the tables have been removed.
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
- $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
- }
-
- /**
- * Tests uninstalling a revisionable entity type that doesn't exist in code.
- *
- * @covers ::uninstallEntityType
- */
- public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void {
- $this->updateEntityTypeToRevisionable(TRUE);
- $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Now uninstall it and check that the tables have been removed.
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
- $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
- }
-
- /**
- * Tests creating, updating, and deleting a base field if no entities exist.
- */
- public function testBaseFieldCreateUpdateDeleteWithoutData(): void {
- // Add a base field, ensure the update manager reports it, and the update
- // creates its schema.
- $this->addBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
-
- // Add an index on the base field, ensure the update manager reports it,
- // and the update creates it.
- $this->addBaseFieldIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
-
- // Remove the above index, ensure the update manager reports it, and the
- // update deletes it.
- $this->removeBaseFieldIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
-
- // Update the type of the base field from 'string' to 'text', ensure the
- // update manager reports it, and the update adjusts the schema
- // accordingly.
- $this->modifyBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
-
- // Remove the base field, ensure the update manager reports it, and the
- // update deletes the schema.
- $this->removeBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
- }
-
- /**
- * Tests creating, updating, and deleting a base field with no label set.
- *
- * See testBaseFieldCreateUpdateDeleteWithoutData() for more details
- */
- public function testBaseFieldWithoutLabelCreateUpdateDelete(): void {
- // Add a base field, ensure the update manager reports it with the
- // field id.
- $this->addBaseField('string', 'entity_test_update', FALSE, FALSE);
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
-
- // Add an index on the base field, ensure the update manager reports it with
- // the field id.
- $this->addBaseFieldIndex();
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
-
- // Remove the base field, ensure the update manager reports it with the
- // field id.
- $this->removeBaseField();
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- }
-
- /**
- * Tests creating, updating, and deleting a bundle field if no entities exist.
- */
- public function testBundleFieldCreateUpdateDeleteWithoutData(): void {
- // Add a bundle field, ensure the update manager reports it, and the update
- // creates its schema.
- $this->addBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
-
- // Update the type of the base field from 'string' to 'text', ensure the
- // update manager reports it, and the update adjusts the schema
- // accordingly.
- $this->modifyBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
-
- // Remove the bundle field, ensure the update manager reports it, and the
- // update deletes the schema.
- $this->removeBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
- }
-
- /**
- * Tests creating and deleting a base field if entities exist.
- *
- * This tests deletion when there are existing entities, but non-existent data
- * for the field being deleted.
- *
- * @see testBaseFieldDeleteWithExistingData()
- */
- public function testBaseFieldCreateDeleteWithExistingEntities(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Add a base field and run the update. Ensure the base field's column is
- // created and the prior saved entity data is still there.
- $this->addBaseField();
- $this->applyEntityUpdates();
- $schema_handler = $this->database->schema();
- $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
-
- // Remove the base field and run the update. Ensure the base field's column
- // is deleted and the prior saved entity data is still there.
- $this->removeBaseField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
-
- // Add a base field with a required property and run the update. Ensure
- // 'not null' is not applied and thus no exception is thrown.
- $this->addBaseField('shape_required');
- $this->applyEntityUpdates();
- $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
- $this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
-
- // Recreate the field after emptying the base table and check that its
- // columns are not 'not null'.
- // @todo Revisit this test when allowing for required storage field
- // definitions. See https://www.drupal.org/node/2390495.
- $entity->delete();
- $this->removeBaseField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.');
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.');
- $this->addBaseField('shape_required');
- $this->applyEntityUpdates();
- $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
- $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
- }
-
- /**
- * Tests creating and deleting a bundle field if entities exist.
- *
- * This tests deletion when there are existing entities, but non-existent data
- * for the field being deleted.
- *
- * @see testBundleFieldDeleteWithExistingData()
- */
- public function testBundleFieldCreateDeleteWithExistingEntities(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Add a bundle field and run the update. Ensure the bundle field's table
- // is created and the prior saved entity data is still there.
- $this->addBundleField();
- $this->applyEntityUpdates();
- $schema_handler = $this->database->schema();
- $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
-
- // Remove the base field and run the update. Ensure the bundle field's
- // table is deleted and the prior saved entity data is still there.
- $this->removeBundleField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
-
- // Test that required columns are created as 'not null'.
- $this->addBundleField('shape_required');
- $this->applyEntityUpdates();
- $message = 'The new_bundle_field_shape column is not nullable.';
- $values = [
- 'bundle' => $entity->bundle(),
- 'deleted' => 0,
- 'entity_id' => $entity->id(),
- 'revision_id' => $entity->id(),
- 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
- 'delta' => 0,
- 'new_bundle_field_color' => $this->randomString(),
- ];
- try {
- // Try to insert a record without providing a value for the 'not null'
- // column. This should fail.
- $this->database->insert('entity_test_update__new_bundle_field')
- ->fields($values)
- ->execute();
- $this->fail($message);
- }
- catch (IntegrityConstraintViolationException) {
- // Now provide a value for the 'not null' column. This is expected to
- // succeed.
- $values['new_bundle_field_shape'] = $this->randomString();
- $this->database->insert('entity_test_update__new_bundle_field')
- ->fields($values)
- ->execute();
- }
- }
-
- /**
- * Tests deleting a bundle field when it has existing data.
- */
- public function testBundleFieldDeleteWithExistingData(): void {
- /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $schema_handler = $this->database->schema();
-
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
- $table_mapping = $storage->getTableMapping();
- $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field'];
-
- // Check that the bundle field has a dedicated table.
- $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
- $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.');
-
- // Save an entity with the bundle field populated.
- EntityTestHelper::createBundle('custom');
- $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']);
- $entity->save();
-
- // Remove the bundle field and apply updates.
- $this->removeBundleField();
- $this->applyEntityUpdates();
-
- // Check that the table of the bundle field has been renamed to use a
- // 'deleted' table name.
- $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.');
-
- $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
- $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.');
-
- // Check that the deleted field's data is preserved in the dedicated
- // 'deleted' table.
- $result = $this->database->select($dedicated_deleted_table_name, 't')
- ->fields('t')
- ->execute()
- ->fetchAll();
- $this->assertCount(1, $result);
-
- $expected = [
- 'bundle' => $entity->bundle(),
- 'deleted' => '1',
- 'entity_id' => $entity->id(),
- 'revision_id' => $entity->id(),
- 'langcode' => $entity->language()->getId(),
- 'delta' => '0',
- 'new_bundle_field_value' => $entity->new_bundle_field->value,
- ];
- // Use assertEquals and not assertSame here to prevent that a different
- // sequence of the columns in the table will affect the check.
- $this->assertEquals($expected, (array) $result[0]);
-
- // Check that the field definition is marked for purging.
- $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
- $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.');
-
- // Check that the field storage definition is marked for purging.
- $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
- $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.');
-
- // Purge field data, and check that the storage definition has been
- // completely removed once the data is purged.
- field_purge_batch(10);
- $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
- $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.');
- $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
- $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.');
- $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.');
- }
-
- /**
- * Tests updating a base field when it has existing data.
- */
- public function testBaseFieldUpdateWithExistingData(): void {
- // Add the base field and run the update.
- $this->addBaseField();
- $this->applyEntityUpdates();
-
- // Save an entity with the base field populated.
- $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
-
- // Change the field's field type and apply updates. It's expected to
- // throw an exception.
- $this->modifyBaseField();
- try {
- $this->applyEntityUpdates();
- $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
- }
- catch (FieldStorageDefinitionUpdateForbiddenException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Tests updating a bundle field when it has existing data.
- */
- public function testBundleFieldUpdateWithExistingData(): void {
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- // Save an entity with the bundle field populated.
- EntityTestHelper::createBundle('custom');
- $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
-
- // Change the field's field type and apply updates. It's expected to
- // throw an exception.
- $this->modifyBundleField();
- try {
- $this->applyEntityUpdates();
- $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
- }
- catch (FieldStorageDefinitionUpdateForbiddenException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Tests updating a bundle field when the entity type schema has changed.
- */
- public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void {
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- // Update the entity type schema to revisionable but don't run the updates
- // yet.
- $this->updateEntityTypeToRevisionable();
-
- // Perform a no-op update on the bundle field, which should work because
- // both the storage and the storage schema are using the last installed
- // entity type definition.
- $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
- $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update'));
- }
-
- /**
- * Tests creating and deleting a multi-field index when there are no existing entities.
- */
- public function testEntityIndexCreateDeleteWithoutData(): void {
- // Add an entity index and ensure the update manager reports that as an
- // update to the entity type.
- $this->addEntityIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the new index is created.
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
-
- // Remove the index and ensure the update manager reports that as an
- // update to the entity type.
- $this->removeEntityIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the index is deleted.
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
-
- // Test that composite indexes are handled correctly when dropping and
- // re-creating one of their columns.
- $this->addEntityIndex();
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
-
- $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update');
- $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
- $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.');
- }
-
- /**
- * Tests creating a multi-field index when there are existing entities.
- */
- public function testEntityIndexCreateWithData(): void {
- // Save an entity.
- $name = $this->randomString();
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]);
- $entity->save();
-
- // Add an entity index, run the update. Ensure that the index is created
- // despite having data.
- $this->addEntityIndex();
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.');
- }
-
- /**
- * Tests entity type and field storage definition events.
- */
- public function testDefinitionEvents(): void {
- /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */
- $event_subscriber = $this->container->get('entity_test.definition.subscriber');
- $event_subscriber->enableEventTracking();
- $event_subscriber->enableLiveDefinitionUpdates();
-
- // Test field storage definition events.
- $storage_definition = FieldStorageDefinition::create('string')
- ->setName('field_storage_test')
- ->setLabel(new TranslatableMarkup('Field storage test'))
- ->setTargetEntityTypeId('entity_test_rev');
-
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.');
-
- // Check that the newly added field can be retrieved from the live field
- // storage definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertArrayHasKey('field_storage_test', $field_storage_definitions);
-
- $updated_storage_definition = clone $storage_definition;
- $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test'));
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.');
-
- // Check that the updated field can be retrieved from the live field storage
- // definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel());
-
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.');
-
- // Check that the deleted field can no longer be retrieved from the live
- // field storage definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions);
-
- // Test entity type events.
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
-
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.');
-
- $updated_entity_type = clone $entity_type;
- $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev'));
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.');
-
- // Check that the updated definition can be retrieved from the live entity
- // type definitions.
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
- $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel());
-
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.');
-
- // Check that the deleted entity type can no longer be retrieved from the
- // live entity type definitions.
- $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE));
- }
-
- /**
- * Tests applying single updates.
- */
- public function testSingleActionCalls(): void {
- $db_schema = $this->database->schema();
-
- // Ensure that a non-existing entity type cannot be installed.
- $message = 'A non-existing entity type cannot be installed';
- try {
- $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo']));
- $this->fail($message);
- }
- catch (PluginNotFoundException) {
- // Expected exception; just continue testing.
- }
-
- // Ensure that a field cannot be installed on non-existing entity type.
- $message = 'A field cannot be installed on a non-existing entity type';
- try {
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition);
- $this->fail($message);
- }
- catch (PluginNotFoundException) {
- // Expected exception; just continue testing.
- }
-
- // Ensure that installing an existing entity type is a no-op.
- $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
- $this->entityDefinitionUpdateManager->installEntityType($entity_type);
- $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op');
-
- // Create a new base field.
- $this->addRevisionableBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
-
- // Ensure that installing an existing field is a no-op.
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op');
-
- // Update an existing field schema.
- $this->modifyBaseField();
- $storage_definition = BaseFieldDefinition::create('text')
- ->setName('new_base_field')
- ->setTargetEntityTypeId('entity_test_update')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists.");
- $this->assertTrue(
- $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
- "New schema for 'new_base_field' has been created."
- );
-
- // Drop an existing field schema.
- $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
- $this->assertFalse(
- $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
- "The schema for 'new_base_field' has been dropped."
- );
-
- // Make the entity type revisionable.
- $this->updateEntityTypeToRevisionable();
- $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update.");
-
- $this->updateEntityTypeToRevisionable(TRUE);
- $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
- }
-
- /**
- * Ensures that a new field and index on a shared table are created.
- *
- * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
- */
- public function testCreateFieldAndIndexOnSharedTable(): void {
- $this->addBaseField();
- $this->addBaseFieldIndex();
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
- // Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
- $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
- $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
- }
- }
-
- /**
- * Ensures that a new entity level index is created when data exists.
- *
- * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate
- */
- public function testCreateIndexUsingEntityStorageSchemaWithData(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Create an index.
- $indexes = [
- 'entity_test_update__type_index' => ['type'],
- ];
- $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
-
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
- // Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
- $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
- $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
- }
- }
-
- /**
- * Tests updating a base field when it has existing data.
- */
- public function testBaseFieldEntityKeyUpdateWithExistingData(): void {
- // Add the base field and run the update.
- $this->addBaseField();
- $this->applyEntityUpdates();
-
- // Save an entity with the base field populated.
- $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
-
- // Save an entity with the base field not populated.
- /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->create();
- $entity->save();
-
- // Promote the base field to an entity key. This will trigger the addition
- // of a NOT NULL constraint.
- $this->makeBaseFieldEntityKey();
-
- // Field storage CRUD operations use the last installed entity type
- // definition so we need to update it before doing any other field storage
- // updates.
- $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
-
- // Try to apply the update and verify they fail since we have a NULL value.
- $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
- try {
- $this->applyEntityUpdates();
- $this->fail($message);
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
-
- // Check that the update is correctly applied when no NULL data is left.
- $entity->set('new_base_field', $this->randomString());
- $entity->save();
- $this->applyEntityUpdates();
-
- // Check that the update actually applied a NOT NULL constraint.
- $entity->set('new_base_field', NULL);
- $message = 'The NOT NULL constraint was correctly applied.';
- try {
- $entity->save();
- $this->fail($message);
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Check that field schema is correctly handled with long-named fields.
- */
- public function testLongNameFieldIndexes(): void {
- $this->addLongNameBaseField();
- $entity_type_id = 'entity_test_update';
- $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
- $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type);
- $name = 'new_long_named_entity_reference_base_field';
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]);
- $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.');
- }
-
- /**
- * Tests adding a base field with initial values.
- */
- public function testInitialValue(): void {
- $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
- $db_schema = $this->database->schema();
-
- // Create two entities before adding the base field.
- $storage->create()->save();
- $storage->create()->save();
-
- // Add a base field with an initial value.
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new base field')
- ->setInitialValue('test value');
-
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
-
- // Check that the initial values have been applied.
- $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
- $entities = $storage->loadMultiple();
- $this->assertEquals('test value', $entities[1]->get('new_base_field')->value);
- $this->assertEquals('test value', $entities[2]->get('new_base_field')->value);
- }
-
- /**
- * Tests the error handling when using initial values from another field.
- */
- public function testInitialValueFromFieldErrorHandling(): void {
- // Check that setting invalid values for 'initial value from field' doesn't
- // work.
- try {
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new base field')
- ->setInitialValueFromField('field_that_does_not_exist');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->fail('Using a non-existent field as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage());
- }
-
- try {
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('integer')
- ->setLabel('A new base field')
- ->setInitialValueFromField('name');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->fail('Using a field of a different type as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage());
- }
-
- try {
- // Add a base field that will not be stored in the shared tables.
- $initial_field = BaseFieldDefinition::create('string')
- ->setName('initial_field')
- ->setLabel('An initial field')
- ->setCardinality(2);
- $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field);
-
- // Now add the base field which will try to use the previously added field
- // as the source of its initial values.
- $new_base_field = BaseFieldDefinition::create('string')
- ->setName('new_base_field')
- ->setLabel('A new base field')
- ->setInitialValueFromField('initial_field');
- $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field);
- $this->fail('Using a field that is not stored in the shared tables as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage());
- }
- }
-
- /**
* @covers ::getEntityTypes
*/
public function testGetEntityTypes(): void {
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php
new file mode 100644
index 000000000000..020ef21c2273
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\KernelTests\KernelTestBase;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+/**
+ * Tests the legacy requirements severity deprecations.
+ *
+ * @coversDefaultClass \Drupal\Core\Extension\Requirement\RequirementSeverity
+ * @group extension
+ * @group legacy
+ */
+class LegacyRequirementSeverityTest extends KernelTestBase {
+
+ /**
+ * @covers \drupal_requirements_severity
+ * @dataProvider requirementProvider
+ */
+ public function testGetMaxSeverity(array $requirements, int $expectedSeverity): void {
+ $this->expectDeprecation(
+ 'drupal_requirements_severity() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() instead. See https://www.drupal.org/node/3410939'
+ );
+ $this->expectDeprecation(
+ 'Calling Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939'
+ );
+ $severity = drupal_requirements_severity($requirements);
+ $this->assertEquals($expectedSeverity, $severity);
+ }
+
+ /**
+ * Data provider for requirement helper test.
+ *
+ * @return array
+ * Test data.
+ */
+ public static function requirementProvider(): array {
+ $info = [
+ 'title' => 'Foo',
+ 'severity' => \REQUIREMENT_INFO,
+ ];
+ $warning = [
+ 'title' => 'Baz',
+ 'severity' => \REQUIREMENT_WARNING,
+ ];
+ $error = [
+ 'title' => 'Wiz',
+ 'severity' => \REQUIREMENT_ERROR,
+ ];
+ $ok = [
+ 'title' => 'Bar',
+ 'severity' => \REQUIREMENT_OK,
+ ];
+
+ return [
+ 'error is most severe' => [
+ [
+ $info,
+ $error,
+ $ok,
+ ],
+ \REQUIREMENT_ERROR,
+ ],
+ 'ok is most severe' => [
+ [
+ $info,
+ $ok,
+ ],
+ \REQUIREMENT_OK,
+ ],
+ 'warning is most severe' => [
+ [
+ $warning,
+ $info,
+ $ok,
+ ],
+ \REQUIREMENT_WARNING,
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
index 8ee28a968fd5..c90cb5e0aad7 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
+use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\KernelTests\KernelTestBase;
/**
@@ -36,4 +37,78 @@ class ModuleHandlerTest extends KernelTestBase {
$this->assertNotNull(\Drupal::service('module_handler')->getName('module_test'));
}
+ /**
+ * Tests that resetImplementations() clears the hook memory cache.
+ *
+ * @covers ::resetImplementations
+ */
+ public function testResetImplementationsClearsHooks(): void {
+ $oldModuleHandler = \Drupal::moduleHandler();
+ $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler);
+
+ // Installing a module does not trigger ->resetImplementations().
+ /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */
+ $moduleInstaller = \Drupal::service('module_installer');
+ $moduleInstaller->install(['module_test']);
+ $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler);
+ // Only the new ModuleHandler instance has the updated implementations.
+ $moduleHandler = \Drupal::moduleHandler();
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+ $backupModuleList = $moduleHandler->getModuleList();
+ $moduleListWithout = array_diff_key($backupModuleList, ['module_test' => TRUE]);
+ $this->assertArrayHasKey('module_test', $backupModuleList);
+
+ // Silently setting the property does not clear the hooks cache.
+ $moduleListProperty = (new \ReflectionProperty($moduleHandler, 'moduleList'));
+ $this->assertSame($backupModuleList, $moduleListProperty->getValue($moduleHandler));
+ $moduleListProperty->setValue($moduleHandler, $moduleListWithout);
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Directly calling ->resetImplementations() clears the hook caches.
+ $moduleHandler->resetImplementations();
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleListProperty->setValue($moduleHandler, $backupModuleList);
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleHandler->resetImplementations();
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Calling ->setModuleList() triggers ->resetImplementations().
+ $moduleHandler->setModuleList(['system']);
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleHandler->setModuleList($backupModuleList);
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Uninstalling a module triggers ->resetImplementations().
+ /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */
+ $moduleInstaller = \Drupal::service('module_installer');
+ $moduleInstaller->uninstall(['module_test']);
+ $this->assertSame($moduleListWithout, $moduleHandler->getModuleList());
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ }
+
+ /**
+ * Asserts whether certain hook implementations exist.
+ *
+ * This is used to verify that all internal hook cache properties have been
+ * reset and updated.
+ *
+ * @param bool $exists
+ * TRUE if the implementations are expected to exist, FALSE if not.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+ * The module handler.
+ *
+ * @see \module_test_test_reset_implementations_hook()
+ * @see \module_test_test_reset_implementations_alter()
+ */
+ protected function assertHasResetHookImplementations(bool $exists, ModuleHandlerInterface $moduleHandler): void {
+ $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_hook'));
+ $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_alter'));
+ $expected_list = $exists ? ['module_test_test_reset_implementations_hook'] : [];
+ $this->assertSame($expected_list, $moduleHandler->invokeAll('test_reset_implementations_hook'));
+ $expected_alter_list = $exists ? ['module_test_test_reset_implementations_alter'] : [];
+ $alter_list = [];
+ $moduleHandler->alter('test_reset_implementations', $alter_list);
+ $this->assertSame($expected_alter_list, $alter_list);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
index 0cb098bfccab..ca1fab258f3c 100644
--- a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
@@ -45,8 +45,14 @@ class FileSystemRequirementsTest extends KernelTestBase {
* An array of system requirements.
*/
protected function checkSystemRequirements() {
+ // This loadInclude() is to ensure that the install API is available.
+ // Since we're loading an include of type 'install', this will also
+ // include core/includes/install.inc for us, which is where
+ // drupal_verify_install_file() is currently defined.
+ // @todo Remove this once the function lives in a better place.
+ // @see https://www.drupal.org/project/drupal/issues/3526388
$this->container->get('module_handler')->loadInclude('system', 'install');
- return system_requirements('runtime');
+ return \Drupal::moduleHandler()->invoke('system', 'runtime_requirements');
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
index dfe537e4d1fa..7d1e641f485a 100644
--- a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
@@ -92,6 +92,15 @@ class HtaccessTest extends KernelTestBase {
}
/**
+ * @covers ::write
+ */
+ public function testHtaccessSaveDisabled(): void {
+ $this->setSetting('auto_create_htaccess', FALSE);
+ $this->assertTrue($this->htaccessWriter->write($this->public, FALSE));
+ $this->assertFileDoesNotExist($this->public . '/.htaccess');
+ }
+
+ /**
* Asserts expected file permissions for a given file.
*
* @param string $uri
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
index ffc5a85fdb0b..8fc0c2013d29 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
@@ -17,7 +17,9 @@ use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/**
@@ -289,4 +291,82 @@ YAML
RecipeRunner::processRecipe($recipe);
}
+ /**
+ * Tests that the askHidden prompt forwards arguments correctly.
+ */
+ public function testAskHiddenPromptArgumentsForwarded(): void {
+ $input = $this->createMock(InputInterface::class);
+ $output = $this->createMock(OutputInterface::class);
+ $io = new SymfonyStyle($input, $output);
+
+ $recipe = $this->createRecipe(<<<YAML
+name: 'Prompt askHidden Test'
+input:
+ foo:
+ data_type: string
+ description: Foo
+ prompt:
+ method: askHidden
+ default:
+ source: value
+ value: bar
+YAML
+ );
+ $collector = new ConsoleInputCollector($input, $io);
+ // askHidden prompt should have an ArgumentCountError rather than a general
+ // error.
+ $this->expectException(\ArgumentCountError::class);
+ $recipe->input->collectAll($collector);
+ }
+
+ /**
+ * Tests getting default input values from environment variables.
+ */
+ public function testDefaultInputFromEnvironmentVariables(): void {
+ $this->config('system.site')
+ ->set('name', 'Hello Thar')
+ ->set('slogan', 'Very important')
+ ->save();
+
+ $recipe = $this->createRecipe(<<<YAML
+name: 'Input from environment variables'
+input:
+ name:
+ data_type: string
+ description: The name of the site.
+ default:
+ source: env
+ env: SITE_NAME
+ slogan:
+ data_type: string
+ description: The site slogan.
+ default:
+ source: env
+ env: SITE_SLOGAN
+config:
+ actions:
+ system.site:
+ simpleConfigUpdate:
+ name: \${name}
+ slogan: \${slogan}
+YAML
+ );
+ putenv('SITE_NAME=Input Test');
+
+ // Mock a collector that only returns the default value.
+ $collector = $this->createMock(InputCollectorInterface::class);
+ $collector->expects($this->any())
+ ->method('collectValue')
+ ->withAnyParameters()
+ ->willReturnArgument(2);
+ $recipe->input->collectAll($collector);
+
+ RecipeRunner::processRecipe($recipe);
+ $config = $this->config('system.site');
+ $this->assertSame('Input Test', $config->get('name'));
+ // There was no SITE_SLOGAN environment variable, so it should have been
+ // set to an empty string.
+ $this->assertSame('', $config->get('slogan'));
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
index 62f14df4d202..64b4c17869f5 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
@@ -761,6 +761,36 @@ extra:
YAML,
NULL,
];
+ yield 'input env variable name is not a string' => [
+ <<<YAML
+name: Bad input
+input:
+ bad_news:
+ data_type: string
+ description: 'Bad default definition'
+ default:
+ source: env
+ env: -40
+YAML,
+ [
+ '[input][bad_news][default][env]' => ['This value should be of type string.'],
+ ],
+ ];
+ yield 'input env variable name is empty' => [
+ <<<YAML
+name: Bad input
+input:
+ bad_news:
+ data_type: string
+ description: 'Bad default definition'
+ default:
+ source: env
+ env: ''
+YAML,
+ [
+ '[input][bad_news][default][env]' => ['This value should not be blank.'],
+ ],
+ ];
}
/**
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
index 09931b10f3a3..16c0a7fe12bf 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
@@ -43,7 +43,8 @@ class DeprecatedElementTest extends KernelTestBase {
], $info_manager->getInfo('deprecated_extends_form'));
// Ensure the constructor is triggering a deprecation error.
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
@@ -84,7 +85,8 @@ class DeprecatedElementTest extends KernelTestBase {
* Test use of static methods trigger deprecations.
*/
public function testDeprecatedStaticMethods(): void {
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php
new file mode 100644
index 000000000000..93470153f326
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Render\Element;
+
+use Drupal\Core\Render\Element\StatusReport;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the status report element legacy methods.
+ *
+ * @group Render
+ * @group legacy
+ */
+class LegacyStatusReportTest extends KernelTestBase {
+
+ /**
+ * Tests the getSeverities() method deprecation.
+ */
+ public function testGetSeveritiesDeprecation(): void {
+ $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::getSeverities() is deprecated in drupal:11.2.0 and is removed from in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3410939');
+ $severities = StatusReport::getSeverities();
+ $this->assertIsArray($severities);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
index 7ad8afa75be6..c300cd4c019f 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
@@ -23,7 +23,7 @@ class PluginAlterTest extends KernelTestBase {
$info_manager = $this->container->get('plugin.manager.element_info');
$this->assertArrayHasKey('weight', $info_manager->getDefinitions());
- // @see element_info_test_element_plugin_alter()
+ // @see ElementInfoTestHooks::elementPluginAlter().
$this->container->get('state')->set('hook_element_plugin_alter:remove_weight', TRUE);
// The definition will be cached.
$this->assertArrayHasKey('weight', $info_manager->getDefinitions());
@@ -33,4 +33,27 @@ class PluginAlterTest extends KernelTestBase {
$this->assertArrayNotHasKey('weight', $info_manager->getDefinitions());
}
+ /**
+ * Tests hook_element_plugin_alter().
+ */
+ public function testPluginClassSwap(): void {
+ $info_manager = $this->container->get('plugin.manager.element_info');
+ $test_details = [
+ '#type' => 'details',
+ '#title' => 'Title',
+ '#description' => 'Description',
+ '#open' => TRUE,
+ ];
+
+ // @see ElementInfoTestHooks::elementPluginAlter().
+ $expected = [
+ 'class' => 'Drupal\element_info_test\Render\Element\Details',
+ 'provider' => 'element_info_test',
+ 'id' => 'details',
+ ];
+ $this->assertEquals($expected, $info_manager->getDefinitions()['details']);
+ \Drupal::service('renderer')->renderRoot($test_details);
+ $this->assertArrayHasKey('#custom', $test_details);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php
new file mode 100644
index 000000000000..90e4a9c36be7
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Render\Element;
+
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\Render\Element\StatusReport;
+use Drupal\KernelTests\KernelTestBase;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+/**
+ * Tests the status report element.
+ *
+ * @group Render
+ * @group legacy
+ */
+class StatusReportTest extends KernelTestBase {
+
+ /**
+ * Tests the status report element.
+ */
+ public function testPreRenderGroupRequirements(): void {
+ $element = [
+ '#priorities' => [
+ 'error',
+ 'warning',
+ 'checked',
+ 'ok',
+ ],
+ '#requirements' => [
+ 'foo' => [
+ 'title' => 'Foo',
+ 'severity' => RequirementSeverity::Info,
+ ],
+ 'baz' => [
+ 'title' => 'Baz',
+ 'severity' => RequirementSeverity::Warning,
+ ],
+ 'wiz' => [
+ 'title' => 'Wiz',
+ 'severity' => RequirementSeverity::Error,
+ ],
+ 'bar' => [
+ 'title' => 'Bar',
+ 'severity' => RequirementSeverity::OK,
+ ],
+ 'legacy' => [
+ 'title' => 'Legacy',
+ 'severity' => \REQUIREMENT_OK,
+ ],
+ ],
+ ];
+
+ $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::preRenderGroupRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939');
+
+ $element = StatusReport::preRenderGroupRequirements($element);
+ $groups = $element['#grouped_requirements'];
+
+ $errors = $groups['error'];
+ $this->assertEquals('Errors found', (string) $errors['title']);
+ $this->assertEquals('error', $errors['type']);
+ $errorItems = $errors['items'];
+ $this->assertCount(1, $errorItems);
+ $this->assertArrayHasKey('wiz', $errorItems);
+
+ $warnings = $groups['warning'];
+ $this->assertEquals('Warnings found', (string) $warnings['title']);
+ $this->assertEquals('warning', $warnings['type']);
+ $warningItems = $warnings['items'];
+ $this->assertCount(1, $warningItems);
+ $this->assertArrayHasKey('baz', $warningItems);
+
+ $checked = $groups['checked'];
+ $this->assertEquals('Checked', (string) $checked['title']);
+ $this->assertEquals('checked', $checked['type']);
+ $checkedItems = $checked['items'];
+ $this->assertCount(3, $checkedItems);
+ $this->assertArrayHasKey('foo', $checkedItems);
+ $this->assertArrayHasKey('bar', $checkedItems);
+ $this->assertArrayHasKey('legacy', $checkedItems);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
index 00b7948f2a81..e36da16d9028 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
@@ -8,6 +8,7 @@ use Drupal\Core\Form\FormState;
use Drupal\Core\Render\Element\Number;
use Drupal\Core\Render\Element\Select;
use Drupal\Core\Render\Element\Weight;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\element_info_test\ElementInfoTestNumberBuilder;
use Drupal\KernelTests\KernelTestBase;
@@ -40,7 +41,7 @@ class WeightTest extends KernelTestBase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Weight([], 'weight', []);
+ $element_object = new Weight([], 'weight', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php
new file mode 100644
index 000000000000..a8697cc84968
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Test;
+
+use Drupal\Core\Test\PhpUnitTestDiscovery;
+use Drupal\Core\Test\TestDiscovery;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
+/**
+ * Tests ::findAllClassFiles() between TestDiscovery and PhpUnitTestDiscovery.
+ *
+ * PhpUnitTestDiscovery uses PHPUnit API to build the list of test classes,
+ * while TestDiscovery uses Drupal legacy code.
+ */
+#[CoversClass(PhpUnitTestDiscovery::class)]
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+class PhpUnitApiFindAllClassFilesTest extends KernelTestBase {
+
+ /**
+ * Checks that Drupal legacy and PHPUnit API based discoveries are equal.
+ */
+ #[DataProvider('argumentsProvider')]
+ #[IgnoreDeprecations]
+ public function testEquality(?string $extension = NULL, ?string $directory = NULL): void {
+ // PHPUnit discovery.
+ $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+ $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath);
+ $phpUnitList = $phpUnitTestDiscovery->findAllClassFiles($extension, $directory);
+
+ // Legacy TestDiscovery.
+ $testDiscovery = new TestDiscovery(
+ $this->container->getParameter('app.root'),
+ $this->container->get('class_loader')
+ );
+ $internalList = $testDiscovery->findAllClassFiles($extension, $directory);
+
+ // Downgrade results to make them comparable, working around bugs and
+ // additions.
+ // 1. TestDiscovery discovers non-test classes that PHPUnit does not.
+ $internalList = array_intersect_key($internalList, $phpUnitList);
+
+ $this->assertEquals($internalList, $phpUnitList);
+ }
+
+ /**
+ * Provides test data to ::testEquality.
+ */
+ public static function argumentsProvider(): \Generator {
+ yield 'All tests' => [];
+ yield 'Extension: system' => ['extension' => 'system'];
+ yield 'Extension: system, directory' => [
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php
new file mode 100644
index 000000000000..caedbc0d2b62
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Test;
+
+use Drupal\Core\Test\PhpUnitTestDiscovery;
+use Drupal\Core\Test\TestDiscovery;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
+/**
+ * Tests ::getTestClasses() between TestDiscovery and PhpPUnitTestDiscovery.
+ *
+ * PhpPUnitTestDiscovery uses PHPUnit API to build the list of test classes,
+ * while TestDiscovery uses Drupal legacy code.
+ */
+#[CoversClass(PhpUnitTestDiscovery::class)]
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+class PhpUnitApiGetTestClassesTest extends KernelTestBase {
+
+ /**
+ * Checks that Drupal legacy and PHPUnit API based discoveries are equal.
+ */
+ #[DataProvider('argumentsProvider')]
+ #[IgnoreDeprecations]
+ public function testEquality(array $suites, ?string $extension = NULL, ?string $directory = NULL): void {
+ // PHPUnit discovery.
+ $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+ $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath);
+ $phpUnitList = $phpUnitTestDiscovery->getTestClasses($extension, $suites, $directory);
+
+ // Legacy TestDiscovery.
+ $testDiscovery = new TestDiscovery(
+ $this->container->getParameter('app.root'),
+ $this->container->get('class_loader')
+ );
+ $internalList = $testDiscovery->getTestClasses($extension, $suites, $directory);
+
+ // Downgrade results to make them comparable, working around bugs and
+ // additions.
+ // 1. Remove TestDiscovery empty groups.
+ $internalList = array_filter($internalList);
+ // 2. Remove TestDiscovery '##no-group-annotations' group.
+ unset($internalList['##no-group-annotations']);
+ // 3. Remove 'file' and 'tests_count' keys from PHPUnit results.
+ foreach ($phpUnitList as &$group) {
+ foreach ($group as &$testClass) {
+ unset($testClass['file']);
+ unset($testClass['tests_count']);
+ }
+ }
+ // 4. Remove from PHPUnit results groups not found by TestDiscovery.
+ $phpUnitList = array_intersect_key($phpUnitList, $internalList);
+ // 5. Remove from PHPUnit groups classes not found by TestDiscovery.
+ foreach ($phpUnitList as $groupName => &$group) {
+ $group = array_intersect_key($group, $internalList[$groupName]);
+ }
+ // 6. Remove from PHPUnit test classes groups not found by TestDiscovery.
+ foreach ($phpUnitList as $groupName => &$group) {
+ foreach ($group as $testClassName => &$testClass) {
+ $testClass['groups'] = array_intersect_key($testClass['groups'], $internalList[$groupName][$testClassName]['groups']);
+ }
+ }
+
+ $this->assertEquals($internalList, $phpUnitList);
+ }
+
+ /**
+ * Provides test data to ::testEquality.
+ */
+ public static function argumentsProvider(): \Generator {
+ yield 'All tests' => ['suites' => []];
+ yield 'Testsuite: functional-javascript' => ['suites' => ['PHPUnit-FunctionalJavascript']];
+ yield 'Testsuite: functional' => ['suites' => ['PHPUnit-Functional']];
+ yield 'Testsuite: kernel' => ['suites' => ['PHPUnit-Kernel']];
+ yield 'Testsuite: unit' => ['suites' => ['PHPUnit-Unit']];
+ yield 'Testsuite: unit-component' => ['suites' => ['PHPUnit-Unit-Component']];
+ yield 'Testsuite: build' => ['suites' => ['PHPUnit-Build']];
+ yield 'Extension: system' => ['suites' => [], 'extension' => 'system'];
+ yield 'Extension: system, testsuite: unit' => [
+ 'suites' => ['PHPUnit-Unit'],
+ 'extension' => 'system',
+ ];
+ yield 'Extension: system, directory' => [
+ 'suites' => [],
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ yield 'Extension: system, testsuite: unit, directory' => [
+ 'suites' => ['PHPUnit-Unit'],
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
index 705981f75079..345cc9282d2e 100644
--- a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
@@ -7,8 +7,8 @@ namespace Drupal\KernelTests\Core\Test;
use Drupal\Core\Test\TestDiscovery;
use Drupal\KernelTests\KernelTestBase;
use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
-use PHPUnit\TextUI\Configuration\Builder;
-use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use Symfony\Component\Process\Process;
/**
@@ -22,11 +22,11 @@ use Symfony\Component\Process\Process;
* list thus generated, with the list generated by
* \Drupal\Core\Test\TestDiscovery, which is used by run-tests.sh, to ensure
* both methods will run the same tests,
- *
- * @group TestSuites
- * @group Test
- * @group #slow
*/
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+#[IgnoreDeprecations]
class PhpUnitTestDiscoveryTest extends KernelTestBase {
private const TEST_LIST_MISMATCH_MESSAGE =
@@ -76,11 +76,19 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase {
$internalList = array_unique($internalList);
asort($internalList);
+ // Location of PHPUnit configuration file.
+ $configurationFilePath = $this->root . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+
// PHPUnit's test discovery - via CLI execution.
$process = new Process([
'vendor/bin/phpunit',
'--configuration',
- 'core',
+ $configurationFilePath,
'--list-tests-xml',
$this->xmlOutputFile,
], $this->root);
@@ -112,27 +120,6 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase {
// Check against Drupal's discovery.
$this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
-
- // @todo once PHPUnit 10 is no longer used re-enable the rest of the test.
- // @see https://www.drupal.org/project/drupal/issues/3497116
- if (RunnerVersion::getMajor() >= 11) {
- $this->markTestIncomplete('On PHPUnit 11+ the test triggers warnings due to phpunit.xml setup. Re-enable in https://www.drupal.org/project/drupal/issues/3497116.');
- }
-
- // PHPUnit's test discovery - via API.
- $phpUnitConfiguration = (new Builder())->build(['--configuration', 'core']);
- $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration);
- $phpUnitApiList = [];
- foreach ($phpUnitTestSuite->tests() as $testSuite) {
- foreach ($testSuite->tests() as $test) {
- $phpUnitApiList[] = $test->name();
- }
- }
- asort($phpUnitApiList);
-
- // Check against Drupal's discovery.
- $this->assertEquals(implode("\n", $phpUnitApiList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
-
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
index f2f9bd16a04e..cd4fdc1258c8 100644
--- a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Updater;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\KernelTests\KernelTestBase;
@@ -27,7 +28,7 @@ class UpdateRequirementsTest extends KernelTestBase {
'title' => 'UpdateError',
'value' => 'None',
'description' => 'Update Error.',
- 'severity' => REQUIREMENT_ERROR,
+ 'severity' => RequirementSeverity::Error,
];
$requirements = update_check_requirements()['test.update.error'];
$this->assertEquals($testRequirements, $requirements);
@@ -36,7 +37,7 @@ class UpdateRequirementsTest extends KernelTestBase {
'title' => 'UpdateWarning',
'value' => 'None',
'description' => 'Update Warning.',
- 'severity' => REQUIREMENT_WARNING,
+ 'severity' => RequirementSeverity::Warning,
];
$alterRequirements = update_check_requirements()['test.update.error.alter'];
$this->assertEquals($testAlterRequirements, $alterRequirements);
diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php
new file mode 100644
index 000000000000..aa845c50b1da
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Validation;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the UriHost validator.
+ *
+ * @group Validation
+ */
+class UriHostValidatorTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['config_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->installConfig('config_test');
+ }
+
+ /**
+ * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint
+ */
+ public function testUriHost(): void {
+ $typed_config_manager = \Drupal::service('config.typed');
+ /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
+ $typed_config = $typed_config_manager->get('config_test.validation');
+
+ // Test valid names.
+ $typed_config->get('host')->setValue('example.com');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('example.com.');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('default');
+ $this->assertCount(0, $typed_config->validate());
+
+ // Test invalid names.
+ $typed_config->get('host')->setValue('.example.com');
+ $this->assertCount(1, $typed_config->validate());
+
+ // Test valid IPv6 literals.
+ $typed_config->get('host')->setValue('[::1]');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('[2001:DB8::]');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]');
+ $this->assertCount(0, $typed_config->validate());
+
+ // Test invalid IPv6 literals.
+ $typed_config->get('host')->setValue('::1');
+ $this->assertCount(1, $typed_config->validate());
+
+ // Test valid IPv4 addresses.
+ $typed_config->get('host')->setValue('127.0.0.1');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('192.0.2.254');
+ $this->assertCount(0, $typed_config->validate());
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
index 7c7d6aa0e39b..5889553a1491 100644
--- a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
@@ -44,48 +44,4 @@ class UuidValidatorTest extends KernelTestBase {
$this->assertCount(1, $typed_config->validate());
}
- /**
- * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint
- */
- public function testUriHost(): void {
- $typed_config_manager = \Drupal::service('config.typed');
- /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
- $typed_config = $typed_config_manager->get('config_test.validation');
-
- // Test valid names.
- $typed_config->get('host')->setValue('example.com');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('example.com.');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('default');
- $this->assertCount(0, $typed_config->validate());
-
- // Test invalid names.
- $typed_config->get('host')->setValue('.example.com');
- $this->assertCount(1, $typed_config->validate());
-
- // Test valid IPv6 literals.
- $typed_config->get('host')->setValue('[::1]');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('[2001:DB8::]');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]');
- $this->assertCount(0, $typed_config->validate());
-
- // Test invalid IPv6 literals.
- $typed_config->get('host')->setValue('::1');
- $this->assertCount(1, $typed_config->validate());
-
- // Test valid IPv4 addresses.
- $typed_config->get('host')->setValue('127.0.0.1');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('192.0.2.254');
- $this->assertCount(0, $typed_config->validate());
- }
-
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 34aca08f15a9..c45e937d371e 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -439,7 +439,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root, TRUE);
+ $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE);
Database::addConnectionInfo('default', 'default', $database);
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
index e8dfc3be6f45..828b4ab54ec0 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
@@ -25,7 +25,7 @@ class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase {
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
if (in_array($database['driver'], ['mysql', 'pgsql'])) {
// Change the used database driver to the one provided by the module
diff --git a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
index 02521bdda2f8..2f8b7309b520 100644
--- a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
+++ b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
@@ -286,7 +286,7 @@ class TestSiteApplicationTest extends KernelTestBase {
* The database key of the added connection.
*/
protected function addTestDatabase($db_prefix): string {
- $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'), $this->root);
+ $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'));
$database['prefix'] = $db_prefix;
$target = __CLASS__ . $db_prefix;
Database::addConnectionInfo($target, 'default', $database);
diff --git a/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js
new file mode 100644
index 000000000000..98916702a88f
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js
@@ -0,0 +1,77 @@
+// The javascript that creates dropbuttons is not present on the /page at
+// initial load. If the once data property is added then the JS was loaded
+// and triggered on the inserted content.
+// @see \Drupal\test_htmx\Controller\HtmxTestAttachmentsController
+// @see core/modules/system/tests/modules/test_htmx/js/reveal-merged-settings.js
+
+const scriptSelector = 'script[src*="test_htmx/js/behavior.js"]';
+const cssSelector = 'link[rel="stylesheet"][href*="test_htmx/css/style.css"]';
+const elementSelector = '.ajax-content';
+const elementInitSelector = `${elementSelector}[data-once="htmx-init"]`;
+
+module.exports = {
+ '@tags': ['core', 'htmx'],
+ before(browser) {
+ browser.drupalInstall({
+ setupFile: 'core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php',
+ installProfile: 'minimal',
+ });
+ },
+ afterEach(browser) {
+ browser.drupalLogAndEnd({ onlyOnError: true });
+ },
+ after(browser) {
+ browser.drupalUninstall();
+ },
+
+ 'Asset Load': (browser) => {
+ // Load the route htmx will use for the request on click and confirm the
+ // markup we will be looking for is present in the source markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/replace')
+ .waitForElementVisible('body', 1000)
+ .assert.elementPresent(elementInitSelector);
+ // Now load the page with the htmx enhanced button and verify the absence
+ // of the markup to be inserted. Click the button
+ // and check for inserted javascript and markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/page')
+ .waitForElementVisible('body', 1000)
+ .assert.not.elementPresent(scriptSelector)
+ .assert.not.elementPresent(cssSelector)
+ .waitForElementVisible('[name="replace"]', 1000)
+ .click('[name="replace"]')
+ .waitForElementVisible(elementSelector, 6000)
+ .waitForElementVisible(elementInitSelector, 6000)
+ .assert.elementPresent(scriptSelector)
+ .assert.elementPresent(cssSelector);
+ },
+
+ 'Ajax Load HTMX Element': (browser) => {
+ // Load the route htmx will use for the request on click and confirm the
+ // markup we will be looking for is present in the source markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/replace')
+ .waitForElementVisible('body', 1000)
+ .assert.elementPresent(scriptSelector);
+ // Now load the page with the ajax powered button. Click the button
+ // to insert an htmx enhanced button and verify the absence
+ // of the markup to be inserted. Click the button
+ // and check for inserted javascript and markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/ajax')
+ .waitForElementVisible('body', 1000)
+ .assert.not.elementPresent(scriptSelector)
+ .assert.not.elementPresent(cssSelector)
+ .waitForElementVisible('[data-drupal-selector="edit-ajax-button"]', 1000)
+ .pause(1000)
+ .click('[data-drupal-selector="edit-ajax-button"]')
+ .waitForElementVisible('[name="replace"]', 1000)
+ .pause(1000)
+ .click('[name="replace"]')
+ .waitForElementVisible(elementSelector, 6000)
+ .waitForElementVisible(elementInitSelector, 6000)
+ .assert.elementPresent(scriptSelector)
+ .assert.elementPresent(cssSelector);
+ },
+};
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
index 1459f0cdfee3..470d3ef92e22 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
@@ -280,18 +280,18 @@ class TestSiteInstallCommand extends Command {
}
/**
- * {@inheritdoc}
+ * Changes the database connection to the prefixed one.
*/
- protected function changeDatabasePrefix() {
+ protected function changeDatabasePrefix(): void {
// Ensure that we use the database from SIMPLETEST_DB environment variable.
Database::removeConnection('default');
$this->changeDatabasePrefixTrait();
}
/**
- * {@inheritdoc}
+ * Generates a database prefix for the site installation.
*/
- protected function prepareDatabasePrefix() {
+ protected function prepareDatabasePrefix(): void {
// Override this method so that we can force a lock to be created.
$test_db = new TestDatabase(NULL, TRUE);
$this->siteDirectory = $test_db->getTestSitePath();
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
index 3d80f3a1c178..7e601981d4c6 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
@@ -84,7 +84,7 @@ class TestSiteTearDownCommand extends Command {
protected function tearDown(TestDatabase $test_database, $db_url): void {
// Connect to the test database.
$root = dirname(__DIR__, 5);
- $database = Database::convertDbUrlToConnectionInfo($db_url, $root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
$database['prefix'] = $test_database->getDatabasePrefix();
Database::addConnectionInfo(__CLASS__, 'default', $database);
diff --git a/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php b/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php
new file mode 100644
index 000000000000..03bb3fbdb113
--- /dev/null
+++ b/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\TestSite;
+
+use Drupal\Core\Extension\ModuleInstallerInterface;
+use Drupal\Core\Extension\ThemeInstallerInterface;
+
+/**
+ * Setup file used by tests/src/Nightwatch/Tests/htmxAssetLoadTest.js.
+ *
+ * @see \Drupal\Tests\Scripts\TestSiteApplicationTest
+ */
+class HtmxAssetLoadTestSetup implements TestSetupInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setup(): void {
+ // Install Olivero and set it as the default theme.
+ $theme_installer = \Drupal::service('theme_installer');
+ assert($theme_installer instanceof ThemeInstallerInterface);
+ $theme_installer->install(['olivero'], TRUE);
+ $system_theme_config = \Drupal::configFactory()->getEditable('system.theme');
+ $system_theme_config->set('default', 'olivero')->save();
+
+ // Install required modules.
+ $module_installer = \Drupal::service('module_installer');
+ assert($module_installer instanceof ModuleInstallerInterface);
+ $module_installer->install(['test_htmx']);
+ }
+
+}
diff --git a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
index d60a4f3062c1..ed73ca8fd933 100644
--- a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
+++ b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\TestTools\Extension\DeprecationBridge;
-use Drupal\Core\Utility\Error;
use Drupal\TestTools\ErrorHandler\TestErrorHandler;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
@@ -41,7 +40,7 @@ trait ExpectDeprecationTrait {
}
DeprecationHandler::reset();
- set_error_handler(new TestErrorHandler(Error::currentErrorHandler(), $this));
+ set_error_handler(new TestErrorHandler(get_error_handler(), $this));
}
/**
@@ -61,8 +60,7 @@ trait ExpectDeprecationTrait {
// ::setUpErrorHandler() prior to the start of the test execution. If not,
// the error handler was changed during the test execution but not properly
// restored during ::tearDown().
- $handler = Error::currentErrorHandler();
- if (!$handler instanceof TestErrorHandler) {
+ if (!get_error_handler() instanceof TestErrorHandler) {
throw new \RuntimeException(sprintf('%s registered its own error handler without restoring the previous one before or during tear down. This can cause unpredictable test results. Ensure the test cleans up after itself.', $this->name()));
}
restore_error_handler();
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 12228dbea721..98aafad7df13 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -173,16 +173,6 @@ abstract class BrowserTestBase extends TestCase {
protected $originalShutdownCallbacks = [];
/**
- * The original container.
- *
- * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no
- * longer provides the same value.
- *
- * @var \Symfony\Component\DependencyInjection\ContainerInterface
- */
- protected $originalContainer;
-
- /**
* {@inheritdoc}
*/
public function __construct(string $name) {
@@ -512,8 +502,14 @@ abstract class BrowserTestBase extends TestCase {
*
* @return array
* Associative array of option keys and values.
+ *
+ * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is
+ * no direct replacement.
+ *
+ * @see https://www.drupal.org/node/3523039
*/
protected function getOptions($select, ?Element $container = NULL) {
+ @trigger_error(__METHOD__ . 'is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no direct replacement. See https://www.drupal.org/node/3523039', E_DEPRECATED);
if (is_string($select)) {
$select = $this->assertSession()->selectExists($select, $container);
}
diff --git a/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php
new file mode 100644
index 000000000000..7026a2ceac5a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Component\Gettext;
+
+use Drupal\Component\Gettext\PoItem;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Gettext\PoItem
+ * @group Gettext
+ */
+class PoItemTest extends TestCase {
+
+ /**
+ * @return array
+ * - Source string
+ * - Context (optional)
+ * - Translated string (optional)
+ * - Expected value
+ */
+ public static function providerStrings(): array {
+ // cSpell:disable
+ return [
+ [
+ '',
+ NULL,
+ NULL,
+ 'msgid ""' . "\n" . 'msgstr ""' . "\n\n",
+ ],
+ // Translated String without contesxt.
+ [
+ 'Next',
+ NULL,
+ 'Suivant',
+ 'msgid "Next"' . "\n" . 'msgstr "Suivant"' . "\n\n",
+ ],
+ // Translated string with context.
+ [
+ 'Apr',
+ 'Abbreviated month name',
+ 'Avr',
+ 'msgctxt "Abbreviated month name"' . "\n" . 'msgid "Apr"' . "\n" . 'msgstr "Avr"' . "\n\n",
+ ],
+ // Translated string with placeholder.
+ [
+ '%email is not a valid email address.',
+ NULL,
+ '%email n\'est pas une adresse de courriel valide.',
+ 'msgid "%email is not a valid email address."' . "\n" . 'msgstr "%email n\'est pas une adresse de courriel valide."' . "\n\n",
+ ],
+ // Translated Plural String without context.
+ [
+ ['Installed theme', 'Installed themes'],
+ NULL,
+ ['Thème installé', 'Thèmes installés'],
+ 'msgid "Installed theme"' . "\n" . 'msgid_plural "Installed themes"' . "\n" . 'msgstr[0] "Thème installé"' . "\n" . 'msgstr[1] "Thèmes installés"' . "\n\n",
+ ],
+ ];
+ // cSpell:enable
+ }
+
+ /**
+ * @dataProvider providerStrings
+ */
+ public function testFormat($source, $context, $translation, $expected): void {
+ $item = new PoItem();
+
+ $item->setSource($source);
+
+ if (is_array($source)) {
+ $item->setPlural(TRUE);
+ }
+ if (!empty($context)) {
+ $item->setContext($context);
+ }
+ if (!empty($translation)) {
+ $item->setTranslation($translation);
+ }
+
+ $this->assertEquals($expected, (string) $item);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php b/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php
new file mode 100644
index 000000000000..c3116c6f4988
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests accessible groups.
+ *
+ * @group Access
+ */
+class AccessGroupAndTest extends UnitTestCase {
+
+ use AccessibleTestingTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ }
+
+ /**
+ * @covers \Drupal\Core\Access\AccessGroupAnd
+ */
+ public function testGroups(): void {
+ $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed());
+ $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden());
+ $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral());
+
+ // Ensure that groups with no dependencies return a neutral access result.
+ $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isNeutral());
+
+ $andNeutral = new AccessGroupAnd();
+ $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible);
+ $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral());
+
+ $andForbidden = $andNeutral;
+ $andForbidden->addDependency($forbiddenAccessible);
+ $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden());
+
+ // Ensure that groups added to other groups works.
+ $andGroupsForbidden = new AccessGroupAnd();
+ $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ // Ensure you can add a non-group accessible object.
+ $andGroupsForbidden->addDependency($allowedAccessible);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php
new file mode 100644
index 000000000000..cf6d663a91a7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResultInterface;
+
+/**
+ * Helper methods testing accessible interfaces.
+ */
+trait AccessibleTestingTrait {
+
+ /**
+ * The test account.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $account;
+
+ /**
+ * Creates AccessibleInterface object from access result object for testing.
+ *
+ * @param \Drupal\Core\Access\AccessResultInterface $accessResult
+ * The accessible result to return.
+ *
+ * @return \Drupal\Core\Access\AccessibleInterface
+ * The AccessibleInterface object.
+ */
+ private function createAccessibleDouble(AccessResultInterface $accessResult) {
+ $accessible = $this->prophesize(AccessibleInterface::class);
+ $accessible->access('view', $this->account, TRUE)
+ ->willReturn($accessResult);
+ return $accessible->reveal();
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php
new file mode 100644
index 000000000000..43e924087ab3
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php
@@ -0,0 +1,161 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\RefinableDependentAccessInterface;
+use Drupal\Core\Access\RefinableDependentAccessTrait;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Access\RefinableDependentAccessTrait
+ *
+ * @group Access
+ */
+class DependentAccessTest extends UnitTestCase {
+ use AccessibleTestingTrait;
+
+ /**
+ * An accessible object that results in forbidden access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $forbidden;
+
+ /**
+ * An accessible object that results in neutral access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $neutral;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so'));
+ $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion'));
+ }
+
+ /**
+ * Tests that the previous dependency is replaced when using set.
+ *
+ * @covers ::setAccessDependency
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testSetAccessDependency($use_set_first): void {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ // Calling setAccessDependency() replaces the existing dependency.
+ $testRefinable->setAccessDependency($this->neutral);
+ $dependency = $testRefinable->getAccessDependency();
+ $this->assertNotInstanceOf(AccessGroupAnd::class, $dependency);
+ $accessResult = $dependency->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResult->getReason());
+ }
+
+ /**
+ * Tests merging a new dependency with existing non-group access dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeNonGroup($use_set_first): void {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\Core\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+ // Ensure the new dependency create a new AND group when merged.
+ $this->assertInstanceOf(AccessGroupAnd::class, $dependency);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+ }
+
+ /**
+ * Tests merging a new dependency with an existing access group dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeGroup($use_set_first): void {
+ $andGroup = new AccessGroupAnd();
+ $andGroup->addDependency($this->forbidden);
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($andGroup);
+ }
+ else {
+ $testRefinable->addAccessDependency($andGroup);
+ }
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\Core\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+
+ // Ensure the new dependency is merged with the existing group.
+ $this->assertInstanceOf(AccessGroupAnd::class, $dependency);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+ }
+
+ /**
+ * Data provider for all test methods.
+ *
+ * Provides test cases for calling setAccessDependency() or
+ * mergeAccessDependency() first. A call to either should behave the same on a
+ * new RefinableDependentAccessInterface object.
+ */
+ public static function providerTestSetFirst(): array {
+ return [
+ [TRUE],
+ [FALSE],
+ ];
+ }
+
+}
+
+/**
+ * Test class that implements RefinableDependentAccessInterface.
+ */
+class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface {
+
+ use RefinableDependentAccessTrait;
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
index ab5fce6d191c..9dd5727f2a4b 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CommandInterface;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
@@ -98,4 +99,95 @@ class AjaxResponseTest extends UnitTestCase {
$this->assertEquals('<textarea>[]</textarea>', $response->getContent());
}
+ /**
+ * Tests the mergeWith() method.
+ *
+ * @see \Drupal\Core\Ajax\AjaxResponse::mergeWith()
+ *
+ * @throws \PHPUnit\Framework\MockObject\Exception
+ */
+ public function testMergeWithOtherAjaxResponse(): void {
+ $response = new AjaxResponse([]);
+
+ $command_one = $this->createCommandMock('one');
+
+ $command_two = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\HtmlCommand', [
+ 'setting1' => 'value1',
+ 'setting2' => 'value2',
+ ], ['jquery', 'drupal'], 'two');
+ $command_three = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\InsertCommand', [
+ 'setting1' => 'overridden',
+ 'setting3' => 'value3',
+ ], ['jquery', 'ajax'], 'three');
+
+ $response->addCommand($command_one);
+ $response->addCommand($command_two);
+
+ $response2 = new AjaxResponse([]);
+ $response2->addCommand($command_three);
+
+ $response->mergeWith($response2);
+ self::assertEquals([
+ 'library' => ['jquery', 'drupal', 'jquery', 'ajax'],
+ 'drupalSettings' => [
+ 'setting1' => 'overridden',
+ 'setting2' => 'value2',
+ 'setting3' => 'value3',
+ ],
+ ], $response->getAttachments());
+ self::assertEquals([['command' => 'one'], ['command' => 'two'], ['command' => 'three']], $response->getCommands());
+ }
+
+ /**
+ * Creates a mock of a provided subclass of CommandInterface.
+ *
+ * Adds given settings and libraries to assets mock
+ * that is attached to the command mock.
+ *
+ * @param string $command_class_name
+ * The command class name to create the mock for.
+ * @param array|null $settings
+ * The settings to attach.
+ * @param array|null $libraries
+ * The libraries to attach.
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMockWithSettingsAndLibrariesAttachments(
+ string $command_class_name,
+ array|null $settings,
+ array|null $libraries,
+ string $command_name,
+ ): CommandInterface {
+ $command = $this->createMock($command_class_name);
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ $assets = $this->createMock('Drupal\Core\Asset\AttachedAssetsInterface');
+ $assets->expects($this->once())->method('getLibraries')->willReturn($libraries);
+ $assets->expects($this->once())->method('getSettings')->willReturn($settings);
+
+ $command->expects($this->once())->method('getAttachedAssets')->willReturn($assets);
+
+ return $command;
+ }
+
+ /**
+ * Creates a mock of the Drupal\Core\Ajax\CommandInterface.
+ *
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMock(string $command_name): CommandInterface {
+ $command = $this->createMock('Drupal\Core\Ajax\CommandInterface');
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ return $command;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
index 798735a2c8a1..9dc1a0d113ee 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
@@ -148,4 +148,40 @@ class CssCollectionOptimizerLazyUnitTest extends UnitTestCase {
self::assertStringEqualsFile(__DIR__ . '/css_test_files/css_license.css.optimized.aggregated.css', $aggregate);
}
+ /**
+ * Test that external minified CSS assets do not trigger optimization.
+ *
+ * This ensures that fully external asset groups do not result in a
+ * CssOptimizer exception and are safely ignored.
+ */
+ public function testExternalMinifiedCssAssetOptimizationIsSkipped(): void {
+ $mock_grouper = $this->createMock(AssetCollectionGrouperInterface::class);
+ $mock_optimizer = $this->createMock(AssetOptimizerInterface::class);
+ $mock_optimizer->expects($this->never())->method('optimize');
+
+ $optimizer = new CssCollectionOptimizerLazy(
+ $mock_grouper,
+ $mock_optimizer,
+ $this->createMock(ThemeManagerInterface::class),
+ $this->createMock(LibraryDependencyResolverInterface::class),
+ new RequestStack(),
+ $this->createMock(FileSystemInterface::class),
+ $this->createMock(ConfigFactoryInterface::class),
+ $this->createMock(FileUrlGeneratorInterface::class),
+ $this->createMock(TimeInterface::class),
+ $this->createMock(LanguageManagerInterface::class)
+ );
+ $optimizer->optimizeGroup([
+ 'items' => [
+ [
+ 'type' => 'external',
+ 'data' => __DIR__ . '/css_test_files/css_external.optimized.aggregated.css',
+ 'license' => FALSE,
+ 'preprocess' => TRUE,
+ 'minified' => TRUE,
+ ],
+ ],
+ ]);
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
index 7a75824bef44..0b26b97aae5f 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
@@ -117,7 +117,7 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all'],
],
[
- 0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all'),
+ 0 => $create_link_element('generated-relative-url:public://css/file-all?', 'all'),
],
],
// Single file CSS asset with custom attributes.
@@ -126,7 +126,7 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'attributes' => $custom_attributes],
],
[
- 0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all', $custom_attributes),
+ 0 => $create_link_element('generated-relative-url:public://css/file-all?', 'all', $custom_attributes),
],
],
// 31 file CSS assets: expect 31 link elements.
@@ -165,37 +165,37 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
30 => $create_file_css_asset('public://css/31.css'),
],
[
- 0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
- 1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
- 2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
- 3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
- 4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
- 5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
- 6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
- 7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
- 8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
- 9 => $create_link_element('generated-relative-url:public://css/10.css' . '?'),
- 10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
- 11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
- 12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
- 13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
- 14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
- 15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
- 16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
- 17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
- 18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
- 19 => $create_link_element('generated-relative-url:public://css/20.css' . '?'),
- 20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
- 21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
- 22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
- 23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
- 24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
- 25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
- 26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
- 27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
- 28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
- 29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
- 30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
+ 0 => $create_link_element('generated-relative-url:public://css/1.css?'),
+ 1 => $create_link_element('generated-relative-url:public://css/2.css?'),
+ 2 => $create_link_element('generated-relative-url:public://css/3.css?'),
+ 3 => $create_link_element('generated-relative-url:public://css/4.css?'),
+ 4 => $create_link_element('generated-relative-url:public://css/5.css?'),
+ 5 => $create_link_element('generated-relative-url:public://css/6.css?'),
+ 6 => $create_link_element('generated-relative-url:public://css/7.css?'),
+ 7 => $create_link_element('generated-relative-url:public://css/8.css?'),
+ 8 => $create_link_element('generated-relative-url:public://css/9.css?'),
+ 9 => $create_link_element('generated-relative-url:public://css/10.css?'),
+ 10 => $create_link_element('generated-relative-url:public://css/11.css?'),
+ 11 => $create_link_element('generated-relative-url:public://css/12.css?'),
+ 12 => $create_link_element('generated-relative-url:public://css/13.css?'),
+ 13 => $create_link_element('generated-relative-url:public://css/14.css?'),
+ 14 => $create_link_element('generated-relative-url:public://css/15.css?'),
+ 15 => $create_link_element('generated-relative-url:public://css/16.css?'),
+ 16 => $create_link_element('generated-relative-url:public://css/17.css?'),
+ 17 => $create_link_element('generated-relative-url:public://css/18.css?'),
+ 18 => $create_link_element('generated-relative-url:public://css/19.css?'),
+ 19 => $create_link_element('generated-relative-url:public://css/20.css?'),
+ 20 => $create_link_element('generated-relative-url:public://css/21.css?'),
+ 21 => $create_link_element('generated-relative-url:public://css/22.css?'),
+ 22 => $create_link_element('generated-relative-url:public://css/23.css?'),
+ 23 => $create_link_element('generated-relative-url:public://css/24.css?'),
+ 24 => $create_link_element('generated-relative-url:public://css/25.css?'),
+ 25 => $create_link_element('generated-relative-url:public://css/26.css?'),
+ 26 => $create_link_element('generated-relative-url:public://css/27.css?'),
+ 27 => $create_link_element('generated-relative-url:public://css/28.css?'),
+ 28 => $create_link_element('generated-relative-url:public://css/29.css?'),
+ 29 => $create_link_element('generated-relative-url:public://css/30.css?'),
+ 30 => $create_link_element('generated-relative-url:public://css/31.css?'),
],
],
// 32 file CSS assets with the same properties, except for the 10th and
@@ -236,38 +236,38 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
31 => $create_file_css_asset('public://css/32.css'),
],
[
- 0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
- 1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
- 2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
- 3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
- 4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
- 5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
- 6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
- 7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
- 8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
- 9 => $create_link_element('generated-relative-url:public://css/10.css' . '?', 'screen'),
- 10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
- 11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
- 12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
- 13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
- 14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
- 15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
- 16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
- 17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
- 18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
- 19 => $create_link_element('generated-relative-url:public://css/20.css' . '?', 'print'),
- 20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
- 21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
- 22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
- 23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
- 24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
- 25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
- 26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
- 27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
- 28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
- 29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
- 30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
- 31 => $create_link_element('generated-relative-url:public://css/32.css' . '?'),
+ 0 => $create_link_element('generated-relative-url:public://css/1.css?'),
+ 1 => $create_link_element('generated-relative-url:public://css/2.css?'),
+ 2 => $create_link_element('generated-relative-url:public://css/3.css?'),
+ 3 => $create_link_element('generated-relative-url:public://css/4.css?'),
+ 4 => $create_link_element('generated-relative-url:public://css/5.css?'),
+ 5 => $create_link_element('generated-relative-url:public://css/6.css?'),
+ 6 => $create_link_element('generated-relative-url:public://css/7.css?'),
+ 7 => $create_link_element('generated-relative-url:public://css/8.css?'),
+ 8 => $create_link_element('generated-relative-url:public://css/9.css?'),
+ 9 => $create_link_element('generated-relative-url:public://css/10.css?', 'screen'),
+ 10 => $create_link_element('generated-relative-url:public://css/11.css?'),
+ 11 => $create_link_element('generated-relative-url:public://css/12.css?'),
+ 12 => $create_link_element('generated-relative-url:public://css/13.css?'),
+ 13 => $create_link_element('generated-relative-url:public://css/14.css?'),
+ 14 => $create_link_element('generated-relative-url:public://css/15.css?'),
+ 15 => $create_link_element('generated-relative-url:public://css/16.css?'),
+ 16 => $create_link_element('generated-relative-url:public://css/17.css?'),
+ 17 => $create_link_element('generated-relative-url:public://css/18.css?'),
+ 18 => $create_link_element('generated-relative-url:public://css/19.css?'),
+ 19 => $create_link_element('generated-relative-url:public://css/20.css?', 'print'),
+ 20 => $create_link_element('generated-relative-url:public://css/21.css?'),
+ 21 => $create_link_element('generated-relative-url:public://css/22.css?'),
+ 22 => $create_link_element('generated-relative-url:public://css/23.css?'),
+ 23 => $create_link_element('generated-relative-url:public://css/24.css?'),
+ 24 => $create_link_element('generated-relative-url:public://css/25.css?'),
+ 25 => $create_link_element('generated-relative-url:public://css/26.css?'),
+ 26 => $create_link_element('generated-relative-url:public://css/27.css?'),
+ 27 => $create_link_element('generated-relative-url:public://css/28.css?'),
+ 28 => $create_link_element('generated-relative-url:public://css/29.css?'),
+ 29 => $create_link_element('generated-relative-url:public://css/30.css?'),
+ 30 => $create_link_element('generated-relative-url:public://css/31.css?'),
+ 31 => $create_link_element('generated-relative-url:public://css/32.css?'),
],
],
];
diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
index 6b8df5044f27..9db6abea52c1 100644
--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
@@ -87,7 +87,8 @@ class LibraryDiscoveryTest extends UnitTestCase {
* Tests getting a deprecated library.
*/
public function testAssetLibraryDeprecation(): void {
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
diff --git a/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
new file mode 100644
index 000000000000..dac82b6b80f6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
@@ -0,0 +1 @@
+/* Placeholder external CSS file. */
diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
index f9b25e8e7072..16fde5781255 100644
--- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -910,7 +910,7 @@ class ConnectionTest extends UnitTestCase {
#[IgnoreDeprecations]
#[DataProvider('providerSupportedLegacyFetchModes')]
public function testSupportedLegacyFetchModes(int $mode): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$mockPdo = $this->createMock(StubPDO::class);
$mockConnection = new StubConnection($mockPdo, []);
$statement = new StatementPrefetchIterator($mockPdo, $mockConnection, '');
@@ -921,7 +921,7 @@ class ConnectionTest extends UnitTestCase {
/**
* Provides data for testSupportedFetchModes.
*
- * @return array<string,array<\Drupal\Core\Database\FetchAs>>
+ * @return array<string,array<\Drupal\Core\Database\Statement\FetchAs>>
* The FetchAs cases.
*/
public static function providerSupportedFetchModes(): array {
@@ -975,7 +975,7 @@ class ConnectionTest extends UnitTestCase {
#[IgnoreDeprecations]
#[DataProvider('providerUnsupportedFetchModes')]
public function testUnsupportedFetchModes(int $mode): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/^Fetch mode FETCH_.* is not supported\\. Use supported modes only/");
$mockPdo = $this->createMock(StubPDO::class);
diff --git a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
index 7ecc4518d714..569a6e3fd18e 100644
--- a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
@@ -27,7 +27,7 @@ class RowCountExceptionTest extends UnitTestCase {
*/
public static function providerTestExceptionMessage() {
return [
- [static::DEFAULT_EXCEPTION_MESSAGE, ''],
+ [self::DEFAULT_EXCEPTION_MESSAGE, ''],
['test', 'test'],
];
}
@@ -47,7 +47,7 @@ class RowCountExceptionTest extends UnitTestCase {
*/
public function testExceptionMessageNull(): void {
$e = new RowCountException(NULL);
- $this->assertSame(static::DEFAULT_EXCEPTION_MESSAGE, $e->getMessage());
+ $this->assertSame(self::DEFAULT_EXCEPTION_MESSAGE, $e->getMessage());
}
}
diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
index e36767d2f5a4..c74090fb599b 100644
--- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
// cspell:ignore dummydb
@@ -31,7 +32,7 @@ class UrlConversionTest extends UnitTestCase {
* @dataProvider providerConvertDbUrlToConnectionInfo
*/
public function testDbUrlToConnectionConversion($url, $database_array, $include_test_drivers): void {
- $result = Database::convertDbUrlToConnectionInfo($url, $this->root, $include_test_drivers);
+ $result = Database::convertDbUrlToConnectionInfo($url, $include_test_drivers);
$this->assertEquals($database_array, $result);
}
@@ -279,10 +280,10 @@ class UrlConversionTest extends UnitTestCase {
*
* @dataProvider providerInvalidArgumentsUrlConversion
*/
- public function testGetInvalidArgumentExceptionInUrlConversion($url, $root, $expected_exception_message): void {
+ public function testGetInvalidArgumentExceptionInUrlConversion($url, $expected_exception_message): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage($expected_exception_message);
- Database::convertDbUrlToConnectionInfo($url, $root);
+ Database::convertDbUrlToConnectionInfo($url);
}
/**
@@ -291,14 +292,12 @@ class UrlConversionTest extends UnitTestCase {
* @return array
* Array of arrays with the following elements:
* - An invalid URL string.
- * - Drupal root string.
* - The expected exception message.
*/
public static function providerInvalidArgumentsUrlConversion() {
return [
- ['foo', '', "Missing scheme in URL 'foo'"],
- ['foo', 'bar', "Missing scheme in URL 'foo'"],
- ['foo/bar/baz', 'bar2', "Missing scheme in URL 'foo/bar/baz'"],
+ ['foo', "Missing scheme in URL 'foo'"],
+ ['foo/bar/baz', "Missing scheme in URL 'foo/bar/baz'"],
];
}
@@ -307,7 +306,7 @@ class UrlConversionTest extends UnitTestCase {
*/
public function testNoModuleSpecifiedDefaultsToDriverName(): void {
$url = 'dummydb://test_user:test_pass@test_host/test_database';
- $connection_info = Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ $connection_info = Database::convertDbUrlToConnectionInfo($url, TRUE);
$expected = [
'driver' => 'dummydb',
'username' => 'test_user',
@@ -518,7 +517,7 @@ class UrlConversionTest extends UnitTestCase {
$url = 'foo_bar_mysql://test_user:test_pass@test_host:3306/test_database?module=foo_bar';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\foo_bar\\Driver\\Database\\foo_bar_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
}
/**
@@ -528,7 +527,16 @@ class UrlConversionTest extends UnitTestCase {
$url = 'driver_test_mysql://test_user:test_pass@test_host:3306/test_database?module=driver_test';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\driver_test\\Driver\\Database\\driver_test_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
+ }
+
+ /**
+ * @covers ::convertDbUrlToConnectionInfo
+ */
+ #[IgnoreDeprecations]
+ public function testDeprecationOfRootParameter(): void {
+ $this->expectDeprecation('Passing a string $root value to Drupal\\Core\\Database\\Database::convertDbUrlToConnectionInfo() is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287');
+ Database::convertDbUrlToConnectionInfo('sqlite://localhost/test_database', $this->root, TRUE);
}
}
diff --git a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
index ad6f98c3cf78..bd64798d2406 100644
--- a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
+++ b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
@@ -19,18 +19,55 @@ class FinderTest extends UnitTestCase {
*/
public function testFoundDataIsInDependencyOrder(): void {
$finder = new Finder(__DIR__ . '/../../../../fixtures/default_content');
+ $actual_order = array_keys($finder->data);
- $expected_order = [
- // First is the author of the node.
- '94503467-be7f-406c-9795-fc25baa22203',
- // Next, the taxonomy term referenced by the node.
- '550f86ad-aa11-4047-953f-636d42889f85',
- // Then we have the node itself, since it has no other dependencies.
- 'e1714f23-70c0-4493-8e92-af1901771921',
- // Finally, the menu link to the node.
- '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b',
- ];
- $this->assertSame($expected_order, array_slice(array_keys($finder->data), 0, 4));
+ $node_uuid = 'e1714f23-70c0-4493-8e92-af1901771921';
+ // The author of the node should come before the node itself. We're using
+ // named arguments here purely for clarity.
+ $this->assertRelativeOrder(
+ $actual_order,
+ earlier: '94503467-be7f-406c-9795-fc25baa22203',
+ later: $node_uuid,
+ );
+ // Same with the taxonomy term referenced by the node.
+ $this->assertRelativeOrder(
+ $actual_order,
+ earlier: '550f86ad-aa11-4047-953f-636d42889f85',
+ later: $node_uuid,
+ );
+ // The menu link to the node should come after the node.
+ $this->assertRelativeOrder(
+ $actual_order,
+ earlier: $node_uuid,
+ later: '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b',
+ );
+
+ // A node that is in a workspace should come after the workspace itself.
+ $this->assertRelativeOrder(
+ $actual_order,
+ earlier: '384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d',
+ later: '48475954-e878-439c-9d3d-226724a44269',
+ );
+ }
+
+ /**
+ * Asserts that an item in an array comes before another item in that array.
+ *
+ * @param array $haystack
+ * The array to examine.
+ * @param mixed $earlier
+ * The item which should come first.
+ * @param mixed $later
+ * The item which should come after.
+ */
+ private function assertRelativeOrder(array $haystack, mixed $earlier, mixed $later): void {
+ $haystack = array_values($haystack);
+ $earlier_index = array_search($earlier, $haystack, TRUE);
+ $later_index = array_search($later, $haystack, TRUE);
+ $this->assertIsInt($earlier_index);
+ $this->assertIsInt($later_index);
+ // "Later" should be greater than "earlier".
+ $this->assertGreaterThan($earlier_index, $later_index);
}
/**
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
index c910a3c99c77..7af0c490d074 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\DependencyInjection\Compiler;
+use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@@ -92,6 +94,16 @@ class BackendCompilerPassTest extends UnitTestCase {
$container->setDefinition('DriverTestMysql.service', new Definition(__NAMESPACE__ . '\\ServiceClassDriverTestMysql'));
$this->backendPass->process($container);
$this->assertEquals($prefix . 'DriverTestMysql', get_class($container->get('service')));
+
+ // Verify that if the container has a default_backend parameter,
+ // and there is a service named ".my-service", the right alias is created.
+ $container = $this->getMockDriverContainerWithDefaultBackendParameterArgumentAndDotPrefixedService();
+ $this->backendPass->process($container);
+
+ // Verify that if the db service returns no driver, no invalid aliases are
+ // created.
+ $container = $this->getMockDriverContainerWithNullDriverBackend();
+ $this->backendPass->process($container);
}
/**
@@ -154,6 +166,82 @@ class BackendCompilerPassTest extends UnitTestCase {
return $container;
}
+ /**
+ * Creates a container with a database mock definition in it.
+ *
+ * This mock won't declare a driver nor databaseType to ensure no invalid
+ * aliases are set.
+ *
+ * @return \Symfony\Component\DependencyInjection\ContainerBuilder
+ * The container with a mock database service in it.
+ */
+ protected function getMockDriverContainerWithNullDriverBackend(): ContainerBuilder&MockObject {
+ $container = $this->getMockBuilder(ContainerBuilder::class)->getMock();
+ $mock = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
+ $mock->expects($this->once())
+ ->method('driver')
+ ->willReturn(NULL);
+ $mock->expects($this->once())
+ ->method('databaseType')
+ ->willReturn(NULL);
+ $container->expects($this->any())
+ ->method('get')
+ ->with('database')
+ ->willReturn($mock);
+ $container->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->willReturn(['fakeService' => ['class' => 'fakeServiceClass']]);
+ $container->expects($this->never())
+ ->method('hasDefinition')
+ ->with('.fakeService')
+ ->willReturn(TRUE);
+ $container->expects($this->never())
+ ->method('setAlias');
+ return $container;
+ }
+
+ /**
+ * Creates a container with a database mock definition in it.
+ *
+ * This mock container has a default_backend parameter and a dot-prefixed
+ * service to verify the right aliases are set.
+ *
+ * @return \Symfony\Component\DependencyInjection\ContainerBuilder
+ * The container with a mock database service in it.
+ */
+ protected function getMockDriverContainerWithDefaultBackendParameterArgumentAndDotPrefixedService(): ContainerBuilder&MockObject {
+ $container = $this->getMockBuilder(ContainerBuilder::class)->getMock();
+ $container->expects($this->once())
+ ->method('hasParameter')
+ ->with('default_backend')
+ ->willReturn(TRUE);
+ $container->expects($this->once())
+ ->method('getParameter')
+ ->with('default_backend')
+ ->willReturn('a_valid_default_backend');
+
+ $mock = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
+ $mock->expects($this->never())
+ ->method('driver');
+ $mock->expects($this->never())
+ ->method('databaseType');
+ $container->expects($this->any())
+ ->method('get')
+ ->with('database')
+ ->willReturn($mock);
+ $container->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->willReturn(['fakeService' => ['class' => 'fakeServiceClass']]);
+ $container->expects($this->once())
+ ->method('hasDefinition')
+ ->with('a_valid_default_backend.fakeService')
+ ->willReturn(TRUE);
+ $container->expects($this->once())
+ ->method('setAlias')
+ ->with('fakeService', new Alias('a_valid_default_backend.fakeService'));
+ return $container;
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
index df504f4b7269..cdfb897046c6 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
@@ -6,6 +6,7 @@ namespace Drupal\Tests\Core\DependencyInjection\Compiler;
use Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass;
use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
@@ -307,6 +308,31 @@ class TaggedHandlersPassTest extends UnitTestCase {
}
/**
+ * Tests child handler with parent service.
+ *
+ * @covers ::process
+ */
+ public function testProcessChildDefinition(): void {
+ $container = $this->buildContainer();
+
+ $container
+ ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer')
+ ->addTag('service_collector');
+ $container
+ ->register('root_handler', __NAMESPACE__ . '\ValidHandler');
+ $container->addDefinitions([
+ 'parent_handler' => new ChildDefinition('root_handler'),
+ 'child_handler' => (new ChildDefinition('parent_handler'))->addTag('consumer_id'),
+ ]);
+
+ $handler_pass = new TaggedHandlersPass();
+ $handler_pass->process($container);
+
+ $method_calls = $container->getDefinition('consumer_id')->getMethodCalls();
+ $this->assertCount(1, $method_calls);
+ }
+
+ /**
* Tests consumer method with extra parameters.
*
* @covers ::process
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
new file mode 100644
index 000000000000..29d3d17dba0f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Entity;
+
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Entity\EntityViewBuilder
+ * @group Entity
+ */
+class EntityViewBuilderTest extends UnitTestCase {
+
+ const string ENTITY_TYPE_ID = 'test_entity_type';
+
+ /**
+ * The entity view builder under test.
+ *
+ * @var \Drupal\Core\Entity\EntityViewBuilder
+ */
+ protected EntityViewBuilder $viewBuilder;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->viewBuilder = new class() extends EntityViewBuilder {
+
+ public function __construct() {
+ $this->entityTypeId = EntityViewBuilderTest::ENTITY_TYPE_ID;
+ }
+
+ };
+ }
+
+ /**
+ * Tests build components using a mocked Iterator.
+ */
+ public function testBuildComponents(): void {
+ $field_name = $this->randomMachineName();
+ $bundle = $this->randomMachineName();
+ $entity_id = mt_rand(20, 30);
+ $field_item_list = $this->createStub(FieldItemListInterface::class);
+ $item = new \stdClass();
+ $this->setupMockIterator($field_item_list, [$item]);
+ $entity = $this->createConfiguredStub(FieldableEntityInterface::class, [
+ 'bundle' => $bundle,
+ 'hasField' => TRUE,
+ 'get' => $field_item_list,
+ ]);
+ $formatter_result = [
+ $entity_id => ['#' . $this->randomMachineName() => $this->randomString()],
+ ];
+ $display = $this->createConfiguredStub(EntityViewDisplayInterface::class, [
+ 'getComponents' => [$field_name => []],
+ 'buildMultiple' => $formatter_result,
+ ]);
+ $entities = [$entity_id => $entity];
+ $displays = [$bundle => $display];
+ $build = [$entity_id => []];
+ $view_mode = $this->randomMachineName();
+ // Assert the hook is invoked.
+ $module_handler = $this->createMock(ModuleHandlerInterface::class);
+ $module_handler->expects($this->once())
+ ->method('invokeAll')
+ ->with('entity_prepare_view', [self::ENTITY_TYPE_ID, $entities, $displays, $view_mode]);
+ $this->viewBuilder->setModuleHandler($module_handler);
+ $this->viewBuilder->buildComponents($build, $entities, $displays, $view_mode);
+ $this->assertSame([], $item->_attributes);
+ $this->assertSame($formatter_result, $build);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php b/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php
new file mode 100644
index 000000000000..f91af07ddcbc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Extension\Requirement;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\Requirement\RequirementSeverity
+ *
+ * @group Extension
+ */
+class RequirementSeverityTest extends UnitTestCase {
+
+ /**
+ * @covers ::convertLegacyIntSeveritiesToEnums
+ * @group legacy
+ */
+ public function testConvertLegacySeverities(): void {
+ $requirements['foo'] = [
+ 'title' => new TranslatableMarkup('Foo'),
+ 'severity' => \REQUIREMENT_INFO,
+ ];
+ $requirements['bar'] = [
+ 'title' => new TranslatableMarkup('Bar'),
+ 'severity' => \REQUIREMENT_ERROR,
+ ];
+ $this->expectDeprecation(
+ 'Calling ' . __METHOD__ . '() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939'
+ );
+ RequirementSeverity::convertLegacyIntSeveritiesToEnums($requirements, __METHOD__);
+ $this->assertEquals(
+ RequirementSeverity::Info,
+ $requirements['foo']['severity']
+ );
+ $this->assertEquals(
+ RequirementSeverity::Error,
+ $requirements['bar']['severity']
+ );
+ }
+
+ /**
+ * @covers ::maxSeverityFromRequirements
+ * @dataProvider requirementProvider
+ */
+ public function testGetMaxSeverity(array $requirements, RequirementSeverity $expectedSeverity): void {
+ $severity = RequirementSeverity::maxSeverityFromRequirements($requirements);
+ $this->assertEquals($expectedSeverity, $severity);
+ }
+
+ /**
+ * Data provider for requirement helper test.
+ */
+ public static function requirementProvider(): array {
+ $info = [
+ 'title' => 'Foo',
+ 'severity' => RequirementSeverity::Info,
+ ];
+ $warning = [
+ 'title' => 'Baz',
+ 'severity' => RequirementSeverity::Warning,
+ ];
+ $error = [
+ 'title' => 'Wiz',
+ 'severity' => RequirementSeverity::Error,
+ ];
+ $ok = [
+ 'title' => 'Bar',
+ 'severity' => RequirementSeverity::OK,
+ ];
+
+ return [
+ 'error is most severe' => [
+ [
+ $info,
+ $error,
+ $ok,
+ ],
+ RequirementSeverity::Error,
+ ],
+ 'ok is most severe' => [
+ [
+ $info,
+ $ok,
+ ],
+ RequirementSeverity::OK,
+ ],
+ 'warning is most severe' => [
+ [
+ $warning,
+ $info,
+ $ok,
+ ],
+ RequirementSeverity::Warning,
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index d7a364170f04..f2afab3f8a9c 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -1002,6 +1002,57 @@ class FormBuilderTest extends FormTestBase {
];
}
+ /**
+ * Tests the detection of the triggering element.
+ */
+ public function testTriggeringElement(): void {
+ $form_arg = 'Drupal\Tests\Core\Form\TestForm';
+
+ // No triggering element.
+ $form_state = new FormState();
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $this->assertNull($form_state->getTriggeringElement());
+
+ // When no op is provided, default to the first button element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+ $this->assertSame('Submit', $triggeringElement['#value']);
+
+ // A single triggering element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'op' => 'Submit']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+
+ // A different triggering element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'other_action' => 'Other action']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('other_action', $triggeringElement['#name']);
+
+ // Two triggering elements.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'op' => 'Submit', 'other_action' => 'Other action']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+
+ // Verify that only the first triggering element is respected.
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc b/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
index 0e34dd9110d5..0e633183d331 100644
--- a/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
+++ b/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
@@ -36,5 +36,10 @@ function test_form_id() {
'#type' => 'submit',
'#value' => 'Submit',
];
+ $form['actions']['other_action'] = [
+ '#type' => 'submit',
+ '#name' => 'other_action',
+ '#value' => 'Other action',
+ ];
return $form;
}
diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
index 08fa2eceaf54..d0759a4bf082 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
@@ -203,6 +203,8 @@ class LocalActionManagerTest extends UnitTestCase {
}
public static function getActionsForRouteProvider() {
+ $originalContainer = \Drupal::hasContainer() ? \Drupal::getContainer() : NULL;
+
$cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens(Argument::any())
->willReturn(TRUE);
@@ -384,6 +386,11 @@ class LocalActionManagerTest extends UnitTestCase {
],
];
+ // Restore the original container if needed.
+ if ($originalContainer) {
+ \Drupal::setContainer($originalContainer);
+ }
+
return $data;
}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php b/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php
new file mode 100644
index 000000000000..d05413eb444e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Plugin;
+
+use Drupal\Core\Plugin\ConfigurablePluginBase;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests ConfigurablePluginBase.
+ *
+ * @group Plugin
+ *
+ * @coversDefaultClass \Drupal\Core\Plugin\ConfigurablePluginBase
+ */
+class ConfigurablePluginBaseTest extends TestCase {
+
+ /**
+ * Tests the Constructor.
+ */
+ public function testConstructor(): void {
+ $provided_configuration = [
+ 'foo' => 'bar',
+ ];
+ $merged_configuration = ['default' => 'default'] + $provided_configuration;
+ $plugin = new ConfigurablePluginBaseTestClass($provided_configuration, '', []);
+ $this->assertSame($merged_configuration, $plugin->getConfiguration());
+ }
+
+}
+
+/**
+ * Test class for ConfigurablePluginBase.
+ */
+class ConfigurablePluginBaseTestClass extends ConfigurablePluginBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return [
+ 'default' => 'default',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php b/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php
new file mode 100644
index 000000000000..21543658769c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Plugin;
+
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurableTrait;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests for ConfigurableTrait.
+ *
+ * @group Plugin
+ *
+ * @coversDefaultClass \Drupal\Core\Plugin\ConfigurableTrait
+ */
+class ConfigurableTraitTest extends TestCase {
+
+ /**
+ * Tests ConfigurableTrait::defaultConfiguration.
+ *
+ * @covers ::defaultConfiguration
+ */
+ public function testDefaultConfiguration(): void {
+ /** @var \Drupal\Component\Plugin\ConfigurableInterface $configurable_plugin */
+ $configurable_plugin = new ConfigurableTestClass();
+ $this->assertSame([], $configurable_plugin->defaultConfiguration());
+ }
+
+ /**
+ * Tests ConfigurableTrait::getConfiguration.
+ *
+ * @covers ::getConfiguration
+ */
+ public function testGetConfiguration(): void {
+ $test_configuration = [
+ 'config_key_1' => 'config_value_1',
+ 'config_key_2' => [
+ 'nested_key_1' => 'nested_value_1',
+ 'nested_key_2' => 'nested_value_2',
+ ],
+ ];
+ $configurable_plugin = new ConfigurableTestClass($test_configuration);
+ $this->assertSame($test_configuration, $configurable_plugin->getConfiguration());
+ }
+
+ /**
+ * Tests configurableTrait::setConfiguration.
+ *
+ * Specifically test the way default and provided configurations are merged.
+ *
+ * @param array $default_configuration
+ * The default configuration to use for the trait.
+ * @param array $test_configuration
+ * The configuration to test.
+ * @param array $final_configuration
+ * The expected final plugin configuration.
+ *
+ * @covers ::setConfiguration
+ *
+ * @dataProvider setConfigurationDataProvider
+ */
+ public function testSetConfiguration(array $default_configuration, array $test_configuration, array $final_configuration): void {
+ $test_object = new ConfigurableTestClass();
+ $test_object->setDefaultConfiguration($default_configuration);
+ $test_object->setConfiguration($test_configuration);
+ $this->assertSame($final_configuration, $test_object->getConfiguration());
+ }
+
+ /**
+ * Provides data for testSetConfiguration.
+ *
+ * @return array
+ * The data.
+ */
+ public static function setConfigurationDataProvider(): array {
+ return [
+ 'Direct Override' => [
+ 'default_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'default_nested_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ ],
+ ],
+ 'test_configuration' => [
+ 'default_key_1' => 'override_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_nested_value_1',
+ 'default_nested_key_2' => 'override_nested_value_2',
+ ],
+ ],
+ 'final_configuration' => [
+ 'default_key_1' => 'override_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_nested_value_1',
+ 'default_nested_key_2' => 'override_nested_value_2',
+ ],
+ ],
+ ],
+ 'Mixed Override' => [
+ 'default_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'default_nested_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ ],
+ ],
+ 'test_configuration' => [
+ 'override_key_1' => 'config_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_value_1',
+ 'override_nested_key' => 'override_value',
+ ],
+ ],
+ 'final_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ 'override_nested_key' => 'override_value',
+ ],
+ 'override_key_1' => 'config_value_1',
+ ],
+ ],
+ 'indexed_override' => [
+ 'default_configuration' => [
+ 'config_value_1',
+ 'config_value_2',
+ 'config_value_3',
+ ],
+ 'test_configuration' => [
+ 'override_value_1',
+ 'override_value_2',
+ ],
+ 'final_configuration' => [
+ 'override_value_1',
+ 'override_value_2',
+ 'config_value_3',
+ ],
+ ],
+ 'indexed_override_complex' => [
+ 'default_configuration' => [
+ 'config_value_1',
+ 'config_value_2',
+ 'config_value_3',
+ ],
+ 'test_configuration' => [
+ 0 => 'override_value_1',
+ 2 => 'override_value_3',
+ ],
+ 'final_configuration' => [
+ 'override_value_1',
+ 'config_value_2',
+ 'override_value_3',
+ ],
+ ],
+ ];
+ }
+
+}
+
+/**
+ * A test class using ConfigurablePluginTrait that can modify the de.
+ */
+class ConfigurableTestClass extends PluginBase implements ConfigurableInterface {
+ use ConfigurableTrait {
+ defaultConfiguration as traitDefaultConfiguration;
+ }
+
+ /**
+ * A default configuration for the test class to return.
+ *
+ * @var array|null
+ */
+ protected ?array $defaultConfiguration = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(array $configuration = [], string $plugin_id = '', array $plugin_definition = []) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->setConfiguration($configuration);
+ }
+
+ /**
+ * Sets the default configuration this test will return.
+ *
+ * @param array $default_configuration
+ * The default configuration to use.
+ */
+ public function setDefaultConfiguration(array $default_configuration): void {
+ $this->defaultConfiguration = $default_configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return $this->defaultConfiguration ?? $this->traitDefaultConfiguration();
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php b/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
index 99da7a2f9741..60e147f434a5 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
@@ -4,8 +4,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Plugin;
-use Drupal\Component\Plugin\ConfigurableInterface;
-use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurablePluginBase;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
@@ -98,24 +97,5 @@ class DefaultSingleLazyPluginCollectionTest extends LazyPluginCollectionTestBase
/**
* Stub configurable plugin class for testing.
*/
-class ConfigurablePlugin extends PluginBase implements ConfigurableInterface {
-
- public function __construct(array $configuration, $plugin_id, $plugin_definition) {
- parent::__construct($configuration, $plugin_id, $plugin_definition);
-
- $this->configuration = $configuration + $this->defaultConfiguration();
- }
-
- public function defaultConfiguration() {
- return [];
- }
-
- public function getConfiguration() {
- return $this->configuration;
- }
-
- public function setConfiguration(array $configuration): void {
- $this->configuration = $configuration;
- }
-
+class ConfigurablePlugin extends ConfigurablePluginBase {
}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php b/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
index 55695ace6de7..7b72a3df4530 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
@@ -4,35 +4,13 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Plugin\Fixtures;
-use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
-use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurablePluginBase;
/**
* A configurable plugin implementation used for testing.
*/
-class TestConfigurablePlugin extends PluginBase implements ConfigurableInterface, DependentPluginInterface {
-
- /**
- * {@inheritdoc}
- */
- public function getConfiguration() {
- return $this->configuration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setConfiguration(array $configuration) {
- $this->configuration = $configuration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function defaultConfiguration() {
- return [];
- }
+class TestConfigurablePlugin extends ConfigurablePluginBase implements DependentPluginInterface {
/**
* {@inheritdoc}
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
index d0c09e97fc53..8490a5c0876e 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
@@ -6,6 +6,7 @@ namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Tests\Core\Render\RendererTestBase;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\Element\HtmlTag;
/**
@@ -18,7 +19,7 @@ class HtmlTagTest extends RendererTestBase {
* @covers ::getInfo
*/
public function testGetInfo(): void {
- $htmlTag = new HtmlTag([], 'test', 'test');
+ $htmlTag = new HtmlTag([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $htmlTag->getInfo();
$this->assertArrayHasKey('#pre_render', $info);
$this->assertArrayHasKey('#attributes', $info);
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php
new file mode 100644
index 000000000000..c91b74f8c0a4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Render\Element;
+
+use Drupal\Component\Plugin\Factory\FactoryInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Element\Textfield;
+use Drupal\Core\Render\ElementInfoManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Element\RenderElementBase
+ * @group Render
+ */
+class ModernRenderElementTest extends UnitTestCase {
+
+ public function testChildren(): void {
+ $factory = $this->createMock(FactoryInterface::class);
+ $elementInfoManager = new class ($factory) extends ElementInfoManager {
+
+ public function __construct(protected $factory) {}
+
+ };
+ $factory->expects($this->any())
+ ->method('createInstance')
+ ->willReturnCallback(fn () => new Textfield([], '', NULL, $elementInfoManager));
+ // If the type is not given ::fromRenderable presumes "form" and uses the
+ // plugin discovery to find which class provides the form element. This
+ // test does not set up discovery so some type must be provided.
+ $element = ['#type' => 'ignored by the mock factory'];
+ $elementObject = $elementInfoManager->fromRenderable($element);
+ for ($i = 0; $i <= 2; $i++) {
+ $child = [
+ '#type' => 'ignored by the mock factory',
+ '#test' => $i,
+ ];
+ $elementObject->addChild("test$i", $child);
+ // addChild() takes the $child render array by reference and stores a
+ // reference to it in the render object. To avoid modifying the
+ // previously created render object when reusing the $child variable,
+ // unset() it to break the reference before reassigning.
+ unset($child);
+ }
+ foreach ([1 => ['test0', 'test1', 'test2'], 2 => ['test0', 'test2']] as $delta => $expectedChildrenKeys) {
+ $i = 0;
+ foreach ($elementObject->getChildren() as $name => $child) {
+ $this->assertSame($name, "test$i");
+ $this->assertSame($i, $child->test);
+ $i += $delta;
+ }
+ $this->assertSame(Element::children($elementObject->toRenderable()), $expectedChildrenKeys);
+ // The first iteration tests removing an existing child. The second
+ // iteration tests removing a nonexistent child.
+ $elementObject->removeChild('test1');
+ }
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
index fc58c1db4efe..7acfef4ca509 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\Form\FormState;
use Drupal\Core\Link;
use Drupal\Core\Render\Element\Tableselect;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
@@ -25,7 +26,7 @@ class TableSelectTest extends UnitTestCase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Tableselect([], 'table_select', []);
+ $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
@@ -50,7 +51,7 @@ class TableSelectTest extends UnitTestCase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Tableselect([], 'table_select', []);
+ $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index 2b897fe40a1c..78f2f60e62a6 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -78,7 +78,7 @@ class RendererPlaceholdersTest extends RendererTestBase {
// \Drupal\Core\Render\Markup::create() is necessary as the render
// system would mangle this markup. As this is exactly what happens at
// runtime this is a valid use-case.
- return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
+ return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
};
$extract_placeholder_render_array = function ($placeholder_render_array) {
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 69301eae9bb8..e4fbfa60caf7 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -1097,7 +1097,7 @@ class RendererTest extends RendererTestBase {
'max-age' => 600,
],
],
- new \stdClass(),
+ (new CacheableMetadata())->setCacheMaxAge(0),
[
'#cache' => [
'contexts' => ['theme'],
@@ -1126,6 +1126,68 @@ class RendererTest extends RendererTestBase {
$this->assertFalse($this->renderer->hasRenderContext());
}
+ /**
+ * @covers ::executeInRenderContext
+ */
+ public function testExecuteInRenderContext(): void {
+ $return = $this->renderer->executeInRenderContext(new RenderContext(), function () {
+ $fiber_callback = function () {
+
+ // Create a #pre_render callback that renders a render array in
+ // isolation. This has its own #pre_render callback that calls
+ // Fiber::suspend(). This ensures that suspending a Fiber within
+ // multiple nested calls to ::executeInRenderContext() doesn't
+ // allow render context to get out of sync. This simulates similar
+ // conditions to BigPipe placeholder rendering.
+ $fiber_suspend_pre_render = function ($elements) {
+ $fiber_suspend = function ($elements) {
+ \Fiber::suspend();
+ return $elements;
+ };
+ $build = [
+ 'foo' => [
+ '#markup' => 'foo',
+ '#pre_render' => [$fiber_suspend],
+ ],
+ ];
+ $markup = $this->renderer->renderInIsolation($build);
+ $elements['#markup'] = $markup;
+ return $elements;
+ };
+ $build = [
+ 'foo' => [
+ '#pre_render' => [$fiber_suspend_pre_render],
+ ],
+ ];
+ return $this->renderer->render($build);
+ };
+
+ // Build an array of two fibers that executes the code defined above. This
+ // ensures that Fiber::suspend() is called from within two
+ // ::renderInIsolation() calls without either having been completed.
+ $fibers = [];
+ foreach ([0, 1] as $key) {
+ $fibers[] = new \Fiber(static fn () => $fiber_callback());
+ }
+ while ($fibers) {
+ foreach ($fibers as $key => $fiber) {
+ if ($fiber->isTerminated()) {
+ unset($fibers[$key]);
+ continue;
+ }
+ if ($fiber->isSuspended()) {
+ $fiber->resume();
+ }
+ else {
+ $fiber->start();
+ }
+ }
+ }
+ return $fiber->getReturn();
+ });
+ $this->assertEquals(Markup::create('foo'), $return);
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php b/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
deleted file mode 100644
index 6ab42a6c41a4..000000000000
--- a/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Tests\Core\Template;
-
-use Twig\Source;
-use Twig\Template;
-
-/**
- * A stub of the Twig Template class for testing.
- */
-class StubTwigTemplate extends Template {
-
- /**
- * {@inheritdoc}
- */
- public function getTemplateName(): string {
- return '';
- }
-
- /**
- * {@inheritdoc}
- */
- public function getDebugInfo(): array {
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getSourceContext(): Source {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDisplay(array $context, array $blocks = []): iterable {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
-}
diff --git a/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
index 43ff47406ecc..d1a043446f8a 100644
--- a/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
@@ -25,7 +25,7 @@ class BrowserTestBaseTest extends UnitTestCase {
->method('getDriver')
->willReturn($driver);
- $btb = $this->getMockBuilder(BrowserTestBaseMockableClass::class)
+ $btb = $this->getMockBuilder(BrowserTestBaseMockableClassTest::class)
->disableOriginalConstructor()
->onlyMethods(['getSession'])
->getMock();
@@ -82,7 +82,7 @@ class BrowserTestBaseTest extends UnitTestCase {
public function testTearDownWithoutSetUp(): void {
$method = 'cleanupEnvironment';
$this->assertTrue(method_exists(BrowserTestBase::class, $method));
- $btb = $this->getMockBuilder(BrowserTestBaseMockableClass::class)
+ $btb = $this->getMockBuilder(BrowserTestBaseMockableClassTest::class)
->disableOriginalConstructor()
->onlyMethods([$method])
->getMock();
@@ -96,6 +96,6 @@ class BrowserTestBaseTest extends UnitTestCase {
/**
* A class extending BrowserTestBase for testing purposes.
*/
-class BrowserTestBaseMockableClass extends BrowserTestBase {
+class BrowserTestBaseMockableClassTest extends BrowserTestBase {
}
diff --git a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
index 957b2f61f97b..9138d54523d6 100644
--- a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
@@ -6,12 +6,18 @@ namespace Drupal\Tests\Core\Test\RunTests;
use Drupal\Core\Test\RunTests\TestFileParser;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
- * @coversDefaultClass \Drupal\Core\Test\RunTests\TestFileParser
- * @group Test
- * @group RunTests
+ * Tests for the deprecated TestFileParser class.
*/
+#[CoversClass(TestFileParser::class)]
+#[Group('Test')]
+#[Group('RunTest')]
+#[IgnoreDeprecations]
class TestFileParserTest extends UnitTestCase {
public static function provideTestFileContents() {
@@ -66,9 +72,9 @@ COMPOUND
}
/**
- * @covers ::parseContents
- * @dataProvider provideTestFileContents
+ * @legacy-covers ::parseContents
*/
+ #[DataProvider('provideTestFileContents')]
public function testParseContents($expected, $contents): void {
$parser = new TestFileParser();
@@ -78,7 +84,7 @@ COMPOUND
}
/**
- * @covers ::getTestListFromFile
+ * @legacy-covers ::getTestListFromFile
*/
public function testGetTestListFromFile(): void {
$parser = new TestFileParser();
diff --git a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
index bfbb4ca2e40d..0fb55e6c7f8a 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
@@ -13,17 +13,23 @@ use Drupal\Core\Test\Exception\MissingGroupException;
use Drupal\Core\Test\TestDiscovery;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
- * @coversDefaultClass \Drupal\Core\Test\TestDiscovery
- * @group Test
+ * Unit tests for TestDiscovery.
*/
+#[CoversClass(TestDiscovery::class)]
+#[Group('Test')]
+#[IgnoreDeprecations]
class TestDiscoveryTest extends UnitTestCase {
/**
- * @covers ::getTestInfo
- * @dataProvider infoParserProvider
+ * @legacy-covers ::getTestInfo
*/
+ #[DataProvider('infoParserProvider')]
public function testTestInfoParser($expected, $classname, $doc_comment = NULL): void {
$info = TestDiscovery::getTestInfo($classname, $doc_comment);
$this->assertEquals($expected, $info);
@@ -34,14 +40,14 @@ class TestDiscoveryTest extends UnitTestCase {
$tests[] = [
// Expected result.
[
- 'name' => static::class,
+ 'name' => TestDatabaseTest::class,
'group' => 'Test',
- 'groups' => ['Test'],
- 'description' => 'Tests \Drupal\Core\Test\TestDiscovery.',
+ 'groups' => ['Test', 'simpletest', 'Template'],
+ 'description' => 'Tests \Drupal\Core\Test\TestDatabase.',
'type' => 'PHPUnit-Unit',
],
// Classname.
- static::class,
+ TestDatabaseTest::class,
];
// A core unit test.
@@ -217,7 +223,7 @@ class TestDiscoveryTest extends UnitTestCase {
}
/**
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testTestInfoParserMissingGroup(): void {
$classname = 'Drupal\KernelTests\field\BulkDeleteTest';
@@ -232,7 +238,7 @@ EOT;
}
/**
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testTestInfoParserMissingSummary(): void {
$classname = 'Drupal\KernelTests\field\BulkDeleteTest';
@@ -311,7 +317,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestClasses(): void {
$this->setupVfsWithTestClasses();
@@ -380,7 +386,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestClassesWithSelectedTypes(): void {
$this->setupVfsWithTestClasses();
@@ -425,7 +431,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestsInProfiles(): void {
$this->setupVfsWithTestClasses();
@@ -454,9 +460,9 @@ EOF;
}
/**
- * @covers ::getPhpunitTestSuite
- * @dataProvider providerTestGetPhpunitTestSuite
+ * @legacy-covers ::getPhpunitTestSuite
*/
+ #[DataProvider('providerTestGetPhpunitTestSuite')]
public function testGetPhpunitTestSuite($classname, $expected): void {
$this->assertEquals($expected, TestDiscovery::getPhpunitTestSuite($classname));
}
@@ -482,7 +488,7 @@ EOF;
/**
* Ensure that classes are not reflected when the docblock is empty.
*
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testGetTestInfoEmptyDocblock(): void {
// If getTestInfo() performed reflection, it won't be able to find the
@@ -497,7 +503,7 @@ EOF;
/**
* Ensure TestDiscovery::scanDirectory() ignores certain abstract file types.
*
- * @covers ::scanDirectory
+ * @legacy-covers ::scanDirectory
*/
public function testScanDirectoryNoAbstract(): void {
$this->setupVfsWithTestClasses();
diff --git a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
index ba1d28a50c6d..5627d068aa77 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
@@ -29,7 +29,7 @@ class TestSetupTraitTest extends UnitTestCase {
public function testChangeDatabasePrefix(): void {
$root = dirname(__FILE__, 7);
putenv('SIMPLETEST_DB=pgsql://user:pass@127.0.0.1/db');
- $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', $root);
+ $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db');
Database::addConnectionInfo('default', 'default', $connection_info);
$this->assertEquals('mysql', Database::getConnectionInfo()['default']['driver']);
$this->assertEquals('localhost', Database::getConnectionInfo()['default']['host']);
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php
new file mode 100644
index 000000000000..c39827ca7098
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Theme\Component;
+
+use Drupal\Core\Plugin\Component;
+use Drupal\Core\Template\Loader\ComponentLoader;
+use Drupal\Core\Theme\ComponentPluginManager;
+use Drupal\Tests\UnitTestCaseTest;
+use org\bovigo\vfs\vfsStream;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Unit tests for the component loader class.
+ *
+ * @coversDefaultClass \Drupal\Core\Template\Loader\ComponentLoader
+ * @group sdc
+ */
+class ComponentLoaderTest extends UnitTestCaseTest {
+
+ /**
+ * Tests the is fresh function for component loader.
+ */
+ public function testIsFresh(): void {
+ $vfs_root = vfsStream::setup();
+ $component_directory = vfsStream::newDirectory('loader-test')->at($vfs_root);
+ $current_time = time();
+ $component_twig_file = vfsStream::newFile('loader-test.twig')
+ ->at($component_directory)
+ ->setContent('twig')
+ // Mark files as changed before the current time.
+ ->lastModified($current_time - 1000);
+ $component_yml_file = vfsStream::newFile('loader-test.component.yml')
+ ->at($component_directory)
+ ->setContent('')
+ // Mark file as changed before the current time.
+ ->lastModified($current_time - 1000);
+
+ $component = new Component(
+ ['app_root' => '/fake/root'],
+ 'sdc_test:loader-test',
+ [
+ 'machineName' => 'loader-test',
+ 'extension_type' => 'module',
+ 'id' => 'sdc_test:loader-test',
+ 'path' => 'vfs://' . $component_directory->path(),
+ 'provider' => 'sdc_test',
+ 'template' => 'loader-test.twig',
+ 'group' => 'my-group',
+ 'description' => 'My description',
+ '_discovered_file_path' => 'vfs://' . $component_yml_file->path(),
+ ]
+ );
+
+ $component_manager = $this->prophesize(ComponentPluginManager::class);
+ $component_manager->find('sdc_test:loader-test')->willReturn($component);
+ $component_loader = new ComponentLoader(
+ $component_manager->reveal(),
+ $this->createMock(LoggerInterface::class),
+ );
+
+ // Assert the component is fresh, as it changed before the current time.
+ $this->assertTrue($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig and YAML files were supposed to be fresh');
+ // Pretend that we changed the twig file.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig file was supposed to be outdated');
+
+ // Pretend that we changed the YAML file.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time);
+ $component_yml_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'YAML file was supposed to be outdated');
+
+ // Pretend that we changed both files.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time + 1000);
+ $component_yml_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig and YAML files were supposed to be outdated');
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
index 4e0555eb2ae8..71f0098a902d 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Theme\Component\ComponentMetadata;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Tests\UnitTestCaseTest;
+use PHPUnit\Framework\Attributes\DataProvider;
/**
* Unit tests for the component metadata class.
@@ -18,9 +20,8 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests that the correct data is returned for each property.
- *
- * @dataProvider dataProviderMetadata
*/
+ #[DataProvider('dataProviderMetadata')]
public function testMetadata(array $metadata_info, array $expectations): void {
$metadata = new ComponentMetadata($metadata_info, 'foo/', FALSE);
$this->assertSame($expectations['path'], $metadata->path);
@@ -31,9 +32,8 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests the correct checks when enforcing schemas or not.
- *
- * @dataProvider dataProviderMetadata
*/
+ #[DataProvider('dataProviderMetadata')]
public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema): void {
if ($missing_schema) {
$this->expectException(InvalidComponentException::class);
@@ -71,7 +71,283 @@ class ComponentMetadataTest extends UnitTestCaseTest {
],
TRUE,
],
- 'complete example with schema' => [
+ 'complete example with schema, but no meta:enum' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('external', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema, but no matching meta:enum' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'fav' => 'Favorite',
+ 'external' => 'External',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema, but no meta:enum, prop value not as string' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'col' => [
+ 'type' => 'string',
+ 'title' => 'Column',
+ 'enum' => [
+ 1,
+ 2,
+ 3,
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'col' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Column',
+ 'enum' => [
+ 1,
+ 2,
+ 3,
+ ],
+ 'meta:enum' => [
+ 1 => new TranslatableMarkup('1', [], ['context' => '']),
+ 2 => new TranslatableMarkup('2', [], ['context' => '']),
+ 3 => new TranslatableMarkup('3', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum)' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'like' => 'Like',
+ 'external' => 'External',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('Like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum and x-translation-context)' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
@@ -100,6 +376,12 @@ class ComponentMetadataTest extends UnitTestCaseTest {
'like',
'external',
],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'like' => 'Like',
+ 'external' => 'External',
+ ],
+ 'x-translation-context' => 'Icon Type',
],
],
],
@@ -130,6 +412,85 @@ class ComponentMetadataTest extends UnitTestCaseTest {
'like',
'external',
],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => 'Icon Type']),
+ 'like' => new TranslatableMarkup('Like', [], ['context' => 'Icon Type']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => 'Icon Type']),
+ ],
+ 'x-translation-context' => 'Icon Type',
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum and x-translation-context and an empty value)' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'target' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ '',
+ '_blank',
+ ],
+ 'meta:enum' => [
+ '' => 'Opens in same window',
+ '_blank' => 'Opens in new window',
+ ],
+ 'x-translation-context' => 'Link target',
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'target' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ '',
+ '_blank',
+ ],
+ 'meta:enum' => [
+ '' => new TranslatableMarkup('Opens in same window', [], ['context' => 'Link target']),
+ '_blank' => new TranslatableMarkup('Opens in new window', [], ['context' => 'Link target']),
+ ],
+ 'x-translation-context' => 'Link target',
],
],
],
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
index 7e706edcfe0e..a126e4a81d96 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
@@ -5,15 +5,17 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component;
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Component\ComponentValidator;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Core\Plugin\Component;
+use Drupal\Tests\UnitTestCaseTest;
+use JsonSchema\ConstraintError;
use JsonSchema\Constraints\Factory;
use JsonSchema\Constraints\FormatConstraint;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Validator;
-use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Yaml;
/**
@@ -22,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
* @coversDefaultClass \Drupal\Core\Theme\Component\ComponentValidator
* @group sdc
*/
-class ComponentValidatorTest extends TestCase {
+class ComponentValidatorTest extends UnitTestCaseTest {
/**
* Tests that valid component definitions don't cause errors.
@@ -190,6 +192,11 @@ class ComponentValidatorTest extends TestCase {
* @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException
*/
public function testValidatePropsValid(array $context, string $component_id, array $definition): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:' . $component_id,
@@ -234,6 +241,11 @@ class ComponentValidatorTest extends TestCase {
* Tests we can use a custom validator to validate props.
*/
public function testCustomValidator(): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:my-cta',
@@ -265,6 +277,11 @@ class ComponentValidatorTest extends TestCase {
* @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException
*/
public function testValidatePropsInvalid(array $context, string $component_id, array $definition, string $expected_exception_message): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:' . $component_id,
@@ -359,7 +376,7 @@ class UrlHelperFormatConstraint extends FormatConstraint {
}
if ($schema->format === 'uri') {
if (\is_string($element) && !UrlHelper::isValid($element)) {
- $this->addError($path, 'Invalid URL format', 'format', ['format' => $schema->format]);
+ $this->addError(ConstraintError::FORMAT_URL, $path, ['format' => $schema->format]);
}
return;
}
diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
index 2c67cf9193fb..d1a6e6433407 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Icon;
use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\Element\Icon;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Icon\IconDefinition;
@@ -41,7 +42,7 @@ class IconTest extends UnitTestCase {
* Test the Icon::getInfo method.
*/
public function testGetInfo(): void {
- $icon = new Icon([], 'test', 'test');
+ $icon = new Icon([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $icon->getInfo();
$this->assertArrayHasKey('#pre_render', $info);
diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
index 0f1146f84d89..59bce7791113 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Icon\Plugin;
// cspell:ignore corge
-use Drupal\Component\Render\FormattableMarkup;
+
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Icon\IconDefinition;
use Drupal\Core\Theme\Icon\IconDefinitionInterface;
@@ -288,7 +288,6 @@ class SvgExtractorTest extends UnitTestCase {
$this->assertInstanceOf(IconDefinitionInterface::class, $icon_loaded);
$data_loaded = $icon_loaded->getAllData();
- $expected_content[$index] = new FormattableMarkup($expected_content[$index], []);
$this->assertEquals($expected_content[$index], $data_loaded['content']);
$expected_attributes[$index] = new Attribute($expected_attributes[$index] ?? []);
diff --git a/core/tests/Drupal/Tests/RequirementsPageTrait.php b/core/tests/Drupal/Tests/RequirementsPageTrait.php
index c10e5676079d..25f39e49ed80 100644
--- a/core/tests/Drupal/Tests/RequirementsPageTrait.php
+++ b/core/tests/Drupal/Tests/RequirementsPageTrait.php
@@ -14,7 +14,7 @@ trait RequirementsPageTrait {
/**
* Handles the update requirements page.
*/
- protected function updateRequirementsProblem() {
+ protected function updateRequirementsProblem(): void {
// Assert a warning is shown on older test environments.
$links = $this->getSession()->getPage()->findAll('named', ['link', 'try again']);
@@ -37,7 +37,7 @@ trait RequirementsPageTrait {
* next screen of the installer. If an expected warning is not found, or if
* a warning not in the list is present, a fail is raised.
*/
- protected function continueOnExpectedWarnings($expected_warnings = []) {
+ protected function continueOnExpectedWarnings($expected_warnings = []): void {
$this->assertSession()->pageTextNotContains('Errors found');
$this->assertWarningSummaries($expected_warnings);
$this->clickLink('continue anyway');
@@ -54,7 +54,7 @@ trait RequirementsPageTrait {
* A list of warning summaries to expect on the requirements screen (e.g.
* 'PHP', 'PHP OPcode caching', etc.).
*/
- protected function assertWarningSummaries(array $summaries) {
+ protected function assertWarningSummaries(array $summaries): void {
$this->assertRequirementSummaries($summaries, 'warning');
}
@@ -68,7 +68,7 @@ trait RequirementsPageTrait {
* A list of error summaries to expect on the requirements screen (e.g.
* 'PHP', 'PHP OPcode caching', etc.).
*/
- protected function assertErrorSummaries(array $summaries) {
+ protected function assertErrorSummaries(array $summaries): void {
$this->assertRequirementSummaries($summaries, 'error');
}
@@ -84,7 +84,7 @@ trait RequirementsPageTrait {
* @param string $type
* The type of requirement, either 'warning' or 'error'.
*/
- protected function assertRequirementSummaries(array $summaries, string $type) {
+ protected function assertRequirementSummaries(array $summaries, string $type): void {
// The selectors are different for Claro.
$is_claro = stripos($this->getSession()->getPage()->getContent(), 'claro/css/theme/maintenance-page.css') !== FALSE;
diff --git a/core/tests/Drupal/Tests/SchemaCheckTestTrait.php b/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
index 26f97c482b4c..177744e9a2de 100644
--- a/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
+++ b/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
@@ -24,7 +24,7 @@ trait SchemaCheckTestTrait {
* @param array $config_data
* The configuration data.
*/
- public function assertConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data) {
+ public function assertConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data): void {
$check = $this->checkConfigSchema($typed_config, $config_name, $config_data);
$message = '';
if ($check === FALSE) {
@@ -46,7 +46,7 @@ trait SchemaCheckTestTrait {
* @param string $config_name
* The configuration name.
*/
- public function assertConfigSchemaByName($config_name) {
+ public function assertConfigSchemaByName($config_name): void {
$config = $this->config($config_name);
$this->assertConfigSchema(\Drupal::service('config.typed'), $config->getName(), $config->get());
}
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 2257148d0698..b279bd1ed250 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -13,6 +13,7 @@ use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
use Drupal\TestTools\Extension\Dump\DebugDump;
use PHPUnit\Framework\Attributes\BeforeClass;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\VarDumper\VarDumper;
@@ -210,4 +211,32 @@ abstract class UnitTestCase extends TestCase {
return $class_resolver;
}
+ /**
+ * Set up a traversable class mock to return specific items when iterated.
+ *
+ * Test doubles for types extending \Traversable are required to implement
+ * \Iterator which requires setting up five methods. Instead, this helper
+ * can be used.
+ *
+ * @param \PHPUnit\Framework\MockObject\MockObject&\Iterator $mock
+ * A mock object mocking a traversable class.
+ * @param array $items
+ * The items to return when this mock is iterated.
+ *
+ * @return \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * The same mock object ready to be iterated.
+ *
+ * @template T of \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * @phpstan-param T $mock
+ * @phpstan-return T
+ * @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
+ */
+ protected function setupMockIterator(MockObject&\Iterator $mock, array $items): MockObject&\Iterator {
+ $iterator = new \ArrayIterator($items);
+ foreach (get_class_methods(\Iterator::class) as $method) {
+ $mock->method($method)->willReturnCallback([$iterator, $method]);
+ }
+ return $mock;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index c79916fca289..deaa20997fb8 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -36,7 +36,7 @@ trait UpdatePathTestTrait {
* @param string|null $update_url
* The update URL.
*/
- protected function runUpdates($update_url = NULL) {
+ protected function runUpdates($update_url = NULL): void {
if (!$update_url) {
$update_url = Url::fromRoute('system.db_update');
}
@@ -167,7 +167,7 @@ trait UpdatePathTestTrait {
/**
* Tests the selection page.
*/
- protected function doSelectionTest() {
+ protected function doSelectionTest(): void {
// No-op. Tests wishing to do test the selection page or the general
// update.php environment before running update.php can override this method
// and implement their required tests.
@@ -176,7 +176,7 @@ trait UpdatePathTestTrait {
/**
* Installs the update_script_test module and makes an update available.
*/
- protected function ensureUpdatesToRun() {
+ protected function ensureUpdatesToRun(): void {
\Drupal::service('module_installer')->install(['update_script_test']);
// Reset the schema so there is an update to run.
\Drupal::service('update.update_hook_registry')->setInstalledVersion('update_script_test', 8000);