diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index 76971211296..59216da3bbc 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -73,8 +73,6 @@ jobs: - name: Configure E2E app run: | - echo 'APP_ENV=prod' >> .env.local - echo 'APP_DEBUG=0' >> .env.local echo 'APP_SECRET=df4c071596e64cc75a349456f2887ae2419ae650' >> .env.local working-directory: apps/e2e @@ -83,11 +81,12 @@ jobs: with: working-directory: apps/e2e dependency-versions: highest - composer-options: --no-dev custom-cache-suffix: symfony-${{ matrix.symfony }} - name: Prepare E2E app run: | + echo 'APP_ENV=prod' >> .env.local + echo 'APP_DEBUG=0' >> .env.local symfony composer dump-autoload --classmap-authoritative --no-dev symfony composer dump-env symfony console asset-map:compile diff --git a/apps/e2e/.env b/apps/e2e/.env index c5badfcd659..ae8e8d47fba 100644 --- a/apps/e2e/.env +++ b/apps/e2e/.env @@ -23,7 +23,7 @@ APP_SECRET= # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # -DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" diff --git a/apps/e2e/composer.json b/apps/e2e/composer.json index f0fa6f37408..4736d725fae 100644 --- a/apps/e2e/composer.json +++ b/apps/e2e/composer.json @@ -7,7 +7,8 @@ "auto-scripts": { "cache:clear": "symfony-cmd", "assets:install %PUBLIC_DIR%": "symfony-cmd", - "importmap:install": "symfony-cmd" + "importmap:install": "symfony-cmd", + "foundry:load-fixtures": "symfony-cmd" }, "post-install-cmd": [ "@auto-scripts" @@ -69,7 +70,8 @@ "symfony/debug-bundle": "6.4.*|7.3.*", "symfony/maker-bundle": "^1.64", "symfony/stopwatch": "6.4.*|7.3.*", - "symfony/web-profiler-bundle": "6.4.*|7.3.*" + "symfony/web-profiler-bundle": "6.4.*|7.3.*", + "zenstruck/foundry": "^2.8" }, "config": { "platform": { diff --git a/apps/e2e/config/bundles.php b/apps/e2e/config/bundles.php index a90e1a7edd1..0354973075c 100644 --- a/apps/e2e/config/bundles.php +++ b/apps/e2e/config/bundles.php @@ -30,4 +30,5 @@ Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], Symfony\UX\Typed\TypedBundle::class => ['all' => true], Symfony\UX\Vue\VueBundle::class => ['all' => true], + Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/apps/e2e/config/packages/zenstruck_foundry.yaml b/apps/e2e/config/packages/zenstruck_foundry.yaml new file mode 100644 index 00000000000..2f60dd01843 --- /dev/null +++ b/apps/e2e/config/packages/zenstruck_foundry.yaml @@ -0,0 +1,16 @@ +when@dev: &dev + # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration + zenstruck_foundry: + persistence: + # Flush only once per call of `PersistentObjectFactory::create()` + flush_once: true + + # If you use the `make:factory --test` command, you may need to uncomment the following. + # See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#generate + #services: + # App\Tests\Factory\: + # resource: '%kernel.project_dir%/tests/Factory/' + # autowire: true + # autoconfigure: true + +when@test: *dev diff --git a/apps/e2e/src/Controller/AutocompleteController.php b/apps/e2e/src/Controller/AutocompleteController.php index ee3a33be6c3..bc530c60543 100644 --- a/apps/e2e/src/Controller/AutocompleteController.php +++ b/apps/e2e/src/Controller/AutocompleteController.php @@ -2,26 +2,66 @@ namespace App\Controller; -use App\Form\Type\AutocompleteSelectType; +use App\Form\FruitAutocompleteField; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; #[Route('/ux-autocomplete')] final class AutocompleteController extends AbstractController { #[Route('/without-ajax')] - public function index() + public function withoutAjax(): Response { - $form = $this->createForm(AutocompleteSelectType::class); + $formBuilder = $this->createFormBuilder(); + $formBuilder->add('favorite_fruit', ChoiceType::class, [ + 'autocomplete' => true, + 'label' => 'Your favorite fruit:', + 'choices' => [ + 'Apple' => 'apple', + 'Banana' => 'banana', + 'Cherry' => 'cherry', + 'Coconut' => 'coconut', + 'Grape' => 'grape', + 'Kiwi' => 'kiwi', + 'Lemon' => 'lemon', + 'Mango' => 'mango', + 'Orange' => 'orange', + 'Papaya' => 'papaya', + 'Peach' => 'peach', + 'Pineapple' => 'pineapple', + 'Pear' => 'pear', + 'Pomegranate' => 'pomegranate', + 'Pomelo' => 'pomelo', + 'Raspberry' => 'raspberry', + 'Strawberry' => 'strawberry', + 'Watermelon' => 'watermelon', + ], + ]); - return $this->render( - 'ux_autocomplete/index.html.twig', - ['form' => $form->createView()] - ); + $form = $formBuilder->getForm(); + + return $this->render('ux_autocomplete/without_ajax.html.twig', [ + 'form' => $form->createView() + ]); + } + + #[Route('/with-ajax')] + public function withAjax(): Response + { + $formBuilder = $this->createFormBuilder(); + $formBuilder->add('favorite_fruit', FruitAutocompleteField::class); + + $form = $formBuilder->getForm(); + + return $this->render('ux_autocomplete/with_ajax.html.twig', [ + 'form' => $form->createView() + ]); } #[Route('/custom-controller')] - public function customController() + public function customController(): Response { return $this->render('ux_autocomplete/custom_controller.html.twig'); } diff --git a/apps/e2e/src/Entity/Fruit.php b/apps/e2e/src/Entity/Fruit.php new file mode 100644 index 00000000000..3ad9f798b9a --- /dev/null +++ b/apps/e2e/src/Entity/Fruit.php @@ -0,0 +1,36 @@ +id = $id; + $fruit->name = $name; + + return $fruit; + } + + public function getId(): string + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/apps/e2e/src/Factory/FruitFactory.php b/apps/e2e/src/Factory/FruitFactory.php new file mode 100644 index 00000000000..b30b525f577 --- /dev/null +++ b/apps/e2e/src/Factory/FruitFactory.php @@ -0,0 +1,36 @@ + + */ +final class FruitFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return Fruit::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + */ + protected function defaults(): array|callable + { + return []; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): static + { + return $this + ->instantiateWith(Instantiator::namedConstructor('create')) + ; + } +} diff --git a/apps/e2e/src/Form/FruitAutocompleteField.php b/apps/e2e/src/Form/FruitAutocompleteField.php new file mode 100644 index 00000000000..33d0d1776d5 --- /dev/null +++ b/apps/e2e/src/Form/FruitAutocompleteField.php @@ -0,0 +1,28 @@ +setDefaults([ + 'class' => Fruit::class, + 'placeholder' => 'Choose a Fruit', + 'choice_value' => static fn (?Fruit $fruit) => $fruit?->getId(), + 'choice_label' => static fn (Fruit $fruit) => $fruit->getName(), + ]); + } + + public function getParent(): string + { + return BaseEntityAutocompleteType::class; + } +} diff --git a/apps/e2e/src/Form/Type/AutocompleteSelectType.php b/apps/e2e/src/Form/Type/AutocompleteSelectType.php deleted file mode 100644 index 219f11eb9a0..00000000000 --- a/apps/e2e/src/Form/Type/AutocompleteSelectType.php +++ /dev/null @@ -1,42 +0,0 @@ -add( - 'favorite_fruit', - ChoiceType::class, - [ - 'choices' => [ - 'Apple' => 'apple', - 'Banana' => 'banana', - 'Cherry' => 'cherry', - 'Coconut' => 'coconut', - 'Grape' => 'grape', - 'Kiwi' => 'kiwi', - 'Lemon' => 'lemon', - 'Mango' => 'mango', - 'Orange' => 'orange', - 'Papaya' => 'papaya', - 'Peach' => 'peach', - 'Pineapple' => 'pineapple', - 'Pear' => 'pear', - 'Pomegranate' => 'pomegranate', - 'Pomelo' => 'pomelo', - 'Raspberry' => 'raspberry', - 'Strawberry' => 'strawberry', - 'Watermelon' => 'watermelon', - ], - 'autocomplete' => true, - 'label' => 'Your favorite fruit:' - ] - ); - } -} diff --git a/apps/e2e/src/Repository/ExampleRepository.php b/apps/e2e/src/Repository/ExampleRepository.php index fad465e07ee..86e8b78b673 100644 --- a/apps/e2e/src/Repository/ExampleRepository.php +++ b/apps/e2e/src/Repository/ExampleRepository.php @@ -24,8 +24,9 @@ class ExampleRepository public function __construct() { $this->examples = [ - new Example(UxPackage::Autocomplete, 'Autocomplete (without AJAX)', 'An autocomplete component to enhance a simple choice field.', '/ux-autocomplete/without-ajax'), - new Example(UxPackage::Autocomplete, 'Autocomplete (custom controller)', 'An autocomplete component with a custom Stimulus controller for AJAX results.', '/ux-autocomplete/custom-controller'), + new Example(UxPackage::Autocomplete, 'Autocomplete (with AJAX)', 'An autocomplete form field, by fetching results with AJAX.', '/ux-autocomplete/with-ajax'), + new Example(UxPackage::Autocomplete, 'Autocomplete (without AJAX)', 'An autocomplete form field, by using the choses from the choice type field.', '/ux-autocomplete/without-ajax'), + new Example(UxPackage::Autocomplete, 'Autocomplete (custom controller)', 'An autocomplete form field, with a custom Stimulus controller for AJAX results.', '/ux-autocomplete/custom-controller'), new Example(UxPackage::Map, 'Basic map (Leaflet)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=leaflet'), new Example(UxPackage::Map, 'Basic map (Google)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=google'), new Example(UxPackage::Map, 'With markers, fit bounds (Leaflet)', 'A map with 2 markers, and the bounds are automatically adjusted to fit both markers', '/ux-map/with-markers-and-fit-bounds-to-markers?renderer=leaflet'), diff --git a/apps/e2e/src/Repository/FruitRepository.php b/apps/e2e/src/Repository/FruitRepository.php new file mode 100644 index 00000000000..0118efe9de4 --- /dev/null +++ b/apps/e2e/src/Repository/FruitRepository.php @@ -0,0 +1,18 @@ + + */ +class FruitRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Fruit::class); + } +} diff --git a/apps/e2e/src/Story/AppStory.php b/apps/e2e/src/Story/AppStory.php new file mode 100644 index 00000000000..07b1bc46459 --- /dev/null +++ b/apps/e2e/src/Story/AppStory.php @@ -0,0 +1,35 @@ + 'apple', 'name' => 'Apple'], + ['id' => 'banana', 'name' => 'Banana'], + ['id' => 'cherry', 'name' => 'Cherry'], + ['id' => 'coconut', 'name' => 'Coconut'], + ['id' => 'grape', 'name' => 'Grape'], + ['id' => 'kiwi', 'name' => 'Kiwi'], + ['id' => 'lemon', 'name' => 'Lemon'], + ['id' => 'mango', 'name' => 'Mango'], + ['id' => 'orange', 'name' => 'Orange'], + ['id' => 'papaya', 'name' => 'Papaya'], + ['id' => 'peach', 'name' => 'Peach'], + ['id' => 'pineapple', 'name' => 'Pineapple'], + ['id' => 'pear', 'name' => 'Pear'], + ['id' => 'pomegranate', 'name' => 'Pomegranate'], + ['id' => 'pomelo', 'name' => 'Pomelo'], + ['id' => 'raspberry', 'name' => 'Raspberry'], + ['id' => 'strawberry', 'name' => 'Strawberry'], + ['id' => 'watermelon', 'name' => 'Watermelon'], + ]); + } +} diff --git a/apps/e2e/symfony.lock b/apps/e2e/symfony.lock index fc7be91565d..e84905f5a25 100644 --- a/apps/e2e/symfony.lock +++ b/apps/e2e/symfony.lock @@ -452,5 +452,18 @@ }, "twig/extra-bundle": { "version": "v3.21.0" + }, + "zenstruck/foundry": { + "version": "2.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.7", + "ref": "7fc98f546dfeaa83cc2110634f8ff078d070b965" + }, + "files": [ + "config/packages/zenstruck_foundry.yaml", + "src/Story/AppStory.php" + ] } } diff --git a/apps/e2e/templates/ux_autocomplete/index.html.twig b/apps/e2e/templates/ux_autocomplete/with_ajax.html.twig similarity index 52% rename from apps/e2e/templates/ux_autocomplete/index.html.twig rename to apps/e2e/templates/ux_autocomplete/with_ajax.html.twig index bc4d51d6b78..f99295f8d25 100644 --- a/apps/e2e/templates/ux_autocomplete/index.html.twig +++ b/apps/e2e/templates/ux_autocomplete/with_ajax.html.twig @@ -1,8 +1,7 @@ {% extends 'example.html.twig' %} {% block example %} -Autocomplete: - {{ form_start(form) }} - {{ form_widget(form.favorite_fruit) }} + {{ form_start(form) }} + {{ form_row(form.favorite_fruit) }} {{ form_end(form) }} {% endblock %} diff --git a/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig b/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig new file mode 100644 index 00000000000..f99295f8d25 --- /dev/null +++ b/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig @@ -0,0 +1,7 @@ +{% extends 'example.html.twig' %} + +{% block example %} + {{ form_start(form) }} + {{ form_row(form.favorite_fruit) }} + {{ form_end(form) }} +{% endblock %} diff --git a/src/Autocomplete/assets/test/browser/autocomplete.test.ts b/src/Autocomplete/assets/test/browser/autocomplete.test.ts new file mode 100644 index 00000000000..fb6f7c35db5 --- /dev/null +++ b/src/Autocomplete/assets/test/browser/autocomplete.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; + +test('Can display and interact with AJAX-based autocomplete field', async ({ page }) => { + await page.goto('/ux-autocomplete/with-ajax'); + + await expect(page.locator('css=.ts-wrapper'), 'TomSelect has not been initialized.').toBeVisible(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue(''); + + await page.getByRole('combobox', { name: 'Favorite fruit' }).click(); + await page.waitForResponse('**/autocomplete/fruit_autocomplete_field?query='); + await page.getByRole('option', { name: 'Apple' }).click(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue('apple'); + + await page.getByText('⨯').click(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue(''); + + await page.getByRole('combobox', { name: 'Favorite fruit' }).click(); + await page.getByRole('option', { name: 'Grape' }).click(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue('grape'); +}); + +test('Can display and interact with non-AJAX-based autocomplete field', async ({ page }) => { + await page.goto('/ux-autocomplete/without-ajax'); + + await expect(page.locator('css=.ts-wrapper'), 'TomSelect has not been initialized.').toBeVisible(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue('apple'); + + await page.getByText('⨯').click(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue(''); + + await page.getByRole('combobox', { name: 'Favorite fruit' }).click(); + await page.getByRole('option', { name: 'Grape' }).filter({ visible: true }).click(); + await expect(page.locator('css=select[id="form_favorite_fruit"]')).toHaveValue('grape'); +}); diff --git a/src/Autocomplete/assets/test/browser/placeholder.test.ts b/src/Autocomplete/assets/test/browser/placeholder.test.ts deleted file mode 100644 index f5f1d10f22e..00000000000 --- a/src/Autocomplete/assets/test/browser/placeholder.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('Can see homepage', async ({ page }) => { - await page.goto('/'); - - await expect(page.getByText("Symfony UX's E2E App")).toBeVisible(); -});