From 5067140493ec08e392ef37b804f2afd85414c6a9 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Wed, 20 Aug 2025 15:12:58 +0200 Subject: [PATCH 1/2] [E2E] Hide React/Vue/Svelte components, until we start to add E2E tests for them --- .../assets/react/controllers/{Hello.jsx => Hello.jsx.ignore} | 0 .../svelte/controllers/{Hello.svelte => Hello.svelte.ignore} | 0 .../assets/vue/controllers/{Hello.vue => Hello.vue.ignore} | 0 test_apps/e2e-app/importmap.php | 3 +++ 4 files changed, 3 insertions(+) rename test_apps/e2e-app/assets/react/controllers/{Hello.jsx => Hello.jsx.ignore} (100%) rename test_apps/e2e-app/assets/svelte/controllers/{Hello.svelte => Hello.svelte.ignore} (100%) rename test_apps/e2e-app/assets/vue/controllers/{Hello.vue => Hello.vue.ignore} (100%) diff --git a/test_apps/e2e-app/assets/react/controllers/Hello.jsx b/test_apps/e2e-app/assets/react/controllers/Hello.jsx.ignore similarity index 100% rename from test_apps/e2e-app/assets/react/controllers/Hello.jsx rename to test_apps/e2e-app/assets/react/controllers/Hello.jsx.ignore diff --git a/test_apps/e2e-app/assets/svelte/controllers/Hello.svelte b/test_apps/e2e-app/assets/svelte/controllers/Hello.svelte.ignore similarity index 100% rename from test_apps/e2e-app/assets/svelte/controllers/Hello.svelte rename to test_apps/e2e-app/assets/svelte/controllers/Hello.svelte.ignore diff --git a/test_apps/e2e-app/assets/vue/controllers/Hello.vue b/test_apps/e2e-app/assets/vue/controllers/Hello.vue.ignore similarity index 100% rename from test_apps/e2e-app/assets/vue/controllers/Hello.vue rename to test_apps/e2e-app/assets/vue/controllers/Hello.vue.ignore diff --git a/test_apps/e2e-app/importmap.php b/test_apps/e2e-app/importmap.php index 45debfa15de..fd47adc87d8 100644 --- a/test_apps/e2e-app/importmap.php +++ b/test_apps/e2e-app/importmap.php @@ -85,6 +85,9 @@ 'react-dom' => [ 'version' => '18.3.1', ], + 'react-dom/client' => [ + 'version' => '18.3.1', + ], 'scheduler' => [ 'version' => '0.23.2', ], From 174b837e92ef69a0fe46c39f153ed9ba86a80b52 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Thu, 21 Aug 2025 10:36:16 +0200 Subject: [PATCH 2/2] [Map] Add E2E tests --- .../Google/assets/test/browser/map.test.ts | 13 + .../test/browser/map_controller.test.ts | 71 ------ .../Leaflet/assets/test/browser/map.test.ts | 178 +++++++++++++ .../test/browser/map_controller.test.ts | 72 ------ test/playwright-helpers.ts | 16 ++ .../e2e-app/assets/icons/mdi/eiffel-tower.svg | 1 + .../e2e-app/assets/icons/mdi/glass-wine.svg | 1 + test_apps/e2e-app/config/packages/ux_map.yaml | 4 +- .../e2e-app/src/Controller/MapController.php | 235 +++++++++++++++++- test_apps/e2e-app/src/Kernel.php | 23 +- test_apps/e2e-app/src/MapRenderer.php | 9 + .../e2e-app/templates/ux_map/index.html.twig | 1 - .../templates/ux_map/render_map.html.twig | 5 + 13 files changed, 475 insertions(+), 154 deletions(-) create mode 100644 src/Map/src/Bridge/Google/assets/test/browser/map.test.ts delete mode 100644 src/Map/src/Bridge/Google/assets/test/browser/map_controller.test.ts create mode 100644 src/Map/src/Bridge/Leaflet/assets/test/browser/map.test.ts delete mode 100644 src/Map/src/Bridge/Leaflet/assets/test/browser/map_controller.test.ts create mode 100644 test/playwright-helpers.ts create mode 100644 test_apps/e2e-app/assets/icons/mdi/eiffel-tower.svg create mode 100644 test_apps/e2e-app/assets/icons/mdi/glass-wine.svg create mode 100644 test_apps/e2e-app/src/MapRenderer.php delete mode 100644 test_apps/e2e-app/templates/ux_map/index.html.twig create mode 100644 test_apps/e2e-app/templates/ux_map/render_map.html.twig diff --git a/src/Map/src/Bridge/Google/assets/test/browser/map.test.ts b/src/Map/src/Bridge/Google/assets/test/browser/map.test.ts new file mode 100644 index 00000000000..a81d741eb3b --- /dev/null +++ b/src/Map/src/Bridge/Google/assets/test/browser/map.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +test('Can render basic map', async ({ page }) => { + await page.goto('/ux-map/basic?renderer=google'); + + await expect(await page.getByTestId('map')).toBeVisible(); + + // Since we can't test Google Maps rendering due to API costs, we only assert that Google Maps API is (wrongly) loaded + await expect(await page.getByTestId('map')).toContainText('Oops! Something went wrong.'); + await expect(await page.getByTestId('map')).toContainText( + "This page didn't load Google Maps correctly. See the JavaScript console for technical details." + ); +}); diff --git a/src/Map/src/Bridge/Google/assets/test/browser/map_controller.test.ts b/src/Map/src/Bridge/Google/assets/test/browser/map_controller.test.ts deleted file mode 100644 index 8e0f3392c62..00000000000 --- a/src/Map/src/Bridge/Google/assets/test/browser/map_controller.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Application, Controller } from '@hotwired/stimulus'; -import { getByTestId, waitFor } from '@testing-library/dom'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { clearDOM, mountDOM } from '../../../../../../../../test/stimulus-helpers'; -import GoogleController from '../../src/map_controller'; - -// Controller used to check the actual controller was properly booted -class CheckController extends Controller { - connect() { - this.element.addEventListener('ux:map:pre-connect', (_event) => { - this.element.classList.add('pre-connected'); - }); - - this.element.addEventListener('ux:map:connect', (_event) => { - this.element.classList.add('connected'); - }); - } -} - -const startStimulus = () => { - const application = Application.start(); - application.register('check', CheckController); - application.register('symfony--ux-google-map--map', GoogleController); -}; - -describe('GoogleMapsController', () => { - let container: HTMLElement; - - beforeEach(() => { - container = mountDOM(` -
- `); - }); - - afterEach(() => { - clearDOM(); - }); - - it('connect', async () => { - const div = getByTestId(container, 'map'); - expect(div).not.toHaveClass('pre-connected'); - expect(div).not.toHaveClass('connected'); - - startStimulus(); - await waitFor(() => expect(div).toHaveClass('pre-connected')); - await waitFor(() => expect(div).toHaveClass('connected')); - }); -}); diff --git a/src/Map/src/Bridge/Leaflet/assets/test/browser/map.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/browser/map.test.ts new file mode 100644 index 00000000000..735a1b75846 --- /dev/null +++ b/src/Map/src/Bridge/Leaflet/assets/test/browser/map.test.ts @@ -0,0 +1,178 @@ +import { expect, type Page, test } from '@playwright/test'; +import { getSymfonyKernelVersionId } from '../../../../../../../../test/playwright-helpers'; + +async function expectMapToBeVisible(page: Page) { + await expect(await page.getByTestId('map')).toBeVisible(); + await expect(await page.getByTestId('map').locator('.leaflet-pane').first()).toBeAttached(); + await expect(await page.getByTestId('map').locator('.leaflet-control-container')).toBeAttached(); +} + +async function expectOneInfoWindowToBeOpenedAndContainText(page: Page, text: string) { + const popups = page.locator('.leaflet-popup'); + await expect(popups).toHaveCount(1); + await expect(popups.first()).toBeInViewport(); + await expect(popups.first()).toContainText(text); +} + +test('Can render basic map', async ({ page }) => { + await page.goto('/ux-map/basic?renderer=leaflet'); + await expectMapToBeVisible(page); +}); + +test('Can render markers and fit bounds to marker', async ({ page }) => { + await page.goto('/ux-map/with-markers-and-fit-bounds-to-markers?renderer=leaflet'); + await expectMapToBeVisible(page); + + const markers = page.getByTestId('map').locator('.leaflet-marker-icon'); + await expect(markers, '2 markers should be present').toHaveCount(2); + for (const marker of await markers.all()) { + await expect(marker).toBeInViewport(); + } + + await expect(markers.nth(0)).toHaveAttribute('title', 'Paris'); + await expect(markers.nth(1)).toHaveAttribute('title', 'Lyon'); +}); + +test('Can render markers, zoomed on Paris, Lyon marker should be hidden', async ({ page }) => { + await page.goto('/ux-map/with-markers-and-zoomed-on-paris?renderer=leaflet'); + await expectMapToBeVisible(page); + + // Ensure the two markers are rendered, but only the Paris marker should be visible in the viewport + const markers = page.getByTestId('map').locator('.leaflet-marker-icon'); + await expect(markers).toHaveCount(2); + await expect(markers.nth(0)).toHaveAttribute('title', 'Paris'); + await expect(markers.nth(0)).toBeInViewport(); + await expect(markers.nth(1)).toHaveAttribute('title', 'Lyon'); + await expect(markers.nth(1), 'The "Lyon" marker should not be visible').not.toBeInViewport(); +}); + +test('Can render markers and info windows', async ({ page }) => { + await page.goto('/ux-map/with-markers-and-info-windows?renderer=leaflet'); + await expectMapToBeVisible(page); + + const markers = page.getByTestId('map').locator('.leaflet-marker-icon'); + await expect(markers, '2 markers should be present').toHaveCount(2); + for (const marker of await markers.all()) { + await expect(marker).toBeInViewport(); + } + + await expect(markers.nth(0)).toHaveAttribute('title', 'Paris'); + await expect(markers.nth(1)).toHaveAttribute('title', 'Lyon'); + + // Ensure only one popup is visible at a time, the popup for Paris should be opened by default + await expectOneInfoWindowToBeOpenedAndContainText(page, 'Capital of France'); + + // Click on the Lyon marker to open its popup, the Paris popup should close + await markers.nth(1).click(); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'Famous for its gastronomy'); +}); + +test('Can render markers with custom icons', async ({ page }) => { + await page.goto('/ux-map/with-markers-and-custom-icons?renderer=leaflet'); + await expectMapToBeVisible(page); + + const markers = page.getByTestId('map').locator('.leaflet-marker-icon'); + await expect(markers, '3 markers should be present').toHaveCount(3); + for (const marker of await markers.all()) { + await expect(marker).toBeInViewport(); + } + + await expect(markers.nth(0)).toHaveAttribute('title', 'Paris'); + expect(await markers.nth(0).innerHTML()).toEqual( + '' + ); + + await expect(markers.nth(1)).toHaveAttribute('title', 'Lyon'); + expect(await markers.nth(1).innerHTML()).toEqual( + '' + ); + + await expect(markers.nth(2)).toHaveAttribute('title', 'Bordeaux'); + await expect(markers.nth(2)).toHaveAttribute( + 'src', + getSymfonyKernelVersionId() >= 70200 + ? '/assets/icons/mdi/glass-wine-SOLVwOG.svg' + : '/assets/icons/mdi/glass-wine-48e2d5c0e18f9b07dab82e113ec7490e.svg' + ); +}); + +test('Can render polygons', async ({ page }) => { + await page.goto('/ux-map/with-polygons?renderer=leaflet'); + await expectMapToBeVisible(page); + + const paths = page.getByTestId('map').locator('path.leaflet-interactive'); + await expect(paths, '2 polygons must be present').toHaveCount(2); + await expect(paths.nth(0)).toHaveAttribute( + 'd', + 'M548 276L656 188L762 260L708 433L573 384zM615 352L696 354L678 236L640 250z' + ); + await expect(paths.nth(1)).toHaveAttribute('d', 'M870 476L795 364L844 395L911 508z'); + + // Workaround for `paths.nth(0).click({ relative: { ... } })` which does not work, it tries to click the center of the polygon, + // but since it's empty, the popup can't be opened. + const firstPathBoundingBox = await paths.nth(0).boundingBox(); + await page.mouse.click(firstPathBoundingBox.x + 40, firstPathBoundingBox.y + 100); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A weird shape on the France'); + + const secondPathBoundingBox = await paths.nth(1).boundingBox(); + await page.mouse.click(secondPathBoundingBox.x + 50, secondPathBoundingBox.y + 40); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A polygon covering some of the main cities in Italy'); +}); + +test('Can render polylines', async ({ page }) => { + await page.goto('/ux-map/with-polylines?renderer=leaflet'); + await expectMapToBeVisible(page); + + const paths = page.getByTestId('map').locator('path.leaflet-interactive'); + await expect(paths, '2 polylines must be present').toHaveCount(2); + await expect(paths.nth(0)).toHaveAttribute('d', 'M640 250L696 354L708 433L573 384'); + await expect(paths.nth(1)).toHaveAttribute('d', 'M548 276L551 306L603 302'); + + // Workaround for `paths.nth(0).click({ relative: { ... } })` which does not work, it tries to click the center of the polygon, + // but since it's empty, the popup can't be opened. + const firstPathBoundingBox = await paths.nth(0).boundingBox(); + await page.mouse.click(firstPathBoundingBox.x + 95, firstPathBoundingBox.y + 50); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A line passing through Paris, Lyon, Marseille, Bordeaux'); + + const secondPathBoundingBox = await paths.nth(1).boundingBox(); + await page.mouse.click(secondPathBoundingBox.x + 5, secondPathBoundingBox.y + 25); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A line passing through Rennes, Nantes and Tours'); +}); + +test('Can render circles', async ({ page }) => { + await page.goto('/ux-map/with-circles?renderer=leaflet'); + await expectMapToBeVisible(page); + + const paths = page.getByTestId('map').locator('path.leaflet-interactive'); + await expect(paths, '2 circles must be present').toHaveCount(2); + await expect(paths.nth(0)).toHaveAttribute( + 'd', + 'M623.5256177777774,250.21082480695986a16,16 0 1,0 32,0 a16,16 0 1,0 -32,0 ' + ); + await expect(paths.nth(1)).toHaveAttribute( + 'd', + 'M687.0390399999997,354.0936387274919a9,9 0 1,0 18,0 a9,9 0 1,0 -18,0 ' + ); + + await paths.nth(0).click(); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A 50km radius circle centered on Paris'); + + await paths.nth(1).click(); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A 30km radius circle centered on Lyon'); +}); + +test('Can render rectangles', async ({ page }) => { + await page.goto('/ux-map/with-rectangles?renderer=leaflet'); + await expectMapToBeVisible(page); + + const paths = page.getByTestId('map').locator('path.leaflet-interactive'); + await expect(paths, '2 rectangles must be present').toHaveCount(2); + await expect(paths.nth(0)).toHaveAttribute('d', 'M640 250L640 188L656 188L656 250z'); + await expect(paths.nth(1)).toHaveAttribute('d', 'M573 384L573 354L696 354L696 384z'); + + await paths.nth(0).click(); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A rectangle from Paris to Lille'); + + await paths.nth(1).click(); + await expectOneInfoWindowToBeOpenedAndContainText(page, 'A rectangle from Bordeaux to Lyon'); +}); diff --git a/src/Map/src/Bridge/Leaflet/assets/test/browser/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/browser/map_controller.test.ts deleted file mode 100644 index ec0fddc911f..00000000000 --- a/src/Map/src/Bridge/Leaflet/assets/test/browser/map_controller.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Application, Controller } from '@hotwired/stimulus'; -import { getByTestId, waitFor } from '@testing-library/dom'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { clearDOM, mountDOM } from '../../../../../../../../test/stimulus-helpers'; -import LeafletController from '../../src/map_controller'; - -// Controller used to check the actual controller was properly booted -class CheckController extends Controller { - connect() { - this.element.addEventListener('ux:map:pre-connect', (_event) => { - this.element.classList.add('pre-connected'); - }); - - this.element.addEventListener('ux:map:connect', (_event) => { - this.element.classList.add('connected'); - }); - } -} - -const startStimulus = () => { - const application = Application.start(); - application.register('check', CheckController); - application.register('symfony--ux-leaflet-map--map', LeafletController); -}; - -describe('LeafletController', () => { - let container: HTMLElement; - - beforeEach(() => { - container = mountDOM(` -
- - `); - }); - - afterEach(() => { - clearDOM(); - }); - - it('connect', async () => { - const div = getByTestId(container, 'map'); - expect(div).not.toHaveClass('pre-connected'); - expect(div).not.toHaveClass('connected'); - - startStimulus(); - await waitFor(() => expect(div).toHaveClass('pre-connected')); - await waitFor(() => expect(div).toHaveClass('connected')); - }); -}); diff --git a/test/playwright-helpers.ts b/test/playwright-helpers.ts new file mode 100644 index 00000000000..cb60739e631 --- /dev/null +++ b/test/playwright-helpers.ts @@ -0,0 +1,16 @@ +import * as path from 'node:path'; +import * as fs from 'fs'; + +export function getSymfonyKernelVersionId(): number { + const kernelPath = path.join(import.meta.dirname, '../test_apps/e2e-app/vendor/symfony/http-kernel/Kernel.php'); + if (!fs.existsSync(kernelPath)) { + throw new Error(`Unable to read Symfony Kernel version ID, the file "${kernelPath}" does not exist.`) + } + + const match = fs.readFileSync(kernelPath, 'utf8').match((/VERSION_ID = (\d+)/)); + if (match === null) { + throw new Error(`Unable to extract Symfony Kernel version ID.`) + } + + return Number(match[1]); +} diff --git a/test_apps/e2e-app/assets/icons/mdi/eiffel-tower.svg b/test_apps/e2e-app/assets/icons/mdi/eiffel-tower.svg new file mode 100644 index 00000000000..bc65d311bea --- /dev/null +++ b/test_apps/e2e-app/assets/icons/mdi/eiffel-tower.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_apps/e2e-app/assets/icons/mdi/glass-wine.svg b/test_apps/e2e-app/assets/icons/mdi/glass-wine.svg new file mode 100644 index 00000000000..6929cc7450d --- /dev/null +++ b/test_apps/e2e-app/assets/icons/mdi/glass-wine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_apps/e2e-app/config/packages/ux_map.yaml b/test_apps/e2e-app/config/packages/ux_map.yaml index 684ccd20305..344ee2a9b86 100644 --- a/test_apps/e2e-app/config/packages/ux_map.yaml +++ b/test_apps/e2e-app/config/packages/ux_map.yaml @@ -1,6 +1,6 @@ ux_map: # https://symfony.com/bundles/ux-map/current/index.html#available-renderers - renderer: '%env(resolve:default::UX_MAP_DSN)%' + renderer: '' # configured in Kernel.php google_maps: # define the default map id for all maps (https://developers.google.com/maps/documentation/get-map-id) - default_map_id: null + default_map_id: '%env(resolve:default::GOOGLE_MAPS_DEFAULT_MAP_ID)%' diff --git a/test_apps/e2e-app/src/Controller/MapController.php b/test_apps/e2e-app/src/Controller/MapController.php index 376f532afc2..629fbbbdf79 100644 --- a/test_apps/e2e-app/src/Controller/MapController.php +++ b/test_apps/e2e-app/src/Controller/MapController.php @@ -2,18 +2,243 @@ namespace App\Controller; +use App\MapRenderer; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Asset\PackageInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; +use Symfony\UX\Map\Circle; +use Symfony\UX\Map\Icon\Icon; +use Symfony\UX\Map\InfoWindow; +use Symfony\UX\Map\Map; +use Symfony\UX\Map\Marker; +use Symfony\UX\Map\Point; +use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; #[Route('/ux-map')] final class MapController extends AbstractController { - #[Route('/')] - public function index(): Response + #[Route('/basic')] + public function basic( + #[MapQueryParameter] MapRenderer $renderer + ): Response { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(12) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-markers-and-fit-bounds-to-markers')] + public function withMarkersAndFitBoundsToMarkers(#[MapQueryParameter] MapRenderer $renderer): Response { - return $this->render('ux_map/index.html.twig', [ - 'controller_name' => 'MapController', - ]); + $map = (new Map(rendererName: $renderer->value)) + ->fitBoundsToMarkers() + ->addMarker(new Marker( + position: new Point(48.8566, 2.3522), + title: 'Paris', + )) + ->addMarker(new Marker( + position: new Point(45.7640, 4.8357), + title: 'Lyon', + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-markers-and-zoomed-on-paris')] + public function withMarkersZoomedOnParis(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(10) + ->addMarker(new Marker( + position: new Point(48.8566, 2.3522), + title: 'Paris', + )) + ->addMarker(new Marker( + position: new Point(45.7640, 4.8357), + title: 'Lyon', + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-markers-and-info-windows')] + public function withMarkersAndInfoWindows(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->fitBoundsToMarkers() + ->addMarker(new Marker( + position: new Point(48.8566, 2.3522), + title: 'Paris', + infoWindow: new InfoWindow(content: 'Capital of France', opened: true), + )) + ->addMarker(new Marker( + position: new Point(45.7640, 4.8357), + title: 'Lyon', + infoWindow: new InfoWindow(content: 'Famous for its gastronomy'), + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-markers-and-custom-icons')] + public function withMarkersAndCustomIcons( + #[MapQueryParameter] MapRenderer $renderer, + #[Autowire(service: 'asset_mapper.asset_package')] PackageInterface $package, + ): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->fitBoundsToMarkers() + ->addMarker(new Marker( + position: new Point(48.8566, 2.3522), + title: 'Paris', + icon: Icon::ux('mdi:eiffel-tower'), + )) + ->addMarker(new Marker( + position: new Point(45.7640, 4.8357), + title: 'Lyon', + icon: Icon::svg('') + )) + ->addMarker(new Marker( + position: new Point(44.8378, -0.5792), + title: 'Bordeaux', + icon: Icon::url($package->getUrl('icons/mdi/glass-wine.svg')) + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-polygons')] + public function withPolygons(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(5) + ->addPolygon(new Polygon( + points: [ + // First path, the outer boundary of the polygon + [ + new Point(48.117266, -1.677792), // Rennes + new Point(50.629250, 3.057256), // Lille + new Point(48.573405, 7.752111), // Strasbourg + new Point(43.296482, 5.369780), // Marseille + new Point(44.837789, -0.579180), // Bordeaux + ], + // Second path, making a hole in the first path + [ + new Point(45.833619, 1.261105), // Limoges + new Point(45.764043, 4.835659), // Lyon + new Point(49.258329, 4.031696), // Reims + new Point(48.856613, 2.352222), // Paris + ], + ], + infoWindow: new InfoWindow(content: 'A weird shape on the France'), + )) + + ->addPolygon(new Polygon( + points: [ + new Point(41.902782, 12.496366), // Rome + new Point(45.464211, 9.191383), // Milan + new Point(44.494887, 11.342616), // Bologna + new Point(40.851798, 14.268124), // Naples + ], + infoWindow: new InfoWindow(content: 'A polygon covering some of the main cities in Italy'), + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-polylines')] + public function withPolylines(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(5) + ->addPolyline(new Polyline( + points: [ + new Point(48.8566, 2.3522), // Paris + new Point(45.7640, 4.8357), // Lyon + new Point(43.2965, 5.3698), // Marseille + new Point(44.8378, -0.5792), // Bordeaux + ], + infoWindow: new InfoWindow( + content: 'A line passing through Paris, Lyon, Marseille, Bordeaux', + ), + )) + ->addPolyline(new Polyline( + points: [ + new Point(48.1173, -1.6778), // Rennes + new Point(47.2184, -1.5536), // Nantes + new Point(47.3493, 0.7484), // Tours + ], + infoWindow: new InfoWindow( + content: 'A line passing through Rennes, Nantes and Tours' + ) + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-circles')] + public function withCircles(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(5) + ->addCircle(new Circle( + center: new Point(48.8566, 2.3522), + radius: 50_000, + infoWindow: new InfoWindow( + content: 'A 50km radius circle centered on Paris', + ), + )) + ->addCircle(new Circle( + center: new Point(45.7640, 4.8357), + radius: 30_000, + infoWindow: new InfoWindow( + content: 'A 30km radius circle centered on Lyon', + ), + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); + } + + #[Route('/with-rectangles')] + public function withRectangles(#[MapQueryParameter] MapRenderer $renderer): Response + { + $map = (new Map(rendererName: $renderer->value)) + ->center(new Point(48.8566, 2.3522)) + ->zoom(5) + ->addRectangle(new Rectangle( + southWest: new Point(48.8566, 2.3522), // Paris + northEast: new Point(50.6292, 3.0573), // Lille + infoWindow: new InfoWindow( + content: 'A rectangle from Paris to Lille', + ), + )) + ->addRectangle(new Rectangle( + southWest: new Point(44.8378, -0.5792), // Bordeaux + northEast: new Point(45.7640, 4.8357), // Lyon + infoWindow: new InfoWindow( + content: 'A rectangle from Bordeaux to Lyon', + ), + )) + ; + + return $this->render('ux_map/render_map.html.twig', ['map' => $map]); } } diff --git a/test_apps/e2e-app/src/Kernel.php b/test_apps/e2e-app/src/Kernel.php index 10870731492..bf987177dcb 100644 --- a/test_apps/e2e-app/src/Kernel.php +++ b/test_apps/e2e-app/src/Kernel.php @@ -3,11 +3,28 @@ namespace App; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -class Kernel extends BaseKernel +class Kernel extends BaseKernel implements CompilerPassInterface { use MicroKernelTrait; + + public function process(ContainerBuilder $container): void + { + // TODO: Add support for multiple UX Map renderers + $mapRenderers = $container->getDefinition('ux_map.renderers'); + $mapRenderers->setArgument(0, [ + MapRenderer::Leaflet->value => 'leaflet://default', + MapRenderer::Google->value => 'google://not-an-api-key@default', + + // Since using Google Maps cost money, you need to use your own Google Maps API settings: + // 1. Create your own API on https://developers.google.com/maps/documentation/javascript/get-api-key, scope it to `127.0.0.1:9876/*` + // 2. Create a default map ID on https://developers.google.com/maps/documentation/javascript/map-ids/get-map-id + // 3. Update the `.env.local` file and define env vars `GOOGLE_MAPS_API_KEY` and `GOOGLE_MAPS_DEFAULT_MAP_ID` + // 4. Uncomment the line below + //MapRenderer::Google->value => 'google://'.$container->resolveEnvPlaceholders('%env(GOOGLE_MAPS_API_KEY)%').'@default', + ]); + } } diff --git a/test_apps/e2e-app/src/MapRenderer.php b/test_apps/e2e-app/src/MapRenderer.php new file mode 100644 index 00000000000..c0e66e79636 --- /dev/null +++ b/test_apps/e2e-app/src/MapRenderer.php @@ -0,0 +1,9 @@ +