Compare commits
2 Commits
adb07bb40e
...
aca07828f1
| Author | SHA1 | Date | |
|---|---|---|---|
| aca07828f1 | |||
| c3d37bd103 |
@ -34,6 +34,7 @@ class LoginForm(BaseModel):
|
|||||||
email: str = Field(min_length=1, max_length=255)
|
email: str = Field(min_length=1, max_length=255)
|
||||||
password: str = Field(min_length=1, max_length=128)
|
password: str = Field(min_length=1, max_length=128)
|
||||||
remember_me: str = ""
|
remember_me: str = ""
|
||||||
|
next: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordForm(BaseModel):
|
class ForgotPasswordForm(BaseModel):
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from fastapi import APIRouter, Request, Form
|
|||||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||||
from devplacepy.database import get_table
|
from devplacepy.database import get_table
|
||||||
from devplacepy.templating import templates
|
from devplacepy.templating import templates
|
||||||
from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge, clear_session_cache
|
from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge, clear_session_cache, safe_next
|
||||||
from devplacepy.seo import base_seo_context
|
from devplacepy.seo import base_seo_context
|
||||||
from devplacepy.models import SignupForm, LoginForm, ForgotPasswordForm, ResetPasswordForm
|
from devplacepy.models import SignupForm, LoginForm, ForgotPasswordForm, ResetPasswordForm
|
||||||
|
|
||||||
@ -30,17 +30,17 @@ async def signup_page(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/login", response_class=HTMLResponse)
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request, next: str | None = None):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if user:
|
if user:
|
||||||
return RedirectResponse(url="/feed", status_code=302)
|
return RedirectResponse(url=safe_next(next), status_code=302)
|
||||||
seo_ctx = base_seo_context(
|
seo_ctx = base_seo_context(
|
||||||
request,
|
request,
|
||||||
title="Sign In",
|
title="Sign In",
|
||||||
description="Sign in to DevPlace to connect with developers.",
|
description="Sign in to DevPlace to connect with developers.",
|
||||||
robots="noindex,nofollow",
|
robots="noindex,nofollow",
|
||||||
)
|
)
|
||||||
return templates.TemplateResponse(request, "login.html", {**seo_ctx, "request": request})
|
return templates.TemplateResponse(request, "login.html", {**seo_ctx, "request": request, "next_url": safe_next(next, "")})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/signup")
|
@router.post("/signup")
|
||||||
@ -108,12 +108,12 @@ async def login(request: Request, data: Annotated[LoginForm, Form()]):
|
|||||||
seo_ctx = base_seo_context(request, title="Sign In", robots="noindex,nofollow")
|
seo_ctx = base_seo_context(request, title="Sign In", robots="noindex,nofollow")
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "login.html",
|
request, "login.html",
|
||||||
{**seo_ctx, "request": request, "errors": errors, "email": email},
|
{**seo_ctx, "request": request, "errors": errors, "email": email, "next_url": safe_next(data.next, "")},
|
||||||
)
|
)
|
||||||
|
|
||||||
token = create_session(user["uid"])
|
token = create_session(user["uid"])
|
||||||
max_age = 86400 * 30 if remember_me else 86400 * 7
|
max_age = 86400 * 30 if remember_me else 86400 * 7
|
||||||
response = RedirectResponse(url="/feed", status_code=302)
|
response = RedirectResponse(url=safe_next(data.next), status_code=302)
|
||||||
response.set_cookie(key="session", value=token, max_age=max_age, httponly=True, samesite="lax")
|
response.set_cookie(key="session", value=token, max_age=max_age, httponly=True, samesite="lax")
|
||||||
logger.info(f"User {user['username']} logged in")
|
logger.info(f"User {user['username']} logged in")
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -59,7 +59,7 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
|
|||||||
|
|
||||||
create_mention_notifications(content, user["uid"], comment_url)
|
create_mention_notifications(content, user["uid"], comment_url)
|
||||||
logger.info(f"Comment by {user['username']} on {target_type} {target_uid}")
|
logger.info(f"Comment by {user['username']} on {target_type} {target_uid}")
|
||||||
return RedirectResponse(url=redirect_url, status_code=302)
|
return RedirectResponse(url=comment_url, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{comment_uid}")
|
@router.post("/delete/{comment_uid}")
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Http } from "./Http.js";
|
||||||
|
|
||||||
export class CommentManager {
|
export class CommentManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initCommentReply();
|
this.initCommentReply();
|
||||||
@ -25,7 +27,10 @@ export class CommentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const template = document.getElementById("comment-reply-template");
|
const template = document.getElementById("comment-reply-template");
|
||||||
if (!template) return;
|
if (!template) {
|
||||||
|
Http.toLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const container = comment.closest(".post-card, .comments-section");
|
const container = comment.closest(".post-card, .comments-section");
|
||||||
const source = container && container.querySelector(".comment-form:not(.comment-reply-form)");
|
const source = container && container.querySelector(".comment-form:not(.comment-reply-form)");
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
export class Http {
|
export class Http {
|
||||||
|
static toLogin() {
|
||||||
|
const next = encodeURIComponent(location.pathname + location.search + location.hash);
|
||||||
|
window.location.href = `/auth/login?next=${next}`;
|
||||||
|
}
|
||||||
|
|
||||||
static async getJson(url) {
|
static async getJson(url) {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
return response.json();
|
return response.json();
|
||||||
@ -13,6 +18,10 @@ export class Http {
|
|||||||
},
|
},
|
||||||
body: new URLSearchParams(params),
|
body: new URLSearchParams(params),
|
||||||
});
|
});
|
||||||
|
if (response.redirected && response.url.includes("/auth/login")) {
|
||||||
|
Http.toLogin();
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`request failed with status ${response.status}`);
|
throw new Error(`request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="auth-form" method="POST" action="/auth/login">
|
<form class="auth-form" method="POST" action="/auth/login">
|
||||||
|
<input type="hidden" name="next" value="{{ next_url or '' }}">
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="email">Email address</label>
|
<label for="email">Email address</label>
|
||||||
<input type="email" id="email" name="email" value="{{ email or '' }}" required maxlength="255" placeholder="you@example.com">
|
<input type="email" id="email" name="email" value="{{ email or '' }}" required maxlength="255" placeholder="you@example.com">
|
||||||
|
|||||||
@ -77,10 +77,16 @@ def get_current_user(request: Request):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def safe_next(value, default="/feed"):
|
||||||
|
if value and value.startswith("/") and not value.startswith("//"):
|
||||||
|
return value
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def require_user(request: Request):
|
def require_user(request: Request):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/"})
|
raise HTTPException(status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/auth/login"})
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ def seed_extra_users(count=30):
|
|||||||
|
|
||||||
def test_admin_redirect_unauth(page, app_server):
|
def test_admin_redirect_unauth(page, app_server):
|
||||||
page.goto(f"{BASE_URL}/admin", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/admin", wait_until="domcontentloaded")
|
||||||
assert page.url.rstrip("/") == BASE_URL
|
assert page.url.startswith(f"{BASE_URL}/auth/login")
|
||||||
|
|
||||||
|
|
||||||
def test_admin_users_loads(alice):
|
def test_admin_users_loads(alice):
|
||||||
|
|||||||
@ -260,3 +260,19 @@ def test_password_toggle(page, app_server):
|
|||||||
assert pw_input.get_attribute("type") == "text"
|
assert pw_input.get_attribute("type") == "text"
|
||||||
toggle.click()
|
toggle.click()
|
||||||
assert pw_input.get_attribute("type") == "password"
|
assert pw_input.get_attribute("type") == "password"
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_next_redirects_to_target(page, app_server, seeded_db):
|
||||||
|
page.goto(f"{BASE_URL}/auth/login?next=/gists", wait_until="domcontentloaded")
|
||||||
|
page.fill("#email", seeded_db["alice"]["email"])
|
||||||
|
page.fill("#password", seeded_db["alice"]["password"])
|
||||||
|
page.click("button:has-text('Sign in')")
|
||||||
|
page.wait_for_url("**/gists", wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_next_rejects_external(page, app_server, seeded_db):
|
||||||
|
page.goto(f"{BASE_URL}/auth/login?next=https://evil.example.com", wait_until="domcontentloaded")
|
||||||
|
page.fill("#email", seeded_db["alice"]["email"])
|
||||||
|
page.fill("#password", seeded_db["alice"]["password"])
|
||||||
|
page.click("button:has-text('Sign in')")
|
||||||
|
page.wait_for_url("**/feed", wait_until="domcontentloaded")
|
||||||
|
|||||||
@ -89,8 +89,9 @@ def test_bug_comment_reply(alice):
|
|||||||
page.wait_for_timeout(500)
|
page.wait_for_timeout(500)
|
||||||
assert page.is_visible("text=Root comment")
|
assert page.is_visible("text=Root comment")
|
||||||
page.click("button:has-text('Reply')")
|
page.click("button:has-text('Reply')")
|
||||||
page.fill("textarea[name='content']", "Nested reply")
|
reply_form = page.locator(".comment-reply-form").first
|
||||||
page.click("button:has-text('Post')")
|
reply_form.locator("textarea[name='content']").fill("Nested reply")
|
||||||
|
reply_form.locator("button:has-text('Post')").click()
|
||||||
page.wait_for_timeout(500)
|
page.wait_for_timeout(500)
|
||||||
assert page.is_visible("text=Nested reply")
|
assert page.is_visible("text=Nested reply")
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,41 @@ def _seed_posts(count):
|
|||||||
return topic
|
return topic
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_post_with_comments(comment_texts, topic="devlog"):
|
||||||
|
owner = str(uuid4())
|
||||||
|
get_table("users").insert({
|
||||||
|
"uid": owner, "username": f"seed_{owner[:8]}", "email": f"{owner[:8]}@seed.devplace",
|
||||||
|
"password_hash": "x", "role": "Member", "is_active": True,
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
})
|
||||||
|
post_uid = str(uuid4())
|
||||||
|
marker = f"seedpost-{post_uid[:8]}"
|
||||||
|
get_table("posts").insert({
|
||||||
|
"uid": post_uid, "user_uid": owner, "slug": make_combined_slug(marker, post_uid),
|
||||||
|
"title": None, "content": marker, "topic": topic, "project_uid": None,
|
||||||
|
"image": None, "stars": 0, "created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
})
|
||||||
|
comments = get_table("comments")
|
||||||
|
base = datetime.now(timezone.utc)
|
||||||
|
for i, text in enumerate(comment_texts):
|
||||||
|
comments.insert({
|
||||||
|
"uid": str(uuid4()), "target_type": "post", "target_uid": post_uid, "post_uid": post_uid,
|
||||||
|
"user_uid": owner, "content": text, "parent_uid": None,
|
||||||
|
"created_at": (base + timedelta(seconds=i)).isoformat(),
|
||||||
|
})
|
||||||
|
return marker, post_uid
|
||||||
|
|
||||||
|
|
||||||
|
def _create_post_ui(page, content, topic="devlog"):
|
||||||
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
|
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||||
|
page.locator(".feed-fab").first.click()
|
||||||
|
page.check(f"input[value='{topic}']")
|
||||||
|
page.fill("#post-content", content)
|
||||||
|
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||||
|
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
|
||||||
def test_feed_vote_voted_state_persists(alice):
|
def test_feed_vote_voted_state_persists(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
@ -189,34 +224,63 @@ def test_topnav_user_menu(alice):
|
|||||||
|
|
||||||
def test_feed_inline_comment(alice):
|
def test_feed_inline_comment(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
|
marker = f"inline-{uuid4().hex[:8]}"
|
||||||
|
_create_post_ui(page, f"{marker} post with inline comment test")
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
page.locator(".feed-fab").click()
|
card = page.locator(".post-card").filter(has_text=marker).first
|
||||||
page.check("input[value='devlog']")
|
inline_form = card.locator(".comment-form")
|
||||||
page.fill("#post-content", "Post with inline comment test " + "x" * 30)
|
|
||||||
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
|
||||||
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
|
||||||
inline_form = page.locator(".feed-comment-form").first
|
|
||||||
expect(inline_form).to_be_visible()
|
expect(inline_form).to_be_visible()
|
||||||
inline_form.locator("input[name='content']").fill("Inline comment from feed")
|
inline_form.locator("textarea[name='content']").fill("Inline comment from feed")
|
||||||
inline_form.locator(".feed-comment-submit").click()
|
inline_form.locator("button.comment-form-submit").click()
|
||||||
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
||||||
expect(page.locator(".comment-text:has-text('Inline comment from feed')")).to_be_visible()
|
expect(page.locator(".comment-text:has-text('Inline comment from feed')")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
def test_feed_inline_comment_placeholder(alice):
|
def test_feed_inline_comment_placeholder(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
|
marker = f"placeholder-{uuid4().hex[:8]}"
|
||||||
|
_create_post_ui(page, f"{marker} placeholder check post")
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
page.locator(".feed-fab").first.click()
|
card = page.locator(".post-card").filter(has_text=marker).first
|
||||||
page.fill("#post-content", "Placeholder check post " + "x" * 20)
|
inline_input = card.locator(".comment-form textarea[name='content']")
|
||||||
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
|
||||||
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
|
||||||
inline_input = page.locator(".feed-comment-form input[name='content']").first
|
|
||||||
expect(inline_input).to_be_visible()
|
expect(inline_input).to_be_visible()
|
||||||
assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."
|
assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed_shows_last_three_comments(page, app_server):
|
||||||
|
m = uuid4().hex[:6]
|
||||||
|
marker, _ = _seed_post_with_comments([f"{m}-one", f"{m}-two", f"{m}-three", f"{m}-four"])
|
||||||
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
|
card = page.locator(".post-card").filter(has_text=marker).first
|
||||||
|
card.wait_for(state="visible")
|
||||||
|
previews = card.locator(".post-card-comments .comment-text")
|
||||||
|
expect(previews).to_have_count(3)
|
||||||
|
expect(previews.nth(0)).to_contain_text(f"{m}-two")
|
||||||
|
expect(previews.nth(1)).to_contain_text(f"{m}-three")
|
||||||
|
expect(previews.nth(2)).to_contain_text(f"{m}-four")
|
||||||
|
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{m}-one")
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed_guest_vote_redirects_to_login(page, app_server):
|
||||||
|
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
|
||||||
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
|
card = page.locator(".post-card").filter(has_text=marker).first
|
||||||
|
card.wait_for(state="visible")
|
||||||
|
card.locator(".post-action-btn.vote-up").first.click()
|
||||||
|
page.wait_for_url("**/auth/login**", wait_until="domcontentloaded")
|
||||||
|
assert "next=" in page.url
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed_guest_reply_redirects_to_login(page, app_server):
|
||||||
|
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
|
||||||
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
|
card = page.locator(".post-card").filter(has_text=marker).first
|
||||||
|
card.wait_for(state="visible")
|
||||||
|
card.locator(".post-card-comments [data-action='reply']").first.click()
|
||||||
|
page.wait_for_url("**/auth/login**", wait_until="domcontentloaded")
|
||||||
|
assert "next=" in page.url
|
||||||
|
|
||||||
|
|
||||||
def test_feed_signals_topic(alice):
|
def test_feed_signals_topic(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
@ -266,5 +330,7 @@ def test_feed_guest_no_fab(page, app_server):
|
|||||||
|
|
||||||
|
|
||||||
def test_feed_guest_no_inline_comment(page, app_server):
|
def test_feed_guest_no_inline_comment(page, app_server):
|
||||||
|
_seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
assert page.locator(".feed-comment-form").count() == 0
|
assert page.locator(".post-card").count() > 0
|
||||||
|
assert page.locator(".comment-form").count() == 0
|
||||||
|
|||||||
@ -89,7 +89,7 @@ def test_news_comment(alice, news_article):
|
|||||||
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
|
||||||
page.fill("textarea[name='content']", "Test comment on news article")
|
page.fill("textarea[name='content']", "Test comment on news article")
|
||||||
page.click("button:has-text('Post')")
|
page.click("button:has-text('Post')")
|
||||||
page.wait_for_url(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
|
page.wait_for_url(f"{BASE_URL}/news/{slug}**", wait_until="domcontentloaded")
|
||||||
assert page.is_visible("text=Test comment on news article")
|
assert page.is_visible("text=Test comment on news article")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -193,6 +193,49 @@ def test_comment_and_vote_then_delete(alice):
|
|||||||
expect(comment).to_have_count(0)
|
expect(comment).to_have_count(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_reply_inline_form(alice):
|
||||||
|
page, _ = alice
|
||||||
|
create_post(page, "devlog", "Post for inline reply")
|
||||||
|
page.locator(".comment-form textarea[name='content']").fill("Parent comment here")
|
||||||
|
page.locator(".comment-form button:has-text('Post')").click()
|
||||||
|
expect(page.locator(".comment-text:has-text('Parent comment here')")).to_be_visible()
|
||||||
|
page.locator(".comment [data-action='reply']").first.click()
|
||||||
|
reply_form = page.locator(".comment-reply-form").first
|
||||||
|
expect(reply_form).to_be_visible()
|
||||||
|
reply_form.locator("textarea[name='content']").fill("Inline reply text")
|
||||||
|
reply_form.locator("button:has-text('Post')").click()
|
||||||
|
expect(page.locator(".comment-replies .comment-text:has-text('Inline reply text')")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_reply_toggle_and_cancel(alice):
|
||||||
|
page, _ = alice
|
||||||
|
create_post(page, "devlog", "Post for reply toggle")
|
||||||
|
page.locator(".comment-form textarea[name='content']").fill("Toggle parent")
|
||||||
|
page.locator(".comment-form button:has-text('Post')").click()
|
||||||
|
expect(page.locator(".comment-text:has-text('Toggle parent')")).to_be_visible()
|
||||||
|
reply_btn = page.locator(".comment [data-action='reply']").first
|
||||||
|
reply_btn.click()
|
||||||
|
expect(page.locator(".comment-reply-form")).to_have_count(1)
|
||||||
|
reply_btn.click()
|
||||||
|
expect(page.locator(".comment-reply-form")).to_have_count(0)
|
||||||
|
reply_btn.click()
|
||||||
|
expect(page.locator(".comment-reply-form")).to_have_count(1)
|
||||||
|
page.locator(".comment-reply-cancel").first.click()
|
||||||
|
expect(page.locator(".comment-reply-form")).to_have_count(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_create_scrolls_to_anchor(alice):
|
||||||
|
page, _ = alice
|
||||||
|
create_post(page, "devlog", "Post for comment anchor")
|
||||||
|
page.locator(".comment-form textarea[name='content']").fill("Anchor comment text")
|
||||||
|
page.locator(".comment-form button:has-text('Post')").click()
|
||||||
|
expect(page.locator(".comment-text:has-text('Anchor comment text')")).to_be_visible()
|
||||||
|
assert "#comment-" in page.url
|
||||||
|
comment_uid = page.url.split("#comment-")[1]
|
||||||
|
page.wait_for_selector(f"#comment-{comment_uid}.comment-highlight", timeout=5000)
|
||||||
|
expect(page.locator(f"#comment-{comment_uid}")).to_contain_text("Anchor comment text")
|
||||||
|
|
||||||
|
|
||||||
def test_post_edit_button(alice):
|
def test_post_edit_button(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||||
|
|||||||
@ -316,8 +316,9 @@ def test_project_comment_reply(alice):
|
|||||||
page.wait_for_timeout(500)
|
page.wait_for_timeout(500)
|
||||||
assert page.is_visible("text=First comment")
|
assert page.is_visible("text=First comment")
|
||||||
page.click("button:has-text('Reply')")
|
page.click("button:has-text('Reply')")
|
||||||
page.fill("textarea[name='content']", "Reply to comment")
|
reply_form = page.locator(".comment-reply-form").first
|
||||||
page.click("button:has-text('Post')")
|
reply_form.locator("textarea[name='content']").fill("Reply to comment")
|
||||||
|
reply_form.locator("button:has-text('Post')").click()
|
||||||
page.wait_for_timeout(500)
|
page.wait_for_timeout(500)
|
||||||
assert page.is_visible("text=Reply to comment")
|
assert page.is_visible("text=Reply to comment")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user