From ff5195a7873cba2ffd5ff6b2036a01124529d87e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 3 Oct 2024 18:35:52 +0200 Subject: [PATCH 1/7] fix: make sure anonymous is owner at create The tricky thing is that the Map.is_owner() method check for cookies on the request, but at create this cookie is not set yet on the request, so we have to deal with an exception here. fix #2176 --- umap/static/umap/js/umap.js | 6 ++---- umap/tests/integration/test_anonymous_owned_map.py | 1 + umap/tests/test_map_views.py | 1 + umap/views.py | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index a5792c07..2faaa658 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1063,10 +1063,8 @@ U.Map = L.Map.extend({ window.open(data.login_required) return } - if (data.user?.id) { - this.options.user = data.user - this.renderEditToolbar() - } + this.options.user = data.user + this.renderEditToolbar() if (!this.options.umap_id) { this.options.umap_id = data.id this.permissions.setOptions(data.permissions) diff --git a/umap/tests/integration/test_anonymous_owned_map.py b/umap/tests/integration/test_anonymous_owned_map.py index c406e189..69d7e1b5 100644 --- a/umap/tests/integration/test_anonymous_owned_map.py +++ b/umap/tests/integration/test_anonymous_owned_map.py @@ -156,6 +156,7 @@ def test_can_change_perms_after_create(tilelayer, live_server, page): ".datalayer-permissions select[name='edit_status'] option:checked" ) expect(option).to_have_text("Inherit") + expect(page.get_by_label("Secret edit link:")).to_be_visible() def test_alert_message_after_create( diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index 03c1adee..8aa9947b 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -368,6 +368,7 @@ def test_anonymous_create(cookieclient, post_data): assert ( created_map.get_anonymous_edit_url() in j["permissions"]["anonymous_edit_url"] ) + assert j["user"]["is_owner"] is True assert created_map.name == name key, value = created_map.signed_cookie_elements assert key in cookieclient.cookies diff --git a/umap/views.py b/umap/views.py index a85d4b7a..3091bf1c 100644 --- a/umap/views.py +++ b/umap/views.py @@ -863,15 +863,17 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView): form.instance.owner = self.request.user self.object = form.save() permissions = self.get_permissions() + user_data = self.get_user_data() # User does not have the cookie yet. if not self.object.owner: anonymous_url = self.object.get_anonymous_edit_url() permissions["anonymous_edit_url"] = anonymous_url + user_data["is_owner"] = True response = simple_json_response( id=self.object.pk, url=self.object.get_absolute_url(), permissions=permissions, - user=self.get_user_data(), + user=user_data, ) if not self.request.user.is_authenticated: key, value = self.object.signed_cookie_elements From 13a7a2257d4ecbf551c1fa0dd76a77c568317b09 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 09:26:53 +0200 Subject: [PATCH 2/7] fix: do not fail in autocomplete if on_unselect is undefined --- umap/static/umap/js/modules/autocomplete.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js index 38ac27b7..003c82fb 100644 --- a/umap/static/umap/js/modules/autocomplete.js +++ b/umap/static/umap/js/modules/autocomplete.js @@ -313,7 +313,7 @@ export const SingleMixin = (Base) => DomEvent.on(close, 'click', () => { this.selectedContainer.innerHTML = '' this.input.style.display = 'block' - this.options.on_unselect(result) + this.options.on_unselect?.(result) }) this.hide() } @@ -342,7 +342,7 @@ export const MultipleMixin = (Base) => }) DomEvent.on(close, 'click', () => { this.selectedContainer.removeChild(result_el) - this.options.on_unselect(result) + this.options.on_unselect?.(result) }) this.hide() } From 73c83cfa86f3594a5f711f04397af849f45ea1d9 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 10:03:11 +0200 Subject: [PATCH 3/7] chore: always return user infos on map save --- umap/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/umap/views.py b/umap/views.py index 3091bf1c..5726a838 100644 --- a/umap/views.py +++ b/umap/views.py @@ -910,7 +910,7 @@ def get_websocket_auth_token(request, map_id, map_inst): return simple_json_response(token=signed_token) -class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView): +class MapUpdate(FormLessEditMixin, PermissionsMixin, SessionMixin, UpdateView): model = Map form_class = MapSettingsForm pk_url_kwarg = "map_id" @@ -922,6 +922,7 @@ class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView): id=self.object.pk, url=self.object.get_absolute_url(), permissions=self.get_permissions(), + user=self.get_user_data(), ) From 0fdb70ce669838f9fd66eb5de570ffde78e3653d Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 10:11:34 +0200 Subject: [PATCH 4/7] chore: review save strategy All is now orchestrated from a single method, instead of chaining calls. --- umap/static/umap/js/modules/data/layer.js | 3 +-- umap/static/umap/js/modules/permissions.js | 7 +++--- umap/static/umap/js/umap.js | 28 ++++++++++------------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index 5668b2f5..f02c73c5 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -1026,7 +1026,7 @@ export class DataLayer { } async save() { - if (this.isDeleted) return this.saveDelete() + if (this.isDeleted) return await this.saveDelete() if (!this.isLoaded()) { return } @@ -1091,7 +1091,6 @@ export class DataLayer { await this.map.server.post(this.getDeleteUrl()) } this.isDirty = false - this.map.continueSaving() } getMap() { diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index f66de593..70c25301 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -182,7 +182,7 @@ export class MapPermissions { } async save() { - if (!this.isDirty) return this.map.continueSaving() + if (!this.isDirty) return const formData = new FormData() if (!this.isAnonymousMap() && this.options.editors) { const editors = this.options.editors.map((u) => u.id) @@ -205,7 +205,6 @@ export class MapPermissions { if (!error) { this.commit() this.isDirty = false - this.map.continueSaving() this.map.fire('postsync') } } @@ -288,8 +287,9 @@ export class DataLayerPermissions { pk: this.datalayer.umap_id, }) } + async save() { - if (!this.isDirty) return this.datalayer.map.continueSaving() + if (!this.isDirty) return const formData = new FormData() formData.append('edit_status', this.options.edit_status) const [data, response, error] = await this.datalayer.map.server.post( @@ -300,7 +300,6 @@ export class DataLayerPermissions { if (!error) { this.commit() this.isDirty = false - this.datalayer.map.continueSaving() } } diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 2faaa658..516b5489 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1025,11 +1025,6 @@ U.Map = L.Map.extend({ } }, - continueSaving: function () { - if (this.dirty_datalayers.length) this.dirty_datalayers[0].save() - else this.fire('saved') - }, - exportOptions: function () { const properties = {} for (const option of Object.keys(U.SCHEMA)) { @@ -1059,7 +1054,7 @@ U.Map = L.Map.extend({ return } if (data.login_required) { - window.onLogin = () => this.saveSelf() + window.onLogin = () => this.save() window.open(data.login_required) return } @@ -1069,7 +1064,7 @@ U.Map = L.Map.extend({ this.options.umap_id = data.id this.permissions.setOptions(data.permissions) this.permissions.commit() - if (data?.permissions?.anonymous_edit_url) { + if (data.permissions?.anonymous_edit_url) { this.once('saved', () => { U.AlertCreation.info( L._('Your map has been created with an anonymous account!'), @@ -1102,22 +1097,25 @@ U.Map = L.Map.extend({ } else { window.location = data.url } - this.permissions.save() + return true }, - save: function () { + save: async function () { if (!this.isDirty) return if (this._default_extent) this._setCenterAndZoom() this.backup() - this.once('saved', () => { - this.isDirty = false - }) if (this.options.editMode === 'advanced') { // Only save the map if the user has the rights to do so. - this.saveSelf() - } else { - this.permissions.save() + const ok = await this.saveSelf() + if (!ok) return } + await this.permissions.save() + for (const datalayer of this.dirty_datalayers) { + await datalayer.save() + } + this.isDirty = false + this.renderEditToolbar() + this.fire('saved') }, star: async function () { From 0d7c6e451d0440304b3c55a799670c27ded81e9c Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 10:17:34 +0200 Subject: [PATCH 5/7] chore: new strategy to update map name when editing it Also propagate share_status (this may be moved to permissions) --- umap/static/umap/js/modules/caption.js | 7 ++++++- umap/static/umap/js/umap.controls.js | 27 +++++++++----------------- umap/static/umap/js/umap.core.js | 6 +++--- umap/static/umap/js/umap.js | 19 ++++++++++++++++++ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/umap/static/umap/js/modules/caption.js b/umap/static/umap/js/modules/caption.js index 5496688b..d31dec94 100644 --- a/umap/static/umap/js/modules/caption.js +++ b/umap/static/umap/js/modules/caption.js @@ -19,7 +19,12 @@ export default class Caption { open() { const container = DomUtil.create('div', 'umap-caption') const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container }) - DomUtil.createTitle(hgroup, this.map.options.name, 'icon-caption icon-block') + DomUtil.createTitle( + hgroup, + this.map.getDisplayName(), + 'icon-caption icon-block', + 'map-name' + ) this.map.addAuthorLink('h4', hgroup) if (this.map.options.description) { const description = DomUtil.element({ diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 0a54e66e..869a7211 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -578,11 +578,15 @@ const ControlsMixin = { ], renderEditToolbar: function () { - const container = L.DomUtil.create( - 'div', - 'umap-main-edit-toolbox with-transition dark', - this._controlContainer - ) + const className = 'umap-main-edit-toolbox' + const container = + document.querySelector(`.${className}`) || + L.DomUtil.create( + 'div', + `${className} with-transition dark`, + this._controlContainer + ) + container.innerHTML = '' const leftContainer = L.DomUtil.create('div', 'umap-left-edit-toolbox', container) const rightContainer = L.DomUtil.create('div', 'umap-right-edit-toolbox', container) const logo = L.DomUtil.create('div', 'logo', leftContainer) @@ -623,23 +627,10 @@ const ControlsMixin = { }, this ) - const update = () => { - const status = this.permissions.getShareStatusDisplay() - nameButton.textContent = this.getDisplayName() - // status is not set until map is saved once - if (status) { - shareStatusButton.textContent = L._('Visibility: {status}', { - status: status, - }) - } - } - update() - this.once('saved', L.bind(update, this)) if (this.options.editMode === 'advanced') { L.DomEvent.on(nameButton, 'click', this.editCaption, this) L.DomEvent.on(shareStatusButton, 'click', this.permissions.edit, this.permissions) } - this.on('postsync', L.bind(update, this)) if (this.options.user?.id) { L.DomUtil.createLink( 'umap-user', diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 9b785f86..48c81524 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -141,10 +141,10 @@ L.DomUtil.createButtonIcon = (parent, className, title, callback, size = 16) => return el } -L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => { +L.DomUtil.createTitle = (parent, text, iconClassName, className = '', tag = 'h3') => { const title = L.DomUtil.create(tag, '', parent) - if (className) L.DomUtil.createIcon(title, className) - L.DomUtil.add('span', '', title, text) + if (className) L.DomUtil.createIcon(title, iconClassName) + L.DomUtil.add('span', className, title, text) return title } diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 516b5489..f196c60c 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -200,6 +200,7 @@ U.Map = L.Map.extend({ this.backup() this.on('click', this.closeInplaceToolbar) this.on('contextmenu', this.onContextMenu) + this.propagate() }, initSyncEngine: async function () { @@ -231,6 +232,7 @@ U.Map = L.Map.extend({ this.renderEditToolbar() this.renderControls() this.browser.redraw() + this.propagate() break case 'data': this.redrawVisibleDataLayers() @@ -1097,6 +1099,7 @@ U.Map = L.Map.extend({ } else { window.location = data.url } + this.propagate() return true }, @@ -1118,6 +1121,22 @@ U.Map = L.Map.extend({ this.fire('saved') }, + propagate: function () { + let els = document.querySelectorAll('.map-name') + for (const el of els) { + el.textContent = this.getDisplayName() + } + const status = this.permissions.getShareStatusDisplay() + els = document.querySelectorAll('.share-status') + for (const el of els) { + if (status) { + el.textContent = L._('Visibility: {status}', { + status: status, + }) + } + } + }, + star: async function () { if (!this.options.umap_id) { return U.Alert.error(L._('Please save the map first')) From a35b37f423bc03c937f14a2e8a661ae782a06fa2 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 10:18:43 +0200 Subject: [PATCH 6/7] chore: use map getter instead of getMap in permissions --- umap/static/umap/js/modules/permissions.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index 70c25301..3971a159 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -42,10 +42,6 @@ export class MapPermissions { return !this.map.options.permissions.owner } - getMap() { - return this.map - } - _editAnonymous(container) { const fields = [] if (this.isOwner()) { @@ -257,7 +253,7 @@ export class DataLayerPermissions { return this._isDirty } - getMap() { + get map() { return this.datalayer.map } @@ -270,7 +266,7 @@ export class DataLayerPermissions { label: translate('Who can edit "{layer}"', { layer: this.datalayer.getName(), }), - selectOptions: this.datalayer.map.options.datalayer_edit_statuses, + selectOptions: this.map.options.datalayer_edit_statuses, }, ], ] @@ -282,8 +278,8 @@ export class DataLayerPermissions { } getUrl() { - return Utils.template(this.datalayer.map.options.urls.datalayer_permissions, { - map_id: this.datalayer.map.options.umap_id, + return this.map.urls.get('datalayer_permissions', { + map_id: this.map.options.umap_id, pk: this.datalayer.umap_id, }) } @@ -292,7 +288,7 @@ export class DataLayerPermissions { if (!this.isDirty) return const formData = new FormData() formData.append('edit_status', this.options.edit_status) - const [data, response, error] = await this.datalayer.map.server.post( + const [data, response, error] = await this.map.server.post( this.getUrl(), {}, formData From 03ae31c18beb90e2b929dbbcd3801f26dde41d33 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 4 Oct 2024 12:16:46 +0200 Subject: [PATCH 7/7] chore: remove map.dirty_datalayers Let's try to make it simpler. map.datalayers stores all datalayers map.datalayers_index the non deleted ones sorted --- umap/static/umap/js/modules/data/layer.js | 14 +++---- umap/static/umap/js/modules/permissions.js | 8 ++-- umap/static/umap/js/umap.js | 41 +++++++------------ umap/tests/integration/test_edit_datalayer.py | 2 - umap/tests/integration/test_edit_map.py | 2 +- 5 files changed, 27 insertions(+), 40 deletions(-) diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index f02c73c5..96807f13 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -92,12 +92,12 @@ export class DataLayer { set isDirty(status) { this._isDirty = status if (status) { - this.map.addDirtyDatalayer(this) + this.map.isDirty = true // A layer can be made dirty by indirect action (like dragging layers) // we need to have it loaded before saving it. if (!this.isLoaded()) this.fetchData() } else { - this.map.removeDirtyDatalayer(this) + this.map.checkDirty() this.isDeleted = false } } @@ -339,11 +339,11 @@ export class DataLayer { const id = stamp(this) if (!this.map.datalayers[id]) { this.map.datalayers[id] = this - if (!this.map.datalayers_index.includes(this)) { - this.map.datalayers_index.push(this) - } - this.map.onDataLayersChanged() } + if (!this.map.datalayers_index.includes(this)) { + this.map.datalayers_index.push(this) + } + this.map.onDataLayersChanged() } _dataUrl() { @@ -559,7 +559,6 @@ export class DataLayer { erase() { this.hide() - delete this.map.datalayers[stamp(this)] this.map.datalayers_index.splice(this.getRank(), 1) this.parentPane.removeChild(this.pane) this.map.onDataLayersChanged() @@ -1090,6 +1089,7 @@ export class DataLayer { if (this.umap_id) { await this.map.server.post(this.getDeleteUrl()) } + delete this.map.datalayers[stamp(this)] this.isDirty = false } diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index 3971a159..3300da2a 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -225,9 +225,11 @@ export class MapPermissions { } getShareStatusDisplay() { - return Object.fromEntries(this.map.options.share_statuses)[ - this.options.share_status - ] + if (this.map.options.share_statuses) { + return Object.fromEntries(this.map.options.share_statuses)[ + this.options.share_status + ] + } } } diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index f196c60c..d376e7b0 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -13,7 +13,7 @@ L.Map.mergeOptions({ // we cannot rely on this because of the y is overriden by Leaflet // See https://github.com/Leaflet/Leaflet/pull/9201 // And let's remove this -y when this PR is merged and released. - demoTileInfos: { 's': 'a', 'z': 9, 'x': 265, 'y': 181, '-y': 181, 'r': '' }, + demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, licences: [], licence: '', enableMarkerDraw: true, @@ -110,10 +110,9 @@ U.Map = L.Map.extend({ delete this.options.advancedFilterKey } - // Global storage for retrieving datalayers and features - this.datalayers = {} - this.datalayers_index = [] - this.dirty_datalayers = [] + // Global storage for retrieving datalayers and features. + this.datalayers = {} // All datalayers, including deleted. + this.datalayers_index = [] // Datalayers actually on the map and ordered. this.features_index = {} // Needed for actions labels @@ -489,7 +488,7 @@ U.Map = L.Map.extend({ loadDataLayers: async function () { this.datalayersLoaded = true this.fire('datalayersloaded') - for (const datalayer of Object.values(this.datalayers)) { + for (const datalayer of this.datalayers_index) { if (datalayer.showAtLoad()) await datalayer.show() } this.dataloaded = true @@ -935,6 +934,7 @@ U.Map = L.Map.extend({ if (mustReindex) datalayer.reindex() datalayer.redraw() }) + this.propagate() this.fire('postsync') this.isDirty = true }, @@ -998,12 +998,12 @@ U.Map = L.Map.extend({ if (this.editTools) this.editTools.stopDrawing() this.resetOptions() this.datalayers_index = [].concat(this._datalayers_index_bk) - this.dirty_datalayers.slice().forEach((datalayer) => { + // Iter over all datalayers, including deleted if any. + for (const datalayer of Object.values(this.datalayers)) { if (datalayer.isDeleted) datalayer.connectToMap() - datalayer.reset() - }) + if (datalayer.isDirty) datalayer.reset() + } this.ensurePanesOrder() - this.dirty_datalayers = [] this.initTileLayers() this.isDirty = false this.onDataLayersChanged() @@ -1013,20 +1013,6 @@ U.Map = L.Map.extend({ this._container.classList.toggle('umap-is-dirty', this.isDirty) }, - addDirtyDatalayer: function (datalayer) { - if (this.dirty_datalayers.indexOf(datalayer) === -1) { - this.dirty_datalayers.push(datalayer) - this.isDirty = true - } - }, - - removeDirtyDatalayer: function (datalayer) { - if (this.dirty_datalayers.indexOf(datalayer) !== -1) { - this.dirty_datalayers.splice(this.dirty_datalayers.indexOf(datalayer), 1) - this.checkDirty() - } - }, - exportOptions: function () { const properties = {} for (const option of Object.keys(U.SCHEMA)) { @@ -1113,8 +1099,9 @@ U.Map = L.Map.extend({ if (!ok) return } await this.permissions.save() - for (const datalayer of this.dirty_datalayers) { - await datalayer.save() + // Iter over all datalayers, including deleted. + for (const datalayer of Object.values(this.datalayers)) { + if (datalayer.isDirty) await datalayer.save() } this.isDirty = false this.renderEditToolbar() @@ -1859,7 +1846,7 @@ U.Map = L.Map.extend({ getFeatureById: function (id) { let feature - for (const datalayer of Object.values(this.datalayers)) { + for (const datalayer of this.datalayers_index) { feature = datalayer.getFeatureById(id) if (feature) return feature } diff --git a/umap/tests/integration/test_edit_datalayer.py b/umap/tests/integration/test_edit_datalayer.py index 12c92c10..986a6596 100644 --- a/umap/tests/integration/test_edit_datalayer.py +++ b/umap/tests/integration/test_edit_datalayer.py @@ -63,9 +63,7 @@ def test_cancel_deleting_datalayer_should_restore( page.once("dialog", lambda dialog: dialog.accept()) page.locator(".panel.right").get_by_title("Delete layer").click() expect(markers).to_have_count(0) - page.get_by_role("button", name="Open browser").click() expect(page.get_by_text("test datalayer")).to_be_hidden() - page.once("dialog", lambda dialog: dialog.accept()) page.get_by_role("button", name="Cancel edits").click() page.locator("dialog").get_by_role("button", name="OK").click() expect(markers).to_have_count(1) diff --git a/umap/tests/integration/test_edit_map.py b/umap/tests/integration/test_edit_map.py index 802c6412..4dfa3704 100644 --- a/umap/tests/integration/test_edit_map.py +++ b/umap/tests/integration/test_edit_map.py @@ -40,7 +40,7 @@ def test_map_name_impacts_ui(live_server, page, tilelayer): name_input.fill("something else") - expect(page.get_by_role("button", name="something else").nth(1)).to_be_visible() + expect(page.get_by_role("button", name="something else").first).to_be_visible() def test_zoomcontrol_impacts_ui(live_server, page, tilelayer):