utils/repo_contract/tests/test_launcher_variants.py
Krzysztof kuhy Rudnicki d1ca1e3d5b
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
repo_contract: launcher gate (icon ink radius, compliant label, hidden debug variants) + hook_sync
Flutter repos now fail the repo contract when a launcher icon's ink is not on
the shared 340px circle (png_ink), the launcher label breaks the naming rule
(capital, ASCII letters, <= 10 chars, <= 522 wide in GoogleSansFlex), or an
applicationIdSuffix variant still carries the LAUNCHER category. hook_sync
rewrites a repo's stale repo-contract `files:` line to the widened template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017k9j5dbjenpfZj7dh2faPN
2026-10-23 14:54:15 +02:00

134 lines
4.3 KiB
Python

"""Rule 3: an applicationIdSuffix variant hides LAUNCHER but keeps MAIN."""
from __future__ import annotations
from pathlib import Path
from repo_contract import android_manifest as am
from .conftest import write
from .flutter_app import MAIN, NS, REMOVE, SANDBOX_GRADLE, flutter_app, hide, manifest
SANDBOX = "android/app/src/sandbox/AndroidManifest.xml"
def sandbox_app(root: Path) -> Path:
return flutter_app(root, gradle=SANDBOX_GRADLE)
def test_no_suffix_variant_passes(tmp_path: Path) -> None:
assert am.variant_problems(flutter_app(tmp_path)) == []
def test_no_gradle_passes(tmp_path: Path) -> None:
assert am.variant_problems(flutter_app(tmp_path, gradle=None)) == []
def test_hidden_variant_passes(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide())
assert am.variant_problems(app) == []
def test_fully_qualified_activity_name_matches(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide("com.kuhy.demo.MainActivity"))
assert am.variant_problems(app) == []
def test_missing_variant_manifest_fails(tmp_path: Path) -> None:
got = am.variant_problems(sandbox_app(tmp_path))
assert got == [
(
f"variant 'sandbox' sets applicationIdSuffix but {SANDBOX} is missing:"
' hide it with <category android:name="android.intent.category.LAUNCHER"'
' tools:node="remove"/> in .MainActivity\'s intent-filter'
)
]
def test_variant_manifest_without_remove_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide(tag=""))
assert am.variant_problems(app) == [
(
f"variant 'sandbox': {SANDBOX} does not remove the LAUNCHER category from"
' .MainActivity (tools:node="remove" on the <category>)'
)
]
def test_remove_on_other_activity_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide(".OtherActivity"))
assert len(am.variant_problems(app)) == 1
def test_removing_main_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide(main=REMOVE))
assert am.variant_problems(app) == [
(
f"variant 'sandbox': {SANDBOX} removes android.intent.action.MAIN from"
" .MainActivity; keep MAIN so `am start -n` still works"
)
]
def test_removing_whole_activity_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
body = hide().replace(
'<activity android:name=".MainActivity">',
'<activity android:name=".MainActivity" tools:node="remove">',
)
write(app, SANDBOX, body)
assert "removes android.intent.action.MAIN" in am.variant_problems(app)[0]
def test_removing_main_intent_filter_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(
app,
SANDBOX,
hide(tag="").replace("<intent-filter>", f"<intent-filter{REMOVE}>"),
)
assert "removes android.intent.action.MAIN" in am.variant_problems(app)[0]
def test_invalid_variant_manifest_fails(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, SANDBOX, hide().replace(f" {am.TOOLS_DECL}", ""))
got = am.variant_problems(app)
assert len(got) == 1 and got[0].startswith(f"{SANDBOX} is not valid XML:")
def test_main_manifest_without_launcher_needs_nothing(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, MAIN.as_posix(), f"<manifest {NS}><application/></manifest>")
assert am.variant_problems(app) == []
def test_unreadable_main_manifest_needs_nothing(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
write(app, MAIN.as_posix(), "<manifest")
assert am.variant_problems(app) == []
def test_activity_alias_is_a_launcher_too(tmp_path: Path) -> None:
app = sandbox_app(tmp_path)
body = (
manifest()
.replace("<activity ", "<activity-alias ")
.replace("</activity>", "</activity-alias>")
)
write(app, MAIN.as_posix(), body)
got = am.variant_problems(app)
assert len(got) == 1 and "is missing" in got[0]
def test_groovy_buildtype_suffix(tmp_path: Path) -> None:
gradle = "android { buildTypes { debug { applicationIdSuffix '.debug' } } }\n"
app = flutter_app(tmp_path, gradle=gradle, kts=False)
write(app, "android/app/src/debug/AndroidManifest.xml", hide())
assert am.variant_problems(app) == []