// Workspace switcher — stage-1 contract: the control renders the // active selection, lists "All projects" plus every registry // project (colour dot, shield for protected), and switching // updates the shared Workspace notifier. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:chain_studio/data/hub.dart'; import 'package:chain_studio/data/sealed_areas.dart'; import 'package:chain_studio/data/workspace.dart'; import 'package:chain_studio/data/workspace_prefs.dart'; import 'package:chain_studio/l10n/app_localizations.dart'; import 'package:chain_studio/widgets/chain_workspace_switcher.dart'; Widget _host() { return const MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( appBar: PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Material(child: ChainWorkspaceSwitcher()), ), body: SizedBox(), ), ); } const _general = ProjectRef(slug: 'general', name: 'General'); const _clientA = ProjectRef( slug: 'client-a', name: 'Client A', color: '#8b7cf6', isolation: 'protected', ); void main() { setUp(() { // Seed the singleton without a hub: tests drive the notifier // directly through its test hook. Workspace.instance.debugSeed(projects: [_general, _clientA], active: ''); }); tearDown(() { WorkspacePrefs.sealedNamesVisible.value = false; }); testWidgets('sealed areas stay aggregated until deliberately revealed', ( tester, ) async { Workspace.instance.debugSeed( projects: [_general], active: '', sealed: [_sealedGrid, _sealedLab], ); await tester.pumpWidget(_host()); await tester.tap(find.byType(ChainWorkspaceSwitcher)); await tester.pumpAndSettle(); // No names on a casual glance — only the aggregate row. expect(find.text('grid'), findsNothing); expect(find.text('lab'), findsNothing); expect(find.text('2 sealed areas'), findsOneWidget); await tester.tap(find.text('Show names')); await tester.pumpAndSettle(); expect(find.text('grid'), findsOneWidget); expect(find.text('lab'), findsOneWidget); }); testWidgets('the Settings toggle restores the direct listing', ( tester, ) async { WorkspacePrefs.sealedNamesVisible.value = true; Workspace.instance.debugSeed( projects: [_general], active: '', sealed: [_sealedGrid, _sealedLab], ); await tester.pumpWidget(_host()); await tester.tap(find.byType(ChainWorkspaceSwitcher)); await tester.pumpAndSettle(); expect(find.text('grid'), findsOneWidget); expect(find.text('lab'), findsOneWidget); expect(find.text('2 sealed areas'), findsNothing); }); testWidgets('shows "All projects" when no project is active', ( tester, ) async { await tester.pumpWidget(_host()); expect(find.text('All projects'), findsOneWidget); }); testWidgets('menu lists all-projects entry plus every registry project', ( tester, ) async { await tester.pumpWidget(_host()); await tester.tap(find.byType(ChainWorkspaceSwitcher)); await tester.pumpAndSettle(); expect(find.text('All projects'), findsWidgets); expect(find.text('General'), findsOneWidget); expect(find.text('Client A'), findsOneWidget); // Protected projects carry the shield marker. expect(find.byIcon(Icons.shield_outlined), findsOneWidget); }); testWidgets('selecting a project updates the workspace and the label', ( tester, ) async { await tester.pumpWidget(_host()); await tester.tap(find.byType(ChainWorkspaceSwitcher)); await tester.pumpAndSettle(); await tester.tap(find.text('Client A').last); await tester.pumpAndSettle(); expect(Workspace.instance.activeSlug, 'client-a'); expect(Workspace.instance.active?.isProtected, isTrue); // The closed control now shows the active project (label + // shield marker for protected). expect(find.text('Client A'), findsOneWidget); expect(find.byIcon(Icons.shield_outlined), findsOneWidget); }); test('active falls back to null when the slug left the registry', () { Workspace.instance.debugSeed(projects: [_general], active: 'gone'); expect(Workspace.instance.activeSlug, 'gone'); expect(Workspace.instance.active, isNull); expect(Workspace.instance.isAll, isFalse); }); testWidgets('lists sealed areas with lock + running/stopped status', ( tester, ) async { Workspace.instance.debugSeed( projects: [_general], active: '', sealed: const [ SealedArea( slug: 'grid', name: 'Grid', color: '#e0a458', port: 51100, running: true, ), SealedArea( slug: 'bank', name: 'Bank', color: '#c25e5e', port: 51101, running: false, ), ], ); await tester.pumpWidget(_host()); await tester.tap(find.byType(ChainWorkspaceSwitcher)); await tester.pumpAndSettle(); // Sealed areas appear under the sealed header, aggregated by // default (confidentiality); after the reveal every area shows // its lock icon and running/stopped status word. expect(find.text('SEALED AREAS'), findsOneWidget); expect(find.byIcon(Icons.lock_outline), findsWidgets); await tester.tap(find.text('Show names')); await tester.pumpAndSettle(); expect(find.text('Grid'), findsOneWidget); expect(find.text('Bank'), findsOneWidget); expect(find.text('running'), findsOneWidget); expect(find.text('stopped'), findsOneWidget); }); testWidgets('the pill shows the active sealed area with a lock', ( tester, ) async { Workspace.instance.debugSeed( projects: [_general], active: '', sealed: const [], activeSealed: const SealedArea( slug: 'grid', name: 'Grid', color: '#e0a458', port: 51100, running: true, ), ); await tester.pumpWidget(_host()); expect(Workspace.instance.inSealedArea, isTrue); // The closed pill names the sealed area and carries a lock. expect(find.text('Grid'), findsOneWidget); expect(find.byIcon(Icons.lock_outline), findsWidgets); }); } // Sealed-area confidentiality (usertest security finding): the // switcher must not disclose sealed-area names — often client // identity — on a casual glance. Aggregated row by default, // deliberate reveal, Settings toggle for the direct listing. const _sealedGrid = SealedArea( slug: 'grid', name: 'grid', color: '#e0a458', port: 51100, running: false, ); const _sealedLab = SealedArea( slug: 'lab', name: 'lab', color: '#c25e5e', port: 51101, running: true, );