Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.9.7] - 2026-05-12

### Added
- **Biometric authentication testing** — three new ProbeScript steps that drive Face ID / Touch ID / fingerprint flows on iOS Simulator and Android emulator without real hardware. Skipped on physical devices with a warning (same pattern as `set location` and other simulator-only ops).
- `enroll biometric` — marks the simulator/emulator as having an enrolled face or finger. iOS posts the `com.apple.BiometricKit.enrollmentChanged` Darwin notification via `xcrun simctl spawn booted notifyutil`. Android requires the fingerprint to be pre-enrolled in Settings.
- `biometric match` — simulates a successful capture, satisfying any pending biometric prompt. iOS posts `*_Sim.faceCapture.match` AND `*_Sim.fingerTouch.match` so the same step works on Face ID and Touch ID devices. Android runs `adb -s <serial> emu finger touch 1`.
- `biometric no match` — simulates a failed capture so the app's "authentication failed" path can be tested. iOS posts the `.no-match` variants; Android runs `adb emu finger touch 9999` (an unregistered id).
- **Annotation DSL**: matching `EnrollBiometric()`, `BiometricMatch()`, `BiometricNoMatch()` const Step classes in `flutter_probe_annotation`, with a new `biometric_auth` golden fixture in `flutter_probe_gen/test/fixtures/` that round-trips through the Go parser via the cross-language integration test.
- **Parser**: 2 new tokens (`TOKEN_BIOMETRIC`, `TOKEN_ENROLL`), 3 new `ActionVerb` constants, 2 new parser dispatch cases. 3 new unit tests in `parser_test.go`.
- **Runner**: `EnrollBiometric` / `BiometricMatch` / `BiometricNoMatch` methods on `DeviceContext`, dispatch cases in `Executor.runAction`, and human-readable strings in `stepDescription`.
- **Docs**: new section in [annotations.md](https://flutterprobe.dev/probescript/annotations/#biometric-authentication-v097) and [syntax.md](https://flutterprobe.dev/probescript/syntax/#biometric-authentication) on the website. Per-package CHANGELOGs updated.

## [0.9.6] - 2026-05-12

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ Test definitions are now type-checked by `flutter analyze` — a misspelt step n

**v0.9.6** completes the annotation surface: full composite-test DSL (`@ProbeCompositeTest`, `Device`, `OnDevice`, `Sync`), id/selector-based `See`/`DontSee`, `WaitUntil.idAppears`, and composable `state` + `containing` + `matching` assertions. Plus fixes for two emitter bugs (`Mock` paths and `See` suffix dropping).

**v0.9.7** adds **biometric authentication testing** — `enroll biometric`, `biometric match`, `biometric no match` steps (and matching `EnrollBiometric()` / `BiometricMatch()` / `BiometricNoMatch()` annotation classes) drive Face ID / Touch ID / fingerprint flows on iOS Simulator and Android emulator. Skipped on physical devices.

Full reference: [flutterprobe.dev/probescript/annotations](https://flutterprobe.dev/probescript/annotations/) (or [`docs/wiki/Annotations.md`](docs/wiki/Annotations.md) on GitHub).

## CLI Commands
Expand Down
3 changes: 3 additions & 0 deletions docs/wiki/Annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ constructible.
| `GrantAllPermissions()` / `RevokeAllPermissions()` | `grant all permissions` |
| `CopyToClipboard('x')` / `PasteFromClipboard()` | `copy "x" to clipboard` |
| `SetLocation(lat, lng)` | `set location lat, lng` |
| `EnrollBiometric()` (v0.9.7+) | `enroll biometric` — see [Biometric auth](https://flutterprobe.dev/probescript/annotations/#biometric-authentication-v097) |
| `BiometricMatch()` (v0.9.7+) | `biometric match` — simulate Face ID / Touch ID success |
| `BiometricNoMatch()` (v0.9.7+) | `biometric no match` — simulate Face ID / Touch ID failure |
| `VerifyExternalBrowser()` | `verify external browser opened` |
| `TakeScreenshot('name')` / `CompareScreenshot('name')` | `take screenshot "name"` |
| `DumpWidgetTree()` / `SaveLogs()` / `Pause()` / `Log('msg')` | as named |
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Welcome to the FlutterProbe wiki. This documentation covers architecture details

## Project Status

FlutterProbe is in active development. Current version: **0.9.6**.
FlutterProbe is in active development. Current version: **0.9.7**.

### Repository Structure

Expand Down
4 changes: 4 additions & 0 deletions internal/parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ const (
VerbVerifyBrowser ActionVerb = "verify_browser"
VerbOpenLink ActionVerb = "open_link" // open link "url"
VerbStore ActionVerb = "store" // store "value" as varName
// Biometric (Face ID / Touch ID / fingerprint) — simulator/emulator only.
VerbEnrollBiometric ActionVerb = "enroll_biometric"
VerbBiometricMatch ActionVerb = "biometric_match"
VerbBiometricNoMatch ActionVerb = "biometric_no_match"
)

type SwipeDirection string
Expand Down
49 changes: 49 additions & 0 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ func (p *Parser) parseStep() (Step, error) {
return p.parseHTTPCall()
case TOKEN_STORE:
return p.parseStore()
case TOKEN_BIOMETRIC:
return p.parseBiometric()
case TOKEN_ENROLL:
return p.parseEnrollBiometric()
case TOKEN_NEWLINE:
p.advance()
return nil, nil
Expand Down Expand Up @@ -1569,3 +1573,48 @@ func (p *Parser) parseStore() (Step, error) {
p.consumeNewline()
return ActionStep{Verb: VerbStore, Text: value, Name: varName, Line: line}, nil
}

// parseBiometric parses one of:
//
// biometric match
// biometric no match
//
// Simulator/emulator only — `xcrun simctl spawn booted notifyutil ...`
// on iOS and `adb emu finger touch <id>` on Android.
func (p *Parser) parseBiometric() (Step, error) {
line := p.peek().Line
p.advance() // biometric
p.skipFillers()
verb := VerbBiometricMatch
// "no match" — the lexer emits "no" as TOKEN_IDENT (not a keyword).
if p.peek().Type == TOKEN_IDENT && strings.ToLower(p.peek().Literal) == "no" {
p.advance()
p.skipFillers()
verb = VerbBiometricNoMatch
}
// Match is optional after `no` to allow either `no match` or `no-match`-ish
// patterns. Consume it if present.
if p.peek().Type == TOKEN_IDENT && strings.ToLower(p.peek().Literal) == "match" {
p.advance()
}
p.consumeNewline()
return ActionStep{Verb: verb, Line: line}, nil
}

// parseEnrollBiometric parses:
//
// enroll biometric
//
// Sets the simulator/emulator's biometric enrollment state to "enrolled"
// so subsequent `biometric match` / `biometric no match` operations
// satisfy a pending biometric prompt in the app under test.
func (p *Parser) parseEnrollBiometric() (Step, error) {
line := p.peek().Line
p.advance() // enroll
p.skipFillers()
if p.peek().Type == TOKEN_BIOMETRIC {
p.advance()
}
p.consumeNewline()
return ActionStep{Verb: VerbEnrollBiometric, Line: line}, nil
}
59 changes: 59 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1142,3 +1142,62 @@ func TestComposite_DeviceTargetParsed(t *testing.T) {
t.Errorf("device[1] target: got %q, want %q", ct.Devices[1].Target, "iPad Pro 12.9 Simulator")
}
}

// ---- Biometric action tests ----

func TestBiometric_EnrollBiometric(t *testing.T) {
prog := mustParse(t, `test "auth"
enroll biometric
see "Dashboard"
`)
assertTestCount(t, prog, 1)
body := prog.Tests[0].Body
if len(body) < 1 {
t.Fatal("missing enroll biometric step")
}
a, ok := body[0].(parser.ActionStep)
if !ok {
t.Fatalf("first step: got %T, want ActionStep", body[0])
}
if a.Verb != parser.VerbEnrollBiometric {
t.Errorf("verb: got %q, want %q", a.Verb, parser.VerbEnrollBiometric)
}
}

func TestBiometric_Match(t *testing.T) {
prog := mustParse(t, `test "auth"
tap "Sign in with Face ID"
biometric match
see "Dashboard"
`)
steps := prog.Tests[0].Body
if len(steps) != 3 {
t.Fatalf("step count: got %d, want 3", len(steps))
}
a, ok := steps[1].(parser.ActionStep)
if !ok {
t.Fatalf("biometric step: got %T", steps[1])
}
if a.Verb != parser.VerbBiometricMatch {
t.Errorf("verb: got %q, want %q", a.Verb, parser.VerbBiometricMatch)
}
}

func TestBiometric_NoMatch(t *testing.T) {
prog := mustParse(t, `test "auth fail"
tap "Sign in with Face ID"
biometric no match
see "Authentication failed"
`)
steps := prog.Tests[0].Body
if len(steps) != 3 {
t.Fatalf("step count: got %d, want 3", len(steps))
}
a, ok := steps[1].(parser.ActionStep)
if !ok {
t.Fatalf("biometric step: got %T", steps[1])
}
if a.Verb != parser.VerbBiometricNoMatch {
t.Errorf("verb: got %q, want %q", a.Verb, parser.VerbBiometricNoMatch)
}
}
6 changes: 6 additions & 0 deletions internal/parser/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ const (
// Composite test keywords
TOKEN_COMPOSITE // "composite" — starts a composite test definition
TOKEN_SYNC // "sync" — cross-device barrier step inside composite tests

// Biometric authentication (v0.9.7+) — simulator/emulator only.
TOKEN_BIOMETRIC // "biometric" — head of `biometric match` / `biometric no match`
TOKEN_ENROLL // "enroll" — head of `enroll biometric`
)

// Token is a single lexical unit.
Expand Down Expand Up @@ -293,6 +297,8 @@ var keywords = map[string]TokenType{
"store": TOKEN_STORE,
"composite": TOKEN_COMPOSITE,
"sync": TOKEN_SYNC,
"biometric": TOKEN_BIOMETRIC,
"enroll": TOKEN_ENROLL,
}

// fillerWords are stripped by the forgiving parser.
Expand Down
98 changes: 98 additions & 0 deletions internal/runner/device_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,101 @@ func (dc *DeviceContext) SetLocation(ctx context.Context, lat, lng string) error
}
return nil
}

// EnrollBiometric sets the simulator/emulator's biometric enrollment state
// to "enrolled" so the app under test sees a registered Face ID / Touch ID /
// fingerprint when it requests biometric authentication.
//
// - iOS Simulator: sends the Darwin notification
// `com.apple.BiometricKit.enrollmentChanged` (toggles state).
// - Android emulator: no-op — fingerprints are enrolled in Settings before
// the test runs (typically via a CI bootstrap script).
// - Physical devices: skipped with a warning.
func (dc *DeviceContext) EnrollBiometric(ctx context.Context) error {
if dc.IsPhysical {
fmt.Println(" \033[33m⚠\033[0m enroll biometric is not supported on physical devices — skipping")
return nil
}
fmt.Println(" \033[36m🔐\033[0m Enrolling biometric")
switch dc.Platform {
case device.PlatformIOS:
if _, err := dc.Manager.SimCtl().Spawn(ctx, dc.Serial,
"notifyutil", "-s", "com.apple.BiometricKit.enrollmentChanged", "1"); err != nil {
return fmt.Errorf("enroll biometric: set enrollment flag: %w", err)
}
if _, err := dc.Manager.SimCtl().Spawn(ctx, dc.Serial,
"notifyutil", "-p", "com.apple.BiometricKit.enrollmentChanged"); err != nil {
return fmt.Errorf("enroll biometric: post enrollment notification: %w", err)
}
case device.PlatformAndroid:
// Android fingerprints are enrolled in Settings, not via adb. We
// document the requirement in the .probe error message rather than
// failing here — the user's CI script should pre-enroll.
fmt.Println(" (Android: ensure fingerprint ID 1 is pre-enrolled in Settings)")
}
return nil
}

// BiometricMatch simulates a successful biometric capture, satisfying a
// pending Face ID / Touch ID / fingerprint prompt.
//
// - iOS Simulator: posts `com.apple.BiometricKit_Sim.fingerTouch.match`
// and `.faceCapture.match` so the same step works regardless of the
// simulator's biometric kind.
// - Android emulator: `adb -s <serial> emu finger touch 1` (matches the
// fingerprint enrolled with ID 1).
// - Physical devices: skipped with a warning.
func (dc *DeviceContext) BiometricMatch(ctx context.Context) error {
return dc.biometricCapture(ctx, true)
}

// BiometricNoMatch simulates a failed biometric capture so the app's
// "authentication failed" path can be tested.
//
// - iOS Simulator: posts `*_Sim.fingerTouch.no-match` and `.faceCapture.no-match`.
// - Android emulator: `adb emu finger touch 9999` (an unregistered id).
// - Physical devices: skipped with a warning.
func (dc *DeviceContext) BiometricNoMatch(ctx context.Context) error {
return dc.biometricCapture(ctx, false)
}

func (dc *DeviceContext) biometricCapture(ctx context.Context, match bool) error {
if dc.IsPhysical {
fmt.Println(" \033[33m⚠\033[0m biometric capture is not supported on physical devices — skipping")
return nil
}
verb := "match"
icon := "✓"
if !match {
verb = "no-match"
icon = "✗"
}
fmt.Printf(" \033[36m🔐\033[0m Biometric capture: %s %s\n", icon, verb)
switch dc.Platform {
case device.PlatformIOS:
// Post both fingerprint and face notifications so the same step
// works on Touch ID devices and Face ID devices alike — the simulator
// ignores the one that doesn't match its hardware profile.
notifications := []string{
fmt.Sprintf("com.apple.BiometricKit_Sim.fingerTouch.%s", verb),
fmt.Sprintf("com.apple.BiometricKit_Sim.faceCapture.%s", verb),
}
for _, n := range notifications {
if _, err := dc.Manager.SimCtl().Spawn(ctx, dc.Serial, "notifyutil", "-p", n); err != nil {
return fmt.Errorf("biometric %s: post %s: %w", verb, n, err)
}
}
case device.PlatformAndroid:
// Fingerprint ID 1 is matching by convention; any unregistered ID
// (we use 9999) returns no-match. The user's CI bootstrap script
// enrolls fingerprint ID 1 before tests run.
fingerID := "1"
if !match {
fingerID = "9999"
}
if _, err := dc.Manager.ADB().Run(ctx, dc.Serial, "emu", "finger", "touch", fingerID); err != nil {
return fmt.Errorf("biometric %s: adb emu finger touch %s: %w", verb, fingerID, err)
}
}
return nil
}
27 changes: 27 additions & 0 deletions internal/runner/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,12 @@ func (e *Executor) stepDescription(step parser.Step) string {
return fmt.Sprintf("set location %s", s.Name)
case parser.VerbVerifyBrowser:
return "verify external browser opened"
case parser.VerbEnrollBiometric:
return "enroll biometric"
case parser.VerbBiometricMatch:
return "biometric match"
case parser.VerbBiometricNoMatch:
return "biometric no match"
default:
return string(s.Verb)
}
Expand Down Expand Up @@ -647,6 +653,27 @@ func (e *Executor) runAction(ctx context.Context, a parser.ActionStep) error {
case parser.VerbStore:
e.vars[a.Name] = e.resolve(a.Text)
return nil

case parser.VerbEnrollBiometric:
if e.deviceCtx == nil {
fmt.Println(" \033[33m⚠\033[0m Skipping enroll biometric (cloud mode)")
return nil
}
return e.deviceCtx.EnrollBiometric(ctx)

case parser.VerbBiometricMatch:
if e.deviceCtx == nil {
fmt.Println(" \033[33m⚠\033[0m Skipping biometric match (cloud mode)")
return nil
}
return e.deviceCtx.BiometricMatch(ctx)

case parser.VerbBiometricNoMatch:
if e.deviceCtx == nil {
fmt.Println(" \033[33m⚠\033[0m Skipping biometric no-match (cloud mode)")
return nil
}
return e.deviceCtx.BiometricNoMatch(ctx)
}

return fmt.Errorf("unknown action verb %q at line %d", a.Verb, a.Line)
Expand Down
6 changes: 6 additions & 0 deletions probe_agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.9.7 - 2026-05-12

- Version bump to match CLI v0.9.7. No agent code changes — biometric
authentication is driven via simctl/adb from the CLI, no on-device
agent involvement needed.

## 0.9.6 - 2026-05-12

- Version bump to match CLI v0.9.6. No agent code changes — annotation DSL
Expand Down
2 changes: 1 addition & 1 deletion probe_agent/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: >-
On-device E2E test agent for FlutterProbe. Embeds in your Flutter app and
executes test commands via direct widget-tree access with sub-50ms latency.

version: 0.9.6
version: 0.9.7
homepage: https://flutterprobe.dev
repository: https://github.com/AlphaWaveSystems/flutter-probe
issue_tracker: https://github.com/AlphaWaveSystems/flutter-probe/issues
Expand Down
9 changes: 9 additions & 0 deletions probe_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.9.7 - 2026-05-12

### Added

- **`EnrollBiometric`, `BiometricMatch`, `BiometricNoMatch` Step classes** —
drive Face ID / Touch ID / fingerprint flows on iOS Simulator and
Android emulator from annotation-authored tests. Skipped on physical
devices with a warning. See https://flutterprobe.dev/probescript/annotations/#biometric-authentication-v097.

## 0.9.6 - 2026-05-12

### Added
Expand Down
Loading
Loading