- CommandReceiver: exported broadcast receiver for adb CLI control - start/stop/status/set_config commands - Usage: adb shell am broadcast -a com.sing.vpn.CMD -n com.sing.vpn/.CommandReceiver --es cmd start - SingConfig: add controlplane.tailscale.com → direct route rule to prevent tailnet control traffic from going through proxy
11 KiB
11 KiB
Android UI Redesign — Test Plan
Environment Setup
# Create emulator (API 34, x86_64 for faster emulation)
sdkmanager "system-images;android-34;google_apis;x86_64"
avdmanager create avd -n sing-test -k "system-images;android-34;google_apis;x86_64" -d pixel_6
# Start emulator
emulator -avd sing-test -no-snapshot-load
# Build + install (from android/ dir)
./gradlew installDebug
adb shell am start -n com.sing.vpn/.MainActivity
Test Suites
1. UI Navigation
| # | Test | Steps | Expected |
|---|---|---|---|
| 1.1 | Splash → Overview | Cold start app | Splash icon on dark bg, transitions to Overview tab |
| 1.2 | Bottom nav tabs | Tap Overview → Nodes → Config | Each screen renders, no blank screens |
| 1.3 | Tab state preservation | Scroll down in Config → switch to Nodes → back to Config | Scroll position preserved |
| 1.4 | Deep nav: Connections | Config → Active Connections | ConnectionsScreen opens, back arrow returns to Config |
| 1.5 | Deep nav: Logs | Config → Logs | LogsScreen opens, shows "No logs yet" |
| 1.6 | Deep nav: Rules | Config → Proxy domains | EditRulesScreen opens, can type text, back saves |
| 1.7 | Overview → Connections | Connected → "View all →" link | Opens ConnectionsScreen |
| 1.8 | System back button | Open Logs → press system back | Returns to Config, not exit |
| 1.9 | Rapid tab switching | Tap tabs quickly 10 times | No crash, no blank screen |
| 1.10 | Theme consistency | All screens | Dark background (#0D0D0D), correct text hierarchy, purple accents |
2. Node Management
| # | Test | Steps | Expected |
|---|---|---|---|
| 2.1 | Empty state | First launch / delete all nodes | Empty list, header shows "0 nodes" |
| 2.2 | Import URI | Nodes → "+" → Paste URI → paste vless://... |
Node added, list refreshes, count updates |
| 2.3 | Import subscription | Nodes → "+" → Subscription → paste URL | Nodes fetched, count updates, toast shows count |
| 2.4 | Import JSON | Nodes → "+" → JSON → paste sing-box config | Nodes parsed correctly |
| 2.5 | Import empty text | Nodes → "+" → Paste URI → leave empty → Import | No crash, no action |
| 2.6 | Filter fake nodes | Import sub with "剩余流量" / "套餐到期" entries | Fake metadata nodes filtered out |
| 2.7 | Select node (tap) | Tap node row | Green dot moves to tapped node, previous deactivated |
| 2.8 | Long press → sheet | Long press node | Bottom sheet: Set Active, Test Latency, Delete |
| 2.9 | Delete node | Long press → Delete | Node removed, list refreshes, helper restarts |
| 2.10 | Speed test (button) | Tap refresh icon in header | Delay values appear with color coding |
| 2.11 | Delay colors | After speed test | Green <300ms, yellow 300-600ms, red >600ms, "timeout" for -1 |
| 2.12 | Tailscale tab hidden | Tailscale disabled in Config | Only "Proxy" chip, no "Tailscale" |
| 2.13 | Tailscale tab visible | Enable Tailscale in Config → Nodes tab | "Tailscale" chip appears, tap shows peers |
| 2.14 | Node persistence | Add nodes → kill app → reopen | Nodes still in list |
3. VPN Connection
| # | Test | Steps | Expected |
|---|---|---|---|
| 3.1 | First connect | Overview → Connect | VPN permission dialog → approve → "Online" + green dot |
| 3.2 | Connection details | Connected state | Node name, server IP:port, protocol label, uptime visible |
| 3.3 | Protocol label | Various node types | "VLESS + Reality", "TROJAN + TLS", "SS" etc. |
| 3.4 | Uptime counter | Stay connected 10s | Timer ticks: 00:00:01 → 00:00:10, format HH:MM:SS |
| 3.5 | Traffic stats | Browse via proxy | Upload/download totals + rates update every ~2s |
| 3.6 | Recent connections | Browse a few sites | Up to 5 recent connections show host + chain |
| 3.7 | Disconnect | Tap Disconnect (red outlined button) | Status → "Offline", details disappear, centered message |
| 3.8 | Reconnect | Disconnect → Connect again | No permission prompt second time, reconnects cleanly |
| 3.9 | Hot switch node | Connected → Nodes → select different node | VPN stays up, node switches (ACTION_SWITCH, no disconnect flash) |
| 3.10 | Notification | Connect VPN | Foreground notification "Sing is active" in status bar |
| 3.11 | Auto-connect | Enable in Config → kill app → reopen | VPN starts automatically on launch |
| 3.12 | Disconnected state UI | Not connected | Centered "Not connected", selected node name, purple Connect button |
4. Proxy Functionality
Requires: A valid proxy node configured and selected.
| # | Test | Steps | Expected |
|---|---|---|---|
| 4.1 | HTTP browsing | Browser → http://www.baidu.com | Page loads (direct) |
| 4.2 | HTTPS browsing | Browser → https://www.google.com | Page loads (via proxy) |
| 4.3 | DNS resolution | Browser → google.com | DNS resolves, no NXDOMAIN |
| 4.4 | CN direct routing | Browse www.baidu.com → check Connections | chain = "direct" |
| 4.5 | Foreign proxy routing | Browse youtube.com → check Connections | chain = "proxy" |
| 4.6 | Ads block | Enable Block Ads → visit ad-heavy site | Ad domains show "block" chain in Connections |
| 4.7 | Global mode | Config → set Global proxy → browse baidu.com | chain = "proxy" (not direct) |
| 4.8 | Direct mode | Config → set Direct → browse google.com | chain = "direct" |
| 4.9 | Custom proxy keyword | Add "openai" to proxy domains → browse openai.com | Routes via proxy |
| 4.10 | Custom direct keyword | Add "google" to direct domains → browse google.com | Routes direct |
| 4.11 | Custom block keyword | Add "ads" to block domains | Matching domains blocked |
5. Config Persistence
| # | Test | Steps | Expected |
|---|---|---|---|
| 5.1 | Routing mode | Set "Global proxy" → navigate to Nodes → back | Still "Global proxy" |
| 5.2 | Routing mode (restart) | Set "Global proxy" → kill app → reopen | Still "Global proxy" |
| 5.3 | DNS servers | Change remote DNS to 1.1.1.1 → restart | Still 1.1.1.1 |
| 5.4 | Toggle persistence | Enable Auto-connect → restart | Toggle still on |
| 5.5 | Custom rules | Add "openai" to proxy domains → back → reopen rules | Text persisted |
| 5.6 | Tailscale auth key | Enter auth key → navigate away → return | Key still there |
| 5.7 | Process death | Change settings → swipe app from recents → reopen | All settings intact |
| 5.8 | Auto-connect + boot | Enable auto-connect → reboot device | VPN starts after boot (BootReceiver) |
6. Connections Monitor
| # | Test | Steps | Expected |
|---|---|---|---|
| 6.1 | Active list | Connect → browse → Connections | List with host, chain, rule, traffic per connection |
| 6.2 | Stats bar | Open Connections while connected | "N active", total upload/download |
| 6.3 | Real-time update | Keep Connections open, browse more | New connections appear, traffic values change |
| 6.4 | Not connected | Disconnect → open Connections | "VPN not connected" centered text |
| 6.5 | Traffic reset | Disconnect | Overview traffic resets to zero |
7. Logs
| # | Test | Steps | Expected |
|---|---|---|---|
| 7.1 | Log streaming | Connect VPN → Config → Logs | Monospace log text appears, auto-scrolls to bottom |
| 7.2 | Horizontal scroll | Long log lines | Can scroll horizontally to see full lines |
| 7.3 | Clear logs | Tap Clear button | Text resets to "No logs yet" |
| 7.4 | Logs after clear | Clear → browse a site | New logs appear from cleared offset |
| 7.5 | File truncation | Restart VPN (causes log reset) | Logs handle gracefully, no duplicate content |
| 7.6 | Long session | Leave logs open 5 min | Buffer capped at 50K chars, no OOM |
| 7.7 | No log file | Open logs before first VPN connect | Shows "No logs yet", no crash |
8. Edge Cases
| # | Test | Steps | Expected |
|---|---|---|---|
| 8.1 | No node selected | Delete all nodes → tap Connect | Graceful failure, VPN doesn't start, error logged |
| 8.2 | Invalid node | Add node with wrong host/port → Connect | Error in logs, VPN stops, app doesn't crash |
| 8.3 | Screen rotation | Rotate device while connected | State preserved, UI re-renders correctly |
| 8.4 | Background resume | Connect → press home → wait 5min → reopen | Still connected, stats resume polling |
| 8.5 | Kill process | Connect → force stop → reopen | App reconnects to VPN state (isRunning check) |
| 8.6 | Empty import | Import with blank text → tap Import | No crash, no action (button returns early) |
| 8.7 | Double tap connect | Tap Connect rapidly twice | Only one VPN session starts (isRunning guard) |
| 8.8 | Clash API unavailable | Connect before Clash API ready | Traffic stats silently fail, show "--" |
| 8.9 | Helper process dead | Speed test when helper not running | Auto-restarts helper, retries up to 5 times |
| 8.10 | 100+ nodes | Import large subscription | List scrolls smoothly (LazyColumn), no jank |
9. Performance
| # | Test | Steps | Expected |
|---|---|---|---|
| 9.1 | Cold start | Launch from scratch | Splash → Overview < 2s |
| 9.2 | Tab switching | Tap between tabs | Instant, no visible delay |
| 9.3 | Scroll smoothness | 50+ nodes in list, scroll | 60fps, no dropped frames |
| 9.4 | Traffic polling | Connected 10 min, monitor CPU | No ANR, no battery drain spike from 2s polling |
| 9.5 | Log polling | Logs open 5 min | No ANR from 1s poll + 50KB buffer |
| 9.6 | Connections list | 100+ active connections | LazyColumn renders smoothly |
| 9.7 | Memory stability | Use app 30 min (profiler) | No memory leak from polling coroutines |
Automated Tests
# Run existing UI automation tests
./gradlew connectedAndroidTest
# Run from command line (after emulator boot)
adb shell am instrument -w com.sing.vpn.test/androidx.test.runner.AndroidJUnitRunner
Priority for Automation
- Node import (URI parsing, subscription fetch) — pure logic, unit testable
- Config persistence — write prefs → kill → read prefs
- VPN connect/disconnect lifecycle — UiAutomator
- Navigation flow — Compose testing framework
- Edge cases (empty state, invalid node) — Compose testing
Quick Smoke Test (5 minutes)
Run these 10 steps after every build:
- Install & launch — splash → Overview, dark theme, bottom nav with 3 tabs
- Import node — Nodes → "+" → Paste URI → paste a
vless://URI → verify it appears in list - Select node — tap the imported node → green dot activates
- Connect — Overview → Connect → approve VPN → verify "Online", uptime ticking
- Check traffic — open browser, load a page → verify upload/download stats updating
- Check connections — "View all →" → verify connections list populating
- Check logs — Config → Logs → verify log output streaming in monospace
- Change setting — Config → toggle Block Ads → go to Nodes → back to Config → toggle still on
- Disconnect — Overview → Disconnect → verify "Offline" state, traffic resets to zero
- Kill & reopen — swipe app from recents → reopen → verify node selected, settings intact