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:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+4 -12
View File
@@ -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.
+7 -40
View File
@@ -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);
});
+36 -27
View File
@@ -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);
+1 -3
View File
@@ -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);
});
+2 -6
View File
@@ -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);
+6 -9
View File
@@ -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 {
+52 -36
View File
@@ -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();
+47 -39
View File
@@ -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');
+21 -12
View File
@@ -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);
+3 -15
View File
@@ -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);
+2 -14
View File
@@ -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();
});
+29 -33
View File
@@ -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)))
+27 -26
View File
@@ -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);
+1 -6
View File
@@ -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);
});
+16 -14
View File
@@ -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);