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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 +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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user