Repository: symfony/ux
Pull request #1503 (2024-02-15 18:13:06)

[Twig][Live] Migrate PHPUnit configuration
```
Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!
```

(will batch the other ones if there are no bad surprises in the CI with those ones)





Patch:
diff --git a/UPGRADE.md b/UPGRADE.md
deleted file mode 100644
index 263b4102..00000000
--- a/UPGRADE.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# UPGRADE
-
-## FROM 2.14 to 2.15
-
--   **Live Components**: Change `data-live-id` attributes to `id` #1484.
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index 5b78d7ec..39032dcc 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## Unreleased
-
--   Add doctrine/orm 3 support.
-
 ## 2.14.0
 
 -   Fixed behavior of Autocomplete when the underlying `select` or `option`
diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js
index 5659d9bc..49587145 100644
--- a/src/Autocomplete/assets/dist/controller.js
+++ b/src/Autocomplete/assets/dist/controller.js
@@ -116,7 +116,6 @@ class default_1 extends Controller {
     }
     resetTomSelect() {
         if (this.tomSelect) {
-            this.dispatchEvent('before-reset', { tomSelect: this.tomSelect });
             this.stopMutationObserver();
             const currentHtml = this.element.innerHTML;
             const currentValue = this.tomSelect.getValue();
@@ -144,7 +143,6 @@ class default_1 extends Controller {
                 subtree: true,
                 attributes: true,
                 characterData: true,
-                attributeOldValue: true,
             });
             this.isObserving = true;
         }
@@ -166,11 +164,7 @@ class default_1 extends Controller {
                         break;
                     }
                     if (mutation.target === this.element && mutation.attributeName === 'multiple') {
-                        const isNowMultiple = this.element.hasAttribute('multiple');
-                        const wasMultiple = mutation.oldValue === 'multiple';
-                        if (isNowMultiple !== wasMultiple) {
-                            requireReset = true;
-                        }
+                        requireReset = true;
                         break;
                     }
                     break;
@@ -197,14 +191,12 @@ class default_1 extends Controller {
         });
     }
     areOptionsEquivalent(newOptions) {
-        const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== '');
-        const filteredNewOptions = newOptions.filter((option) => option.value !== '');
-        if (filteredOriginalOptions.length !== filteredNewOptions.length) {
+        if (this.originalOptions.length !== newOptions.length) {
             return false;
         }
         const normalizeOption = (option) => `${option.value}-${option.text}-${option.group}`;
-        const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
-        const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
+        const originalOptionsSet = new Set(this.originalOptions.map(normalizeOption));
+        const newOptionsSet = new Set(newOptions.map(normalizeOption));
         return (originalOptionsSet.size === newOptionsSet.size &&
             [...originalOptionsSet].every((option) => newOptionsSet.has(option)));
     }
diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts
index bc3812b7..580552a2 100644
--- a/src/Autocomplete/assets/src/controller.ts
+++ b/src/Autocomplete/assets/src/controller.ts
@@ -325,7 +325,6 @@ export default class extends Controller {
 
     private resetTomSelect(): void {
         if (this.tomSelect) {
-            this.dispatchEvent('before-reset', { tomSelect: this.tomSelect });
             this.stopMutationObserver();
 
             // Grab the current HTML then restore it after destroying TomSelect
@@ -359,7 +358,6 @@ export default class extends Controller {
                 subtree: true,
                 attributes: true,
                 characterData: true,
-                attributeOldValue: true,
             });
             this.isObserving = true;
         }
@@ -386,11 +384,7 @@ export default class extends Controller {
                     }
 
                     if (mutation.target === this.element && mutation.attributeName === 'multiple') {
-                        const isNowMultiple = this.element.hasAttribute('multiple');
-                        const wasMultiple = mutation.oldValue === 'multiple';
-                        if (isNowMultiple !== wasMultiple) {
-                            requireReset = true;
-                        }
+                        requireReset = true;
 
                         break;
                     }
@@ -425,18 +419,14 @@ export default class extends Controller {
     }
 
     private areOptionsEquivalent(newOptions: Array<{ value: string; text: string; group: string | null }>): boolean {
-        // remove the empty option, which is added by TomSelect so may be missing from new options
-        const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== '');
-        const filteredNewOptions = newOptions.filter((option) => option.value !== '');
-
-        if (filteredOriginalOptions.length !== filteredNewOptions.length) {
+        if (this.originalOptions.length !== newOptions.length) {
             return false;
         }
 
         const normalizeOption = (option: { value: string; text: string; group: string | null }) =>
             `${option.value}-${option.text}-${option.group}`;
-        const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
-        const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
+        const originalOptionsSet = new Set(this.originalOptions.map(normalizeOption));
+        const newOptionsSet = new Set(newOptions.map(normalizeOption));
 
         return (
             originalOptionsSet.size === newOptionsSet.size &&
diff --git a/src/Autocomplete/composer.json b/src/Autocomplete/composer.json
index 95aee140..bedb41fb 100644
--- a/src/Autocomplete/composer.json
+++ b/src/Autocomplete/composer.json
@@ -26,7 +26,6 @@
     "require": {
         "php": ">=8.1",
         "symfony/dependency-injection": "^6.3|^7.0",
-        "symfony/deprecation-contracts": "^2.5|^3",
         "symfony/http-foundation": "^6.3|^7.0",
         "symfony/http-kernel": "^6.3|^7.0",
         "symfony/property-access": "^6.3|^7.0",
@@ -35,7 +34,7 @@
     "require-dev": {
         "doctrine/collections": "^1.6.8|^2.0",
         "doctrine/doctrine-bundle": "^2.4.3",
-        "doctrine/orm": "^2.9.4|^3.0",
+        "doctrine/orm": "^2.9.4",
         "fakerphp/faker": "^1.22",
         "mtdowling/jmespath.php": "^2.6",
         "symfony/form": "^6.3|^7.0",
diff --git a/src/Autocomplete/src/Doctrine/EntityMetadata.php b/src/Autocomplete/src/Doctrine/EntityMetadata.php
index 7a01d0d8..6a5807fb 100644
--- a/src/Autocomplete/src/Doctrine/EntityMetadata.php
+++ b/src/Autocomplete/src/Doctrine/EntityMetadata.php
@@ -42,41 +42,13 @@ class EntityMetadata
     }
 
     public function getPropertyMetadata(string $propertyName): array
-    {
-        trigger_deprecation('symfony/ux-autocomplete', '2.15.0', 'Calling EntityMetadata::getPropertyMetadata() is deprecated. You should stop using it, as it will be removed in the future.');
-
-        try {
-            return $this->getFieldMetadata($propertyName);
-        } catch (\InvalidArgumentException $e) {
-            return $this->getAssociationMetadata($propertyName);
-        }
-    }
-
-    /**
-     * @internal
-     *
-     * @return array<string, mixed>
-     */
-    public function getFieldMetadata(string $propertyName): array
     {
         if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) {
-            // Cast to array, because in doctrine/orm:^3.0; $metadata will be a FieldMapping object
-            return (array) $this->metadata->fieldMappings[$propertyName];
+            return $this->metadata->fieldMappings[$propertyName];
         }
 
-        throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->metadata->getName()));
-    }
-
-    /**
-     * @internal
-     *
-     * @return array<string, mixed>
-     */
-    public function getAssociationMetadata(string $propertyName): array
-    {
         if (\array_key_exists($propertyName, $this->metadata->associationMappings)) {
-            // Cast to array, because in doctrine/orm:^3.0; $metadata will be an AssociationMapping object
-            return (array) $this->metadata->associationMappings[$propertyName];
+            return $this->metadata->associationMappings[$propertyName];
         }
 
         throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->metadata->getName()));
@@ -84,11 +56,7 @@ class EntityMetadata
 
     public function getPropertyDataType(string $propertyName): string
     {
-        if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) {
-            return $this->getFieldMetadata($propertyName)['type'];
-        }
-
-        return $this->getAssociationMetadata($propertyName)['type'];
+        return $this->getPropertyMetadata($propertyName)['type'];
     }
 
     public function getIdValue(object $entity): string
diff --git a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
index 7cdf1057..44986dad 100644
--- a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
+++ b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
@@ -60,7 +60,7 @@ class EntitySearchUtil
                 }
 
                 $originalPropertyName = $associatedProperties[0];
-                $originalPropertyMetadata = $entityMetadata->getAssociationMetadata($originalPropertyName);
+                $originalPropertyMetadata = $entityMetadata->getPropertyMetadata($originalPropertyName);
                 $associatedEntityDto = $this->metadataFactory->create($originalPropertyMetadata['targetEntity']);
 
                 for ($i = 0; $i < $numAssociatedProperties - 1; ++$i) {
@@ -75,8 +75,9 @@ class EntitySearchUtil
                     }
 
                     if ($i < $numAssociatedProperties - 2) {
-                        $propertyMetadata = $associatedEntityDto->getAssociationMetadata($associatedPropertyName);
-                        $associatedEntityDto = $this->metadataFactory->create($propertyMetadata['targetEntity']);
+                        $propertyMetadata = $associatedEntityDto->getPropertyMetadata($associatedPropertyName);
+                        $targetEntity = $propertyMetadata['targetEntity'];
+                        $associatedEntityDto = $this->metadataFactory->create($targetEntity);
                     }
                 }
 
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 7e5e4b78..2968fd82 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -617,7 +617,7 @@ var Idiomorph = (function () {
          * @returns {boolean}
          */
         function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
-            return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
+            return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
         }
 
         /**
@@ -1378,7 +1378,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
         syncAttributes(newElement, oldElement);
     });
     Idiomorph.morph(rootFromElement, rootToElement, {
-        ignoreActiveValue: true,
         callbacks: {
             beforeNodeMorphed: (fromEl, toEl) => {
                 if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {
diff --git a/src/LiveComponent/assets/package.json b/src/LiveComponent/assets/package.json
index c93a7868..a0d5dba4 100644
--- a/src/LiveComponent/assets/package.json
+++ b/src/LiveComponent/assets/package.json
@@ -27,7 +27,7 @@
         }
     },
     "dependencies": {
-        "idiomorph": "https://github.com/bigskysoftware/idiomorph.git"
+        "idiomorph": "^0.3.0"
     },
     "peerDependencies": {
         "@hotwired/stimulus": "^3.0.0"
diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts
index 92a31080..7dba0668 100644
--- a/src/LiveComponent/assets/src/morphdom.ts
+++ b/src/LiveComponent/assets/src/morphdom.ts
@@ -53,12 +53,6 @@ export function executeMorphdom(
     });
 
     Idiomorph.morph(rootFromElement, rootToElement, {
-        // We handle updating the value of fields that have been changed
-        // since the HTML was requested. However, the active element is
-        // a special case: replacing the value isn't enough. We need to
-        // prevent the value from being changed in the first place so the
-        // user's cursor position is maintained.
-        ignoreActiveValue: true,
         callbacks: {
             beforeNodeMorphed: (fromEl: Element, toEl: Element) => {
                 // Idiomorph loop also over Text node
diff --git a/src/LiveComponent/phpunit.xml.dist b/src/LiveComponent/phpunit.xml.dist
index 989e6115..8798e3cc 100644
--- a/src/LiveComponent/phpunit.xml.dist
+++ b/src/LiveComponent/phpunit.xml.dist
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
@@ -8,10 +7,16 @@
          failOnRisky="true"
          failOnWarning="true"
 >
+    <coverage processUncoveredFiles="true">
+        <include>
+            <directory suffix=".php">./src</directory>
+        </include>
+    </coverage>
+
     <php>
-        <ini name="error_reporting" value="-1" />
-        <server name="KERNEL_CLASS" value="Symfony\UX\LiveComponent\Tests\Fixtures\Kernel" />
-        <server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db" />
+        <ini name="error_reporting" value="-1"/>
+        <server name="KERNEL_CLASS" value="Symfony\UX\LiveComponent\Tests\Fixtures\Kernel"/>
+        <server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db"/>
         <env name="SHELL_VERBOSITY" value="-1"/>
         <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
     </php>
@@ -22,12 +27,6 @@
         </testsuite>
     </testsuites>
 
-    <filter>
-        <whitelist processUncoveredFilesFromWhitelist="true">
-            <directory suffix=".php">./src</directory>
-        </whitelist>
-    </filter>
-
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
     </listeners>
diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
index 8683214a..97c48d75 100644
--- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
+++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php
@@ -256,11 +256,7 @@ final class LiveComponentExtension extends Extension implements PrependExtension
             ->setArguments(['%kernel.cache_dir%/'.self::TEMPLATES_MAP_FILENAME]);
 
         $container->register('ux.live_component.twig.cache_warmer', TemplateCacheWarmer::class)
-            ->setArguments([
-                new Reference('twig.template_iterator'),
-                self::TEMPLATES_MAP_FILENAME,
-                '%kernel.secret%',
-            ])
+            ->setArguments([new Reference('twig.template_iterator'), self::TEMPLATES_MAP_FILENAME])
             ->addTag('kernel.cache_warmer');
     }
 
diff --git a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
index 3c9d18ac..18b33be2 100644
--- a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
+++ b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
@@ -22,18 +22,15 @@ use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
  */
 final class TemplateCacheWarmer implements CacheWarmerInterface
 {
-    public function __construct(
-        private readonly \IteratorAggregate $templateIterator,
-        private readonly string $cacheFilename,
-        private readonly string $secret,
-    ) {
+    public function __construct(private \IteratorAggregate $templateIterator, private readonly string $cacheFilename)
+    {
     }
 
     public function warmUp(string $cacheDir, ?string $buildDir = null): array
     {
         $map = [];
         foreach ($this->templateIterator as $item) {
-            $map[hash('xxh128', $item.$this->secret)] = $item;
+            $map[bin2hex(random_bytes(16))] = $item;
         }
 
         (new PhpArrayAdapter($cacheDir.'/'.$this->cacheFilename, new NullAdapter()))->warmUp(['map' => $map]);
diff --git a/src/LiveComponent/src/Twig/TemplateMap.php b/src/LiveComponent/src/Twig/TemplateMap.php
index 03dd2ffd..75b8f289 100644
--- a/src/LiveComponent/src/Twig/TemplateMap.php
+++ b/src/LiveComponent/src/Twig/TemplateMap.php
@@ -28,7 +28,7 @@ final class TemplateMap
         $this->map = (new PhpArrayAdapter($cacheFile, new NullAdapter()))->getItem('map')->get();
     }
 
-    public function resolve(string $obscuredName): string
+    public function resolve(string $obscuredName)
     {
         return $this->map[$obscuredName] ?? throw new \RuntimeException(sprintf('Cannot find a template matching "%s". Cache may be corrupt.', $obscuredName));
     }
diff --git a/src/TwigComponent/phpunit.xml.dist b/src/TwigComponent/phpunit.xml.dist
index 59ac0154..cd1dda80 100644
--- a/src/TwigComponent/phpunit.xml.dist
+++ b/src/TwigComponent/phpunit.xml.dist
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
 <!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
@@ -8,9 +7,15 @@
          failOnRisky="true"
          failOnWarning="true"
 >
+    <coverage processUncoveredFiles="true">
+        <include>
+            <directory suffix=".php">./src</directory>
+        </include>
+    </coverage>
+
     <php>
-        <ini name="error_reporting" value="-1" />
-        <server name="KERNEL_CLASS" value="Symfony\UX\TwigComponent\Tests\Fixtures\Kernel" />
+        <ini name="error_reporting" value="-1"/>
+        <server name="KERNEL_CLASS" value="Symfony\UX\TwigComponent\Tests\Fixtures\Kernel"/>
         <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
     </php>
 
@@ -20,12 +25,6 @@
         </testsuite>
     </testsuites>
 
-    <filter>
-        <whitelist processUncoveredFilesFromWhitelist="true">
-            <directory suffix=".php">./src</directory>
-        </whitelist>
-    </filter>
-
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
     </listeners>
diff --git a/ux.symfony.com/assets/controllers.json b/ux.symfony.com/assets/controllers.json
index 48507999..d37124f1 100644
--- a/ux.symfony.com/assets/controllers.json
+++ b/ux.symfony.com/assets/controllers.json
@@ -87,7 +87,7 @@
         "@symfony/ux-turbo": {
             "turbo-core": {
                 "enabled": true,
-                "fetch": "eager"
+                "fetch": "lazy"
             },
             "mercure-turbo-stream": {
                 "enabled": true,
diff --git a/ux.symfony.com/importmap.php b/ux.symfony.com/importmap.php
index b2ce3c77..6e58beaa 100644
--- a/ux.symfony.com/importmap.php
+++ b/ux.symfony.com/importmap.php
@@ -102,7 +102,7 @@ return [
         'version' => '2.0.2',
     ],
     '@hotwired/turbo' => [
-        'version' => '8.0.2',
+        'version' => '7.3.0',
     ],
     'typed.js' => [
         'version' => '2.0.16',
diff --git a/ux.symfony.com/templates/base.html.twig b/ux.symfony.com/templates/base.html.twig
index 9b3a562a..edeccc30 100644
--- a/ux.symfony.com/templates/base.html.twig
+++ b/ux.symfony.com/templates/base.html.twig
@@ -10,7 +10,6 @@
         {% if meta.canonical|default %}
             <link rel="canonical" href="{{ meta.canonical }}">
         {% endif %}
-        <meta name="view-transition" content="same-origin" />
         <link rel="icon" href="/favicon.ico" sizes="48x48">
         <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml">
         <link rel="apple-touch-icon" href="/apple-touch-icon.png">


Test patch:
diff --git a/src/Autocomplete/assets/test/controller.test.ts b/src/Autocomplete/assets/test/controller.test.ts
index f7c5060f..440e2004 100644
--- a/src/Autocomplete/assets/test/controller.test.ts
+++ b/src/Autocomplete/assets/test/controller.test.ts
@@ -815,46 +815,4 @@ describe('AutocompleteController', () => {
         });
         expect(getSelectedValues()).toEqual(['2', '3']);
     });
-
-    it('does not trigger a reset when the style of "multiple" attribute changes', async () => {
-        const { container } = await startAutocompleteTest(`
-            <select multiple data-testid='main-element' data-controller='autocomplete'>
-                <option value=''>Select dogs</option>
-                <option value='1'>dog1</option>
-                <option value='2'>dog2</option>
-                <option value='3'>dog3</option>
-            </select>
-        `);
-
-        let wasReset = false;
-        container.addEventListener('autocomplete:before-reset', () => {
-            wasReset = true;
-        });
-
-        const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement;
-        selectElement.setAttribute('multiple', 'multiple');
-        // wait for the mutation observe
-        await shortDelay(10);
-        expect(wasReset).toBe(false);
-    });
-
-    it('does not trigger a reset based on the extra, empty select', async () => {
-        const { container, tomSelect } = await startAutocompleteTest(`
-            <select data-testid='main-element' data-controller='autocomplete'>
-                <option value='1'>dog1</option>
-                <option value='2'>dog2</option>
-                <option value='3'>dog3</option>
-            </select>
-        `);
-
-        let wasReset = false;
-        container.addEventListener('autocomplete:before-reset', () => {
-            wasReset = true;
-        });
-
-        tomSelect.addItem('2');
-        // wait for the mutation observe
-        await shortDelay(10);
-        expect(wasReset).toBe(false);
-    });
 });
diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
index 0023abe7..1dd30667 100644
--- a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
+++ b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php
@@ -54,7 +54,7 @@ class EntityMetadataTest extends KernelTestCase
         $this->assertEquals(ClassMetadataInfo::MANY_TO_ONE, $metadata->getPropertyDataType('category'));
     }
 
-    public function testGetFieldMetadata(): void
+    public function testGetPropertyMetadata(): void
     {
         $metadata = $this->getMetadata();
         $this->assertSame([
@@ -66,48 +66,7 @@ class EntityMetadataTest extends KernelTestCase
             'nullable' => false,
             'precision' => null,
             'columnName' => 'name',
-        ], $metadata->getFieldMetadata('name'));
-    }
-
-    public function testGetAssociationMetadata(): void
-    {
-        $metadata = $this->getMetadata();
-        $this->assertSame([
-            'fieldName' => 'category',
-            'joinColumns' => [
-                [
-                    'name' => 'category_id',
-                    'unique' => false,
-                    'nullable' => false,
-                    'onDelete' => null,
-                    'columnDefinition' => null,
-                    'referencedColumnName' => 'id',
-                ],
-            ],
-            'cascade' => [],
-            'inversedBy' => 'products',
-            'targetEntity' => 'Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category',
-            'fetch' => 2,
-            'type' => 2,
-            'mappedBy' => null,
-            'isOwningSide' => true,
-            'sourceEntity' => 'Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product',
-            'isCascadeRemove' => false,
-            'isCascadePersist' => false,
-            'isCascadeRefresh' => false,
-            'isCascadeMerge' => false,
-            'isCascadeDetach' => false,
-            'sourceToTargetKeyColumns' => [
-                'category_id' => 'id',
-            ],
-            'joinColumnFieldNames' => [
-                'category_id' => 'category_id',
-            ],
-            'targetToSourceKeyColumns' => [
-                'id' => 'category_id',
-            ],
-            'orphanRemoval' => false,
-        ], $metadata->getAssociationMetadata('category'));
+        ], $metadata->getPropertyMetadata('name'));
     }
 
     public function testIsEmbeddedClassProperty(): void
diff --git a/src/LiveComponent/assets/test/controller/model.test.ts b/src/LiveComponent/assets/test/controller/model.test.ts
index 49b30365..81ef9189 100644
--- a/src/LiveComponent/assets/test/controller/model.test.ts
+++ b/src/LiveComponent/assets/test/controller/model.test.ts
@@ -810,9 +810,9 @@ describe('LiveController data-model Tests', () => {
             <div ${initComponent(data)}>
                 <form data-model>
                     <textarea name="comment" data-testid="comment">${data.comment}</textarea>
-                </form>
+               </form>
 
-                <input data-testid="other-input">
+               <button data-action="live#$render">Reload</button>
            </div>
        `);
 
@@ -823,18 +823,17 @@ describe('LiveController data-model Tests', () => {
             // delay slightly so we can type in the textarea
             .delayResponse(10);
 
-        const renderPromise = test.component.render();
+        getByText(test.element, 'Reload').click();
         // mimic changing the field, but without (yet) triggering the change event
         const commentField = getByTestId(test.element, 'comment');
-        const inputField = getByTestId(test.element, 'other-input');
+        if (!(commentField instanceof HTMLTextAreaElement)) {
+            throw new Error('wrong type');
+        }
         userEvent.type(commentField, ' ftw!');
-        // To make the test more robust, make the textarea NOT the active element.
-        // The active element's value is ignored during morphing. Here, we want
-        // to test that even if the morph system *does* want to update the value
-        // of the textarea, the value will be kept.
-        userEvent.type(inputField, 'making this element active');
 
-        await renderPromise;
+        // wait for loading start and end
+        await waitFor(() => expect(test.element).toHaveAttribute('busy'));
+        await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
 
         expect(commentField).toHaveValue('Live components ftw!');
 
@@ -846,7 +845,10 @@ describe('LiveController data-model Tests', () => {
                 data.comment = 'server changed comment';
             });
 
-        await test.component.render();
+        getByText(test.element, 'Reload').click();
+        // wait for loading start and end
+        await waitFor(() => expect(test.element).toHaveAttribute('busy'));
+        await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
 
         expect(commentField).toHaveValue('server changed comment');
     });
diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts
index 70493eb8..df096692 100644
--- a/src/LiveComponent/assets/test/controller/render.test.ts
+++ b/src/LiveComponent/assets/test/controller/render.test.ts
@@ -149,28 +149,6 @@ describe('LiveController rendering Tests', () => {
         expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts');
     });
 
-    it('conserves cursor position of active model element', async () => {
-        const test = await createTest({ name: '' }, (data) => `
-            <div ${initComponent(data)}>
-                <input data-model="name" class="anything">
-            </div>
-        `);
-
-        test.expectsAjaxCall()
-            .expectUpdatedData({ name: 'Hello' })
-
-        const input = test.queryByDataModel('name') as HTMLInputElement;
-        userEvent.type(input, 'Hello');
-        userEvent.keyboard('{ArrowLeft}{ArrowLeft}');
-
-        await test.component.render();
-
-        // the cursor position should be preserved
-        expect(input.selectionStart).toBe(3);
-        userEvent.type(input, '!');
-        expect(input.value).toBe('Hel!lo');
-    });
-
     it('does not render over elements with data-live-ignore', async () => {
         const test = await createTest({ firstName: 'Ryan' }, (data: any) => `
             <div ${initComponent(data)}>
diff --git a/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php b/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php
deleted file mode 100644
index 5e52311b..00000000
--- a/src/LiveComponent/tests/Unit/Twig/TemplateCacheWarmerTest.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\LiveComponent\Tests\Unit\Twig;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Cache\Adapter\NullAdapter;
-use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
-use Symfony\UX\LiveComponent\Twig\TemplateCacheWarmer;
-
-/**
- * @author Simon André <smn.andre@gmail.com>
- */
-final class TemplateCacheWarmerTest extends TestCase
-{
-    private string $cacheDir;
-    private string $cacheFile;
-    private TemplateCacheWarmer $templateCacheWarmer;
-
-    protected function setUp(): void
-    {
-        $this->cacheDir ??= sys_get_temp_dir();
-        $this->cacheFile ??= $this->cacheDir.'/cache_file';
-        if (file_exists($this->cacheFile)) {
-            unlink($this->cacheFile);
-        }
-        $this->templateCacheWarmer ??= new TemplateCacheWarmer(
-            new \ArrayObject(['template1', 'template2']),
-            'cache_file',
-            'secret'
-        );
-    }
-
-    public function testWarmUpCreatesCacheFile(): void
-    {
-        $this->assertFileDoesNotExist($this->cacheFile);
-
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-
-        $this->assertFileExists($this->cacheFile);
-    }
-
-    public function testWarmUpCreatesCorrectCacheContent(): void
-    {
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $item = $adapter->getItem('map');
-
-        $this->assertSame(
-            [
-                hash('xxh128', 'template1secret') => 'template1',
-                hash('xxh128', 'template2secret') => 'template2',
-            ],
-            $item->get()
-        );
-    }
-
-    public function testWarmUpCreatesReproductibleTemplateMap(): void
-    {
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $map1 = $adapter->getItem('map')->get();
-
-        $this->templateCacheWarmer->warmUp($this->cacheDir);
-        $adapter = new PhpArrayAdapter($this->cacheFile, new NullAdapter());
-        $map2 = $adapter->getItem('map')->get();
-
-        $this->assertSame($map1, $map2);
-    }
-}