Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,31 @@ The active mode for each category is also exposed as ``filter_mode`` in the
``/api/categories-full`` response, so a custom frontend can render the
right control (checkbox vs. radio) without hardcoding category names.

Photo Uploads
~~~~~~~~~~~~~

Users can attach a photo when suggesting a new location. By default, Goodmap
accepts JPEG photos up to 5 MiB. To allow other formats or change the size
limit, set the ``ATTACHMENT:`` key in your configuration file (see
`platzky's AttachmentConfig
<https://platzky.readthedocs.io/en/latest/api.html#platzky.config.AttachmentConfig>`_):

.. code-block:: yaml

ATTACHMENT:
allowed_mime_types: ["image/jpeg", "image/png"]
allowed_extensions: ["jpg", "jpeg", "png"]
max_size: 8388608 # 8 MiB

Omit ``ATTACHMENT:`` to keep the default (JPEG only, 5 MiB).

A photo in an unsupported format is rejected with an error message asking the
user to pick a different file. A photo in an allowed format that exceeds the
size limit is automatically compressed in the browser before upload; the user
is warned that this may reduce image quality. If the photo still exceeds the
limit after compression, it is rejected. The server enforces the same limits
on upload, independently of the browser-side checks.

.. _data-model-visible_data:

Database Types
Expand Down
107 changes: 106 additions & 1 deletion e2e-tests/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions e2e-tests/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ruff = "^0.8.4" # Linting and formatting
black = "^26.3.1" # Code formatting
babel = "^2.17.0" # For compiling translation files
pip-audit = "^2.10.1" # Dependency vulnerability scanning
pillow = "^12.3.0"

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
107 changes: 107 additions & 0 deletions e2e-tests/tests/basic/test_suggest_new_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Suggest New Point Tests

Tests the "suggest a new point" dialog's validation and error feedback.
"""

import io

from PIL import Image
from playwright.sync_api import Page, expect

from tests.conftest import BASE_URL


def _open_suggest_new_point_dialog(page: Page):
suggest_button = page.locator('[data-testid="suggest-new-point"]')
expect(suggest_button).to_have_css("opacity", "1", timeout=5000)
suggest_button.click()

# Matched by name: #left-panel also has role="dialog".
dialog = page.get_by_role("dialog", name="Suggest a New Point")
expect(dialog).to_be_visible()
return dialog


def _upload_tall_photo(page: Page) -> None:
"""
Attaches a 200x3000px JPEG, tall enough that the dialog overflows and scrolls.
"""
buffer = io.BytesIO()
Image.new("RGB", (200, 3000), "#3366ff").save(buffer, format="JPEG")

page.locator('[data-testid="photo-of-point"]').set_input_files(
files=[{"name": "tall-photo.jpg", "mimeType": "image/jpeg", "buffer": buffer.getvalue()}]
)


class TestSuggestNewPointValidation:
"""Test suite for the suggest-new-point dialog's inline validation feedback"""

def test_submitting_empty_required_fields_shows_inline_error(self, page: Page, geolocation):
"""
Submitting with required fields empty must show a visible, in-dialog error
and must not submit the form.
"""
geolocation(51.10655, 17.0555) # Wroclaw
page.goto(BASE_URL, wait_until="domcontentloaded")

dialog = _open_suggest_new_point_dialog(page)
dialog.get_by_role("button", name="Submit").click()

alert = dialog.get_by_role("alert")
expect(alert).to_be_visible(timeout=5000)
expect(alert).to_contain_text("Please fill in required fields")

expect(dialog).to_be_visible()

def test_error_banner_clears_when_dialog_is_reopened(self, page: Page, geolocation):
"""
A validation error from a previous attempt must not persist into a fresh
dialog session after cancel + reopen.
"""
geolocation(51.10655, 17.0555)
page.goto(BASE_URL, wait_until="domcontentloaded")

dialog = _open_suggest_new_point_dialog(page)
dialog.get_by_role("button", name="Submit").click()
expect(dialog.get_by_role("alert")).to_be_visible(timeout=5000)

dialog.get_by_role("button", name="Cancel").click()
expect(dialog).not_to_be_visible()

dialog = _open_suggest_new_point_dialog(page)
expect(dialog.get_by_role("alert")).not_to_be_visible()

def test_error_is_scrolled_into_view_when_dialog_content_is_tall(self, page: Page, geolocation):
"""
When a tall photo makes the dialog scrollable and the user has scrolled down,
submitting with required fields empty must scroll the error back into view,
not just render it in the DOM above the current scroll position.
"""
geolocation(51.10655, 17.0555)
page.goto(BASE_URL, wait_until="domcontentloaded")

dialog = _open_suggest_new_point_dialog(page)
_upload_tall_photo(page)

# Wait for the decoded image, not just the tag: the element reports a
# non-final height until decoding completes, racing the scroll below.
page.wait_for_function("""
() => {
const img = document.querySelector('img[alt="Selected"]');
return img && img.complete && img.naturalHeight > 1000;
}
""")

# The role="dialog" element is the scroll container itself.
dialog.evaluate("el => { el.scrollTop = el.scrollHeight; }")
scroll_top = dialog.evaluate("el => el.scrollTop")
assert scroll_top > 0, "Dialog did not scroll - test setup is broken"

dialog.get_by_role("button", name="Submit").click()

alert = dialog.get_by_role("alert")
expect(alert).to_contain_text("Please fill in required fields")
# Timeout covers the smooth-scroll animation settling.
expect(alert).to_be_in_viewport(timeout=8000)
Loading
Loading