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
132 changes: 132 additions & 0 deletions apps/mobile/integration_test/demo_walkthrough_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intentguard/bootstrap.dart';
import 'package:intentguard/core/storage/token_storage.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

Future<void> snap(String name) async {
await binding.takeScreenshot(name);
}

Future<void> settle(WidgetTester tester, {int pumps = 12}) async {
for (var i = 0; i < pumps; i++) {
await tester.pump(const Duration(milliseconds: 350));
}
}

testWidgets(
'intentguard demo walkthrough',
(tester) async {
await GetIt.instance.reset();
await bootstrapApp(
storage: MemoryTokenStorage(),
apiBase: const String.fromEnvironment(
'API_BASE',
defaultValue: 'http://127.0.0.1:8080',
),
);
await settle(tester, pumps: 24);
await snap('01-login');

await tester.tap(find.text('Sign in'));
await settle(tester, pumps: 28);
await snap('02-home');

await tester.tap(find.text('Balances'));
await settle(tester, pumps: 18);
await snap('03-balances');

final back = find.byTooltip('Back');
if (back.evaluate().isNotEmpty) {
await tester.tap(back);
await settle(tester, pumps: 12);
}

await tester.tap(find.text('New intent'));
await settle(tester, pumps: 16);
await snap('04-composer');

// Prefer short fixture chip for mock planner.
final chip = find.text('swap 10 USDC');
if (chip.evaluate().isNotEmpty) {
await tester.ensureVisible(chip.first);
await tester.tap(chip.first);
await settle(tester, pumps: 8);
}
await snap('05-composer-ready');

await tester.tap(find.text('Create plan'));
// Wait for navigation to plan review.
var onReview = false;
for (var i = 0; i < 40 && !onReview; i++) {
await tester.pump(const Duration(milliseconds: 500));
if (find.text('Plan review').evaluate().isNotEmpty) {
onReview = true;
}
}
expect(onReview, isTrue, reason: 'did not reach plan review');
await settle(tester, pumps: 10);
await snap('06-plan-review');

// Approve each step in order.
for (var step = 0; step < 4; step++) {
final approve = find.text('Approve');
if (approve.evaluate().isEmpty) {
break;
}
await tester.ensureVisible(approve.first);
await tester.tap(approve.first);
await settle(tester, pumps: 20);
await snap('07-approve-step-${step + 1}');
if (find.textContaining('completed').evaluate().isNotEmpty ||
find.textContaining('Completed').evaluate().isNotEmpty) {
break;
}
}
await settle(tester, pumps: 12);
await snap('08-plan-completed');

// History
if (back.evaluate().isNotEmpty) {
await tester.tap(find.byTooltip('Back').first);
await settle(tester, pumps: 12);
}
if (find.text('History').evaluate().isNotEmpty) {
await tester.tap(find.text('History').first);
await settle(tester, pumps: 16);
await snap('09-history');
}

// Reject path: compose policy reject
if (find.byTooltip('Back').evaluate().isNotEmpty) {
await tester.tap(find.byTooltip('Back').first);
await settle(tester, pumps: 10);
}
if (find.text('New intent').evaluate().isNotEmpty) {
await tester.tap(find.text('New intent'));
await settle(tester, pumps: 12);
final rejectChip = find.text('swap 150 USDC');
if (rejectChip.evaluate().isNotEmpty) {
await tester.ensureVisible(rejectChip.first);
await tester.tap(rejectChip.first);
await settle(tester, pumps: 6);
}
await tester.tap(find.text('Create plan'));
for (var i = 0; i < 30; i++) {
await tester.pump(const Duration(milliseconds: 400));
if (find.textContaining('policy').evaluate().isNotEmpty ||
find.textContaining('Rejected').evaluate().isNotEmpty ||
find.text('Plan review').evaluate().isNotEmpty) {
break;
}
}
await settle(tester, pumps: 10);
await snap('10-policy-reject');
}
},
timeout: const Timeout(Duration(minutes: 4)),
);
}
14 changes: 14 additions & 0 deletions apps/mobile/lib/bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';

import 'app.dart';
import 'core/di/injection.dart';
import 'core/storage/token_storage.dart';

Future<void> bootstrapApp({
TokenStorage? storage,
String? apiBase,
}) async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies(storage: storage, apiBase: apiBase);
runApp(const IntentGuardApp());
}
10 changes: 9 additions & 1 deletion apps/mobile/lib/core/di/injection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ import '../storage/token_storage.dart';

final getIt = GetIt.instance;

Future<void> configureDependencies({TokenStorage? storage}) async {
Future<void> configureDependencies({
TokenStorage? storage,
String? apiBase,
}) async {
if (getIt.isRegistered<AuthCubit>()) {
return;
}

final tokenStorage = storage ?? SecureTokenStorage();
getIt.registerSingleton<TokenStorage>(tokenStorage);

late final AuthCubit authCubit;
final api = ApiClient(
storage: tokenStorage,
baseUrl: apiBase,
onSessionExpired: () => authCubit.sessionExpired(),
);
getIt.registerSingleton<ApiClient>(api);
Expand Down
11 changes: 2 additions & 9 deletions apps/mobile/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
import 'package:flutter/material.dart';
import 'bootstrap.dart';

import 'app.dart';
import 'core/di/injection.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
runApp(const IntentGuardApp());
}
Future<void> main() => bootstrapApp();
39 changes: 39 additions & 0 deletions apps/mobile/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.1.1"
flutter_driver:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -264,6 +269,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
description:
Expand Down Expand Up @@ -328,6 +338,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
io:
dependency: transitive
description:
Expand Down Expand Up @@ -552,6 +567,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
source: hosted
version: "5.0.5"
provider:
dependency: transitive
description:
Expand Down Expand Up @@ -661,6 +684,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
Expand Down Expand Up @@ -749,6 +780,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
webkit_inspection_protocol:
dependency: transitive
description:
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ dev_dependencies:
flutter_lints: ^5.0.0
bloc_test: ^10.0.0
mocktail: ^1.0.4
integration_test:
sdk: flutter
flutter_driver:
sdk: flutter

flutter:
uses-material-design: true
20 changes: 20 additions & 0 deletions apps/mobile/test_driver/integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:io';

import 'package:flutter_driver/flutter_driver.dart';
import 'package:integration_test/integration_test_driver_extended.dart';

Future<void> main() async {
final driver = await FlutterDriver.connect();
await integrationDriver(
driver: driver,
onScreenshot: (String name, List<int> bytes, [Map<String, Object?>? args]) async {
final file = File(
'/Users/omid/code/intentguard/demos/screenshots/$name.png',
);
await file.parent.create(recursive: true);
await file.writeAsBytes(bytes);
stdout.writeln('Wrote screenshot ${file.path}');
return true;
},
);
}
16 changes: 16 additions & 0 deletions demos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Demo captures

## Latest

- Recording: [`recordings/latest.mp4`](recordings/latest.mp4)
- Screenshots: [`recordings/latest-screenshots/`](recordings/latest-screenshots)

## Re-record

```bash
cd deploy && docker compose up --build -d
cd .. && ./scripts/deploy-anvil.sh && ./scripts/seed-anvil.sh
./scripts/record_demo_walkthrough.sh
```

Requires iOS Simulator + API on `http://127.0.0.1:8080`.
Binary file added demos/intentguard-demo-walkthrough.mp4
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions demos/recordings/latest-screenshots
1 change: 1 addition & 0 deletions demos/recordings/latest.mp4
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/01-login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/02-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/03-balances.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/04-composer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/05-composer-ready.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/06-plan-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/07-approve-step-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/07-approve-step-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/08-plan-completed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/screenshots/10-policy-reject.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions scripts/record_demo_walkthrough.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MOBILE="$ROOT/apps/mobile"
OUT="$ROOT/demos"
SHOTS="$OUT/screenshots"
RECORDINGS="$OUT/recordings"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
VIDEO="$OUT/intentguard-demo-walkthrough.mp4"
SAVED_VIDEO="$RECORDINGS/intentguard-demo-$STAMP.mp4"
SAVED_SHOTS="$RECORDINGS/screenshots-$STAMP"
API_BASE="${API_BASE:-http://127.0.0.1:8080}"
export PATH="$HOME/.foundry/bin:$PATH"

mkdir -p "$SHOTS" "$RECORDINGS"
rm -f "$SHOTS"/*.png
rm -f "$VIDEO"

echo "Checking API at $API_BASE/health"
curl -fsS "$API_BASE/health" >/dev/null

echo "Booting iOS Simulator if needed"
open -a Simulator || true
sleep 3
DEVICE_ID="$(xcrun simctl list devices booted | awk -F '[()]' '/iPhone/{print $2; exit}')"
if [[ -z "${DEVICE_ID:-}" ]]; then
(cd "$MOBILE" && fvm flutter emulators --launch apple_ios_simulator)
sleep 10
DEVICE_ID="$(xcrun simctl list devices booted | awk -F '[()]' '/iPhone/{print $2; exit}')"
fi
echo "Using simulator $DEVICE_ID"

echo "Starting screen recording → $VIDEO"
xcrun simctl io "$DEVICE_ID" recordVideo --codec=h264 "$VIDEO" &
REC_PID=$!
cleanup() {
if kill -0 "$REC_PID" 2>/dev/null; then
kill -INT "$REC_PID" 2>/dev/null || true
wait "$REC_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT

FLUTTER_BIN="${FLUTTER_BIN:-$HOME/fvm/versions/3.38.6/bin/flutter}"
if [[ ! -x "$FLUTTER_BIN" ]]; then
FLUTTER_BIN="$HOME/code/walletops/.fvm/flutter_sdk/bin/flutter"
fi

cd "$MOBILE"
"$FLUTTER_BIN" drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/demo_walkthrough_test.dart \
-d "$DEVICE_ID" \
--dart-define=API_BASE="$API_BASE"

cleanup
trap - EXIT

mkdir -p "$SAVED_SHOTS"
cp -f "$SHOTS"/*.png "$SAVED_SHOTS/" 2>/dev/null || true
cp -f "$VIDEO" "$SAVED_VIDEO"
ln -sfn "$(basename "$SAVED_VIDEO")" "$RECORDINGS/latest.mp4"
ln -sfn "$(basename "$SAVED_SHOTS")" "$RECORDINGS/latest-screenshots"

echo "Done."
echo "Latest screenshots: $SHOTS"
ls -la "$SHOTS" || true
echo "Saved recording: $SAVED_VIDEO"
ls -la "$RECORDINGS" || true
Loading