Skip to content

Commit 98a5d19

Browse files
authored
feat: add an elonw feature check and screenshot recipe (#61)
Adds a feature check and a screenshot recipe for phpbbmodders/elonw. **Feature check** (`feature_checks.py`) - Posts an external link and a link to the board, then checks in Chromium that: - only the external link is marked, with a tooltip and the same text for screen readers; - it opens in a new window, also after a script-driven link was clicked; - the link to the board opens in the same window. - Saves the UCP setting as 0, -1 and 1, and checks what is stored and that turning it off removes the script. - Answers requests to other hosts locally, so nothing leaves the machine. **Screenshot recipe** (`screenshots.py`) - Saves a post with both kinds of link and the UCP setting. Documented in the README. **Testing** Claude ran these checks: - `db-matrix.sh` with the check on phpBB 3.3 and 4.0 with SQLite, MySQL, MariaDB, PostgreSQL and SQL Server. - On the elonw code from before its fixes, the check fails the screen-reader, new-window and stored-value cases. - `tests/test-phpbb-test-board.sh` passes. - The recipe produced the screenshots in the elonw PR. --- Investigated and written by [Claude](https://www.anthropic.com/claude) on behalf of William Jacoby ([bonelifer](https://github.com/bonelifer)). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent c1e4db1 commit 98a5d19

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

‎phpbb-test-board/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ Saves PNG screenshots of an extension's or style's pages with [Playwright](https
138138

139139
`phpbbmodders/autodbbackup` saves its ACP settings set to daily gzip backups keeping seven, then runs the backup cron twice and saves phpBB's ACP Restore page listing the two backups, which it deletes afterwards.
140140

141+
`phpbbmodders/elonw` rewrites the first post of the busiest topic to hold an external link and a link to the board itself, then saves that post, showing the new-window icon on the external link only, and the UCP setting. Use `--seed` with seed-forum's standard fixtures.
142+
141143
`phpbbmodders/genders` gives every member a gender and saves the UCP selector, a topic, a profile and the ACP settings with the defaults, then the same topic, profile and settings with gender text before the icon, the phpBB 3.0 MOD prosilver icon set and gender in the main profile details. Use `--seed` with seed-forum's standard fixtures.
142144

143145
`phpbbmodders/knowledgebase` makes its own categories, articles (live, waiting for review, with a revision, and a draft) and forum questions, so a board search for `backup` shows its block; `--seed` with seed-forum's standard fixtures adds members to write them. Its two ACP pages are taken in full, so a new setting is not cut off.

‎phpbb-test-board/feature_checks.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,8 +1097,103 @@ def run_cron(times: int) -> None:
10971097
b.purge_cache()
10981098

10991099

1100+
def check_elonw(b: Board) -> None:
1101+
"""Post external and same-site links, then check marking, new windows and the UCP setting in a browser."""
1102+
# Only this check needs a browser
1103+
from playwright.sync_api import TimeoutError as PlaywrightTimeout, sync_playwright
1104+
1105+
forum = b.sql("SELECT forum_id FROM phpbb_posts WHERE post_id = 1")[0][0]
1106+
external = "https://example.org/elonw-probe"
1107+
internal = f"{b.base}/index.php"
1108+
admin = requests.Session()
1109+
sid = b.login(admin)
1110+
page_url = f"{b.base}/posting.php?mode=post&f={forum}&sid={sid}"
1111+
action, fields = form_fields(admin.get(page_url, timeout=20).text, "postform")
1112+
title = "elonw probe " + secrets.token_hex(4) # unique, so a rerun on the same board reads its own topic
1113+
post_back(admin, b.base, page_url, action, {**fields, "subject": title, "post": "Submit",
1114+
"message": f"[url]{external}[/url] and [url]{internal}[/url]"})
1115+
topic = b.sql("SELECT topic_id FROM phpbb_topics WHERE topic_title = ?", (title,))
1116+
ok(topic, "the probe topic was posted")
1117+
if not topic:
1118+
return
1119+
topic_url = f"{b.base}/viewtopic.php?t={topic[0][0]}"
1120+
ok("elonw.js" in requests.get(topic_url, timeout=20).text, "guests get the script by default")
1121+
1122+
def external_stub(route) -> None:
1123+
"""Answer every request to another host locally, so nothing leaves the machine."""
1124+
if route.request.url.startswith(b.base):
1125+
route.continue_()
1126+
else:
1127+
route.fulfill(status=200, content_type="text/html", body="<p>stub</p>")
1128+
1129+
with sync_playwright() as playwright:
1130+
browser = playwright.chromium.launch()
1131+
try:
1132+
context = browser.new_context()
1133+
context.route("**/*", external_stub)
1134+
context.add_cookies([{"name": c.name, "value": c.value, "url": b.base} for c in admin.cookies])
1135+
page = context.new_page()
1136+
page.goto(topic_url)
1137+
ext_link = page.locator(f'.content a[href="{external}"]').first
1138+
int_link = page.locator(f'.content a[href="{internal}"]').first
1139+
ok("elonw" in (ext_link.get_attribute("class") or "") and ext_link.get_attribute("title"),
1140+
"an external link in a post is marked, with a tooltip")
1141+
hidden = ext_link.locator(".elonw-sr-only")
1142+
ok(hidden.count() == 1 and hidden.inner_text().strip() == ext_link.get_attribute("title"),
1143+
"the marked link carries the same text for screen readers")
1144+
ok("elonw" not in (int_link.get_attribute("class") or ""), "a link to the board itself is not marked")
1145+
# A script-driven link is left alone, and does not stop later external links (old NotInClass bug)
1146+
page.evaluate("""() => { const a = document.createElement('a'); a.id = 'elonw-ignored';
1147+
a.href = 'https://example.net/'; a.className = 'lightbox'; a.textContent = 'x';
1148+
a.setAttribute('onclick', 'return false'); document.body.appendChild(a); }""")
1149+
pages_before = len(context.pages)
1150+
page.click("#elonw-ignored")
1151+
page.wait_for_timeout(500)
1152+
ok(len(context.pages) == pages_before and page.url.startswith(topic_url),
1153+
"a script-driven external link is left alone")
1154+
try:
1155+
with context.expect_page(timeout=5000) as popup:
1156+
ext_link.click()
1157+
opened = popup.value.url.startswith(external) and page.url.startswith(topic_url)
1158+
popup.value.close()
1159+
except PlaywrightTimeout:
1160+
opened = False
1161+
page.goto(topic_url)
1162+
ok(opened, "an external link opens in a new window, also after a script-driven link was clicked")
1163+
page.click(f'.content a[href="{internal}"]')
1164+
page.wait_for_load_state()
1165+
ok(len(context.pages) == 1 and page.url.startswith(internal),
1166+
"a link to the board itself opens in the same window")
1167+
finally:
1168+
browser.close()
1169+
1170+
# The UCP setting: off removes the script, and any other value than 0 is stored as 1
1171+
ucp = f"{b.base}/ucp.php?i=ucp_prefs&mode=personal&sid={sid}"
1172+
for value, stored in (("0", "0"), ("-1", "1"), ("1", "1")):
1173+
page = admin.get(ucp, timeout=20).text
1174+
if value == "0":
1175+
ok('name="elonw"' in page, "the UCP shows the setting")
1176+
action, fields = form_fields(page, "ucp")
1177+
# Send back every other setting as the form shows it
1178+
current = {}
1179+
for name, options in re.findall(r'(?s)<select[^>]*name="([^"]+)"[^>]*>(.*?)</select>', page):
1180+
chosen = re.search(r'<option[^>]*value="([^"]*)"[^>]*selected', options)
1181+
if chosen:
1182+
current[name] = chosen.group(1)
1183+
current.update(re.findall(r'<input[^>]*type="text"[^>]*name="([^"]+)"[^>]*value="([^"]*)"', page))
1184+
current.update(re.findall(r'<input[^>]*type="radio"[^>]*name="([^"]+)"[^>]*value="([^"]*)"[^>]*checked', page))
1185+
# phpBB 3.3 selects the timezone with JavaScript, so the page marks none of them
1186+
current["tz"] = b.sql("SELECT user_timezone FROM phpbb_users WHERE user_id = 2")[0][0] or "UTC"
1187+
post_back(admin, b.base, ucp, action, {**fields, **current, "elonw": value, "submit": "Submit"})
1188+
saved = b.sql("SELECT user_elonw FROM phpbb_users WHERE user_id = 2")
1189+
ok(saved and str(saved[0][0]) == stored, f"the UCP stores {value} as {stored}")
1190+
if value == "0":
1191+
ok("elonw.js" not in admin.get(topic_url, timeout=20).text, "turning it off removes the script")
1192+
1193+
11001194
CHECKS = {
11011195
"phpbbmodders/autodbbackup": check_autodbbackup,
1196+
"phpbbmodders/elonw": check_elonw,
11021197
"phpbbmodders/genders": check_genders,
11031198
"rmcgirr83/activity24hours": check_activity24hours,
11041199
"phpbbmodders/contactadmin": check_contactadmin,

‎phpbb-test-board/screenshots.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,30 @@ def look_up(p) -> None:
11271127
shoot.shot("posteripinviewtopic-lookup", f"viewtopic.php?t={topic}&sid={{sid}}", admin=True, prepare=look_up)
11281128

11291129

1130+
def shots_elonw(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
1131+
"""phpbbmodders/elonw: a post with an external link, marked with the new-window icon, next to a link
1132+
to the board itself, and the setting in the UCP.
1133+
1134+
Needs a board with posts: run with --seed and seed-forum's
1135+
bin/seed-standard-fixtures.php. The first post of the busiest topic is
1136+
rewritten to hold both links.
1137+
"""
1138+
_, topic = busiest_forum_and_topic(board)
1139+
post = board.sql("SELECT topic_first_post_id FROM phpbb_topics WHERE topic_id = ?", (topic,))[0][0]
1140+
text = ("Version 2.0 is out. The full release notes are on the "
1141+
"[url=https://www.example.org/releases/2.0]project site[/url], and questions about the "
1142+
f"update go in our [url={board.base}/viewforum.php?f=2]support forum[/url].")
1143+
board.php("$t = " + php_string(text) + "; $uid = $bit = ''; $flags = 0;"
1144+
"generate_text_for_storage($t, $uid, $bit, $flags, true, true, true);"
1145+
"$db->sql_query('UPDATE ' . POSTS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', "
1146+
"['post_text' => $t, 'bbcode_uid' => $uid, 'bbcode_bitfield' => $bit]) . ' WHERE post_id = "
1147+
+ str(int(post)) + "');", check=True)
1148+
board.purge_cache()
1149+
shoot.shot("elonw-post-links", f"viewtopic.php?t={topic}", selector=f"#p{post}")
1150+
shoot.shot("elonw-ucp-setting", "ucp.php?i=ucp_prefs&mode=personal&sid={sid}", admin=True,
1151+
selector="form#ucp")
1152+
1153+
11301154
def shots_genders(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
11311155
"""phpbbmodders/genders: the gender selector, genders on posts and profiles, and the ACP settings.
11321156
@@ -1250,6 +1274,7 @@ def public_directory(p) -> None:
12501274
"phpbbmodders/adduser": shots_adduser,
12511275
"phpbbmodders/autodbbackup": shots_autodbbackup,
12521276
"phpbbmodders/contactadmin": shots_contactadmin,
1277+
"phpbbmodders/elonw": shots_elonw,
12531278
"phpbbmodders/genders": shots_genders,
12541279
"phpbbmodders/copylinks": shots_copylinks,
12551280
"phpbbmodders/documentation": shots_documentation,

0 commit comments

Comments
 (0)