forked from retoor/devplacepy
feat: add threaded comments with polymorphic targets and slug-based routing
Introduce `load_comments` helper supporting polymorphic target types (post, project, bug) with nested parent-child threading. Refactor comment creation to use `target_type`/`target_uid` and `resolve_target_redirect` for correct redirects. Replace raw UID-based post/project routes with slug-aware resolution via `resolve_post`/`resolve_project` and `make_combined_slug`. Update SEO, sitemap, and FAB styling to use slugs and hardcoded red accent. Add reusable `_comment_section.html` template.
This commit is contained in:
+9
-6
@@ -6,6 +6,11 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
BASE_URL = "http://127.0.0.1:10501"
|
||||
SCREENSHOT_DIR = Path("/tmp/devplace_test_screenshots")
|
||||
|
||||
_TEST_DB = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
_TEST_DB.close()
|
||||
os.environ["DEVPLACE_DATABASE_URL"] = f"sqlite:///{_TEST_DB.name}"
|
||||
os.environ["SECRET_KEY"] = "test-secret-key"
|
||||
|
||||
|
||||
def save_failure_screenshot(page, test_name):
|
||||
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
@@ -32,21 +37,19 @@ def pytest_runtest_makereport(item, call):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db_path():
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
tmp.close()
|
||||
yield tmp.name
|
||||
yield _TEST_DB.name
|
||||
try:
|
||||
os.unlink(tmp.name)
|
||||
os.unlink(_TEST_DB.name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_server(test_db_path):
|
||||
env = os.environ.copy()
|
||||
env["DEVPLACE_DATABASE_URL"] = f"sqlite:///{test_db_path}"
|
||||
env["SECRET_KEY"] = "test-secret-key"
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
|
||||
|
||||
+65
-8
@@ -14,18 +14,13 @@ def test_bugs_create_modal(alice):
|
||||
assert page.is_visible("h3:has-text('Report a Bug')")
|
||||
|
||||
|
||||
def test_bugs_create_submit(alice):
|
||||
def test_bugs_empty_state(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
page.click("button:has-text('Report Bug')")
|
||||
page.fill("#bug-title", "Test bug report")
|
||||
page.fill("#bug-description", "This is a test bug description")
|
||||
page.click("button:has-text('Submit Report')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Test bug report")
|
||||
assert page.is_visible("text=No bug reports yet.")
|
||||
|
||||
|
||||
def test_bugs_empty_state(alice):
|
||||
def test_bugs_create_submit(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=No bug reports yet.")
|
||||
@@ -53,3 +48,65 @@ def test_bugs_unauth_empty(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
assert page.is_visible("h1:has-text('Bug Reports')")
|
||||
|
||||
|
||||
def test_bug_comments_form_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
page.click("button:has-text('Report Bug')")
|
||||
page.fill("#bug-title", "Bug with comments")
|
||||
page.fill("#bug-description", "Testing comment form on bug")
|
||||
page.click("button:has-text('Submit Report')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Comments")
|
||||
assert page.is_visible("textarea[name='content']")
|
||||
|
||||
|
||||
def test_bug_comment_create(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
page.click("button:has-text('Report Bug')")
|
||||
page.fill("#bug-title", "Bug comment test")
|
||||
page.fill("#bug-description", "Bug description for comment")
|
||||
page.click("button:has-text('Submit Report')")
|
||||
page.wait_for_timeout(500)
|
||||
page.fill("textarea[name='content']", "Bug comment here")
|
||||
page.click("button:has-text('Post')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Bug comment here")
|
||||
|
||||
|
||||
def test_bug_comment_reply(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
page.click("button:has-text('Report Bug')")
|
||||
page.fill("#bug-title", "Bug threaded reply")
|
||||
page.fill("#bug-description", "Bug for thread test")
|
||||
page.click("button:has-text('Submit Report')")
|
||||
page.wait_for_timeout(500)
|
||||
page.fill("textarea[name='content']", "Root comment")
|
||||
page.click("button:has-text('Post')")
|
||||
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')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Nested reply")
|
||||
|
||||
|
||||
def test_bug_comment_delete(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
page.click("button:has-text('Report Bug')")
|
||||
page.fill("#bug-title", "Bug delete comment")
|
||||
page.fill("#bug-description", "Bug for delete test")
|
||||
page.click("button:has-text('Submit Report')")
|
||||
page.wait_for_timeout(500)
|
||||
page.fill("textarea[name='content']", "Delete me")
|
||||
page.click("button:has-text('Post')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Delete me")
|
||||
page.locator(".comment-body:has-text('Delete me') .comment-action-btn:has-text('Delete')").click()
|
||||
page.wait_for_timeout(500)
|
||||
assert not page.is_visible("text=Delete me")
|
||||
|
||||
+4
-3
@@ -105,10 +105,11 @@ def test_full_user_journey(browser, app_server):
|
||||
pa.locator("#platforms-input").fill(plat)
|
||||
pa.locator("#platforms-input").press("Enter")
|
||||
pa.click("button:has-text('Create Project')")
|
||||
pa.wait_for_timeout(500)
|
||||
pa.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=N8Nme")
|
||||
|
||||
# Second project — released
|
||||
pa.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
pa.locator("#create-project-btn").click()
|
||||
pa.fill("#title", "DWN")
|
||||
pa.fill("#description", "Decentralized web node in Rust.")
|
||||
@@ -117,7 +118,7 @@ def test_full_user_journey(browser, app_server):
|
||||
pa.locator("#platforms-input").fill("Linux")
|
||||
pa.locator("#platforms-input").press("Enter")
|
||||
pa.click("button:has-text('Create Project')")
|
||||
pa.wait_for_timeout(500)
|
||||
pa.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=DWN")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
@@ -213,7 +214,7 @@ def test_full_user_journey(browser, app_server):
|
||||
pa.bring_to_front()
|
||||
pa.goto(post1_url)
|
||||
assert pa.is_visible("text=Nice work! What framework did you use?")
|
||||
pa.locator("textarea[name='content']").fill("Thanks! I used FastAPI with SQLAlchemy. Vanilla JS frontend.")
|
||||
pa.locator(".comment-form textarea[name='content']").fill("Thanks! I used FastAPI with SQLAlchemy. Vanilla JS frontend.")
|
||||
pa.click("button:has-text('Post')")
|
||||
pa.wait_for_timeout(500)
|
||||
assert pa.is_visible("text=Thanks! I used FastAPI")
|
||||
|
||||
+69
-11
@@ -52,7 +52,7 @@ def test_create_project_full(alice):
|
||||
page.locator("#platforms-input").fill("PC")
|
||||
page.locator("#platforms-input").press("Enter")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=Playwright Test Game")
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_create_project_software(alice):
|
||||
page.check("input[value='software']")
|
||||
page.check("input[value='Released']")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=CLI Tool")
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def test_create_project_mobile_app(alice):
|
||||
page.locator("#platforms-input").fill("Android")
|
||||
page.locator("#platforms-input").press("Enter")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=Mobile Messenger")
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ def test_project_search(alice):
|
||||
page.fill("#title", "SearchableProject")
|
||||
page.fill("#description", "Find me via search")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
page.fill("input[placeholder='Search projects...']", "SearchableProject")
|
||||
page.locator("input[placeholder='Search projects...']").press("Enter")
|
||||
page.wait_for_timeout(500)
|
||||
@@ -135,9 +136,6 @@ def test_project_detail_page(alice):
|
||||
page.fill("#title", "DetailTestProject")
|
||||
page.fill("#description", "Project detail page test")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
card = page.locator("a.project-card:has-text('DetailTestProject')").first
|
||||
card.click()
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=DetailTestProject")
|
||||
assert page.is_visible("text=Back to Projects")
|
||||
@@ -154,11 +152,71 @@ def test_project_detail_back_link(alice):
|
||||
page.fill("#title", "BackLinkProject")
|
||||
page.fill("#description", "Test back link")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_timeout(500)
|
||||
card = page.locator("a.project-card:has-text('BackLinkProject')").first
|
||||
card.click()
|
||||
page.wait_for_timeout(500)
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
back = page.locator("a:has-text('Back to Projects')")
|
||||
assert back.is_visible()
|
||||
back.click()
|
||||
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_project_comments_form_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "CommentProject")
|
||||
page.fill("#description", "Project for testing comments")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=Comments")
|
||||
assert page.is_visible("text=No comments yet")
|
||||
assert page.is_visible("textarea[name='content']")
|
||||
|
||||
|
||||
def test_project_comment_create(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "CommentCreateProj")
|
||||
page.fill("#description", "Project for creating a comment")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
page.fill("textarea[name='content']", "Great project!")
|
||||
page.click("button:has-text('Post')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Great project!")
|
||||
|
||||
|
||||
def test_project_comment_reply(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "CommentReplyProj")
|
||||
page.fill("#description", "Project for testing reply")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
page.fill("textarea[name='content']", "First comment")
|
||||
page.click("button:has-text('Post')")
|
||||
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')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Reply to comment")
|
||||
|
||||
|
||||
def test_project_comment_delete(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "CommentDeleteProj")
|
||||
page.fill("#description", "Project for testing delete")
|
||||
page.click("button:has-text('Create Project')")
|
||||
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
page.fill("textarea[name='content']", "Comment to delete")
|
||||
page.click("button:has-text('Post')")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible("text=Comment to delete")
|
||||
page.locator(".comment-action-btn:has-text('Delete')").click()
|
||||
page.wait_for_timeout(500)
|
||||
assert not page.is_visible("text=Comment to delete")
|
||||
|
||||
Reference in New Issue
Block a user