diff --git a/phpbb-test-board/feature_checks.py b/phpbb-test-board/feature_checks.py
index 46a7b51..b2e9f11 100755
--- a/phpbb-test-board/feature_checks.py
+++ b/phpbb-test-board/feature_checks.py
@@ -129,6 +129,49 @@ def warnings() -> int:
"moderator can warn once all the user's groups are ticked")
+def check_wiki(b: Board) -> None:
+ # Articles and a post stored through phpBB's own text parser, so the
+ # extension's [[ ]] markup is parsed exactly as on a real board.
+ def store(text: str) -> str:
+ return ("$t = " + json.dumps(text) + "; $uid = $bit = ''; $flags = 0;"
+ "generate_text_for_storage($t, $uid, $bit, $flags, true, true, true);")
+
+ articles = (
+ ("First article", "Read [[Second article]] and [[Missing page|this one]]."),
+ ("Second article", "The second article."),
+ )
+ for title, text in articles:
+ b.php(store(text) + "$db->sql_query('INSERT INTO ' . $table_prefix . 'wiki_article ' . "
+ "$db->sql_build_array('INSERT', ['article_title' => " + json.dumps(title) + ", 'article_url' => "
+ + json.dumps(title) + ", 'article_text' => $t, 'bbcode_uid' => $uid, 'bbcode_bitfield' => $bit, "
+ "'article_approved' => 1, 'article_user_id' => 2, 'article_last_edit' => time(), "
+ "'article_time_created' => time(), 'article_sources' => '', 'article_description' => '']));",
+ check=True)
+ b.php(store("See [[Second article]].\n[code][[Not a link]][/code]")
+ + "$db->sql_query('UPDATE ' . POSTS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', "
+ "['post_text' => $t, 'bbcode_uid' => $uid, 'bbcode_bitfield' => $bit]) . ' WHERE post_id = 1');",
+ check=True)
+ b.purge_cache()
+
+ admin = requests.Session()
+ sid = b.login(admin)
+ first = admin.get(f"{b.base}/app.php/wiki/First%20article").text
+ ok(re.search(r']*>this one', first),
+ "[[Missing page|this one]] shows its own text and is marked as a new article")
+ second = admin.get(f"{b.base}/app.php/wiki/Second%20article").text
+ ok("What links here" in second and re.search(r'href="[^"]*/wiki/First%20article[^"]*">First article', second),
+ "Second article lists First article under What links here")
+ ok("What links here" not in first, "an article nothing links to has no What links here list")
+ topic = admin.get(f"{b.base}/viewtopic.php?t=1").text
+ ok(re.search(r' None:
# The default build location moved over time (ext/.../docs-build, then
# store/phpbbmodders_documentation), so write the probe build wherever
@@ -321,6 +364,7 @@ def check_sfscompanion(b: Board) -> None:
CHECKS = {
"phpbbmodders/useridviewtopic": check_useridviewtopic,
+ "phpbbmodders/wiki": check_wiki,
"phpbbmodders/banlist": check_banlist,
"phpbbmodders/bannerrotator": check_bannerrotator,
"phpbbmodders/groupwarn": check_groupwarn,
diff --git a/phpbb-test-board/screenshots.py b/phpbb-test-board/screenshots.py
index 6b3bc7e..d210458 100755
--- a/phpbb-test-board/screenshots.py
+++ b/phpbb-test-board/screenshots.py
@@ -15,6 +15,7 @@
Exit status: 0 if every screenshot was saved, 1 if any failed, 2 for bad arguments.
"""
import argparse
+import json
import shutil
import subprocess
import sys
@@ -65,13 +66,14 @@ def admin_sid(self) -> str:
return self._admin_sid
def shot(self, name: str, url: str, admin: bool = False, phone: bool = False,
- selector: str = "", prepare: Callable = None) -> None:
+ selector: str = "", prepare: Callable = None, full_page: bool = False) -> None:
"""Save one screenshot as OUT_DIR/NAME.png.
url is relative to the board root. With admin=True the admin's session
is used and {sid} in url is replaced by its session id. selector crops
the image to that element; prepare(page) runs before the capture, for
example to open a tab or wait for results loaded by JavaScript.
+ full_page captures the whole page instead of the visible window.
"""
if admin:
url = url.replace("{sid}", self.admin_sid())
@@ -94,7 +96,7 @@ def shot(self, name: str, url: str, admin: bool = False, phone: bool = False,
if selector:
page.locator(selector).first.screenshot(path=str(target))
else:
- page.screenshot(path=str(target))
+ page.screenshot(path=str(target), full_page=full_page)
self.saved.append(target)
print(f" [ok] {target.name}")
except (PlaywrightError, RuntimeError) as e:
@@ -167,6 +169,80 @@ def shots_prominodeux(board: Board, shoot: Shooter, args: argparse.Namespace) ->
shoot.shot("prominodeux-viewtopic-phone", f"viewtopic.php?t={topic}", phone=True)
+def shots_wiki(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
+ """phpbbmodders/wiki: overview, articles, editing, approval and links between articles."""
+ def add_article(title: str, text: str, description: str = "", views: int = 0, sticky: int = 0,
+ approved: int = 1, days_ago: float = 0, edit_reason: str = "") -> int:
+ """Store one article version through phpBB's own text parser; return its id."""
+ out = board.php(
+ "$t = " + json.dumps(text) + "; $uid = $bit = ''; $flags = 0;"
+ "generate_text_for_storage($t, $uid, $bit, $flags, true, true, true);"
+ "$when = time() - " + str(int(days_ago * 86400)) + ";"
+ "$db->sql_query('INSERT INTO ' . $table_prefix . 'wiki_article ' . "
+ "$db->sql_build_array('INSERT', ['article_title' => " + json.dumps(title) + ", 'article_url' => "
+ + json.dumps(title) + ", 'article_text' => $t, 'bbcode_uid' => $uid, 'bbcode_bitfield' => $bit, "
+ "'article_approved' => " + str(approved) + ", 'article_user_id' => 2, 'article_last_edit' => $when, "
+ "'article_time_created' => $when, 'article_sources' => '', 'article_description' => "
+ + json.dumps(description) + ", 'article_views' => " + str(views) + ", 'article_sticky' => "
+ + str(sticky) + ", 'article_edit_reason' => " + json.dumps(edit_reason) + "]));"
+ "echo $db->sql_nextid();",
+ check=True)
+ return int(out.strip().splitlines()[-1])
+
+ add_article("Getting Started",
+ "Welcome to the community wiki. Anyone can read it, and members can help keep it up to date."
+ "\n\n[b]Where to begin[/b]\n[list]\n[*]New to the board? Read the [[Frequently Asked Questions]]."
+ "\n[*]Installing the software yourself? Start with [[Installation]].\n[*]Writing posts? The "
+ "[[Formatting Guide]] covers bold text, lists, links and code.\n[/list]\n\nEvery edit is kept, "
+ "so nothing is lost: open [i]Versions[/i] on any article to see who changed what.",
+ "A quick tour of the board and the wiki.", views=148, sticky=1, days_ago=12)
+ add_article("Installation",
+ "Before you start, read the [[Requirements]]. Then follow [[Upgrading]] if you already run an "
+ "older version, or see the [[Troubleshooting|troubleshooting tips]] if something goes wrong."
+ "\n\nOnce installed, the [[Configuration]] page explains every setting.",
+ "Installing the software step by step.", views=96, days_ago=9)
+ add_article("Requirements", "You need phpBB 3.3 and PHP 7.4 or later. See [[Installation]] for the next steps.",
+ "What your server needs.", views=64, days_ago=8)
+ add_article("Configuration", "Every setting is described here. Return to [[Installation]].",
+ "Every setting explained.", views=51, days_ago=6)
+ add_article("Frequently Asked Questions",
+ "[b]How do I change my avatar?[/b]\nOpen the User Control Panel and choose Profile.",
+ "Common questions from new members.", views=23, days_ago=4)
+ guide = ("Use [b]bold[/b] for emphasis, [i]italic[/i] for titles and [code]code[/code] for code."
+ "\n\nSee the [[Getting Started]] page for more help.")
+ active = add_article("Formatting Guide", guide, "How to format posts with BBCode.", views=37, days_ago=3)
+ pending = add_article("Formatting Guide",
+ guide + "\n\nTo quote another post, click [i]Quote[/i] below it.",
+ "How to format posts with BBCode.", views=37, approved=0, days_ago=0.1,
+ edit_reason="Added a note about quoting")
+ board.purge_cache()
+
+ shoot.shot("wiki-overview", "app.php/wiki/?sid={sid}", admin=True, full_page=True)
+ shoot.shot("wiki-article-view", "app.php/wiki/Getting%20Started?sid={sid}", admin=True)
+ shoot.shot("wiki-edit", "app.php/wiki/Getting%20Started?action=edit&sid={sid}", admin=True, full_page=True)
+ # The first list on the overview is the pending one, shown to moderators.
+ shoot.shot("wiki-pending-articles", "app.php/wiki/?sid={sid}", admin=True, selector=".forumbg")
+ shoot.shot("wiki-compare", f"app.php/wiki/Formatting%20Guide?action=compare&from={pending}&to={active}"
+ "&sid={sid}", admin=True)
+
+ def open_moderator_controls(p) -> None:
+ p.click("#quickmod .dropdown-trigger")
+ p.wait_for_selector("#quickmod .dropdown:visible")
+ shoot.shot("wiki-moderator-controls", "app.php/wiki/Formatting%20Guide?action=versions&sid={sid}",
+ admin=True, prepare=open_moderator_controls)
+
+ # Requirements is linked from Installation and links back, so its page
+ # shows both a link and the What links here list.
+ shoot.shot("wiki-article-links", "app.php/wiki/Installation?sid={sid}", admin=True,
+ selector=".panel.bg1")
+ shoot.shot("wiki-what-links-here", "app.php/wiki/Requirements?sid={sid}", admin=True)
+
+ def highlight_button(p) -> None:
+ p.add_style_tag(content=".bbcode-wikilink { outline: 3px solid #d31141; outline-offset: 2px; }")
+ shoot.shot("wiki-link-button", "app.php/wiki/Requirements?action=edit&sid={sid}", admin=True,
+ selector="#format-buttons", prepare=highlight_button)
+
+
# Made-up values for the StopForumSpam screenshots. Nothing is ever sent to
# stopforumspam.com: the pages shown only read the board's own data and logs.
SFS_EXAMPLE_API_KEY = "0123456789abcdef"
@@ -254,6 +330,7 @@ def highlight_link(p) -> None:
"phpbbmodders/documentation": shots_documentation,
"phpbbmodders/sfscompanion": shots_sfscompanion,
"phpbbmodders/stopforumspam": shots_stopforumspam,
+ "phpbbmodders/wiki": shots_wiki,
"ProMinoDeux": shots_prominodeux,
}