Compare commits

...
6 Commits
Author SHA1 Message Date
typosaurus a7f7df7714 test(sveta): Extend vote-state tests to assert the non-color cue
DevPlace CI / test (pull_request) Failing after 1h37m29s
Outcome: done
Changed: tests/conftest.py:293-300, tests/e2e/post.py:5,74-157, tests/e2e/posts/index.py:17,396-418, tests/e2e/notifications/index.py:313,799, tests/e2e/leaderboard.py:42
Verified by: python -m pytest tests/e2e/post.py tests/e2e/posts/index.py tests/e2e/notifications/index.py tests/e2e/leaderboard.py -> 68 passed (verify() passed); make test full suite -> 3344 passed, 1 skipped, 10 failed (9x tests/api/projects/workspace.py: docker binary absent in this environment, same as prior sibling run; 1x tests/e2e/game/farm.py::test_victim_notification_names_the_raider_and_the_amount, passes standalone and 9/9 in its file, game code has no vote markup)
Findings: New vote_glyph helper (tests/conftest.py:293) reads getComputedStyle(el,'::before').content and normalizes CSS escapes; glyph assertions cover post upvote (+ -> U+2295), post downvote (U+2212 -> U+2296), and comment upvote (+ -> U+2295), each also asserting unvoted glyph absence and toggle-off restoration to the unvoted glyph. New test fails against pre-change code: on 72e11db, test_post_upvote_voted_state_persists fails with "assert 'none' == '+'" (no ::before rule existed; buttons held literal text). Fixed all three has_text="+" vote locators (notifications/index.py:313,799, leaderboard.py:42) to class-based button.post-action-btn.vote-up; the three tests now pass. test_comment_voted_state_persists was made deterministic by wrapping the click in expect_response (pre-existing reload race); it now passes reliably

Typosaurus-Run: c36faeceb55d45b6bf38a45eec4f47ab
Typosaurus-Node: 9b6a5ec294a2472e800fbfeed775b27b
Typosaurus-Agent: @sveta
Refs: #162
2026-08-16 01:12:14 +00:00
typosaurus 285bb25c06 feat(zoya): Review the vote-button change map against the acceptance criteria
Outcome: done
Changed: none
Verified by: e2e run attempted (3 tests) but blocked at browser launch by missing libglib-2.0.so.0 (environment fault, not a result); breakage proven from Playwright source elementText() in driver coreBundle.js plus empty-button markup
Findings: REJECT - the implementation empties all four vote buttons (_post_votes.html:4,9; _comment.html:6,11) and moves glyphs into CSS ::before, so has_text="+" locators at tests/e2e/notifications/index.py:313, :799 and tests/e2e/leaderboard.py:42 match zero elements (Playwright text matching reads DOM text nodes, never ::before content) and those 3 tests time out
Findings: commit 625940a claimed "Changed: none" but deleted the glyph text from post-downvote and comment-upvote, leaving HEAD with two glyph-less vote buttons (regression only repaired by the uncommitted diff adding the ::before rules)
Findings: map line references are accurate for 625940a^ (text glyphs, no ::before rules) but stale for the working tree, where glyph rules are feed.css:193-217 and post.css:141-165
Findings: the voted cue itself satisfies the ticket - `+`->U+2295 and U+2212/U+002D->U+2296 under .voted (feed.css:211-217, post.css:159-165), non-color, consistent with the star precedent base.css:1319-1325, with aria-pressed synced client-side (VoteManager.js:60-62)
Findings: unvoted downvote glyphs remain inconsistent in touched rules: post U+2212 (feed.css:198) vs comment U+002D (post.css:146)
Open: fix the 3 e2e locators to class-based .vo

Typosaurus-Run: c36faeceb55d45b6bf38a45eec4f47ab
Typosaurus-Node: ebdbae6c04a84a239287bd62262cb254
Typosaurus-Agent: @zoya
Refs: #162
2026-08-16 00:23:16 +00:00
typosaurus 338f023c0d feat(mila): Confirm the star precedent, enumerate test dependencies, recommend the non-color
Outcome: done
Changed: none
Verified by: not verified (read-only node; no files changed)
Findings: RECOMMENDATION TO BUILDER: swap the glyph in the voted state only — upvote `+` (U+002B) → `⊕` (U+2295), downvote `−`/`-` (U+2212/U+002D) → `⊖` (U+2296) — via template conditionals in _post_votes.html:4,9 and _comment.html:6,11 plus a VoteManager.js render() textContent mirror scoped to `.vote-up`/`.vote-down` only (VoteManager also binds `.vote-star` forms, whose `::before` glyph must not be overwritten).
Findings: Precedent confirmed at base.css:1319-1325: `.vote-star::before` swaps outline `\2606` (line 1320) to filled `\2605` (line 1324) under `.voted`; templates/CLAUDE.md:70 confirms the star glyph is pseudo-element-only, while the +/- glyphs are text nodes, so a pure-CSS swap would require moving them into `::before`.
Findings: Only 3 of 18 vote-button test hits are text-coupled — tests/e2e/notifications/index.py:313, :799 and tests/e2e/leaderboard.py:42, all `button.post-action-btn` filtered by `has_text="+"`; each resolves immediately before the click, so a voted-state-only glyph swap that preserves the unvoted `+` text breaks none of them.
Findings: The other 15 hits (posts/index.py:407,413; projects/devlog.py:133-134; feed.py:687; post.py:63,70,80,83,87,97,100,104,187) and all comment tests (post.py:117-124,218-221,274-278) use `.vote-up`/`.vote-down`/`.comment-vote-btn` classes and `\bvoted\b` assertions, so they break only if those class names change.
Findings: Empiri

Typosaurus-Run: c36faeceb55d45b6bf38a45eec4f47ab
Typosaurus-Node: 9f61b1db85c147a6b2b8376a6c8ae798
Typosaurus-Agent: @mila
Refs: #162
2026-08-16 00:23:16 +00:00
retoorandClaude Sonnet 5 8db0efff29 Fix Devii open-trigger race and close two flaky e2e waits
DevPlace CI / test (push) Failing after 1h30m15s
DeviiTerminal bound [data-devii-open] click listeners only after the
async /devii/session fetch resolved, silently dropping early clicks.
Switch to a single delegated document listener bound in the
constructor, matching the ModalManager/dp-lightbox pattern.

The steal-confirm and comment-vote e2e tests asserted DOM state right
after a click with no wait for the triggering POST to land, racing the
server under CI load. Wrap those clicks in page.expect_response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 02:02:34 +02:00
retoor fa8751a4ca Uppdate
DevPlace CI / test (push) Failing after 1h27m55s
2026-08-15 20:38:24 +02:00
retoorandClaude Sonnet 5 682be0861f Fail fast with one clear diagnostic when app_server dies mid-session
A shared-server crash mid-suite previously cascaded into hundreds of
opaque connection-refused errors across every later api/e2e test,
making the real cause invisible. pytest_runtest_setup now polls the
tracked subprocess and, on the first test after it exits, reports the
exit code plus the server's own log tail once instead of forcing every
subsequent test to fail blind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:25:39 +02:00
15 changed files with 168 additions and 46 deletions
+2
View File
@@ -57,8 +57,10 @@
}
.auth-field-check input {
width: auto;
margin-top: 0.2rem;
flex: none;
accent-color: var(--accent);
}
.auth-field-check a {
+16
View File
@@ -190,6 +190,14 @@
color: var(--danger);
}
.post-action-btn.vote-up::before {
content: "+";
}
.post-action-btn.vote-down::before {
content: "\2212";
}
.post-action-btn.vote-up.voted {
color: var(--accent);
font-weight: 700;
@@ -200,6 +208,14 @@
font-weight: 700;
}
.post-action-btn.vote-up.voted::before {
content: "\2295";
}
.post-action-btn.vote-down.voted::before {
content: "\2296";
}
.post-votes {
display: inline-flex;
align-items: center;
+16
View File
@@ -138,6 +138,14 @@
color: var(--danger);
}
.comment-vote-btn.vote-up::before {
content: "+";
}
.comment-vote-btn.vote-down::before {
content: "-";
}
.comment-vote-btn.vote-up.voted {
color: var(--accent);
font-weight: 700;
@@ -148,6 +156,14 @@
font-weight: 700;
}
.comment-vote-btn.vote-up.voted::before {
content: "\2295";
}
.comment-vote-btn.vote-down.voted::before {
content: "\2296";
}
.comment-vote-count {
font-size: 0.75rem;
font-weight: 700;
+11 -7
View File
@@ -10,6 +10,7 @@ export class DeviiTerminal {
this.avatar = null;
this.bindEscapeTrigger();
this.bindDoubleClickTrigger();
this.bindOpenTrigger();
this.ready = this.init();
}
@@ -29,16 +30,19 @@ export class DeviiTerminal {
this.element.setAttribute("avatar", "devii");
document.body.appendChild(this.element);
this.bindTriggers();
this.maybeAutoOpen();
}
bindTriggers() {
document.querySelectorAll("[data-devii-open]").forEach((trigger) => {
trigger.addEventListener("click", (event) => {
event.preventDefault();
this.open(trigger.dataset.deviiPrompt || "");
});
bindOpenTrigger() {
document.addEventListener("click", (event) => {
const trigger = event.target.closest("[data-devii-open]");
if (!trigger) return;
event.preventDefault();
this.open(trigger.dataset.deviiPrompt || "");
});
}
maybeAutoOpen() {
const stored = this.element && this.element.hasStoredState && this.element.hasStoredState();
if (document.querySelector("[data-devii-autoopen]") && !stored) {
this.open();
+3 -1
View File
@@ -57,7 +57,9 @@ export class VoteManager extends OptimisticAction {
});
document.querySelectorAll(`form[action="${action}"] button[type="submit"]`).forEach((button) => {
const formValue = parseInt(button.closest("form").querySelector('input[name="value"]').value, 10);
button.classList.toggle("voted", result.value !== 0 && formValue === result.value);
const isVoted = result.value !== 0 && formValue === result.value;
button.classList.toggle("voted", isVoted);
button.setAttribute("aria-pressed", String(isVoted));
});
}
}
+2 -1
View File
@@ -66,7 +66,7 @@ Do NOT hand-write the overlay/header markup. Use the shared macro in `templates/
Reuse these via `{% set _x = ... %}{% include %}` (the `_avatar_link.html` convention) instead of copy-pasting markup:
- `_post_composer_form.html` - the create-post form (topic selector, content/title, project select, attachments, poll builder, footer). Locals: `_composer_topic` (preselected topic, default `random`), `_composer_project` (preselected project uid or `""`). Wrapped in the `modal()` macro by `feed.html` (Create New Post) and `project_detail.html` (owner-only Post an update, preset to `devlog` + the project). Never fork a second copy of this form.
- `_post_votes.html` - post +/- vote bar. Locals: `_uid`, `_my_vote`, `_count`.
- `_post_votes.html` - post +/- vote bar. Locals: `_uid`, `_my_vote`, `_count`. The +/- glyphs come from the vote CSS classes via `::before` (`feed.css` for `.post-action-btn`, `post.css` for `.comment-vote-btn`), swapping to `\u2295`/`\u2296` when `.voted` - do not put literal glyphs in markup.
- `_star_vote.html` - project/gist star button. Locals: `_type` (`project`|`gist`), `_uid`, `_my_vote`, `_count`, `_btn_class`, optional `_stop` (adds `data-stop-propagation`). The star glyph (`☆`→`★` when `.voted`) comes from the `vote-star` CSS class via `::before` (`base.css`) - do not put a literal star in markup.
- `_post_header.html` - post author/avatar/time header (`.post-header`). Locals: `_author`, `_time`.
- `_topic_selector.html` - topic radio group. Locals: `_topics`, `_selected`.
@@ -89,3 +89,4 @@ The three public listings (`/feed`, `/gists`, `/projects`) share one search box:
## Modal pattern
`Application.js` `initModals()` toggles a `.visible` CSS class on `.modal-overlay`; the CSS rule `.modal-overlay.visible { display: flex; }` handles visibility. Triggers usually have `href="#"`, so call `e.preventDefault()` in click handlers. `.modal-close` is wired generically - no inline JS needed.
+3 -2
View File
@@ -3,12 +3,12 @@
<div class="comment-votes" role="group" aria-label="Comment votes">
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
<input type="hidden" name="value" value="1">
<button type="submit" class="comment-vote-btn vote-up{% if item.my_vote == 1 %} voted{% endif %}" aria-label="Upvote" title="Upvote" aria-pressed="{% if item.my_vote == 1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}>+</button>
<button type="submit" class="comment-vote-btn vote-up{% if item.my_vote == 1 %} voted{% endif %}" aria-label="Upvote" title="Upvote" aria-pressed="{% if item.my_vote == 1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}></button>
</form>
<span class="comment-vote-count" data-vote-count="{{ item.comment['uid'] }}">{{ item.votes.up - item.votes.down }}</span>
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
<input type="hidden" name="value" value="-1">
<button type="submit" class="comment-vote-btn vote-down{% if item.my_vote == -1 %} voted{% endif %}" aria-label="Downvote" title="Downvote" aria-pressed="{% if item.my_vote == -1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}>-</button>
<button type="submit" class="comment-vote-btn vote-down{% if item.my_vote == -1 %} voted{% endif %}" aria-label="Downvote" title="Downvote" aria-pressed="{% if item.my_vote == -1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}></button>
</form>
</div>
@@ -70,3 +70,4 @@
{{ render_comment_flat(child, depth, item.author) }}
{% endfor %}
{% endmacro %}
+3 -2
View File
@@ -1,11 +1,12 @@
<div class="post-votes" role="group" aria-label="Post votes">
<form method="POST" action="/votes/post/{{ _uid }}" class="inline-form">
<input type="hidden" name="value" value="-1">
<button type="submit" class="post-action-btn vote-down{% if _my_vote == -1 %} voted{% endif %}" aria-label="Downvote" title="Downvote" aria-pressed="{% if _my_vote == -1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}>−</button>
<button type="submit" class="post-action-btn vote-down{% if _my_vote == -1 %} voted{% endif %}" aria-label="Downvote" title="Downvote" aria-pressed="{% if _my_vote == -1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}></button>
</form>
<span class="post-vote-count" data-vote-count="{{ _uid }}">{{ _count }}</span>
<form method="POST" action="/votes/post/{{ _uid }}" class="inline-form">
<input type="hidden" name="value" value="1">
<button type="submit" class="post-action-btn vote-up{% if _my_vote == 1 %} voted{% endif %}" aria-label="Upvote" title="Upvote" aria-pressed="{% if _my_vote == 1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}>+</button>
<button type="submit" class="post-action-btn vote-up{% if _my_vote == 1 %} voted{% endif %}" aria-label="Upvote" title="Upvote" aria-pressed="{% if _my_vote == 1 %}true{% else %}false{% endif %}"{{ guest_disabled(user) }}></button>
</form>
</div>
+1 -1
View File
@@ -69,7 +69,7 @@
<div class="auth-field auth-field-check">
<label for="accept_terms">
<input type="checkbox" id="accept_terms" name="accept_terms" value="1" required aria-required="true"{% if accept_terms %} checked{% endif %}>
I accept the <a href="/docs/terms.html" target="_blank" rel="noopener">Terms of Service</a> and the <a href="/docs/community-guidelines.html" target="_blank" rel="noopener">Community Guidelines</a>, and I have read the <a href="/docs/privacy.html" target="_blank" rel="noopener">Privacy Policy</a>.
<span>I accept the <a href="/docs/terms.html" target="_blank" rel="noopener">Terms of Service</a> and the <a href="/docs/community-guidelines.html" target="_blank" rel="noopener">Community Guidelines</a>, and I have read the <a href="/docs/privacy.html" target="_blank" rel="noopener">Privacy Policy</a>.</span>
</label>
</div>
+40
View File
@@ -125,6 +125,13 @@ def _port_in_use(port):
return sock.connect_ex(("127.0.0.1", port)) == 0
# Set once app_server has successfully started, so pytest_runtest_setup can
# detect a mid-session crash (e.g. resource exhaustion on a constrained CI
# runner) and fail every subsequent HTTP-dependent test with one clear
# diagnostic instead of a wall of opaque connection-refused errors.
_APP_SERVER_STATE = {"proc": None, "log_path": None}
@pytest.fixture(scope="session")
def app_server(test_db_path):
if _port_in_use(PORT):
@@ -192,7 +199,11 @@ def app_server(test_db_path):
_set_setting("rate_limit_per_minute", "1000000")
_set_setting("rate_limit_window_seconds", "60")
_APP_SERVER_STATE["proc"] = proc
_APP_SERVER_STATE["log_path"] = log_file.name
yield proc
_APP_SERVER_STATE["proc"] = None
try:
proc.terminate()
proc.wait(timeout=10)
@@ -207,6 +218,27 @@ def app_server(test_db_path):
pass
def pytest_runtest_setup(item):
proc = _APP_SERVER_STATE["proc"]
if proc is None or "tests/unit/" in item.nodeid:
return
exit_code = proc.poll()
if exit_code is None:
return
log_path = _APP_SERVER_STATE["log_path"]
try:
with open(log_path, "r", errors="replace") as f:
log_tail = f.read()[-5000:]
except OSError:
log_tail = "(log unavailable)"
pytest.fail(
f"app_server died mid-session (exit code {exit_code}). It was healthy "
f"earlier in this run and has since crashed - this is a shared-server "
f"failure, not a bug in {item.nodeid}. Server log tail:\n{log_tail}",
pytrace=False,
)
@pytest.fixture(scope="session")
def playwright_instance():
from playwright.sync_api import sync_playwright
@@ -290,6 +322,14 @@ def assert_share_copies(page, expected_fragment):
)
def vote_glyph(locator):
value = locator.evaluate("el => getComputedStyle(el, '::before').content")
value = value.strip().strip('"').strip("'")
if value.startswith("\\") and len(value) == 5 and value[1:].isdigit():
value = chr(int(value[1:], 16))
return value
def assert_no_horizontal_overflow(page, layout):
report = page.evaluate(
"""(selector) => {
+4 -2
View File
@@ -128,7 +128,8 @@ def test_steal_takes_build_and_pays_thief(bob):
steal.click()
confirm_btn = page.locator(".dialog-confirm")
confirm_btn.wait_for(state="visible")
confirm_btn.click()
with page.expect_response(lambda r: "/steal" in r.url and r.request.method == "POST"):
confirm_btn.click()
expect(page.locator("form[data-game-action='steal']")).to_have_count(0)
page.wait_for_timeout(400)
assert _farm_coins("bob_test") == 18
@@ -165,7 +166,8 @@ def test_victim_notification_names_the_raider_and_the_amount(bob):
steal.click()
confirm_btn = page.locator(".dialog-confirm")
confirm_btn.wait_for(state="visible")
confirm_btn.click()
with page.expect_response(lambda r: "/steal" in r.url and r.request.method == "POST"):
confirm_btn.click()
expect(page.locator("form[data-game-action='steal']")).to_have_count(0)
page.wait_for_timeout(400)
note = get_table("notifications").find_one(
+1 -1
View File
@@ -39,7 +39,7 @@ def test_leaderboard_ranks_after_upvote(app_server, browser, seeded_db):
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
vote_btn = pa.locator("button.post-action-btn.vote-up").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
+2 -2
View File
@@ -310,7 +310,7 @@ def test_vote_notification_on_post(app_server, browser, seeded_db):
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
vote_btn = pa.locator("button.post-action-btn.vote-up").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
@@ -796,7 +796,7 @@ def test_vote_notification_click_opens_target(app_server, browser, seeded_db):
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 = pa.locator("button.post-action-btn.vote-up").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
+55 -22
View File
@@ -2,7 +2,7 @@
import re
from playwright.sync_api import expect
from tests.conftest import BASE_URL, assert_share_copies
from tests.conftest import BASE_URL, assert_share_copies, vote_glyph
def create_post(page, topic="random", content="Test post content", title=None):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
@@ -74,35 +74,57 @@ def test_post_vote_increment(alice):
def test_post_upvote_voted_state_persists(alice):
page, _ = alice
create_post(page, "showcase", "Upvote persistence test")
vote_up = page.locator(".post-action-btn.vote-up").first
assert vote_glyph(vote_up) == "+"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
page.locator(".post-action-btn.vote-up").first.click()
vote_up.click()
expect(page.locator(".post-vote-count").first).to_have_text("1")
expect(vote_up).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_up) == "\u2295"
page.reload(wait_until="domcontentloaded")
expect(page.locator(".post-action-btn.vote-up").first).to_have_class(
re.compile(r"\bvoted\b")
)
assert "voted" not in (
page.locator(".post-action-btn.vote-down").first.get_attribute("class") or ""
)
vote_up = page.locator(".post-action-btn.vote-up").first
expect(vote_up).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_up) == "\u2295"
vote_down = page.locator(".post-action-btn.vote-down").first
assert "voted" not in (vote_down.get_attribute("class") or "")
assert vote_glyph(vote_down) == "\u2212"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
vote_up.click()
expect(page.locator(".post-vote-count").first).to_have_text("0")
expect(vote_up).not_to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_up) == "+"
def test_post_downvote_voted_state_persists(alice):
page, _ = alice
create_post(page, "showcase", "Downvote persistence test")
vote_down = page.locator(".post-action-btn.vote-down").first
assert vote_glyph(vote_down) == "\u2212"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
page.locator(".post-action-btn.vote-down").first.click()
vote_down.click()
expect(page.locator(".post-vote-count").first).to_have_text("-1")
expect(vote_down).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_down) == "\u2296"
page.reload(wait_until="domcontentloaded")
expect(page.locator(".post-action-btn.vote-down").first).to_have_class(
re.compile(r"\bvoted\b")
)
assert "voted" not in (
page.locator(".post-action-btn.vote-up").first.get_attribute("class") or ""
)
vote_down = page.locator(".post-action-btn.vote-down").first
expect(vote_down).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_down) == "\u2296"
vote_up = page.locator(".post-action-btn.vote-up").first
assert "voted" not in (vote_up.get_attribute("class") or "")
assert vote_glyph(vote_up) == "+"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
vote_down.click()
expect(page.locator(".post-vote-count").first).to_have_text("0")
expect(vote_down).not_to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_down) == "\u2212"
def test_comment_voted_state_persists(alice):
@@ -114,14 +136,25 @@ def test_comment_voted_state_persists(alice):
expect(
page.locator(".comment-text:has-text('Comment whose vote should persist')")
).to_be_visible()
page.locator(".comment-vote-btn").first.click()
expect(page.locator(".comment-vote-btn").first).to_have_class(
re.compile(r"\bvoted\b")
)
comment_vote = page.locator(".comment-vote-btn.vote-up").first
assert vote_glyph(comment_vote) == "+"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
comment_vote.click()
expect(comment_vote).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(comment_vote) == "\u2295"
page.reload(wait_until="domcontentloaded")
expect(page.locator(".comment-vote-btn").first).to_have_class(
re.compile(r"\bvoted\b")
)
comment_vote = page.locator(".comment-vote-btn.vote-up").first
expect(comment_vote).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(comment_vote) == "\u2295"
with page.expect_response(
lambda r: "/votes/" in r.url and r.request.method == "POST"
):
comment_vote.click()
expect(page.locator(".comment-vote-count").first).to_have_text("0")
expect(comment_vote).not_to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(comment_vote) == "+"
def test_mention_first_match_highlighted(alice):
+9 -5
View File
@@ -14,7 +14,7 @@ def _create_plain_post_engagement_ui(page, content):
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from tests.conftest import BASE_URL, assert_share_copies
from tests.conftest import BASE_URL, assert_share_copies, vote_glyph
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def _seed_posts(count):
@@ -404,15 +404,19 @@ def test_feed_vote_voted_state_persists(alice):
card = page.locator(
".post-card", has_text="Feed vote persistence test content"
).first
card.locator(".post-action-btn.vote-up").click()
vote_up = card.locator(".post-action-btn.vote-up")
assert vote_glyph(vote_up) == "+"
vote_up.click()
expect(card.locator(".post-vote-count")).to_have_text("1")
expect(vote_up).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_up) == "\u2295"
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(
".post-card", has_text="Feed vote persistence test content"
).first
expect(card.locator(".post-action-btn.vote-up")).to_have_class(
re.compile(r"\bvoted\b")
)
vote_up = card.locator(".post-action-btn.vote-up")
expect(vote_up).to_have_class(re.compile(r"\bvoted\b"))
assert vote_glyph(vote_up) == "\u2295"
def test_feed_card_share_button(alice):