From 10e2b947bb4b396a01cdbfca124ddcfb5ce76ed5 Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Wed, 7 Oct 2026 11:49:46 -0500 Subject: [PATCH 1/2] Add screenshots.py for extension documentation images Takes PNG screenshots of an extension's board and ACP pages with Playwright (Chromium, prosilver, English) on a test board renamed "Example board", for the extension's docs/images. Pages are listed per extension in SHOTS; phpbbmodders/documentation is the first, served from a phpbbdocs-hugo build given with --build. The first run downloads Playwright's Chromium. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 + phpbb-test-board/requirements.txt | 1 + phpbb-test-board/screenshots.py | 228 ++++++++++++++++++++++++++++++ tests/test-phpbb-test-board.sh | 9 +- 4 files changed, 237 insertions(+), 3 deletions(-) create mode 100755 phpbb-test-board/screenshots.py diff --git a/README.md b/README.md index 065aee6..227130e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ phpbb-test-board/setup-board.sh -d ~/phpbb-test-board export PHPBB_TEST_BOARD=~/phpbb-test-board phpbb-test-board/smoke_test.py path/to/extension --ref origin/main phpbb-test-board/feature_checks.py path/to/extension@origin/main +phpbb-test-board/screenshots.py path/to/extension@origin/main -o path/to/extension/docs/images ``` For an extension that needs another one, pass the other extension's checkout with `--with` (repeat it for several, in the order they must be enabled): @@ -107,6 +108,7 @@ phpbb-test-board/smoke_test.py path/to/sfscompanion --with path/to/stopforumspam - `setup-board.sh` installs the current phpBB 3.3 release (or `--version`) with no extensions, and keeps a clean copy of the database so every test starts from the same state. The admin password is generated and stored only in `board.env`. - `smoke_test.py` installs the extension from git, loads board pages as a guest and as the admin, the extension's routes, ACP/MCP/UCP modules and cron tasks, then disables, deletes data and re-enables it. It reports any server error, empty page, phpBB debug notice or PHP error-log entry. - `feature_checks.py` exercises the main feature of the phpbbmodders extensions listed in its `--help` (for example: a moderator can't warn a user in an unticked group). Add a check for a new extension in its `CHECKS` table. +- `screenshots.py` saves PNG screenshots of an extension's board and ACP pages for its documentation, using [Playwright](https://playwright.dev/python/) with Chromium on a board named "Example board" (prosilver, English). The pages are listed per extension in its `SHOTS` table. The first run downloads Playwright's Chromium. phpbbmodders/documentation also needs `--build DIR`, a phpbbdocs-hugo build to serve. - The board is restored after each run. Each script has `--help`. - `--with` extensions are installed and enabled before the extension under test and stay enabled; only the extension under test goes through the disable and delete data round trip. `feature_checks.py` installs a `--with` extension only for the extensions whose `composer.json` requires it. If `composer.json` requires a package you didn't supply, the scripts print a note naming it. diff --git a/phpbb-test-board/requirements.txt b/phpbb-test-board/requirements.txt index 5942511..2fab61e 100644 --- a/phpbb-test-board/requirements.txt +++ b/phpbb-test-board/requirements.txt @@ -1,2 +1,3 @@ requests>=2.31 PyYAML>=6.0 +playwright>=1.63 diff --git a/phpbb-test-board/screenshots.py b/phpbb-test-board/screenshots.py new file mode 100755 index 0000000..385686f --- /dev/null +++ b/phpbb-test-board/screenshots.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +Take documentation screenshots of a phpbbmodders extension on a local test board. + +Installs the extension from git into a clean copy of the board (created by +setup-board.sh), renames the board to "Example board", serves it, and saves +PNG screenshots of the extension's board pages and ACP pages with Playwright +(Chromium, prosilver, English). The pages for each extension are listed in +SHOTS below, keyed by composer name; add an entry for a new extension. +Restores the board afterwards. + +The first run installs Playwright's Chromium into the user's cache. + +Exit status: 0 if every screenshot was saved, 1 if any failed, 2 for bad arguments. +""" +import argparse +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Callable + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from _venv import ensure_venv # noqa: E402 + +ensure_venv() + +import requests # noqa: E402 +from playwright.sync_api import Browser, Error as PlaywrightError, sync_playwright # noqa: E402 + +from _board import Board, ext_name, parse_spec, required_packages # noqa: E402 + +BOARD_NAME = "Example board" +BOARD_DESCRIPTION = "A phpBB board" +DESKTOP = {"width": 1280, "height": 900} +PHONE = {"width": 390, "height": 844} +TIMEOUT_MS = 30000 + + +class Shooter: + """Saves screenshots of board pages as a guest or as the admin.""" + + def __init__(self, board: Board, browser: Browser, out_dir: Path) -> None: + self.board = board + self.browser = browser + self.out_dir = out_dir + self.saved: list = [] + self.failed: list = [] + self.cleanup: list = [] # paths to delete once the screenshots are taken + self._admin_sid = "" + self._admin_cookies: list = [] + self._admin_agent = "" + + def admin_sid(self) -> str: + """Log the admin in, ACP included, once; return the session id.""" + if not self._admin_sid: + session = requests.Session() + self._admin_sid = self.board.login(session, acp=True) + # phpBB checks the browser string against the one that logged in. + self._admin_agent = session.headers["User-Agent"] + self._admin_cookies = [{"name": c.name, "value": c.value, "domain": "localhost", "path": "/"} + for c in session.cookies] + return self._admin_sid + + def shot(self, name: str, url: str, admin: bool = False, phone: bool = False, + selector: str = "", prepare: Callable = None) -> 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. + """ + if admin: + url = url.replace("{sid}", self.admin_sid()) + options = {"viewport": PHONE if phone else DESKTOP, "locale": "en-GB", + "device_scale_factor": 1, "is_mobile": phone, "has_touch": phone} + if admin: + options["user_agent"] = self._admin_agent + context = self.browser.new_context(**options) + if admin: + context.add_cookies(self._admin_cookies) + page = context.new_page() + page.set_default_timeout(TIMEOUT_MS) + target = self.out_dir / f"{name}.png" + try: + response = page.goto(f"{self.board.base}/{url}", wait_until="networkidle") + if response is None or response.status >= 400: + raise RuntimeError(f"HTTP {response.status if response else 'no response'}") + if prepare: + prepare(page) + if selector: + page.locator(selector).first.screenshot(path=str(target)) + else: + page.screenshot(path=str(target)) + self.saved.append(target) + print(f" [ok] {target.name}") + except (PlaywrightError, RuntimeError) as e: + self.failed.append(name) + print(f" [FAIL] {name}: {str(e).splitlines()[0][:200]}") + finally: + context.close() + + +# -- screenshots, one function per extension -------------------------------- + +def shots_documentation(board: Board, shoot: Shooter, args: argparse.Namespace) -> None: + """phpbbmodders/documentation: needs --build, a phpbbdocs-hugo build.""" + if not args.build: + raise SystemExit("error: phpbbmodders/documentation needs --build DIR, a phpbbdocs-hugo build") + path = board.sql("SELECT config_value FROM phpbb_config " + "WHERE config_name = 'phpbbmodders_documentation_docs_path'")[0][0] + build = board.root / path + shutil.rmtree(build, ignore_errors=True) + shutil.copytree(args.build, build) + shoot.cleanup.append(build) + requests.get(f"{board.base}/index.php") # runs the permission sync left pending by the install + board.purge_cache() + + page = "app.php/documentation/en/userguide/user_permissions" + shoot.shot("documentation-page", page) + def show_article(p) -> None: + # On a phone the sidebar comes first; show the article instead. + p.locator(".documentation-main").scroll_into_view_if_needed() + shoot.shot("documentation-page-phone", page, phone=True, prepare=show_article) + shoot.shot("documentation-navbar", "index.php", selector="#page-header") + + def wait_for_results(p) -> None: + p.wait_for_selector(".doc-search-results li") + shoot.shot("documentation-search", "app.php/documentation-search/en?q=permissions&js=1", + prepare=wait_for_results) + + shoot.shot("documentation-acp-settings", + "adm/index.php?i=-phpbbmodders-documentation-acp-main_module&mode=settings&sid={sid}", + admin=True) + + guests = board.sql("SELECT group_id FROM phpbb_groups WHERE group_name = 'GUESTS'")[0][0] + + def open_misc_tab(p) -> None: + # Advanced permissions, Misc category, where the documentation permissions are listed. + p.locator("a[onclick*='swap_options'], a:has-text('Advanced Permissions')").first.click() + p.locator("li[id^='tab'] a:has-text('Misc'), a:has-text('Misc')").first.click() + shoot.shot("documentation-acp-permissions", + f"adm/index.php?i=acp_permissions&mode=setting_group_global&group_id[0]={guests}&type=u_&sid={{sid}}", + admin=True, prepare=open_misc_tab, selector="fieldset:has(legend:has-text('Guests'))") + + +SHOTS = { + "phpbbmodders/documentation": shots_documentation, +} + + +def ensure_chromium(playwright) -> Browser: + """Launch Chromium, installing Playwright's copy first if it is missing.""" + try: + return playwright.chromium.launch() + except PlaywrightError: + print("Installing Playwright's Chromium ...", file=sys.stderr) + subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True) + return playwright.chromium.launch() + + +def main() -> int: + ap = argparse.ArgumentParser(description="Take documentation screenshots of a phpbbmodders extension " + "on a local test board.", + epilog="Extensions with screenshots: " + ", ".join(sorted(SHOTS)) + + ". Exit status: 0 all saved, 1 a screenshot failed, 2 bad arguments.") + ap.add_argument("repo", metavar="REPO[@REF]", + help="extension git checkout, optionally with a git ref to install (default ref: HEAD)") + ap.add_argument("-o", "--out", required=True, metavar="DIR", + help="directory to save the PNG files in, for example the extension's docs/images") + ap.add_argument("--build", metavar="DIR", + help="phpbbdocs-hugo build to serve (phpbbmodders/documentation only)") + ap.add_argument("-w", "--with", dest="needed", action="append", default=[], metavar="PATH[@REF]", + help="git checkout of an extension the extension requires (default ref: HEAD); " + "installed and enabled first; repeat for several, in the order they must be enabled") + ap.add_argument("-b", "--board-dir", + help="board directory created by setup-board.sh (default: $PHPBB_TEST_BOARD)") + ap.add_argument("-p", "--port", type=int, default=8083, + help="port for the temporary web server (default: 8083)") + args = ap.parse_args() + + try: + repo, ref = parse_spec(args.repo) + needed = [parse_spec(spec) for spec in args.needed] + except ValueError as e: + ap.error(str(e)) + if args.build and not (Path(args.build) / "index.html").is_file(): + ap.error(f"--build {args.build} is not a built site (no index.html)") + name = ext_name(repo, ref) + if name not in SHOTS: + ap.error(f"no screenshots defined for {name}; add them to SHOTS in {Path(__file__).name}") + out_dir = Path(args.out).expanduser().resolve() + out_dir.mkdir(parents=True, exist_ok=True) + + board = Board(args.board_dir, args.port) + for package in required_packages(repo, ref): + if package not in {ext_name(r, dep_ref) for r, dep_ref in needed}: + print(f" [note] {repo.name} requires {package}; if it is a phpBB extension, add --with PATH") + ext, target, out = board.install_ext(repo, ref, needed) + print(f"== {ext} ({repo.name} @ {ref}) -> {out_dir}") + try: + if "Successfully" not in out: + print(f" [FAIL] enable: {out.strip()[-200:]}") + return 1 + board.sql("UPDATE phpbb_config SET config_value = ? WHERE config_name = 'sitename'", (BOARD_NAME,)) + board.sql("UPDATE phpbb_config SET config_value = ? WHERE config_name = 'site_desc'", (BOARD_DESCRIPTION,)) + board.purge_cache() + with board.serve(), sync_playwright() as playwright: + browser = ensure_chromium(playwright) + shooter = Shooter(board, browser, out_dir) + try: + SHOTS[ext](board, shooter, args) + finally: + browser.close() + for path in shooter.cleanup: + shutil.rmtree(path, ignore_errors=True) + errors = board.php_errors() + if errors: + print(f" [FAIL] PHP errors logged: {errors[0][:200]}") + print(f"== {len(shooter.saved)} saved, {len(shooter.failed)} failed") + return 1 if shooter.failed or errors else 0 + finally: + board.remove_ext(target) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test-phpbb-test-board.sh b/tests/test-phpbb-test-board.sh index 5d0bf6e..4b799ec 100755 --- a/tests/test-phpbb-test-board.sh +++ b/tests/test-phpbb-test-board.sh @@ -51,8 +51,8 @@ check "setup-board.sh without --dir exits 2" exits_with 2 "$tools/setup-board.sh check "setup-board.sh unknown option exits 2" exits_with 2 "$tools/setup-board.sh" --bogus check "setup-board.sh bad --version exits 2" exits_with 2 "$tools/setup-board.sh" -d "$tmp/board" -v 4.0.0 -# smoke_test.py and feature_checks.py -for script in smoke_test.py feature_checks.py; do +# smoke_test.py, feature_checks.py and screenshots.py +for script in smoke_test.py feature_checks.py screenshots.py; do check "$script --help exits 0" exits_with 0 "$tools/$script" --help check "$script without arguments exits 2" exits_with 2 "$tools/$script" check "$script with a non-git directory exits 2" exits_with 2 "$tools/$script" "$tmp" -b "$tmp" @@ -68,7 +68,10 @@ check "feature_checks.py with a missing board.env exits 1" \ exits_with 1 "$tools/feature_checks.py" "$tmp/repo" -b "$tmp/no-board" check "feature_checks.py --help lists the extensions with checks" \ output_has "phpbbmodders/groupwarn" "$tools/feature_checks.py" --help -for script in smoke_test.py feature_checks.py; do +check "screenshots.py --help lists the extensions with screenshots" \ + output_has "phpbbmodders/documentation" "$tools/screenshots.py" --help +check "screenshots.py without --out exits 2" exits_with 2 "$tools/screenshots.py" "$tmp/repo" +for script in smoke_test.py feature_checks.py screenshots.py; do check "$script --help describes --with" output_has "--with PATH[@REF]" "$tools/$script" --help check "$script --with a non-git directory exits 2" \ exits_with 2 "$tools/$script" "$tmp/repo" --with "$tmp" -b "$tmp" From 5213aae4107e9b73a056ba99db8076426b7c4ec9 Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Wed, 7 Oct 2026 12:08:36 -0500 Subject: [PATCH 2/2] Let screenshots.py take style screenshots and seed content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A checkout with a style.cfg is installed as a style (Board.install_style registers it like ACP » Styles » Install and makes it every user's style) and shown on board pages. --seed SCRIPT runs a PHP seed script, such as seed-forum's bin/seed-standard-fixtures.php, after the install so pages show real forums, topics and users. ProMinoDeux is the first style in SHOTS. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- phpbb-test-board/_board.py | 62 ++++++++++++++++++++++++ phpbb-test-board/screenshots.py | 86 +++++++++++++++++++++++++-------- tests/test-phpbb-test-board.sh | 4 ++ 4 files changed, 133 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 227130e..6a9dfed 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ phpbb-test-board/smoke_test.py path/to/sfscompanion --with path/to/stopforumspam - `setup-board.sh` installs the current phpBB 3.3 release (or `--version`) with no extensions, and keeps a clean copy of the database so every test starts from the same state. The admin password is generated and stored only in `board.env`. - `smoke_test.py` installs the extension from git, loads board pages as a guest and as the admin, the extension's routes, ACP/MCP/UCP modules and cron tasks, then disables, deletes data and re-enables it. It reports any server error, empty page, phpBB debug notice or PHP error-log entry. - `feature_checks.py` exercises the main feature of the phpbbmodders extensions listed in its `--help` (for example: a moderator can't warn a user in an unticked group). Add a check for a new extension in its `CHECKS` table. -- `screenshots.py` saves PNG screenshots of an extension's board and ACP pages for its documentation, using [Playwright](https://playwright.dev/python/) with Chromium on a board named "Example board" (prosilver, English). The pages are listed per extension in its `SHOTS` table. The first run downloads Playwright's Chromium. phpbbmodders/documentation also needs `--build DIR`, a phpbbdocs-hugo build to serve. +- `screenshots.py` saves PNG screenshots of an extension's or a style's pages for its documentation, using [Playwright](https://playwright.dev/python/) with Chromium on a board named "Example board" (English). An extension is shown in prosilver, with its board and ACP pages; a checkout with a `style.cfg` is installed as a style and made every user's style. The pages are listed per project in its `SHOTS` table. `--seed SCRIPT` fills the board with forums, topics and users first, for example with [seed-forum](https://github.com/phpbbmodders/seed-forum)'s `bin/seed-standard-fixtures.php`. The first run downloads Playwright's Chromium. phpbbmodders/documentation also needs `--build DIR`, a phpbbdocs-hugo build to serve. - The board is restored after each run. Each script has `--help`. - `--with` extensions are installed and enabled before the extension under test and stay enabled; only the extension under test goes through the disable and delete data round trip. `feature_checks.py` installs a `--with` extension only for the extensions whose `composer.json` requires it. If `composer.json` requires a package you didn't supply, the scripts print a note naming it. diff --git a/phpbb-test-board/_board.py b/phpbb-test-board/_board.py index 1203bc2..69fee70 100644 --- a/phpbb-test-board/_board.py +++ b/phpbb-test-board/_board.py @@ -130,6 +130,49 @@ def remove_ext(self, target: Path) -> None: self.installed = [] self.reset() + # -- styles ------------------------------------------------------------ + + def install_style(self, repo: Path, ref: str) -> tuple: + """Copy a style from git into a clean board, install it and make it every user's style. + + The style goes in styles// and + is registered directly in the database, the way ACP » Customise » + Styles » Install does, which is only acceptable on a disposable test + board. Returns (style name, installed path); remove it with + remove_style(). + """ + self.reset() + cfg = style_cfg(repo, ref) + name = cfg["name"] + directory = re.sub(r"[^a-z0-9]", "", name.lower()) + target = self.root / "styles" / directory + shutil.rmtree(target, ignore_errors=True) + target.mkdir(parents=True) + tar = subprocess.run(["git", "-C", str(repo), "archive", ref], capture_output=True, check=True).stdout + subprocess.run(["tar", "-x", "-C", str(target)], input=tar, check=True) + + parent_id, parent_tree, bitfield = 0, "", "" + if cfg.get("parent") and cfg["parent"] != name: + rows = self.sql("SELECT style_id, style_path, style_parent_tree, bbcode_bitfield FROM phpbb_styles " + "WHERE style_name = ?", (cfg["parent"],)) + if not rows: + raise RuntimeError(f"parent style {cfg['parent']} is not installed on the board") + parent_id, parent_path, grandparents, bitfield = rows[0] + parent_tree = f"{grandparents}/{parent_path}" if grandparents else parent_path + self.sql("INSERT INTO phpbb_styles (style_name, style_copyright, style_active, style_path, bbcode_bitfield, " + "style_parent_id, style_parent_tree) VALUES (?, ?, 1, ?, ?, ?, ?)", + (name, cfg.get("copyright", ""), directory, bitfield or "kNg=", parent_id, parent_tree)) + style_id = self.sql("SELECT style_id FROM phpbb_styles WHERE style_path = ?", (directory,))[0][0] + self.sql("UPDATE phpbb_config SET config_value = ? WHERE config_name = 'default_style'", (str(style_id),)) + self.sql("UPDATE phpbb_users SET user_style = ?", (style_id,)) + self.purge_cache() + return name, target + + def remove_style(self, target: Path) -> None: + """Remove a style installed by install_style() and restore the clean board.""" + shutil.rmtree(target, ignore_errors=True) + self.reset() + # -- web server and sessions ------------------------------------------- @contextmanager @@ -215,6 +258,25 @@ def required_packages(repo: Path, ref: str) -> list: return sorted(name for name in require if not NOT_EXTENSIONS.match(name)) +def is_style(repo: Path, ref: str) -> bool: + """Whether a checkout at a git ref is a phpBB style (style.cfg at its root).""" + return subprocess.run(["git", "-C", str(repo), "cat-file", "-e", f"{ref}:style.cfg"], + capture_output=True).returncode == 0 + + +def style_cfg(repo: Path, ref: str) -> dict: + """Read a style's style.cfg (name = value lines) at a git ref.""" + out = subprocess.run(["git", "-C", str(repo), "show", f"{ref}:style.cfg"], + capture_output=True, text=True, check=True).stdout + cfg = {} + for line in out.splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + cfg[key.strip()] = value.strip() + return cfg + + def parse_spec(spec: str) -> tuple: """Split "PATH[@REF]" into (resolved path, ref); ref defaults to HEAD. diff --git a/phpbb-test-board/screenshots.py b/phpbb-test-board/screenshots.py index 385686f..61f5441 100755 --- a/phpbb-test-board/screenshots.py +++ b/phpbb-test-board/screenshots.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 """ -Take documentation screenshots of a phpbbmodders extension on a local test board. +Take documentation screenshots of a phpbbmodders extension or style on a local test board. -Installs the extension from git into a clean copy of the board (created by -setup-board.sh), renames the board to "Example board", serves it, and saves -PNG screenshots of the extension's board pages and ACP pages with Playwright -(Chromium, prosilver, English). The pages for each extension are listed in -SHOTS below, keyed by composer name; add an entry for a new extension. -Restores the board afterwards. +Installs the extension or style from git into a clean copy of the board +(created by setup-board.sh), renames the board to "Example board", serves +it, and saves PNG screenshots with Playwright (Chromium, English). An +extension is shown in prosilver, with its board and ACP pages; a style is +made every user's style and shown on board pages. The pages are listed in +SHOTS below, keyed by an extension's composer name or a style's name from +style.cfg; add an entry for a new project. Restores the board afterwards. The first run installs Playwright's Chromium into the user's cache. @@ -28,7 +29,7 @@ import requests # noqa: E402 from playwright.sync_api import Browser, Error as PlaywrightError, sync_playwright # noqa: E402 -from _board import Board, ext_name, parse_spec, required_packages # noqa: E402 +from _board import Board, ext_name, is_style, parse_spec, required_packages, style_cfg # noqa: E402 BOARD_NAME = "Example board" BOARD_DESCRIPTION = "A phpBB board" @@ -102,7 +103,16 @@ def shot(self, name: str, url: str, admin: bool = False, phone: bool = False, context.close() -# -- screenshots, one function per extension -------------------------------- +# -- screenshots, one function per project ---------------------------------- + +def busiest_forum_and_topic(board: Board) -> tuple: + """The forum with the most topics and the topic with the most replies, so pages show real content.""" + forum = board.sql("SELECT forum_id FROM phpbb_forums WHERE forum_type = 1 " + "ORDER BY forum_topics_approved DESC, forum_id LIMIT 1")[0][0] + topic = board.sql("SELECT topic_id FROM phpbb_topics " + "ORDER BY topic_posts_approved DESC, topic_id LIMIT 1")[0][0] + return forum, topic + def shots_documentation(board: Board, shoot: Shooter, args: argparse.Namespace) -> None: """phpbbmodders/documentation: needs --build, a phpbbdocs-hugo build.""" @@ -145,8 +155,20 @@ def open_misc_tab(p) -> None: admin=True, prepare=open_misc_tab, selector="fieldset:has(legend:has-text('Guests'))") +def shots_prominodeux(board: Board, shoot: Shooter, args: argparse.Namespace) -> None: + """ProMinoDeux style: the main board pages, on desktop and phone. Use --seed for real content.""" + forum, topic = busiest_forum_and_topic(board) + shoot.shot("prominodeux-index", "index.php") + shoot.shot("prominodeux-viewforum", f"viewforum.php?f={forum}") + shoot.shot("prominodeux-viewtopic", f"viewtopic.php?t={topic}") + shoot.shot("prominodeux-posting", f"posting.php?mode=reply&t={topic}&sid={{sid}}", admin=True) + shoot.shot("prominodeux-index-phone", "index.php", phone=True) + shoot.shot("prominodeux-viewtopic-phone", f"viewtopic.php?t={topic}", phone=True) + + SHOTS = { "phpbbmodders/documentation": shots_documentation, + "ProMinoDeux": shots_prominodeux, } @@ -161,19 +183,24 @@ def ensure_chromium(playwright) -> Browser: def main() -> int: - ap = argparse.ArgumentParser(description="Take documentation screenshots of a phpbbmodders extension " + ap = argparse.ArgumentParser(description="Take documentation screenshots of a phpbbmodders extension or style " "on a local test board.", - epilog="Extensions with screenshots: " + ", ".join(sorted(SHOTS)) + epilog="Projects with screenshots: " + ", ".join(sorted(SHOTS)) + ". Exit status: 0 all saved, 1 a screenshot failed, 2 bad arguments.") ap.add_argument("repo", metavar="REPO[@REF]", - help="extension git checkout, optionally with a git ref to install (default ref: HEAD)") + help="extension or style git checkout, optionally with a git ref to install " + "(default ref: HEAD); a style is recognised by its style.cfg") ap.add_argument("-o", "--out", required=True, metavar="DIR", - help="directory to save the PNG files in, for example the extension's docs/images") + help="directory to save the PNG files in, for example the project's docs/images") + ap.add_argument("-s", "--seed", metavar="SCRIPT", + help="PHP script that fills the board with forums, topics and users after the install, " + "run as 'php SCRIPT BOARD_ROOT'; for example seed-forum's bin/seed-standard-fixtures.php") ap.add_argument("--build", metavar="DIR", help="phpbbdocs-hugo build to serve (phpbbmodders/documentation only)") ap.add_argument("-w", "--with", dest="needed", action="append", default=[], metavar="PATH[@REF]", help="git checkout of an extension the extension requires (default ref: HEAD); " - "installed and enabled first; repeat for several, in the order they must be enabled") + "installed and enabled first; repeat for several, in the order they must be enabled " + "(extensions only)") ap.add_argument("-b", "--board-dir", help="board directory created by setup-board.sh (default: $PHPBB_TEST_BOARD)") ap.add_argument("-p", "--port", type=int, default=8083, @@ -185,24 +212,40 @@ def main() -> int: needed = [parse_spec(spec) for spec in args.needed] except ValueError as e: ap.error(str(e)) + if args.seed and not Path(args.seed).is_file(): + ap.error(f"--seed {args.seed} is not a file") if args.build and not (Path(args.build) / "index.html").is_file(): ap.error(f"--build {args.build} is not a built site (no index.html)") - name = ext_name(repo, ref) + style = is_style(repo, ref) + if style and needed: + ap.error("--with is for extensions; a style needs no other extensions") + name = style_cfg(repo, ref)["name"] if style else ext_name(repo, ref) if name not in SHOTS: ap.error(f"no screenshots defined for {name}; add them to SHOTS in {Path(__file__).name}") out_dir = Path(args.out).expanduser().resolve() out_dir.mkdir(parents=True, exist_ok=True) board = Board(args.board_dir, args.port) - for package in required_packages(repo, ref): - if package not in {ext_name(r, dep_ref) for r, dep_ref in needed}: - print(f" [note] {repo.name} requires {package}; if it is a phpBB extension, add --with PATH") - ext, target, out = board.install_ext(repo, ref, needed) + if style: + ext, target = board.install_style(repo, ref) + out = "Successfully installed" + else: + for package in required_packages(repo, ref): + if package not in {ext_name(r, dep_ref) for r, dep_ref in needed}: + print(f" [note] {repo.name} requires {package}; if it is a phpBB extension, add --with PATH") + ext, target, out = board.install_ext(repo, ref, needed) print(f"== {ext} ({repo.name} @ {ref}) -> {out_dir}") try: if "Successfully" not in out: print(f" [FAIL] enable: {out.strip()[-200:]}") return 1 + if args.seed: + seeded = subprocess.run(["php", "-d", "opcache.enable_cli=0", str(Path(args.seed).resolve()), + str(board.root)], capture_output=True, text=True) + if seeded.returncode != 0: + print(f" [FAIL] seed: {(seeded.stderr or seeded.stdout).strip()[-300:]}") + return 1 + print(f" [ok] seeded: {seeded.stdout.strip().splitlines()[-1][:200] if seeded.stdout.strip() else 'done'}") board.sql("UPDATE phpbb_config SET config_value = ? WHERE config_name = 'sitename'", (BOARD_NAME,)) board.sql("UPDATE phpbb_config SET config_value = ? WHERE config_name = 'site_desc'", (BOARD_DESCRIPTION,)) board.purge_cache() @@ -221,7 +264,10 @@ def main() -> int: print(f"== {len(shooter.saved)} saved, {len(shooter.failed)} failed") return 1 if shooter.failed or errors else 0 finally: - board.remove_ext(target) + if style: + board.remove_style(target) + else: + board.remove_ext(target) if __name__ == "__main__": diff --git a/tests/test-phpbb-test-board.sh b/tests/test-phpbb-test-board.sh index 4b799ec..1eeba1d 100755 --- a/tests/test-phpbb-test-board.sh +++ b/tests/test-phpbb-test-board.sh @@ -71,6 +71,10 @@ check "feature_checks.py --help lists the extensions with checks" \ check "screenshots.py --help lists the extensions with screenshots" \ output_has "phpbbmodders/documentation" "$tools/screenshots.py" --help check "screenshots.py without --out exits 2" exits_with 2 "$tools/screenshots.py" "$tmp/repo" +check "screenshots.py --help describes --seed" output_has "--seed SCRIPT" "$tools/screenshots.py" --help +check "screenshots.py --help lists styles with screenshots" output_has "ProMinoDeux" "$tools/screenshots.py" --help +check "screenshots.py with a missing --seed script exits 2" \ + exits_with 2 "$tools/screenshots.py" "$tmp/repo" -o "$tmp/out" --seed "$tmp/no-such-seed.php" for script in smoke_test.py feature_checks.py screenshots.py; do check "$script --help describes --with" output_has "--with PATH[@REF]" "$tools/$script" --help check "$script --with a non-git directory exits 2" \