chore: 0.82.0 — switcher-rebuild changelog, anchor caption, capture harness
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
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>
This commit is contained in:
parent
d69277d29d
commit
10f38beafa
9 changed files with 234 additions and 7 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -6,6 +6,40 @@ lockstep.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed (0.82.0)
|
||||||
|
|
||||||
|
Workspace-switcher rebuild (persona review 2026-08-27; both
|
||||||
|
high-rated findings fixed):
|
||||||
|
|
||||||
|
- **Sealed-switch privacy race closed.** A connection switch now
|
||||||
|
runs inside an explicit switching window: the identity bar
|
||||||
|
announces the target BEFORE the client re-points, page pollers
|
||||||
|
pause, replies from the previous hub are dropped, and the sealed
|
||||||
|
context is announced only after the new hub answered healthy.
|
||||||
|
Guard: `workspace_switch_race_test`.
|
||||||
|
- **Project filter survives the sealed round trip.** Entering an
|
||||||
|
area parks the shared-hub filter; returning restores it — live
|
||||||
|
state and prefs agree again.
|
||||||
|
- **One global switcher anchor.** The switcher lives once in the
|
||||||
|
sidebar (context visible on every page, Cmd+P opens it); the four
|
||||||
|
per-page embeddings are gone. The sidebar endpoint label updates
|
||||||
|
with the workspace instead of lagging until the next health tick.
|
||||||
|
- **Stopped areas ask before starting** ("Bereich starten?") —
|
||||||
|
running areas keep switching with one click.
|
||||||
|
- **Honest scope tooltip** (the switcher applies everywhere, not
|
||||||
|
"this view"), **why-line at the aggregated sealed row** with a
|
||||||
|
jump to Settings → Security, **checkmark on the active entry**,
|
||||||
|
and the Cmd+K palette knows projects/areas (recent-use ranked,
|
||||||
|
sealed names honour the privacy setting).
|
||||||
|
- **Sealed-aware flow list.** Inside a sealed area the editor lists
|
||||||
|
the instance's own flows dir (it used to keep showing the shared
|
||||||
|
hub's files), the editor state is dropped on a connection switch,
|
||||||
|
sample flows follow the hub's FlowSummary.sample flag (chip +
|
||||||
|
collapsed "Beispiele" group, editor 0.26.0), and an empty sealed
|
||||||
|
list offers "Beispiel-Flows importieren".
|
||||||
|
- **Runs empty state names the filter** ("Keine Läufe in Projekt X"
|
||||||
|
+ show-all action) instead of claiming the feature is off.
|
||||||
|
|
||||||
### Changed (0.81.0)
|
### Changed (0.81.0)
|
||||||
|
|
||||||
- **Approvals explain themselves.** The pending approval card now
|
- **Approvals explain themselves.** The pending approval card now
|
||||||
|
|
|
||||||
193
integration_test/workspace_shots_test.dart
Normal file
193
integration_test/workspace_shots_test.dart
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
/// Studio's own build version. Bump on every UI release so the
|
/// Studio's own build version. Bump on every UI release so the
|
||||||
/// running app self-identifies.
|
/// running app self-identifies.
|
||||||
const String kStudioVersion = '0.81.0';
|
const String kStudioVersion = '0.82.0';
|
||||||
|
|
||||||
const String kProductName = 'Ch∆In Studio';
|
const String kProductName = 'Ch∆In Studio';
|
||||||
const String kVendorName = 'Flemming.AI (F∆I)';
|
const String kVendorName = 'Flemming.AI (F∆I)';
|
||||||
|
|
|
||||||
|
|
@ -1837,7 +1837,7 @@
|
||||||
"workspaceAll": "Alle Projekte",
|
"workspaceAll": "Alle Projekte",
|
||||||
"workspaceDefaultProject": "Allgemein",
|
"workspaceDefaultProject": "Allgemein",
|
||||||
"workspaceSwitcherTooltip": "Arbeitskontext wählen — gilt überall: filtert Flows, Läufe, Audit und Freigaben und stempelt neue Läufe mit dem gewählten Projekt",
|
"workspaceSwitcherTooltip": "Arbeitskontext wählen — gilt überall: filtert Flows, Läufe, Audit und Freigaben und stempelt neue Läufe mit dem gewählten Projekt",
|
||||||
"workspaceAnchorCaption": "Projekt / Bereich",
|
"workspaceAnchorCaption": "Kontext",
|
||||||
"workspaceStartConfirmTitle": "Bereich „{name}“ starten?",
|
"workspaceStartConfirmTitle": "Bereich „{name}“ starten?",
|
||||||
"@workspaceStartConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
"@workspaceStartConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"workspaceStartConfirmBody": "Dieser abgeschottete Bereich ist gerade gestoppt. Studio startet seine eigene Hub-Instanz auf diesem Rechner und verbindet sich mit ihr — laufende Bereiche wechseln ohne diese Rückfrage.",
|
"workspaceStartConfirmBody": "Dieser abgeschottete Bereich ist gerade gestoppt. Studio startet seine eigene Hub-Instanz auf diesem Rechner und verbindet sich mit ihr — laufende Bereiche wechseln ohne diese Rückfrage.",
|
||||||
|
|
|
||||||
|
|
@ -1876,7 +1876,7 @@
|
||||||
"workspaceAll": "All projects",
|
"workspaceAll": "All projects",
|
||||||
"workspaceDefaultProject": "General",
|
"workspaceDefaultProject": "General",
|
||||||
"workspaceSwitcherTooltip": "Choose the working context — applies everywhere: filters flows, runs, audit and approvals, and stamps new runs with the selected project",
|
"workspaceSwitcherTooltip": "Choose the working context — applies everywhere: filters flows, runs, audit and approvals, and stamps new runs with the selected project",
|
||||||
"workspaceAnchorCaption": "Project / area",
|
"workspaceAnchorCaption": "Context",
|
||||||
"workspaceStartConfirmTitle": "Start area “{name}”?",
|
"workspaceStartConfirmTitle": "Start area “{name}”?",
|
||||||
"@workspaceStartConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
"@workspaceStartConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
||||||
"workspaceStartConfirmBody": "This sealed area is currently stopped. Studio starts its own hub instance on this machine and connects to it — running areas switch without this prompt.",
|
"workspaceStartConfirmBody": "This sealed area is currently stopped. Studio starts its own hub instance on this machine and connects to it — running areas switch without this prompt.",
|
||||||
|
|
|
||||||
|
|
@ -5776,7 +5776,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @workspaceAnchorCaption.
|
/// No description provided for @workspaceAnchorCaption.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Project / area'**
|
/// **'Context'**
|
||||||
String get workspaceAnchorCaption;
|
String get workspaceAnchorCaption;
|
||||||
|
|
||||||
/// No description provided for @workspaceStartConfirmTitle.
|
/// No description provided for @workspaceStartConfirmTitle.
|
||||||
|
|
|
||||||
|
|
@ -3426,7 +3426,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Arbeitskontext wählen — gilt überall: filtert Flows, Läufe, Audit und Freigaben und stempelt neue Läufe mit dem gewählten Projekt';
|
'Arbeitskontext wählen — gilt überall: filtert Flows, Läufe, Audit und Freigaben und stempelt neue Läufe mit dem gewählten Projekt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get workspaceAnchorCaption => 'Projekt / Bereich';
|
String get workspaceAnchorCaption => 'Kontext';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String workspaceStartConfirmTitle(String name) {
|
String workspaceStartConfirmTitle(String name) {
|
||||||
|
|
|
||||||
|
|
@ -3419,7 +3419,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'Choose the working context — applies everywhere: filters flows, runs, audit and approvals, and stamps new runs with the selected project';
|
'Choose the working context — applies everywhere: filters flows, runs, audit and approvals, and stamps new runs with the selected project';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get workspaceAnchorCaption => 'Project / area';
|
String get workspaceAnchorCaption => 'Context';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String workspaceStartConfirmTitle(String name) {
|
String workspaceStartConfirmTitle(String name) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: chain_studio
|
name: chain_studio
|
||||||
description: "Ch∆In Studio — desktop GUI for the Ch∆In hub"
|
description: "Ch∆In Studio — desktop GUI for the Ch∆In hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.81.0
|
version: 0.82.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue