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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user