Files
clide/test/a11y/contrast_test.dart
T
jpmschweitzerandClaude cbbbc526f9 split contrast gate + ship -hc theme variants (T-114, T-118)
The expanded canonicalPairs from T-114 (muted text, status chips,
syntax tokens on the code-block surface, panel focus border) made the
four named themes fail WCAG-AA. Retuning their palettes to pass would
have changed the look users picked them for, so the gate is split
instead.

`canonicalPairs` shrinks back to the baseline every named theme passes;
the new `extendedPairs` carries the stricter set and only runs against
themes whose name ends `-hc` or `-cb`. Sibling files (`clide-hc`,
`midnight-hc`, `paper-hc`, `terminal-hc`) ship today; the policy lives
in D-69 with a back-ref from D-22.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 08:56:02 +02:00

59 lines
2.2 KiB
Dart

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.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('contrast — all bundled themes meet WCAG AA', () {
const bundledPaths = [
'lib/kernel/src/theme/themes/clide.yaml',
'lib/kernel/src/theme/themes/midnight.yaml',
'lib/kernel/src/theme/themes/paper.yaml',
'lib/kernel/src/theme/themes/terminal.yaml',
'lib/kernel/src/theme/themes/clide-hc.yaml',
'lib/kernel/src/theme/themes/midnight-hc.yaml',
'lib/kernel/src/theme/themes/paper-hc.yaml',
'lib/kernel/src/theme/themes/terminal-hc.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')}',
);
}
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')}',
);
}
}
});
}
});
}