From c3033fe304dbb4c6016b8c25e02c6aa4ecc5cd32 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 12 May 2026 09:03:29 +0200 Subject: [PATCH] test sweep: cover i18n catalog loaders + theme_picker _pick (T-91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test files / batches: - test/kernel/src/i18n/catalog_loader_test.dart (11 tests): all three CatalogLoader implementations end-to-end. AssetCatalogLoader against an in-memory AssetBundle covers happy path, FlutterError catch (missing asset), FormatException catch (malformed JSON), blank asset, non-object JSON. FileCatalogLoader against a tempdir covers happy path, missing file, malformed, blank. InMemoryCatalogLoader covers lookup + namespace-miss. - Extends test/builtin/theme_picker/widget_test.dart with two tests for the _pick command callback: defensive not-activated branch (drive the contribution's run() before activate runs, _ctx is still null) and the dialog-resolution happy path (register + activate, invoke theme.pick, manually drive dialog.dismiss('forest'), assert IpcResponse data['selected']). Coverage: kernel/src/i18n/catalog_loader.dart 14/26 -> 26/26; builtin/theme_picker/src/extension.dart 9/18 -> 15/18 (remaining 3 lines are the dialog builder body — needs a DialogHost in the test harness, out of scope). Total coverage 74.08% -> 74.55%; floor bumped to 74. Co-Authored-By: Claude Opus 4.7 (1M context) --- pubspec.yaml | 2 +- test/builtin/theme_picker/widget_test.dart | 29 ++++ test/kernel/src/i18n/catalog_loader_test.dart | 124 ++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 test/kernel/src/i18n/catalog_loader_test.dart diff --git a/pubspec.yaml b/pubspec.yaml index 667b4cb5..4d0aa330 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ repository: https://github.com/postmeridiem/clide # Pre-push line-coverage floor. Ratchets up only — see D-66. # Reading: `awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2}' pubspec.yaml`. -coverage_floor: 73 +coverage_floor: 74 # Project metadata (was project.yaml, folded in per D-056). # version: above is the single source of truth. The Makefile reads diff --git a/test/builtin/theme_picker/widget_test.dart b/test/builtin/theme_picker/widget_test.dart index bb692299..f33ae42b 100644 --- a/test/builtin/theme_picker/widget_test.dart +++ b/test/builtin/theme_picker/widget_test.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:clide/builtin/theme_picker/theme_picker.dart'; +import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -112,5 +113,33 @@ void main() { await tester.pumpAndSettle(); expect(dismissed, isNull); }); + + test('_pick before activate returns a not-activated error', () async { + // Reach the command's run callback without going through activate — + // _ctx is still null, so _pick hits the defensive error branch. + final ext = ThemePickerExtension(); + final cmd = ext.contributions.whereType().single; + final resp = await cmd.run(const []); + expect(resp.ok, isFalse); + expect(resp.error?.message, contains('not activated')); + }); + + testWidgets('_pick (after activate) opens a dialog and resolves the user selection', (tester) async { + f.services.extensions.register(ThemePickerExtension()); + await f.services.extensions.activateAll(); + // Pump a tree so dialog has a parent BuildContext to render under. + await tester.pumpWidget(harness(f, const SizedBox())); + await tester.pump(); + // Kick off the command. _pick awaits ctx.dialog.show; the future + // resolves once dialog is dismissed. + final responseFuture = f.services.commands.execute('theme.pick'); + await tester.pump(); + expect(f.services.dialog.isOpen, isTrue); + // Simulate user picking 'forest' and closing. + f.services.dialog.dismiss('forest'); + final resp = await responseFuture; + expect(resp.ok, isTrue); + expect(resp.data['selected'], 'forest'); + }); }); } diff --git a/test/kernel/src/i18n/catalog_loader_test.dart b/test/kernel/src/i18n/catalog_loader_test.dart new file mode 100644 index 00000000..87a5ffc3 --- /dev/null +++ b/test/kernel/src/i18n/catalog_loader_test.dart @@ -0,0 +1,124 @@ +/// Tests for the three CatalogLoader implementations in +/// `lib/kernel/src/i18n/catalog_loader.dart`. AssetCatalogLoader is +/// exercised via an in-memory AssetBundle; FileCatalogLoader via a +/// tempdir; InMemoryCatalogLoader inline. +library; + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:clide/kernel/src/i18n/catalog_loader.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:test/test.dart'; + +class _MapAssetBundle extends CachingAssetBundle { + _MapAssetBundle(this._files); + final Map _files; + + @override + Future load(String key) async { + final v = _files[key]; + if (v == null) throw FlutterError('asset not found: $key'); + final bytes = utf8.encode(v); + return ByteData.view(Uint8List.fromList(bytes).buffer); + } +} + +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 loader = AssetCatalogLoader(bundle: bundle); + final r = await loader.load('welcome', const Locale('en', 'US')); + expect(r['title'], isA()); + }); + + test('returns an empty map when the asset is missing (FlutterError catch)', () async { + final loader = AssetCatalogLoader(bundle: _MapAssetBundle(const {})); + final r = await loader.load('nope', const Locale('en', 'US')); + expect(r, isEmpty); + }); + + 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 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 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 loader = AssetCatalogLoader(bundle: bundle); + expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty); + }); + }); + + group('FileCatalogLoader', () { + late Directory tmp; + + setUp(() async { + tmp = await Directory.systemTemp.createTemp('clide-catalog-'); + }); + + tearDown(() async { + if (tmp.existsSync()) await tmp.delete(recursive: true); + }); + + test('reads and parses a present file', () async { + await File('${tmp.path}/welcome_en_us.json').writeAsString('{"k":{"translation":"v"}}'); + final loader = FileCatalogLoader(rootDir: tmp); + final r = await loader.load('welcome', const Locale('en', 'US')); + expect(r['k'], isA()); + }); + + test('returns an empty map when the file is missing', () async { + final loader = FileCatalogLoader(rootDir: tmp); + expect(await loader.load('nope', const Locale('en', 'US')), isEmpty); + }); + + test('returns an empty map on malformed JSON (FormatException catch)', () async { + await File('${tmp.path}/welcome_en_us.json').writeAsString('garbage'); + final loader = FileCatalogLoader(rootDir: tmp); + expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty); + }); + + test('returns an empty map when the file is blank', () async { + await File('${tmp.path}/welcome_en_us.json').writeAsString(' '); + final loader = FileCatalogLoader(rootDir: tmp); + expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty); + }); + }); + + group('InMemoryCatalogLoader', () { + test('returns the catalog when (namespace, locale) matches', () async { + final loader = InMemoryCatalogLoader({ + 'welcome': { + const Locale('en', 'US'): const {'k': 'v'}, + }, + }); + final r = await loader.load('welcome', const Locale('en', 'US')); + expect(r['k'], 'v'); + }); + + test('returns an empty map when namespace is missing', () async { + final loader = InMemoryCatalogLoader(const {}); + expect(await loader.load('nope', const Locale('en', 'US')), isEmpty); + }); + }); +}