원클릭으로
appium-test-authoring
Appium test authoring, page objects, locators, assertions, and new-screen coverage.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Appium test authoring, page objects, locators, assertions, and new-screen coverage.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use this skill to generate well-branded interfaces and assets for RIPDPI (an Android-native, Compose-first VPN and DPI-bypass app), either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, brand-ship icons, and an interactive UI kit of every public composable from the live ui/components tree.
Use when managing the Rust workspace, adding/removing crates, editing workspace dependencies, running cargo nextest/audit/deny, configuring Cargo profiles for Android cross-compilation, debugging Cargo.lock churn, migrating crate edition, or wiring Gradle to cargo via the ripdpi.android.rust-native plugin.
Use when modifying the diagnostics scan pipeline, ScanRequest/ScanReport types, ProbeTask families, ripdpi-monitor-engine / ripdpi-diagnostics-* crates, strategy-probe candidates, the diagnostics catalog (packs/profiles), wire-schema contracts between Rust and Kotlin, DIAGNOSTICS_ENGINE_SCHEMA_VERSION, golden contract tests, or adding a new probe type / profile. Triggers on diagnostics scans, strategy probes, automatic audit, dpi-detector profiles, or anything in core/diagnostics or native/rust/crates/ripdpi-monitor-*.
Android-specific Rust build, verification, and packaging — per-target 16 KiB page alignment, size-optimized release profile, ELF symbol allowlist, .so size budgets, NDK 29 specifics. Use when modifying .cargo/config.toml for Android targets, the workspace [profile.release] / [profile.android-jni] block, or when verifying a built .so before release.
Telemetry and observability discipline for the RIPDPI Android Rust stack — control-plane vs data-plane logging, bounded flume event queues, atomic counters, snapshot polling, readiness callbacks, and deterministic JSON contracts. Use when authoring or modifying telemetry emission code, the bounded event ring, the Kotlin-side telemetry consumer, or any per-packet logging.
Custom detekt rules, DI/privacy/suppression guardrails, detekt.yml configuration, and false-positive triage.
| name | appium-test-authoring |
| description | Appium test authoring, page objects, locators, assertions, and new-screen coverage. |
Python pytest suite under appium/. Page Object Model with resource-id locators via AppiumBy.ID. Automation contract drives app state (see appium-automation-contract skill).
Tag source of truth:
app/src/main/kotlin/com/poyka/ripdpi/ui/testing/RipDpiTestTags.ktdocs/automation/selector-contract.mdappium/
conftest.py # Session driver + per-test launch fixture
pytest.ini # Pytest config and custom markers
requirements.txt # Pinned deps (Appium-Python-Client, pytest, selenium)
lib/
capabilities.py # UiAutomator2Options builder
launch_contract.py # Automation contract constants + arg builder
driver_helpers.py # Shared wait/find helpers
pages/
base_page.py # Base class -- all page objects inherit this
{screen}_page.py # One page object per screen
tests/
test_{NN}_{description}.py # Numbered test files
File: appium/pages/{screen}_page.py
Checklist:
"""{Screen name} screen page object."""from __future__ import annotations + from .base_page import BasePageBasePageSCREEN = "{screen}-screen" class constantSCREAMING_SNAKE class constantsis_loaded() method: return self.is_visible(self.SCREEN)is_*_visible() methods| Component | Pattern | Example |
|---|---|---|
| Screen root | {screen}-screen | home-screen |
| Button | {screen}-{action}[-button] | home-connection-button |
| Input field | {screen}-{field} | dns-plain-address |
| Toggle | {screen}-{name}-toggle | settings-webrtc-toggle |
| Card | {screen}-{name}-card | home-approach-card |
| Dialog | {component}-dialog | vpn-permission-dialog |
| Dialog action | {component}-dialog-{action} | vpn-permission-dialog-continue |
| Dynamic | {screen}-{type}-{identifier} | dns-resolver-cloudflare |
"""{Screen name} screen page object."""
from __future__ import annotations
from .base_page import BasePage
class ExamplePage(BasePage):
SCREEN = "example-screen"
TITLE = "example-title"
PRIMARY_BUTTON = "example-primary-button"
INPUT_FIELD = "example-input-field"
def is_loaded(self) -> bool:
return self.is_visible(self.SCREEN)
def tap_primary(self) -> None:
self.tap(self.PRIMARY_BUTTON)
def set_input(self, text: str) -> None:
self.clear_and_type(self.INPUT_FIELD, text)
def is_title_visible(self) -> bool:
return self.is_visible(self.TITLE)
| Action | Pattern | Return |
|---|---|---|
| Tap/click | tap_{component}() | None |
| Type text | set_{field}(value) | None |
| Clear + type | fill_{field}(value) or set_{field}(value) | None |
| Check visibility | is_{component}_visible() | bool |
| Select option | select_{option}(value) | None |
| Toggle switch | toggle_{setting}() | None |
| Navigate away | tap_{destination}() | destination page object (optional) |
| Scroll to element | use self.scroll_to(TAG) internally | None or element |
Source: appium/pages/base_page.py
| Method | Signature | Default | Notes |
|---|---|---|---|
_resource_id | (tag: str) -> str | -- | Prepends com.poyka.ripdpi:id/ if tag has no : |
find | (tag: str) -> WebElement | -- | Immediate lookup, no wait. Raises if not found. |
wait_for | (tag: str, timeout: int = 10) -> WebElement | 10s | Explicit wait via EC.presence_of_element_located |
is_visible | (tag: str, timeout: int = 3) -> bool | 3s | Returns False on timeout, never raises |
tap | (tag: str, timeout: int = 10) -> None | 10s | wait_for then .click() |
clear_and_type | (tag: str, text: str, timeout: int = 10) -> None | 10s | .clear() then .send_keys(text) |
swipe_horizontal | (direction: str = "left") -> None | left | Swipes across screen center, 300ms duration |
scroll_to | (tag: str, max_swipes: int = 5) -> WebElement | 5 swipes | Swipes down until visible, returns element |
AppiumBy.ID with resource-id tags. Never XPath, class name, or accessibility-id.: are auto-prefixed by _resource_id() to com.poyka.ripdpi:id/{tag}.Modifier.testTag("{tag}") in Jetpack Compose source.dns_settings).diagnostics-strategy-winning-path, diagnostics-strategy-full-matrix-toggle, and diagnostics-workflow-restriction-action.File: appium/tests/test_{NN}_{description}.py (use next sequential number)
Checklist:
"""Test {NN}: {description}."""import pytest + import page object(s)@pytest.mark.automation(...) with keyword paramsdef test_{descriptive_name}(driver):page = PageClass(driver)assert page.is_loaded(), "Screen should be visible""""Test NN: Description of what this test covers."""
import pytest
from pages.example_page import ExamplePage
@pytest.mark.automation(
start_route="example",
data_preset="settings_ready",
)
def test_example_feature(driver):
page = ExamplePage(driver)
assert page.is_loaded(), "Example screen should be visible"
page.set_input("test value")
page.tap_primary()
assert page.is_title_visible(), "Title should remain visible after action"
When testing different flows on the same screen, use separate functions with their own markers:
@pytest.mark.automation(start_route="dns_settings", data_preset="settings_ready")
def test_doh_resolver_selection(driver):
dns = DnsSettingsPage(driver)
assert dns.is_loaded(), "DNS settings should be visible"
dns.select_doh_resolver("cloudflare")
@pytest.mark.automation(start_route="dns_settings", data_preset="settings_ready")
def test_custom_doh_url(driver):
dns = DnsSettingsPage(driver)
assert dns.is_loaded(), "DNS settings should be visible"
dns.set_custom_doh_url("https://example.com/dns-query")
assert dns.is_custom_save_visible(), "Save button should appear"
# Visibility check -- always include failure message
assert page.is_visible(TAG), "Description of what should be true"
# Negative check
assert not page.is_visible(TAG), "Element should not be visible after action"
# Screen transition -- assert new page loaded after navigation
page.tap_settings()
settings = SettingsPage(driver)
assert settings.is_loaded(), "Settings screen should appear"
Rules:
assert (not unittest assertions)| Tier | Timeout | Use For |
|---|---|---|
| Short | 3s | is_visible() probes, elements expected on current screen |
| Medium | 10s | wait_for(), tap(), normal element appearance |
| Long | 15s | Screen launch in conftest.py only |
time.sleep() in test filespage.wait_for("slow-element", timeout=15)| Change | Action |
|---|---|
| Locator tag renamed | Update the class constant in the page object only |
| New element on existing screen | Add constant + semantic method to page object, add test |
| Screen added | Create new page object + new test file |
| Screen removed | Delete page object + test file |
| Navigation path changed | Update test flow, not page object API |
| Preset values changed | Update marker params in affected tests |
| Mistake | Fix |
|---|---|
| Using XPath or class name locators | Use AppiumBy.ID with resource-id tags only |
| Assertions in page objects | Move to test functions; page objects expose state |
time.sleep() in tests | Use wait_for() or is_visible() with timeout |
| Missing failure message on assert | Always provide descriptive string |
Hardcoding com.poyka.ripdpi:id/ prefix | Use tag shorthand; _resource_id() adds prefix |
Skipping is_loaded() check | Always verify screen loaded as first assertion |
| Monolithic tests testing many features | One behavior per test function |
Missing @pytest.mark.automation | Every test needs the marker for the launch contract |
.github/skills/appium-automation-contract/SKILL.md -- Preset values and marker parameters.github/skills/appium-test-debug/SKILL.md -- Troubleshooting failures and flakiness