Files
clide/test/a11y/contrast_test.dart
jpmschweitzerandClaude Opus 4.8 b7db22924d feat(theme): canonical theme/namespace lists + ship Summer Night (T-371, T-478)
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>
2026-06-24 15:31:16 +02:00

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');
});
}