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

## [Unreleased]

## [0.9.9] - 2026-05-13

### Added
- **`deliver signal "name" ["value"]`** — new ProbeScript step that resolves
a pending `awaitSignal(name)` call in the Flutter app. Use to unblock any
OS-level interaction that isn't in the Flutter widget tree: push permission
dialogs, payment sheets, App Tracking Transparency, deep-link handlers, etc.
The value defaults to `"true"` when omitted.
- **`awaitSignal(String name)`** — new public function exported from
`flutter_probe_agent`. Returns a `Future<String>` that resolves with the
value sent by the CLI. Generalises the `awaitBiometricResult()` pattern to
any named signal.
- **`DeliverSignal(String name, {String value})`** — new annotation step class
in `flutter_probe_annotation`. Emits `deliver signal "name"` or
`deliver signal "name" "value"`.
- Parser: `TOKEN_DELIVER`, `TOKEN_SIGNAL`, `VerbDeliverSignal`.
- Agent: `probe.signal` JSON-RPC method, `ProbeMethods.signal` constant.

## [0.9.8] - 2026-05-12

### Fixed
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.8**.
FlutterProbe is in active development. Current version: **0.9.9**.

### Repository Structure

Expand Down
5 changes: 5 additions & 0 deletions internal/parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ const (
VerbEnrollBiometric ActionVerb = "enroll_biometric"
VerbBiometricMatch ActionVerb = "biometric_match"
VerbBiometricNoMatch ActionVerb = "biometric_no_match"

// Native-prompt signal API (v0.9.9+)
// deliver signal "name" ["value"]
// Name = signal name, Text = value (default "true")
VerbDeliverSignal ActionVerb = "deliver_signal"
)

type SwipeDirection string
Expand Down
44 changes: 43 additions & 1 deletion internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ func (p *Parser) parseStep() (Step, error) {
return p.parseBiometric()
case TOKEN_ENROLL:
return p.parseEnrollBiometric()
case TOKEN_DELIVER:
return p.parseDeliverSignal()
case TOKEN_NEWLINE:
p.advance()
return nil, nil
Expand Down Expand Up @@ -821,13 +823,27 @@ func (p *Parser) parseWait() (Step, error) {
}
}

// "wait [for] animations to end" — TOKEN_FOR_KW is a filler so skipFillers()
// above may have already consumed it; check TOKEN_ANIMATIONS directly first.
if tok.Type == TOKEN_ANIMATIONS {
p.advance()
// consume trailing words ("to end", "to finish") up to the newline
for p.peek().Type != TOKEN_NEWLINE && p.peek().Type != TOKEN_EOF {
p.advance()
}
p.consumeNewline()
return WaitStep{Kind: WaitAnimations, Line: line}, nil
}

// "wait for animations to end" / "wait for the page to load"
if tok.Type == TOKEN_FOR_KW || p.peekLiteral("for") {
p.advance()
p.skipFillers()
if p.peek().Type == TOKEN_ANIMATIONS {
p.advance()
p.skipFillers() // "to end" / "to finish"
for p.peek().Type != TOKEN_NEWLINE && p.peek().Type != TOKEN_EOF {
p.advance()
}
p.consumeNewline()
return WaitStep{Kind: WaitAnimations, Line: line}, nil
}
Expand Down Expand Up @@ -1574,6 +1590,32 @@ func (p *Parser) parseStore() (Step, error) {
return ActionStep{Verb: VerbStore, Text: value, Name: varName, Line: line}, nil
}

// parseDeliverSignal parses:
//
// deliver signal "name"
// deliver signal "name" "value"
//
// Resolves a pending awaitSignal(name) call in the Flutter app.
// If value is omitted it defaults to "true".
func (p *Parser) parseDeliverSignal() (Step, error) {
line := p.peek().Line
p.advance() // deliver
p.skipFillers()
if p.peek().Type != TOKEN_SIGNAL {
return nil, fmt.Errorf("line %d: expected 'signal' after 'deliver'", line)
}
p.advance() // signal
p.skipFillers()
name := p.expectString("signal name")
p.skipFillers()
value := "true"
if p.peek().Type == TOKEN_STRING {
value = p.advance().Literal
}
p.consumeNewline()
return ActionStep{Verb: VerbDeliverSignal, Name: name, Text: value, Line: line}, nil
}

// parseBiometric parses one of:
//
// biometric match
Expand Down
39 changes: 39 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,42 @@ func TestBiometric_NoMatch(t *testing.T) {
t.Errorf("verb: got %q, want %q", a.Verb, parser.VerbBiometricNoMatch)
}
}

func TestDeliverSignal(t *testing.T) {
prog := mustParse(t, `
test "signal"
deliver signal "payment_confirmed"
deliver signal "push_token" "abc123"
`)
steps := prog.Tests[0].Body
if len(steps) != 2 {
t.Fatalf("step count: got %d, want 2", len(steps))
}

// default value "true"
a0, ok := steps[0].(parser.ActionStep)
if !ok {
t.Fatalf("step 0: got %T", steps[0])
}
if a0.Verb != parser.VerbDeliverSignal {
t.Errorf("verb: got %q, want %q", a0.Verb, parser.VerbDeliverSignal)
}
if a0.Name != "payment_confirmed" {
t.Errorf("name: got %q, want %q", a0.Name, "payment_confirmed")
}
if a0.Text != "true" {
t.Errorf("value: got %q, want %q", a0.Text, "true")
}

// explicit value
a1, ok := steps[1].(parser.ActionStep)
if !ok {
t.Fatalf("step 1: got %T", steps[1])
}
if a1.Name != "push_token" {
t.Errorf("name: got %q, want %q", a1.Name, "push_token")
}
if a1.Text != "abc123" {
t.Errorf("value: got %q, want %q", a1.Text, "abc123")
}
}
6 changes: 6 additions & 0 deletions internal/parser/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ const (
// 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`

// Native-prompt signal API (v0.9.9+)
TOKEN_DELIVER // "deliver" — head of `deliver signal "name"`
TOKEN_SIGNAL // "signal" — second word of deliver signal
)

// Token is a single lexical unit.
Expand Down Expand Up @@ -299,6 +303,8 @@ var keywords = map[string]TokenType{
"sync": TOKEN_SYNC,
"biometric": TOKEN_BIOMETRIC,
"enroll": TOKEN_ENROLL,
"deliver": TOKEN_DELIVER,
"signal": TOKEN_SIGNAL,
}

// fillerWords are stripped by the forgiving parser.
Expand Down
5 changes: 5 additions & 0 deletions internal/probelink/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ const (
// Dart Completer resolves without depending on LAContext behaviour.
MethodBiometricSignal = "probe.biometric_signal"

// Native-prompt signal API (v0.9.9)
// Sent by `deliver signal "name" ["value"]` — resolves a pending
// awaitSignal(name) call in the Flutter app.
MethodSignal = "probe.signal"

// Notification methods (agent → CLI, no response expected)
NotifyRecordedEvent = "probe.recorded_event"
NotifyExecDart = "probe.exec_dart"
Expand Down
15 changes: 15 additions & 0 deletions internal/runner/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ func (e *Executor) stepDescription(step parser.Step) string {
return "biometric match"
case parser.VerbBiometricNoMatch:
return "biometric no match"
case parser.VerbDeliverSignal:
return fmt.Sprintf("deliver signal %q", s.Name)
default:
return string(s.Verb)
}
Expand All @@ -367,6 +369,8 @@ func (e *Executor) stepDescription(step parser.Step) string {
return "wait for page to load"
case parser.WaitNetworkIdle:
return "wait for network idle"
case parser.WaitAnimations:
return "wait for animations to end"
default:
return "wait"
}
Expand Down Expand Up @@ -682,6 +686,17 @@ func (e *Executor) runAction(ctx context.Context, a parser.ActionStep) error {
}
_, err := e.client.Call(ctx, probelink.MethodBiometricSignal, map[string]any{"result": false})
return err

case parser.VerbDeliverSignal:
value := a.Text
if value == "" {
value = "true"
}
_, err := e.client.Call(ctx, probelink.MethodSignal, map[string]any{
"name": a.Name,
"value": value,
})
return err
}

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

## 0.9.9 - 2026-05-13

- **`awaitSignal(String name)`** — new public function. Blocks until the CLI
delivers `deliver signal "name"`. Returns the value string sent with the
step (default `"true"`). Use to unblock any OS-level interaction not in
the Flutter widget tree: push permission prompts, payment sheets, App
Tracking Transparency, custom deep-link handlers, etc.
- New `probe.signal` JSON-RPC method handled by `ProbeExecutor`.

## 0.9.8 - 2026-05-12

- **`awaitBiometricResult()`** — new public function exported from
Expand Down
1 change: 1 addition & 0 deletions probe_agent/lib/flutter_probe_agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ library flutter_probe_agent;
// Public API — only what app developers need
export 'src/agent.dart' show ProbeAgent, isProbeEnabled;
export 'src/biometric.dart' show awaitBiometricResult;
export 'src/signal.dart' show awaitSignal;
7 changes: 7 additions & 0 deletions probe_agent/lib/src/executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'biometric.dart' as biometric;
import 'finder.dart';
import 'protocol.dart';
import 'recorder.dart';
import 'signal.dart' as signal_lib;
import 'sync.dart';

typedef SendFn = void Function(String message);
Expand Down Expand Up @@ -269,6 +270,12 @@ class ProbeExecutor {
biometric.completeBiometricResult(result);
return {};

case ProbeMethods.signal:
final name = (req.params['name'] as String?) ?? '';
final value = (req.params['value'] as String?) ?? 'true';
signal_lib.deliverSignal(name, value);
return {};

default:
throw ProbeError(ProbeError.methodNotFound, 'Unknown method: ${req.method}');
}
Expand Down
3 changes: 3 additions & 0 deletions probe_agent/lib/src/protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class ProbeMethods {
// Biometric simulation signal (v0.9.8)
static const biometricSignal = 'probe.biometric_signal';

// Native-prompt signal API (v0.9.9)
static const signal = 'probe.signal';

// Notification methods (agent → CLI)
static const notifyRecordedEvent = 'probe.recorded_event';
static const notifyExecDart = 'probe.exec_dart';
Expand Down
38 changes: 38 additions & 0 deletions probe_agent/lib/src/signal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:async';

// name → pending Completer
final _pending = <String, Completer<String>>{};

/// Waits for the CLI to deliver a named signal via `deliver signal "name"`.
///
/// Returns the value sent with the step (defaults to `"true"` when no value
/// is specified in the ProbeScript). Use this to block on any OS-level
/// interaction the probe can't tap directly — system permission dialogs,
/// payment sheets, push notification prompts, etc.:
///
/// ```dart
/// import 'package:flutter_probe_agent/flutter_probe_agent.dart';
///
/// // In your widget:
/// if (const bool.fromEnvironment('PROBE_AGENT')) {
/// await awaitSignal('push_permission_granted');
/// } else {
/// await requestNotificationPermission();
/// }
/// ```
///
/// ProbeScript side:
/// ```
/// allow permission "notifications"
/// deliver signal "push_permission_granted"
/// ```
Future<String> awaitSignal(String name) {
final completer = Completer<String>();
_pending[name] = completer;
return completer.future;
}

/// Called by [ProbeExecutor] when [probe.signal] arrives from the CLI.
void deliverSignal(String name, String value) {
_pending.remove(name)?.complete(value);
}
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.8
version: 0.9.9
homepage: https://flutterprobe.dev
repository: https://github.com/AlphaWaveSystems/flutter-probe
issue_tracker: https://github.com/AlphaWaveSystems/flutter-probe/issues
Expand Down
24 changes: 24 additions & 0 deletions probe_annotation/lib/src/steps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,30 @@ class BiometricNoMatch extends Step {
const BiometricNoMatch();
}

// ---- Native-prompt signal API ----

/// Emits: `deliver signal "name"` or `deliver signal "name" "value"`.
///
/// Resolves a pending [awaitSignal] call in the Flutter app. Use to unblock
/// any OS-level interaction the probe cannot tap directly — permission dialogs
/// not in the widget tree, payment sheets, push notification prompts, etc.
///
/// ```dart
/// @ProbeSuite(tests: [
/// ProbeTest('push permission granted', steps: [
/// Open(),
/// DeliverSignal('push_permission'),
/// See('Notifications enabled'),
/// ]),
/// ])
/// class NotificationScreen extends StatelessWidget {}
/// ```
class DeliverSignal extends Step {
final String name;
final String value;
const DeliverSignal(this.name, {this.value = 'true'});
}

// ---- Diagnostics ----

/// Emits: `take screenshot "name"`.
Expand Down
2 changes: 1 addition & 1 deletion probe_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: >-
Dart annotations for FlutterProbe — declare ProbeScript end-to-end tests as
decorators on your Flutter screen classes. Pair with flutter_probe_gen to
generate .probe test files at build time.
version: 0.9.7
version: 0.9.9
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_gen/lib/src/probe_emitter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,15 @@ class ProbeEmitter {
case 'BiometricNoMatch':
_buf.writeln('${indent}biometric no match');
break;
case 'DeliverSignal':
final sigName = _str(step, 'name') ?? '';
final sigValue = _str(step, 'value') ?? 'true';
if (sigValue == 'true') {
_buf.writeln('${indent}deliver signal "${_escape(sigName)}"');
} else {
_buf.writeln('${indent}deliver signal "${_escape(sigName)}" "${_escape(sigValue)}"');
}
break;

// Diagnostics
case 'TakeScreenshot':
Expand Down
Loading
Loading