Skip to content

Commit 98fc68a

Browse files
authored
fix(map) :: ignore map coordinates that are not a pair of numbers (#1368)
1 parent e44f993 commit 98fc68a

3 files changed

Lines changed: 128 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Added a `toast` component with plain-text or Markdown content, icons, colors, six screen placements, configurable auto-dismiss timing, optional manual dismissal, URL-fragment triggers, and automatic stacking of queued notifications.
77
- `sqlpage.send_mail` now supports rich email bodies. Use `body_html` for a caller-provided HTML alternative, or `body_md` to render Markdown as HTML. Messages retain a plain-text alternative; `body` may be omitted when `body_md` is used, and `body_md` and `body_html` cannot be combined.
88
- Form `options_source` URLs now preserve existing query parameters when adding the dynamic `search` parameter.
9+
- Map coordinates that are not a pair of numbers, like a latitude with no longitude, are now reported in the browser console and skipped, instead of breaking the whole map.
910
- Screen readers now announce the title of the modal component instead of an unnamed dialog.
1011

1112
## v0.45

‎sqlpage/sqlpage.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,19 @@ function sqlpage_map() {
182182
onLeafletLoad();
183183
}
184184
/**
185-
*
186185
* @param {string|undefined} coords
187186
* @returns {[number, number] | undefined}
188187
*/
189188
function parseCoords(coords) {
190-
return coords?.split(",", 2).map((c) => Number.parseFloat(c));
189+
if (!coords) return undefined;
190+
const parsed = coords.split(",", 2).map((c) => Number.parseFloat(c));
191+
if (parsed.length !== 2 || !parsed.every(Number.isFinite)) {
192+
console.error(
193+
`Invalid map coordinates: ${JSON.stringify(coords)}. Expected a "latitude,longitude" pair of numbers.`,
194+
);
195+
return undefined;
196+
}
197+
return [parsed[0], parsed[1]];
191198
}
192199
function onLeafletLoad() {
193200
is_leaflet_loaded = true;
@@ -230,6 +237,7 @@ function sqlpage_map() {
230237
const marker = dataset.coords
231238
? createMarker(marker_elem, options)
232239
: createGeoJSONMarker(marker_elem, options);
240+
if (!marker) return;
233241
marker.addTo(map);
234242
map._sqlpage_markers.push(marker);
235243
if (marker_elem.textContent.trim()) marker.bindPopup(marker_elem);
@@ -241,6 +249,7 @@ function sqlpage_map() {
241249
}
242250
function createMarker(marker_elem, options) {
243251
const coords = parseCoords(marker_elem.dataset.coords);
252+
if (!coords) return undefined;
244253
const icon_obj = marker_elem.getElementsByClassName("mapicon")[0];
245254
if (icon_obj) {
246255
const size =
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { expect, type Page, test } from "@playwright/test";
2+
3+
const BASE = process.env.SQLPAGE_TEST_BASE ?? "http://localhost:8080/";
4+
5+
declare global {
6+
function sqlpage_map(): void;
7+
}
8+
9+
type Marker = { coords?: string; title: string };
10+
11+
const PARIS = "48.85,2.35";
12+
const PARIS_WITHOUT_ITS_LONGITUDE = "48.85,";
13+
const NOT_COORDINATES = "somewhere nice";
14+
15+
async function renderMap(
16+
page: Page,
17+
center: string | null,
18+
markers: Marker[] = [],
19+
) {
20+
return page.evaluate(
21+
async ({ center, markers }) => {
22+
document.getElementById("test-map")?.remove();
23+
const container = document.createElement("div");
24+
container.id = "test-map";
25+
container.className = "leaflet";
26+
container.style.height = "200px";
27+
container.dataset.zoom = "5";
28+
container.dataset.max_zoom = "18";
29+
if (center !== null) container.dataset.center = center;
30+
container.innerHTML = markers
31+
.map(
32+
(m) =>
33+
`<div class="marker"${m.coords === undefined ? "" : ` data-coords="${m.coords}"`}><h3>${m.title}</h3></div>`,
34+
)
35+
.join("");
36+
container.dataset.preInit = "map";
37+
document.body.appendChild(container);
38+
39+
const errors: string[] = [];
40+
const record = (e: ErrorEvent) => errors.push(e.message);
41+
window.addEventListener("error", record);
42+
43+
const logged: string[] = [];
44+
const console_error = console.error;
45+
console.error = (...args) => logged.push(args.join(" "));
46+
47+
sqlpage_map();
48+
await new Promise((resolve) => setTimeout(resolve, 500));
49+
50+
console.error = console_error;
51+
window.removeEventListener("error", record);
52+
53+
return {
54+
errors,
55+
logged,
56+
markers: container.querySelectorAll(".leaflet-marker-icon").length,
57+
initialized: !!container.querySelector(".leaflet-map-pane"),
58+
};
59+
},
60+
{ center, markers },
61+
);
62+
}
63+
64+
test.beforeEach(async ({ page }) => {
65+
await page.goto(`${BASE}/documentation.sql?component=map#component`);
66+
await page.waitForFunction(() => "L" in window);
67+
});
68+
69+
test("centers the map on a pair of coordinates", async ({ page }) => {
70+
const map = await renderMap(page, PARIS);
71+
72+
expect(map.errors).toEqual([]);
73+
expect(map.logged).toEqual([]);
74+
expect(map.initialized).toBe(true);
75+
});
76+
77+
test("reports a center whose longitude is missing", async ({ page }) => {
78+
const map = await renderMap(page, PARIS_WITHOUT_ITS_LONGITUDE);
79+
80+
expect(map.errors).toEqual([]);
81+
expect(map.logged).toEqual([
82+
expect.stringContaining(PARIS_WITHOUT_ITS_LONGITUDE),
83+
]);
84+
expect(map.initialized).toBe(true);
85+
});
86+
87+
test("reports a center that is not a pair of numbers", async ({ page }) => {
88+
const map = await renderMap(page, NOT_COORDINATES);
89+
90+
expect(map.errors).toEqual([]);
91+
expect(map.logged).toEqual([expect.stringContaining(NOT_COORDINATES)]);
92+
expect(map.initialized).toBe(true);
93+
});
94+
95+
test("draws a marker at a pair of coordinates", async ({ page }) => {
96+
const map = await renderMap(page, PARIS, [{ coords: PARIS, title: "Paris" }]);
97+
98+
expect(map.errors).toEqual([]);
99+
expect(map.logged).toEqual([]);
100+
expect(map.markers).toBe(1);
101+
});
102+
103+
test("reports a marker whose longitude is missing, keeping the others", async ({
104+
page,
105+
}) => {
106+
const map = await renderMap(page, PARIS, [
107+
{ coords: PARIS_WITHOUT_ITS_LONGITUDE, title: "Half of Paris" },
108+
{ coords: PARIS, title: "Paris" },
109+
]);
110+
111+
expect(map.errors).toEqual([]);
112+
expect(map.logged).toEqual([
113+
expect.stringContaining(PARIS_WITHOUT_ITS_LONGITUDE),
114+
]);
115+
expect(map.markers).toBe(1);
116+
});

0 commit comments

Comments
 (0)