Four layers under app/test/:
* kernel/, extension/ — unit tests for every kernel service and
the extension contract. i18n fallback chain gets the full
matrix (exact/lang/default/placeholder, namespace isolation,
unknown namespace, interpolation). Theme resolver and
extension-manager topo sort covered.
* widgets/, builtin/ — widget tests for every primitive and each
Tier 0 built-in. Assertions reach into the Semantics node so
a missing label fails structurally, not visually.
* goldens/ — Alchemist + Ahem font for cross-platform pixel
stability; primitives only (button, tab bar, icon set). No
goldens for compositions — they'd churn through every tier.
* a11y/ — contract-level gate: semantic coverage walks the
built-in catalogue, contrast walks every token pair in every
bundled theme against WCAG-AA thresholds (catches real
regressions: the first run caught `tab.inactive_text` at 2.81
in summer-night), i18n coverage asserts every referenced key
exists in its catalog (exists-in-map, not "key == value" — too
ambiguous for keys that happen to equal their translation).
Helpers under helpers/: `KernelFixture.create()` boots a
KernelServices with in-memory stores + a FakeDaemonClient (no
socket, drivable connected-state), `widget_harness` wraps a
widget in the minimum tree that resolves theme + i18n.
Co-Authored-By: Claude <noreply@anthropic.com>
38 lines
1.3 KiB
Dart
38 lines
1.3 KiB
Dart
import 'package:clide_app/kernel/kernel.dart';
|
|
import 'package:flutter/services.dart' show rootBundle;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
/// WCAG-AA contrast gate. Every bundled theme's token pairs (listed in
|
|
/// [canonicalPairs]) must clear 4.5:1 for normal text / 3:1 for large
|
|
/// text. Failing pairs are printed with their computed ratio so a
|
|
/// theme-token regression shows exactly which pair broke.
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('contrast — all bundled themes meet WCAG AA', () {
|
|
const bundledPaths = [
|
|
'lib/kernel/src/theme/themes/summer-night.yaml',
|
|
];
|
|
|
|
for (final path in bundledPaths) {
|
|
test('theme: $path', () async {
|
|
final def = await const ThemeLoader().fromAsset(rootBundle, path);
|
|
const resolver = ThemeResolver();
|
|
final tokens = resolver.resolve(
|
|
palette: def.palette,
|
|
semanticOverride: def.semanticOverride,
|
|
surfaceOverride: def.surfaceOverride,
|
|
extensionOverride: def.extensionOverride,
|
|
);
|
|
final failures = failingPairs(tokens);
|
|
if (failures.isNotEmpty) {
|
|
fail(
|
|
'Contrast failures in ${def.name}:\n'
|
|
'${failures.map((f) => ' - $f').join('\n')}',
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|