Files
RustBox/tests/roundcube_skin_contract.py
T
2026-10-30 14:41:15 +01:00

158 lines
7.4 KiB
Python

from pathlib import Path
import json
import sqlite3
from html.parser import HTMLParser
import tinycss2
ROOT = Path(__file__).resolve().parents[1]
class IdCollector(HTMLParser):
def __init__(self):
super().__init__()
self.ids = []
def handle_starttag(self, _tag, attrs):
attrs = dict(attrs)
if "id" in attrs:
self.ids.append(attrs["id"])
def js_bundle_source():
js_dir = ROOT / "src/web/js"
names = [line.strip() for line in (js_dir / "manifest.txt").read_text().splitlines() if line.strip() and not line.lstrip().startswith("#")]
assert len(names) >= 10
assert len(names) == len(set(names))
return names, "\n".join((js_dir / name).read_text() for name in names)
def test_schema_and_response_templates():
conn = sqlite3.connect(":memory:")
migrations = sorted(ROOT.joinpath("migrations").glob("V*.sql"))
for migration in migrations:
conn.executescript(migration.read_text())
assert int(migrations[-1].name[1:4]) == 46
display_columns = {row[1] for row in conn.execute("pragma table_info(display_preferences)")}
response_columns = {row[1] for row in conn.execute("pragma table_info(response_templates)")}
assert "interface_skin" in display_columns
assert "preset_scale_unlocked" in display_columns
assert "format" in response_columns
conn.execute("insert into response_templates(name,body,format) values (?,?,?)", ("Text", "Hello", "text"))
conn.execute("insert into response_templates(name,body,format) values (?,?,?)", ("HTML", "<b>Hello</b>", "html"))
assert conn.execute("select count(*) from response_templates").fetchone()[0] == 2
def test_html_contract():
source = ROOT.joinpath("src/web/index.html").read_text()
parser = IdCollector()
parser.feed(source)
duplicates = {value for value in parser.ids if parser.ids.count(value) > 1}
assert not duplicates
assert "folder-drawer-header" in source
assert "#icon-arrow-left" in source
assert "pref-interface-skin" in source
assert "compose-response-template" in source
assert 'data-default-theme="__RUSTBOX_DEFAULT_THEME__"' in source
assert 'id="folder-quota"' in source
assert 'id="folder-quota-bar"' in source
assert 'id="reader-to"' in source and 'aria-expanded="false"' in source
for control_id in ["message-details-menu", "message-transfer-menu", "reader-move-action", "reader-copy-action", "compose-mode"]:
assert f'id="{control_id}"' in source
assert "data-appearance-preset" not in source
for value in ["rustbox", "roundcube-classic", "roundcube-larry", "roundcube-elastic", "clean", "gmail", "proton", "zoho", "outlook", "minimal", "graphite", "high-contrast", "fastmail", "apple-mail", "yahoo", "hey", "nord"]:
assert f'value="{value}"' in source
def test_roundcube_palette_contract():
classic_elastic = (ROOT / "src/web/css/55-roundcube-skins.css").read_text().lower()
extra_styles = (ROOT / "src/web/css/56-interface-styles.css").read_text().lower()
for css in [classic_elastic, extra_styles]:
errors = [item for item in tinycss2.parse_stylesheet(css, skip_comments=False, skip_whitespace=False) if item.type == "error"]
assert not errors
for token in ["#f6f6f6", "#f2f2f2", "#cc3333", "#929292", "#37beff", "#2f3a3f", "#21292c", "#c5d1d3", "#4d6066", "#374549"]:
assert token in classic_elastic
for selector in [
'roundcube-classic"][data-theme="light',
'roundcube-classic"][data-theme="dark',
'roundcube-elastic"][data-theme="light',
'roundcube-elastic"][data-theme="dark',
]:
assert selector in classic_elastic
for selector in ["roundcube-larry", "clean", "gmail", "proton", "zoho", "outlook", "minimal", "graphite", "high-contrast"]:
assert f'data-interface-skin="{selector}"' in extra_styles
manifest = (ROOT / "src/web/css/manifest.txt").read_text()
assert "55-roundcube-skins.css" in manifest
assert "56-interface-styles.css" in manifest
def test_modular_js_contract():
assert not (ROOT / "src/web/app.js").exists()
names, app = js_bundle_source()
assert names[0] == "00-bootstrap.js"
assert names[-1] == "95-bindings-boot.js"
assert "interfaceStyleProfiles" in app
assert '"roundcube-classic"' in app
assert '"roundcube-larry"' in app
assert '"roundcube-elastic"' in app
assert "loadResponseTemplates" in app
assert "insertSavedResponse" in app
assert "loadQuota();" in app
assert "toggleReaderAddressExpansion" in app
assert "translateScopedUiText" in app
assert "presetScaleUnlocked" in app
assert "positionToolbarMenu" in app
assert "progressToast" not in app
assert "attachmentPreviewLimitBytes" in app
assert 'label: "Preview"' in app
assert "This attachment is too large for preview. Download it instead?" in app
assert "appearancePresets" not in app
assert "applyAppearancePreset" not in app
assert "src/web/js/manifest.txt" in (ROOT / "README.md").read_text()
build = (ROOT / "build.rs").read_text()
assert 'root.join("src/web/js")' in build
assert 'out.join("rustbox-ui.js")' in build
assert 'include_bytes!(concat!(env!("OUT_DIR"), "/rustbox-ui.js"))' in (ROOT / "src/ui.rs").read_text()
def test_backend_skin_contract_and_version():
db_source = (ROOT / "src/db/display_preferences.rs").read_text()
for value in ["rustbox", "roundcube-classic", "roundcube-larry", "roundcube-elastic", "clean", "gmail", "proton", "zoho", "outlook", "minimal", "graphite", "high-contrast", "fastmail", "apple-mail", "yahoo", "hey", "nord"]:
assert f'"{value}"' in db_source
cargo = (ROOT / "Cargo.toml").read_text()
assert 'version = "0.0.6"' in cargo
config = (ROOT / "src/config.rs").read_text()
assert "pub default_theme: String" in config
assert '"system" | "dark" | "light"' in config
ui = (ROOT / "src/ui.rs").read_text()
assert 'replace("__RUSTBOX_DEFAULT_THEME__", &config.default_theme)' in ui
assert "V044__interface_skin.sql" in (ROOT / "src/db/pool.rs").read_text()
assert "V045__response_template_format.sql" in (ROOT / "src/db/pool.rs").read_text()
assert "V046__preset_scale_unlock.sql" in (ROOT / "src/db/pool.rs").read_text()
for path in ROOT.glob("src/web/i18n/*.json"):
json.loads(path.read_text())
def test_mobile_003_contract():
responsive = (ROOT / "src/web/css/60-responsive.css").read_text()
assert '.sieve-builder .checkbox-row input[type="checkbox"]' in responsive
assert ".dialog>.dialog-card" in responsive
assert "grid-template-rows: auto minmax(0, 1fr)" in responsive
assert ".calendar-toolbar" in responsive
assert ".message-meta.addresses-expanded" in responsive
pl = json.loads((ROOT / "src/web/i18n/pl.json").read_text())
assert len(pl.get("_text", {})) >= 450
for key in ["Rule enabled", "Stop after this rule", "Calendar", "Message headers", "Message source", "Forward as attachment", "Show headers", "Show source", "Write a message", "Preview", "Download", "Preview unavailable"]:
assert key in pl["_text"]
for key in ["reply", "reply_all", "forward", "message_details", "move_copy", "move_to", "copy_to"]:
assert key in pl
if __name__ == "__main__":
test_schema_and_response_templates()
test_html_contract()
test_roundcube_palette_contract()
test_modular_js_contract()
test_backend_skin_contract_and_version()
test_mobile_003_contract()
print("roundcube-skin-contract: OK")