Repository: symfony/ux (php)

Issue:
Issue with test submitForm with LiveCollectionType
Hi, 

I'm struggling to use the submitForm helper with a form containing a LiveCollectionType.

This is the setup.

EventCreationTyp is the following:
```php
class EventCreationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ... adding other fields
            ->add('participants', LiveCollectionType::class, [
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
                'entry_type' => EventParticipantType::class, // => only a FormType with 2 fields : name and email text field and both should be not null and not blank
            ])
        ;
    }
...
}
```

So in my test, I want to submit the following:
```php
  $component = $this->createLiveComponent(EventForm::class);
  
  $component->submitForm(['event_creation' => [
     ... other fields
      'participants' => [
          [
              'name' => "participant_1",
              'email' => "participant_1@email.com"
          ],
           [
              'email' => "participant_2@email.com",
              'name' => "participant_2"
          ],
      ]
  ]], 'save');
```

And my issue is that I get a form validation error for the `participants` saying that for the 1st item in the participants, the `name` should not be empty and for the second the `email` should be not null...

When I debug and look what is existing as `$formValues` at the save time in my LiveComponent I get the following:
```php
 [
 ... other fields
  "participants" => array:2 [
    0 => array:1 [
      "email" => "participant_1@email.com"
    ]
    1 => array:1 [
      "name" => "participant_2"
    ]
  ]
]
```

this debug is done at the start of the save function in my LiveComponent
```php
#[AsLiveComponent]
final class EventForm extends AbstractController
{
    use DefaultActionTrait;
    use ComponentWithFormTrait;
    use LiveCollectionTrait;

    #[LiveProp]
    public ?EventCreation $formData = null;

    /**
     * @return FormInterface<EventCreation>
     */
    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(EventCreationType::class, $this->formData ?? new EventCreation());
    }

    #[LiveAction]
    public function save(MessageBusInterface $bus): \Symfony\Component\HttpFoundation\RedirectResponse
    {
        dd($this->formValues);
        
        // Submit the form! If validation fails, an exception is thrown
        // and the component is automatically re-rendered with the errors
        $this->submitForm();

      ... treatment of the information
    }
}
```

in each of the Collection Item, only the last key=>value has been kept... I've tried to debug but can't see where these values are trimmed down. Am I doing something off?

Pull request description:
[LiveComponent] Add Documentation for testing with LiveCollectionType
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no 
| Issues        | Fix #2512
| License       | MIT

As mentioned in the #2512, I had trouble testing the submission of a form containing a LiveCollectionType field due to lack of proper guidance in the documentation. Hence this pull request to propose an addendum in the documentation linked to that capability


Patch:
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index 8720236c..1f8cea05 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -4,7 +4,6 @@
 
 -   Deprecate `ExtraLazyChoiceLoader` in favor of `Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader`
 -   Reset TomSelect when updating url attribute #1505
--   Add `getAttributes()` method to define additional attributes for autocomplete results #2541
 
 ## 2.22.0
 
diff --git a/src/Autocomplete/src/AutocompleteResultsExecutor.php b/src/Autocomplete/src/AutocompleteResultsExecutor.php
index 03d10e03..0061ca8a 100644
--- a/src/Autocomplete/src/AutocompleteResultsExecutor.php
+++ b/src/Autocomplete/src/AutocompleteResultsExecutor.php
@@ -73,7 +73,10 @@ final class AutocompleteResultsExecutor
 
         if (!method_exists($autocompleter, 'getGroupBy') || null === $groupBy = $autocompleter->getGroupBy()) {
             foreach ($paginator as $entity) {
-                $results[] = $this->formatResult($autocompleter, $entity);
+                $results[] = [
+                    'value' => $autocompleter->getValue($entity),
+                    'text' => $autocompleter->getLabel($entity),
+                ];
             }
 
             return new AutocompleteResults($results, $hasNextPage);
@@ -101,7 +104,10 @@ final class AutocompleteResultsExecutor
         $optgroupLabels = [];
 
         foreach ($paginator as $entity) {
-            $result = $this->formatResult($autocompleter, $entity);
+            $result = [
+                'value' => $autocompleter->getValue($entity),
+                'text' => $autocompleter->getLabel($entity),
+            ];
 
             $groupLabels = $groupBy($entity, $result['value'], $result['text']);
 
@@ -118,21 +124,4 @@ final class AutocompleteResultsExecutor
 
         return new AutocompleteResults($results, $hasNextPage, $optgroups);
     }
-
-    /**
-     * @return array<string, mixed>
-     */
-    private function formatResult(EntityAutocompleterInterface $autocompleter, object $entity): array
-    {
-        $attributes = [];
-        if (method_exists($autocompleter, 'getAttributes')) {
-            $attributes = $autocompleter->getAttributes($entity);
-        }
-
-        return [
-            ...$attributes,
-            'value' => $autocompleter->getValue($entity),
-            'text' => $autocompleter->getLabel($entity),
-        ];
-    }
 }
diff --git a/src/Autocomplete/src/EntityAutocompleterInterface.php b/src/Autocomplete/src/EntityAutocompleterInterface.php
index 4ad8a855..df0874fd 100644
--- a/src/Autocomplete/src/EntityAutocompleterInterface.php
+++ b/src/Autocomplete/src/EntityAutocompleterInterface.php
@@ -18,50 +18,30 @@ use Symfony\Bundle\SecurityBundle\Security;
 /**
  * Interface for classes that will have an "autocomplete" endpoint exposed.
  *
- * @template T of object
- *
- * TODO Remove next lines for Symfony UX 3
- *
- * @method array getAttributes(object $entity) Returns extra attributes to add to the autocomplete result.
- * @method mixed getGroupBy()                  Return group_by option.
+ * @method mixed getGroupBy() Return group_by option.
  */
 interface EntityAutocompleterInterface
 {
     /**
      * The fully-qualified entity class this will be autocompleting.
-     *
-     * @return class-string<T>
      */
     public function getEntityClass(): string;
 
     /**
      * Create a query builder that filters for the given "query".
-     *
-     * @param EntityRepository<T> $repository
      */
     public function createFilteredQueryBuilder(EntityRepository $repository, string $query): QueryBuilder;
 
     /**
      * Returns the "choice_label" used to display this entity.
-     *
-     * @param T $entity
      */
     public function getLabel(object $entity): string;
 
     /**
      * Returns the "value" attribute for this entity, usually the id.
-     *
-     * @param T $entity
      */
     public function getValue(object $entity): mixed;
 
-    /**
-     * Returns extra attributes to add to the autocomplete result.
-     *
-     * TODO Uncomment for Symfony UX 3
-     */
-    /* public function getAttributes(object $entity): array; */
-
     /**
      * Return true if access should be granted to the autocomplete results for the current user.
      *
@@ -71,8 +51,6 @@ interface EntityAutocompleterInterface
 
     /*
      * Return group_by option.
-     *
-     * TODO Uncomment for Symfony UX 3
      */
     /* public function getGroupBy(): mixed; */
 }
diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md
index 7ba03516..a43e9518 100644
--- a/src/LiveComponent/CHANGELOG.md
+++ b/src/LiveComponent/CHANGELOG.md
@@ -3,7 +3,6 @@
 ## 2.23.0
 
 -   Allow configuring the secret used to compute fingerprints and checksums.
--   Prevent `__component` property to be serialized when called `JSON.stringify()`
 
 ## 2.22.0
 
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 41177512..375a5b15 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -3059,10 +3059,7 @@ class LiveControllerDefault extends Controller {
         const id = this.element.id || null;
         this.component = new Component(this.element, this.nameValue, this.propsValue, this.listenersValue, id, LiveControllerDefault.backendFactory(this), new StimulusElementDriver(this));
         this.proxiedComponent = proxifyComponent(this.component);
-        Object.defineProperty(this.element, '__component', {
-            value: this.proxiedComponent,
-            writable: true,
-        });
+        this.element.__component = this.proxiedComponent;
         if (this.hasDebounceValue) {
             this.component.defaultDebounce = this.debounceValue;
         }
diff --git a/src/LiveComponent/assets/src/live_controller.ts b/src/LiveComponent/assets/src/live_controller.ts
index a9ea7f11..278915e0 100644
--- a/src/LiveComponent/assets/src/live_controller.ts
+++ b/src/LiveComponent/assets/src/live_controller.ts
@@ -285,10 +285,8 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
         );
         this.proxiedComponent = proxifyComponent(this.component);
 
-        Object.defineProperty(this.element, '__component', {
-            value: this.proxiedComponent,
-            writable: true,
-        });
+        // @ts-ignore Adding the dynamic property
+        this.element.__component = this.proxiedComponent;
 
         if (this.hasDebounceValue) {
             this.component.defaultDebounce = this.debounceValue;
diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst
index 1b6b58be..a2ad793f 100644
--- a/src/LiveComponent/doc/index.rst
+++ b/src/LiveComponent/doc/index.rst
@@ -1326,39 +1326,6 @@ The files will be available in a regular ``$request->files`` files bag::
     need to specify ``multiple`` attribute on HTML element and end ``name``
     with ``[]``.
 
-.. _downloads:
-
-Downloading files
------------------
-
-Currently, Live Components do not natively support returning file responses directly from a LiveAction. However, you can implement file downloads by redirecting to a route that handles the file response.
-
-Create a LiveAction that generates the URL for the file download and returns a `RedirectResponse`::
-
-        #[LiveAction]
-        public function initiateDownload(UrlGeneratorInterface $urlGenerator): RedirectResponse
-        {
-            $url = $urlGenerator->generate('app_file_download');
-            return new RedirectResponse($url);
-        }
-
-.. code-block:: html+twig
-
-    <div {{ attributes }} data-turbo="false">
-        <button
-            data-action="live#action"
-            data-live-action-param="initiateDownload"
-        >
-            Download
-        </button>
-    </div>
-
-
-.. tip::
-
-    When Turbo is enabled, if a LiveAction response redirects to another URL, Turbo will make a request to prefetch the content. Here, adding ``data-turbo="false"`` ensures that the download URL is called only once.
-
-
 .. _forms:
 
 Forms
@@ -3722,6 +3689,9 @@ Test Helper
 
     The test helper was added in LiveComponents 2.11.
 
+Interact With Live-Components
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 For testing, you can use the ``InteractsWithLiveComponents`` trait which
 uses Symfony's test client to render and make requests to your components::
 
@@ -3808,6 +3778,54 @@ uses Symfony's test client to render and make requests to your components::
     The ``InteractsWithLiveComponents`` trait can only be used in tests that extend
     ``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``.
 
+Test LiveCollectionType
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To test the submission of a form within a Live Component (with the above ``submitForm`` helper) containing a ``LiveCollectionType``, you first need to programmatically add the desired number of entries to the form, replicating the action of clicking the "Add" button.
+
+So, if the following are the forms used::
+
+    use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
+
+    // Parent FormType used in the Live Component
+    class LiveCollectionFormType extends AbstractType
+    {
+        public function buildForm(FormBuilderInterface $builder, array $options): void
+        {
+            $builder->add('children', LiveCollectionType::class, [
+                    'entry_type' => ChildFormType::class,
+                ])
+            ;
+        }
+    }
+
+    // Child Form Type used for each entry in the collection
+    class ChildFormType extends AbstractType
+    {
+        public function buildForm(FormBuilderInterface $builder, array $options): void
+        {
+            $builder
+                ->add('name', TextType::class)
+                ->add('age', IntegerType::class)
+            ;
+        }
+    }
+
+Use the addCollectionItem method from the LiveCollectionTrait to dynamically add entries to the children field of the form before submitting it::
+
+    // Call the addCollectionItem method as many times as needed, specifying the name of the collection field.
+    $component->call('addCollectionItem', ['name' => 'children']);
+    $component->call('addCollectionItem', ['name' => 'children']);
+    //... can be called as many times as you need entries in your 'children' field
+
+    // ... then submit the form by providing data for all the fields in the ChildFormType for each added entry:
+    $component->submitForm([ 'live_collection_form' => [
+        'children' => [
+            ['name' => 'childName1', 'age' => 10],
+            ['name' => 'childName2', 'age' => 15],
+        ]
+    ]]);
+
 Backward Compatibility promise
 ------------------------------
 
diff --git a/src/LiveComponent/src/ComponentWithFormTrait.php b/src/LiveComponent/src/ComponentWithFormTrait.php
index 8f2aceab..c8ab525d 100644
--- a/src/LiveComponent/src/ComponentWithFormTrait.php
+++ b/src/LiveComponent/src/ComponentWithFormTrait.php
@@ -11,7 +11,6 @@
 
 namespace Symfony\UX\LiveComponent;
 
-use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
 use Symfony\Component\Form\ClearableErrorsInterface;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\Form\FormView;
@@ -256,39 +255,25 @@ trait ComponentWithFormTrait
                 continue;
             }
 
-            // <input type="checkbox">
             if (\array_key_exists('checked', $child->vars)) {
+                // special handling for check boxes
                 $values[$name] = $child->vars['checked'] ? $child->vars['value'] : null;
                 continue;
             }
 
-            // <select> - Simulate browser behavior
-            // When no option is selected, browsers send the value of the first
-            // option, when the following conditions are met:
-            if (
-                \array_key_exists('choices', $child->vars)
-                && $child->vars['choices']                  // has defined choices
-                && $child->vars['required']                 // is required
-                && !$child->vars['disabled']                // is not disabled
-                && '' === $child->vars['value']             // has no value set  ("0" can be a value)
-                && !array_diff_key(
-                    /* @see \Symfony\Component\Form\Extension\Core\Type\ChoiceType::buildView() */
-                    array_flip(['choices', 'expanded', 'multiple', 'placeholder', 'placeholder_in_choices', 'preferred_choices']),
-                    $child->vars,
-                )
-                && !$child->vars['expanded']                // is a <select>     (not a radio/checkbox)
-                && !$child->vars['multiple']                // is not multiple
-                && !\is_string($child->vars['placeholder'])  // has no placeholder (empty string is valid)
+            if (\array_key_exists('choices', $child->vars)
+                && $child->vars['required']
+                && !$child->vars['disabled']
+                && !$child->vars['value']
+                && (false === $child->vars['placeholder'] || null === $child->vars['placeholder'])
+                && !$child->vars['multiple']
+                && !$child->vars['expanded']
+                && $child->vars['choices']
             ) {
-                $choices = $child->vars['choices'];
-                do {
-                    $choice = $choices[array_key_first($choices)];
-                    if (!$choice instanceof ChoiceGroupView) {
-                        break;
-                    }
-                } while ($choices = $choice->choices);
-
-                $values[$name] = $choice?->value;
+                if (null !== $firstKey = array_key_first($child->vars['choices'])) {
+                    $values[$name] = $child->vars['choices'][$firstKey]->value ?? null;
+                }
+
                 continue;
             }
 
diff --git a/src/Turbo/doc/index.rst b/src/Turbo/doc/index.rst
index 83b781de..4618bd70 100644
--- a/src/Turbo/doc/index.rst
+++ b/src/Turbo/doc/index.rst
@@ -342,6 +342,11 @@ clients. There are two main ways to receive the updates:
 Forms
 ^^^^^
 
+.. versionadded:: 2.1
+
+    Prior to 2.1, ``TurboStreamResponse::STREAM_FORMAT`` was used instead of ``TurboBundle::STREAM_FORMAT``.
+    Also, one had to return a new ``TurboStreamResponse()`` object as the third argument to ``$this->render()``.
+
 Let's discover how to use Turbo Streams to enhance your `Symfony forms`_::
 
     // src/Controller/TaskController.php
@@ -378,6 +383,7 @@ Let's discover how to use Turbo Streams to enhance your `Symfony forms`_::
                 return $this->redirectToRoute('task_success', [], Response::HTTP_SEE_OTHER);
             }
 
+            // Symfony 6.2+
             return $this->render('task/new.html.twig', [
                 'form' => $form,
             ]);
@@ -494,7 +500,7 @@ Then, enable the "mercure stream" controller in ``assets/controllers.json``:
         "mercure-turbo-stream": {
     +         "enabled": true,
     -         "enabled": false,
-            "fetch": "eager"
+            "fetch": "lazy"
         }
     },
 
@@ -506,19 +512,14 @@ If you use Symfony Flex, the configuration has been generated for you,
 be sure to update the ``MERCURE_URL`` in the ``.env`` file to point to a
 Mercure Hub (it's not necessary if you are using Symfony Docker).
 
-Otherwise, configure Mercure Hub(s) as explained in the documentation:
+Otherwise, configure Mercure Hub(s) to use:
 
 .. code-block:: yaml
 
-    # config/packages/mercure.yaml
-    mercure:
-        hubs:
-            default:
-                url: '%env(MERCURE_URL)%'
-                public_url: '%env(MERCURE_PUBLIC_URL)%'
-                jwt:
-                    secret: '%env(MERCURE_JWT_SECRET)%'
-                    publish: '*'
+    # config/packages/turbo.yaml
+    turbo:
+        mercure:
+            hubs: [default]
 
 Let's create our chat::
 
@@ -784,6 +785,13 @@ the following configuration:
                 url: https://hub2.example.net/.well-known/mercure
                 jwt: snip
 
+.. code-block:: yaml
+
+    # config/packages/turbo.yaml
+    turbo:
+        mercure:
+            hubs: [hub1, hub2]
+
 Use the appropriate Mercure ``HubInterface`` service to send a change
 using a specific transport::
 
diff --git a/src/TwigComponent/src/Twig/ComponentNode.php b/src/TwigComponent/src/Twig/ComponentNode.php
index df735079..439175c5 100644
--- a/src/TwigComponent/src/Twig/ComponentNode.php
+++ b/src/TwigComponent/src/Twig/ComponentNode.php
@@ -112,7 +112,7 @@ final class ComponentNode extends Node implements NodeOutputInterface
             ->raw('), ')
             ->raw($this->getAttribute('only') ? '[]' : '$context')
             ->raw(', ')
-            ->string($this->getAttribute('embedded_template'))
+            ->string(TemplateNameParser::parse($this->getAttribute('embedded_template')))
             ->raw(', ')
             ->raw($this->getAttribute('embedded_index'))
             ->raw(");\n");
diff --git a/src/TwigComponent/src/Twig/ComponentTokenParser.php b/src/TwigComponent/src/Twig/ComponentTokenParser.php
index 31756468..3f94c09d 100644
--- a/src/TwigComponent/src/Twig/ComponentTokenParser.php
+++ b/src/TwigComponent/src/Twig/ComponentTokenParser.php
@@ -63,7 +63,7 @@ final class ComponentTokenParser extends AbstractTokenParser
         $this->parser->embedTemplate($module);
 
         // override the embedded index with a deterministic value, so it can be loaded in a controlled manner
-        $module->setAttribute('index', $this->generateEmbeddedTemplateIndex($stream->getSourceContext()->getName(), $token->getLine()));
+        $module->setAttribute('index', $this->generateEmbeddedTemplateIndex(TemplateNameParser::parse($stream->getSourceContext()->getName()), $token->getLine()));
 
         $stream->expect(Token::BLOCK_END_TYPE);
 
diff --git a/src/TwigComponent/src/Twig/TemplateNameParser.php b/src/TwigComponent/src/Twig/TemplateNameParser.php
new file mode 100644
index 00000000..2c9d5918
--- /dev/null
+++ b/src/TwigComponent/src/Twig/TemplateNameParser.php
@@ -0,0 +1,36 @@
+<?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\TwigComponent\Twig;
+
+/**
+ * @internal
+ */
+final class TemplateNameParser
+{
+    /**
+     * Copied from Twig\Loader\FilesystemLoader, and adjusted to needs for this class.
+     *
+     * @see \Twig\Loader\FilesystemLoader::parseName
+     */
+    public static function parse(string $name): string
+    {
+        if (str_starts_with($name, '@')) {
+            if (!str_contains($name, '/')) {
+                throw new \LogicException(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
+            }
+
+            return $name;
+        }
+
+        return $name;
+    }
+}


Test patch:
diff --git a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomAttributesProductAutocompleter.php b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomAttributesProductAutocompleter.php
deleted file mode 100644
index 7f86431a..00000000
--- a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomAttributesProductAutocompleter.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter;
-
-class CustomAttributesProductAutocompleter extends CustomProductAutocompleter
-{
-    public function getAttributes(object $entity): array
-    {
-        return [
-            'disabled' => true,
-            'value' => 'This value should be replaced with the result of getValue()',
-            'text' => 'This value should be replaced with the result of getText()',
-        ];
-    }
-}
diff --git a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
index 09b5b96c..912a864c 100644
--- a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
+++ b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
@@ -19,9 +19,6 @@ use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
 use Symfony\UX\Autocomplete\EntityAutocompleterInterface;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
 
-/**
- * @implements EntityAutocompleterInterface<Product>
- */
 class CustomProductAutocompleter implements EntityAutocompleterInterface
 {
     public function __construct(
@@ -61,11 +58,6 @@ class CustomProductAutocompleter implements EntityAutocompleterInterface
         return $entity->getId();
     }
 
-    public function getAttributes(object $entity): array
-    {
-        return [];
-    }
-
     public function isGranted(Security $security): bool
     {
         if ($this->requestStack->getCurrentRequest()?->query->get('enforce_test_security')) {
diff --git a/src/Autocomplete/tests/Fixtures/Kernel.php b/src/Autocomplete/tests/Fixtures/Kernel.php
index de2877b0..f1991a22 100644
--- a/src/Autocomplete/tests/Fixtures/Kernel.php
+++ b/src/Autocomplete/tests/Fixtures/Kernel.php
@@ -34,7 +34,6 @@ use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
 use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
 use Symfony\UX\Autocomplete\AutocompleteBundle;
 use Symfony\UX\Autocomplete\DependencyInjection\AutocompleteFormTypePass;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomAttributesProductAutocompleter;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomGroupByProductAutocompleter;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomProductAutocompleter;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Form\ProductType;
@@ -177,13 +176,6 @@ final class Kernel extends BaseKernel
                 'alias' => 'custom_group_by_product',
             ]);
 
-        $services->set(CustomAttributesProductAutocompleter::class)
-            ->public()
-            ->arg(1, new Reference('ux.autocomplete.entity_search_util'))
-            ->tag(AutocompleteFormTypePass::ENTITY_AUTOCOMPLETER_TAG, [
-                'alias' => 'custom_attributes_product',
-            ]);
-
         $services->alias('public.results_executor', 'ux.autocomplete.results_executor')
             ->public();
 
diff --git a/src/Autocomplete/tests/Integration/AutocompleteResultsExecutorTest.php b/src/Autocomplete/tests/Integration/AutocompleteResultsExecutorTest.php
deleted file mode 100644
index b804afd1..00000000
--- a/src/Autocomplete/tests/Integration/AutocompleteResultsExecutorTest.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * 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\Autocomplete\Tests\Integration;
-
-use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
-use Symfony\UX\Autocomplete\AutocompleteResultsExecutor;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomAttributesProductAutocompleter;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Kernel;
-use Zenstruck\Foundry\Test\Factories;
-use Zenstruck\Foundry\Test\ResetDatabase;
-
-class AutocompleteResultsExecutorTest extends KernelTestCase
-{
-    use Factories;
-    use ResetDatabase;
-
-    public function testItReturnsExtraAttributes(): void
-    {
-        $kernel = new Kernel('test', true);
-        $kernel->disableForms();
-        $kernel->boot();
-
-        $product = ProductFactory::createOne(['name' => 'Foo']);
-
-        /** @var AutocompleteResultsExecutor $executor */
-        $executor = $kernel->getContainer()->get('public.results_executor');
-        $autocompleter = $kernel->getContainer()->get(CustomAttributesProductAutocompleter::class);
-        $data = $executor->fetchResults($autocompleter, '', 1);
-        $this->assertCount(1, $data->results);
-        $this->assertSame(['disabled' => true, 'value' => $product->getId(), 'text' => 'Foo'], $data->results[0]);
-    }
-}
diff --git a/src/LiveComponent/tests/Fixtures/Form/FormWithManyDifferentFieldsType.php b/src/LiveComponent/tests/Fixtures/Form/FormWithManyDifferentFieldsType.php
index 59537991..f2a0e322 100644
--- a/src/LiveComponent/tests/Fixtures/Form/FormWithManyDifferentFieldsType.php
+++ b/src/LiveComponent/tests/Fixtures/Form/FormWithManyDifferentFieldsType.php
@@ -65,15 +65,6 @@ class FormWithManyDifferentFieldsType extends AbstractType
                     'foo' => 1,
                 ],
             ])
-            ->add('choice_required_without_placeholder_and_choice_group', ChoiceType::class, [
-                'choices' => [
-                    'Bar Group' => [
-                        'Bar Label' => 'ok',
-                        'Foo Label' => 'foo_value',
-                    ],
-                    'foo' => 1,
-                ],
-            ])
             ->add('choice_expanded', ChoiceType::class, [
                 'choices' => [
                     'foo' => 1,
diff --git a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
index bfbe477c..a3ed0f7c 100644
--- a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
+++ b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
@@ -181,7 +181,6 @@ class ComponentWithFormTest extends KernelTestCase
             'choice_required_with_placeholder' => '',
             'choice_required_with_empty_placeholder' => '',
             'choice_required_without_placeholder' => '2',
-            'choice_required_without_placeholder_and_choice_group' => 'ok',
             'choice_expanded' => '',
             'choice_multiple' => ['2'],
             'select_multiple' => ['2'],
diff --git a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
index 097e4d77..6bae36f9 100644
--- a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
+++ b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php
@@ -18,7 +18,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component2;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CategoryFixtureEntityFactory;
-use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 
 /**
@@ -26,7 +25,6 @@ use Zenstruck\Foundry\Test\ResetDatabase;
  */
 final class InteractsWithLiveComponentsTest extends KernelTestCase
 {
-    use Factories;
     use InteractsWithLiveComponents;
     use ResetDatabase;
 
diff --git a/src/LiveComponent/tests/Unit/Form/ComponentWithFormTest.php b/src/LiveComponent/tests/Unit/Form/ComponentWithFormTest.php
index 01941f10..931430d8 100644
--- a/src/LiveComponent/tests/Unit/Form/ComponentWithFormTest.php
+++ b/src/LiveComponent/tests/Unit/Form/ComponentWithFormTest.php
@@ -14,7 +14,6 @@ namespace Symfony\UX\LiveComponent\Tests\Unit\Form;
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Component\FormComponentWithManyDifferentFieldsType;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CategoryFixtureEntityFactory;
-use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 
 /**
@@ -22,7 +21,6 @@ use Zenstruck\Foundry\Test\ResetDatabase;
  */
 class ComponentWithFormTest extends KernelTestCase
 {
-    use Factories;
     use ResetDatabase;
 
     public function testFormValues(): void
@@ -48,7 +46,6 @@ class ComponentWithFormTest extends KernelTestCase
                 'choice_required_with_placeholder' => '',
                 'choice_required_with_empty_placeholder' => '',
                 'choice_required_without_placeholder' => '2',
-                'choice_required_without_placeholder_and_choice_group' => 'ok',
                 'choice_expanded' => '',
                 'choice_multiple' => ['2'],
                 'select_multiple' => ['2'],
diff --git a/src/TwigComponent/tests/Unit/Twig/TemplateNameParserTest.php b/src/TwigComponent/tests/Unit/Twig/TemplateNameParserTest.php
new file mode 100644
index 00000000..890a8caa
--- /dev/null
+++ b/src/TwigComponent/tests/Unit/Twig/TemplateNameParserTest.php
@@ -0,0 +1,47 @@
+<?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\TwigComponent\Tests\Unit\Twig;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\UX\TwigComponent\Twig\TemplateNameParser;
+
+/**
+ * @author Simon André <smn.andre@gmail.com>
+ */
+class TemplateNameParserTest extends TestCase
+{
+    /**
+     * @dataProvider provideParse
+     */
+    public function testParse(string $name, string $parsedName): void
+    {
+        $this->assertSame($parsedName, TemplateNameParser::parse($name));
+    }
+
+    public function testParseThrowsExceptionWhenInvalidName(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Malformed namespaced template name "@foo" (expecting "@namespace/template_name").');
+        TemplateNameParser::parse('@foo');
+    }
+
+    /**
+     * @return iterable<array{0: string, 1: string}>
+     */
+    public static function provideParse(): iterable
+    {
+        yield ['foo', 'foo'];
+        yield ['foo/bar', 'foo/bar'];
+        yield ['@Admin/foo', '@Admin/foo'];
+        yield ['Admin/foo', 'Admin/foo'];
+    }
+}