test(i18n): cover the i18n facade, selector, manifest routing + locale (T-462)
Tests for the epic's new code so the 95% floor holds: ClideSettings.i18n string/interpolated null-safe + localizedCommandTitle; settings-renderer localization (section/field/help/select/file, project-scope tag, didUpdateWidget); extension-activation rollback + settings-contribution unregister; the Dutch gate check; RootShell applying app.locale on boot; and prompt/conversation tool-body + file-ref coverage. Also harden the menu toggle re-tap (capture the button point while stable, then tapAt) so it no longer throws getCenter under the serial coverage load. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/builtin/settings_ui/settings_ui.dart';
|
||||
import 'package:clide/clide.dart' show IpcResponse;
|
||||
import 'package:clide/extension/extension.dart';
|
||||
@@ -193,6 +195,37 @@ void main() {
|
||||
expect(f.services.settings.effectiveLayer('app.demo.flag'), isNull);
|
||||
});
|
||||
|
||||
testWidgets('a value stored at project scope shows the This project tag', (tester) async {
|
||||
// project.* keys live only in the project layer, which needs a project dir
|
||||
// open. Point the store at a temp dir and write the key there at project
|
||||
// scope so _appearance(SettingsScope.project) renders (lines 560-562).
|
||||
const projCat = SettingsCategory(
|
||||
id: 'pj',
|
||||
title: 'PJ',
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'S',
|
||||
fields: [SettingsField(key: 'project.pj.flag', kind: SettingsFieldKind.toggle, label: 'ProjFlag', defaultValue: false)],
|
||||
),
|
||||
],
|
||||
);
|
||||
late Directory projDir;
|
||||
await tester.runAsync(() async {
|
||||
projDir = await Directory.systemTemp.createTemp('clide_scope_');
|
||||
await f.services.settings.setProjectDir(projDir);
|
||||
await f.services.settings.setAt(SettingsScope.project, 'project.pj.flag', true);
|
||||
});
|
||||
addTearDown(() async {
|
||||
if (await projDir.exists()) {
|
||||
try {
|
||||
await projDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: projCat))));
|
||||
expect(find.bySemanticsLabel('ProjFlag scope: This project'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('moving an unset value to All clide writes it at app scope', (tester) async {
|
||||
// _other's key (app.other.x) is written by no other test → pristine here.
|
||||
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: _other))));
|
||||
@@ -269,6 +302,28 @@ void main() {
|
||||
expect(f.services.settings.get<Object>('app.nr.size'), isNot('abc'));
|
||||
});
|
||||
|
||||
testWidgets('a text field reflects an external value change when not focused (didUpdateWidget)', (tester) async {
|
||||
// Dedicated key — the store is shared across tests in this file.
|
||||
const extCat = SettingsCategory(
|
||||
id: 'tx',
|
||||
title: 'TX',
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'S',
|
||||
fields: [SettingsField(key: 'app.tx.name', kind: SettingsFieldKind.text, label: 'Name', defaultValue: 'initial')],
|
||||
),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: extCat))));
|
||||
expect(find.text('initial'), findsOneWidget);
|
||||
// External write (e.g. a reset or another surface) with the field unfocused
|
||||
// → the ListenableBuilder rebuilds _EditControl with a new value, and
|
||||
// didUpdateWidget pushes it into the controller (lines 433-438).
|
||||
await tester.runAsync(() => f.services.settings.set('app.tx.name', 'updated'));
|
||||
await tester.pump();
|
||||
expect(find.text('updated'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a text field commits the trimmed value', (tester) async {
|
||||
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: textCat))));
|
||||
await tester.enterText(find.byType(EditableText), ' hello ');
|
||||
@@ -372,4 +427,65 @@ void main() {
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('i18n localization (T-462)', () {
|
||||
testWidgets('section/field/help labels resolve through the category namespace', (tester) async {
|
||||
// KernelFixture.create does real temp-dir I/O; run it on the real event
|
||||
// loop (not the fake-async testWidgets body) or it traps under load (T-122).
|
||||
late KernelFixture lf;
|
||||
await tester.runAsync(
|
||||
() async => lf = await KernelFixture.create(
|
||||
i18nCatalogs: {
|
||||
'test.loc': {
|
||||
const Locale('en', 'US'): const {
|
||||
'sec.label': {'translation': 'LOCSEC'},
|
||||
'fld.label': {'translation': 'LocField'},
|
||||
'fld.help': {'translation': 'LocHelp'},
|
||||
'sel.label': {'translation': 'LocSelect'},
|
||||
'opt.a': {'translation': 'LocOptA'},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
addTearDown(lf.dispose);
|
||||
const cat = SettingsCategory(
|
||||
id: 'loc',
|
||||
title: 'EngCat',
|
||||
i18nNamespace: 'test.loc',
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'EngSec',
|
||||
labelKey: 'sec.label',
|
||||
fields: [
|
||||
SettingsField(
|
||||
key: 'app.loc.flag',
|
||||
kind: SettingsFieldKind.toggle,
|
||||
label: 'EngField',
|
||||
labelKey: 'fld.label',
|
||||
help: 'EngHelp',
|
||||
helpKey: 'fld.help',
|
||||
defaultValue: false,
|
||||
),
|
||||
SettingsField(
|
||||
key: 'app.loc.sel',
|
||||
kind: SettingsFieldKind.select,
|
||||
label: 'EngSelect',
|
||||
labelKey: 'sel.label',
|
||||
defaultValue: 'a',
|
||||
options: [SettingsOption(value: 'a', label: 'EngOptA', labelKey: 'opt.a')],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(harness(lf, _bounded(const SettingsCategoryView(category: cat))));
|
||||
expect(find.text('LOCSEC'), findsOneWidget); // section header from the catalog
|
||||
expect(find.text('LocField'), findsOneWidget); // field label localized
|
||||
expect(find.text('LocHelp'), findsOneWidget); // help localized
|
||||
expect(find.text('LocSelect'), findsOneWidget); // select field label localized
|
||||
expect(find.text('LocOptA'), findsOneWidget); // select shows the localized current option
|
||||
expect(find.text('EngField'), findsNothing); // English placeholder replaced
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ void main() {
|
||||
'panel.empty': {'translation': 'No settings categories are registered yet.'},
|
||||
},
|
||||
},
|
||||
// Namespace for a category whose rail title resolves via titleKey.
|
||||
'test.railcat': {
|
||||
const Locale('en', 'US'): const {
|
||||
'cat.title': {'translation': 'Localized Rail'},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -64,5 +70,27 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(dismissed, 1);
|
||||
});
|
||||
|
||||
testWidgets('a rail row resolves its title via titleKey + namespace (T-462)', (tester) async {
|
||||
// A category with titleKey + i18nNamespace → the rail localizes the row
|
||||
// title through the catalog instead of the English fallback (line 251).
|
||||
f.services.settingsRegistry.register(
|
||||
const SettingsCategory(
|
||||
id: 'railcat',
|
||||
title: 'EngRail',
|
||||
titleKey: 'cat.title',
|
||||
i18nNamespace: 'test.railcat',
|
||||
sections: [
|
||||
SettingsSection(
|
||||
label: 'S',
|
||||
fields: [SettingsField(key: 'app.railcat.flag', kind: SettingsFieldKind.toggle, label: 'F', defaultValue: false)],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, SettingsModal(onDismiss: () {})));
|
||||
expect(find.text('Localized Rail'), findsOneWidget);
|
||||
expect(find.text('EngRail'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user