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 @@ -160,6 +160,8 @@ Saves PNG screenshots of an extension's or style's pages with [Playwright](https

`phpbbmodders/seenotes` needs `--seed` with seed-forum's standard fixtures; it then runs seed-forum's `bin/seed-seenotes-fixtures.php` from the same folder, which gives the members reasons, notes and warnings. Reasons show only to staff, so every shot is taken as the admin: a post's mini-profile with the reason, a profile with the reason and the Moderator notes link, a post by a member without a reason with the notes button, the MCP post details notice, MCP → Moderator notes, ACP → Manage users → Moderator notes, and the ACP reasons and settings pages (in full).

`phpbbmodders/translationhelper` needs `--seed` with seed-forum's standard fixtures; it then runs seed-forum's `bin/seed-translationhelper-fixtures.php` (a test Swedish language and the `seedforum/translatable` fixture extension), saves a partial translation through the extension, and runs `bin/translationhelper-upgrade-fixture.php` to replace the fixture with 1.1.0. It saves the extensions page with the "put back" notice, the editor with a string whose English changed, the Older English versions page and the settings, all in full. The fixture files are removed again afterwards.

`phpbbmodders/prunenotifications` needs `--seed` with seed-forum's standard fixtures. It makes three members dormant, with last visits two to five years ago and 1,840 unread notifications between them, gives the admin 12, then saves the ACP page with the counts and settings (in full), the Prune now confirmation, and the message after every batch has run and the table has been optimized.

`phpbbmodders/topicclaim` needs `--seed` with seed-forum's standard fixtures, for its `forum_moderator` user. That user claims the busiest topic with a note, and the admin claims another topic in the same forum. Claims show only to moderators, so every shot is taken as the admin: the forum's topic list, the claim notice on the topic page, the claim page (with take over, transfer and release), MCP → Claimed topics and the ACP settings.
Expand Down
132 changes: 132 additions & 0 deletions phpbb-test-board/feature_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
"""
import argparse
import hashlib
import html
import io
import json
import re
import secrets
import shutil
import subprocess
import sys
import time
import zipfile
from pathlib import Path
from typing import Callable

Expand Down Expand Up @@ -1632,7 +1635,136 @@ def follow_refresh(body: str) -> tuple:
b.purge_cache()


TH_EXT = "thcheck/demo"
TH_LANG_HEAD = ("<?php\nif (!defined('IN_PHPBB'))\n{\n\texit;\n}\n\nif (empty($lang) || !is_array($lang))\n{\n"
"\t$lang = [];\n}\n\n$lang = array_merge($lang, [\n")


def php_str(text: str) -> str:
"""A PHP single-quoted string literal, so nothing in the text is interpolated."""
return "'" + text.replace("\\", "\\\\").replace("'", "\\'") + "'"


def th_fixture_php(version: str, entries: str) -> str:
"""PHP that (re)creates the thcheck/demo fixture extension at a version, deleting the old folder first
the way an administrator replaces an extension, and adds a test "sv" language when the board lacks one."""
composer = json.dumps({"name": TH_EXT, "type": "phpbb-extension", "version": version,
"license": "GPL-2.0-only", "authors": [{"name": "feature check"}],
"require": {"php": ">=7.2"},
"extra": {"display-name": "Translation Check Fixture",
"soft-require": {"phpbb/phpbb": ">=3.3.0"}}})
lang_file = TH_LANG_HEAD + entries + "]);\n"
ext_php = "<?php\nnamespace thcheck\\demo;\n\nclass ext extends \\phpbb\\extension\\base\n{\n}\n"
return (
"$dir = 'ext/thcheck/demo';"
"if (is_dir($dir)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);"
" foreach ($it as $f) { $f->isDir() ? rmdir($f) : unlink($f); } rmdir($dir); }"
"mkdir($dir . '/language/en', 0777, true);"
f"file_put_contents($dir . '/composer.json', {php_str(composer)});"
f"file_put_contents($dir . '/ext.php', {php_str(ext_php)});"
f"file_put_contents($dir . '/language/en/common.php', {php_str(lang_file)});"
"chmod($dir . '/language', 0777);"
"if (!is_dir('language/sv')) { $src = 'language/en/'; mkdir('language/sv', 0777);"
" $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);"
" foreach ($it as $f) { $t = 'language/sv/' . substr($f->getPathname(), strlen($src)); $f->isDir() ? mkdir($t, 0777, true) : copy($f, $t); }"
" file_put_contents('language/sv/iso.txt', \"Swedish\\nSvenska (test copy)\\nfeature check\\n\"); }"
"$r = $db->sql_query(\"SELECT lang_id FROM \" . LANG_TABLE . \" WHERE lang_iso = 'sv'\");"
"if (!$db->sql_fetchfield('lang_id')) { $db->sql_query('INSERT INTO ' . LANG_TABLE . ' ' . $db->sql_build_array('INSERT',"
" ['lang_iso' => 'sv', 'lang_dir' => 'sv', 'lang_english_name' => 'Swedish', 'lang_local_name' => 'Svenska (test copy)', 'lang_author' => 'feature check'])); }"
"echo 'fixture ok';"
)


def check_translationhelper(b: Board) -> None:
"""phpbbmodders/translationhelper: translate a fixture extension in the ACP (tricky text stays text), then
replace the extension's folder with a new version as an upgrade does: the saved translation is put back
from store/, a notice says so, and the string whose English changed shows its old and new English."""
out = b.php(th_fixture_php("1.0.0", "\t'TH_WELCOME' => 'Welcome, %s!',\n\t'TH_RULES' => 'Read the <b>rules</b>.',\n"
"\t'TH_POSTS' => [1 => '%d post', 2 => '%d posts'],\n"))
ok("fixture ok" in out, "the fixture extension and test language are set up")
b.purge_cache()
admin = requests.Session()
sid = b.login(admin, acp=True)
acp = f"{b.base}/adm/index.php?i=-phpbbmodders-translationhelper-acp-main_module&mode=extensions&lang=sv&sid={sid}"

overview = admin.get(acp, timeout=30).text
ok("Translation Check Fixture" in overview, "the extensions page lists the fixture extension")

page_url = f"{acp}&action=edit&ext={TH_EXT}&file=common.php"
page = admin.get(page_url, timeout=30).text
action, fields = form_fields(page, "translationhelper_edit")
keys = {name: html.unescape(value) for name, value in fields.items() if name.startswith("key[")}
ok(len(keys) == 4, f"the editor shows the four untranslated strings ({len(keys)})")
texts = {
'["TH_WELCOME"]': "Välkommen, %s!",
'["TH_RULES"]': "Läs <b>reglerna</b>. It's \\ <?php echo 'x'; ?>",
'["TH_POSTS",1]': "%d inlägg",
'["TH_POSTS",2]': "%d inlägg",
}
data = {name: value for name, value in fields.items() if not name.startswith("key[")}
for name, path_key in keys.items():
data[name] = path_key
data["text" + name[3:]] = texts.get(path_key, "")
data["save"] = "Save translation"
body = post_back(admin, f"{b.base}/adm", page_url, action, data)
ok("saved and written to the board" in body, "saving writes the translation to the board")

read = ("$lang = []; include 'ext/thcheck/demo/language/sv/common.php'; echo json_encode($lang);")
written = b.php(read).strip().splitlines()
lang = json.loads(written[-1]) if written else {}
ok(lang.get("TH_RULES") == texts['["TH_RULES"]'] and lang.get("TH_POSTS", {}).get("2") == "%d inlägg",
"the written language file gives back exactly what was typed")
saved = b.php("echo is_file('store/phpbbmodders_translationhelper/translations/thcheck/demo/sv.json') ? 'yes' : 'no';")
ok(saved.strip().endswith("yes"), "a copy of the translation is saved in store/")

# Older English versions: a release ZIP, then loose language files, in the one upload field
versions_url = f"{acp}&action=versions&ext={TH_EXT}"
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as archive:
archive.writestr("demo-0.9.0/composer.json", json.dumps({"name": TH_EXT, "type": "phpbb-extension"}))
archive.writestr("demo-0.9.0/language/en/common.php", TH_LANG_HEAD + "\t'TH_WELCOME' => 'Hello, %s!',\n]);\n")
for label, files in (
("0.9.0", [("english_files[]", ("demo-0.9.0.zip", zip_buffer.getvalue(), "application/zip"))]),
("0.8.0", [("english_files[]", ("common.php", TH_LANG_HEAD + "\t'TH_WELCOME' => 'Hi, %s!',\n]);\n", "text/plain")),
("english_files[]", ("info.php", TH_LANG_HEAD + "\t'TH_INFO' => 'Info',\n]);\n", "text/plain"))]),
):
action, fields = form_fields(admin.get(versions_url, timeout=30).text, "translationhelper_upload")
time.sleep(1.1)
body = admin.post(f"{b.base}/adm/" + action.lstrip("./"), data={**fields, "version": label, "upload": "Upload"},
files=files, timeout=30).text
added = b.sql("SELECT COUNT(*) FROM phpbb_translationhelper_snapshots s JOIN phpbb_translationhelper_strings t "
"ON t.snapshot_id = s.snapshot_id WHERE s.ext_name = ? AND s.ext_version = ? "
"AND s.snapshot_source = 'upload'", (TH_EXT, label))
expected = 1 if label == "0.9.0" else 2
ok("were added" in body and added and int(added[0][0]) == expected,
f"uploading {'a release ZIP' if label == '0.9.0' else 'two language files'} adds version {label}")

# Upgrade: the folder is replaced by 1.1.0, which changes one English string and has no Swedish files
b.php(th_fixture_php("1.1.0", "\t'TH_WELCOME' => 'Welcome back, %s!',\n\t'TH_RULES' => 'Read the <b>rules</b>.',\n"
"\t'TH_POSTS' => [1 => '%d post', 2 => '%d posts'],\n\t'TH_NEW' => 'New string',\n"))
gone = b.php("echo is_file('ext/thcheck/demo/language/sv/common.php') ? 'there' : 'gone';")
ok(gone.strip().endswith("gone"), "the upgrade removed the Swedish file")
overview = admin.get(acp, timeout=30).text
ok("translation put back for Translation Check Fixture" in overview, "the extensions page says it was put back")
restored = b.php(read).strip().splitlines()
lang = json.loads(restored[-1]) if restored else {}
ok(lang.get("TH_WELCOME") == "Välkommen, %s!" and lang.get("TH_NEW") == "New string",
"the translation is back, with English for the new string")
page = admin.get(page_url, timeout=30).text
ok("English changed" in page and "Welcome, %s!" in page and "Welcome back, %s!" in page,
"the changed string shows its old and new English")
ok("&lt;b&gt;rules&lt;/b&gt;" in admin.get(page_url + "&filter=all", timeout=30).text,
"English text is shown escaped, not as HTML")

# The fixture extension and its saved translation are files, which a database reset does not undo
b.php("foreach (['ext/thcheck', 'store/phpbbmodders_translationhelper/translations/thcheck'] as $dir) {"
"if (!is_dir($dir)) { continue; }"
"$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);"
"foreach ($it as $f) { $f->isDir() ? rmdir($f) : unlink($f); } rmdir($dir); }")


CHECKS = {
"phpbbmodders/translationhelper": check_translationhelper,
"phpbbmodders/prunenotifications": check_prunenotifications,
"phpbbmodders/feedlinks": check_feedlinks,
"phpbbmodders/topicclaim": check_topicclaim,
Expand Down
59 changes: 59 additions & 0 deletions phpbb-test-board/screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,64 @@ def public_directory(p) -> None:
shoot.shot("autodbbackup-update-directory-error", acp, admin=True, full_page=True, prepare=public_directory)


def shots_translationhelper(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
"""phpbbmodders/translationhelper: the extensions page right after an extension upgrade (with
the "put back" notice), the editor showing a string whose English changed, the older English
versions page, and the settings.

Needs seed-forum's fixtures: run with --seed and seed-forum's
bin/seed-standard-fixtures.php. seed-forum's bin/seed-translationhelper-fixtures.php,
next to it, adds a test Swedish language and the seedforum/translatable 1.0.0
fixture extension; a translation is then saved through the extension, and
bin/translationhelper-upgrade-fixture.php replaces the fixture with 1.1.0 the way an
upgrade does.
"""
if not args.seed:
raise RuntimeError("run with --seed and seed-forum's bin/seed-standard-fixtures.php")
seed_bin = Path(args.seed).resolve().parent
try:
_translationhelper_shots(board, shoot, seed_bin)
finally:
# The fixtures are files, which the board's database reset does not undo
for leftover in ("ext/seedforum", "language/sv", "store/phpbbmodders_translationhelper"):
shutil.rmtree(board.root / leftover, ignore_errors=True)


def _translationhelper_shots(board: Board, shoot: Shooter, seed_bin: Path) -> None:
"""The work of shots_translationhelper(), whose caller removes the fixture files afterwards."""
for script in ("seed-translationhelper-fixtures.php",):
done = subprocess.run(["php", "-d", "opcache.enable_cli=0", str(seed_bin / script), str(board.root)],
capture_output=True, text=True)
if done.returncode != 0:
raise RuntimeError(f"{script} failed: {(done.stderr or done.stdout).strip()[-300:]}")
board.purge_cache()
texts = {
'["TRANSLATABLE_QUOTE"]': "Det är en citerad text",
'["TRANSLATABLE_HTML"]': "Läs <strong>reglerna</strong> först.",
'["TRANSLATABLE_POSTS",1]': "%d inlägg",
'["TRANSLATABLE_POSTS",2]': "%d inlägg",
}
saved = board.php("$m = $phpbb_container->get('phpbbmodders.translationhelper.manager');"
"$m->take_snapshots(true);"
f"$p = $m->save_file('seedforum/translatable', 'sv', 'common.php', json_decode({json.dumps(json.dumps(texts))}, true), [], 2);"
"echo $p ? 'problems' : 'saved';", check=True)
if "saved" not in saved:
raise RuntimeError(f"saving the fixture translation failed: {saved.strip()[-300:]}")
upgraded = subprocess.run(["php", str(seed_bin / "translationhelper-upgrade-fixture.php"), str(board.root)],
capture_output=True, text=True)
if upgraded.returncode != 0:
raise RuntimeError(f"upgrade fixture failed: {(upgraded.stderr or upgraded.stdout).strip()[-300:]}")

acp = "adm/index.php?i=-phpbbmodders-translationhelper-acp-main_module&mode={mode}&sid={{sid}}"
ext = acp.format(mode="extensions") + "&lang=sv"
shoot.shot("translationhelper-extensions", ext, admin=True, full_page=True)
shoot.shot("translationhelper-editor", ext + "&action=edit&ext=seedforum%2Ftranslatable&file=common.php",
admin=True, full_page=True)
shoot.shot("translationhelper-versions", ext + "&action=versions&ext=seedforum%2Ftranslatable",
admin=True, full_page=True)
shoot.shot("translationhelper-settings", acp.format(mode="settings"), admin=True, full_page=True)


def shots_seenotes(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
"""phpbbmodders/seenotes: the reason line next to a post and on a profile, the notes button
next to a post by a member without a reason, the MCP post notice, MCP → Moderator notes, and
Expand Down Expand Up @@ -1434,6 +1492,7 @@ def run_prune(p) -> None:


SHOTS = {
"phpbbmodders/translationhelper": shots_translationhelper,
"phpbbmodders/prunenotifications": shots_prunenotifications,
"phpbbmodders/seenotes": shots_seenotes,
"phpbbmodders/adduser": shots_adduser,
Expand Down
Loading