Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Map/src/Bridge/Google/assets/test/browser/map.test.ts
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not do this test.

i think this could be a (tiny but still very real) problem in term of privacy, as we are making requests on behalf of the user... (even if it's on its own local machine)..

If someone want Google Maps fully tested, they probably already pay for GoogleMaps .....
so maybe they can invest some coins around here to support the work....

In the meantime I clearly would not spend too much time or energy to test a (very) costly (and "not-always" privacy-compliant) solution your our own time and/or money 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I see what you mean, but since #2146 Map Bridges tests were run inside a browser and so requests were already made to Google.

I think that's fine if we make it opt-out explicitly:

  • pnpm run test:browser: run browsers tests on all UX packages with test:browser pnpm script except UX Google Map
  • pnpm run test:browser:no-privacy (or something similar): run browsers tests on all UX packages with test:browser pnpm script including UX Google Map

If someone want Google Maps fully tested, they probably already pay for GoogleMaps .....
so maybe they can invest some coins around here to support the work....

About writing real tests for Google Map Bridge too, I thought about putting my own API key as a GitHub secret (so no one can see it), but if someone decide to trigger 10k times the CI, I'm bankrupt 🤯

Running a test that ensure the Google Maps API is loaded is the best of both worlds imo

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."
);
});

This file was deleted.

178 changes: 178 additions & 0 deletions src/Map/src/Bridge/Leaflet/assets/test/browser/map.test.ts
Original file line number Diff line number Diff line change
@@ -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(
'<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" d="M8.21 17c.44-.85.85-1.84 1.23-3H9v-2h1c.61-2.6 1-5.87 1-10h2c0 4.13.4 7.4 1 10h1v2h-.44c.38 1.16.79 2.15 1.23 3H17v2l2 3h-2.42c-.77-1.76-2.53-3-4.58-3s-3.81 1.24-4.58 3H5l2-3l-.03-2zm4.38-3h-1.18a22 22 0 0 1-1.13 3h3.44c-.4-.87-.79-1.87-1.13-3"></path></svg>'
);

await expect(markers.nth(1)).toHaveAttribute('title', 'Lyon');
expect(await markers.nth(1).innerHTML()).toEqual(
'<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4"></path></svg>'
);

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');
});

This file was deleted.

16 changes: 16 additions & 0 deletions test/playwright-helpers.ts
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would use an ENV_VAR here (as its done for KERNEL_CLASS in a way)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it too but re-using SYMFONY_REQUIRE env var would makes things counterproductive IMHO:

  • it's not very intuitive to pass this env var while running Playwright, I'm sure everyone including me will forgot it
  • if you change your Symfony version through SYMFONY_REQUIRE=x.y.* composer update, you must close Playwright and re-open Playwright with SYMFONY_REQUIRE=x.y.*
  • there is no native version_compare() function in Node.js, we must re-implement the logic ourselves with https://github.com/npm/node-semver for example

The VERSION_ID from test_apps/e2e-app/vendor/symfony/http-kernel/Kernel.php is the best source of truth we can get when being inside Playwright context

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]);
}
1 change: 1 addition & 0 deletions test_apps/e2e-app/assets/icons/mdi/eiffel-tower.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test_apps/e2e-app/assets/icons/mdi/glass-wine.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions test_apps/e2e-app/config/packages/ux_map.yaml
Original file line number Diff line number Diff line change
@@ -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)%'
3 changes: 3 additions & 0 deletions test_apps/e2e-app/importmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
'react-dom' => [
'version' => '18.3.1',
],
'react-dom/client' => [
'version' => '18.3.1',
],
Comment on lines +88 to +90
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #3030

'scheduler' => [
'version' => '0.23.2',
],
Expand Down
Loading
Loading