+ {% if act['url'] %}
+ {% set _href = act['url'] %}
+ {% set _label = (act['type'] == 'post') and 'View post' or 'View comment on post' %}
+ {% include "_card_link.html" %}
+ {% endif %}
{% if act['type'] == 'post' %}📝{% else %}💬{% endif %}
diff --git a/tests/api/profile/index.py b/tests/api/profile/index.py
index 790ac614..8c9bb522 100644
--- a/tests/api/profile/index.py
+++ b/tests/api/profile/index.py
@@ -170,6 +170,103 @@ def test_missing_profile_returns_404(app_server):
assert r.status_code == 404
+def _seed_comment_on_post(commenter_uid):
+ from datetime import datetime, timezone
+ from uuid import uuid4
+ from devplacepy.database import get_table
+
+ post_slug, post_uid = _seed_post_seo()
+ comment_uid = str(uuid4())
+ get_table("comments").insert(
+ {
+ "deleted_at": None,
+ "deleted_by": None,
+ "uid": comment_uid,
+ "target_type": "post",
+ "target_uid": post_uid,
+ "post_uid": post_uid,
+ "user_uid": commenter_uid,
+ "content": "Activity tab link comment body",
+ "parent_uid": None,
+ "created_at": datetime.now(timezone.utc).isoformat(),
+ }
+ )
+ return post_slug, post_uid, comment_uid
+
+
+def test_activity_comment_exposes_parent_post_url(app_server):
+ from devplacepy.database import get_table, refresh_snapshot
+
+ commenter = _seed_owner()
+ username = get_table("users").find_one(uid=commenter)["username"]
+ post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
+ refresh_snapshot()
+
+ r = requests.get(
+ f"{BASE_URL}/profile/{username}?tab=activity",
+ headers={"Accept": "application/json"},
+ )
+ assert r.status_code == 200
+ activities = r.json()["activities"]
+ comment_acts = [a for a in activities if a.get("type") == "comment"]
+ assert comment_acts, "expected a comment activity"
+ act = comment_acts[0]
+ assert act["url"] == f"/posts/{post_slug}#comment-{comment_uid}"
+ assert act["target_type"] == "post"
+
+
+def test_activity_comment_url_matches_notification_target(app_server):
+ from devplacepy.database import get_table, refresh_snapshot, resolve_object_url
+
+ commenter = _seed_owner()
+ username = get_table("users").find_one(uid=commenter)["username"]
+ _post_slug, post_uid, comment_uid = _seed_comment_on_post(commenter)
+ refresh_snapshot()
+
+ expected = f"{resolve_object_url('post', post_uid)}#comment-{comment_uid}"
+
+ r = requests.get(
+ f"{BASE_URL}/profile/{username}?tab=activity",
+ headers={"Accept": "application/json"},
+ )
+ act = [a for a in r.json()["activities"] if a.get("type") == "comment"][0]
+ assert act["url"] == expected
+
+
+def test_activity_post_exposes_url(app_server):
+ from datetime import datetime, timezone
+ from devplacepy.database import get_table, refresh_snapshot
+
+ owner = _seed_owner()
+ username = get_table("users").find_one(uid=owner)["username"]
+ post_slug, post_uid = _seed_post_seo()
+ get_table("posts").update({"uid": post_uid, "user_uid": owner}, ["uid"])
+ refresh_snapshot()
+
+ r = requests.get(
+ f"{BASE_URL}/profile/{username}?tab=activity",
+ headers={"Accept": "application/json"},
+ )
+ assert r.status_code == 200
+ post_acts = [a for a in r.json()["activities"] if a.get("type") == "post"]
+ assert post_acts, "expected a post activity"
+ assert post_acts[0]["url"] == f"/posts/{post_slug}"
+
+
+def test_activity_comment_card_renders_overlay_link(app_server):
+ from devplacepy.database import get_table, refresh_snapshot
+
+ commenter = _seed_owner()
+ username = get_table("users").find_one(uid=commenter)["username"]
+ post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
+ refresh_snapshot()
+
+ r = requests.get(f"{BASE_URL}/profile/{username}?tab=activity")
+ assert r.status_code == 200
+ assert "activity-card card-link-host" in r.text
+ assert f'class="card-link" href="/posts/{post_slug}#comment-{comment_uid}"' in r.text
+
+
def test_viewing_profile_marks_notification_read(app_server):
import time
from datetime import datetime, timezone
diff --git a/tests/conftest.py b/tests/conftest.py
index 2321d955..d4e34a95 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,7 +12,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
SCREENSHOT_DIR = Path("/tmp/devplace_test_screenshots")
-PORT = 10501
+PORT = int(os.environ.get("DEVPLACE_TEST_PORT", "10501"))
BASE_URL = f"http://127.0.0.1:{PORT}"
_TEST_DB = tempfile.NamedTemporaryFile(suffix=f"_{PORT}.db", delete=False)
diff --git a/tests/e2e/admin/users/aiusage.py b/tests/e2e/admin/users/aiusage.py
index 46210597..ce9f6e48 100644
--- a/tests/e2e/admin/users/aiusage.py
+++ b/tests/e2e/admin/users/aiusage.py
@@ -93,5 +93,8 @@ def test_admin_user_ai_usage_endpoint_returns_data(alice, seeded_db):
def test_member_forbidden_from_user_ai_usage_endpoint(bob, seeded_db):
page, _ = bob
bob_uid = get_table("users").find_one(username=seeded_db["bob"]["username"])["uid"]
- resp = page.request.get(f"{BASE_URL}/admin/users/{bob_uid}/ai-usage")
+ resp = page.request.get(
+ f"{BASE_URL}/admin/users/{bob_uid}/ai-usage",
+ headers={"Accept": "application/json"},
+ )
assert resp.status == 403
diff --git a/tests/e2e/game/index.py b/tests/e2e/game/index.py
index 1829418e..e7e0f0bc 100644
--- a/tests/e2e/game/index.py
+++ b/tests/e2e/game/index.py
@@ -63,7 +63,7 @@ def test_game_page_loads(alice):
assert page.locator(".game-header h1").is_visible()
expect(page.locator("[data-hud-coins]")).to_have_text("100000")
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(4)
- expect(page.locator(".perk-card")).to_have_count(4)
+ expect(page.locator(".perk-card:not(.legacy-card)")).to_have_count(4)
expect(page.locator(".quest-item")).to_have_count(3)
diff --git a/tests/e2e/profile/index.py b/tests/e2e/profile/index.py
index 90409b7a..07ac41f5 100644
--- a/tests/e2e/profile/index.py
+++ b/tests/e2e/profile/index.py
@@ -4,6 +4,72 @@ from tests.conftest import BASE_URL
FOLLOW = "form[action='/follow/bob_test'] button"
UNFOLLOW = "form[action='/follow/unfollow/bob_test'] button"
from playwright.sync_api import expect
+from tests.e2e.post import create_post
+
+
+def _post_comment(page, body):
+ textarea = page.locator(".comment-form textarea[name='content']")
+ textarea.fill(body)
+ page.locator(".comment-form button:has-text('Post')").click()
+ expect(page.locator(f".comment-text:has-text('{body}')")).to_be_visible()
+
+
+def test_activity_comment_card_links_to_parent_post(alice):
+ page, user = alice
+ create_post(page, "devlog", "Post for activity comment link")
+ post_url = page.url
+ _post_comment(page, "Activity clickable comment body")
+
+ page.goto(
+ f"{BASE_URL}/profile/{user['username']}?tab=activity",
+ wait_until="domcontentloaded",
+ )
+ card_link = page.locator(
+ ".activity-card.card-link-host a.card-link[href*='#comment-']"
+ ).first
+ expect(card_link).to_have_count(1)
+ href = card_link.get_attribute("href")
+ assert "/posts/" in href and "#comment-" in href
+
+ card_link.click()
+ page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
+ expect(
+ page.locator(".comment-text:has-text('Activity clickable comment body')")
+ ).to_be_visible()
+
+
+def test_activity_post_card_links_to_post(alice):
+ page, user = alice
+ create_post(page, "showcase", "Post for activity post link", title="Activity Post Title")
+
+ page.goto(
+ f"{BASE_URL}/profile/{user['username']}?tab=activity",
+ wait_until="domcontentloaded",
+ )
+ post_link = page.locator(
+ ".activity-card.card-link-host a.card-link[href^='/posts/']:not([href*='#comment-'])"
+ ).first
+ expect(post_link).to_have_count(1)
+ post_link.click()
+ page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
+ expect(page.locator(".post-detail-title:has-text('Activity Post Title')")).to_be_visible()
+
+
+def test_activity_inner_content_link_stays_clickable(alice, bob):
+ page, user = alice
+ create_post(page, "random", "Post for activity mention comment")
+ _post_comment(page, "Ping @bob_test in activity")
+
+ page.goto(
+ f"{BASE_URL}/profile/{user['username']}?tab=activity",
+ wait_until="domcontentloaded",
+ )
+ mention = page.locator(
+ ".activity-card.card-link-host .activity-content a[href='/profile/bob_test']"
+ ).first
+ expect(mention).to_have_count(1)
+ mention.click()
+ page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
def test_follow_then_unfollow(alice):
diff --git a/tests/e2e/profile/regenerateavatar.py b/tests/e2e/profile/regenerateavatar.py
index 547165e4..a6b6a480 100644
--- a/tests/e2e/profile/regenerateavatar.py
+++ b/tests/e2e/profile/regenerateavatar.py
@@ -22,9 +22,9 @@ def test_regenerate_avatar_updates_preview(alice):
def test_regenerate_button_hidden_for_other_user(alice, bob):
- page, _ = alice
- bob_page, bob_user = bob
- page.goto(
- f"{BASE_URL}/profile/{bob_user['username']}", wait_until="domcontentloaded"
+ alice_page, alice_user = alice
+ bob_page, _ = bob
+ bob_page.goto(
+ f"{BASE_URL}/profile/{alice_user['username']}", wait_until="domcontentloaded"
)
- expect(page.locator("[data-regenerate-avatar]")).to_have_count(0)
+ expect(bob_page.locator("[data-regenerate-avatar]")).to_have_count(0)
diff --git a/tests/unit/services/game/economy.py b/tests/unit/services/game/economy.py
index e92a38d8..81345659 100644
--- a/tests/unit/services/game/economy.py
+++ b/tests/unit/services/game/economy.py
@@ -72,8 +72,8 @@ def test_prestige_multiplier():
def test_fertilize_cost_has_floor():
- assert economy.fertilize_cost(1) == economy.FERTILIZE_MIN_COST
- assert economy.fertilize_cost(10000) > economy.FERTILIZE_MIN_COST
+ assert economy.fertilize_click_cost(1, 1, 1_000_000) == 1
+ assert economy.fertilize_click_cost(10_000, 50, 100) > 1
def test_crop_payload_locked_flag():
diff --git a/tests/unit/services/game/store.py b/tests/unit/services/game/store.py
index 60700ff0..311a6d8b 100644
--- a/tests/unit/services/game/store.py
+++ b/tests/unit/services/game/store.py
@@ -32,6 +32,8 @@ def _reset(username, coins=100000):
get_table("game_plots").delete(farm_uid=farm["uid"])
get_table("game_quests").delete(farm_uid=farm["uid"])
get_table("game_farms").delete(uid=farm["uid"])
+ get_table("game_steals").delete(thief_uid=user["uid"])
+ get_table("game_steals").delete(owner_uid=user["uid"])
farm = store.ensure_farm(user["uid"])
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
return user