From 3a006ea45d92a835c01a9d874c51b62fa481e003 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Sun, 3 Nov 2024 21:47:34 -0500 Subject: [PATCH] Implementation of HTMLMapmlViewerElement.matchMedia API, depends on media-query-parser and media-query-solver. Use multi-select values of contentPreference sent by mapml-extension (relies on M.options.contentPreference being an array). Update map-zoom handling to compare current map zoom against query Add window.matchMedia query + event listener for color-scheme changes, allows map to adapt without having to shake it. Change the way prefers-lang selects the language (use navigator.language, not navigator.languages, because other values aren't of use). Make matchMedia(query).matches a boolean value Added tests for map-zoom and prefers-color-scheme Added tests for bounding box and map-projection Added test for combined matchMedia API Add matchMedia to web-map.js Fix up some other stuff that had fallen through the cracks of keeping mapml-viewer and web-map in sync Create elementSupport/viewers/matchMedia.js, de-duplicates code Import matchMedia from matchMedia.js into both mapml-viewer and web-map Add MIT license for media-query-parser and -solver Update version to 0.15.0 Added web-map test for combined-matchMedia Add ${mapType} to title of each test so logs are explicit Use map.getByTestId, on id generated to be unique within parent map --- LICENSE.md | 19 ++ package-lock.json | 28 ++ package.json | 5 +- src/mapml-viewer.js | 14 +- .../elementSupport/viewers/matchMedia.js | 251 ++++++++++++++++++ src/web-map.js | 7 +- .../api/matchMedia/combined-matchMedia.html | 88 ++++++ .../matchMedia/combined-matchMedia.test.js | 98 +++++++ test/e2e/api/matchMedia/dummy-osmtile.mapml | 16 ++ test/e2e/api/matchMedia/map-bounding-box.html | 66 +++++ .../api/matchMedia/map-bounding-box.test.js | 54 ++++ test/e2e/api/matchMedia/map-projection.html | 78 ++++++ .../e2e/api/matchMedia/map-projection.test.js | 35 +++ test/e2e/api/matchMedia/map-zoom.html | 62 +++++ test/e2e/api/matchMedia/map-zoom.test.js | 32 +++ .../api/matchMedia/prefers-color-scheme.html | 45 ++++ .../matchMedia/prefers-color-scheme.test.js | 32 +++ test/e2e/api/matchMedia/prefers-lang.html | 88 ++++++ test/e2e/api/matchMedia/prefers-lang.test.js | 27 ++ test/server.js | 1 + 20 files changed, 1039 insertions(+), 7 deletions(-) create mode 100644 src/mapml/elementSupport/viewers/matchMedia.js create mode 100644 test/e2e/api/matchMedia/combined-matchMedia.html create mode 100644 test/e2e/api/matchMedia/combined-matchMedia.test.js create mode 100644 test/e2e/api/matchMedia/dummy-osmtile.mapml create mode 100644 test/e2e/api/matchMedia/map-bounding-box.html create mode 100644 test/e2e/api/matchMedia/map-bounding-box.test.js create mode 100644 test/e2e/api/matchMedia/map-projection.html create mode 100644 test/e2e/api/matchMedia/map-projection.test.js create mode 100644 test/e2e/api/matchMedia/map-zoom.html create mode 100644 test/e2e/api/matchMedia/map-zoom.test.js create mode 100644 test/e2e/api/matchMedia/prefers-color-scheme.html create mode 100644 test/e2e/api/matchMedia/prefers-color-scheme.test.js create mode 100644 test/e2e/api/matchMedia/prefers-lang.html create mode 100644 test/e2e/api/matchMedia/prefers-lang.test.js diff --git a/LICENSE.md b/LICENSE.md index 189a815ed..68642651c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -184,3 +184,22 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +MIT License + +Copyright (c) 2023 Tom Golden + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package-lock.json b/package-lock.json index 9dcae99f4..3da544812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,8 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.81.1", "mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension", + "media-query-parser": "^3.0.2", + "media-query-solver": "^0.1.3", "path": "^0.12.7", "playwright": "^1.39.0", "proj4": "^2.6.2", @@ -2006,6 +2008,32 @@ "node": ">=8" } }, + "node_modules/media-query-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-3.0.2.tgz", + "integrity": "sha512-3WLXSFQZVuo1d9xpt72Ul0khXy72qgtz1KqDBO6KbmyvMsBsQRcePhYYR2U5QBSOz/u5pb5zj2s2FmgJe8jaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/media-query-solver": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/media-query-solver/-/media-query-solver-0.1.3.tgz", + "integrity": "sha512-OkL4tpvexcAOG09rNdbAO1MebJzRa2s0RK7hTO9gVAL1jefbm6rLG9jcRYbwO1M3N8UGhHymMtOXK2ciDy3r8w==", + "dev": true, + "license": "MIT", + "bin": { + "media-query-solver": "dist/cjs/cli.js" + }, + "engines": { + "node": ">=16.0.0 || ^14.13.1" + }, + "peerDependencies": { + "media-query-parser": "^3.0.2" + } + }, "node_modules/media-typer": { "version": "0.3.0", "dev": true, diff --git a/package.json b/package.json index 6c10babc8..0bb41f89b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maps4html/mapml", - "version": "0.14.1", + "version": "0.15.0", "description": "Custom element suite", "keywords": [ "mapml-viewer", @@ -56,6 +56,9 @@ "grunt-prettier": "^2.2.0", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.81.1", + "mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension", + "media-query-parser": "^3.0.2", + "media-query-solver": "^0.1.3", "path": "^0.12.7", "playwright": "^1.39.0", "proj4": "^2.6.2", diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 5aab491dc..b22cbfe51 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -9,7 +9,7 @@ import { import Proj from 'proj4leaflet/src/proj4leaflet.js'; import { Util } from './mapml/utils/Util.js'; import { DOMTokenList } from './mapml/utils/DOMTokenList.js'; - +import { matchMedia } from './mapml/elementSupport/viewers/matchMedia.js'; import { HTMLLayerElement } from './map-layer.js'; import { LayerDashElement } from './layer-.js'; import { HTMLMapCaptionElement } from './map-caption.js'; @@ -240,7 +240,8 @@ export class HTMLMapmlViewerElement extends HTMLElement { } }) .catch((e) => { - throw new Error('Projection not defined: ' + e); + console.log(e); + throw new Error('Error: ' + e); }); } _setLocale() { @@ -411,7 +412,7 @@ export class HTMLMapmlViewerElement extends HTMLElement { this._map.options.projection = newValue; let layersReady = []; this._map.announceMovement.disable(); - for (let layer of this.querySelectorAll('map-layer')) { + for (let layer of this.querySelectorAll('map-layer,layer-')) { layer.removeAttribute('disabled'); let reAttach = this.removeChild(layer); this.appendChild(reAttach); @@ -986,7 +987,6 @@ export class HTMLMapmlViewerElement extends HTMLElement { } }); } - locate(options) { //options: https://leafletjs.com/reference.html#locate-options if (this._geolocationButton) { @@ -1468,6 +1468,12 @@ export class HTMLMapmlViewerElement extends HTMLElement { return geojsonLayer; } } + +// ensure that 'this' always refers the the map on which the function runs +HTMLMapmlViewerElement.prototype.matchMedia = function (...args) { + return matchMedia.apply(this, args); +}; + window.customElements.define('mapml-viewer', HTMLMapmlViewerElement); try { window.customElements.define('web-map', HTMLWebMapElement, { diff --git a/src/mapml/elementSupport/viewers/matchMedia.js b/src/mapml/elementSupport/viewers/matchMedia.js new file mode 100644 index 000000000..08239fa10 --- /dev/null +++ b/src/mapml/elementSupport/viewers/matchMedia.js @@ -0,0 +1,251 @@ +import { parseMediaQueryList } from 'media-query-parser'; +import { solveMediaQueryList } from 'media-query-solver'; + +export const matchMedia = function (query) { + // useful features for maps: prefers-color-scheme, prefers-lang, projection, zoom, extent + const parsedQuery = parseMediaQueryList(query); + + // less obviously useful: aspect-ratio, orientation, (device) resolution, overflow-block, overflow-inline + + const map = this; + const features = { + 'prefers-lang': { + type: 'discrete', + get values() { + return [navigator.language.substring(0, 2)]; + } + }, + 'map-projection': { + type: 'discrete', + get values() { + return [map.projection.toLowerCase()]; + } + }, + 'map-zoom': { + type: 'range', + valueType: 'integer', + canBeNegative: false, + canBeZero: true, + get extraValues() { + return { + min: 0, + max: map.zoom + }; + } + }, + 'map-top-left-easting': { + type: 'range', + valueType: 'integer', + canBeNegative: true, + canBeZero: true, + get values() { + return [Math.trunc(map.extent.topLeft.pcrs.horizontal)]; + } + }, + 'map-top-left-northing': { + type: 'range', + valueType: 'integer', + canBeNegative: true, + canBeZero: true, + get values() { + return [Math.trunc(map.extent.topLeft.pcrs.vertical)]; + } + }, + 'map-bottom-right-easting': { + type: 'range', + valueType: 'integer', + canBeNegative: true, + canBeZero: true, + get values() { + return [Math.trunc(map.extent.bottomRight.pcrs.horizontal)]; + } + }, + 'map-bottom-right-northing': { + type: 'range', + valueType: 'integer', + canBeNegative: true, + canBeZero: true, + get values() { + return [Math.trunc(map.extent.bottomRight.pcrs.vertical)]; + } + }, + 'prefers-color-scheme': { + type: 'discrete', + get values() { + return [ + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + ]; + } + }, + 'prefers-map-content': { + type: 'discrete', + get values() { + return M.options.contentPreference; + } + } + }; + + const solveUnknownFeature = (featureNode) => { + let feature = featureNode.feature; + let queryValue = featureNode.value.value; + + if (feature === 'prefers-lang') { + return features['prefers-lang'].values.includes(queryValue).toString(); + } else if ( + feature === 'map-zoom' || + feature === 'map-top-left-easting' || + feature === 'map-top-left-northing' || + feature === 'map-bottom-right-easting' || + feature === 'map-bottom-right-northing' + ) { + return solveRangeFeature(featureNode); + } else if (feature === 'map-projection') { + return features['map-projection'].values + .some((p) => p === queryValue) + .toString(); + } else if (feature === 'prefers-color-scheme') { + return features['prefers-color-scheme'].values + .some((s) => s === queryValue) + .toString(); + } else if (feature === 'prefers-map-content') { + return features[feature].values + .some((pref) => pref === queryValue) + .toString(); + } + return 'false'; + }; + let matches = + solveMediaQueryList(parsedQuery, { + features, + solveUnknownFeature + }) === 'true' + ? true + : false; + + function solveRangeFeature(featureNode) { + const { context, feature, value, op } = featureNode; + + if (!feature.startsWith('map-')) { + return 'unknown'; + } + + const currentValue = getMapFeatureValue(feature); + + if (currentValue === undefined) { + return 'unknown'; + } + + if (context === 'value') { + // Plain case: : + // Example: (map-zoom: 15) + return currentValue === value.value ? 'true' : 'false'; + } + + if (context === 'range') { + // Range case: + // Example: (0 <= map-zoom < 15) + switch (op) { + case '<': + return currentValue < value.value ? 'true' : 'false'; + case '<=': + return currentValue <= value.value ? 'true' : 'false'; + case '>': + return currentValue > value.value ? 'true' : 'false'; + case '>=': + return currentValue >= value.value ? 'true' : 'false'; + case '=': + return currentValue === value.value ? 'true' : 'false'; + default: + return 'unknown'; + } + } + + return 'unknown'; // If the context is neither "value" nor "range" + } + + function getMapFeatureValue(feature) { + switch (feature) { + case 'map-zoom': + return map.zoom; + case 'map-top-left-easting': + return Math.trunc(map.extent.topLeft.pcrs.horizontal); + case 'map-top-left-northing': + return Math.trunc(map.extent.topLeft.pcrs.vertical); + case 'map-bottom-right-easting': + return Math.trunc(map.extent.bottomRight.pcrs.horizontal); + case 'map-bottom-right-northing': + return Math.trunc(map.extent.bottomRight.pcrs.vertical); + default: + return undefined; // Unsupported or unknown feature + } + } + + // Make mediaQueryList an EventTarget for dispatching events + const mediaQueryList = Object.assign(new EventTarget(), { + matches, + media: query, + listeners: [], + // this is a client facing api + addEventListener(event, listener) { + if (event === 'change') { + this.listeners.push(listener); + + // Start observing properties only if there is at least one listener + if (this.listeners.length !== 0) { + observeProperties(); + } + EventTarget.prototype.addEventListener.call(this, event, listener); + } + }, + + // this is a client facing api + removeEventListener(event, listener) { + if (event === 'change') { + this.listeners = this.listeners.filter((l) => l !== listener); + + // Stop observing if there are no more listeners + if (this.listeners.length === 0) { + stopObserving(); + } + EventTarget.prototype.removeEventListener.call(this, event, listener); + } + } + }); + + const observeProperties = () => { + const notifyIfChanged = () => { + const newMatches = + solveMediaQueryList(parsedQuery, { + features, + solveUnknownFeature + }) === 'true' + ? true + : false; + if (newMatches !== mediaQueryList.matches) { + mediaQueryList.matches = newMatches; + + // Dispatch a "change" event to notify listeners of the update + mediaQueryList.dispatchEvent(new Event('change')); + } + }; + notifyIfChanged.bind(this); + // Subscribe to internal events for changes in projection, zoom, and extent + this.addEventListener('map-projectionchange', notifyIfChanged); + this.addEventListener('map-moveend', notifyIfChanged); + const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + colorSchemeQuery.addEventListener('change', notifyIfChanged); + + // Stop observing function + stopObserving = () => { + this.removeEventListener('map-projectionchange', notifyIfChanged); + this.removeEventListener('map-moveend', notifyIfChanged); + colorSchemeQuery.removeEventListener('change', notifyIfChanged); + }; + }; + + let stopObserving; // Declare here so it can be assigned within observeProperties + + return mediaQueryList; +}; diff --git a/src/web-map.js b/src/web-map.js index ce4e1e40e..3130cf609 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -10,7 +10,7 @@ import { import Proj from 'proj4leaflet/src/proj4leaflet.js'; import { Util } from './mapml/utils/Util.js'; import { DOMTokenList } from './mapml/utils/DOMTokenList.js'; - +import { matchMedia } from './mapml/elementSupport/viewers/matchMedia.js'; import { layerControl } from './mapml/control/LayerControl.js'; import { AttributionButton } from './mapml/control/AttributionButton.js'; import { reloadButton } from './mapml/control/ReloadButton.js'; @@ -1031,7 +1031,6 @@ export class HTMLWebMapElement extends HTMLMapElement { } }); } - locate(options) { //options: https://leafletjs.com/reference.html#locate-options if (this._geolocationButton) { @@ -1534,3 +1533,7 @@ export class HTMLWebMapElement extends HTMLMapElement { } } } +// ensure that 'this' always refers the the map on which the function runs +HTMLWebMapElement.prototype.matchMedia = function (...args) { + return matchMedia.apply(this, args); +}; diff --git a/test/e2e/api/matchMedia/combined-matchMedia.html b/test/e2e/api/matchMedia/combined-matchMedia.html new file mode 100644 index 000000000..9049f7472 --- /dev/null +++ b/test/e2e/api/matchMedia/combined-matchMedia.html @@ -0,0 +1,88 @@ + + + + + + + + Combined matchMedia API Test + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/combined-matchMedia.test.js b/test/e2e/api/matchMedia/combined-matchMedia.test.js new file mode 100644 index 000000000..a2e5c07e3 --- /dev/null +++ b/test/e2e/api/matchMedia/combined-matchMedia.test.js @@ -0,0 +1,98 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('MatchMedia Query Tests', () => { + let page; + let context; + + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', { + locale: 'en-US' + }); + page = await context.newPage(); + await page.goto('combined-matchMedia.html'); + }); + + test.afterAll(async () => { + await context.close(); + }); + + const mapTypes = ['mapml-viewer', 'map']; + + for (const mapType of mapTypes) { + test.describe(`Tests for ${mapType}`, () => { + test(`${mapType} - ${mapType} - All conditions are met`, async () => { + const map = page.locator(mapType); + const layer = map.getByTestId(`test-media-query-${mapType}`); + await expect(layer).not.toHaveAttribute('hidden'); + }); + + test(`${mapType} - Prefers-color-scheme does not match`, async () => { + const map = page.locator(mapType); + await page.emulateMedia({ colorScheme: 'dark' }); + const layer = map.getByTestId(`test-media-query-${mapType}`); + await expect(layer).toHaveAttribute('hidden'); + await page.emulateMedia({ colorScheme: 'light' }); + }); + + test(`${mapType} - Prefers-lang does not match`, async () => { + const browser = await chromium.launch(); + const frContext = await browser.newContext({ locale: 'fr-CA' }); + const frPage = await frContext.newPage(); + + await frPage.goto('combined-matchMedia.html'); + const map = frPage.locator(mapType); + const layer = map.getByTestId(`test-media-query-${mapType}`); + await expect(layer).toHaveAttribute('hidden'); + + await frContext.close(); + }); + + test(`${mapType} - map-projection does not match`, async () => { + const map = page.locator(mapType); + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.projection = 'CBMTILE'; + }, mapType); + const layer = map.getByTestId(`test-media-query-${mapType}`); + await expect(layer).toHaveAttribute('hidden'); + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.projection = 'OSMTILE'; + }, mapType); + }); + + test(`${mapType} - map-zoom does not match`, async () => { + const map = page.locator(mapType); + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.zoomTo(45.406314, -75.6883335, 15); + }, mapType); + const layer = map.getByTestId(`test-media-query-${mapType}`); + await expect(layer).toHaveAttribute('hidden'); + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.zoomTo(45.406314, -75.6883335, 13); + }, mapType); + }); + + test(`${mapType} - Map does not overlap with the bounding box`, async () => { + const map = page.locator(mapType); + const layer = map.getByTestId(`test-media-query-${mapType}`); + + // move the map so that the layer is out of the map's extent + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.zoomTo(0, 0, 13); + }, mapType); + await expect(layer).toHaveAttribute('hidden'); + + // move the map back so that the layer is within the map's extent + await page.evaluate((mapType) => { + const map = document.querySelector(mapType); + map.zoomTo(45.406314, -75.6883335, 13); + }, mapType); + await expect(layer).not.toHaveAttribute('hidden'); + }); + }); + } +}); diff --git a/test/e2e/api/matchMedia/dummy-osmtile.mapml b/test/e2e/api/matchMedia/dummy-osmtile.mapml new file mode 100644 index 000000000..7cad04660 --- /dev/null +++ b/test/e2e/api/matchMedia/dummy-osmtile.mapml @@ -0,0 +1,16 @@ + + + Canada Base Map - Transportation (OSM) + + + + + + + + + + + + + diff --git a/test/e2e/api/matchMedia/map-bounding-box.html b/test/e2e/api/matchMedia/map-bounding-box.html new file mode 100644 index 000000000..efe0a9bca --- /dev/null +++ b/test/e2e/api/matchMedia/map-bounding-box.html @@ -0,0 +1,66 @@ + + + + + + + + Map Bounding Box + + + + + + + + + + + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/map-bounding-box.test.js b/test/e2e/api/matchMedia/map-bounding-box.test.js new file mode 100644 index 000000000..81876bbc6 --- /dev/null +++ b/test/e2e/api/matchMedia/map-bounding-box.test.js @@ -0,0 +1,54 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('matchMedia map-bounding-box tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', { headless: false }); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + page = await context.newPage(); + await page.goto('map-bounding-box.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('matchMedia API detects changes in map-extents', async () => { + const map = page.locator('mapml-viewer'); + const zoomIn = page.locator('.leaflet-control-zoom-in'); + const zoomOut = page.locator('.leaflet-control-zoom-out'); + + const layer = page.locator('map-layer[label="test media query"]'); + + // upon creation, the test query layer should not be hidden + await expect(layer).not.toHaveAttribute('hidden'); + + // the test query layer should be hidden as map-zoom is no longer less than 14 + await zoomOut.click(); + await page.waitForTimeout(250); + await zoomIn.click(); + await expect(layer).toHaveAttribute('hidden'); + + // the layer should be shown as we zoom out again + await zoomOut.click(); + await page.waitForTimeout(250); + await expect(layer).not.toHaveAttribute('hidden'); + + // move the map so that the layer is out of the map's extent + await page.evaluate(() => { + const map = document.querySelector('mapml-viewer'); + map.zoomTo(0, 0, 13); + }); + await expect(layer).toHaveAttribute('hidden'); + + // move the map back so that the layer is within of the map's extent + await page.evaluate(() => { + const map = document.querySelector('mapml-viewer'); + map.zoomTo(45.406314, -75.6883335, 13); + }); + await expect(layer).not.toHaveAttribute('hidden'); + }); +}); diff --git a/test/e2e/api/matchMedia/map-projection.html b/test/e2e/api/matchMedia/map-projection.html new file mode 100644 index 000000000..d26371e1f --- /dev/null +++ b/test/e2e/api/matchMedia/map-projection.html @@ -0,0 +1,78 @@ + + + + + + + + Map Projection Toggle + + + + + + + + + + + + + + +
+

Descriptions

+ + +
+ + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/map-projection.test.js b/test/e2e/api/matchMedia/map-projection.test.js new file mode 100644 index 000000000..61c93a27c --- /dev/null +++ b/test/e2e/api/matchMedia/map-projection.test.js @@ -0,0 +1,35 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('matchMedia map-projection tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', {}); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + page = await context.newPage(); + await page.goto('map-projection.html'); + await page.waitForTimeout(500); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('matchMedia API shows maps based on projections selected by user', async () => { + const map = page.locator('mapml-viewer'); + const osmLayer = page.locator('#OSMTILE'); + const cbmtLayer = page.locator('#CBMTILE'); + const switchOSM = page.locator('.switchOSM'); + const switchCBMT = page.locator('.switchCBMT'); + + await switchCBMT.click(); + await page.waitForTimeout(500); + await expect(osmLayer).toHaveAttribute('hidden'); + + await switchOSM.click(); + await page.waitForTimeout(500); + await expect(cbmtLayer).toHaveAttribute('hidden'); + }); +}); diff --git a/test/e2e/api/matchMedia/map-zoom.html b/test/e2e/api/matchMedia/map-zoom.html new file mode 100644 index 000000000..4da89ee45 --- /dev/null +++ b/test/e2e/api/matchMedia/map-zoom.html @@ -0,0 +1,62 @@ + + + + + + + + Map Zoom + + + + + + + + + + + + + + + + + + + +
Zoom Level: 1
+ + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/map-zoom.test.js b/test/e2e/api/matchMedia/map-zoom.test.js new file mode 100644 index 000000000..b99a1918a --- /dev/null +++ b/test/e2e/api/matchMedia/map-zoom.test.js @@ -0,0 +1,32 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('matchMedia map-zoom tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', {}); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + page = await context.newPage(); + await page.goto('map-zoom.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('matchMedia API detects changes in zoom', async () => { + const zoomIn = await page.locator('.leaflet-control-zoom-in'); + const zoomIndicator = await page.waitForSelector('#zoom-indicator'); + + for (let n = 1; n < 18; n++) { + const updatedZoomLevel = await zoomIndicator.getAttribute( + 'data-zoom-level' + ); + expect(updatedZoomLevel).toBe(String(n)); + await zoomIn.click(); + await page.waitForTimeout(500); + } + }); +}); diff --git a/test/e2e/api/matchMedia/prefers-color-scheme.html b/test/e2e/api/matchMedia/prefers-color-scheme.html new file mode 100644 index 000000000..96dfba686 --- /dev/null +++ b/test/e2e/api/matchMedia/prefers-color-scheme.html @@ -0,0 +1,45 @@ + + + + + + + + matchMedia API + + + + + + + + + + +

+ + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/prefers-color-scheme.test.js b/test/e2e/api/matchMedia/prefers-color-scheme.test.js new file mode 100644 index 000000000..718d41182 --- /dev/null +++ b/test/e2e/api/matchMedia/prefers-color-scheme.test.js @@ -0,0 +1,32 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('matchMedia prefers-color-scheme tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', {}); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + page = await context.newPage(); + await page.goto('prefers-color-scheme.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('matchMedia recognizes light scheme', async () => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto('prefers-color-scheme.html'); + const preferredColor = await page.locator('#preferred-color').textContent(); + expect(preferredColor).toBe('Prefers light'); + }); + + test('matchMedia recognizes dark scheme', async () => { + await page.emulateMedia({ colorScheme: 'dark' }); + await page.goto('prefers-color-scheme.html'); + const preferredColor = await page.locator('#preferred-color').textContent(); + expect(preferredColor).toBe('Prefers dark'); + }); +}); diff --git a/test/e2e/api/matchMedia/prefers-lang.html b/test/e2e/api/matchMedia/prefers-lang.html new file mode 100644 index 000000000..959ecc763 --- /dev/null +++ b/test/e2e/api/matchMedia/prefers-lang.html @@ -0,0 +1,88 @@ + + + + + matchMedia.html + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/e2e/api/matchMedia/prefers-lang.test.js b/test/e2e/api/matchMedia/prefers-lang.test.js new file mode 100644 index 000000000..169b427bf --- /dev/null +++ b/test/e2e/api/matchMedia/prefers-lang.test.js @@ -0,0 +1,27 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('mapml-viewer matchMedia API tests', () => { + test('Test prefers-lang: en media feature', async () => { + const browser = await chromium.launch(); + const context = await browser.newContext({ locale: 'en-CA' }); + const page = await context.newPage(); + + await page.goto('prefers-lang.html'); + let layer = await page.locator('map-layer[lang=fr]'); + await expect(layer).toHaveAttribute('hidden'); + + await context.close(); + }); + + test('Test prefers-lang: fr media feature', async () => { + const browser = await chromium.launch(); + const context = await browser.newContext({ locale: 'fr-CA' }); + const page = await context.newPage(); + + await page.goto('prefers-lang.html'); + let layer = await page.locator('map-layer[lang=en]'); + await expect(layer).toHaveAttribute('hidden'); + + await context.close(); + }); +}); diff --git a/test/server.js b/test/server.js index 4b5ea595c..b48500698 100644 --- a/test/server.js +++ b/test/server.js @@ -27,6 +27,7 @@ app.use(express.static(path.join(__dirname, 'e2e/elements/map-style'))); app.use(express.static(path.join(__dirname, 'e2e/elements/map-layer'))); app.use(express.static(path.join(__dirname, 'e2e/elements/layer-'))); app.use(express.static(path.join(__dirname, 'e2e/api'))); +app.use(express.static(path.join(__dirname, 'e2e/api/matchMedia'))); app.use(express.static(path.join(__dirname, 'e2e/data'))); app.use(express.static(path.join(__dirname, 'e2e/geojson'))); // serveStatic enables byte range requests, only required on this directory