This commit is contained in:
parent
347e5f0f31
commit
a5c71fd2f8
@ -385,6 +385,32 @@ def resolve_by_slug(table, slug):
|
||||
return entry
|
||||
|
||||
|
||||
def resolve_object_url(target_type: str, target_uid: str) -> str:
|
||||
if target_type == "post":
|
||||
post = resolve_by_slug(get_table("posts"), target_uid)
|
||||
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
|
||||
if target_type == "project":
|
||||
project = resolve_by_slug(get_table("projects"), target_uid)
|
||||
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
|
||||
if target_type == "news":
|
||||
article = resolve_by_slug(get_table("news"), target_uid)
|
||||
if article:
|
||||
return f"/news/{article.get('slug', '') or article['uid']}"
|
||||
return "/news"
|
||||
if target_type == "bug":
|
||||
return f"/bugs?highlight={target_uid}"
|
||||
if target_type == "gist":
|
||||
gist = resolve_by_slug(get_table("gists"), target_uid)
|
||||
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
|
||||
if target_type == "comment":
|
||||
comment = get_table("comments").find_one(uid=target_uid)
|
||||
if not comment:
|
||||
return "/feed"
|
||||
parent_url = resolve_object_url(comment.get("target_type", "post"), comment.get("target_uid") or comment.get("post_uid", ""))
|
||||
return f"{parent_url}#comment-{target_uid}"
|
||||
return "/feed"
|
||||
|
||||
|
||||
VOTABLE_TARGETS: dict[str, str] = {
|
||||
"post": "posts",
|
||||
"project": "projects",
|
||||
|
||||
@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table, resolve_by_slug
|
||||
from devplacepy.database import get_table, resolve_object_url
|
||||
from devplacepy.attachments import link_attachments, delete_target_attachments
|
||||
from devplacepy.utils import generate_uid, require_user, create_mention_notifications, create_notification, award_badge
|
||||
from devplacepy.models import CommentForm
|
||||
@ -12,26 +12,6 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def resolve_target_redirect(target_type, target_uid):
|
||||
if target_type == "post":
|
||||
post = resolve_by_slug(get_table("posts"), target_uid)
|
||||
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
|
||||
if target_type == "project":
|
||||
project = resolve_by_slug(get_table("projects"), target_uid)
|
||||
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
|
||||
if target_type == "news":
|
||||
article = resolve_by_slug(get_table("news"), target_uid)
|
||||
if article:
|
||||
return f"/news/{article.get('slug', '') or article['uid']}"
|
||||
return "/news"
|
||||
if target_type == "bug":
|
||||
return f"/bugs?highlight={target_uid}"
|
||||
if target_type == "gist":
|
||||
gist = resolve_by_slug(get_table("gists"), target_uid)
|
||||
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
|
||||
return "/bugs"
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_comment(request: Request, data: Annotated[CommentForm, Form()]):
|
||||
user = require_user(request)
|
||||
@ -40,7 +20,7 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
|
||||
target_type = data.target_type
|
||||
parent_uid = data.parent_uid
|
||||
|
||||
redirect_url = resolve_target_redirect(target_type, target_uid)
|
||||
redirect_url = resolve_object_url(target_type, target_uid)
|
||||
|
||||
comment_uid = generate_uid()
|
||||
insert = {
|
||||
@ -61,20 +41,22 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
|
||||
|
||||
award_badge(user["uid"], "First Comment")
|
||||
|
||||
comment_url = f"{redirect_url}#comment-{comment_uid}"
|
||||
|
||||
if target_type == "post":
|
||||
if parent_uid:
|
||||
parent = get_table("comments").find_one(uid=parent_uid)
|
||||
if parent and parent["user_uid"] != user["uid"]:
|
||||
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"])
|
||||
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"], comment_url)
|
||||
else:
|
||||
posts = get_table("posts")
|
||||
post = posts.find_one(uid=target_uid)
|
||||
if not post:
|
||||
post = posts.find_one(slug=target_uid)
|
||||
if post and post["user_uid"] != user["uid"]:
|
||||
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"])
|
||||
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"], comment_url)
|
||||
|
||||
create_mention_notifications(content, user["uid"], redirect_url)
|
||||
create_mention_notifications(content, user["uid"], comment_url)
|
||||
logger.info(f"Comment by {user['username']} on {target_type} {target_uid}")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
@ -94,5 +76,5 @@ async def delete_comment(request: Request, comment_uid: str):
|
||||
get_table("votes").delete(target_uid=comment_uid, target_type="comment")
|
||||
comments.delete(id=comment["id"])
|
||||
logger.info(f"Comment {comment_uid} deleted by {user['username']}")
|
||||
redirect_url = resolve_target_redirect(target_type, target_uid)
|
||||
redirect_url = resolve_object_url(target_type, target_uid)
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
@ -29,7 +29,7 @@ async def follow_user(request: Request, username: str):
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"])
|
||||
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"], f"/profile/{user['username']}")
|
||||
|
||||
logger.info(f"{user['username']} followed {username}")
|
||||
return RedirectResponse(url=f"/profile/{username}", status_code=302)
|
||||
|
||||
@ -174,6 +174,7 @@ async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
|
||||
"type": "message",
|
||||
"message": f"{user['username']} sent you a message",
|
||||
"related_uid": user["uid"],
|
||||
"target_url": f"/messages?with_uid={user['uid']}",
|
||||
"read": False,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
@ -86,6 +86,19 @@ async def notifications_page(request: Request):
|
||||
})
|
||||
|
||||
|
||||
@router.get("/open/{notification_uid}")
|
||||
async def open_notification(request: Request, notification_uid: str):
|
||||
user = require_user(request)
|
||||
notifications_table = get_table("notifications")
|
||||
n = notifications_table.find_one(uid=notification_uid)
|
||||
if not n or n["user_uid"] != user["uid"]:
|
||||
return RedirectResponse(url="/notifications", status_code=302)
|
||||
if not n["read"]:
|
||||
notifications_table.update({"id": n["id"], "read": True}, ["id"])
|
||||
clear_unread_cache(user["uid"])
|
||||
return RedirectResponse(url=n.get("target_url") or "/notifications", status_code=302)
|
||||
|
||||
|
||||
@router.post("/mark-read/{notification_uid}")
|
||||
async def mark_read(request: Request, notification_uid: str):
|
||||
user = require_user(request)
|
||||
|
||||
@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid
|
||||
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid, resolve_object_url
|
||||
from devplacepy.utils import generate_uid, require_user, create_notification
|
||||
from devplacepy.models import VoteForm
|
||||
|
||||
@ -45,7 +45,8 @@ async def vote(request: Request, target_type: str, target_uid: str, data: Annota
|
||||
if value == 1 and target_type in NOTIFY_ON_VOTE:
|
||||
owner_uid = get_target_owner_uid(target_type, target_uid)
|
||||
if owner_uid and owner_uid != user["uid"]:
|
||||
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"])
|
||||
target_url = resolve_object_url(target_type, target_uid)
|
||||
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"], target_url)
|
||||
|
||||
referer = request.headers.get("Referer", "/feed")
|
||||
return RedirectResponse(url=referer, status_code=302)
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-card.unread {
|
||||
|
||||
@ -115,6 +115,22 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-highlight {
|
||||
animation: comment-highlight-fade 2s ease-out;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
@keyframes comment-highlight-fade {
|
||||
from {
|
||||
background: var(--accent-light);
|
||||
box-shadow: 0 0 0 4px var(--accent-light);
|
||||
}
|
||||
to {
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0 4px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ModalManager } from "./ModalManager.js";
|
||||
import { FormManager } from "./FormManager.js";
|
||||
import { VoteManager } from "./VoteManager.js";
|
||||
import { NotificationManager } from "./NotificationManager.js";
|
||||
import { MessageSearch } from "./MessageSearch.js";
|
||||
import { ProfileEditor } from "./ProfileEditor.js";
|
||||
import { MobileNav } from "./MobileNav.js";
|
||||
@ -15,6 +16,7 @@ class Application {
|
||||
this.modals = new ModalManager();
|
||||
this.forms = new FormManager();
|
||||
this.votes = new VoteManager();
|
||||
this.notifications = new NotificationManager();
|
||||
this.messageSearch = new MessageSearch();
|
||||
this.profile = new ProfileEditor();
|
||||
this.mobileNav = new MobileNav();
|
||||
|
||||
51
devplacepy/static/js/NotificationManager.js
Normal file
51
devplacepy/static/js/NotificationManager.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class NotificationManager {
|
||||
constructor() {
|
||||
this.initCardNavigation();
|
||||
this.initDismiss();
|
||||
this.initHashScroll();
|
||||
}
|
||||
|
||||
initCardNavigation() {
|
||||
document.querySelectorAll(".notification-card[data-href]").forEach((card) => {
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest("a, button")) {
|
||||
return;
|
||||
}
|
||||
window.location.href = card.dataset.href;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
Http.postForm(`/notifications/mark-read/${uid}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initHashScroll() {
|
||||
const hash = window.location.hash;
|
||||
if (!hash.startsWith("#comment-")) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("load", () => {
|
||||
const target = document.getElementById(hash.slice(1));
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
target.classList.add("comment-highlight");
|
||||
setTimeout(() => target.classList.remove("comment-highlight"), 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ import { Http } from "./Http.js";
|
||||
export class VoteManager {
|
||||
constructor() {
|
||||
this.initVoteButtons();
|
||||
this.initNotificationDismiss();
|
||||
}
|
||||
|
||||
initVoteButtons() {
|
||||
@ -15,16 +14,4 @@ export class VoteManager {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initNotificationDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
Http.postForm(`/notifications/mark-read/${uid}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="comment-body" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-body" id="comment-{{ item.comment['uid'] }}" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-header">
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
|
||||
|
||||
@ -17,14 +17,13 @@
|
||||
<div class="notification-group">
|
||||
<div class="notification-group-label">{{ group.label }}</div>
|
||||
{% for item in group.entries %}
|
||||
{% set target_url = item.notification.get('target_url', '') %}
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}">
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}" data-href="/notifications/open/{{ item.notification['uid'] }}">
|
||||
{% set actor_username = item.actor['username'] if item.actor else '#' %}
|
||||
<a href="/profile/{{ actor_username }}" style="flex-shrink:0">
|
||||
<img src="{{ avatar_url('multiavatar', actor_username, 32) }}" class="avatar-img avatar-sm" alt="{{ actor_username }}" loading="lazy">
|
||||
</a>
|
||||
<div class="notification-body">
|
||||
<div class="notification-text">{% if target_url %}<a href="{{ target_url }}" style="color:inherit;text-decoration:none">{% endif %}{{ item.notification['message'] }}{% if target_url %}</a>{% endif %}</div>
|
||||
<div class="notification-text">{{ item.notification['message'] }}</div>
|
||||
<div class="notification-time">{{ item.time_ago }}</div>
|
||||
</div>
|
||||
<form method="POST" action="/notifications/mark-read/{{ item.notification['uid'] }}" style="display:inline;">
|
||||
|
||||
@ -212,6 +212,10 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
|
||||
if not usernames:
|
||||
return
|
||||
users = get_table("users")
|
||||
actor = users.find_one(uid=actor_uid)
|
||||
if not actor:
|
||||
return
|
||||
actor_username = actor["username"]
|
||||
seen = set()
|
||||
for username in usernames:
|
||||
if username in seen:
|
||||
@ -219,7 +223,7 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
|
||||
seen.add(username)
|
||||
mentioned = users.find_one(username=username)
|
||||
if mentioned and mentioned["uid"] != actor_uid:
|
||||
create_notification(mentioned["uid"], "mention", f"@{username} mentioned you", actor_uid, target_url)
|
||||
create_notification(mentioned["uid"], "mention", f"@{actor_username} mentioned you", actor_uid, target_url)
|
||||
|
||||
|
||||
def format_date(dt_str: str, include_time: bool = False) -> str:
|
||||
|
||||
@ -332,3 +332,182 @@ def test_reply_notification(app_server, browser, seeded_db):
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
|
||||
def test_mention_notification_names_actor(app_server, browser, seeded_db):
|
||||
from tests.conftest import login_user
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(15000)
|
||||
pb.set_default_timeout(15000)
|
||||
|
||||
login_user(pa, seeded_db["alice"])
|
||||
login_user(pb, seeded_db["bob"])
|
||||
|
||||
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
pb.locator(".feed-fab").first.click()
|
||||
pb.fill("#post-content", "Naming check @alice_test please read")
|
||||
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(1500)
|
||||
texts = pa.locator(".notification-text").all_text_contents()
|
||||
assert any("@bob_test mentioned you" in t for t in texts), f"mention message must name the actor (bob): {texts}"
|
||||
assert not any("@alice_test mentioned you" in t for t in texts), f"mention message must not name the mentioned user (alice): {texts}"
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
|
||||
def test_comment_notification_click_opens_comment(app_server, browser, seeded_db):
|
||||
from tests.conftest import login_user
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(15000)
|
||||
pb.set_default_timeout(15000)
|
||||
|
||||
login_user(pa, seeded_db["alice"])
|
||||
login_user(pb, seeded_db["bob"])
|
||||
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
pa.locator(".feed-fab").first.click()
|
||||
pa.fill("#post-content", "Post for click navigation test")
|
||||
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
|
||||
post_url = pa.url
|
||||
|
||||
pb.goto(post_url, wait_until="domcontentloaded")
|
||||
textarea = pb.locator("form.comment-form textarea[name='content']").first
|
||||
textarea.wait_for(state="visible", timeout=10000)
|
||||
textarea.fill("Bob comment that alice should jump to")
|
||||
pb.locator("button.comment-form-submit").first.click()
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="commented on your post").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
href = card.get_attribute("data-href")
|
||||
assert href.startswith("/notifications/open/"), f"unexpected data-href: {href}"
|
||||
|
||||
card.locator(".notification-text").click()
|
||||
pa.wait_for_url("**/posts/**", timeout=10000, wait_until="domcontentloaded")
|
||||
assert "#comment-" in pa.url, f"click did not deep-link to a comment: {pa.url}"
|
||||
|
||||
comment_uid = pa.url.split("#comment-")[1]
|
||||
pa.locator(f"#comment-{comment_uid}").wait_for(state="visible", timeout=10000)
|
||||
pa.wait_for_selector(f"#comment-{comment_uid}.comment-highlight", timeout=5000)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(1000)
|
||||
target = pa.locator(f'.notification-card[data-href="{href}"]')
|
||||
target.wait_for(state="visible", timeout=10000)
|
||||
assert "unread" not in (target.get_attribute("class") or ""), "opening a notification should mark it read"
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
|
||||
def test_follow_notification_click_opens_profile(app_server, browser, seeded_db):
|
||||
from tests.conftest import login_user
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(15000)
|
||||
pb.set_default_timeout(15000)
|
||||
|
||||
login_user(pa, seeded_db["alice"])
|
||||
login_user(pb, seeded_db["bob"])
|
||||
|
||||
pb.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
|
||||
pb.wait_for_timeout(1000)
|
||||
follow_btn = pb.locator("button:has-text('Follow')")
|
||||
if follow_btn.is_visible():
|
||||
follow_btn.click()
|
||||
pb.wait_for_timeout(1000)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="started following you").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
pa.wait_for_url("**/profile/bob_test", timeout=10000, wait_until="domcontentloaded")
|
||||
assert pa.url.endswith("/profile/bob_test"), f"follow notification should open the follower profile: {pa.url}"
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
|
||||
def test_message_notification_click_opens_conversation(app_server, browser, seeded_db):
|
||||
from tests.conftest import login_user
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(15000)
|
||||
pb.set_default_timeout(15000)
|
||||
|
||||
login_user(pa, seeded_db["alice"])
|
||||
login_user(pb, seeded_db["bob"])
|
||||
|
||||
pb.goto(f"{BASE_URL}/messages?search=alice_test", wait_until="domcontentloaded")
|
||||
pb.wait_for_timeout(2000)
|
||||
msg_input = pb.locator("input[name='content']").first
|
||||
msg_input.wait_for(state="visible", timeout=10000)
|
||||
msg_input.fill("Click-through message from bob")
|
||||
pb.locator("button[type='submit']").last.click()
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="sent you a message").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
pa.wait_for_url("**/messages**", timeout=10000, wait_until="domcontentloaded")
|
||||
assert "with_uid=" in pa.url, f"message notification should open the conversation: {pa.url}"
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
|
||||
def test_vote_notification_click_opens_target(app_server, browser, seeded_db):
|
||||
from tests.conftest import login_user
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(15000)
|
||||
pb.set_default_timeout(15000)
|
||||
|
||||
login_user(pa, seeded_db["alice"])
|
||||
login_user(pb, seeded_db["bob"])
|
||||
|
||||
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
pb.locator(".feed-fab").first.click()
|
||||
pb.fill("#post-content", "Post for vote click-through test")
|
||||
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
|
||||
post_path = "/posts/" + pb.url.split("/posts/")[1]
|
||||
|
||||
pa.goto(f"{BASE_URL}{post_path}", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(1000)
|
||||
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
|
||||
vote_btn.wait_for(state="visible", timeout=10000)
|
||||
vote_btn.click()
|
||||
pa.wait_for_timeout(1500)
|
||||
|
||||
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pb.locator(".notification-card[data-href]").filter(has_text="++'d").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
pb.wait_for_url(f"**{post_path}", timeout=10000, wait_until="domcontentloaded")
|
||||
assert post_path in pb.url, f"vote notification should open the voted post: {pb.url}"
|
||||
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user