diff --git a/phpbb-test-board/README.md b/phpbb-test-board/README.md index e7ffcb1..6365b37 100644 --- a/phpbb-test-board/README.md +++ b/phpbb-test-board/README.md @@ -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. diff --git a/phpbb-test-board/feature_checks.py b/phpbb-test-board/feature_checks.py index d2eea9b..e7089a0 100755 --- a/phpbb-test-board/feature_checks.py +++ b/phpbb-test-board/feature_checks.py @@ -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="

stub

") + + 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)]*name="([^"]+)"[^>]*>(.*?)', page): + chosen = re.search(r']*value="([^"]*)"[^>]*selected', options) + if chosen: + current[name] = chosen.group(1) + current.update(re.findall(r']*type="text"[^>]*name="([^"]+)"[^>]*value="([^"]*)"', page)) + current.update(re.findall(r']*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, diff --git a/phpbb-test-board/screenshots.py b/phpbb-test-board/screenshots.py index d8d0458..d6a242b 100755 --- a/phpbb-test-board/screenshots.py +++ b/phpbb-test-board/screenshots.py @@ -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. @@ -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,