Skip to content
Merged
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GOODMAP_PATH=/path/to/your/goodmap
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ __pycache__/
# Compiled translation files (generated from .po files)
translations/**/messages.mo

*~

CLAUDE.md
.playwright-mcp
.plans
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-include .env

CONFIG_PATH ?= e2e_test_config.yml
STRESS_CONFIG_PATH ?= e2e_stress_test_config.yml
GOODMAP_PATH ?= .
PYTEST_SPEC ?= tests/

Expand Down Expand Up @@ -33,6 +34,9 @@ e2e-stress-tests:
run-e2e-env:
poetry --project '$(GOODMAP_PATH)' run flask --app "goodmap.goodmap:create_app(config_path='$(CONFIG_PATH)')" --debug run

run-e2e-stress-env:
poetry --project '$(GOODMAP_PATH)' run flask --app "goodmap.goodmap:create_app(config_path='$(STRESS_CONFIG_PATH)')" --debug run

compile-translations:
poetry run pybabel compile -d translations

2 changes: 1 addition & 1 deletion e2e_test_config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ USE_WWW: False
TRANSLATION_DIRECTORIES: [__E2E_TESTS_DIR__/translations]
DB:
TYPE: json_file
PATH: e2e_test_data.json
PATH: __E2E_TESTS_DIR__/e2e_test_data.json

#TODO this should not be necessary argument
LANGUAGES:
Expand Down
5 changes: 5 additions & 0 deletions e2e_test_data_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
"str"
]
],
"reported_issue_types": [
"under construction",
"has a hole",
"other"
],
"categories": {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"accessible_by": [
"bikes",
Expand Down
23 changes: 13 additions & 10 deletions tests/basic/test_left_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def test_panel_opens_on_toggle_click(self, mobile_page: Page):
expect(filter_dialog).to_be_visible(timeout=5000)

# Filter form should be visible inside dialog
filter_form = mobile_page.locator('#filter-form')
filter_form = mobile_page.locator("#filter-form")
expect(filter_form).to_be_visible()

@pytest.mark.parametrize("mobile_page", ["iphone-6"], indirect=True)
Expand Down Expand Up @@ -318,15 +318,15 @@ def test_filter_text_not_truncated_on_tablet(self, page: Page):

# Check that filter category headers are fully visible
# Look for specific text that was previously truncated
panel_text = page.evaluate(
"() => document.querySelector('#left-panel').textContent"
)
panel_text = page.evaluate("() => document.querySelector('#left-panel').textContent")

# These should be fully visible, not truncated
assert "type_of_place" in panel_text or "type of place" in panel_text.lower(), \
"type_of_place should be fully visible"
assert "accessible_by" in panel_text or "accessible by" in panel_text.lower(), \
"accessible_by should be fully visible"
assert (
"type_of_place" in panel_text or "type of place" in panel_text.lower()
), "type_of_place should be fully visible"
assert (
"accessible_by" in panel_text or "accessible by" in panel_text.lower()
), "accessible_by should be fully visible"


class TestLeftPanelFilterHelpers:
Expand Down Expand Up @@ -376,8 +376,11 @@ def test_panel_has_custom_scrollbar_styling(self, page: Page):
# Chromium uses ::-webkit-scrollbar (can't easily check via JS)
# Just verify the property is set (may be empty string in Chromium)
# This test mainly verifies the CSS is being applied
assert scrollbar_width in ["thin", "auto", ""], \
f"Unexpected scrollbar-width value: {scrollbar_width}"
assert scrollbar_width in [
"thin",
"auto",
"",
], f"Unexpected scrollbar-width value: {scrollbar_width}"

def test_offcanvas_body_has_overflow_auto(self, page: Page):
"""
Expand Down
13 changes: 1 addition & 12 deletions tests/basic/test_location_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ def test_buttons_respond_to_granted_permission_on_load(self, page: Page, geoloca
expect(location_button).to_have_css("opacity", "1", timeout=5000)
expect(location_button).to_have_css("filter", "none")

def test_buttons_show_disabled_when_permission_denied_on_load(self, browser, webpack_script):
def test_buttons_show_disabled_when_permission_denied_on_load(self, browser):
"""
Verify that when geolocation permission is denied/not granted,
buttons show disabled state on page load.
"""
from tests.conftest import WEBPACK_SCRIPT_URL

# Mock geolocation API to simulate permission denied - add at context level
# so it runs before any page script
geolocation_denied_script = """
Expand Down Expand Up @@ -76,18 +74,9 @@ def test_buttons_show_disabled_when_permission_denied_on_load(self, browser, web

page = context.new_page()

# Setup webpack route interception
def handle_webpack_route(route):
route.fulfill(
status=200,
content_type="application/javascript; charset=utf-8",
body=webpack_script,
)

def block_hmr_route(route):
route.abort()

page.route(WEBPACK_SCRIPT_URL, handle_webpack_route)
page.route("**/ws", block_hmr_route)
page.route("**/*.hot-update.*", block_hmr_route)

Expand Down
16 changes: 10 additions & 6 deletions tests/basic/test_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ def test_should_not_have_scrollbars(self, page: Page):
)

# Assert no scrollbars (scroll dimensions should not exceed viewport)
assert (
dimensions["scrollWidth"] <= dimensions["innerWidth"]
), f"Horizontal scrollbar detected: scrollWidth={dimensions['scrollWidth']}, innerWidth={dimensions['innerWidth']}"
assert dimensions["scrollWidth"] <= dimensions["innerWidth"], (
f"Horizontal scrollbar detected: "
f"scrollWidth={dimensions['scrollWidth']}, "
f"innerWidth={dimensions['innerWidth']}"
)

assert (
dimensions["scrollHeight"] <= dimensions["innerHeight"]
), f"Vertical scrollbar detected: scrollHeight={dimensions['scrollHeight']}, innerHeight={dimensions['innerHeight']}"
assert dimensions["scrollHeight"] <= dimensions["innerHeight"], (
f"Vertical scrollbar detected: "
f"scrollHeight={dimensions['scrollHeight']}, "
f"innerHeight={dimensions['innerHeight']}"
)

def test_filter_checkbox_filters_markers(self, page: Page):
"""Verify clicking filter checkbox actually filters the markers on the map"""
Expand Down
179 changes: 179 additions & 0 deletions tests/basic/test_share.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""
Share Feature Tests

Tests the share button functionality in marker popups:
- Desktop: copies a ?locationId=<uuid> link to clipboard and shows a toast
- Mobile: triggers the Web Share API (navigator.share())
- Shared link: visiting ?locationId=<uuid> auto-opens the popup for that location
"""

import pytest
from playwright.sync_api import Page, expect

from tests.conftest import ALL_MOBILE_DEVICES, BASE_URL, MARKER_LOAD_TIMEOUT


class TestShareOnDesktop:
"""Test suite for share button functionality on desktop"""

def test_share_button_copies_link_to_clipboard(self, page: Page):
"""
Verify clicking the share button copies a locationId link to clipboard
and shows a toast notification.
"""
page.goto(BASE_URL, wait_until="domcontentloaded")

# Grant clipboard permissions
page.context.grant_permissions(["clipboard-read", "clipboard-write"])

# Click first marker to trigger cluster expansion
first_marker = page.locator(".leaflet-marker-icon").first
first_marker.click()

# Wait for markers to appear (should be 2 after cluster expansion)
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(2, timeout=MARKER_LOAD_TIMEOUT)

# Click the rightmost marker
page.evaluate(
"""
() => {
const markers = document.querySelectorAll('.leaflet-marker-icon');
let rightmostMarker = null;
let maxX = -Infinity;

markers.forEach(marker => {
const rect = marker.getBoundingClientRect();
if (rect.x > maxX) {
maxX = rect.x;
rightmostMarker = marker;
}
});

if (rightmostMarker) {
rightmostMarker.click();
}
}
"""
)

# Verify popup is visible
popup = page.locator(".leaflet-popup-content")
expect(popup).to_be_visible()

# Click the share button
share_button = page.get_by_role("button", name="share")
expect(share_button).to_be_visible()
share_button.click()

# Verify toast notification appears
toast = page.get_by_role("status")
expect(toast).to_contain_text("Link copied to clipboard")

# Verify clipboard contains URL with ?locationId=
clipboard_text = page.evaluate("() => navigator.clipboard.readText()")
assert "?locationId=" in clipboard_text

def test_shared_link_opens_popup_with_correct_content(self, page: Page):
"""
Verify navigating to a URL with ?locationId= auto-opens the popup
with the correct location content.
"""
page.goto(f"{BASE_URL}/?locationId=dattarro", wait_until="domcontentloaded")

# Verify popup is visible
popup = page.locator(".leaflet-popup-content")
expect(popup).to_be_visible(timeout=MARKER_LOAD_TIMEOUT)

# Verify popup shows correct location
title = popup.locator("h3")
expect(title).to_have_text("Zwierzyniecka")

subtitle = popup.locator("p").first
expect(subtitle).to_have_text("small bridge")


class TestShareOnMobile:
"""Test suite for share button functionality on mobile devices"""

@pytest.mark.parametrize("device_name", ALL_MOBILE_DEVICES)
def test_share_button_triggers_native_share(self, mobile_page: Page, device_name: str):
"""
Verify clicking the share button triggers navigator.share() on mobile.

Tests on all mobile devices: iphone-x, iphone-6, ipad-2, samsung-s10
"""
# Stub navigator.share() before navigating
mobile_page.add_init_script(
"""
window.__shareArgs = [];
navigator.share = (data) => {
window.__shareArgs.push(data);
return Promise.resolve();
};
"""
)

mobile_page.goto(BASE_URL, wait_until="domcontentloaded")

# Click first marker to expand cluster
first_marker = mobile_page.locator(".leaflet-marker-icon").first
first_marker.evaluate("el => el.click()")

# Wait for markers to appear (should be 2 after expansion)
markers = mobile_page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(2, timeout=MARKER_LOAD_TIMEOUT)

# Click the rightmost marker
mobile_page.evaluate(
"""
() => {
const markers = document.querySelectorAll('.leaflet-marker-icon');
let rightmostMarker = null;
let maxX = -Infinity;

markers.forEach(marker => {
const rect = marker.getBoundingClientRect();
if (rect.x > maxX) {
maxX = rect.x;
rightmostMarker = marker;
}
});

if (rightmostMarker) {
rightmostMarker.click();
}
}
"""
)

# On mobile, popup appears as Material-UI Dialog
dialog_content = mobile_page.locator(".MuiDialogContent-root")
expect(dialog_content).to_be_visible(timeout=5000)

# Click the share button
share_button = mobile_page.get_by_role("button", name="share")
expect(share_button).to_be_visible()
share_button.evaluate("el => el.click()")

# Verify navigator.share() was called with correct URL data
share_args = mobile_page.evaluate("() => window.__shareArgs")
assert len(share_args) > 0, "navigator.share() was not called"
assert "?locationId=" in share_args[0].get("url", "")

@pytest.mark.parametrize("device_name", ALL_MOBILE_DEVICES)
def test_shared_link_opens_popup_on_mobile(self, mobile_page: Page, device_name: str):
"""
Verify navigating to a URL with ?locationId= auto-opens the popup on mobile.

Tests on all mobile devices: iphone-x, iphone-6, ipad-2, samsung-s10
"""
mobile_page.goto(f"{BASE_URL}/?locationId=dattarro", wait_until="domcontentloaded")

# On mobile, popup appears as Material-UI Dialog
dialog_content = mobile_page.locator(".MuiDialogContent-root")
expect(dialog_content).to_be_visible(timeout=MARKER_LOAD_TIMEOUT)

# Verify popup shows correct location
title = dialog_content.locator("h3")
expect(title).to_have_text("Zwierzyniecka")
Loading