|
| 1 | +// Full-stack control-plane e2e for the macOS desktop control app (SPEC-03). |
| 2 | +// |
| 3 | +// Counterpart to the mobile stub suite (integration_test/stub/): instead of the |
| 4 | +// WS client, this drives the real desktop control screens against a real daemon |
| 5 | +// control socket served by `server/test/e2e-control-server.ts`. The whole stack |
| 6 | +// is genuine — `MakitControlClient` speaks the real NDJSON protocol over the |
| 7 | +// real unix socket to the real `createServerBackend`, and the real |
| 8 | +// `DesktopDashboard`/`DesktopController` render the responses. |
| 9 | +// |
| 10 | +// The socket path is injected by `app/tool/e2e-desktop.sh` via |
| 11 | +// `--dart-define=MAKIT_CONTROL_SOCK`. We wire the same client/controller |
| 12 | +// `runDesktopApp` builds, but pump `DesktopDashboard` directly so the test |
| 13 | +// needs no tray/window native plugins. |
| 14 | +// |
| 15 | +// ignore_for_file: depend_on_referenced_packages |
| 16 | +import 'package:flutter/material.dart'; |
| 17 | +import 'package:flutter_riverpod/flutter_riverpod.dart'; |
| 18 | +import 'package:flutter_test/flutter_test.dart'; |
| 19 | +import 'package:integration_test/integration_test.dart'; |
| 20 | +import 'package:makit/control/control_client.dart'; |
| 21 | +import 'package:makit/control/reconnecting_control_client.dart'; |
| 22 | +import 'package:makit/desktop/daemon/daemon_lifecycle.dart'; |
| 23 | +import 'package:makit/desktop/desktop_app.dart'; |
| 24 | +import 'package:makit/desktop/desktop_controller.dart'; |
| 25 | +import 'package:makit/desktop/screens/providers.dart'; |
| 26 | + |
| 27 | +const _socketPath = String.fromEnvironment('MAKIT_CONTROL_SOCK'); |
| 28 | +const _timeout = Duration(seconds: 20); |
| 29 | + |
| 30 | +/// Pump at 100ms steps until [finder] matches or [_timeout] expires. |
| 31 | +Future<void> _pumpUntil( |
| 32 | + WidgetTester tester, |
| 33 | + Finder finder, { |
| 34 | + String? reason, |
| 35 | +}) async { |
| 36 | + final deadline = DateTime.now().add(_timeout); |
| 37 | + while (DateTime.now().isBefore(deadline)) { |
| 38 | + await tester.pump(const Duration(milliseconds: 100)); |
| 39 | + if (finder.evaluate().isNotEmpty) { |
| 40 | + await tester.pump(const Duration(milliseconds: 100)); |
| 41 | + return; |
| 42 | + } |
| 43 | + } |
| 44 | + fail(reason ?? 'timed out waiting for $finder'); |
| 45 | +} |
| 46 | + |
| 47 | +void main() { |
| 48 | + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); |
| 49 | + |
| 50 | + testWidgets('desktop dashboard drives a real daemon control socket', ( |
| 51 | + tester, |
| 52 | + ) async { |
| 53 | + expect( |
| 54 | + _socketPath.isNotEmpty, |
| 55 | + isTrue, |
| 56 | + reason: 'MAKIT_CONTROL_SOCK must be passed via --dart-define', |
| 57 | + ); |
| 58 | + |
| 59 | + final client = ReconnectingControlClient( |
| 60 | + create: () => MakitControlClient(socketPath: _socketPath), |
| 61 | + connect: (c) => (c as MakitControlClient).connect(), |
| 62 | + dispose: (c) => (c as MakitControlClient).dispose(), |
| 63 | + ); |
| 64 | + final controller = DesktopController( |
| 65 | + client: client, |
| 66 | + lifecycle: DaemonLifecycle(resolver: MakitCliResolver()), |
| 67 | + ); |
| 68 | + addTearDown(() async { |
| 69 | + controller.dispose(); |
| 70 | + await client.close(); |
| 71 | + }); |
| 72 | + controller.startPolling(); |
| 73 | + |
| 74 | + await tester.pumpWidget( |
| 75 | + ProviderScope( |
| 76 | + overrides: [ |
| 77 | + controlClientProvider.overrideWithValue(client), |
| 78 | + desktopControllerProvider.overrideWithValue(controller), |
| 79 | + ], |
| 80 | + child: const MaterialApp(home: DesktopDashboard()), |
| 81 | + ), |
| 82 | + ); |
| 83 | + |
| 84 | + // status → the header renders the live pid over the real socket. |
| 85 | + await _pumpUntil( |
| 86 | + tester, |
| 87 | + find.textContaining('Server running (pid'), |
| 88 | + reason: |
| 89 | + 'header never showed a running daemon — control socket handshake ' |
| 90 | + 'or status verb failed', |
| 91 | + ); |
| 92 | + |
| 93 | + // devices.list → the seeded device shows on the (default) Devices tab. |
| 94 | + await _pumpUntil( |
| 95 | + tester, |
| 96 | + find.text('e2e phone'), |
| 97 | + reason: 'devices.list did not surface the seeded device', |
| 98 | + ); |
| 99 | + |
| 100 | + // pair.mint → the QR tab mints and renders a makit:// pair url. |
| 101 | + await tester.tap(find.text('Pair QR')); |
| 102 | + await _pumpUntil( |
| 103 | + tester, |
| 104 | + find.textContaining('makit://pair'), |
| 105 | + reason: 'pair.mint did not return a pair url', |
| 106 | + ); |
| 107 | + |
| 108 | + // sessions.list → the Sessions tab lists the seeded default session. |
| 109 | + await tester.tap(find.textContaining('Sessions (')); |
| 110 | + await _pumpUntil( |
| 111 | + tester, |
| 112 | + find.text('new session'), |
| 113 | + reason: 'sessions.list did not surface the default session', |
| 114 | + ); |
| 115 | + }); |
| 116 | +} |
0 commit comments