Files
RustBox/tests/roundcube_skin_contract.py
T
Mateusz Gruszczyński 93762fe9ef
RustBox CI / rustbox (push) Waiting to run
v0.0.3
2026-10-30 00:12:09 +01:00

155 lines
7.2 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" 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.3"' 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"]:
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")