The bundled-theme list and the Tier-0 i18n namespace list were each hand-maintained in 3+ places that had drifted: the testmode harness loaded 8 themes (catppuccin silently unvalidated) while the app and contrast gate loaded 10, and the i18n gate checked 4 of the shipped namespaces. Export one canonical const each — kBundledThemePaths and kTier0Namespaces — and have the app, the testmode harness, and the a11y gates iterate them. Drift-proof meta-assertions: - contrast gate fails if any theme YAML on disk is absent from kBundledThemePaths (so a theme can't sit unvalidated). - i18n gate derives its subjects from assets/i18n/en_us/ at test time and asserts en/nl key parity for every shipped catalog (26, was 4), plus that every kTier0Namespaces entry has a shipped catalog. Surfaced summer-night.yaml: a legacy-ported palette on disk, unbundled and never contrast-checked. Per the maintainer's call, ship it: flesh the palette to full token + syntax coverage (honouring the v1.2 colours, clide derivation pattern for the new keys) and add a structurally identical summer-night-hc sibling that clears the strict extended gate. Both pass. No licenses.yaml change: summer-night is clide's own (ported from legacy clide v1.2 under legacy/, MIT); the only third-party palette, Catppuccin, is already acknowledged; no fonts added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.9 KiB
Dart
69 lines
2.9 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:clide/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.
|
|
///
|
|
/// Themes whose name ends in `-hc` (high-contrast) or `-cb`
|
|
/// (colour-blind) additionally have to clear the stricter
|
|
/// [extendedPairs] set — D-69.
|
|
///
|
|
/// The subject list is [kBundledThemePaths] — the SAME list main.dart loads
|
|
/// and the testmode harness validates (T-371). A meta-test asserts every
|
|
/// `.yaml` on disk is in that list, so a new theme cannot ship unvalidated.
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('contrast — all bundled themes meet WCAG AA', () {
|
|
for (final path in kBundledThemePaths) {
|
|
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')}',
|
|
);
|
|
}
|
|
final isStrict = def.name.endsWith('-hc') || def.name.endsWith('-cb');
|
|
if (isStrict) {
|
|
final extended = failingExtendedPairs(tokens);
|
|
if (extended.isNotEmpty) {
|
|
fail(
|
|
'Extended contrast failures in ${def.name} (strict gate):\n'
|
|
'${extended.map((f) => ' - $f').join('\n')}',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Drift guard (T-371): a theme YAML on disk that isn't in kBundledThemePaths
|
|
// would never be loaded, shipped, or contrast-checked. Fail loudly so a new
|
|
// theme has to be added to the canonical list (which auto-validates it above)
|
|
// rather than sit on disk unvalidated.
|
|
test('every theme YAML on disk is in kBundledThemePaths', () {
|
|
final onDisk = Directory(
|
|
kThemesDir,
|
|
).listSync().whereType<File>().map((f) => f.path).where((p) => p.endsWith('.yaml')).map((p) => p.replaceAll(r'\', '/')).toSet();
|
|
final bundled = kBundledThemePaths.toSet();
|
|
final unbundled = onDisk.difference(bundled);
|
|
expect(unbundled, isEmpty, reason: 'theme YAML(s) on disk but not in kBundledThemePaths (so unvalidated/unshipped): $unbundled');
|
|
final missing = bundled.difference(onDisk);
|
|
expect(missing, isEmpty, reason: 'kBundledThemePaths references missing file(s): $missing');
|
|
});
|
|
}
|