diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index 5b18286e..4c6bfee3 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -70,7 +70,6 @@ export default function getPurify() { export function escapeHTML(s) { s = s ? s.toString() : '' s = getPurify().sanitize(s, { - USE_PROFILES: { html: true }, ADD_TAGS: ['iframe'], ALLOWED_TAGS: [ 'h3', @@ -86,9 +85,10 @@ export function escapeHTML(s) { 'iframe', 'img', 'br', + 'span', ], ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'], - ALLOWED_ATTR: ['href', 'src', 'width', 'height'], + ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'style'], // Added: `geo:` URL scheme as defined in RFC5870: // https://www.rfc-editor.org/rfc/rfc5870.html // The base RegExp comes from: diff --git a/umap/static/umap/js/umap.autocomplete.js b/umap/static/umap/js/umap.autocomplete.js index 3da1bdeb..a1aa930a 100644 --- a/umap/static/umap/js/umap.autocomplete.js +++ b/umap/static/umap/js/umap.autocomplete.js @@ -35,27 +35,25 @@ U.AutoComplete = L.Class.extend({ }, createInput: function () { - this.input = L.DomUtil.element( - 'input', - { - type: 'text', - placeholder: this.options.placeholder, - autocomplete: 'off', - className: this.options.className, - }, - this.el - ) + this.input = L.DomUtil.element({ + tagName: 'input', + type: 'text', + parent: this.el, + placeholder: this.options.placeholder, + autocomplete: 'off', + className: this.options.className, + }) L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this) L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this) L.DomEvent.on(this.input, 'blur', this.onBlur, this) }, createContainer: function () { - this.container = L.DomUtil.element( - 'ul', - { className: 'umap-autocomplete' }, - document.body - ) + this.container = L.DomUtil.element({ + tagName: 'ul', + parent: document.body, + className: 'umap-autocomplete', + }) }, resizeContainer: function () { @@ -174,8 +172,11 @@ U.AutoComplete = L.Class.extend({ }, createResult: function (item) { - const el = L.DomUtil.element('li', {}, this.container) - el.textContent = item.label + const el = L.DomUtil.element({ + tagName: 'li', + parent: this.container, + textContent: item.label, + }) const result = { item: item, el: el, @@ -276,15 +277,22 @@ U.AutoComplete.Ajax.SelectMultiple = U.AutoComplete.Ajax.extend({ initSelectedContainer: function () { return L.DomUtil.after( this.input, - L.DomUtil.element('ul', { className: 'umap-multiresult' }) + L.DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' }) ) }, displaySelected: function (result) { - const result_el = L.DomUtil.element('li', {}, this.selected_container) + const result_el = L.DomUtil.element({ + tagName: 'li', + parent: this.selected_container, + }) result_el.textContent = result.item.label - const close = L.DomUtil.element('span', { className: 'close' }, result_el) - close.textContent = '×' + const close = L.DomUtil.element({ + tagName: 'span', + parent: result_el, + className: 'close', + textContent: '×', + }) L.DomEvent.on( close, 'click', @@ -302,15 +310,22 @@ U.AutoComplete.Ajax.Select = U.AutoComplete.Ajax.extend({ initSelectedContainer: function () { return L.DomUtil.after( this.input, - L.DomUtil.element('div', { className: 'umap-singleresult' }) + L.DomUtil.element({ tagName: 'div', className: 'umap-singleresult' }) ) }, displaySelected: function (result) { - const result_el = L.DomUtil.element('div', {}, this.selected_container) + const result_el = L.DomUtil.element({ + tagName: 'div', + parent: this.selected_container, + }) result_el.textContent = result.item.label - const close = L.DomUtil.element('span', { className: 'close' }, result_el) - close.textContent = '×' + const close = L.DomUtil.element({ + tagName: 'span', + parent: result_el, + className: 'close', + textContent: '×', + }) this.input.style.display = 'none' L.DomEvent.on( close, diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 6163f298..2b349c7e 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -669,20 +669,27 @@ const ControlsMixin = { L.DomUtil.createTitle(container, this.options.name, 'icon-caption') this.permissions.addOwnerLink('h5', container) if (this.options.description) { - const description = L.DomUtil.create('div', 'umap-map-description', container) - description.innerHTML = U.Utils.toHTML(this.options.description) + const description = L.DomUtil.element({ + tagName: 'div', + className: 'umap-map-description', + safeHTML: U.Utils.toHTML(this.options.description), + parent: container, + }) } const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container) this.eachVisibleDataLayer((datalayer) => { if (!datalayer.options.inCaption) return const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer), legend = L.DomUtil.create('span', '', p), - headline = L.DomUtil.create('strong', '', p), - description = L.DomUtil.create('span', '', p) + headline = L.DomUtil.create('strong', '', p) datalayer.onceLoaded(function () { datalayer.renderLegend(legend) if (datalayer.options.description) { - description.innerHTML = U.Utils.toHTML(datalayer.options.description) + L.DomUtil.element({ + tagName: 'span', + parent: p, + safeHTML: U.Utils.toHTML(datalayer.options.description), + }) } }) datalayer.renderToolbox(headline) @@ -692,12 +699,11 @@ const ControlsMixin = { credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits')) title = L.DomUtil.add('h5', '', credits, L._('User content credits')) if (this.options.shortCredit || this.options.longCredit) { - L.DomUtil.add( - 'p', - '', - credits, - U.Utils.toHTML(this.options.longCredit || this.options.shortCredit) - ) + L.DomUtil.element({ + tagName: 'p', + parent: credits, + safeHTML: U.Utils.toHTML(this.options.longCredit || this.options.shortCredit), + }) } if (this.options.licence) { const licence = L.DomUtil.add( @@ -718,21 +724,26 @@ const ControlsMixin = { L.DomUtil.create('hr', '', credits) title = L.DomUtil.create('h5', '', credits) title.textContent = L._('Map background credits') - const tilelayerCredit = L.DomUtil.create('p', '', credits), - name = L.DomUtil.create('strong', '', tilelayerCredit), - attribution = L.DomUtil.create('span', '', tilelayerCredit) - name.textContent = `${this.selected_tilelayer.options.name} ` - attribution.innerHTML = this.selected_tilelayer.getAttribution() + const tilelayerCredit = L.DomUtil.create('p', '', credits) + L.DomUtil.element({ + tagName: 'strong', + parent: tilelayerCredit, + textContent: `${this.selected_tilelayer.options.name} `, + }) + L.DomUtil.element({ + tagName: 'span', + parent: tilelayerCredit, + safeHTML: this.selected_tilelayer.getAttribution(), + }) L.DomUtil.create('hr', '', credits) - const umapCredit = L.DomUtil.create('p', '', credits), - urls = { - leaflet: 'http://leafletjs.com', - django: 'https://www.djangoproject.com', - umap: 'http://wiki.openstreetmap.org/wiki/UMap', - changelog: 'https://umap-project.readthedocs.io/en/master/changelog/', - version: this.options.umap_version, - } - umapCredit.innerHTML = L._( + const urls = { + leaflet: 'http://leafletjs.com', + django: 'https://www.djangoproject.com', + umap: 'http://wiki.openstreetmap.org/wiki/UMap', + changelog: 'https://umap-project.readthedocs.io/en/master/changelog/', + version: this.options.umap_version, + } + const creditHTML = L._( ` Powered by Leaflet and Django, @@ -741,6 +752,7 @@ const ControlsMixin = { `, urls ) + L.DomUtil.element({ tagName: 'p', innerHTML: creditHTML, parent: credits }) this.panel.open({ content: container }) }, @@ -1046,16 +1058,16 @@ U.AttributionControl = L.Control.Attribution.extend({ // Use our own container, so we can hide/show on small screens const credits = this._container.innerHTML this._container.innerHTML = '' - const container = L.DomUtil.add( - 'div', - 'attribution-container', - this._container, - credits - ) + const container = L.DomUtil.create('div', 'attribution-container', this._container) + container.innerHTML = credits const shortCredit = this._map.getOption('shortCredit'), captionMenus = this._map.getOption('captionMenus') if (shortCredit) { - L.DomUtil.add('span', '', container, ` — ${U.Utils.toHTML(shortCredit)}`) + L.DomUtil.element({ + tagName: 'span', + parent: container, + safeHTML: ` — ${U.Utils.toHTML(shortCredit)}`, + }) } if (captionMenus) { const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index aae48995..ecf6c077 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -118,19 +118,21 @@ L.DomUtil.createLink = (className, container, content, url, target, title) => { } L.DomUtil.createIcon = (parent, className, title, size = 16) => { - return L.DomUtil.element( - 'i', - { className: `icon icon-${size} ${className}`, title: title || '' }, - parent - ) + return L.DomUtil.element({ + tagName: 'i', + parent: parent, + className: `icon icon-${size} ${className}`, + title: title || '', + }) } L.DomUtil.createButtonIcon = (parent, className, title, size = 16) => { - return L.DomUtil.element( - 'button', - { className: `icon icon-${size} ${className}`, title: title || '' }, - parent - ) + return L.DomUtil.element({ + tagName: 'button', + parent: parent, + className: `icon icon-${size} ${className}`, + title: title || '', + }) } L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => { @@ -163,8 +165,13 @@ L.DomUtil.classIf = (el, className, bool) => { else L.DomUtil.removeClass(el, className) } -L.DomUtil.element = (what, attrs, parent) => { - const el = document.createElement(what) +L.DomUtil.element = ({ tagName, parent, ...attrs }) => { + const el = document.createElement(tagName) + if (attrs.innerHTML) { + attrs.innerHTML = U.Utils.escapeHTML(attrs.innerHTML) + } else if (attrs.safeHTML) { + attrs.innerHTML = attrs.safeHTML + } for (const attr in attrs) { el[attr] = attrs[attr] } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 8f9619e2..4039f2d9 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -757,12 +757,15 @@ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ }, buildLabel: function () { - this.label = L.DomUtil.element('legend', {textContent: this.options.label}) + this.label = L.DomUtil.element({ + tagName: 'legend', + textContent: this.options.label, + }) }, buildLi: function (value) { const property_li = L.DomUtil.create('li', '', this.ul) - const label = L.DomUtil.add('label', '', property_li) + const label = L.DomUtil.create('label', '', property_li) const input = L.DomUtil.create('input', '', label) L.DomUtil.add('span', '', label, value) @@ -800,14 +803,14 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ build: function () { this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) this.container.appendChild(this.label) - const {min, max, type} = this.options.criteria + const { min, max, type } = this.options.criteria this.type = type this.inputType = this.getInputType(this.type) const [minLabel, maxLabel] = this.getLabels() this.minLabel = L.DomUtil.create('label', '', this.container) - this.minLabel.innerHTML = minLabel + this.minLabel.textContent = minLabel this.minInput = L.DomUtil.create('input', '', this.minLabel) this.minInput.type = this.inputType @@ -817,9 +820,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.minInput.dataset.value = min } - this.maxLabel = L.DomUtil.create('label', '', this.container) - this.maxLabel.innerHTML = maxLabel + this.maxLabel.textContent = maxLabel this.maxInput = L.DomUtil.create('input', '', this.maxLabel) this.maxInput.type = this.inputType @@ -834,7 +836,10 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ }, buildLabel: function () { - this.label = L.DomUtil.element('legend', {textContent: this.options.label}) + this.label = L.DomUtil.element({ + tagName: 'legend', + textContent: this.options.label, + }) }, toJS: function () { @@ -974,22 +979,23 @@ L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({ }, buildHelpText: function () { - const datalist = L.DomUtil.create( - 'datalist', - 'umap-field-datalist', - this.getHelpTextParent() - ) - datalist.id = `range-${this.options.label || this.name}` - this.input.setAttribute('list', datalist.id) let options = '' - const step = this.options.step || 1, - digits = step < 1 ? 1 : 0 + const step = this.options.step || 1 + const digits = step < 1 ? 1 : 0 + const id = `range-${this.options.label || this.name}` for (let i = this.options.min; i <= this.options.max; i += this.options.step) { options += `` } - datalist.innerHTML = options + const datalist = L.DomUtil.element({ + tagName: 'datalist', + parent: this.getHelpTextParent(), + className: 'umap-field-datalist', + safeHTML: options, + id: id, + }) + this.input.setAttribute('list', id) L.FormBuilder.Input.prototype.buildHelpText.call(this) }, }) diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js index f6b32a2e..037c8452 100644 --- a/umap/static/umap/js/umap.importer.js +++ b/umap/static/umap/js/umap.importer.js @@ -15,21 +15,24 @@ U.Importer = L.Class.extend({ this.presetBox = L.DomUtil.create('div', 'formbox', this.container) this.presetSelect = L.DomUtil.create('select', '', this.presetBox) this.fileBox = L.DomUtil.create('div', 'formbox', this.container) - this.fileInput = L.DomUtil.element( - 'input', - { type: 'file', multiple: 'multiple', autofocus: true }, - this.fileBox - ) - this.urlInput = L.DomUtil.element( - 'input', - { type: 'text', placeholder: L._('Provide an URL here') }, - this.container - ) - this.rawInput = L.DomUtil.element( - 'textarea', - { placeholder: L._('Paste your data here') }, - this.container - ) + this.fileInput = L.DomUtil.element({ + tagName: 'input', + type: 'file', + parent: this.fileBox, + multiple: 'multiple', + autofocus: true, + }) + this.urlInput = L.DomUtil.element({ + tagName: 'input', + type: 'text', + parent: this.container, + placeholder: L._('Provide an URL here'), + }) + this.rawInput = L.DomUtil.element({ + tagName: 'textarea', + parent: this.container, + placeholder: L._('Paste your data here'), + }) this.typeLabel = L.DomUtil.add( 'label', '', @@ -42,33 +45,43 @@ U.Importer = L.Class.extend({ this.container, L._('Choose the layer to import in') ) - this.clearLabel = L.DomUtil.element( - 'label', - { textContent: L._('Replace layer content'), for: 'datalayer-clear-check' }, - this.container - ) - this.submitInput = L.DomUtil.element( - 'input', - { type: 'button', value: L._('Import'), className: 'button' }, - this.container - ) + this.clearLabel = L.DomUtil.element({ + tagName: 'label', + parent: this.container, + textContent: L._('Replace layer content'), + for: 'datalayer-clear-check', + }) + this.submitInput = L.DomUtil.element({ + tagName: 'input', + type: 'button', + parent: this.container, + value: L._('Import'), + className: 'button', + }) this.map.help.button(this.typeLabel, 'importFormats') - this.typeInput = L.DomUtil.element('select', { name: 'format' }, this.typeLabel) - this.layerInput = L.DomUtil.element( - 'select', - { name: 'datalayer' }, - this.layerLabel - ) - this.clearFlag = L.DomUtil.element( - 'input', - { type: 'checkbox', name: 'clear', id: 'datalayer-clear-check' }, - this.clearLabel - ) - L.DomUtil.element( - 'option', - { value: '', textContent: L._('Choose the data format') }, - this.typeInput - ) + this.typeInput = L.DomUtil.element({ + tagName: 'select', + name: 'format', + parent: this.typeLabel, + }) + this.layerInput = L.DomUtil.element({ + tagName: 'select', + name: 'datalayer', + parent: this.layerLabel, + }) + this.clearFlag = L.DomUtil.element({ + tagName: 'input', + type: 'checkbox', + name: 'clear', + id: 'datalayer-clear-check', + parent: this.clearLabel, + }) + L.DomUtil.element({ + tagName: 'option', + value: '', + textContent: L._('Choose the data format'), + parent: this.typeInput, + }) for (let i = 0; i < this.TYPES.length; i++) { option = L.DomUtil.create('option', '', this.typeInput) option.value = option.textContent = this.TYPES[i] @@ -119,11 +132,12 @@ U.Importer = L.Class.extend({ option.value = id } }) - L.DomUtil.element( - 'option', - { value: '', textContent: L._('Import in a new layer') }, - this.layerInput - ) + L.DomUtil.element({ + tagName: 'option', + value: '', + textContent: L._('Import in a new layer'), + parent: this.layerInput, + }) }) }, diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index dcdd2bfd..663d9528 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -62,9 +62,15 @@ U.MapPermissions = L.Class.extend({ L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key') if (this.isAnonymousMap()) { if (this.options.anonymous_edit_url) { - const helpText = `${L._('Secret edit link:')}
${this.options.anonymous_edit_url - }` - L.DomUtil.add('p', 'help-text', container, helpText) + const helpText = `${L._('Secret edit link:')}
${ + this.options.anonymous_edit_url + }` + L.DomUtil.element({ + tagName: 'p', + className: 'help-text', + innerHTML: helpText, + parent: container, + }) fields.push([ 'options.edit_status', { diff --git a/umap/static/umap/js/umap.popup.js b/umap/static/umap/js/umap.popup.js index 2421c954..b81576a3 100644 --- a/umap/static/umap/js/umap.popup.js +++ b/umap/static/umap/js/umap.popup.js @@ -190,7 +190,11 @@ U.PopupTemplate.Table = U.PopupTemplate.BaseWithTitle.extend({ addRow: function (container, key, value) { const tr = L.DomUtil.create('tr', '', container) L.DomUtil.add('th', '', tr, key) - L.DomUtil.add('td', '', tr, this.formatRow(key, value)) + L.DomUtil.element({ + tagName: 'td', + parent: tr, + innerHTML: this.formatRow(key, value), + }) }, renderBody: function () { @@ -281,11 +285,12 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({ } } if (props.website) { - L.DomUtil.element( - 'a', - { href: props.website, textContent: props.website }, - container - ) + L.DomUtil.element({ + tagName: 'a', + parent: container, + href: props.website, + textContent: props.website, + }) } const phone = props.phone || props['contact:phone'] if (phone) { @@ -293,7 +298,7 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({ 'div', '', container, - L.DomUtil.element('a', { href: `tel:${phone}`, textContent: phone }) + L.DomUtil.element({ tagName: 'a', href: `tel:${phone}`, textContent: phone }) ) } if (props.mobile) { @@ -301,7 +306,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({ 'div', '', container, - L.DomUtil.element('a', { + L.DomUtil.element({ + tagName: 'a', href: `tel:${props.mobile}`, textContent: props.mobile, }) @@ -322,7 +328,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({ 'div', 'osm-link', container, - L.DomUtil.element('a', { + L.DomUtil.element({ + tagName: 'a', href: `https://www.openstreetmap.org/${id}`, textContent: L._('See on OpenStreetMap'), }) diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js index 1c8650ec..459183ac 100644 --- a/umap/static/umap/js/umap.share.js +++ b/umap/static/umap/js/umap.share.js @@ -143,7 +143,7 @@ U.Share = L.Class.extend({ } const iframeExporter = new U.IframeExporter(this.map) const buildIframeCode = () => { - iframe.innerHTML = iframeExporter.build() + iframe.textContent = iframeExporter.build() exportUrl.value = window.location.protocol + iframeExporter.buildUrl() } buildIframeCode() diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index 4f238244..8cadc752 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -51,21 +51,22 @@ U.UI = L.Evented.extend({ close, this ) - L.DomUtil.add('i', 'umap-close-icon', closeButton) + L.DomUtil.create('i', 'umap-close-icon', closeButton) const label = L.DomUtil.create('span', '', closeButton) label.title = label.textContent = L._('Close') - L.DomUtil.add('div', '', this._alert, e.content) + L.DomUtil.element({ tagName: 'div', innerHTML: e.content, parent: this._alert }) if (e.actions) { let action, el, input - const form = L.DomUtil.add('div', 'umap-alert-actions', this._alert) + const form = L.DomUtil.create('div', 'umap-alert-actions', this._alert) for (let i = 0; i < e.actions.length; i++) { action = e.actions[i] if (action.input) { - input = L.DomUtil.element( - 'input', - { className: 'umap-alert-input', placeholder: action.input }, - form - ) + input = L.DomUtil.element({ + tagName: 'input', + parent: form, + className: 'umap-alert-input', + placeholder: action.input, + }) } el = L.DomUtil.createButton( 'umap-action', @@ -97,7 +98,7 @@ U.UI = L.Evented.extend({ this.anchorTooltipAbsolute() } L.DomUtil.addClass(this.parent, 'umap-tooltip') - this._tooltip.innerHTML = opts.content + this._tooltip.innerHTML = U.Utils.escapeHTML(opts.content) } this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0) const id = this.TOOLTIP_ID diff --git a/umap/tests/integration/test_basics.py b/umap/tests/integration/test_basics.py index 206cc785..0bf0e60b 100644 --- a/umap/tests/integration/test_basics.py +++ b/umap/tests/integration/test_basics.py @@ -45,3 +45,24 @@ def test_create_map_with_cursor(page, live_server, tilelayer): "z-index: 200;" ), ) + + +def test_cannot_put_script_tag_in_datalayer_name_or_description( + openmap, live_server, page, tilelayer +): + page.goto(f"{live_server.url}{openmap.get_absolute_url()}") + page.get_by_role("button", name="Edit").click() + page.get_by_role("link", name="Manage layers").click() + page.get_by_role("button", name="Add a layer").click() + page.locator('input[name="name"]').click() + page.locator('input[name="name"]').fill('') + page.locator(".umap-field-description textarea").click() + page.locator(".umap-field-description textarea").fill( + '

before after

' + ) + page.get_by_role("button", name="Save").click() + page.get_by_role("button", name="About").click() + # Title should contain raw HTML (we are using textContent) + expect(page.get_by_text('')).to_be_visible() + # Description should contain escaped HTML + expect(page.get_by_text("before after")).to_be_visible() diff --git a/umap/tests/integration/test_tilelayer.py b/umap/tests/integration/test_tilelayer.py index c6897935..e055d356 100644 --- a/umap/tests/integration/test_tilelayer.py +++ b/umap/tests/integration/test_tilelayer.py @@ -112,3 +112,13 @@ def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page) iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer") # The second of the list should be the current expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern) + + +def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page): + map.settings["properties"]["tilelayer"]["attribution"] = ( + "© [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors" + ) + map.save() + page.goto(f"{live_server.url}{map.get_absolute_url()}") + expect(page.get_by_text("© OpenStreetMap contributors")).to_be_visible() + expect(page.get_by_role("link", name="OpenStreetMap")).to_be_visible()