diff --git a/tests/test_admin.py b/tests/test_admin.py index ef2d45d..140f661 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -42,7 +42,7 @@ def seed_extra_users(count=30): def test_admin_redirect_unauth(page, app_server): 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): diff --git a/tests/test_auth.py b/tests/test_auth.py index 8d86012..db0b4fe 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -260,3 +260,19 @@ def test_password_toggle(page, app_server): assert pw_input.get_attribute("type") == "text" toggle.click() 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") diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 4964488..dcb9e7b 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -89,8 +89,9 @@ def test_bug_comment_reply(alice): page.wait_for_timeout(500) assert page.is_visible("text=Root comment") page.click("button:has-text('Reply')") - page.fill("textarea[name='content']", "Nested reply") - page.click("button:has-text('Post')") + reply_form = page.locator(".comment-reply-form").first + reply_form.locator("textarea[name='content']").fill("Nested reply") + reply_form.locator("button:has-text('Post')").click() page.wait_for_timeout(500) assert page.is_visible("text=Nested reply") diff --git a/tests/test_feed.py b/tests/test_feed.py index feb5295..18a118e 100644 --- a/tests/test_feed.py +++ b/tests/test_feed.py @@ -31,6 +31,41 @@ def _seed_posts(count): 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): page, _ = alice 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): 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.locator(".feed-fab").click() - page.check("input[value='devlog']") - 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 + card = page.locator(".post-card").filter(has_text=marker).first + inline_form = card.locator(".comment-form") expect(inline_form).to_be_visible() - inline_form.locator("input[name='content']").fill("Inline comment from feed") - inline_form.locator(".feed-comment-submit").click() + inline_form.locator("textarea[name='content']").fill("Inline comment from feed") + inline_form.locator("button.comment-form-submit").click() 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() def test_feed_inline_comment_placeholder(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.locator(".feed-fab").first.click() - page.fill("#post-content", "Placeholder check post " + "x" * 20) - 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 + card = page.locator(".post-card").filter(has_text=marker).first + inline_input = card.locator(".comment-form textarea[name='content']") expect(inline_input).to_be_visible() 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): page, _ = alice 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): + _seed_post_with_comments([f"{uuid4().hex[:6]}-c"]) 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 diff --git a/tests/test_news.py b/tests/test_news.py index 741dc6a..4eec5f8 100644 --- a/tests/test_news.py +++ b/tests/test_news.py @@ -89,7 +89,7 @@ def test_news_comment(alice, news_article): page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded") page.fill("textarea[name='content']", "Test comment on news article") 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") diff --git a/tests/test_post.py b/tests/test_post.py index 25a17fe..e0d0b83 100644 --- a/tests/test_post.py +++ b/tests/test_post.py @@ -193,6 +193,49 @@ def test_comment_and_vote_then_delete(alice): 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): page, _ = alice page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") diff --git a/tests/test_projects.py b/tests/test_projects.py index 8025b4e..1d0b894 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -316,8 +316,9 @@ def test_project_comment_reply(alice): page.wait_for_timeout(500) assert page.is_visible("text=First comment") page.click("button:has-text('Reply')") - page.fill("textarea[name='content']", "Reply to comment") - page.click("button:has-text('Post')") + reply_form = page.locator(".comment-reply-form").first + reply_form.locator("textarea[name='content']").fill("Reply to comment") + reply_form.locator("button:has-text('Post')").click() page.wait_for_timeout(500) assert page.is_visible("text=Reply to comment")