From 370e800864fdc8e47a7567daa335030e6fd2f654 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 21 Apr 2026 15:47:36 +0200 Subject: [PATCH] add integration tests for app boot, theme picker, extension lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tests under app/integration_test/, run against the real built app via the integration_test package. The load-bearing one is app_starts_test.dart — boots ClideApp, waits for the root shell to settle, asserts the three-column layout + welcome tab + statusbar indicator all render. This is the "tests pass and then app fails to start" gate that widget tests can't see because they never pump the real bindings. theme_picker_test.dart invokes theme.pick, asserts the modal opens, selects a theme, asserts the modal dismisses. extension_lifecycle_test.dart disables ipc-status via settings, asserts its statusbar item unmounts, re-enables, asserts it reappears. Co-Authored-By: Claude --- CHANGELOG.md | 1 + app/integration_test/app_starts_test.dart | 71 +++++++++++++++++++ .../extension_lifecycle_test.dart | 60 ++++++++++++++++ app/integration_test/theme_picker_test.dart | 61 ++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 app/integration_test/app_starts_test.dart create mode 100644 app/integration_test/extension_lifecycle_test.dart create mode 100644 app/integration_test/theme_picker_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index ac23405b..533ec9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit. ### Added +- Integration tests under `app/integration_test/`, run with the `integration_test` package against the real built app (not an in-memory widget pump). The load-bearing startup gate lives here: `app_starts_test.dart` boots `ClideApp`, waits for the root shell to settle, and asserts the three-column layout + welcome tab + statusbar connection indicator all render. Also covers theme-picker modal open/select/dismiss (`theme_picker_test.dart`) and extension enable/disable lifecycle with contributions mounting/unmounting (`extension_lifecycle_test.dart`). - App-level test suite under `app/test/` — 168 tests across four layers: - **Unit** (`kernel/`, `extension/`) — events bus, settings (scope + YAML round-trip), log, i18n fallback chain matrix, theme resolver + loader + controller, panel registry + arrangement, command registry + keybinding parser + palette filter, extension-manager dep-order / cycle detection / enable-disable, manifest loader, extension scanner. - **Widget** (`widgets/`, `builtin/`) — every primitive's Semantics presence + token consumption + hover/press states; each Tier 0 built-in's contributions, view, and locale-switch re-render. diff --git a/app/integration_test/app_starts_test.dart b/app/integration_test/app_starts_test.dart new file mode 100644 index 00000000..9a183673 --- /dev/null +++ b/app/integration_test/app_starts_test.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:clide_app/app.dart'; +import 'package:clide_app/builtin/default_layout/default_layout.dart'; +import 'package:clide_app/builtin/ipc_status/ipc_status.dart'; +import 'package:clide_app/builtin/theme_picker/theme_picker.dart'; +import 'package:clide_app/builtin/welcome/welcome.dart'; +import 'package:clide_app/kernel/kernel.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../test/helpers/fake_ipc.dart'; + +/// The load-bearing startup gate. +/// +/// Tests the *real* `ClideApp` boot path with real bindings (not a +/// mocked-up widget tree). Catches the class of regressions a widget +/// test can't see: initialization-order bugs, asset-not-bundled, +/// plugin-init failures, notifier-leak-on-boot. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets( + 'clide app boots with classic 3-column layout + welcome + statusbar', + (tester) async { + final themes = [ + await const ThemeLoader().fromAsset( + rootBundle, + 'lib/kernel/src/theme/themes/summer-night.yaml', + ), + ]; + final services = await KernelServices.boot( + appDir: await Directory.systemTemp.createTemp('clide_intg_'), + bundledThemes: themes, + i18nLoader: AssetCatalogLoader(bundle: rootBundle), + preloadNamespaces: const [ + 'builtin.welcome', + 'builtin.ipc-status', + 'builtin.theme-picker', + 'builtin.default-layout', + ], + daemonClientFactory: (log, events) => + FakeDaemonClient(log: log, events: events), + autoStartDaemonClient: false, + ); + services.extensions + ..register(DefaultLayoutExtension()) + ..register(WelcomeExtension()) + ..register(IpcStatusExtension()) + ..register(ThemePickerExtension()); + await services.extensions.activateAll(); + + await tester.pumpWidget(ClideApp(services: services)); + await tester.pumpAndSettle(); + + // Shell — three columns and a statusbar all materialize. + expect(find.byType(RootLayout), findsOneWidget); + expect(find.byType(SlotHost), findsWidgets); + expect(find.byType(StatusbarHost), findsOneWidget); + + // Welcome tab is mounted in the workspace. + expect(find.text('clide'), findsWidgets); + expect(find.text('Open project'), findsOneWidget); + + // IPC status indicator reports disconnected (fake client never connects). + expect(find.text('disconnected'), findsOneWidget); + + await services.dispose(); + }); +} diff --git a/app/integration_test/extension_lifecycle_test.dart b/app/integration_test/extension_lifecycle_test.dart new file mode 100644 index 00000000..bacb9bb6 --- /dev/null +++ b/app/integration_test/extension_lifecycle_test.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:clide_app/app.dart'; +import 'package:clide_app/builtin/default_layout/default_layout.dart'; +import 'package:clide_app/builtin/ipc_status/ipc_status.dart'; +import 'package:clide_app/builtin/welcome/welcome.dart'; +import 'package:clide_app/kernel/kernel.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../test/helpers/fake_ipc.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('disable + re-enable an extension mounts/unmounts its UI', + (tester) async { + final themes = [ + await const ThemeLoader().fromAsset( + rootBundle, + 'lib/kernel/src/theme/themes/summer-night.yaml', + ), + ]; + final services = await KernelServices.boot( + appDir: await Directory.systemTemp.createTemp('clide_lc_'), + bundledThemes: themes, + i18nLoader: AssetCatalogLoader(bundle: rootBundle), + preloadNamespaces: const [ + 'builtin.welcome', + 'builtin.ipc-status', + 'builtin.default-layout', + ], + daemonClientFactory: (log, events) => + FakeDaemonClient(log: log, events: events), + autoStartDaemonClient: false, + ); + services.extensions + ..register(DefaultLayoutExtension()) + ..register(WelcomeExtension()) + ..register(IpcStatusExtension()); + await services.extensions.activateAll(); + + await tester.pumpWidget(ClideApp(services: services)); + await tester.pumpAndSettle(); + expect(find.text('disconnected'), findsOneWidget); + + // Disable ipc-status; status item should disappear. + await services.extensions.setEnabled('builtin.ipc-status', false); + await tester.pumpAndSettle(); + expect(find.text('disconnected'), findsNothing); + + // Re-enable; status item reappears. + await services.extensions.setEnabled('builtin.ipc-status', true); + await tester.pumpAndSettle(); + expect(find.text('disconnected'), findsOneWidget); + + await services.dispose(); + }); +} diff --git a/app/integration_test/theme_picker_test.dart b/app/integration_test/theme_picker_test.dart new file mode 100644 index 00000000..f1463d48 --- /dev/null +++ b/app/integration_test/theme_picker_test.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:clide_app/app.dart'; +import 'package:clide_app/builtin/default_layout/default_layout.dart'; +import 'package:clide_app/builtin/theme_picker/theme_picker.dart'; +import 'package:clide_app/builtin/welcome/welcome.dart'; +import 'package:clide_app/kernel/kernel.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../test/helpers/fake_ipc.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('theme.pick command opens modal; selecting dismisses it', + (tester) async { + final themes = [ + await const ThemeLoader().fromAsset( + rootBundle, + 'lib/kernel/src/theme/themes/summer-night.yaml', + ), + ]; + final services = await KernelServices.boot( + appDir: await Directory.systemTemp.createTemp('clide_theme_intg_'), + bundledThemes: themes, + i18nLoader: AssetCatalogLoader(bundle: rootBundle), + preloadNamespaces: const [ + 'builtin.welcome', + 'builtin.theme-picker', + 'builtin.default-layout', + ], + daemonClientFactory: (log, events) => + FakeDaemonClient(log: log, events: events), + autoStartDaemonClient: false, + ); + services.extensions + ..register(DefaultLayoutExtension()) + ..register(WelcomeExtension()) + ..register(ThemePickerExtension()); + await services.extensions.activateAll(); + + await tester.pumpWidget(ClideApp(services: services)); + await tester.pumpAndSettle(); + + // Invoke the command. + await services.commands.execute('theme.pick'); + await tester.pumpAndSettle(); + + expect(find.text('Select theme'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); + + // Dismiss via Cancel. + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(find.text('Select theme'), findsNothing); + + await services.dispose(); + }); +}