Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions phpbb-test-board/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ Saves PNG screenshots of an extension's or style's pages with [Playwright](https

`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.

`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.

`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.

`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.
Expand Down
95 changes: 95 additions & 0 deletions phpbb-test-board/feature_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,8 +1097,103 @@ def run_cron(times: int) -> None:
b.purge_cache()


def check_elonw(b: Board) -> None:
"""Post external and same-site links, then check marking, new windows and the UCP setting in a browser."""
# Only this check needs a browser
from playwright.sync_api import TimeoutError as PlaywrightTimeout, sync_playwright

forum = b.sql("SELECT forum_id FROM phpbb_posts WHERE post_id = 1")[0][0]
external = "https://example.org/elonw-probe"
internal = f"{b.base}/index.php"
admin = requests.Session()
sid = b.login(admin)
page_url = f"{b.base}/posting.php?mode=post&f={forum}&sid={sid}"
action, fields = form_fields(admin.get(page_url, timeout=20).text, "postform")
title = "elonw probe " + secrets.token_hex(4) # unique, so a rerun on the same board reads its own topic
post_back(admin, b.base, page_url, action, {**fields, "subject": title, "post": "Submit",
"message": f"[url]{external}[/url] and [url]{internal}[/url]"})
topic = b.sql("SELECT topic_id FROM phpbb_topics WHERE topic_title = ?", (title,))
ok(topic, "the probe topic was posted")
if not topic:
return
topic_url = f"{b.base}/viewtopic.php?t={topic[0][0]}"
ok("elonw.js" in requests.get(topic_url, timeout=20).text, "guests get the script by default")

def external_stub(route) -> None:
"""Answer every request to another host locally, so nothing leaves the machine."""
if route.request.url.startswith(b.base):
route.continue_()
else:
route.fulfill(status=200, content_type="text/html", body="<p>stub</p>")

with sync_playwright() as playwright:
browser = playwright.chromium.launch()
try:
context = browser.new_context()
context.route("**/*", external_stub)
context.add_cookies([{"name": c.name, "value": c.value, "url": b.base} for c in admin.cookies])
page = context.new_page()
page.goto(topic_url)
ext_link = page.locator(f'.content a[href="{external}"]').first
int_link = page.locator(f'.content a[href="{internal}"]').first
ok("elonw" in (ext_link.get_attribute("class") or "") and ext_link.get_attribute("title"),
"an external link in a post is marked, with a tooltip")
hidden = ext_link.locator(".elonw-sr-only")
ok(hidden.count() == 1 and hidden.inner_text().strip() == ext_link.get_attribute("title"),
"the marked link carries the same text for screen readers")
ok("elonw" not in (int_link.get_attribute("class") or ""), "a link to the board itself is not marked")
# A script-driven link is left alone, and does not stop later external links (old NotInClass bug)
page.evaluate("""() => { const a = document.createElement('a'); a.id = 'elonw-ignored';
a.href = 'https://example.net/'; a.className = 'lightbox'; a.textContent = 'x';
a.setAttribute('onclick', 'return false'); document.body.appendChild(a); }""")
pages_before = len(context.pages)
page.click("#elonw-ignored")
page.wait_for_timeout(500)
ok(len(context.pages) == pages_before and page.url.startswith(topic_url),
"a script-driven external link is left alone")
try:
with context.expect_page(timeout=5000) as popup:
ext_link.click()
opened = popup.value.url.startswith(external) and page.url.startswith(topic_url)
popup.value.close()
except PlaywrightTimeout:
opened = False
page.goto(topic_url)
ok(opened, "an external link opens in a new window, also after a script-driven link was clicked")
page.click(f'.content a[href="{internal}"]')
page.wait_for_load_state()
ok(len(context.pages) == 1 and page.url.startswith(internal),
"a link to the board itself opens in the same window")
finally:
browser.close()

# The UCP setting: off removes the script, and any other value than 0 is stored as 1
ucp = f"{b.base}/ucp.php?i=ucp_prefs&mode=personal&sid={sid}"
for value, stored in (("0", "0"), ("-1", "1"), ("1", "1")):
page = admin.get(ucp, timeout=20).text
if value == "0":
ok('name="elonw"' in page, "the UCP shows the setting")
action, fields = form_fields(page, "ucp")
# Send back every other setting as the form shows it
current = {}
for name, options in re.findall(r'(?s)<select[^>]*name="([^"]+)"[^>]*>(.*?)</select>', page):
chosen = re.search(r'<option[^>]*value="([^"]*)"[^>]*selected', options)
if chosen:
current[name] = chosen.group(1)
current.update(re.findall(r'<input[^>]*type="text"[^>]*name="([^"]+)"[^>]*value="([^"]*)"', page))
current.update(re.findall(r'<input[^>]*type="radio"[^>]*name="([^"]+)"[^>]*value="([^"]*)"[^>]*checked', page))
# phpBB 3.3 selects the timezone with JavaScript, so the page marks none of them
current["tz"] = b.sql("SELECT user_timezone FROM phpbb_users WHERE user_id = 2")[0][0] or "UTC"
post_back(admin, b.base, ucp, action, {**fields, **current, "elonw": value, "submit": "Submit"})
saved = b.sql("SELECT user_elonw FROM phpbb_users WHERE user_id = 2")
ok(saved and str(saved[0][0]) == stored, f"the UCP stores {value} as {stored}")
if value == "0":
ok("elonw.js" not in admin.get(topic_url, timeout=20).text, "turning it off removes the script")


CHECKS = {
"phpbbmodders/autodbbackup": check_autodbbackup,
"phpbbmodders/elonw": check_elonw,
"phpbbmodders/genders": check_genders,
"rmcgirr83/activity24hours": check_activity24hours,
"phpbbmodders/contactadmin": check_contactadmin,
Expand Down
25 changes: 25 additions & 0 deletions phpbb-test-board/screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,30 @@ def look_up(p) -> None:
shoot.shot("posteripinviewtopic-lookup", f"viewtopic.php?t={topic}&sid={{sid}}", admin=True, prepare=look_up)


def shots_elonw(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
"""phpbbmodders/elonw: a post with an external link, marked with the new-window icon, next to a link
to the board itself, and the setting in the UCP.

Needs a board with posts: run with --seed and seed-forum's
bin/seed-standard-fixtures.php. The first post of the busiest topic is
rewritten to hold both links.
"""
_, topic = busiest_forum_and_topic(board)
post = board.sql("SELECT topic_first_post_id FROM phpbb_topics WHERE topic_id = ?", (topic,))[0][0]
text = ("Version 2.0 is out. The full release notes are on the "
"[url=https://www.example.org/releases/2.0]project site[/url], and questions about the "
f"update go in our [url={board.base}/viewforum.php?f=2]support forum[/url].")
board.php("$t = " + php_string(text) + "; $uid = $bit = ''; $flags = 0;"
"generate_text_for_storage($t, $uid, $bit, $flags, true, true, true);"
"$db->sql_query('UPDATE ' . POSTS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', "
"['post_text' => $t, 'bbcode_uid' => $uid, 'bbcode_bitfield' => $bit]) . ' WHERE post_id = "
+ str(int(post)) + "');", check=True)
board.purge_cache()
shoot.shot("elonw-post-links", f"viewtopic.php?t={topic}", selector=f"#p{post}")
shoot.shot("elonw-ucp-setting", "ucp.php?i=ucp_prefs&mode=personal&sid={sid}", admin=True,
selector="form#ucp")


def shots_genders(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
"""phpbbmodders/genders: the gender selector, genders on posts and profiles, and the ACP settings.

Expand Down Expand Up @@ -1250,6 +1274,7 @@ def public_directory(p) -> None:
"phpbbmodders/adduser": shots_adduser,
"phpbbmodders/autodbbackup": shots_autodbbackup,
"phpbbmodders/contactadmin": shots_contactadmin,
"phpbbmodders/elonw": shots_elonw,
"phpbbmodders/genders": shots_genders,
"phpbbmodders/copylinks": shots_copylinks,
"phpbbmodders/documentation": shots_documentation,
Expand Down
Loading