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
+5 -23
View File
@@ -80,31 +80,19 @@ void main() {
tearDown(() => kb.clearState());
test('returns null for KeyUpEvent', () {
final up = KeyUpEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
final up = KeyUpEvent(physicalKey: PhysicalKeyboardKey.keyG, logicalKey: LogicalKeyboardKey.keyG, timeStamp: Duration.zero);
expect(KeybindingResolver.fromKeyEvent(up, kb), isNull);
});
test('returns null when logicalKey has no keyLabel', () {
// A synthetic logical key with an unassigned id has an empty label.
final unlabeled = LogicalKeyboardKey(0x1000fffff);
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.controlLeft,
logicalKey: unlabeled,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.controlLeft, logicalKey: unlabeled, timeStamp: Duration.zero);
expect(KeybindingResolver.fromKeyEvent(down, kb), isNull);
});
test('maps a plain KeyDownEvent to a modifier-free Keybinding', () {
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.keyG, logicalKey: LogicalKeyboardKey.keyG, timeStamp: Duration.zero);
final b = KeybindingResolver.fromKeyEvent(down, kb);
expect(b, isNotNull);
expect(b!.key, 'g');
@@ -118,11 +106,7 @@ void main() {
_holdModifier(PhysicalKeyboardKey.altLeft, LogicalKeyboardKey.altLeft);
_holdModifier(PhysicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaLeft);
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyG,
logicalKey: LogicalKeyboardKey.keyG,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.keyG, logicalKey: LogicalKeyboardKey.keyG, timeStamp: Duration.zero);
final b = KeybindingResolver.fromKeyEvent(down, kb)!;
expect(b.key, 'g');
expect(b.modifiers.toSet(), {'ctrl', 'shift', 'alt', 'cmd'});
@@ -131,7 +115,5 @@ void main() {
}
void _holdModifier(PhysicalKeyboardKey physical, LogicalKeyboardKey logical) {
HardwareKeyboard.instance.handleKeyEvent(
KeyDownEvent(physicalKey: physical, logicalKey: logical, timeStamp: Duration.zero),
);
HardwareKeyboard.instance.handleKeyEvent(KeyDownEvent(physicalKey: physical, logicalKey: logical, timeStamp: Duration.zero));
}
+7 -13
View File
@@ -4,11 +4,11 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
CommandContribution _cmd(String id, {String? title}) => CommandContribution(
id: id,
command: id,
title: title,
run: (_) async => IpcResponse.ok(id: '', data: const {}),
);
id: id,
command: id,
title: title,
run: (_) async => IpcResponse.ok(id: '', data: const {}),
);
void main() {
group('PaletteController', () {
@@ -39,15 +39,9 @@ void main() {
test('filter is case-insensitive against title or command', () {
palette.setFilter('git');
expect(
palette.filtered().map((c) => c.command).toSet(),
{'git.commit', 'git.push'},
);
expect(palette.filtered().map((c) => c.command).toSet(), {'git.commit', 'git.push'});
palette.setFilter('PICK');
expect(
palette.filtered().map((c) => c.command).toSet(),
{'theme.pick'},
);
expect(palette.filtered().map((c) => c.command).toSet(), {'theme.pick'});
});
test('invoke closes the palette + runs the command', () async {
+3 -12
View File
@@ -3,12 +3,8 @@ import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
CommandContribution _cmd(String name, Future<IpcResponse> Function() run) => CommandContribution(
id: name,
command: name,
title: 'cmd $name',
run: (_) => run(),
);
CommandContribution _cmd(String name, Future<IpcResponse> Function() run) =>
CommandContribution(id: name, command: name, title: 'cmd $name', run: (_) => run());
void main() {
group('CommandRegistry', () {
@@ -22,12 +18,7 @@ void main() {
test('execute returns the handler response', () async {
final r = CommandRegistry();
r.register(
_cmd(
'ping',
() async => IpcResponse.ok(id: '', data: const {'pong': true}),
),
);
r.register(_cmd('ping', () async => IpcResponse.ok(id: '', data: const {'pong': true})));
final resp = await r.execute('ping');
expect(resp.ok, true);
expect(resp.data['pong'], true);
+3 -20
View File
@@ -48,12 +48,7 @@ void main() {
test('DaemonEvent merges ts into payload', () {
final ts = DateTime.utc(2026, 5, 11, 12, 0, 0);
final e = DaemonEvent(
subsystem: 'pty',
kind: 'output',
data: {'bytes': 'aGVsbG8='},
ts: ts,
);
final e = DaemonEvent(subsystem: 'pty', kind: 'output', data: {'bytes': 'aGVsbG8='}, ts: ts);
expect(e.subsystem, 'pty');
expect(e.kind, 'output');
expect(e.payload()['ts'], ts.toIso8601String());
@@ -63,22 +58,10 @@ void main() {
group('Team events', () {
test('TeamMemberJoined includes only the set optional fields in payload', () {
const minimal = TeamMemberJoined(
team: 'alpha',
agentId: 'bob@alpha',
name: 'bob',
agentType: 'reviewer',
paneId: '%3',
);
const minimal = TeamMemberJoined(team: 'alpha', agentId: 'bob@alpha', name: 'bob', agentType: 'reviewer', paneId: '%3');
expect(minimal.subsystem, 'team');
expect(minimal.kind, 'member-joined');
expect(minimal.payload(), {
'team': 'alpha',
'agentId': 'bob@alpha',
'name': 'bob',
'agentType': 'reviewer',
'paneId': '%3',
});
expect(minimal.payload(), {'team': 'alpha', 'agentId': 'bob@alpha', 'name': 'bob', 'agentType': 'reviewer', 'paneId': '%3'});
const full = TeamMemberJoined(
team: 'alpha',
+94 -153
View File
@@ -8,13 +8,7 @@ import '../../helpers/kernel_fixture.dart';
/// Minimal no-op extension used as a test actor.
class _Ext extends ClideExtension {
_Ext({
required this.id,
this.dependsOn = const [],
this.contributions = const [],
this.onActivate,
this.onDeactivate,
});
_Ext({required this.id, this.dependsOn = const [], this.contributions = const [], this.onActivate, this.onDeactivate});
@override
final String id;
@@ -56,53 +50,30 @@ void main() {
test('register + activateAll runs extensions in dep order', () async {
final order = <String>[];
f.services.extensions
..register(_Ext(
id: 'a',
onActivate: (_) async => order.add('a'),
))
..register(_Ext(
id: 'b',
dependsOn: const ['a'],
onActivate: (_) async => order.add('b'),
))
..register(_Ext(
id: 'c',
dependsOn: const ['b'],
onActivate: (_) async => order.add('c'),
));
..register(_Ext(id: 'a', onActivate: (_) async => order.add('a')))
..register(_Ext(id: 'b', dependsOn: const ['a'], onActivate: (_) async => order.add('b')))
..register(_Ext(id: 'c', dependsOn: const ['b'], onActivate: (_) async => order.add('c')));
await f.services.extensions.activateAll();
expect(order, ['a', 'b', 'c']);
});
test('missing dep skips the dependent with a warning', () async {
final order = <String>[];
f.services.extensions.register(_Ext(
id: 'needs-missing',
dependsOn: const ['does.not.exist'],
onActivate: (_) async => order.add('needs-missing'),
));
f.services.extensions.register(_Ext(id: 'needs-missing', dependsOn: const ['does.not.exist'], onActivate: (_) async => order.add('needs-missing')));
await f.services.extensions.activateAll();
expect(order, isEmpty);
expect(f.services.extensions.isActivated('needs-missing'), false);
});
test('contribution points wire into panel registry on activate', () async {
f.services.extensions.register(_Ext(
id: 'with-tab',
contributions: [
TabContribution(
id: 'with-tab.view',
slot: Slots.workspace,
title: 'T',
build: (_) => const SizedBox.shrink(),
),
],
));
await f.services.extensions.activateAll();
expect(
f.services.panels.tabsFor(Slots.workspace).map((t) => t.id),
['with-tab.view'],
f.services.extensions.register(
_Ext(
id: 'with-tab',
contributions: [TabContribution(id: 'with-tab.view', slot: Slots.workspace, title: 'T', build: (_) => const SizedBox.shrink())],
),
);
await f.services.extensions.activateAll();
expect(f.services.panels.tabsFor(Slots.workspace).map((t) => t.id), ['with-tab.view']);
});
test('activating an extension auto-loads its localized tab namespace (T-155)', () async {
@@ -119,46 +90,37 @@ void main() {
addTearDown(local.dispose);
// Not registered yet → string() falls back to the placeholder.
expect(
local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'),
'fallback',
);
expect(local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'), 'fallback');
local.services.extensions.register(_Ext(
id: 'ext.localized',
contributions: [
TabContribution(
id: 'ext.localized.view',
slot: Slots.workspace,
title: 'Fallback',
titleKey: 'tab.title',
i18nNamespace: 'ext.localized',
build: (_) => const SizedBox.shrink(),
),
],
));
local.services.extensions.register(
_Ext(
id: 'ext.localized',
contributions: [
TabContribution(
id: 'ext.localized.view',
slot: Slots.workspace,
title: 'Fallback',
titleKey: 'tab.title',
i18nNamespace: 'ext.localized',
build: (_) => const SizedBox.shrink(),
),
],
),
);
await local.services.extensions.activateAll();
// Activation auto-loaded the catalog → resolves, no "namespace not
// registered" warning.
expect(
local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'),
'Localized',
);
expect(local.services.i18n.string('tab.title', namespace: 'ext.localized', placeholder: 'fallback'), 'Localized');
});
test('deactivate removes contributions from the registry', () async {
f.services.extensions.register(_Ext(
id: 'ephemeral',
contributions: [
TabContribution(
id: 'ephemeral.view',
slot: Slots.workspace,
title: 'T',
build: (_) => const SizedBox.shrink(),
),
],
));
f.services.extensions.register(
_Ext(
id: 'ephemeral',
contributions: [TabContribution(id: 'ephemeral.view', slot: Slots.workspace, title: 'T', build: (_) => const SizedBox.shrink())],
),
);
await f.services.extensions.activateAll();
expect(f.services.panels.tabsFor(Slots.workspace), hasLength(1));
await f.services.extensions.deactivate('ephemeral');
@@ -166,32 +128,27 @@ void main() {
});
test('CommandContribution registers + default binding is bound', () async {
f.services.extensions.register(_Ext(
id: 'has-cmd',
contributions: [
CommandContribution(
id: 'c',
command: 'test.cmd',
defaultBinding: 'ctrl+alt+k',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
),
],
));
f.services.extensions.register(
_Ext(
id: 'has-cmd',
contributions: [
CommandContribution(
id: 'c',
command: 'test.cmd',
defaultBinding: 'ctrl+alt+k',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
),
],
),
);
await f.services.extensions.activateAll();
expect(f.services.commands.get('test.cmd'), isNotNull);
expect(
f.services.keybindings.commandFor(Keybinding.parse('ctrl+alt+k')),
'test.cmd',
);
expect(f.services.keybindings.commandFor(Keybinding.parse('ctrl+alt+k')), 'test.cmd');
});
test('setEnabled=false deactivates; =true reactivates', () async {
final order = <String>[];
f.services.extensions.register(_Ext(
id: 'toggle',
onActivate: (_) async => order.add('on'),
onDeactivate: () async => order.add('off'),
));
f.services.extensions.register(_Ext(id: 'toggle', onActivate: (_) async => order.add('on'), onDeactivate: () async => order.add('off')));
await f.services.extensions.activateAll();
expect(order, ['on']);
await f.services.extensions.setEnabled('toggle', false);
@@ -228,16 +185,12 @@ void main() {
});
test('TrayItemContribution lands in TrayRegistry; deactivate removes it', () async {
f.services.extensions.register(_Ext(
id: 'tray-ext',
contributions: [
const TrayItemContribution(
id: 'tray-ext.item',
label: 'Item',
onSelected: _noop,
),
],
));
f.services.extensions.register(
_Ext(
id: 'tray-ext',
contributions: [const TrayItemContribution(id: 'tray-ext.item', label: 'Item', onSelected: _noop)],
),
);
await f.services.extensions.activateAll();
expect(f.services.tray.items.map((i) => i.id), contains('tray-ext.item'));
await f.services.extensions.deactivate('tray-ext');
@@ -245,21 +198,15 @@ void main() {
});
test('StatusItem + ToolbarButton contributions activate and deactivate cleanly', () async {
f.services.extensions.register(_Ext(
id: 'status-and-toolbar',
contributions: [
StatusItemContribution(
id: 'status-and-toolbar.status',
priority: 1,
build: (_) => const SizedBox.shrink(),
),
ToolbarButtonContribution(
id: 'status-and-toolbar.btn',
label: 'B',
onPressed: () {},
),
],
));
f.services.extensions.register(
_Ext(
id: 'status-and-toolbar',
contributions: [
StatusItemContribution(id: 'status-and-toolbar.status', priority: 1, build: (_) => const SizedBox.shrink()),
ToolbarButtonContribution(id: 'status-and-toolbar.btn', label: 'B', onPressed: () {}),
],
),
);
await f.services.extensions.activateAll();
// Both contributions register through PanelRegistry.contributionsFor.
expect(f.services.panels.contributionsFor(Slots.statusbar).whereType<StatusItemContribution>(), hasLength(1));
@@ -271,16 +218,12 @@ void main() {
// LayoutPresetContribution is consumed by the default-layout
// extension's own activate(); the manager's add/remove just hit
// the no-op case branch.
f.services.extensions.register(_Ext(
id: 'preset-only',
contributions: [
const LayoutPresetContribution(
id: 'preset-only.default',
displayName: 'Preset only',
slots: [],
),
],
));
f.services.extensions.register(
_Ext(
id: 'preset-only',
contributions: [const LayoutPresetContribution(id: 'preset-only.default', displayName: 'Preset only', slots: [])],
),
);
await f.services.extensions.activateAll();
await f.services.extensions.deactivate('preset-only');
});
@@ -298,36 +241,32 @@ void main() {
});
test('activate failure is caught and logged (extension survives)', () async {
f.services.extensions.register(_Ext(
id: 'throws-on-activate',
onActivate: (_) async => throw StateError('kaboom'),
));
f.services.extensions.register(_Ext(id: 'throws-on-activate', onActivate: (_) async => throw StateError('kaboom')));
await f.services.extensions.activateAll();
expect(f.services.extensions.isActivated('throws-on-activate'), isFalse);
});
test('deactivate failure is caught and logged', () async {
f.services.extensions.register(_Ext(
id: 'throws-on-deactivate',
onDeactivate: () async => throw StateError('kaboom'),
));
f.services.extensions.register(_Ext(id: 'throws-on-deactivate', onDeactivate: () async => throw StateError('kaboom')));
await f.services.extensions.activateAll();
expect(f.services.extensions.isActivated('throws-on-deactivate'), isTrue);
await f.services.extensions.deactivate('throws-on-deactivate');
});
test('deactivating a CommandContribution removes its keybinding', () async {
f.services.extensions.register(_Ext(
id: 'with-bound-cmd',
contributions: [
CommandContribution(
id: 'with-bound-cmd.cmd',
command: 'bound.cmd',
defaultBinding: 'ctrl+alt+j',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
),
],
));
f.services.extensions.register(
_Ext(
id: 'with-bound-cmd',
contributions: [
CommandContribution(
id: 'with-bound-cmd.cmd',
command: 'bound.cmd',
defaultBinding: 'ctrl+alt+j',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
),
],
),
);
await f.services.extensions.activateAll();
expect(f.services.keybindings.commandFor(Keybinding.parse('ctrl+alt+j')), 'bound.cmd');
await f.services.extensions.deactivate('with-bound-cmd');
@@ -343,12 +282,14 @@ void main() {
test('extension context exposes every kernel service via passthrough getters', () async {
ClideExtensionContext? captured;
f.services.extensions.register(_Ext(
id: 'ctx-capture',
onActivate: (ctx) async {
captured = ctx;
},
));
f.services.extensions.register(
_Ext(
id: 'ctx-capture',
onActivate: (ctx) async {
captured = ctx;
},
),
);
await f.services.extensions.activateAll();
final ctx = captured!;
expect(ctx.id, 'ctx-capture');
+16 -19
View File
@@ -45,14 +45,7 @@ void main() {
},
);
addTearDown(f.dispose);
expect(
f.services.i18n.string(
'title',
namespace: 'builtin.welcome',
placeholder: '-',
),
'clide',
);
expect(f.services.i18n.string('title', namespace: 'builtin.welcome', placeholder: '-'), 'clide');
});
test('dispose shuts down IPC + notifiers without throwing', () async {
@@ -62,17 +55,21 @@ void main() {
testWidgets('ClideKernel.of throws a FlutterError when no ancestor exists', (tester) async {
late Object captured;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Builder(builder: (ctx) {
try {
ClideKernel.of(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
}),
));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Builder(
builder: (ctx) {
try {
ClideKernel.of(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
},
),
),
);
expect(captured, isA<FlutterError>());
});
});
+4 -12
View File
@@ -29,9 +29,7 @@ class _MapAssetBundle extends CachingAssetBundle {
void main() {
group('AssetCatalogLoader', () {
test('returns parsed JSON for a present asset', () async {
final bundle = _MapAssetBundle({
'lib/kernel/src/i18n/catalog/welcome_en_us.json': '{"title":{"translation":"Hi"}}',
});
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': '{"title":{"translation":"Hi"}}'});
final loader = AssetCatalogLoader(bundle: bundle);
final r = await loader.load('welcome', const Locale('en', 'US'));
expect(r['title'], isA<Map>());
@@ -44,25 +42,19 @@ void main() {
});
test('returns an empty map on malformed JSON (FormatException catch)', () async {
final bundle = _MapAssetBundle({
'lib/kernel/src/i18n/catalog/welcome_en_us.json': 'not json at all',
});
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': 'not json at all'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
test('returns an empty map when the asset is blank', () async {
final bundle = _MapAssetBundle({
'lib/kernel/src/i18n/catalog/welcome_en_us.json': ' \n',
});
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': ' \n'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
test('returns an empty map when JSON parses to a non-object', () async {
final bundle = _MapAssetBundle({
'lib/kernel/src/i18n/catalog/welcome_en_us.json': '[1, 2, 3]',
});
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': '[1, 2, 3]'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
+71 -88
View File
@@ -30,11 +30,7 @@ void main() {
}
test('locale getters reflect constructor arguments', () {
final i = build(
catalogs: const {},
initial: const Locale('nl', 'NL'),
defaultLocale: const Locale('en', 'US'),
);
final i = build(catalogs: const {}, initial: const Locale('nl', 'NL'), defaultLocale: const Locale('en', 'US'));
expect(i.currentLocale, const Locale('nl', 'NL'));
expect(i.defaultLocale, const Locale('en', 'US'));
expect(i.availableLocales, [const Locale('en', 'US'), const Locale('nl', 'NL')]);
@@ -43,10 +39,7 @@ void main() {
test('missing key with placeholder returns placeholder', () async {
final i = build(catalogs: const {});
await i.ensureNamespaceLoaded('builtin.x');
expect(
i.string('missing', namespace: 'builtin.x', placeholder: 'fallback'),
'fallback',
);
expect(i.string('missing', namespace: 'builtin.x', placeholder: 'fallback'), 'fallback');
});
test('missing key with null placeholder returns the key', () async {
@@ -57,68 +50,66 @@ void main() {
test('unknown namespace still returns placeholder (no crash)', () {
final i = build(catalogs: const {});
expect(
i.string('k', namespace: 'not.registered', placeholder: 'fb'),
'fb',
);
expect(i.string('k', namespace: 'not.registered', placeholder: 'fb'), 'fb');
});
test('exact locale hit beats fallback', () async {
final i = build(catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'greet': {'translation': 'Hello'},
},
const Locale('en'): {
'greet': {'translation': 'Hi'},
final i = build(
catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'greet': {'translation': 'Hello'},
},
const Locale('en'): {
'greet': {'translation': 'Hi'},
},
},
},
});
await i.ensureNamespaceLoaded('builtin.x');
expect(
i.string('greet', namespace: 'builtin.x', placeholder: 'fb'),
'Hello',
);
await i.ensureNamespaceLoaded('builtin.x');
expect(i.string('greet', namespace: 'builtin.x', placeholder: 'fb'), 'Hello');
});
test('language-only locale hit falls through from exact', () async {
final i = build(catalogs: {
'builtin.x': {
const Locale('nl'): {
'greet': {'translation': 'Hoi'},
final i = build(
catalogs: {
'builtin.x': {
const Locale('nl'): {
'greet': {'translation': 'Hoi'},
},
},
},
}, initial: const Locale('nl', 'NL'));
await i.ensureNamespaceLoaded('builtin.x');
expect(
i.string('greet', namespace: 'builtin.x', placeholder: 'fb'),
'Hoi',
initial: const Locale('nl', 'NL'),
);
await i.ensureNamespaceLoaded('builtin.x');
expect(i.string('greet', namespace: 'builtin.x', placeholder: 'fb'), 'Hoi');
});
test('falls through to default-locale when current locale is empty', () async {
final i = build(catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'greet': {'translation': 'Hello'},
final i = build(
catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'greet': {'translation': 'Hello'},
},
},
},
}, initial: const Locale('nl', 'NL'));
await i.ensureNamespaceLoaded('builtin.x');
expect(
i.string('greet', namespace: 'builtin.x', placeholder: 'fb'),
'Hello',
initial: const Locale('nl', 'NL'),
);
await i.ensureNamespaceLoaded('builtin.x');
expect(i.string('greet', namespace: 'builtin.x', placeholder: 'fb'), 'Hello');
});
test('interpolation replaces all replacers; missing ones silent', () async {
final i = build(catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'welcome': {'translation': 'Hi {name} at {path}'},
final i = build(
catalogs: {
'builtin.x': {
const Locale('en', 'US'): {
'welcome': {'translation': 'Hi {name} at {path}'},
},
},
},
});
);
await i.ensureNamespaceLoaded('builtin.x');
expect(
i.interpolated(
@@ -135,18 +126,20 @@ void main() {
});
test('namespace isolation — same key, different values', () async {
final i = build(catalogs: {
'a': {
const Locale('en', 'US'): {
'k': {'translation': 'A'},
final i = build(
catalogs: {
'a': {
const Locale('en', 'US'): {
'k': {'translation': 'A'},
},
},
'b': {
const Locale('en', 'US'): {
'k': {'translation': 'B'},
},
},
},
'b': {
const Locale('en', 'US'): {
'k': {'translation': 'B'},
},
},
});
);
await i.ensureNamespaceLoaded('a');
await i.ensureNamespaceLoaded('b');
expect(i.string('k', namespace: 'a', placeholder: '-'), 'A');
@@ -154,16 +147,18 @@ void main() {
});
test('setLocale refreshes cached namespaces and notifies listeners', () async {
final i = build(catalogs: {
'x': {
const Locale('en', 'US'): {
'k': {'translation': 'Hello'},
},
const Locale('nl'): {
'k': {'translation': 'Hallo'},
final i = build(
catalogs: {
'x': {
const Locale('en', 'US'): {
'k': {'translation': 'Hello'},
},
const Locale('nl'): {
'k': {'translation': 'Hallo'},
},
},
},
});
);
await i.ensureNamespaceLoaded('x');
var notified = 0;
i.addListener(() => notified++);
@@ -177,10 +172,7 @@ void main() {
i.registerCatalog('ext.linear', const Locale('en', 'US'), const {
'issue.title': {'translation': 'Issues'},
});
expect(
i.string('issue.title', namespace: 'ext.linear', placeholder: '-'),
'Issues',
);
expect(i.string('issue.title', namespace: 'ext.linear', placeholder: '-'), 'Issues');
});
test('unregisterCatalog forgets a namespace', () async {
@@ -196,11 +188,13 @@ void main() {
test('plain-string shape (no nested translation) is accepted', () async {
// Forward-compat: if a catalog later switches to `"k": "v"`
// instead of `"k": {"translation": "v"}`, lookup still works.
final i = build(catalogs: {
'x': {
const Locale('en', 'US'): {'k': 'direct'},
final i = build(
catalogs: {
'x': {
const Locale('en', 'US'): {'k': 'direct'},
},
},
});
);
await i.ensureNamespaceLoaded('x');
expect(i.string('k', namespace: 'x', placeholder: '-'), 'direct');
});
@@ -208,23 +202,12 @@ void main() {
group('FallbackChain.resolve', () {
test('ordering: exact, lang, default, default-lang', () {
final chain = const FallbackChain(
current: Locale('nl', 'NL'),
defaultLocale: Locale('en', 'US'),
).resolve();
expect(chain.map((l) => l.toString()).toList(), [
'nl_NL',
'nl',
'en_US',
'en',
]);
final chain = const FallbackChain(current: Locale('nl', 'NL'), defaultLocale: Locale('en', 'US')).resolve();
expect(chain.map((l) => l.toString()).toList(), ['nl_NL', 'nl', 'en_US', 'en']);
});
test('deduplicates when current == default', () {
final chain = const FallbackChain(
current: Locale('en', 'US'),
defaultLocale: Locale('en', 'US'),
).resolve();
final chain = const FallbackChain(current: Locale('en', 'US'), defaultLocale: Locale('en', 'US')).resolve();
expect(chain, ['en_US', 'en'].map((_) => isA<Locale>()));
expect(chain.length, 2);
});
+2 -9
View File
@@ -113,15 +113,8 @@ void main() {
await daemon.waitForClient();
await Future<void>.delayed(const Duration(milliseconds: 50));
final eventFuture = bus.stream.firstWhere(
(e) => e.event is DaemonEvent,
);
final ev = IpcEvent(
subsystem: 'pty',
kind: 'output',
data: {'bytes': 'aGVsbG8='},
timestamp: DateTime.now(),
);
final eventFuture = bus.stream.firstWhere((e) => e.event is DaemonEvent);
final ev = IpcEvent(subsystem: 'pty', kind: 'output', data: {'bytes': 'aGVsbG8='}, timestamp: DateTime.now());
daemon.send(ev.encode());
final received = await eventFuture.timeout(const Duration(seconds: 2));
final daemonEvent = received.event as DaemonEvent;
@@ -36,11 +36,7 @@ void main() {
// Build a service whose bundle serves the real shipped preset content.
Future<KeymapService> activate(String preset) async {
final src = File('assets/keymaps/$preset.yaml').readAsStringSync();
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/$preset.yaml': src}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/$preset.yaml': src}));
await svc.setPreset(preset);
return svc;
}
@@ -72,11 +68,13 @@ void main() {
test('the Ctrl+K Ctrl+T sequence binds the theme picker', () async {
final svc = await activate('vscode');
final hit = svc.keymap!.effectiveBindings.any((b) =>
b.sequence.length == 2 &&
b.sequence[0] == KeyChord.parse('ctrl+k') &&
b.sequence[1] == KeyChord.parse('ctrl+t') &&
isCommand(b.intent, 'theme.pick'));
final hit = svc.keymap!.effectiveBindings.any(
(b) =>
b.sequence.length == 2 &&
b.sequence[0] == KeyChord.parse('ctrl+k') &&
b.sequence[1] == KeyChord.parse('ctrl+t') &&
isCommand(b.intent, 'theme.pick'),
);
expect(hit, isTrue);
});
});
+4 -20
View File
@@ -102,29 +102,17 @@ void main() {
tearDown(() => kb.clearState());
test('returns null for non-KeyDown / non-Repeat events', () {
final up = KeyUpEvent(
physicalKey: PhysicalKeyboardKey.keyA,
logicalKey: LogicalKeyboardKey.keyA,
timeStamp: Duration.zero,
);
final up = KeyUpEvent(physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, timeStamp: Duration.zero);
expect(KeyChord.fromKeyEvent(up, kb), isNull);
});
test('returns null for a bare modifier press', () {
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.controlLeft,
logicalKey: LogicalKeyboardKey.controlLeft,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.controlLeft, logicalKey: LogicalKeyboardKey.controlLeft, timeStamp: Duration.zero);
expect(KeyChord.fromKeyEvent(down, kb), isNull);
});
test('maps a plain key down to a modifier-free chord', () {
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyA,
logicalKey: LogicalKeyboardKey.keyA,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, timeStamp: Duration.zero);
final chord = KeyChord.fromKeyEvent(down, kb)!;
expect(chord.modifiers, isEmpty);
expect(chord.key, LogicalKeyboardKey.keyA);
@@ -140,11 +128,7 @@ void main() {
]) {
kb.handleKeyEvent(KeyDownEvent(physicalKey: m.$1, logicalKey: m.$2, timeStamp: Duration.zero));
}
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.keyP,
logicalKey: LogicalKeyboardKey.keyP,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.keyP, logicalKey: LogicalKeyboardKey.keyP, timeStamp: Duration.zero);
final chord = KeyChord.fromKeyEvent(down, kb)!;
expect(chord.key, LogicalKeyboardKey.keyP);
expect(chord.modifiers.toSet(), {KeyModifier.ctrl, KeyModifier.alt, KeyModifier.shift, KeyModifier.meta});
+19 -75
View File
@@ -33,9 +33,7 @@ void main() {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n'}),
);
await svc.load();
expect(svc.keymap, isNotNull);
@@ -47,9 +45,7 @@ void main() {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/vscode.yaml': 'name: vscode\nbindings:\n - intent: palette.open\n keys: ctrl+shift+p\n',
}),
bundle: _bundle({'assets/keymaps/vscode.yaml': 'name: vscode\nbindings:\n - intent: palette.open\n keys: ctrl+shift+p\n'}),
);
await svc.load();
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+shift+p'), const {}), isA<PaletteOpenIntent>());
@@ -63,33 +59,25 @@ void main() {
});
test('layers a user file on top of the preset', () async {
await File('${appDir.path}/keybindings.yaml').writeAsString(
'name: user\nbindings:\n - intent: activate\n keys: ctrl+p\n',
);
await File('${appDir.path}/keybindings.yaml').writeAsString('name: user\nbindings:\n - intent: activate\n keys: ctrl+p\n');
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: palette.open\n keys: ctrl+p\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: palette.open\n keys: ctrl+p\n'}),
);
await svc.load();
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+p'), const {}), isA<ActivateIntent>());
});
test('layers a settings overlay above the user file', () async {
await File('${appDir.path}/keybindings.yaml').writeAsString(
'name: user\nbindings:\n - intent: activate\n keys: ctrl+p\n',
);
await File('${appDir.path}/keybindings.yaml').writeAsString('name: user\nbindings:\n - intent: activate\n keys: ctrl+p\n');
await settings.set<List<Object?>>(kKeymapOverridesSetting, [
{'intent': 'dismiss', 'keys': 'ctrl+p'},
]);
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: palette.open\n keys: ctrl+p\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: palette.open\n keys: ctrl+p\n'}),
);
await svc.load();
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+p'), const {}), isA<DismissIntent>());
@@ -100,9 +88,7 @@ void main() {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n'}),
);
await svc.load();
expect(svc.keymap!.resolve(KeyChord.parse('escape'), const {}), isA<DismissIntent>());
@@ -115,9 +101,7 @@ void main() {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n'}),
);
await svc.load();
expect(svc.keymap!.resolve(KeyChord.parse('escape'), const {}), isA<DismissIntent>());
@@ -126,11 +110,7 @@ void main() {
group('registerCommandBinding / unregisterCommandBindings', () {
test('adds an InvokeCommandIntent that resolves after load', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.registerCommandBinding('ctrl+shift+g', 'git.commit');
final intent = svc.keymap!.resolve(KeyChord.parse('ctrl+shift+g'), const {});
@@ -140,11 +120,7 @@ void main() {
});
test('honours a when-clause on the contribution', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.registerCommandBinding('ctrl+s', 'editor.save', when: 'editor.focused');
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+s'), const {}), isNull);
@@ -152,25 +128,15 @@ void main() {
});
test('user file overrides a contributed binding for the same chord', () async {
await File('${appDir.path}/keybindings.yaml').writeAsString(
'name: user\nbindings:\n - intent: dismiss\n keys: ctrl+x\n',
);
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
await File('${appDir.path}/keybindings.yaml').writeAsString('name: user\nbindings:\n - intent: dismiss\n keys: ctrl+x\n');
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.registerCommandBinding('ctrl+x', 'editor.cut');
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+x'), const {}), isA<DismissIntent>());
});
test('unregisterCommandBindings removes prior contributions', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.registerCommandBinding('ctrl+x', 'editor.cut');
expect(svc.keymap!.resolve(KeyChord.parse('ctrl+x'), const {}), isA<InvokeCommandIntent>());
@@ -179,11 +145,7 @@ void main() {
});
test('unregister of an unknown command is a no-op', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.unregisterCommandBindings('nothing-registered'); // doesn't throw
});
@@ -191,11 +153,7 @@ void main() {
group('scope flags', () {
test('setScopeFlag updates the context; notifies listeners on change', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
var notified = 0;
svc.addListener(() => notified++);
@@ -209,11 +167,7 @@ void main() {
});
test('clearScopeFlag removes the entry; no-op when absent', () async {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}),
);
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings: []\n'}));
await svc.load();
svc.setScopeFlag('foo', true);
svc.clearScopeFlag('foo');
@@ -243,11 +197,7 @@ void main() {
group('resolveEvent', () {
test('returns null before load()', () {
final svc = KeymapService(settings: settings, appDir: appDir, bundle: _bundle(const {}));
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.escape,
logicalKey: LogicalKeyboardKey.escape,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: LogicalKeyboardKey.escape, timeStamp: Duration.zero);
expect(svc.resolveEvent(down, HardwareKeyboard.instance), isNull);
});
@@ -255,16 +205,10 @@ void main() {
final svc = KeymapService(
settings: settings,
appDir: appDir,
bundle: _bundle({
'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n',
}),
bundle: _bundle({'assets/keymaps/default.yaml': 'name: default\nbindings:\n - intent: dismiss\n keys: escape\n'}),
);
await svc.load();
final down = KeyDownEvent(
physicalKey: PhysicalKeyboardKey.escape,
logicalKey: LogicalKeyboardKey.escape,
timeStamp: Duration.zero,
);
final down = KeyDownEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: LogicalKeyboardKey.escape, timeStamp: Duration.zero);
expect(svc.resolveEvent(down, HardwareKeyboard.instance), isA<DismissIntent>());
});
});
@@ -45,9 +45,7 @@ void main() {
});
test('default.yaml binds Tab / Shift+Tab to focus traversal', () {
final layer = KeymapLayer.fromYaml(
File('assets/keymaps/default.yaml').readAsStringSync(),
);
final layer = KeymapLayer.fromYaml(File('assets/keymaps/default.yaml').readAsStringSync());
expect(layer.bindings.any((b) => b.intent is NextFocusIntent), isTrue);
expect(layer.bindings.any((b) => b.intent is PreviousFocusIntent), isTrue);
});
+2 -8
View File
@@ -5,10 +5,7 @@ void main() {
group('Logger', () {
test('respects minLevel — lower-level messages drop silently', () {
final out = <LogRecord>[];
final log = Logger(
minLevel: LogLevel.warn,
sinks: [out.add],
);
final log = Logger(minLevel: LogLevel.warn, sinks: [out.add]);
log.debug('s', 'dropped');
log.info('s', 'dropped');
log.warn('s', 'kept');
@@ -38,10 +35,7 @@ void main() {
test('broken sink does not kill the logger', () {
final good = <LogRecord>[];
final log = Logger(minLevel: LogLevel.info, sinks: [
(_) => throw StateError('bad sink'),
good.add,
]);
final log = Logger(minLevel: LogLevel.info, sinks: [(_) => throw StateError('bad sink'), good.add]);
log.info('s', 'still delivered');
expect(good, hasLength(1));
});
+105 -111
View File
@@ -19,28 +19,28 @@ void main() {
testWidgets('horizontal drag adjusts the sidebar slot size', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
));
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.sidebar,
axis: Axis.horizontal,
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
),
);
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(arrangement: arr, slot: Slots.sidebar, axis: Axis.horizontal),
),
),
),
));
);
await tester.pumpAndSettle();
final center = tester.getCenter(find.byType(DragResizeHandle));
final gesture = await tester.startGesture(center, kind: PointerDeviceKind.mouse);
@@ -53,28 +53,28 @@ void main() {
testWidgets('contextPanel drag inverts the delta sign', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
));
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.contextPanel,
axis: Axis.horizontal,
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
),
);
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(arrangement: arr, slot: Slots.contextPanel, axis: Axis.horizontal),
),
),
),
));
);
await tester.pumpAndSettle();
final center = tester.getCenter(find.byType(DragResizeHandle));
final gesture = await tester.startGesture(center, kind: PointerDeviceKind.mouse);
@@ -86,28 +86,26 @@ void main() {
testWidgets('exposes a slider Semantics node with the current size', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 240),
],
));
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 240)],
),
);
final semHandle = tester.ensureSemantics();
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.sidebar,
axis: Axis.horizontal,
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(arrangement: arr, slot: Slots.sidebar, axis: Axis.horizontal),
),
),
),
));
);
await tester.pumpAndSettle();
final data = tester.getSemantics(find.byType(DragResizeHandle));
expect(data.label, 'Sidebar width');
@@ -120,28 +118,26 @@ void main() {
testWidgets('contextPanel slider Semantics label matches the slot', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 320),
],
));
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 320)],
),
);
final semHandle = tester.ensureSemantics();
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.contextPanel,
axis: Axis.horizontal,
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(arrangement: arr, slot: Slots.contextPanel, axis: Axis.horizontal),
),
),
),
));
);
await tester.pumpAndSettle();
final data = tester.getSemantics(find.byType(DragResizeHandle));
expect(data.label, 'Context panel width');
@@ -162,28 +158,26 @@ void main() {
testWidgets('vertical-axis handle uses arrow up/down shortcuts and "height" label', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.workspace, position: SlotPosition.center, defaultSize: 300),
],
));
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [LayoutSlot(slot: Slots.workspace, position: SlotPosition.center, defaultSize: 300)],
),
);
final semHandle = tester.ensureSemantics();
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 200,
height: 40,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.workspace,
axis: Axis.vertical,
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 200,
height: 40,
child: DragResizeHandle(arrangement: arr, slot: Slots.workspace, axis: Axis.vertical),
),
),
),
));
);
await tester.pumpAndSettle();
final data = tester.getSemantics(find.byType(DragResizeHandle));
// Custom (non-sidebar, non-contextPanel) slots fall through to
@@ -194,28 +188,28 @@ void main() {
testWidgets('hovered state flips the line colour without throwing', (tester) async {
final arr = LayoutArrangement();
arr.applyPreset(const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
));
await tester.pumpWidget(harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(
arrangement: arr,
slot: Slots.sidebar,
axis: Axis.horizontal,
arr.applyPreset(
const LayoutPresetContribution(
id: 'test-preset',
displayName: 'Test',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 200),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 200),
],
),
);
await tester.pumpWidget(
harness(
f,
Center(
child: SizedBox(
width: 40,
height: 200,
child: DragResizeHandle(arrangement: arr, slot: Slots.sidebar, axis: Axis.horizontal),
),
),
),
));
);
await tester.pumpAndSettle();
// Hover over the handle.
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+4 -21
View File
@@ -3,18 +3,8 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
TabContribution _tab({
required String id,
required SlotId slot,
int priority = 0,
}) =>
TabContribution(
id: id,
slot: slot,
title: id,
priority: priority,
build: (_) => const SizedBox.shrink(),
);
TabContribution _tab({required String id, required SlotId slot, int priority = 0}) =>
TabContribution(id: id, slot: slot, title: id, priority: priority, build: (_) => const SizedBox.shrink());
void main() {
group('PanelRegistry', () {
@@ -23,10 +13,7 @@ void main() {
setUp(() => r = PanelRegistry());
test('registerSlot creates an empty mount list', () {
r.registerSlot(const SlotDefinition(
id: Slots.sidebar,
position: SlotPosition.left,
));
r.registerSlot(const SlotDefinition(id: Slots.sidebar, position: SlotPosition.left));
expect(r.definitionFor(Slots.sidebar)!.position, SlotPosition.left);
expect(r.contributionsFor(Slots.sidebar), isEmpty);
});
@@ -65,11 +52,7 @@ void main() {
});
test('contributing a non-slot contribution is a no-op for slots', () {
r.contribute(CommandContribution(
id: 'c',
command: 'c',
run: (_) async => throw UnimplementedError(),
));
r.contribute(CommandContribution(id: 'c', command: 'c', run: (_) async => throw UnimplementedError()));
expect(r.tabsFor(Slots.sidebar), isEmpty);
});
@@ -7,12 +7,7 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
TabContribution _tab(String id, SlotId slot) => TabContribution(
id: id,
slot: slot,
title: id.toUpperCase(),
build: (_) => const SizedBox.shrink(),
);
TabContribution _tab(String id, SlotId slot) => TabContribution(id: id, slot: slot, title: id.toUpperCase(), build: (_) => const SizedBox.shrink());
void main() {
group('snapshotViewPanes', () {
@@ -42,11 +37,13 @@ void main() {
});
test('active follows the kernel activeTab; visible follows the arrangement', () {
arrangement.applyPreset(const LayoutPresetContribution(
id: 'test',
displayName: 'Test',
slots: [LayoutSlot(slot: Slots.workspace, position: SlotPosition.center, visible: true)],
));
arrangement.applyPreset(
const LayoutPresetContribution(
id: 'test',
displayName: 'Test',
slots: [LayoutSlot(slot: Slots.workspace, position: SlotPosition.center, visible: true)],
),
);
panels.contribute(_tab('claude', Slots.workspace));
panels.contribute(_tab('editor', Slots.workspace));
panels.activateTab(Slots.workspace, 'editor');
Binary file not shown.
+14 -15
View File
@@ -65,22 +65,21 @@ void main() {
testWidgets('DialogHost renders the child + the dialog over a backdrop', (tester) async {
final r = DialogRouter();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: DialogHost(
router: r,
child: const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()),
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: DialogHost(
router: r,
child: const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()),
),
),
));
);
await tester.pump();
// No dialog open yet — backdrop + modal not present.
expect(find.byType(GestureDetector), findsNothing);
// Open one.
final future = r.show<String>((ctx, dismiss) {
return GestureDetector(
onTap: () => dismiss('inner'),
child: const SizedBox(width: 100, height: 100),
);
return GestureDetector(onTap: () => dismiss('inner'), child: const SizedBox(width: 100, height: 100));
});
await tester.pump();
// Backdrop + inner-wrapper + my own each add a GestureDetector.
@@ -129,7 +128,10 @@ void main() {
test('fire emits an OsLifecycleEvent on the bus', () async {
final bus = DaemonBus();
final bridge = OsBridge(log: Logger(minLevel: LogLevel.error), events: bus);
final bridge = OsBridge(
log: Logger(minLevel: LogLevel.error),
events: bus,
);
final got = bus.on<OsLifecycleEvent>().first;
bridge.fire('resumed');
final e = await got.timeout(const Duration(seconds: 1));
@@ -154,10 +156,7 @@ void main() {
final wc = WindowControls();
// Pre-register a handler that throws MissingPluginException for
// every call, exercising each method's catch clause.
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('clide/window'),
(call) async => throw MissingPluginException(),
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('clide/window'), (call) async => throw MissingPluginException());
await wc.startResize(ResizeEdge.bottomRight);
await wc.startDrag();
await wc.minimize();
+15 -26
View File
@@ -35,17 +35,14 @@ void main() {
testWidgets('writePlain + readPlain go through the platform clipboard channel', (tester) async {
String? last;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') {
last = (call.arguments as Map)['text'] as String?;
} else if (call.method == 'Clipboard.getData') {
return <String, dynamic>{'text': last};
}
return null;
},
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') {
last = (call.arguments as Map)['text'] as String?;
} else if (call.method == 'Clipboard.getData') {
return <String, dynamic>{'text': last};
}
return null;
});
final c = ClideClipboard();
await c.writePlain('hello');
expect(last, 'hello');
@@ -56,15 +53,12 @@ void main() {
testWidgets('write with toPlain syncs to the OS clipboard', (tester) async {
String? last;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') {
last = (call.arguments as Map)['text'] as String?;
}
return null;
},
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') {
last = (call.arguments as Map)['text'] as String?;
}
return null;
});
final c = ClideClipboard();
await c.write<int>(7, toPlain: (n) => 'n=$n');
expect(last, 'n=7');
@@ -260,12 +254,7 @@ void main() {
n.error('e');
n.success('s', duration: const Duration(seconds: 1));
expect(n.active, hasLength(4));
expect(n.active.map((x) => x.level), [
NotificationLevel.info,
NotificationLevel.warning,
NotificationLevel.error,
NotificationLevel.success,
]);
expect(n.active.map((x) => x.level), [NotificationLevel.info, NotificationLevel.warning, NotificationLevel.error, NotificationLevel.success]);
n.dispose();
});
+4 -16
View File
@@ -24,14 +24,8 @@ void main() {
});
test('scope key validation — rejects non-standard prefixes', () async {
expect(
() => store.get<String>('nothing.here'),
throwsA(isA<ArgumentError>()),
);
expect(
() => store.set('notascope.key', 'v'),
throwsA(isA<ArgumentError>()),
);
expect(() => store.get<String>('nothing.here'), throwsA(isA<ArgumentError>()));
expect(() => store.set('notascope.key', 'v'), throwsA(isA<ArgumentError>()));
});
test('app.* scope round-trips via YAML on disk', () async {
@@ -56,10 +50,7 @@ void main() {
});
test('setting a project.* key without an open project throws', () async {
expect(
() => store.set('project.thing', 'x'),
throwsA(isA<StateError>()),
);
expect(() => store.set('project.thing', 'x'), throwsA(isA<StateError>()));
});
test('project scope is isolated from app scope', () async {
@@ -106,10 +97,7 @@ void main() {
});
test('setting a project-scoped key without a project throws StateError', () async {
expect(
() async => store.set<int>('project.unset', 1),
throwsA(isA<StateError>()),
);
expect(() async => store.set<int>('project.unset', 1), throwsA(isA<StateError>()));
});
test('ext.* keys default to app scope; project overrides app for the same key', () async {
@@ -44,9 +44,9 @@ void main() {
return TreeSitterLib.testing(
wasmEngineNew: engineHandle,
wasmEngineDelete: (_) {},
wasmStoreNew: (_, __) => storeHandle(),
wasmStoreNew: (_, _) => storeHandle(),
parserNew: parserHandle,
parserSetWasmStore: (_, __) {},
parserSetWasmStore: (_, _) {},
queryCursorNew: cursorHandle,
wasmStoreLoadLanguage: wasmStoreLoadLanguage,
queryNew: queryNew,
@@ -61,7 +61,7 @@ void main() {
parserDelete: parserDelete,
wasmStoreDelete: wasmStoreDelete,
queryCursorDelete: queryCursorDelete,
parserSetLanguage: parserSetLanguage ?? ((_, __) => true),
parserSetLanguage: parserSetLanguage ?? ((_, _) => true),
);
}
@@ -71,11 +71,7 @@ void main() {
group('TreeSitterService._init — FFI failure branches', () {
test('wasmEngineNew returning nullptr → hasGrammar(.dart) is false', () async {
final svc = TreeSitterService(
lib: TreeSitterLib.testing(),
grammarBytes: okBytes,
grammarQuery: noQuery,
);
final svc = TreeSitterService(lib: TreeSitterLib.testing(), grammarBytes: okBytes, grammarQuery: noQuery);
expect(await svc.hasGrammar('foo.dart'), isFalse);
});
@@ -93,7 +89,7 @@ void main() {
final lib = TreeSitterLib.testing(
wasmEngineNew: engineHandle,
wasmEngineDelete: (_) {},
wasmStoreNew: (_, __) => storeHandle(),
wasmStoreNew: (_, _) => storeHandle(),
// parserNew default → nullptr
);
final svc = TreeSitterService(lib: lib, grammarBytes: okBytes, grammarQuery: noQuery);
@@ -119,7 +115,7 @@ void main() {
group('TreeSitterService._loadGrammar — branches', () {
test('grammarBytes throwing is caught → grammar marked unavailable', () async {
final svc = TreeSitterService(
lib: initOkLib(wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle()),
lib: initOkLib(wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle()),
grammarBytes: (_) async => throw StateError('bundle missing'),
grammarQuery: noQuery,
);
@@ -127,11 +123,7 @@ void main() {
});
test('wasmStoreLoadLanguage returning nullptr → grammar marked unavailable', () async {
final svc = TreeSitterService(
lib: initOkLib(),
grammarBytes: okBytes,
grammarQuery: noQuery,
);
final svc = TreeSitterService(lib: initOkLib(), grammarBytes: okBytes, grammarQuery: noQuery);
expect(await svc.hasGrammar('foo.dart'), isFalse);
// Once marked unavailable, languageFor also returns null.
expect(await svc.languageFor('foo.dart'), isNull);
@@ -139,9 +131,7 @@ void main() {
test('grammarQuery returning null → grammar loads with query=nullptr', () async {
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
),
lib: initOkLib(wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle()),
grammarBytes: okBytes,
grammarQuery: noQuery,
);
@@ -158,7 +148,7 @@ void main() {
test('grammarQuery loads + queryNew fails → grammar still cached with query=nullptr', () async {
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
// queryNew default → nullptr; capture reflection block skipped.
),
grammarBytes: okBytes,
@@ -174,10 +164,10 @@ void main() {
final nameNative = 'keyword'.toNativeUtf8();
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
queryNew: (_, __, ___, ____, _____) => queryHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
queryNew: (_, _, _, _, _) => queryHandle(),
queryCaptureCount: (_) => 1,
queryCaptureNameForId: (_, __, lenOut) {
queryCaptureNameForId: (_, _, lenOut) {
lenOut.value = nameNative.length;
return nameNative;
},
@@ -192,9 +182,7 @@ void main() {
test('a second call for the same language returns the cached grammar', () async {
var byteLoads = 0;
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
),
lib: initOkLib(wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle()),
grammarBytes: (lang) async {
byteLoads++;
return Uint8List.fromList(const [0, 1, 2]);
@@ -226,8 +214,8 @@ void main() {
test('parserParseString returning nullptr → empty spans', () async {
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
queryNew: (_, __, ___, ____, _____) => queryHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
queryNew: (_, _, _, _, _) => queryHandle(),
// parserParseString default → nullptr
),
grammarBytes: okBytes,
@@ -250,14 +238,14 @@ void main() {
var matchCalls = 0;
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
queryNew: (_, __, ___, ____, _____) => queryHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
queryNew: (_, _, _, _, _) => queryHandle(),
queryCaptureCount: (_) => 1,
queryCaptureNameForId: (_, __, lenOut) {
queryCaptureNameForId: (_, _, lenOut) {
lenOut.value = nameNative.length;
return nameNative;
},
parserParseString: (_, __, ___, ____) => treeHandle(),
parserParseString: (_, _, _, _) => treeHandle(),
treeRootNode: (_) => rootNode.ref,
queryCursorNextMatch: (_, match) {
if (matchCalls > 0) return false;
@@ -291,14 +279,14 @@ void main() {
var matchCalls = 0;
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
queryNew: (_, __, ___, ____, _____) => queryHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
queryNew: (_, _, _, _, _) => queryHandle(),
queryCaptureCount: (_) => 1,
queryCaptureNameForId: (_, __, lenOut) {
queryCaptureNameForId: (_, _, lenOut) {
lenOut.value = nameNative.length;
return nameNative;
},
parserParseString: (_, __, ___, ____) => treeHandle(),
parserParseString: (_, _, _, _) => treeHandle(),
treeRootNode: (_) => rootNode.ref,
queryCursorNextMatch: (_, match) {
if (matchCalls > 0) return false;
@@ -325,8 +313,8 @@ void main() {
var cursorDeletes = 0;
final svc = TreeSitterService(
lib: initOkLib(
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
queryNew: (_, __, ___, ____, _____) => queryHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
queryNew: (_, _, _, _, _) => queryHandle(),
queryDelete: (_) => queryDeletes++,
parserDelete: (_) => parserDeletes++,
wasmStoreDelete: (_) => storeDeletes++,
@@ -360,11 +348,11 @@ void main() {
return engineHandle();
},
wasmEngineDelete: (_) {},
wasmStoreNew: (_, __) => storeHandle(),
wasmStoreNew: (_, _) => storeHandle(),
parserNew: parserHandle,
parserSetWasmStore: (_, __) {},
parserSetWasmStore: (_, _) {},
queryCursorNew: cursorHandle,
wasmStoreLoadLanguage: (_, __, ___, ____, _____) => languageHandle(),
wasmStoreLoadLanguage: (_, _, _, _, _) => languageHandle(),
);
final svc = TreeSitterService(lib: lib, grammarBytes: okBytes, grammarQuery: noQuery);
await svc.hasGrammar('foo.dart');
+19 -31
View File
@@ -4,53 +4,41 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
ThemeDefinition _def(String name, Color primary) => ThemeDefinition(
name: name,
displayName: name,
dark: true,
palette: Palette({
'primary': primary,
'accent': primary,
'background': const Color(0xFF000000),
'surface': const Color(0xFF111111),
'panel': const Color(0xFF222222),
'foreground': const Color(0xFFFFFFFF),
'success': const Color(0xFF00FF00),
'warning': const Color(0xFFFFFF00),
'error': const Color(0xFFFF0000),
}),
);
name: name,
displayName: name,
dark: true,
palette: Palette({
'primary': primary,
'accent': primary,
'background': const Color(0xFF000000),
'surface': const Color(0xFF111111),
'panel': const Color(0xFF222222),
'foreground': const Color(0xFFFFFFFF),
'success': const Color(0xFF00FF00),
'warning': const Color(0xFFFFFF00),
'error': const Color(0xFFFF0000),
}),
);
void main() {
group('ThemeController', () {
test('starts on first bundled theme', () {
final c = ThemeController(bundled: [
_def('a', const Color(0xFF111111)),
_def('b', const Color(0xFF222222)),
]);
final c = ThemeController(bundled: [_def('a', const Color(0xFF111111)), _def('b', const Color(0xFF222222))]);
expect(c.currentName, 'a');
});
test('honors initialName when present', () {
final c = ThemeController(
bundled: [_def('a', const Color(0xFF000000)), _def('b', const Color(0xFF999999))],
initialName: 'b',
);
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000)), _def('b', const Color(0xFF999999))], initialName: 'b');
expect(c.currentName, 'b');
});
test('silently falls back to first when initialName is unknown', () {
final c = ThemeController(
bundled: [_def('a', const Color(0xFF000000))],
initialName: 'missing',
);
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000))], initialName: 'missing');
expect(c.currentName, 'a');
});
test('select changes current + notifies listeners', () {
final c = ThemeController(bundled: [
_def('a', const Color(0xFF000000)),
_def('b', const Color(0xFF333333)),
]);
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000)), _def('b', const Color(0xFF333333))]);
var count = 0;
c.addListener(() => count++);
c.select('b');
+4 -16
View File
@@ -48,31 +48,19 @@ surface:
panel.background: "semantic.mainchrome"
panel.border: red
''');
expect(def.surfaceOverride, {
'panel.background': 'semantic.mainchrome',
'panel.border': 'red',
});
expect(def.surfaceOverride, {'panel.background': 'semantic.mainchrome', 'panel.border': 'red'});
});
test('throws when name is missing and no fallback', () {
expect(
() => loader.fromYamlString('palette: { fg: "#fff" }'),
throwsA(isA<FormatException>()),
);
expect(() => loader.fromYamlString('palette: { fg: "#fff" }'), throwsA(isA<FormatException>()));
});
test('throws when palette is missing', () {
expect(
() => loader.fromYamlString('name: t'),
throwsA(isA<FormatException>()),
);
expect(() => loader.fromYamlString('name: t'), throwsA(isA<FormatException>()));
});
test('fallback name is used when name is absent', () {
final def = loader.fromYamlString(
'palette: { fg: "#fff" }',
fallbackName: 'inferred',
);
final def = loader.fromYamlString('palette: { fg: "#fff" }', fallbackName: 'inferred');
expect(def.name, 'inferred');
});
});
+61 -60
View File
@@ -37,32 +37,40 @@ void main() {
);
addTearDown(controller.dispose);
late ClideThemeData captured;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ClideTheme(
controller: controller,
child: Builder(builder: (ctx) {
captured = ClideTheme.of(ctx);
return const SizedBox();
}),
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ClideTheme(
controller: controller,
child: Builder(
builder: (ctx) {
captured = ClideTheme.of(ctx);
return const SizedBox();
},
),
),
),
));
);
expect(captured, isNotNull);
});
testWidgets('of() throws when no ClideTheme ancestor', (tester) async {
late Object captured;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Builder(builder: (ctx) {
try {
ClideTheme.of(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
}),
));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Builder(
builder: (ctx) {
try {
ClideTheme.of(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
},
),
),
);
expect(captured, isA<FlutterError>());
});
@@ -90,32 +98,40 @@ void main() {
);
addTearDown(controller.dispose);
late ThemeController captured;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ClideTheme(
controller: controller,
child: Builder(builder: (ctx) {
captured = ClideTheme.controllerOf(ctx);
return const SizedBox();
}),
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ClideTheme(
controller: controller,
child: Builder(
builder: (ctx) {
captured = ClideTheme.controllerOf(ctx);
return const SizedBox();
},
),
),
),
));
);
expect(captured, same(controller));
});
testWidgets('controllerOf() throws when no ClideTheme ancestor', (tester) async {
late Object captured;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Builder(builder: (ctx) {
try {
ClideTheme.controllerOf(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
}),
));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Builder(
builder: (ctx) {
try {
ClideTheme.controllerOf(ctx);
} catch (e) {
captured = e;
}
return const SizedBox();
},
),
),
);
expect(captured, isA<FlutterError>());
});
});
@@ -133,11 +149,7 @@ void main() {
test('ContrastFailure.toString embeds the pair name, ratio, and minimum', () {
const failure = ContrastFailure(
pair: ContrastPair(
name: 'global.text_on_background',
foreground: Color(0xFF888888),
background: Color(0xFF7F7F7F),
),
pair: ContrastPair(name: 'global.text_on_background', foreground: Color(0xFF888888), background: Color(0xFF7F7F7F)),
ratio: 1.23,
minimum: 4.5,
);
@@ -150,16 +162,11 @@ void main() {
group('ThemeLoader error + file paths', () {
test('fromYamlString throws when the root is not a map', () {
expect(
() => const ThemeLoader().fromYamlString('- this\n- is\n- a list'),
throwsA(isA<FormatException>()),
);
expect(() => const ThemeLoader().fromYamlString('- this\n- is\n- a list'), throwsA(isA<FormatException>()));
});
test('fromFile loads the same content as fromYamlString', () async {
final tmp = await File.fromUri(
Uri.file('${Directory.systemTemp.path}/clide-theme-${DateTime.now().microsecondsSinceEpoch}.yaml'),
).create();
final tmp = await File.fromUri(Uri.file('${Directory.systemTemp.path}/clide-theme-${DateTime.now().microsecondsSinceEpoch}.yaml')).create();
addTearDown(() async {
if (tmp.existsSync()) await tmp.delete();
});
@@ -184,18 +191,12 @@ palette:
group('Palette / SemanticRoles iterables', () {
test('Palette.names yields every registered colour key', () {
final p = Palette(const {
'primary': Color(0xFF000000),
'accent': Color(0xFFFFFFFF),
});
final p = Palette(const {'primary': Color(0xFF000000), 'accent': Color(0xFFFFFFFF)});
expect(p.names, containsAll(['primary', 'accent']));
});
test('SemanticRoles.roles yields every registered role key', () {
final s = SemanticRoles(const {
'text': Color(0xFFFFFFFF),
'background': Color(0xFF000000),
});
final s = SemanticRoles(const {'text': Color(0xFFFFFFFF), 'background': Color(0xFF000000)});
expect(s.roles, containsAll(['text', 'background']));
});
});
+3 -9
View File
@@ -82,9 +82,7 @@ void main() {
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
surfaceOverride: const {
'panel.background': '#00FF00',
},
surfaceOverride: const {'panel.background': '#00FF00'},
);
expect(tokens.panelBackground, const Color(0xFF00FF00));
});
@@ -103,9 +101,7 @@ void main() {
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
semanticOverride: const SemanticRoles({
'focus': Color(0xFF007777),
}),
semanticOverride: const SemanticRoles({'focus': Color(0xFF007777)}),
);
// No 'accent' in palette, so globalFocus falls through to
// semantic.focus which is overridden.
@@ -122,9 +118,7 @@ void main() {
'textHi': Color(0xFFFFFFFF),
'accent': Color(0xFF111111),
}),
extensionOverride: const {
'ext.sqlite.table.background': '#ABCDEF',
},
extensionOverride: const {'ext.sqlite.table.background': '#ABCDEF'},
);
expect(tokens.extensionTokens['ext.sqlite.table.background'], const Color(0xFFABCDEF));
});
+15 -15
View File
@@ -6,21 +6,21 @@ import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_test/flutter_test.dart';
ThemeDefinition _def(String name) => ThemeDefinition(
name: name,
displayName: name,
dark: true,
palette: Palette(const {
'primary': Color(0xFF00A3D2),
'accent': Color(0xFFFA5F8B),
'background': Color(0xFF21262F),
'surface': Color(0xFF393E48),
'panel': Color(0xFF292E38),
'foreground': Color(0xFFE2E8F5),
'success': Color(0xFF00AB9A),
'warning': Color(0xFFD08447),
'error': Color(0xFFF06C6F),
}),
);
name: name,
displayName: name,
dark: true,
palette: Palette(const {
'primary': Color(0xFF00A3D2),
'accent': Color(0xFFFA5F8B),
'background': Color(0xFF21262F),
'surface': Color(0xFF393E48),
'panel': Color(0xFF292E38),
'foreground': Color(0xFFE2E8F5),
'success': Color(0xFF00AB9A),
'warning': Color(0xFFD08447),
'error': Color(0xFFF06C6F),
}),
);
void main() {
group('wireThemePersistence (T-293)', () {
+9 -11
View File
@@ -9,13 +9,9 @@ import 'package:test/test.dart';
void main() {
group('ToolchainView.resolved', () {
test('exposes the supplied paths verbatim', () {
final v = ToolchainView.resolved(const ResolvedPaths(
git: '/opt/git',
pql: '/opt/pql',
tmux: '/opt/tmux',
shell: '/usr/bin/zsh',
gitEnv: {'GIT_EXEC_PATH': '/opt/git-core'},
));
final v = ToolchainView.resolved(
const ResolvedPaths(git: '/opt/git', pql: '/opt/pql', tmux: '/opt/tmux', shell: '/usr/bin/zsh', gitEnv: {'GIT_EXEC_PATH': '/opt/git-core'}),
);
expect(v.git, '/opt/git');
expect(v.pql, '/opt/pql');
expect(v.tmux, '/opt/tmux');
@@ -39,10 +35,12 @@ void main() {
});
test('missing reports only the unresolved tools', () {
final v = ToolchainView.resolved(const ResolvedPaths(
git: '/opt/git',
// pql + tmux null → missing.
));
final v = ToolchainView.resolved(
const ResolvedPaths(
git: '/opt/git',
// pql + tmux null → missing.
),
);
expect(v.missing, ['pql', 'tmux']);
expect(v.allOk, isFalse);
});
+9 -7
View File
@@ -24,13 +24,15 @@ void main() {
final t = Toolchain();
var calls = 0;
t.addListener(() => calls++);
t.applyResolved(const ResolvedPaths(
git: '/usr/bin/git',
pql: '/usr/bin/pql',
tmux: '/usr/bin/tmux',
shell: '/bin/bash',
gitEnv: {'GIT_EXEC_PATH': '/usr/lib/git-core'},
));
t.applyResolved(
const ResolvedPaths(
git: '/usr/bin/git',
pql: '/usr/bin/pql',
tmux: '/usr/bin/tmux',
shell: '/bin/bash',
gitEnv: {'GIT_EXEC_PATH': '/usr/lib/git-core'},
),
);
expect(t.resolved, isTrue);
expect(t.allOk, isTrue);
expect(t.missing, isEmpty);