feat: sample flows follow the hub's wire flag; examples group + import action
The Example chip used to come from a client-side content-marker scan that had drifted from the hub's renamed sample header — no current sample matched it. The host now passes the hub-reported sample set (FlowSummary.sample) and the editor renders truth only: no host info, no chips. Samples collapse under one 'Examples (N)' group below the operator's own flows (expanded only when nothing else exists), and an optional onImportSamples action on the empty list is the deliberate way to pull the bundled examples into a sealed area's empty hub. Guard: flow_list_samples_test pins chips-from-host-only, the grouping, and the import round trip. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
8da50068f3
commit
29294a2971
6 changed files with 330 additions and 31 deletions
144
test/flow_list_samples_test.dart
Normal file
144
test/flow_list_samples_test.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Widget-level proof of the sample handling contract (persona
|
||||
// review 2026-08-27 / sealed-areas rework):
|
||||
//
|
||||
// * The host's sample set (the hub's FlowSummary.sample wire
|
||||
// flag) is the ONLY source of the "Example" chip — no host
|
||||
// info, no chips. The editor's old content-marker scan had
|
||||
// silently drifted from the hub's header and is gone.
|
||||
// * Sample flows collapse under one "Examples (N)" group so
|
||||
// they are never mistaken for the operator's own work; the
|
||||
// group expands only on demand (or when nothing else exists).
|
||||
// * An empty list offers "Import example flows" iff the host
|
||||
// wired the action (sealed areas start empty on purpose).
|
||||
//
|
||||
// Hermetic — temp flows dir, never ~/.chain.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
|
||||
|
||||
Future<void> _pumpEditor(
|
||||
WidgetTester tester, {
|
||||
required String flowsDir,
|
||||
Set<String>? sampleFlowNames,
|
||||
Future<void> Function()? onImportSamples,
|
||||
}) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: FlowEditorPage(
|
||||
flowsDir: flowsDir,
|
||||
sampleFlowNames: sampleFlowNames,
|
||||
onImportSamples: onImportSamples,
|
||||
),
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('chain_editor_samples_test');
|
||||
File('${tmp.path}/my-work.yaml').writeAsStringSync(
|
||||
'name: my-work\nsteps: []\n',
|
||||
);
|
||||
File('${tmp.path}/hello.yaml').writeAsStringSync(
|
||||
'name: hello\nsteps: []\n',
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
testWidgets('host-reported samples collapse under the examples group', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpEditor(
|
||||
tester,
|
||||
flowsDir: tmp.path,
|
||||
sampleFlowNames: const {'hello'},
|
||||
);
|
||||
// Own flow visible, sample hidden behind the collapsed group.
|
||||
expect(find.text('my-work'), findsOneWidget);
|
||||
expect(find.text('hello'), findsNothing);
|
||||
expect(find.text('Examples (1)'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Examples (1)'));
|
||||
await tester.pump();
|
||||
expect(find.text('hello'), findsOneWidget);
|
||||
// The revealed sample row carries the chip.
|
||||
expect(find.text('Example'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('no host sample info means no chips and no group', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpEditor(tester, flowsDir: tmp.path, sampleFlowNames: null);
|
||||
expect(find.text('my-work'), findsOneWidget);
|
||||
expect(find.text('hello'), findsOneWidget);
|
||||
expect(find.text('Example'), findsNothing);
|
||||
expect(find.textContaining('Examples ('), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a samples-only list starts expanded', (tester) async {
|
||||
File('${tmp.path}/my-work.yaml').deleteSync();
|
||||
await _pumpEditor(
|
||||
tester,
|
||||
flowsDir: tmp.path,
|
||||
sampleFlowNames: const {'hello'},
|
||||
);
|
||||
expect(find.text('hello'), findsOneWidget);
|
||||
expect(find.text('Examples (1)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the empty list offers the import action when wired', (
|
||||
tester,
|
||||
) async {
|
||||
File('${tmp.path}/my-work.yaml').deleteSync();
|
||||
File('${tmp.path}/hello.yaml').deleteSync();
|
||||
var imported = 0;
|
||||
await _pumpEditor(
|
||||
tester,
|
||||
flowsDir: tmp.path,
|
||||
onImportSamples: () async {
|
||||
imported++;
|
||||
File('${tmp.path}/hello.yaml').writeAsStringSync(
|
||||
'name: hello\nsteps: []\n',
|
||||
);
|
||||
},
|
||||
);
|
||||
final button = find.text('Import example flows');
|
||||
expect(button, findsOneWidget);
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(button);
|
||||
// Let the import callback, the directory re-list (real IO)
|
||||
// and the FutureBuilder's completion callback all run.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
await tester.pump();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(imported, 1);
|
||||
// The imported flow appears without a manual refresh.
|
||||
expect(find.text('hello'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the empty list stays action-free when nothing is wired', (
|
||||
tester,
|
||||
) async {
|
||||
File('${tmp.path}/my-work.yaml').deleteSync();
|
||||
File('${tmp.path}/hello.yaml').deleteSync();
|
||||
await _pumpEditor(tester, flowsDir: tmp.path);
|
||||
expect(find.text('Import example flows'), findsNothing);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue