diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css
index 2a12c8779..f13cf1105 100644
--- a/umap/static/umap/base.css
+++ b/umap/static/umap/base.css
@@ -194,6 +194,7 @@ input[type="submit"] {
.dark a {
color: #eeeeec;
}
+button.flat,
[type="button"].flat,
.dark [type="button"].flat {
border: none;
@@ -536,9 +537,30 @@ i.info {
margin-top: -8px;
padding: 0 5px;
}
-.umap-pictogram-grid {
+.pictogram-tabs {
display: flex;
- flex-wrap: wrap;
+ justify-content: space-around;
+ font-size: 1.2em;
+ padding-bottom: 20px;
+}
+.pictogram-tabs button {
+ padding: 10px;
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer;
+}
+.pictogram-tabs .on {
+ font-weight: bold;
+ border-bottom: 1px solid #fff;
+}
+.umap-pictogram-category h6 {
+ font-size: 1.3em;
+}
+.umap-pictogram-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, 30px);
+ justify-content: space-between;
+ grid-gap: 5px;
}
.umap-pictogram-choice {
width: 30px;
@@ -548,17 +570,20 @@ i.info {
background-color: #999;
text-align: center;
margin-bottom: 5px;
- margin-right: 5px;
+ display: block;
}
.umap-pictogram-choice img {
vertical-align: middle;
max-width: 24px;
}
.umap-pictogram-choice:hover,
-.umap-pictogram-choice.selected,
.umap-color-picker span:hover {
- box-shadow: 0 0 4px 0 black;
+ background-color: #bebebe;
}
+.umap-pictogram-choice.selected {
+ box-shadow: inset 0 0 0 1px #e9e9e9;
+}
+
.umap-pictogram-choice .leaflet-marker-icon {
bottom: 0;
left: 30px;
diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js
index f392322ac..391ec2e64 100644
--- a/umap/static/umap/js/umap.core.js
+++ b/umap/static/umap/js/umap.core.js
@@ -287,6 +287,13 @@ L.Util.copyToClipboard = function (textToCopy) {
}
}
+L.Util.normalize = function (s) {
+ return (s || '')
+ .toLowerCase()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+}
+
L.DomUtil.add = (tagName, className, container, content) => {
const el = L.DomUtil.create(tagName, className, container)
if (content) {
diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js
index 20cd99b3e..2aa84e768 100644
--- a/umap/static/umap/js/umap.forms.js
+++ b/umap/static/umap/js/umap.forms.js
@@ -1,4 +1,10 @@
L.FormBuilder.Element.include({
+ undefine: function () {
+ L.DomUtil.addClass(this.wrapper, 'undefined')
+ this.clear()
+ this.sync()
+ },
+
getParentNode: function () {
if (this.options.wrapper) {
return L.DomUtil.create(
@@ -29,15 +35,10 @@ L.FormBuilder.Element.include({
},
this
)
- L.DomEvent.on(
+ L.DomEvent.on(undefine, 'click', L.DomEvent.stop).on(
undefine,
'click',
- function (e) {
- L.DomEvent.stop(e)
- L.DomUtil.addClass(this.wrapper, 'undefined')
- this.clear()
- this.sync()
- },
+ this.undefine,
this
)
}
@@ -524,48 +525,111 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
build: function () {
L.FormBuilder.BlurInput.prototype.build.call(this)
- // Try to guess if the icon content has been customized, and if yes
- // directly display the field
- this.input.type = this.value() && !this.value().startsWith('/') ? 'text' : 'hidden'
- this.input.placeholder = L._('Symbol or url')
- this.buttonsContainer = L.DomUtil.create('div', '')
- this.pictogramsContainer = L.DomUtil.create('div', 'umap-pictogram-list')
- L.DomUtil.before(this.input, this.buttonsContainer)
- L.DomUtil.before(this.input, this.pictogramsContainer)
+ this.buttons = L.DomUtil.create('div', '', this.parentNode)
+ this.tabs = L.DomUtil.create('div', 'pictogram-tabs', this.parentNode)
+ this.body = L.DomUtil.create('div', 'umap-pictogram-body', this.parentNode)
+ this.footer = L.DomUtil.create('div', '', this.parentNode)
this.udpatePreview()
- this.on('define', this.fetchIconList)
+ this.on('define', this.onDefine)
+ },
+
+ onDefine: function () {
+ this.buttons.innerHTML = ''
+ this.footer.innerHTML = ''
+ this.buildTabs()
+ const value = this.value()
+ if (!value || value.startsWith('/')) this.showSymbolsTab()
+ else if (value.startsWith('http')) this.showURLTab()
+ else this.showCharsTab()
+ const closeButton = L.DomUtil.createButton(
+ 'button action-button',
+ this.footer,
+ L._('Close'),
+ function (e) {
+ this.body.innerHTML = ''
+ this.tabs.innerHTML = ''
+ this.footer.innerHTML = ''
+ if (this.isDefault()) this.undefine(e)
+ else this.udpatePreview()
+ },
+ this
+ )
+ },
+
+ buildTabs: function () {
+ this.tabs.innerHTML = ''
+ const symbol = L.DomUtil.add(
+ 'button',
+ 'flat tab-symbols',
+ this.tabs,
+ L._('Symbol')
+ ),
+ char = L.DomUtil.add(
+ 'button',
+ 'flat tab-chars',
+ this.tabs,
+ L._('Emoji & Character')
+ )
+ url = L.DomUtil.add('button', 'flat tab-url', this.tabs, L._('URL'))
+ L.DomEvent.on(symbol, 'click', L.DomEvent.stop).on(
+ symbol,
+ 'click',
+ this.showSymbolsTab,
+ this
+ )
+ L.DomEvent.on(char, 'click', L.DomEvent.stop).on(
+ char,
+ 'click',
+ this.showCharsTab,
+ this
+ )
+ L.DomEvent.on(url, 'click', L.DomEvent.stop).on(url, 'click', this.showURLTab, this)
+ },
+
+ openTab: function (name) {
+ const els = this.tabs.querySelectorAll('button')
+ for (let el of els) {
+ L.DomUtil.removeClass(el, 'on')
+ }
+ let el = this.tabs.querySelector(`.tab-${name}`)
+ L.DomUtil.addClass(el, 'on')
+ this.body.innerHTML = ''
+ },
+
+ isPath: function () {
+ const value = this.value()
+ return value && value.length && value.startsWith('/')
+ },
+
+ isRemoteUrl: function () {
+ const value = this.value()
+ return value && value.length && value.startsWith('http')
},
- isUrl: function () {
- return this.value() && this.value().indexOf('/') !== -1
+ isImg: function () {
+ return this.isPath() || this.isRemoteUrl()
},
udpatePreview: function () {
+ this.buttons.innerHTML = ''
+ if (this.isDefault()) return
if (!L.Util.hasVar(this.value())) {
// Do not try to render URL with variables
- if (this.isUrl()) {
- const img = L.DomUtil.create(
- 'img',
- '',
- L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer)
- )
+ const box = L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons)
+ L.DomEvent.on(box, 'click', this.onDefine, this)
+ if (this.isImg()) {
+ const img = L.DomUtil.create('img', '', box)
img.src = this.value()
- L.DomEvent.on(img, 'click', this.fetchIconList, this)
} else {
- const el = L.DomUtil.create(
- 'span',
- '',
- L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer)
- )
+ const el = L.DomUtil.create('span', '', box)
el.textContent = this.value()
- L.DomEvent.on(el, 'click', this.fetchIconList, this)
}
}
this.button = L.DomUtil.createButton(
'button action-button',
- this.buttonsContainer,
+ this.buttons,
this.value() ? L._('Change') : L._('Add'),
- this.fetchIconList,
+ this.onDefine,
this
)
},
@@ -573,64 +637,54 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
addIconPreview: function (pictogram, parent) {
const baseClass = 'umap-pictogram-choice',
value = pictogram.src,
- className = value === this.value() ? `${baseClass} selected` : baseClass,
+ search = L.Util.normalize(this.searchInput.value),
+ title = pictogram.attribution
+ ? `${pictogram.name} — © ${pictogram.attribution}`
+ : pictogram.name
+ if (search && L.Util.normalize(title).indexOf(search) === -1) return
+ const className = value === this.value() ? `${baseClass} selected` : baseClass,
container = L.DomUtil.create('div', className, parent),
img = L.DomUtil.create('img', '', container)
img.src = value
- if (pictogram.name && pictogram.attribution) {
- container.title = `${pictogram.name} — © ${pictogram.attribution}`
- }
+ container.title = title
L.DomEvent.on(
container,
'click',
function (e) {
this.input.value = value
this.sync()
- this.unselectAll(this.pictogramsContainer)
+ this.unselectAll(this.grid)
L.DomUtil.addClass(container, 'selected')
},
this
)
+ return true // Icon has been added (not filtered)
},
clear: function () {
this.input.value = ''
- this.unselectAll(this.pictogramsContainer)
+ this.unselectAll(this.body)
this.sync()
- this.pictogramsContainer.innerHTML = ''
+ this.body.innerHTML = ''
this.udpatePreview()
},
- search: function (e) {
- const icons = [...this.parentNode.querySelectorAll('.umap-pictogram-choice')],
- search = this.searchInput.value.toLowerCase()
- icons.forEach((el) => {
- if (el.title.toLowerCase().indexOf(search) != -1) el.style.display = 'block'
- else el.style.display = 'none'
- })
- },
-
addCategory: function (category, items) {
- const parent = L.DomUtil.create(
- 'div',
- 'umap-pictogram-category',
- this.pictogramsContainer
- ),
+ const parent = L.DomUtil.create('div', 'umap-pictogram-category'),
title = L.DomUtil.add('h6', '', parent, category),
grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent)
+ let status = false
for (let item of items) {
- this.addIconPreview(item, grid)
+ status = this.addIconPreview(item, grid) || status
}
+ if (status) this.grid.appendChild(parent)
},
- buildIconList: function (data) {
- this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer)
- this.searchInput.type = 'search'
- this.searchInput.placeholder = L._('Search')
- L.DomEvent.on(this.searchInput, 'input', this.search, this)
+ buildSymbolsList: function () {
+ this.grid.innerHTML = ''
const categories = {}
let category
- for (const props of data.pictogram_list) {
+ for (const props of this.pictogram_list) {
category = props.category || L._('Generic')
categories[category] = categories[category] || []
categories[category].push(props)
@@ -641,39 +695,60 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
for (let [category, items] of sorted) {
this.addCategory(category, items)
}
- const closeButton = L.DomUtil.createButton(
- 'button action-button',
- this.pictogramsContainer,
- L._('Close'),
- function (e) {
- this.pictogramsContainer.innerHTML = ''
- this.udpatePreview()
- },
- this
- )
- closeButton.style.display = 'block'
- closeButton.style.clear = 'both'
+ },
- const customButton = L.DomUtil.createButton(
- 'flat',
- this.pictogramsContainer,
- L._('Toggle direct input (advanced)'),
- function (e) {
- this.input.type = this.input.type === 'text' ? 'hidden' : 'text'
- },
- this
- )
- this.builder.map.help.button(customButton, 'formatIconSymbol')
+ isDefault: function () {
+ return !this.value() || this.value() === this.obj.getMap().options.default_iconUrl
+ },
+
+ showSymbolsTab: function () {
+ this.openTab('symbols')
+ this.searchInput = L.DomUtil.create('input', '', this.body)
+ this.searchInput.type = 'search'
+ this.searchInput.placeholder = L._('Search')
+ this.grid = L.DomUtil.create('div', '', this.body)
+ L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this)
+ if (this.pictogram_list) {
+ this.buildSymbolsList()
+ } else {
+ this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, {
+ callback: (data) => {
+ this.pictogram_list = data.pictogram_list
+ this.buildSymbolsList()
+ },
+ context: this,
+ })
+ }
+ },
+
+ showCharsTab: function () {
+ this.openTab('chars')
+ const value = !this.isImg() ? this.value() : null
+ const input = this.buildInput(this.body, value)
+ input.placeholder = L._('Type char or paste emoji')
+ input.type = 'text'
+ },
+
+ showURLTab: function () {
+ this.openTab('url')
+ const value = this.isRemoteUrl() ? this.value() : null
+ const input = this.buildInput(this.body, value)
+ input.placeholder = L._('Add image URL')
+ input.type = 'url'
},
- fetchIconList: function (e) {
- // Clean parent element before calling ajax, to prevent blinking
- this.pictogramsContainer.innerHTML = ''
- this.buttonsContainer.innerHTML = ''
- this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, {
- callback: this.buildIconList,
- context: this,
+ buildInput: function (parent, value) {
+ const input = L.DomUtil.create('input', 'blur', parent)
+ const button = L.DomUtil.create('span', 'button blur-button', parent)
+ if (value) input.value = value
+ L.DomEvent.on(input, 'blur', () => {
+ // Do not clear this.input when focus-blur
+ // empty input
+ if (input.value === value) return
+ this.input.value = input.value
+ this.sync()
})
+ return input
},
unselectAll: function (container) {
diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css
index 69e59440f..434faae4c 100644
--- a/umap/static/umap/map.css
+++ b/umap/static/umap/map.css
@@ -1298,6 +1298,7 @@ a.add-datalayer:hover,
vertical-align: middle;
color: white;
font-weight: bold;
+ font-size: 1.2rem;
}
.umap-circle-icon {
border: 1px solid white;
diff --git a/umap/static/umap/test/Util.js b/umap/static/umap/test/Util.js
index 07d2b2847..3449fc40d 100644
--- a/umap/static/umap/test/Util.js
+++ b/umap/static/umap/test/Util.js
@@ -475,6 +475,16 @@ describe('L.Util', function () {
})
})
+ describe("#normalize()", function () {
+
+ if('should remove accents', function () {
+ // French é
+ assert.equal(L.Util.normalize('aéroport'), 'aeroport')
+ // American é
+ assert.equal(L.Util.normalize('aéroport'), 'aeroport')
+ })
+ })
+
describe("#sortFeatures()", function () {
let feat1, feat2, feat3
before(function () {
diff --git a/umap/tests/conftest.py b/umap/tests/conftest.py
index 5cbe70318..249f58552 100644
--- a/umap/tests/conftest.py
+++ b/umap/tests/conftest.py
@@ -24,11 +24,8 @@ def pytest_configure(config):
settings.MEDIA_ROOT = TMP_ROOT
-def pytest_unconfigure(config):
- shutil.rmtree(TMP_ROOT, ignore_errors=True)
-
-
def pytest_runtest_teardown():
+ shutil.rmtree(TMP_ROOT, ignore_errors=True)
cache.clear()
diff --git a/umap/tests/fixtures/circle.svg b/umap/tests/fixtures/circle.svg
new file mode 100644
index 000000000..9b579a760
--- /dev/null
+++ b/umap/tests/fixtures/circle.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/umap/tests/fixtures/star.svg b/umap/tests/fixtures/star.svg
new file mode 100644
index 000000000..2b6f51694
--- /dev/null
+++ b/umap/tests/fixtures/star.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py
new file mode 100644
index 000000000..5a960f204
--- /dev/null
+++ b/umap/tests/integration/test_picto.py
@@ -0,0 +1,217 @@
+from pathlib import Path
+
+import pytest
+from playwright.sync_api import expect
+from django.core.files.base import ContentFile
+
+from umap.models import Map, Pictogram
+
+from ..base import DataLayerFactory
+
+pytestmark = pytest.mark.django_db
+
+
+DATALAYER_DATA = {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [13.68896484375, 48.55297816440071],
+ },
+ "properties": {"_umap_options": {"color": "DarkCyan"}, "name": "Here"},
+ }
+ ],
+ "_umap_options": {"displayOnLoad": True, "name": "FooBarFoo"},
+}
+FIXTURES = Path(__file__).parent.parent / "fixtures"
+
+
+@pytest.fixture
+def pictos():
+ path = FIXTURES / "star.svg"
+ Pictogram(name="star", pictogram=ContentFile(path.read_text(), path.name)).save()
+ path = FIXTURES / "circle.svg"
+ Pictogram(name="circle", pictogram=ContentFile(path.read_text(), path.name)).save()
+
+
+def test_can_change_picto_at_map_level(map, live_server, page, pictos):
+ # Faster than doing a login
+ map.edit_status = Map.ANONYMOUS
+ map.save()
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
+ page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
+ marker = page.locator(".umap-div-icon img")
+ expect(marker).to_have_count(1)
+ # Should have default img
+ expect(marker).to_have_attribute("src", "/static/umap/img/marker.png")
+ edit_settings = page.get_by_title("Edit map settings")
+ expect(edit_settings).to_be_visible()
+ edit_settings.click()
+ shape_settings = page.get_by_text("Default shape properties")
+ expect(shape_settings).to_be_visible()
+ shape_settings.click()
+ define = page.locator(".umap-field-iconUrl .define")
+ undefine = page.locator(".umap-field-iconUrl .undefine")
+ expect(define).to_be_visible()
+ expect(undefine).to_be_hidden()
+ define.click()
+ symbols = page.locator(".umap-pictogram-choice")
+ expect(symbols).to_have_count(2)
+ search = page.locator(".umap-pictogram-body input")
+ search.type("star")
+ expect(symbols).to_have_count(1)
+ symbols.click()
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
+ undefine.click()
+ expect(marker).to_have_attribute("src", "/static/umap/img/marker.png")
+
+
+def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos):
+ # Faster than doing a login
+ map.edit_status = Map.ANONYMOUS
+ map.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg"
+ map.save()
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
+ page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
+ marker = page.locator(".umap-div-icon img")
+ expect(marker).to_have_count(1)
+ # Should have default img
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
+ # Edit datalayer
+ marker.click(modifiers=["Control", "Shift"])
+ settings = page.get_by_text("Layer properties")
+ expect(settings).to_be_visible()
+ shape_settings = page.get_by_text("Shape properties")
+ expect(shape_settings).to_be_visible()
+ shape_settings.click()
+ define = page.locator(".umap-field-iconUrl .define")
+ undefine = page.locator(".umap-field-iconUrl .undefine")
+ expect(define).to_be_visible()
+ expect(undefine).to_be_hidden()
+ define.click()
+ symbols = page.locator(".umap-pictogram-choice")
+ expect(symbols).to_have_count(2)
+ search = page.locator(".umap-pictogram-body input")
+ search.type("circle")
+ expect(symbols).to_have_count(1)
+ symbols.click()
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg")
+ undefine.click()
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
+
+
+def test_can_change_picto_at_marker_level(map, live_server, page, pictos):
+ # Faster than doing a login
+ map.edit_status = Map.ANONYMOUS
+ map.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg"
+ map.save()
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
+ page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
+ marker = page.locator(".umap-div-icon img")
+ expect(marker).to_have_count(1)
+ # Should have default img
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
+ # Edit marker
+ marker.click(modifiers=["Shift"])
+ settings = page.get_by_text("Feature properties")
+ expect(settings).to_be_visible()
+ shape_settings = page.get_by_text("Shape properties")
+ expect(shape_settings).to_be_visible()
+ shape_settings.click()
+ define = page.locator(".umap-field-iconUrl .define")
+ undefine = page.locator(".umap-field-iconUrl .undefine")
+ expect(define).to_be_visible()
+ expect(undefine).to_be_hidden()
+ define.click()
+ symbols = page.locator(".umap-pictogram-choice")
+ expect(symbols).to_have_count(2)
+ search = page.locator(".umap-pictogram-body input")
+ search.type("circle")
+ expect(symbols).to_have_count(1)
+ symbols.click()
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg")
+ undefine.click()
+ expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
+
+
+def test_can_use_remote_url_as_picto(map, live_server, page, pictos):
+ # Faster than doing a login
+ map.edit_status = Map.ANONYMOUS
+ map.save()
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
+ page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
+ marker = page.locator(".umap-div-icon img")
+ expect(marker).to_have_count(1)
+ # Should have default img
+ expect(marker).to_have_attribute("src", "/static/umap/img/marker.png")
+ edit_settings = page.get_by_title("Edit map settings")
+ expect(edit_settings).to_be_visible()
+ edit_settings.click()
+ shape_settings = page.get_by_text("Default shape properties")
+ expect(shape_settings).to_be_visible()
+ shape_settings.click()
+ define = page.locator(".umap-field-iconUrl .define")
+ expect(define).to_be_visible()
+ define.click()
+ url_tab = page.get_by_role("button", name="URL")
+ input_el = page.get_by_placeholder("Add image URL")
+ expect(input_el).to_be_hidden()
+ expect(url_tab).to_be_visible()
+ url_tab.click()
+ expect(input_el).to_be_visible()
+ input_el.fill("https://foo.bar/img.jpg")
+ input_el.blur()
+ expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg")
+ # Now close and reopen the form, it should still be the URL tab
+ close = page.locator("#umap-ui-container").get_by_title("Close")
+ expect(close).to_be_visible()
+ close.click()
+ edit_settings.click()
+ shape_settings.click()
+ modify = page.locator(".umap-field-iconUrl").get_by_text("Change")
+ expect(modify).to_be_visible()
+ modify.click()
+ # Should be on URL tab
+ expect(input_el).to_be_visible()
+
+
+def test_can_use_char_as_picto(map, live_server, page, pictos):
+ # Faster than doing a login
+ map.edit_status = Map.ANONYMOUS
+ map.save()
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
+ page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
+ marker = page.locator(".umap-div-icon span")
+ # Should have default img, so not a span
+ expect(marker).to_have_count(0)
+ edit_settings = page.get_by_title("Edit map settings")
+ expect(edit_settings).to_be_visible()
+ edit_settings.click()
+ shape_settings = page.get_by_text("Default shape properties")
+ expect(shape_settings).to_be_visible()
+ shape_settings.click()
+ define = page.locator(".umap-field-iconUrl .define")
+ define.click()
+ url_tab = page.get_by_role("button", name="Emoji & Character")
+ input_el = page.get_by_placeholder("Type char or paste emoji")
+ expect(input_el).to_be_hidden()
+ expect(url_tab).to_be_visible()
+ url_tab.click()
+ expect(input_el).to_be_visible()
+ input_el.fill("♩")
+ input_el.blur()
+ expect(marker).to_have_count(1)
+ expect(marker).to_have_text("♩")
+ # Now close and reopen the form, it should still be the URL tab
+ close = page.locator("#umap-ui-container").get_by_title("Close")
+ expect(close).to_be_visible()
+ close.click()
+ edit_settings.click()
+ shape_settings.click()
+ preview = page.locator(".umap-pictogram-choice")
+ expect(preview).to_be_visible()
+ preview.click()
+ # Should be on URL tab
+ expect(input_el).to_be_visible()