Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ Test workspace for [FlutterProbe](https://github.com/AlphaWaveSystems/flutter-pr
## Structure

```
mobile/ Flutter test app (iOS + Android) — 11 screens, 78 E2E tests
mobile/ Flutter test app (iOS + Android) — 11 screens, 82 E2E tests
```

## Mobile App

Purpose-built Flutter app for exercising every FlutterProbe framework feature:

- **11 screens**: Home, Login, **Biometric Login (new in v0.9.7)**, Dashboard, Settings, Items, Gestures, API, Device, Visual, Dynamic
- **78 test files** covering: navigation, forms, gestures, HTTP mocking, visual regression, hooks, data-driven tests, and **Face ID / Touch ID / fingerprint flows (new)**
- **82 tests** covering: navigation, forms, gestures, HTTP mocking, visual regression, hooks, data-driven tests, **Face ID / Touch ID / fingerprint flows**, and **native UI-automation verbs (`tap native` / `see native` / `type native`)**
- **Clean Architecture**: domain/data/presentation layers with Provider + get_it

### Biometric auth tests (v0.9.7)
Expand All @@ -28,6 +28,20 @@ The `BiometricLoginPage` exercises the new `enroll biometric` / `biometric match
- Android: emulator with fingerprint ID `1` pre-enrolled in Settings → Security
- Physical devices skip these steps with a warning (`set location`-style behavior)

### Native UI-automation tests

Every keyed widget carries both `ValueKey('name')` (Flutter-side `#name` selector) and
`Semantics(identifier: 'name')`, which Flutter maps to Android's `resource-id` /
iOS `accessibilityIdentifier` — so the native verb family resolves the same widgets
as the Dart-agent selectors. The `mobile/tests/native/` suite (Android-only, uses
uiautomator) runs against `mobile/tests/probe.android.yaml`, which carries the
Android package id (`com.alphawavesystems.probe_test_app`):

```bash
cd mobile
probe test tests/native/ --config tests/probe.android.yaml --device emulator-5554 --reconnect-delay 5s -v -y
```

## Running Tests

```bash
Expand Down
69 changes: 45 additions & 24 deletions mobile/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,44 +102,62 @@ class _ApiPageState extends State<_ApiPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
key: const ValueKey('fetch_users_button'),
onPressed: _loading ? null : _fetchUsers,
child: const Text('Fetch Users'),
Semantics(
identifier: 'fetch_users_button',
child: ElevatedButton(
key: const ValueKey('fetch_users_button'),
onPressed: _loading ? null : _fetchUsers,
child: const Text('Fetch Users'),
),
),
const SizedBox(height: 8),
ElevatedButton(
key: const ValueKey('create_post_button'),
onPressed: _loading ? null : _createPost,
child: const Text('Create Post'),
Semantics(
identifier: 'create_post_button',
child: ElevatedButton(
key: const ValueKey('create_post_button'),
onPressed: _loading ? null : _createPost,
child: const Text('Create Post'),
),
),
const SizedBox(height: 16),
if (_loading)
const Center(
child: CircularProgressIndicator(
key: ValueKey('api_loading'),
Center(
child: Semantics(
identifier: 'api_loading',
child: const CircularProgressIndicator(
key: ValueKey('api_loading'),
),
),
),
if (_error != null)
Text(
_error!,
key: const ValueKey('api_error'),
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 16),
Semantics(
identifier: 'api_error',
child: Text(
_error!,
key: const ValueKey('api_error'),
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 16),
),
),
if (_users.isNotEmpty) ...[
const Text('Users:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
...List.generate(_users.length, (i) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(_users[i], key: ValueKey('user_$i')),
child: Semantics(
identifier: 'user_$i',
child: Text(_users[i], key: ValueKey('user_$i')),
),
)),
],
if (_postResult != null)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
_postResult!,
key: const ValueKey('post_result'),
style: const TextStyle(fontSize: 16, color: Colors.green),
child: Semantics(
identifier: 'post_result',
child: Text(
_postResult!,
key: const ValueKey('post_result'),
style: const TextStyle(fontSize: 16, color: Colors.green),
),
),
),
],
Expand All @@ -160,10 +178,13 @@ class _PlaceholderPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text(
'$title — Coming Soon',
key: ValueKey('placeholder_$title'),
style: Theme.of(context).textTheme.headlineMedium,
child: Semantics(
identifier: 'placeholder_$title',
child: Text(
'$title — Coming Soon',
key: ValueKey('placeholder_$title'),
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,42 +84,54 @@ class _BiometricLoginPageState extends State<BiometricLoginPage> {
const SizedBox(height: 48),
const Icon(Icons.face_retouching_natural, size: 96),
const SizedBox(height: 16),
const Text(
'Use your face or fingerprint to sign in.',
key: ValueKey('biometric_prompt_intro'),
textAlign: TextAlign.center,
Semantics(
identifier: 'biometric_prompt_intro',
child: const Text(
'Use your face or fingerprint to sign in.',
key: ValueKey('biometric_prompt_intro'),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
key: const ValueKey('sign_in_with_face_id'),
onPressed: _busy ? null : _signInWithBiometrics,
icon: const Icon(Icons.face),
label: const Text('Sign in with Face ID'),
Semantics(
identifier: 'sign_in_with_face_id',
child: ElevatedButton.icon(
key: const ValueKey('sign_in_with_face_id'),
onPressed: _busy ? null : _signInWithBiometrics,
icon: const Icon(Icons.face),
label: const Text('Sign in with Face ID'),
),
),
if (_busy) ...[
const SizedBox(height: 24),
const Center(
Center(
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(
key: ValueKey('biometric_progress'),
padding: const EdgeInsets.all(8),
child: Semantics(
identifier: 'biometric_progress',
child: const CircularProgressIndicator(
key: ValueKey('biometric_progress'),
),
),
),
),
],
if (_error != null) ...[
const SizedBox(height: 24),
Container(
key: const ValueKey('biometric_error_banner'),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
border: Border.all(color: Colors.red.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_error!,
style: TextStyle(color: Colors.red.shade800),
Semantics(
identifier: 'biometric_error_banner',
child: Container(
key: const ValueKey('biometric_error_banner'),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
border: Border.all(color: Colors.red.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_error!,
style: TextStyle(color: Colors.red.shade800),
),
),
),
],
Expand Down
Loading
Loading