chain-studio/integration_test/workspace_shots_test.dart
flemming-it 10f38beafa
Some checks failed
Security / Security check (push) Failing after 2s
chore: 0.82.0 — switcher-rebuild changelog, anchor caption, capture harness
The anchor caption 'Projekt / Bereich' truncated uselessly at the
rail width — 'Kontext'/'Context' fits and the tooltip carries the
full explanation. workspace_shots_test renders the anchor, the
open menu (checkmark, aggregated why-line), and the switching /
in-area identity bar per theme as the visual-proof harness.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-08-28 00:28:02 +02:00

193 lines
6.2 KiB
Dart

// 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/<name>-<theme>.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<void> _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<SealedArea>> list() async => _areas;
@override
Future<String?> boundEndpoint(String slug) async => null;
}
class _GatedHub extends FakeHubService {
Completer<void>? healthyGate;
@override
Future<void> reconnect(
HubEndpoint endpoint, {
Object? authToken = const Object(),
bool persist = true,
}) async {}
@override
Future<bool> healthy() async {
final gate = healthyGate;
if (gate != null) await gate.future;
return true;
}
}
Future<void> _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<void> _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<void>();
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);
});
}
}