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]) == 45 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 "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", "Hello", "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-appearance-preset" not in source for value in ["rustbox", "roundcube-classic", "roundcube-larry", "roundcube-elastic", "clean", "graphite", "high-contrast"]: 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", "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 "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", "graphite", "high-contrast"]: assert f'"{value}"' in db_source cargo = (ROOT / "Cargo.toml").read_text() assert 'version = "0.0.2"' in cargo 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() for path in ROOT.glob("src/web/i18n/*.json"): json.loads(path.read_text()) 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() print("roundcube-skin-contract: OK")