chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,9 @@ void main() {
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
Widget box(VoidCallback onDismiss) => MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800, 600)),
|
||||
child: ClideLightbox(onDismiss: onDismiss, child: const SizedBox(width: 200, height: 150)),
|
||||
);
|
||||
data: const MediaQueryData(size: Size(800, 600)),
|
||||
child: ClideLightbox(onDismiss: onDismiss, child: const SizedBox(width: 200, height: 150)),
|
||||
);
|
||||
|
||||
testWidgets('renders the zoom hint and an InteractiveViewer', (tester) async {
|
||||
await tester.pumpWidget(harness(f, box(() {})));
|
||||
@@ -63,13 +63,18 @@ void main() {
|
||||
testWidgets('a tap on the dimmed canvas dismisses; a tap on the image does not (T-309)', (tester) async {
|
||||
final img = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!; // 1:1; real async
|
||||
var dismissed = false;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800, 600)),
|
||||
child: ClideLightbox(onDismiss: () => dismissed = true, child: RawImage(image: img, fit: BoxFit.contain)),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800, 600)),
|
||||
child: ClideLightbox(
|
||||
onDismiss: () => dismissed = true,
|
||||
child: RawImage(image: img, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The 94% box is ~752×564; a 1:1 image fits to 564×564 centred, leaving
|
||||
|
||||
@@ -14,30 +14,13 @@ void main() {
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
testWidgets('renders title + subtitle', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const ClidePaneChrome(
|
||||
title: 'terminal — ~/clide',
|
||||
subtitle: 'bash · 80×24',
|
||||
child: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClidePaneChrome(title: 'terminal — ~/clide', subtitle: 'bash · 80×24', child: SizedBox.shrink())));
|
||||
expect(find.text('terminal — ~/clide'), findsOneWidget);
|
||||
expect(find.text('bash · 80×24'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('no close button when onClose is null', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const ClidePaneChrome(
|
||||
title: 'primary claude',
|
||||
child: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClidePaneChrome(title: 'primary claude', child: SizedBox.shrink())));
|
||||
final handle = tester.ensureSemantics();
|
||||
expect(find.bySemanticsLabel('Close pane'), findsNothing);
|
||||
handle.dispose();
|
||||
@@ -45,16 +28,7 @@ void main() {
|
||||
|
||||
testWidgets('close button invokes onClose when present', (tester) async {
|
||||
var pressed = false;
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClidePaneChrome(
|
||||
title: 'terminal',
|
||||
onClose: () => pressed = true,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClidePaneChrome(title: 'terminal', onClose: () => pressed = true, child: const SizedBox.shrink())));
|
||||
final handle = tester.ensureSemantics();
|
||||
expect(find.bySemanticsLabel('Close pane'), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('Close pane'));
|
||||
|
||||
@@ -72,10 +72,7 @@ After.
|
||||
testWidgets('record-id link fires onRecordTap callback', (tester) async {
|
||||
var tapped = '';
|
||||
const src = 'See [D-1](#anchor) for details.';
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideMarkdown(src, onRecordTap: (id) => tapped = id),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, ClideMarkdown(src, onRecordTap: (id) => tapped = id)));
|
||||
await tester.pumpAndSettle();
|
||||
// The record-link path matches D-1 / Q-N / R-N / T-N and produces
|
||||
// a tappable span. We can't easily simulate a TextSpan tap from
|
||||
@@ -88,9 +85,7 @@ After.
|
||||
testWidgets('record-id link tap actually invokes onRecordTap', (tester) async {
|
||||
var tapped = '';
|
||||
const src = '[D-1](#anchor)';
|
||||
await tester.pumpWidget(
|
||||
harness(f, ClideMarkdown(src, onRecordTap: (id) => tapped = id)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideMarkdown(src, onRecordTap: (id) => tapped = id)));
|
||||
await tester.pumpAndSettle();
|
||||
// The link renders as a ClideTappable embedded in a WidgetSpan.
|
||||
await tester.tap(find.text('D-1'));
|
||||
@@ -200,10 +195,7 @@ After.
|
||||
});
|
||||
|
||||
testWidgets('handles a language whose grammar is unavailable (falls back to plain text)', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
const ClideCodeBlock(source: 'fn main() {}', language: 'rust'),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, const ClideCodeBlock(source: 'fn main() {}', language: 'rust')));
|
||||
await tester.pumpAndSettle();
|
||||
// TreeSitterService can't load the grammar in tests → falls back
|
||||
// to plain rendering.
|
||||
@@ -226,16 +218,12 @@ After.
|
||||
|
||||
testWidgets('collapsed accordion does not render children; tap toggles', (tester) async {
|
||||
var toggled = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideAccordion(
|
||||
label: 'Group',
|
||||
count: 2,
|
||||
expanded: false,
|
||||
onToggle: () => toggled++,
|
||||
children: const [Text('child-one'), Text('child-two')],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideAccordion(label: 'Group', count: 2, expanded: false, onToggle: () => toggled++, children: const [Text('child-one'), Text('child-two')]),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Group · 2'), findsOneWidget);
|
||||
expect(find.text('child-one'), findsNothing);
|
||||
@@ -244,17 +232,9 @@ After.
|
||||
});
|
||||
|
||||
testWidgets('expanded accordion shows children; leading widget renders', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideAccordion(
|
||||
label: 'Open',
|
||||
count: 1,
|
||||
expanded: true,
|
||||
onToggle: () {},
|
||||
leading: const Text('LEAD'),
|
||||
children: const [Text('only-child')],
|
||||
),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
harness(f, ClideAccordion(label: 'Open', count: 1, expanded: true, onToggle: () {}, leading: const Text('LEAD'), children: const [Text('only-child')])),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('only-child'), findsOneWidget);
|
||||
expect(find.text('LEAD'), findsOneWidget);
|
||||
@@ -269,36 +249,25 @@ After.
|
||||
testWidgets('wraps a Scrollable child and exposes ScrollbarTheme', (tester) async {
|
||||
final controller = ScrollController();
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ClideScrollbar(
|
||||
controller: controller,
|
||||
child: SingleChildScrollView(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ClideScrollbar(
|
||||
controller: controller,
|
||||
child: const SizedBox(height: 1000),
|
||||
child: SingleChildScrollView(controller: controller, child: const SizedBox(height: 1000)),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ClideScrollbar), findsOneWidget);
|
||||
});
|
||||
|
||||
test('ScrollbarTheme.updateShouldNotify detects colour changes', () {
|
||||
const a = ScrollbarTheme(
|
||||
slider: Color(0xFF000000),
|
||||
sliderHover: Color(0xFF111111),
|
||||
track: Color(0xFF222222),
|
||||
child: SizedBox.shrink(),
|
||||
);
|
||||
const b = ScrollbarTheme(
|
||||
slider: Color(0xFFFFFFFF),
|
||||
sliderHover: Color(0xFF111111),
|
||||
track: Color(0xFF222222),
|
||||
child: SizedBox.shrink(),
|
||||
);
|
||||
const a = ScrollbarTheme(slider: Color(0xFF000000), sliderHover: Color(0xFF111111), track: Color(0xFF222222), child: SizedBox.shrink());
|
||||
const b = ScrollbarTheme(slider: Color(0xFFFFFFFF), sliderHover: Color(0xFF111111), track: Color(0xFF222222), child: SizedBox.shrink());
|
||||
expect(a.updateShouldNotify(b), isTrue);
|
||||
expect(a.updateShouldNotify(a), isFalse);
|
||||
});
|
||||
@@ -311,10 +280,16 @@ After.
|
||||
|
||||
testWidgets('renders Semantics live region wrapping a TerminalView', (tester) async {
|
||||
final terminal = Terminal(maxLines: 100, onOutput: (_) {});
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(width: 400, height: 200, child: ClidePtyView(terminal: terminal, label: 'test-pane')),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
SizedBox(
|
||||
width: 400,
|
||||
height: 200,
|
||||
child: ClidePtyView(terminal: terminal, label: 'test-pane'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ClidePtyView), findsOneWidget);
|
||||
// The TerminalView inside renders the actual viewport.
|
||||
|
||||
@@ -2,13 +2,7 @@ import 'package:clide/widgets/src/multitab_controller.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
MultitabEntry<String> entry(String id, {bool closeable = true, bool reorderable = true}) {
|
||||
return MultitabEntry<String>(
|
||||
id: id,
|
||||
title: id,
|
||||
payload: id,
|
||||
closeable: closeable,
|
||||
reorderable: reorderable,
|
||||
);
|
||||
return MultitabEntry<String>(id: id, title: id, payload: id, closeable: closeable, reorderable: reorderable);
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -94,31 +88,20 @@ void main() {
|
||||
});
|
||||
|
||||
test('reorder no-ops on non-reorderable entries', () {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
]);
|
||||
final c = MultitabController<String>(initial: [entry('p', reorderable: false), entry('a')]);
|
||||
c.reorder('p', 1);
|
||||
expect(c.entries.map((e) => e.id), ['p', 'a']);
|
||||
});
|
||||
|
||||
test('reorder cannot move a tab past a pinned barrier', () {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
entry('b'),
|
||||
]);
|
||||
final c = MultitabController<String>(initial: [entry('p', reorderable: false), entry('a'), entry('b')]);
|
||||
// 'a' tries to land at index 0 — blocked by pinned 'p'.
|
||||
c.reorder('a', 0);
|
||||
expect(c.entries.map((e) => e.id), ['p', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('reorder respects barriers on the right', () {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('a'),
|
||||
entry('b'),
|
||||
entry('p', reorderable: false),
|
||||
]);
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b'), entry('p', reorderable: false)]);
|
||||
// 'a' tries to land past pinned 'p' — clamped to before it.
|
||||
c.reorder('a', 2);
|
||||
expect(c.entries.map((e) => e.id), ['b', 'a', 'p']);
|
||||
@@ -146,10 +129,7 @@ void main() {
|
||||
|
||||
test('replace rejects id changes', () {
|
||||
final c = MultitabController<String>(initial: [entry('a')]);
|
||||
expect(
|
||||
() => c.replace('a', MultitabEntry<String>(id: 'b', title: 'b', payload: 'b')),
|
||||
throwsStateError,
|
||||
);
|
||||
expect(() => c.replace('a', MultitabEntry<String>(id: 'b', title: 'b', payload: 'b')), throwsStateError);
|
||||
});
|
||||
|
||||
test('notifies listeners on every mutation', () {
|
||||
|
||||
@@ -5,18 +5,10 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import '../helpers/kernel_fixture.dart';
|
||||
import '../helpers/widget_harness.dart';
|
||||
|
||||
Finder _closeIcons() => find.byWidgetPredicate(
|
||||
(w) => w is ClideIcon && w.painter is CloseIcon,
|
||||
);
|
||||
Finder _closeIcons() => find.byWidgetPredicate((w) => w is ClideIcon && w.painter is CloseIcon);
|
||||
|
||||
MultitabEntry<String> entry(String id, {bool closeable = true, bool reorderable = true}) {
|
||||
return MultitabEntry<String>(
|
||||
id: id,
|
||||
title: id,
|
||||
payload: id,
|
||||
closeable: closeable,
|
||||
reorderable: reorderable,
|
||||
);
|
||||
return MultitabEntry<String>(id: id, title: id, payload: id, closeable: closeable, reorderable: reorderable);
|
||||
}
|
||||
|
||||
Widget body(BuildContext _, MultitabEntry<String> e) => SizedBox(key: ValueKey('body-${e.id}'), child: Text('body:${e.payload}'));
|
||||
@@ -39,12 +31,7 @@ class _CountingBodyState extends State<_CountingBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
key: ValueKey('counting-${widget.id}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _bump,
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
return GestureDetector(key: ValueKey('counting-${widget.id}'), behavior: HitTestBehavior.opaque, onTap: _bump, child: const SizedBox.expand());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +42,8 @@ void main() {
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
testWidgets('renders one tab per entry and the active body', (tester) async {
|
||||
final c = MultitabController<String>(
|
||||
initial: [entry('primary', closeable: false), entry('s1'), entry('s2')],
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
final c = MultitabController<String>(initial: [entry('primary', closeable: false), entry('s1'), entry('s2')]);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
|
||||
expect(find.text('primary'), findsOneWidget);
|
||||
expect(find.text('s1'), findsOneWidget);
|
||||
@@ -72,9 +55,7 @@ void main() {
|
||||
|
||||
testWidgets('tapping a tab activates it and swaps the body', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b')]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
expect(find.byKey(const ValueKey('body-a')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('b'));
|
||||
@@ -88,16 +69,7 @@ void main() {
|
||||
testWidgets('add button calls onAddRequested when set', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a')]);
|
||||
var added = 0;
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
MultitabPane<String>(
|
||||
controller: c,
|
||||
bodyBuilder: body,
|
||||
onAddRequested: () => added++,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body, onAddRequested: () => added++)));
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('New tab'));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -106,19 +78,13 @@ void main() {
|
||||
|
||||
testWidgets('add button is absent when onAddRequested is null', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a')]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
expect(find.bySemanticsLabel('New tab'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('non-closeable tabs do not render a close glyph', (tester) async {
|
||||
final c = MultitabController<String>(
|
||||
initial: [entry('p', closeable: false), entry('s')],
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
final c = MultitabController<String>(initial: [entry('p', closeable: false), entry('s')]);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
// A pinned tab has no close target; a closeable one does (it's
|
||||
// hidden via Opacity until hover, but still in the tree).
|
||||
// Two tabs total, one × glyph for 's'.
|
||||
@@ -126,12 +92,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('default close behavior removes the entry', (tester) async {
|
||||
final c = MultitabController<String>(
|
||||
initial: [entry('p', closeable: false), entry('s')],
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
final c = MultitabController<String>(initial: [entry('p', closeable: false), entry('s')]);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
|
||||
await tester.tap(_closeIcons());
|
||||
await tester.pumpAndSettle();
|
||||
@@ -142,16 +104,7 @@ void main() {
|
||||
testWidgets('onCloseRequested overrides default removal', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b')]);
|
||||
MultitabEntry<String>? closed;
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
MultitabPane<String>(
|
||||
controller: c,
|
||||
bodyBuilder: body,
|
||||
onCloseRequested: (e) => closed = e,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body, onCloseRequested: (e) => closed = e)));
|
||||
|
||||
// Both tabs are closeable; tap the first × encountered.
|
||||
await tester.tap(_closeIcons().first);
|
||||
@@ -164,9 +117,7 @@ void main() {
|
||||
|
||||
testWidgets('rebuilds when the controller notifies', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a')]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
expect(find.bySemanticsLabel('b'), findsNothing);
|
||||
|
||||
c.add(entry('b'));
|
||||
@@ -176,17 +127,13 @@ void main() {
|
||||
|
||||
testWidgets('empty controller renders no body', (tester) async {
|
||||
final c = MultitabController<String>();
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
expect(find.byKey(const ValueKey('body-a')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('drag a tab onto another to reorder', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b'), entry('c')]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
|
||||
// Drag tab 'a' to where tab 'c' sits.
|
||||
final from = tester.getCenter(find.text('a'));
|
||||
@@ -202,14 +149,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('drag respects pinned barrier', (tester) async {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
entry('b'),
|
||||
]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
final c = MultitabController<String>(initial: [entry('p', reorderable: false), entry('a'), entry('b')]);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
|
||||
// Try to drag 'a' before pinned 'p' — controller's barrier
|
||||
// logic should reject and the order stays.
|
||||
@@ -226,13 +167,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('pinned tabs are not draggable', (tester) async {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
final c = MultitabController<String>(initial: [entry('p', reorderable: false), entry('a')]);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)));
|
||||
|
||||
// Attempt to drag pinned 'p' to position of 'a'.
|
||||
final from = tester.getCenter(find.text('p'));
|
||||
@@ -302,16 +238,7 @@ void main() {
|
||||
|
||||
testWidgets('allowReorder=false disables drag entirely', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b')]);
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
MultitabPane<String>(
|
||||
controller: c,
|
||||
bodyBuilder: body,
|
||||
allowReorder: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, MultitabPane<String>(controller: c, bodyBuilder: body, allowReorder: false)));
|
||||
|
||||
final from = tester.getCenter(find.text('a'));
|
||||
final to = tester.getCenter(find.text('b'));
|
||||
|
||||
@@ -19,11 +19,15 @@ void main() {
|
||||
setUp(() async {
|
||||
f = await KernelFixture.create();
|
||||
f.ipc.stub(
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.ok(id: '1', data: const {
|
||||
'files': ['lib/main.dart', 'lib/app.dart', 'README.md'],
|
||||
'truncated': false,
|
||||
}));
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.ok(
|
||||
id: '1',
|
||||
data: const {
|
||||
'files': ['lib/main.dart', 'lib/app.dart', 'README.md'],
|
||||
'truncated': false,
|
||||
},
|
||||
),
|
||||
);
|
||||
f.ipc.stub('editor.open', (args) async => IpcResponse.ok(id: '1', data: {'path': args['path']}));
|
||||
});
|
||||
tearDown(() => f.dispose());
|
||||
@@ -102,11 +106,15 @@ void main() {
|
||||
|
||||
testWidgets('truncated walk surfaces the limited-results hint', (tester) async {
|
||||
f.ipc.stub(
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.ok(id: '1', data: const {
|
||||
'files': ['lib/main.dart'],
|
||||
'truncated': true,
|
||||
}));
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.ok(
|
||||
id: '1',
|
||||
data: const {
|
||||
'files': ['lib/main.dart'],
|
||||
'truncated': true,
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
@@ -151,11 +159,12 @@ void main() {
|
||||
|
||||
testWidgets('files.walk failure leaves the list empty (no crash)', (tester) async {
|
||||
f.ipc.stub(
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '1',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'),
|
||||
));
|
||||
'files.walk',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '1',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const QuickOpenOverlay()));
|
||||
f.services.quickOpen.open();
|
||||
await pumpAsync(tester);
|
||||
|
||||
@@ -21,7 +21,7 @@ void main() {
|
||||
f,
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: present,
|
||||
builder: (_, show, __) => show
|
||||
builder: (_, show, _) => show
|
||||
? Align(
|
||||
alignment: Alignment.center,
|
||||
child: ClideAnchoredOverlay(
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
centered: centered,
|
||||
autoFlip: false,
|
||||
anchor: const SizedBox(width: 60, height: 24, child: ClideText('anchor')),
|
||||
overlayBuilder: (_, __) => const SizedBox(width: 120, height: 60, child: ClideText('panel')),
|
||||
overlayBuilder: (_, _) => const SizedBox(width: 120, height: 60, child: ClideText('panel')),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
@@ -143,11 +143,7 @@ void main() {
|
||||
addTearDown(c.dispose);
|
||||
var picked = '';
|
||||
// Right-edge trigger + end alignment — the menu extends left, on-screen.
|
||||
await tester.pumpWidget(anchoredHarness(
|
||||
f,
|
||||
anchoredMenu(c, (v) => picked = v, align: ClideAnchorAlign.end),
|
||||
alignment: Alignment.topRight,
|
||||
));
|
||||
await tester.pumpWidget(anchoredHarness(f, anchoredMenu(c, (v) => picked = v, align: ClideAnchorAlign.end), alignment: Alignment.topRight));
|
||||
c.open();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('C'));
|
||||
@@ -158,11 +154,7 @@ void main() {
|
||||
testWidgets('autoFlip flips below to above when the anchor is near the bottom', (tester) async {
|
||||
final c = ClideOverlayController();
|
||||
addTearDown(c.dispose);
|
||||
await tester.pumpWidget(anchoredHarness(
|
||||
f,
|
||||
anchoredMenu(c, (_) {}, side: ClideAnchorSide.below, autoFlip: true),
|
||||
alignment: Alignment.bottomLeft,
|
||||
));
|
||||
await tester.pumpWidget(anchoredHarness(f, anchoredMenu(c, (_) {}, side: ClideAnchorSide.below, autoFlip: true), alignment: Alignment.bottomLeft));
|
||||
c.open();
|
||||
await tester.pumpAndSettle();
|
||||
// The panel (its first item) sits ABOVE the trigger, not below.
|
||||
|
||||
@@ -18,71 +18,38 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('renders the label text', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideButton(label: 'Save', onPressed: null)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideButton(label: 'Save', onPressed: null)));
|
||||
expect(find.text('Save'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('emits a Semantics node with button: true + label', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, ClideButton(label: 'Commit', onPressed: () {})),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideButton(label: 'Commit', onPressed: () {})));
|
||||
final semantics = tester.getSemantics(find.byType(ClideButton));
|
||||
expect(semantics.label, 'Commit');
|
||||
expect(
|
||||
semantics.getSemanticsData().hasAction(SemanticsAction.tap),
|
||||
isTrue,
|
||||
reason: 'enabled button must expose tap action',
|
||||
);
|
||||
expect(semantics.getSemanticsData().hasAction(SemanticsAction.tap), isTrue, reason: 'enabled button must expose tap action');
|
||||
});
|
||||
|
||||
testWidgets('semanticLabel overrides the visible label for a11y', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideButton(
|
||||
label: 'Save',
|
||||
semanticLabel: 'Save document',
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideButton(label: 'Save', semanticLabel: 'Save document', onPressed: () {})));
|
||||
final semantics = tester.getSemantics(find.byType(ClideButton));
|
||||
expect(semantics.label, 'Save document');
|
||||
});
|
||||
|
||||
testWidgets('semanticHint propagates to the Semantics node', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideButton(
|
||||
label: 'Pick',
|
||||
semanticHint: 'Select a theme',
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideButton(label: 'Pick', semanticHint: 'Select a theme', onPressed: () {})));
|
||||
final semantics = tester.getSemantics(find.byType(ClideButton));
|
||||
expect(semantics.hint, 'Select a theme');
|
||||
});
|
||||
|
||||
testWidgets('disabled button drops the tap action', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideButton(label: 'Nope', onPressed: null)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideButton(label: 'Nope', onPressed: null)));
|
||||
final semantics = tester.getSemantics(find.byType(ClideButton));
|
||||
expect(semantics.getSemanticsData().hasAction(SemanticsAction.tap), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('tap invokes onPressed', (tester) async {
|
||||
var pressed = 0;
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideButton(label: 'Go', onPressed: () => pressed++),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideButton(label: 'Go', onPressed: () => pressed++)));
|
||||
await tester.tap(find.byType(ClideButton));
|
||||
expect(pressed, 1);
|
||||
});
|
||||
|
||||
@@ -27,24 +27,26 @@ void main() {
|
||||
Color? color,
|
||||
bool expanded = false,
|
||||
}) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: ClideCollapserCard(
|
||||
label: label,
|
||||
collapsedSummary: summary,
|
||||
counter: counter,
|
||||
status: status,
|
||||
color: color,
|
||||
initiallyExpanded: expanded,
|
||||
children: children ?? const [Text('item body', textDirection: TextDirection.ltr)],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: ClideCollapserCard(
|
||||
label: label,
|
||||
collapsedSummary: summary,
|
||||
counter: counter,
|
||||
status: status,
|
||||
color: color,
|
||||
initiallyExpanded: expanded,
|
||||
children: children ?? const [Text('item body', textDirection: TextDirection.ltr)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
@@ -70,7 +72,12 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('a single item still sits in the list (1-item collapser)', (tester) async {
|
||||
await pump(tester, counter: '1 edit', children: const [Text('only item', textDirection: TextDirection.ltr)], expanded: true);
|
||||
await pump(
|
||||
tester,
|
||||
counter: '1 edit',
|
||||
children: const [Text('only item', textDirection: TextDirection.ltr)],
|
||||
expanded: true,
|
||||
);
|
||||
expect(find.text('only item'), findsOneWidget);
|
||||
expect(find.text('1 edit'), findsOneWidget);
|
||||
});
|
||||
@@ -98,15 +105,19 @@ void main() {
|
||||
|
||||
testWidgets('a deeper interactive control inside an item still fires (not swallowed)', (tester) async {
|
||||
var tapped = false;
|
||||
await pump(tester, expanded: true, children: [
|
||||
ClideTappable(
|
||||
onTap: () => tapped = true,
|
||||
builder: (_, __, ___) => const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Text('press me', textDirection: TextDirection.ltr),
|
||||
await pump(
|
||||
tester,
|
||||
expanded: true,
|
||||
children: [
|
||||
ClideTappable(
|
||||
onTap: () => tapped = true,
|
||||
builder: (_, _, _) => const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Text('press me', textDirection: TextDirection.ltr),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
await tester.tap(find.text('press me'));
|
||||
await tester.pump();
|
||||
expect(tapped, isTrue); // the item's own control won the hit
|
||||
@@ -115,9 +126,7 @@ void main() {
|
||||
|
||||
testWidgets('the ticker is keyboard-focusable and toggles on Activate (a11y)', (tester) async {
|
||||
await pump(tester);
|
||||
final focusWidget = tester.widget<Focus>(
|
||||
find.ancestor(of: find.text('clide_markdown.dart'), matching: find.byType(Focus)).first,
|
||||
);
|
||||
final focusWidget = tester.widget<Focus>(find.ancestor(of: find.text('clide_markdown.dart'), matching: find.byType(Focus)).first);
|
||||
focusWidget.focusNode!.requestFocus();
|
||||
await tester.pump();
|
||||
expect(focusWidget.focusNode!.hasFocus, isTrue);
|
||||
|
||||
@@ -19,9 +19,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('vertical axis yields a width-constrained container', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideDivider(axis: Axis.vertical, thickness: 2)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideDivider(axis: Axis.vertical, thickness: 2)));
|
||||
final c = tester.widget<Container>(find.byType(Container));
|
||||
expect(c.constraints?.maxWidth, 2.0);
|
||||
});
|
||||
|
||||
@@ -12,9 +12,7 @@ void main() {
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
testWidgets('sizes a SizedBox + CustomPaint to the given size', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideIcon(FolderIcon(), size: 24)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideIcon(FolderIcon(), size: 24)));
|
||||
final sb = tester.widget<SizedBox>(find.byType(SizedBox).first);
|
||||
expect(sb.width, 24);
|
||||
expect(sb.height, 24);
|
||||
@@ -22,9 +20,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('default color is globalForeground', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideIcon(CheckIcon())),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideIcon(CheckIcon())));
|
||||
// Color is inaccessible after painting; ensure it renders without crashing.
|
||||
expect(find.byType(ClideIcon), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -17,21 +17,23 @@ void main() {
|
||||
// Resolves only the one known repo path; everything else is "not a file".
|
||||
String? resolve(String p) => p == 'lib/app.dart' ? '/repo/lib/app.dart' : null;
|
||||
|
||||
ClideMarkdown md(String src, {void Function(String, int?)? onOpen}) => ClideMarkdown(
|
||||
src,
|
||||
resolveFileRef: resolve,
|
||||
onOpenFile: onOpen ?? (_, __) {},
|
||||
);
|
||||
ClideMarkdown md(String src, {void Function(String, int?)? onOpen}) => ClideMarkdown(src, resolveFileRef: resolve, onOpenFile: onOpen ?? (_, _) {});
|
||||
|
||||
testWidgets('a bare repo path in prose is tappable and opens with no line', (tester) async {
|
||||
String? path;
|
||||
int? line = -1;
|
||||
await tester.pumpWidget(harness(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
md('see lib/app.dart for the entry point', onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
})));
|
||||
md(
|
||||
'see lib/app.dart for the entry point',
|
||||
onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('lib/app.dart'));
|
||||
@@ -43,12 +45,18 @@ void main() {
|
||||
testWidgets('a path:line ref opens at the line', (tester) async {
|
||||
String? path;
|
||||
int? line;
|
||||
await tester.pumpWidget(harness(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
md('crash at lib/app.dart:42 today', onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
})));
|
||||
md(
|
||||
'crash at lib/app.dart:42 today',
|
||||
onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('lib/app.dart:42'));
|
||||
@@ -70,12 +78,18 @@ void main() {
|
||||
testWidgets('a backticked path is clickable', (tester) async {
|
||||
String? path;
|
||||
int? line;
|
||||
await tester.pumpWidget(harness(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
md('open `lib/app.dart:7`', onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
})));
|
||||
md(
|
||||
'open `lib/app.dart:7`',
|
||||
onOpen: (p, l) {
|
||||
path = p;
|
||||
line = l;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('lib/app.dart:7'));
|
||||
@@ -96,7 +110,7 @@ void main() {
|
||||
|
||||
testWidgets('a path that does not resolve stays literal (no link, no fire)', (tester) async {
|
||||
var calls = 0;
|
||||
await tester.pumpWidget(harness(f, md('nothing at lib/ghost.dart here', onOpen: (_, __) => calls++)));
|
||||
await tester.pumpWidget(harness(f, md('nothing at lib/ghost.dart here', onOpen: (_, _) => calls++)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Rendered as plain text — tapping it does nothing.
|
||||
@@ -107,7 +121,7 @@ void main() {
|
||||
|
||||
testWidgets('dotted prose (a version number) does not linkify', (tester) async {
|
||||
var calls = 0;
|
||||
await tester.pumpWidget(harness(f, md('shipped version 2.2.0 today', onOpen: (_, __) => calls++)));
|
||||
await tester.pumpWidget(harness(f, md('shipped version 2.2.0 today', onOpen: (_, _) => calls++)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.textContaining('2.2.0'), warnIfMissed: false);
|
||||
|
||||
@@ -9,16 +9,13 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../helpers/widget_harness.dart';
|
||||
|
||||
Widget _boxed(double width, Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: SizedBox(width: width, height: 16, child: child),
|
||||
),
|
||||
);
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: SizedBox(width: width, height: 16, child: child),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _reducedMotion(double width, Widget child) => MediaQuery(
|
||||
data: const MediaQueryData(disableAnimations: true),
|
||||
child: _boxed(width, child),
|
||||
);
|
||||
Widget _reducedMotion(double width, Widget child) => MediaQuery(data: const MediaQueryData(disableAnimations: true), child: _boxed(width, child));
|
||||
|
||||
void main() {
|
||||
testWidgets('shows the child statically when it fits', (tester) async {
|
||||
|
||||
@@ -59,17 +59,19 @@ void main() {
|
||||
testWidgets('renders items + separator; tapping an item fires onSelect + onClose', (tester) async {
|
||||
var picked = '';
|
||||
var closed = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () => closed++,
|
||||
entries: [
|
||||
ClideMenuItem(label: 'Alpha', onSelect: () => picked = 'Alpha'),
|
||||
const ClideMenuSeparator(),
|
||||
ClideMenuItem(label: 'Beta', onSelect: () => picked = 'Beta'),
|
||||
],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () => closed++,
|
||||
entries: [
|
||||
ClideMenuItem(label: 'Alpha', onSelect: () => picked = 'Alpha'),
|
||||
const ClideMenuSeparator(),
|
||||
ClideMenuItem(label: 'Beta', onSelect: () => picked = 'Beta'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('Alpha'), findsOneWidget);
|
||||
expect(find.text('Beta'), findsOneWidget);
|
||||
@@ -81,16 +83,18 @@ void main() {
|
||||
|
||||
testWidgets('arrow-down then Enter activates the highlighted item', (tester) async {
|
||||
var picked = '';
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () {},
|
||||
entries: [
|
||||
ClideMenuItem(label: 'One', onSelect: () => picked = 'One'),
|
||||
ClideMenuItem(label: 'Two', onSelect: () => picked = 'Two'),
|
||||
],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () {},
|
||||
entries: [
|
||||
ClideMenuItem(label: 'One', onSelect: () => picked = 'One'),
|
||||
ClideMenuItem(label: 'Two', onSelect: () => picked = 'Two'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
@@ -101,17 +105,19 @@ void main() {
|
||||
|
||||
testWidgets('disabled item is inert and skipped by arrows', (tester) async {
|
||||
var picked = '';
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () {},
|
||||
entries: [
|
||||
ClideMenuItem(label: 'Able', onSelect: () => picked = 'Able'),
|
||||
ClideMenuItem(label: 'Off', enabled: false, onSelect: () => picked = 'Off'),
|
||||
ClideMenuItem(label: 'Last', onSelect: () => picked = 'Last'),
|
||||
],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () {},
|
||||
entries: [
|
||||
ClideMenuItem(label: 'Able', onSelect: () => picked = 'Able'),
|
||||
ClideMenuItem(label: 'Off', enabled: false, onSelect: () => picked = 'Off'),
|
||||
ClideMenuItem(label: 'Last', onSelect: () => picked = 'Last'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Off'));
|
||||
await tester.pump();
|
||||
@@ -125,7 +131,15 @@ void main() {
|
||||
|
||||
testWidgets('Escape calls onClose', (tester) async {
|
||||
var closed = 0;
|
||||
await tester.pumpWidget(harness(f, ClideMenu(onClose: () => closed++, entries: [ClideMenuItem(label: 'X', onSelect: () {})])));
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () => closed++,
|
||||
entries: [ClideMenuItem(label: 'X', onSelect: () {})],
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pump();
|
||||
@@ -135,13 +149,15 @@ void main() {
|
||||
testWidgets('keepOpenOnSelect item fires onSelect but not onClose', (tester) async {
|
||||
var picked = 0;
|
||||
var closed = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () => closed++,
|
||||
entries: [ClideMenuItem(label: 'Toggle', keepOpenOnSelect: true, onSelect: () => picked++)],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideMenu(
|
||||
onClose: () => closed++,
|
||||
entries: [ClideMenuItem(label: 'Toggle', keepOpenOnSelect: true, onSelect: () => picked++)],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Toggle'));
|
||||
await tester.pump();
|
||||
|
||||
@@ -17,14 +17,16 @@ void main() {
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
testWidgets('conveys its status while its contribution is focused, clears on blur', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
const ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
final focus = f.services.focus;
|
||||
expect(focus.activeStatusWidget, isNull); // not focused yet
|
||||
|
||||
@@ -43,34 +45,38 @@ void main() {
|
||||
// mid-build ("markNeedsBuild during build"). A focus listener must be in
|
||||
// the tree to reproduce it.
|
||||
f.services.focus.setActive(slot: Slots.workspace, contributionId: 'pane.x');
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
const Column(
|
||||
children: [
|
||||
PaneContextStatusItem(),
|
||||
ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
),
|
||||
],
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const Column(
|
||||
children: [
|
||||
PaneContextStatusItem(),
|
||||
ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump(); // run the deferred post-frame convey
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(f.services.focus.activeStatusWidget, isA<Text>());
|
||||
});
|
||||
|
||||
testWidgets('an inactive pane never conveys', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
const ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
active: false,
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
active: false,
|
||||
statusWidget: Text('S', textDirection: TextDirection.ltr),
|
||||
child: SizedBox(),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
f.services.focus.setActive(slot: Slots.workspace, contributionId: 'pane.x');
|
||||
await tester.pump();
|
||||
expect(f.services.focus.activeStatusWidget, isNull);
|
||||
@@ -80,19 +86,21 @@ void main() {
|
||||
f.services.focus.setActive(slot: Slots.workspace, contributionId: 'pane.x');
|
||||
var label = 'A';
|
||||
late StateSetter setLabel;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
StatefulBuilder(
|
||||
builder: (ctx, setState) {
|
||||
setLabel = setState;
|
||||
return ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text(label, textDirection: TextDirection.ltr),
|
||||
child: const SizedBox(),
|
||||
);
|
||||
},
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
StatefulBuilder(
|
||||
builder: (ctx, setState) {
|
||||
setLabel = setState;
|
||||
return ClidePane(
|
||||
contributionId: 'pane.x',
|
||||
statusWidget: Text(label, textDirection: TextDirection.ltr),
|
||||
child: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
expect((f.services.focus.activeStatusWidget! as Text).data, 'A');
|
||||
|
||||
|
||||
@@ -137,12 +137,20 @@ void main() {
|
||||
// Both the paragraph text and the code text should appear in the
|
||||
// clipboard. SelectableRegion concatenates selectables with newlines.
|
||||
final copied = clipboard.text ?? '';
|
||||
expect(copied, contains(paraText),
|
||||
reason: 'paragraph text must be selectable via SelectableRegion after '
|
||||
'converting ClideMarkdown from RichText to Text.rich');
|
||||
expect(copied, contains(codeText),
|
||||
reason: 'code block text must be selectable via SelectableRegion after '
|
||||
'converting ClideCodeBlock from RichText to Text.rich');
|
||||
expect(
|
||||
copied,
|
||||
contains(paraText),
|
||||
reason:
|
||||
'paragraph text must be selectable via SelectableRegion after '
|
||||
'converting ClideMarkdown from RichText to Text.rich',
|
||||
);
|
||||
expect(
|
||||
copied,
|
||||
contains(codeText),
|
||||
reason:
|
||||
'code block text must be selectable via SelectableRegion after '
|
||||
'converting ClideCodeBlock from RichText to Text.rich',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Text.rich nodes are owned by Text widgets, registering with SelectableRegion', (tester) async {
|
||||
@@ -185,13 +193,14 @@ void main() {
|
||||
// what Text.rich builds. A bare RichText(...) has no Text parent.
|
||||
final richTexts = find.byType(RichText);
|
||||
for (final el in richTexts.evaluate()) {
|
||||
final textAncestor = find.ancestor(
|
||||
of: find.byElementPredicate((e) => e == el),
|
||||
matching: find.byType(Text),
|
||||
final textAncestor = find.ancestor(of: find.byElementPredicate((e) => e == el), matching: find.byType(Text));
|
||||
expect(
|
||||
textAncestor,
|
||||
findsAtLeastNWidgets(1),
|
||||
reason:
|
||||
'RichText for "${(el.widget as RichText).text.toPlainText()}" '
|
||||
'should be owned by a Text (built via Text.rich, not bare RichText)',
|
||||
);
|
||||
expect(textAncestor, findsAtLeastNWidgets(1),
|
||||
reason: 'RichText for "${(el.widget as RichText).text.toPlainText()}" '
|
||||
'should be owned by a Text (built via Text.rich, not bare RichText)');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,15 +17,17 @@ void main() {
|
||||
// Disable animations so the spinner is static (no perpetual rotation) and
|
||||
// pumpAndSettle can settle — mirroring how every animated widget behaves
|
||||
// under reduced motion.
|
||||
Future<void> pump(WidgetTester tester, Widget child) => tester.pumpWidget(harness(
|
||||
f,
|
||||
Builder(
|
||||
builder: (ctx) => MediaQuery(
|
||||
data: MediaQuery.of(ctx).copyWith(disableAnimations: true),
|
||||
child: Center(child: child),
|
||||
),
|
||||
Future<void> pump(WidgetTester tester, Widget child) => tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Builder(
|
||||
builder: (ctx) => MediaQuery(
|
||||
data: MediaQuery.of(ctx).copyWith(disableAnimations: true),
|
||||
child: Center(child: child),
|
||||
),
|
||||
));
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Finder iconWith(Object painterType) => find.byWidgetPredicate((w) => w is ClideIcon && w.painter.runtimeType == painterType);
|
||||
|
||||
@@ -64,15 +66,19 @@ void main() {
|
||||
// 'running' key. Pre-fix this threw "Duplicate keys found".
|
||||
var status = ClideRunStatus.running;
|
||||
late StateSetter set;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Center(
|
||||
child: StatefulBuilder(builder: (ctx, s) {
|
||||
set = s;
|
||||
return ClideStatusIndicator(status: status);
|
||||
}),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Center(
|
||||
child: StatefulBuilder(
|
||||
builder: (ctx, s) {
|
||||
set = s;
|
||||
return ClideStatusIndicator(status: status);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
|
||||
void flip(ClideRunStatus next) => set(() => status = next);
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ void main() {
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
testWidgets('default background comes from panelBackground token', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideSurface(child: Text('x'))),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideSurface(child: Text('x'))));
|
||||
final container = tester.widget<Container>(find.byType(Container));
|
||||
final decoration = container.decoration as BoxDecoration;
|
||||
expect(decoration.color, f.services.theme.current.surface.panelBackground);
|
||||
@@ -23,24 +21,14 @@ void main() {
|
||||
|
||||
testWidgets('explicit color overrides the token default', (tester) async {
|
||||
const custom = Color(0xFF123456);
|
||||
await tester.pumpWidget(
|
||||
harness(f, const ClideSurface(color: custom, child: Text('x'))),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, const ClideSurface(color: custom, child: Text('x'))));
|
||||
final container = tester.widget<Container>(find.byType(Container));
|
||||
final decoration = container.decoration as BoxDecoration;
|
||||
expect(decoration.color, custom);
|
||||
});
|
||||
|
||||
testWidgets('border when provided renders a BoxBorder', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideSurface(
|
||||
border: f.services.theme.current.surface.modalSurfaceBorder,
|
||||
child: const Text('x'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, ClideSurface(border: f.services.theme.current.surface.modalSurfaceBorder, child: const Text('x'))));
|
||||
final container = tester.widget<Container>(find.byType(Container));
|
||||
final decoration = container.decoration as BoxDecoration;
|
||||
expect(decoration.border, isNotNull);
|
||||
|
||||
@@ -64,23 +64,11 @@ void main() {
|
||||
final handle = tester.ensureSemantics();
|
||||
expect(
|
||||
tester.getSemantics(find.bySemanticsLabel('Files')),
|
||||
matchesSemantics(
|
||||
label: 'Files',
|
||||
isButton: true,
|
||||
isSelected: true,
|
||||
hasSelectedState: true,
|
||||
hasTapAction: true,
|
||||
),
|
||||
matchesSemantics(label: 'Files', isButton: true, isSelected: true, hasSelectedState: true, hasTapAction: true),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.bySemanticsLabel('Git')),
|
||||
matchesSemantics(
|
||||
label: 'Git',
|
||||
isButton: true,
|
||||
isSelected: false,
|
||||
hasSelectedState: true,
|
||||
hasTapAction: true,
|
||||
),
|
||||
matchesSemantics(label: 'Git', isButton: true, isSelected: false, hasSelectedState: true, hasTapAction: true),
|
||||
);
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
@@ -23,16 +23,14 @@ void main() {
|
||||
var tapped = 0;
|
||||
final node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(
|
||||
focusNode: node,
|
||||
onTap: () => tapped++,
|
||||
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(focusNode: node, onTap: () => tapped++, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
|
||||
expect(node.hasFocus, isFalse);
|
||||
node.requestFocus();
|
||||
@@ -46,17 +44,19 @@ void main() {
|
||||
var tapped = 0;
|
||||
final node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(
|
||||
focusNode: node,
|
||||
autofocus: true,
|
||||
onTap: () => tapped++,
|
||||
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(
|
||||
focusNode: node,
|
||||
autofocus: true,
|
||||
onTap: () => tapped++,
|
||||
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
expect(node.hasFocus, isTrue);
|
||||
// Dispatch ActivateIntent directly against the focused context
|
||||
@@ -70,16 +70,14 @@ void main() {
|
||||
testWidgets('disabled tappable (onTap == null) cannot receive focus', (tester) async {
|
||||
final node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(
|
||||
focusNode: node,
|
||||
onTap: null,
|
||||
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(focusNode: node, onTap: null, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
expect(node.hasFocus, isFalse, reason: 'canRequestFocus is false when onTap is null');
|
||||
@@ -88,16 +86,14 @@ void main() {
|
||||
testWidgets('focus ring appears when focused, gone when unfocused', (tester) async {
|
||||
final node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(
|
||||
focusNode: node,
|
||||
onTap: () {},
|
||||
builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Center(
|
||||
child: ClideTappable(focusNode: node, onTap: () {}, builder: (_, hovered, pressed) => const SizedBox(width: 60, height: 24)),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
|
||||
Iterable<Color> ringColors() => tester
|
||||
.widgetList<DecoratedBox>(find.descendant(of: find.byType(ClideTappable), matching: find.byType(DecoratedBox)))
|
||||
|
||||
@@ -17,22 +17,26 @@ void main() {
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
Widget host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ClideKernel(
|
||||
services: f.services,
|
||||
child: ClideTheme(
|
||||
controller: f.services.theme,
|
||||
child: Align(alignment: Alignment.topLeft, child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ClideKernel(
|
||||
services: f.services,
|
||||
child: ClideTheme(
|
||||
controller: f.services.theme,
|
||||
child: Align(alignment: Alignment.topLeft, child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('renders the message and calls onDismiss when the × is tapped', (tester) async {
|
||||
var dismissed = false;
|
||||
await tester.pumpWidget(host(ClideToast(
|
||||
entry: const ToastEntry(id: 1, message: 'Pushed to origin/main', severity: ToastSeverity.success),
|
||||
onDismiss: () => dismissed = true,
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
ClideToast(
|
||||
entry: const ToastEntry(id: 1, message: 'Pushed to origin/main', severity: ToastSeverity.success),
|
||||
onDismiss: () => dismissed = true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 300)); // settle entrance
|
||||
|
||||
expect(find.text('Pushed to origin/main'), findsOneWidget);
|
||||
@@ -43,26 +47,23 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('exposes the message as a live region (a11y)', (tester) async {
|
||||
await tester.pumpWidget(host(ClideToast(
|
||||
entry: const ToastEntry(id: 1, message: 'Heads up', severity: ToastSeverity.warning),
|
||||
onDismiss: () {},
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
ClideToast(
|
||||
entry: const ToastEntry(id: 1, message: 'Heads up', severity: ToastSeverity.warning),
|
||||
onDismiss: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
// The message is wrapped in a live-region Semantics so screen readers
|
||||
// announce it when it appears.
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.liveRegion == true && w.properties.label == 'Heads up'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.liveRegion == true && w.properties.label == 'Heads up'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('overlay renders a card per queued toast and dismiss removes one', (tester) async {
|
||||
await tester.pumpWidget(host(const SizedBox(
|
||||
width: 800,
|
||||
height: 600,
|
||||
child: Stack(children: [ToastOverlay()]),
|
||||
)));
|
||||
await tester.pumpWidget(host(const SizedBox(width: 800, height: 600, child: Stack(children: [ToastOverlay()]))));
|
||||
await tester.pump();
|
||||
expect(find.byType(ClideToast), findsNothing);
|
||||
|
||||
|
||||
@@ -112,12 +112,7 @@ void main() {
|
||||
|
||||
// The Positioned ancestor of the tooltip uses `bottom:` (above-mode),
|
||||
// not `top:`, when there isn't enough room below.
|
||||
final positioned = tester.widget<Positioned>(
|
||||
find.ancestor(
|
||||
of: find.text('above'),
|
||||
matching: find.byType(Positioned),
|
||||
),
|
||||
);
|
||||
final positioned = tester.widget<Positioned>(find.ancestor(of: find.text('above'), matching: find.byType(Positioned)));
|
||||
expect(positioned.bottom, isNotNull);
|
||||
expect(positioned.top, isNull);
|
||||
});
|
||||
|
||||
@@ -19,21 +19,23 @@ void main() {
|
||||
var picked = '';
|
||||
List<String> sugg = ['alice', 'bob'];
|
||||
late StateSetter setOuter;
|
||||
await tester.pumpWidget(anchoredHarness(
|
||||
f,
|
||||
StatefulBuilder(
|
||||
builder: (ctx, setState) {
|
||||
setOuter = setState;
|
||||
return ClideTypeahead(
|
||||
suggestions: sugg,
|
||||
onSelect: (v) => picked = v,
|
||||
formatLabel: (n) => '@$n',
|
||||
child: const SizedBox(width: 200, height: 24, child: ClideText('field')),
|
||||
);
|
||||
},
|
||||
await tester.pumpWidget(
|
||||
anchoredHarness(
|
||||
f,
|
||||
StatefulBuilder(
|
||||
builder: (ctx, setState) {
|
||||
setOuter = setState;
|
||||
return ClideTypeahead(
|
||||
suggestions: sugg,
|
||||
onSelect: (v) => picked = v,
|
||||
formatLabel: (n) => '@$n',
|
||||
child: const SizedBox(width: 200, height: 24, child: ClideText('field')),
|
||||
);
|
||||
},
|
||||
),
|
||||
alignment: Alignment.topLeft,
|
||||
),
|
||||
alignment: Alignment.topLeft,
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('@alice'), findsOneWidget);
|
||||
expect(find.text('@bob'), findsOneWidget);
|
||||
|
||||
@@ -34,19 +34,23 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('opens to show an input field + the registered commands', (tester) async {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'cmd.one',
|
||||
title: 'Command One',
|
||||
defaultBinding: 'ctrl+1',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'cmd.two',
|
||||
title: 'Command Two',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'cmd.one',
|
||||
title: 'Command One',
|
||||
defaultBinding: 'ctrl+1',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'cmd.two',
|
||||
title: 'Command Two',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -57,15 +61,17 @@ void main() {
|
||||
|
||||
testWidgets('tapping a row invokes the command + closes the palette', (tester) async {
|
||||
var invocations = 0;
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'tap.target',
|
||||
title: 'Tap Target',
|
||||
run: (_) async {
|
||||
invocations++;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'tap.target',
|
||||
title: 'Tap Target',
|
||||
run: (_) async {
|
||||
invocations++;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -75,18 +81,22 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('typing narrows the visible commands via palette.setFilter', (tester) async {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'alpha.cmd',
|
||||
title: 'Alpha',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'beta.cmd',
|
||||
title: 'Beta',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'alpha.cmd',
|
||||
title: 'Alpha',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'beta.cmd',
|
||||
title: 'Beta',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -99,21 +109,25 @@ void main() {
|
||||
|
||||
testWidgets('submitting the input invokes the first filtered command', (tester) async {
|
||||
var invocations = 0;
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'submit.target',
|
||||
title: 'Submit Target',
|
||||
run: (_) async {
|
||||
invocations++;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
));
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'other.cmd',
|
||||
title: 'Other',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'submit.target',
|
||||
title: 'Submit Target',
|
||||
run: (_) async {
|
||||
invocations++;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
),
|
||||
);
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c2',
|
||||
command: 'other.cmd',
|
||||
title: 'Other',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -126,12 +140,14 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('hovering a palette row updates its hover state', (tester) async {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'hover.cmd',
|
||||
title: 'Hoverable',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'hover.cmd',
|
||||
title: 'Hoverable',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -149,12 +165,14 @@ void main() {
|
||||
|
||||
testWidgets('arrow keys move the highlighted command (T-100)', (tester) async {
|
||||
for (final id in ['a', 'b', 'c']) {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: id,
|
||||
command: 'cmd.$id',
|
||||
title: 'Item $id',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: id,
|
||||
command: 'cmd.$id',
|
||||
title: 'Item $id',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
}
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
@@ -181,15 +199,17 @@ void main() {
|
||||
testWidgets('PaletteAcceptIntent invokes the highlighted command (T-100)', (tester) async {
|
||||
var which = '';
|
||||
for (final id in ['a', 'b', 'c']) {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: id,
|
||||
command: 'cmd.$id',
|
||||
title: 'Item $id',
|
||||
run: (_) async {
|
||||
which = id;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: id,
|
||||
command: 'cmd.$id',
|
||||
title: 'Item $id',
|
||||
run: (_) async {
|
||||
which = id;
|
||||
return IpcResponse.ok(id: '', data: const {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
@@ -204,12 +224,14 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('DismissIntent closes the palette (T-100)', (tester) async {
|
||||
f.services.commands.register(CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'thing',
|
||||
title: 'Thing',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
));
|
||||
f.services.commands.register(
|
||||
CommandContribution(
|
||||
id: 'c1',
|
||||
command: 'thing',
|
||||
title: 'Thing',
|
||||
run: (_) async => IpcResponse.ok(id: '', data: const {}),
|
||||
),
|
||||
);
|
||||
f.services.palette.open();
|
||||
await tester.pumpWidget(harness(f, Stack(children: const [ClidePalette()])));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -240,13 +262,7 @@ void main() {
|
||||
|
||||
testWidgets('debounces onChanged calls to the configured duration', (tester) async {
|
||||
final values = <String>[];
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideFilterBox(
|
||||
onChanged: values.add,
|
||||
debounce: const Duration(milliseconds: 50),
|
||||
),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, ClideFilterBox(onChanged: values.add, debounce: const Duration(milliseconds: 50))));
|
||||
// Type into the editable text field.
|
||||
await tester.enterText(find.byType(EditableText), 'abc');
|
||||
// Before the debounce elapses no call.
|
||||
@@ -259,13 +275,7 @@ void main() {
|
||||
|
||||
testWidgets('clear button appears once the field has text and resets onChanged when tapped', (tester) async {
|
||||
final values = <String>[];
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideFilterBox(
|
||||
onChanged: values.add,
|
||||
debounce: const Duration(milliseconds: 10),
|
||||
),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, ClideFilterBox(onChanged: values.add, debounce: const Duration(milliseconds: 10))));
|
||||
// No clear icon when the field is empty.
|
||||
expect(find.byType(GestureDetector), findsNothing);
|
||||
await tester.enterText(find.byType(EditableText), 'foo');
|
||||
@@ -280,13 +290,7 @@ void main() {
|
||||
|
||||
testWidgets('onSubmitted fires when the field is submitted', (tester) async {
|
||||
String? submitted;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideFilterBox(
|
||||
onChanged: (_) {},
|
||||
onSubmitted: (v) => submitted = v,
|
||||
),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, ClideFilterBox(onChanged: (_) {}, onSubmitted: (v) => submitted = v)));
|
||||
await tester.enterText(find.byType(EditableText), 'submit-me');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump();
|
||||
@@ -294,10 +298,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('renders the hint as a placeholder (shown empty, hidden once typed); icon optional', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideFilterBox(onChanged: (_) {}, hint: 'Replace', showIcon: false),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, ClideFilterBox(onChanged: (_) {}, hint: 'Replace', showIcon: false)));
|
||||
// Placeholder visible while empty; the icon slot is kept for alignment but
|
||||
// draws a blank glyph (showIcon: false → EmptyIconPainter).
|
||||
expect(find.text('Replace'), findsOneWidget);
|
||||
@@ -318,14 +319,21 @@ void main() {
|
||||
testWidgets('left / center / right factories render', (tester) async {
|
||||
final wc = WindowControls();
|
||||
addTearDown(wc.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Column(children: [
|
||||
SizedBox(width: 200, child: ColumnHat.left(windowControls: wc)),
|
||||
SizedBox(width: 200, child: ColumnHat.center(windowControls: wc, project: 'clide', branch: 'main')),
|
||||
SizedBox(width: 200, child: ColumnHat.right(windowControls: wc)),
|
||||
]),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(width: 200, child: ColumnHat.left(windowControls: wc)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: ColumnHat.center(windowControls: wc, project: 'clide', branch: 'main'),
|
||||
),
|
||||
SizedBox(width: 200, child: ColumnHat.right(windowControls: wc)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
// Center hat renders the joined label.
|
||||
expect(find.text('clide > main'), findsOneWidget);
|
||||
});
|
||||
@@ -333,10 +341,7 @@ void main() {
|
||||
testWidgets('center hat falls back to "clide" with no project/branch', (tester) async {
|
||||
final wc = WindowControls();
|
||||
addTearDown(wc.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(width: 200, child: ColumnHat.center(windowControls: wc)),
|
||||
));
|
||||
await tester.pumpWidget(harness(f, SizedBox(width: 200, child: ColumnHat.center(windowControls: wc))));
|
||||
expect(find.text('clide'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
@@ -348,17 +353,19 @@ void main() {
|
||||
|
||||
testWidgets('renders one button per item, marks the active one, fires onSelect', (tester) async {
|
||||
String? selected;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideIconRail(
|
||||
items: [
|
||||
ClideIconRailItem(id: 'a', icon: PhosphorIcons.byName('folder'), tooltip: 'A'),
|
||||
ClideIconRailItem(id: 'b', icon: PhosphorIcons.byName('git-branch'), tooltip: 'B'),
|
||||
],
|
||||
activeId: 'a',
|
||||
onSelect: (id) => selected = id,
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideIconRail(
|
||||
items: [
|
||||
ClideIconRailItem(id: 'a', icon: PhosphorIcons.byName('folder'), tooltip: 'A'),
|
||||
ClideIconRailItem(id: 'b', icon: PhosphorIcons.byName('git-branch'), tooltip: 'B'),
|
||||
],
|
||||
activeId: 'a',
|
||||
onSelect: (id) => selected = id,
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// Each item exposes a tooltip-labeled semantics button.
|
||||
expect(find.bySemanticsLabel('A'), findsOneWidget);
|
||||
@@ -368,20 +375,20 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('scrolls instead of overflowing when items exceed the width', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 30,
|
||||
child: ClideIconRail(
|
||||
items: [
|
||||
for (var i = 0; i < 10; i++) ClideIconRailItem(id: '$i', icon: PhosphorIcons.byName('folder'), tooltip: 'Tab $i'),
|
||||
],
|
||||
activeId: '0',
|
||||
onSelect: (_) {},
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 30,
|
||||
child: ClideIconRail(
|
||||
items: [for (var i = 0; i < 10; i++) ClideIconRailItem(id: '$i', icon: PhosphorIcons.byName('folder'), tooltip: 'Tab $i')],
|
||||
activeId: '0',
|
||||
onSelect: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// No RenderFlex overflow (would surface as a thrown FlutterError),
|
||||
// and the rail is horizontally scrollable.
|
||||
@@ -397,17 +404,16 @@ void main() {
|
||||
|
||||
testWidgets('renders the rotated label + a badge dot when badgeCount > 0', (tester) async {
|
||||
var taps = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
SizedBox(
|
||||
height: 200,
|
||||
width: ClideSpine.width,
|
||||
child: ClideSpine(
|
||||
label: 'SIDEBAR',
|
||||
badgeCount: 3,
|
||||
onExpand: () => taps++,
|
||||
)),
|
||||
));
|
||||
child: ClideSpine(label: 'SIDEBAR', badgeCount: 3, onExpand: () => taps++),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('SIDEBAR'), findsOneWidget);
|
||||
// Tap to expand.
|
||||
@@ -416,17 +422,16 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('renders the right-side variant without throwing', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
SizedBox(
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
SizedBox(
|
||||
height: 200,
|
||||
width: ClideSpine.width,
|
||||
child: ClideSpine(
|
||||
label: 'CTX',
|
||||
side: SpineSide.right,
|
||||
onExpand: () {},
|
||||
)),
|
||||
));
|
||||
child: ClideSpine(label: 'CTX', side: SpineSide.right, onExpand: () {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('CTX'), findsOneWidget);
|
||||
});
|
||||
@@ -440,13 +445,15 @@ void main() {
|
||||
testWidgets('wraps a child and renders 8 resize zones', (tester) async {
|
||||
final wc = WindowControls();
|
||||
addTearDown(wc.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClideResizeBorder(
|
||||
windowControls: wc,
|
||||
child: const SizedBox.expand(child: ColoredBox(color: Color(0xFF000000))),
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
ClideResizeBorder(
|
||||
windowControls: wc,
|
||||
child: const SizedBox.expand(child: ColoredBox(color: Color(0xFF000000))),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// 4 corners + 4 edges = 8 zones each with a MouseRegion.
|
||||
expect(find.byType(MouseRegion).evaluate().length, greaterThanOrEqualTo(8));
|
||||
|
||||
Reference in New Issue
Block a user