// Workspace-anchor capture harness — visual proof for the sidebar // switcher rebuild (persona review 2026-08-27). Renders the full // shell against the scriptable fake hub and writes a PNG per theme // via a RepaintBoundary (driverless, headed macOS `flutter test`). // // flutter test integration_test/workspace_shots_test.dart -d macos // // Output: build/workspace-shots/-.png // (or $WORKSPACE_SHOTS_OUT). // // Fixture names are deliberately generic — never real client or // area names. import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:chain_client_sdk/chain_client_sdk.dart' show HubEndpoint; import 'package:chain_studio/data/hub.dart'; import 'package:chain_studio/data/sealed_areas.dart'; import 'package:chain_studio/data/sidebar_prefs.dart'; import 'package:chain_studio/data/workspace.dart'; import 'package:chain_studio/data/workspace_prefs.dart'; import 'package:chain_studio/main.dart'; import 'package:chain_studio/widgets/chain_workspace_switcher.dart'; import '../test/support/fake_hub.dart'; final GlobalKey _shotKey = GlobalKey(); String get _outDir => Platform.environment['WORKSPACE_SHOTS_OUT'] ?? 'build/workspace-shots'; Future _shot(WidgetTester tester, String name) async { await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150)); final boundary = _shotKey.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 2.0); final bytes = await image.toByteData(format: ui.ImageByteFormat.png); image.dispose(); final file = File('$_outDir/$name.png'); file.parent.createSync(recursive: true); file.writeAsBytesSync(bytes!.buffer.asUint8List()); // ignore: avoid_print print('workspace-shot: ${file.path}'); } const _projects = [ ProjectRef(slug: 'general', name: 'General'), ProjectRef( slug: 'projekt-alpha', name: 'Projekt Alpha', color: '#8b7cf6', isolation: 'protected', ), ProjectRef(slug: 'projekt-beta', name: 'Projekt Beta', color: '#2e8f9e'), ]; const _areas = [ SealedArea( slug: 'bereich-nord', name: 'Bereich Nord', color: '#e0a458', port: 51100, running: true, ), SealedArea( slug: 'bereich-sued', name: 'Bereich Süd', color: '#c25e5e', port: 51101, running: false, ), ]; class _FakeSealedAreas extends SealedAreaService { _FakeSealedAreas() : super.forTest(); @override Future> list() async => _areas; @override Future boundEndpoint(String slug) async => null; } class _GatedHub extends FakeHubService { Completer? healthyGate; @override Future reconnect( HubEndpoint endpoint, { Object? authToken = const Object(), bool persist = true, }) async {} @override Future healthy() async { final gate = healthyGate; if (gate != null) await gate.future; return true; } } Future _boot(WidgetTester tester, _GatedHub hub, String theme) async { SharedPreferences.setMockInitialValues({}); hub.projects = _projects; HubService.debugSetInstance(hub); SealedAreaService.debugSetInstance(_FakeSealedAreas()); SidebarPrefs.pinned.value = true; Workspace.instance.debugSeed( projects: _projects, active: 'projekt-alpha', sealed: _areas, ); tester.view.physicalSize = const Size(2560, 1600); tester.view.devicePixelRatio = 2.0; await tester.pumpWidget( RepaintBoundary( key: _shotKey, child: StudioApp( initialThemeMode: theme == 'dark' ? ThemeModeValue.dark : ThemeModeValue.light, initialLocale: const Locale('de'), startSidebarExpanded: true, ), ), ); await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300)); } Future _teardown(WidgetTester tester) async { HubService.debugSetInstance(null); SealedAreaService.debugSetInstance(null); SidebarPrefs.pinned.value = false; WorkspacePrefs.sealedNamesVisible.value = false; Workspace.instance.debugSeed(projects: const [], active: ''); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(const Duration(minutes: 1)); tester.view.reset(); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); for (final theme in ['light', 'dark']) { testWidgets('anchor + menu + switching bar — $theme', (tester) async { final hub = _GatedHub(); await _boot(tester, hub, theme); // 01: the shell with the sidebar anchor (active project). await _shot(tester, '01-anchor-shell-$theme'); // 02: the switcher menu — checkmark on the active project, // aggregated sealed row with the why-line + Settings link. await tester.tap(find.byType(ChainWorkspaceAnchor)); await tester.pump(const Duration(milliseconds: 300)); await _shot(tester, '02-switcher-menu-$theme'); // 03: names revealed for this menu opening. await tester.tap(find.text('Namen einblenden')); await tester.pump(const Duration(milliseconds: 200)); await _shot(tester, '03-switcher-menu-revealed-$theme'); await tester.tapAt(const Offset(1200, 700)); // dismiss menu await tester.pump(const Duration(milliseconds: 300)); // 04: the switching state — identity bar announces the // target BEFORE the connection settles (privacy race fix). hub.healthyGate = Completer(); final switching = Workspace.instance.switchToSealed(_areas.first); await tester.pump(const Duration(milliseconds: 100)); await _shot(tester, '04-identity-switching-$theme'); // 05: inside the sealed area — bar + anchor agree. hub.healthyGate!.complete(); hub.healthyGate = null; await switching; await tester.pump(const Duration(milliseconds: 300)); await _shot(tester, '05-identity-in-area-$theme'); await Workspace.instance.switchToShared(); await _teardown(tester); }); } }