From 99ee8cf8cb96f80cdf45817c48a542c366c26f97 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 21 Mar 2024 12:23:55 -0400 Subject: [PATCH 1/2] Support for map-link[rel=stylesheet] and map-style in map-extent Closes #944 Add sloMo to ArrowKeyNaveContextMenu.test.js --- src/map-extent.js | 24 +- src/map-link.js | 2 + src/map-style.js | 3 +- src/mapml/layers/ExtentLayer.js | 25 ++ src/mapml/layers/TemplatedTileLayer.js | 9 +- test/e2e/core/ArrowKeyNavContextMenu.test.js | 2 +- test/e2e/core/styleParsing.html | 32 ++- test/e2e/core/styleParsing.test.js | 253 ++++++++++++------ .../e2e/data/tiles/cbmt/alabama_feature.mapml | 9 +- test/e2e/data/tiles/wgs84/0/r0_c0.mapml | 17 +- .../data/tiles/wgs84/vector-tile-test.mapml | 13 +- 11 files changed, 284 insertions(+), 105 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index fea5d50b5..303be55cf 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -262,6 +262,11 @@ export class MapExtent extends HTMLElement { // this._layerControlHTML is the fieldset for the extent in the LayerControl this._layerControlHTML = this._createLayerControlExtentHTML(); this._calculateBounds(); + // instead of children using parents' whenReady which can be cyclic, + // when this element is ready, run stuff that is only available after + // initialization + this._runMutationObserver(this.children); + // make sure same thing happens when children are added this._bindMutationObserver(); } /* @@ -290,6 +295,16 @@ export class MapExtent extends HTMLElement { this._validateDisabled(); }); }; + const _addStylesheetLink = (mapLink) => { + this.whenReady().then(() => { + this._extentLayer.appendStyleLink(mapLink); + }); + }; + const _addStyleElement = (mapStyle) => { + this.whenReady().then(() => { + this._extentLayer.appendStyleElement(mapStyle); + }); + }; for (let i = 0; i < elementsGroup.length; ++i) { let element = elementsGroup[i]; switch (element.nodeName) { @@ -303,7 +318,14 @@ export class MapExtent extends HTMLElement { } break; case 'MAP-LINK': - // might need this in future, among others + if (element.link && !element.link.isConnected) + _addStylesheetLink(element); + break; + case 'MAP-STYLE': + if (element.styleElement && !element.styleElement.isConnected) { + _addStyleElement(element); + } + break; default: break; } diff --git a/src/map-link.js b/src/map-link.js index 73baadbc4..850302da9 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -316,6 +316,8 @@ export class MapLink extends HTMLElement { this._stylesheetHost._layer.appendStyleLink(this); } else if (this._stylesheetHost._templatedLayer) { this._stylesheetHost._templatedLayer.appendStyleLink(this); + } else if (this._stylesheetHost._extentLayer) { + this._stylesheetHost._extentLayer.appendStyleLink(this); } function copyAttributes(source, target) { diff --git a/src/map-style.js b/src/map-style.js index 3d8297571..6c050434a 100644 --- a/src/map-style.js +++ b/src/map-style.js @@ -27,11 +27,12 @@ export class MapStyle extends HTMLElement { this.styleElement.mapStyle = this; this.styleElement.textContent = this.textContent; copyAttributes(this, this.styleElement); - if (this._stylesheetHost._layer) { this._stylesheetHost._layer.appendStyleElement(this); } else if (this._stylesheetHost._templatedLayer) { this._stylesheetHost._templatedLayer.appendStyleElement(this); + } else if (this._stylesheetHost._extentLayer) { + this._stylesheetHost._extentLayer.appendStyleElement(this); } function copyAttributes(source, target) { diff --git a/src/mapml/layers/ExtentLayer.js b/src/mapml/layers/ExtentLayer.js index 9b275aff9..a97ae76dc 100644 --- a/src/mapml/layers/ExtentLayer.js +++ b/src/mapml/layers/ExtentLayer.js @@ -75,6 +75,31 @@ export var ExtentLayer = L.LayerGroup.extend({ this._extentEl._opacity = opacity; if (this._extentEl._opacitySlider) this._extentEl._opacitySlider.value = opacity; + }, + appendStyleLink: function (mapLink) { + if (!mapLink.link) return; + let positionAndNode = this._getStylePositionAndNode(); + positionAndNode.node.insertAdjacentElement( + positionAndNode.position, + mapLink.link + ); + }, + _getStylePositionAndNode: function () { + return this._container.lastChild && + (this._container.lastChild.nodeName.toUpperCase() === 'SVG' || + this._container.lastChild.classList.contains('mapml-vector-container')) + ? { position: 'beforebegin', node: this._container.lastChild } + : this._container.lastChild + ? { position: 'afterend', node: this._container.lastChild } + : { position: 'afterbegin', node: this._container }; + }, + appendStyleElement: function (mapStyle) { + if (!mapStyle.styleElement) return; + let positionAndNode = this._getStylePositionAndNode(); + positionAndNode.node.insertAdjacentElement( + positionAndNode.position, + mapStyle.styleElement + ); } }); export var extentLayer = function (options) { diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 5b783d97b..db83346b6 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -196,14 +196,15 @@ export var TemplatedTileLayer = L.TileLayer.extend({ if (href) { if (!container.querySelector("link[href='" + href + "']")) { var linkElm = document.createElement('link'); + copyAttributes(stylesheets[i], linkElm); linkElm.setAttribute('href', href); - linkElm.setAttribute('rel', 'stylesheet'); ss.push(linkElm); } } } else { // var styleElm = document.createElement('style'); + copyAttributes(stylesheets[i], styleElm); styleElm.textContent = stylesheets[i].textContent; ss.push(styleElm); } @@ -215,6 +216,12 @@ export var TemplatedTileLayer = L.TileLayer.extend({ for (var s = ss.length - 1; s >= 0; s--) { container.insertAdjacentElement('afterbegin', ss[s]); } + function copyAttributes(source, target) { + return Array.from(source.attributes).forEach((attribute) => { + if (attribute.nodeName !== 'href') + target.setAttribute(attribute.nodeName, attribute.nodeValue); + }); + } }, _createFeatures: function (markup, coords, tile) { diff --git a/test/e2e/core/ArrowKeyNavContextMenu.test.js b/test/e2e/core/ArrowKeyNavContextMenu.test.js index ae89d7c6e..e57a7397e 100644 --- a/test/e2e/core/ArrowKeyNavContextMenu.test.js +++ b/test/e2e/core/ArrowKeyNavContextMenu.test.js @@ -4,7 +4,7 @@ test.describe('Using arrow keys to navigate context menu', () => { let page; let context; test.beforeAll(async function () { - context = await chromium.launchPersistentContext(''); + context = await chromium.launchPersistentContext('', { slowMo: 250 }); page = context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); diff --git a/test/e2e/core/styleParsing.html b/test/e2e/core/styleParsing.html index 1d4946ad7..a33ec678e 100644 --- a/test/e2e/core/styleParsing.html +++ b/test/e2e/core/styleParsing.html @@ -22,19 +22,19 @@ - + - + - + .second { stroke: aqua; } - + .third { stroke: blue } @@ -88,7 +88,7 @@

Colorado

- +
- - - .secondVector { + + .firstVector { color: aliceblue; } + - - + + diff --git a/test/e2e/core/styleParsing.test.js b/test/e2e/core/styleParsing.test.js index aec54de50..c94847949 100644 --- a/test/e2e/core/styleParsing.test.js +++ b/test/e2e/core/styleParsing.test.js @@ -1,6 +1,6 @@ import { test, expect, chromium } from '@playwright/test'; -test.describe('Style Parsed and Implemented Test', () => { +test.describe('map-style and map-link[rel=stylesheet] tests', () => { let page; let context; test.beforeAll(async () => { @@ -12,95 +12,190 @@ test.describe('Style Parsed and Implemented Test', () => { await page.waitForTimeout(250); }); - test.beforeEach(async () => { - await page.waitForTimeout(250); - }); - test.afterAll(async function () { await context.close(); }); //tests using the 1st map in the page - test('CSS within html page added to inorder to overlay-pane container', async () => { - const styleContent = await page.$eval( - 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div', - (styleE) => styleE.innerHTML - ); - await expect(styleContent.indexOf('first')).toBeLessThan( - styleContent.indexOf('.second') - ); - await expect(styleContent.indexOf('.second')).toBeLessThan( - styleContent.indexOf('.third') - ); - await expect(styleContent.indexOf('.third')).toBeLessThan( - styleContent.indexOf('fourth') - ); + test(`Local-content (no src) styles rendered as style/link in same order as \ +found, in expected shadow root location`, async () => { + const localContentLayer = page.getByTestId('arizona'); + const ids = await localContentLayer.evaluate((layer) => { + const elementSequence = layer.querySelectorAll( + 'map-link[rel=stylesheet],map-style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + const renderedIds = await localContentLayer.evaluate((layer) => { + const elementSequence = layer._layer._container.querySelectorAll( + 'link[rel=stylesheet],style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(ids).toEqual(renderedIds); }); - test('CSS from a retrieved MapML file added inorder inside templated-layer container', async () => { - const firstStyle = await page.$eval( - // this div * - 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-extentlayer-container > .mapml-features-container.mapml-vector-container > link:nth-child(1)', - // no longer exists, (the svg is now the child of the first div) - // but the link isn't created either, and it should be - (styleE) => styleE.outerHTML - ); - const secondStyle = await page.$eval( - 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-extentlayer-container > .mapml-features-container.mapml-vector-container > link:nth-child(2)', - (styleL) => styleL.outerHTML - ); - await expect(firstStyle).toMatch('canvec_cantopo'); - await expect(secondStyle).toMatch('canvec_feature'); + test(`Local layer content (no src) map-link[rel=features] remote styles \ +rendered as style/link in same order as found, in expected shadow root location`, async () => { + const featuresLink = page.getByTestId('alabama-features'); + const ids = await featuresLink.evaluate((link) => { + const elementSequence = link.shadowRoot.querySelectorAll( + 'map-link[rel=stylesheet],map-style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + const renderedIds = await featuresLink.evaluate((link) => { + const elementSequence = link._templatedLayer._container.querySelectorAll( + 'link[rel=stylesheet],style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(ids).toEqual(renderedIds); }); - test('CSS within html page added to overlay-pane container', async () => { - const map1 = await page.getByTestId('map1'); - const foundStyleLinks = await map1.locator('#first'); - const foundStyleTags = await map1.locator( - '.mapml-layer:nth-child(1) > style' - ); - // expect the map-link#first to have been reflected as a into the shadow root - expect(foundStyleLinks).toHaveCount(2); - expect(foundStyleTags).toHaveCount(2); + test(`Local style children of map-extent rendered as style/link in same order \ +as found, in expected shadow root location`, async () => { + const mapExtent = page.getByTestId('map-ext1'); + const ids = await mapExtent.evaluate((e) => { + const elementSequence = e.querySelectorAll( + 'map-link[rel=stylesheet],map-style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + const renderedIds = await mapExtent.evaluate((e) => { + const elementSequence = e._extentLayer._container.querySelectorAll( + ':scope > link[rel=stylesheet],:scope > style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(ids).toEqual(renderedIds); }); - - test('CSS from a retrieved MapML File added to templated-layer container', async () => { - const foundStyleLinkOne = await page.$( - 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-extentlayer-container > .mapml-features-container.mapml-vector-container > link' - ); - const foundStyleLinkTwo = await page.$( - 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-extentlayer-container > .mapml-features-container.mapml-vector-container > link:nth-child(2)' - ); - await expect(foundStyleLinkOne).toBeTruthy(); - await expect(foundStyleLinkTwo).toBeTruthy(); + test(`Local content map-link[rel=tile][type=text/mapml] with remote styles \ +embedded in text/mapml tiles are rendered in same order as found in tile`, async () => { + // it's a bit tricky to work with tiled mapml vectors because the map-feature + // is discarded and only the rendered path is kept. In this case, we're using + // the same layer twice in the styleParsing.html (vector-tile-test.mapml), + // which refers to the WGS84 countries' test tile data + // as a result, we get the same feature rendered whenever we add the layer + // in this case it is used inline and remotely. The inline countries are + // first in DOM order, so we'll take the first location of the rendered thing + // we're looking for (in this test, at least) + const renderedPath = await page.getByTestId('r0_c0').first(); + const renderedStyleIds = await renderedPath.evaluate((p) => { + const elementSequence = p + .closest('.mapml-tile-group.leaflet-tile') + .querySelectorAll(':scope > link[rel=stylesheet],:scope > style'); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(renderedStyleIds).toEqual('one,two,three'); }); - - //testing done on 2nd map in the page - test('CSS from a retrieved MapML file added inorder inside svg within templated-tile-container', async () => { - const firstStyle = await page.$eval( - 'xpath=//html/body/map[2]/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-extentlayer-container .mapml-tile-group > style:nth-child(1)', - (styleE) => styleE.innerHTML - ); - const secondStyle = await page.$eval( - 'xpath=//html/body/map[2]/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-extentlayer-container .mapml-tile-group > style:nth-child(2)', - (styleE) => styleE.innerHTML - ); - const foundStyleLink = await page.$( - 'xpath=//html/body/map[2]/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-extentlayer-container .mapml-tile-group > link' - ); - await expect(firstStyle).toMatch('refStyleOne'); - await expect(secondStyle).toMatch('refStyleTwo'); - await expect(foundStyleLink).toBeTruthy(); + test(`Remote styles (layer- src) in the map-head rendered in the same source order\ +as children of layer-._layer._container`, async () => { + const remoteLayer = page.getByTestId('remote'); + const ids = await remoteLayer.evaluate((l) => { + const elementSequence = l.shadowRoot.querySelectorAll( + ':host > map-style,:host > [rel=stylesheet]' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + const renderedIds = await remoteLayer.evaluate((l) => { + const elementSequence = l._layer._container.querySelectorAll( + ':scope > link[rel=stylesheet],:scope > style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(renderedIds).toEqual(ids); }); - - test('CSS within html page added inorder to overlay-pane container', async () => { - const foundStyleLink = await page.$( - 'xpath=//html/body/map[2]/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > link' - ); - const foundStyleTag = await page.$( - 'xpath=//html/body/map[2]/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > style' - ); - await expect(foundStyleTag).toBeTruthy(); - await expect(foundStyleLink).toBeTruthy(); + test(`Remote (layer- src) styles in the remote map-extent should be rendered \ +in the same order as in remote source, in the map-extent._extentLayer._container`, async () => { + const remoteLayer = page.getByTestId('remote'); + const ids = await remoteLayer.evaluate((l) => { + const elementSequence = l.shadowRoot.querySelectorAll( + ':host > map-extent > map-style,:host > map-extent > [rel=stylesheet]' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + const renderedIds = await remoteLayer.evaluate((l) => { + const e = l.shadowRoot.querySelector('map-extent'); + const elementSequence = e._extentLayer._container.querySelectorAll( + ':scope > link[rel=stylesheet],:scope > style' + ); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + expect(renderedIds).toEqual(ids); + }); + test(`Remote (layer- src) styles embedded within loaded tiles should be \ +rendered in the same order as in the tile, in the tile container`, async () => { + // there's more than one use of vector-tile-test.mapml here, so the test id + // is not unique. this is the second and last occurence of it though: + const renderedPath = await page.getByTestId('r0_c0').last(); + const renderedStyleIds = await renderedPath.evaluate((p) => { + const elementSequence = p + .closest('.mapml-tile-group.leaflet-tile') + .querySelectorAll(':scope > link[rel=stylesheet],:scope > style'); + let ids = ''; + for (let i = 0; i < elementSequence.length; i++) + ids += + elementSequence[i].getAttribute('id') + + (i < elementSequence.length - 1 ? ',' : ''); + return ids; + }); + // see e2e/data/tiles/wgs84/0/r0_c0.mapml for original order, it's one,two,three... + expect(renderedStyleIds).toEqual('one,two,three'); }); }); diff --git a/test/e2e/data/tiles/cbmt/alabama_feature.mapml b/test/e2e/data/tiles/cbmt/alabama_feature.mapml index 6576b9bb6..144959502 100644 --- a/test/e2e/data/tiles/cbmt/alabama_feature.mapml +++ b/test/e2e/data/tiles/cbmt/alabama_feature.mapml @@ -2,9 +2,12 @@ Natural Resources Canada's CanVec+ 031G - Map Markup Language - - - + + + + .all { fill-opacity: 0.7; stroke-width: 2; stroke: white; stroke-opacity: 1; stroke-dasharray: 3; } diff --git a/test/e2e/data/tiles/wgs84/0/r0_c0.mapml b/test/e2e/data/tiles/wgs84/0/r0_c0.mapml index 5b396c6c2..77c95951e 100644 --- a/test/e2e/data/tiles/wgs84/0/r0_c0.mapml +++ b/test/e2e/data/tiles/wgs84/0/r0_c0.mapml @@ -4,23 +4,32 @@ - + .refStyleOne{ color:black; } - + .refStyleTwo{ color:black; } - + - + + + + + -163.4686870536532 -83.0479765610694 -162.1722212049075 -83.1578465482512 -161.8082768723677 -83.0067753158762 -162.4688701702985 -82.8776780809375 -163.711774400293 -82.8405969602637 -163.4686870536532 -83.0479765610694 diff --git a/test/e2e/data/tiles/wgs84/vector-tile-test.mapml b/test/e2e/data/tiles/wgs84/vector-tile-test.mapml index 25cecf69a..1e49796e3 100644 --- a/test/e2e/data/tiles/wgs84/vector-tile-test.mapml +++ b/test/e2e/data/tiles/wgs84/vector-tile-test.mapml @@ -4,21 +4,26 @@ - - - + + .secondLayerVector{ color:black; } + + + From 646dc5ead833c0fbd9b1a078220151e2a38a36bc Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 22 Mar 2024 21:55:19 -0400 Subject: [PATCH 2/2] De-flake popupTabNavigation, hopefully for good --- test/e2e/core/popupTabNavigation.html | 12 +- test/e2e/core/popupTabNavigation.test.js | 255 ++++++++++------------- 2 files changed, 114 insertions(+), 153 deletions(-) diff --git a/test/e2e/core/popupTabNavigation.html b/test/e2e/core/popupTabNavigation.html index 88fe9cdd5..7d7a8f02d 100644 --- a/test/e2e/core/popupTabNavigation.html +++ b/test/e2e/core/popupTabNavigation.html @@ -9,9 +9,9 @@ - - + - + diff --git a/test/e2e/core/popupTabNavigation.test.js b/test/e2e/core/popupTabNavigation.test.js index 57f5e552d..ce1d74e97 100644 --- a/test/e2e/core/popupTabNavigation.test.js +++ b/test/e2e/core/popupTabNavigation.test.js @@ -17,91 +17,84 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { test.describe('Feature Popup Tab Navigation Tests', () => { test('Inline features popup focus order', async () => { - await page.evaluateHandle(() => - document.getElementById('vector').removeAttribute('checked') - ); - await page.evaluateHandle(() => - document.getElementById('query').removeAttribute('checked') - ); - const body = page.locator('body'); - await body.click(); + const vectorLayer = page.getByTestId('vector'); + await vectorLayer.evaluate((l) => { + l.removeAttribute('checked'); + }); + const queryLayer = page.getByTestId('query'); + await queryLayer.evaluate((l) => { + l.removeAttribute('checked'); + }); + const viewer = page.locator('mapml-viewer'); + await viewer.focus(); await page.keyboard.press('Tab'); // focus map await page.keyboard.press('Tab'); // focus feature await page.keyboard.press('Enter'); // display popup with link in it - const viewer = page.locator('mapml-viewer'); - let f = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.className - ); - expect(f).toEqual('mapml-popup-content'); - + const popupContainer = page.locator('.leaflet-popup'); + const popup = popupContainer.locator('.mapml-popup-content'); + await expect(popup).toBeFocused(); await page.keyboard.press('Tab'); // focus link - let f2 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.tagName - ); - expect(f2.toUpperCase()).toEqual('A'); + // there are actually 2 copies of the testid, so we scope relative to the popup + const anchor = popupContainer.getByTestId('anchor'); + await expect(anchor).toBeFocused(); await page.keyboard.press('Tab'); // focus on "zoom to here" link - let f3 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.tagName - ); - expect(f3.toUpperCase()).toEqual('A'); + const zoomToHereLink = popupContainer.getByText('Zoom to here'); + await expect(zoomToHereLink).toBeFocused(); await page.keyboard.press('Tab'); // focus on |< affordance - let f4 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.title - ); - expect(f4).toEqual('Focus Map'); + const focusMapButton = popupContainer.getByRole('button', { + name: 'Focus Map' + }); + await expect(focusMapButton).toBeFocused(); await page.keyboard.press('Tab'); // focus on < affordance - let f5 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.title - ); - expect(f5).toEqual('Previous Feature'); + const previousFeatureButton = popupContainer.getByRole('button', { + name: 'Previous Feature' + }); + await expect(previousFeatureButton).toBeFocused(); await page.keyboard.press('Tab'); // focus on > affordance - let f6 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.title - ); - expect(f6).toEqual('Next Feature'); + const nextFeatureButton = popupContainer.getByRole('button', { + name: 'Next Feature' + }); + await expect(nextFeatureButton).toBeFocused(); await page.keyboard.press('Tab'); // focus on >| affordance - let f7 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.title - ); - expect(f7).toEqual('Focus Controls'); + const focusControlsButton = popupContainer.getByRole('button', { + name: 'Focus Controls' + }); + await expect(focusControlsButton).toBeFocused(); await page.keyboard.press('Tab'); // focus on X dismiss popup affordance - let f8 = await viewer.evaluate( - (viewer) => viewer.shadowRoot.activeElement.className - ); - expect(f8).toEqual('leaflet-popup-close-button'); + const dismissButton = popupContainer.getByRole('button', { + name: 'Close popup' + }); + await expect(dismissButton).toBeFocused(); }); test('Tab to next feature after tabbing out of popup', async () => { + const viewer = page.getByTestId('viewer'); await page.keyboard.press('Escape'); // focus back on feature - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + // according to https://github.com/microsoft/playwright/issues/15929 + // tests should locate the element and then check that it is focused + const bigSquare = viewer.getByTestId('big-square'); + const bigSquarePathData = await bigSquare.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + // no way to get a locator from another locator afaik, but this may work: + const bigSquareGroupEl = viewer.locator( + 'g:has( > path[d="' + bigSquarePathData + '"])' ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh - ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - expect(f).toEqual('M330 83L586 83L586 339L330 339z'); - await page.waitForTimeout(500); + await expect(bigSquareGroupEl).toBeFocused(); // that we have to do this to get the tooltip back is a bug #681 await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Tab'); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); - expect(tooltipCount).toEqual(1); }); @@ -110,26 +103,20 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { await page.waitForTimeout(500); await page.keyboard.press('Shift+Tab'); await page.waitForTimeout(500); - - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') - ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh + const viewer = page.getByTestId('viewer'); + const bigSquare = viewer.getByTestId('big-square'); + const bigSquarePathData = await bigSquare.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + const bigSquareGroupEl = viewer.locator( + 'g:has( > path[d="' + bigSquarePathData + '"])' ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + await expect(bigSquareGroupEl).toBeFocused(); + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); - expect(tooltipCount).toEqual(1); - expect(f).toEqual('M330 83L586 83L586 339L330 339z'); }); test('Previous feature button focuses previous feature', async () => { @@ -145,25 +132,20 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { await page.waitForTimeout(500); await page.keyboard.press('Enter'); // focus should fall on previous feature await page.waitForTimeout(500); - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') - ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh + const viewer = page.getByTestId('viewer'); + const bigSquare = viewer.getByTestId('big-square'); + const bigSquarePathData = await bigSquare.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + const bigSquareGroupEl = viewer.locator( + 'g:has( > path[d="' + bigSquarePathData + '"])' ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + await expect(bigSquareGroupEl).toBeFocused(); + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); - expect(tooltipCount).toEqual(1); - expect(f).toEqual('M330 83L586 83L586 339L330 339z'); }); test('Tooltip appears after pressing esc key', async () => { @@ -172,25 +154,20 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { await page.keyboard.down('Escape'); // focus back on feature await page.keyboard.up('Escape'); await page.waitForTimeout(500); - - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + const viewer = page.getByTestId('viewer'); + const bigSquare = viewer.getByTestId('big-square'); + const bigSquarePathData = await bigSquare.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + const bigSquareGroupEl = viewer.locator( + 'g:has( > path[d="' + bigSquarePathData + '"])' ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh - ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + await expect(bigSquareGroupEl).toBeFocused(); + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); expect(tooltipCount).toEqual(1); - expect(f).toEqual('M330 83L586 83L586 339L330 339z'); }); test('Tooltip appears after pressing enter on close button', async () => { @@ -205,25 +182,20 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { await page.keyboard.down('Enter'); // press x button await page.keyboard.up('Enter'); await page.waitForTimeout(500); - - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') - ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh + const viewer = page.getByTestId('viewer'); + const bigSquare = viewer.getByTestId('big-square'); + const bigSquarePathData = await bigSquare.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + const bigSquareGroupEl = viewer.locator( + 'g:has( > path[d="' + bigSquarePathData + '"])' ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + await expect(bigSquareGroupEl).toBeFocused(); + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); expect(tooltipCount).toEqual(1); - expect(f).toEqual('M330 83L586 83L586 339L330 339z'); }); test('Next feature button focuses next feature', async () => { @@ -240,25 +212,21 @@ test.describe('Playwright Keyboard Navigation + Query Layer Tests', () => { await page.keyboard.press('Tab'); // focus > affordance (next feature) await page.waitForTimeout(500); await page.keyboard.press('Enter'); // focus falls on next feature - const h = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + const viewer = page.getByTestId('viewer'); + const smallTrapezoid = viewer.getByTestId('small-trapezoid'); + const smallTrapezoidPathData = await smallTrapezoid.evaluate((f) => { + return f._groupEl.firstElementChild.getAttribute('d'); + }); + // no way to get a locator from another locator afaik, but this may work: + const smallTrapezoidGroupEl = viewer.locator( + 'g:has( > path[d="' + smallTrapezoidPathData + '"])' ); - const nh = await page.evaluateHandle((doc) => doc.shadowRoot, h); - const rh = await page.evaluateHandle( - (root) => root.activeElement.querySelector('.leaflet-interactive'), - nh - ); - const f = await ( - await page.evaluateHandle((elem) => elem.getAttribute('d'), rh) - ).jsonValue(); - - let tooltipCount = await page.$eval( - 'mapml-viewer .leaflet-tooltip-pane', + await expect(smallTrapezoidGroupEl).toBeFocused(); + const toolTipPane = viewer.locator('.leaflet-tooltip-pane'); + let tooltipCount = await toolTipPane.evaluate( (div) => div.childElementCount ); - expect(tooltipCount).toEqual(1); - expect(f).toEqual('M285 373L460 380L468 477L329 459z'); }); test('Focus Controls focuses the first