feat: add clickable activity cards with comment anchor links to profile tab
Add `url` field to profile activity items so post cards link to `/posts/{slug}` and comment cards link to the parent post with a `#comment-{uid}` anchor, matching notification click behavior. Include the shared `_card_link.html` overlay in the template, fix XP bar rendering for missing `xp` key, and add API + e2e tests verifying the link structure and navigation.
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user