add app test suite — unit, widget, golden, a11y (168 tests)
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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:clide_app/kernel/kernel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
ThemeDefinition _def(String name, Color primary) => ThemeDefinition(
|
||||
name: name,
|
||||
displayName: name,
|
||||
dark: true,
|
||||
palette: Palette({
|
||||
'primary': primary,
|
||||
'accent': primary,
|
||||
'background': const Color(0xFF000000),
|
||||
'surface': const Color(0xFF111111),
|
||||
'panel': const Color(0xFF222222),
|
||||
'foreground': const Color(0xFFFFFFFF),
|
||||
'success': const Color(0xFF00FF00),
|
||||
'warning': const Color(0xFFFFFF00),
|
||||
'error': const Color(0xFFFF0000),
|
||||
}),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('ThemeController', () {
|
||||
test('starts on first bundled theme', () {
|
||||
final c = ThemeController(bundled: [
|
||||
_def('a', const Color(0xFF111111)),
|
||||
_def('b', const Color(0xFF222222)),
|
||||
]);
|
||||
expect(c.currentName, 'a');
|
||||
});
|
||||
|
||||
test('honors initialName when present', () {
|
||||
final c = ThemeController(
|
||||
bundled: [
|
||||
_def('a', const Color(0xFF000000)),
|
||||
_def('b', const Color(0xFF999999))
|
||||
],
|
||||
initialName: 'b',
|
||||
);
|
||||
expect(c.currentName, 'b');
|
||||
});
|
||||
|
||||
test('silently falls back to first when initialName is unknown', () {
|
||||
final c = ThemeController(
|
||||
bundled: [_def('a', const Color(0xFF000000))],
|
||||
initialName: 'missing',
|
||||
);
|
||||
expect(c.currentName, 'a');
|
||||
});
|
||||
|
||||
test('select changes current + notifies listeners', () {
|
||||
final c = ThemeController(bundled: [
|
||||
_def('a', const Color(0xFF000000)),
|
||||
_def('b', const Color(0xFF333333)),
|
||||
]);
|
||||
var count = 0;
|
||||
c.addListener(() => count++);
|
||||
c.select('b');
|
||||
expect(c.currentName, 'b');
|
||||
expect(count, 1);
|
||||
});
|
||||
|
||||
test('select on same theme is a no-op', () {
|
||||
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000))]);
|
||||
var count = 0;
|
||||
c.addListener(() => count++);
|
||||
c.select('a');
|
||||
expect(count, 0);
|
||||
});
|
||||
|
||||
test('select throws on unknown name', () {
|
||||
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000))]);
|
||||
expect(() => c.select('nope'), throwsA(isA<ArgumentError>()));
|
||||
});
|
||||
|
||||
test('registerTheme adds a new theme and rebuilds current if matching', () {
|
||||
final c = ThemeController(bundled: [_def('a', const Color(0xFFAAAAAA))]);
|
||||
expect(c.available.length, 1);
|
||||
c.registerTheme(_def('b', const Color(0xFFBBBBBB)));
|
||||
expect(c.available.length, 2);
|
||||
// re-register 'a' with a different palette — current rebuilds
|
||||
var count = 0;
|
||||
c.addListener(() => count++);
|
||||
c.registerTheme(_def('a', const Color(0xFF123456)));
|
||||
expect(count, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:clide_app/kernel/kernel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
const loader = ThemeLoader();
|
||||
|
||||
group('ThemeLoader.fromYamlString', () {
|
||||
test('parses palette-only theme', () {
|
||||
final def = loader.fromYamlString('''
|
||||
name: summer-night
|
||||
display_name: Summer Night
|
||||
dark: true
|
||||
palette:
|
||||
primary: "#00a3d2"
|
||||
background: "#21262F"
|
||||
foreground: "#E2E8F5"
|
||||
''');
|
||||
expect(def.name, 'summer-night');
|
||||
expect(def.displayName, 'Summer Night');
|
||||
expect(def.dark, true);
|
||||
expect(def.palette.lookup('primary'), const Color(0xFF00A3D2));
|
||||
expect(def.semanticOverride, isNull);
|
||||
expect(def.surfaceOverride, isNull);
|
||||
});
|
||||
|
||||
test('captures semantic overrides as palette-resolved colors', () {
|
||||
final def = loader.fromYamlString('''
|
||||
name: t
|
||||
palette:
|
||||
red: "#FF0000"
|
||||
blue: "#0000FF"
|
||||
semantic:
|
||||
mainchrome: red
|
||||
focus: "#123456"
|
||||
''');
|
||||
expect(
|
||||
def.semanticOverride!.lookup('mainchrome'), const Color(0xFFFF0000));
|
||||
expect(def.semanticOverride!.lookup('focus'), const Color(0xFF123456));
|
||||
});
|
||||
|
||||
test('captures surface overrides as ref strings', () {
|
||||
final def = loader.fromYamlString('''
|
||||
name: t
|
||||
palette:
|
||||
red: "#FF0000"
|
||||
surface:
|
||||
panel.background: "semantic.mainchrome"
|
||||
panel.border: red
|
||||
''');
|
||||
expect(def.surfaceOverride, {
|
||||
'panel.background': 'semantic.mainchrome',
|
||||
'panel.border': 'red',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when name is missing and no fallback', () {
|
||||
expect(
|
||||
() => loader.fromYamlString('palette: { fg: "#fff" }'),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when palette is missing', () {
|
||||
expect(
|
||||
() => loader.fromYamlString('name: t'),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('fallback name is used when name is absent', () {
|
||||
final def = loader.fromYamlString(
|
||||
'palette: { fg: "#fff" }',
|
||||
fallbackName: 'inferred',
|
||||
);
|
||||
expect(def.name, 'inferred');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:clide_app/kernel/kernel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
const resolver = ThemeResolver();
|
||||
|
||||
Palette paletteOf(Map<String, Color> colors) => Palette(colors);
|
||||
|
||||
group('ThemeResolver default fallbacks', () {
|
||||
test('palette-only summer-night shape resolves every token', () {
|
||||
final tokens = resolver.resolve(
|
||||
palette: paletteOf(const {
|
||||
'primary': Color(0xFF00A3D2),
|
||||
'accent': Color(0xFFFA5F8B),
|
||||
'background': Color(0xFF21262F),
|
||||
'surface': Color(0xFF393E48),
|
||||
'panel': Color(0xFF292E38),
|
||||
'foreground': Color(0xFFE2E8F5),
|
||||
'muted': Color(0xFF6A7280),
|
||||
'success': Color(0xFF00AB9A),
|
||||
'warning': Color(0xFFD08447),
|
||||
'error': Color(0xFFF06C6F),
|
||||
'info': Color(0xFF00A3D2),
|
||||
}),
|
||||
);
|
||||
expect(tokens.globalBackground, const Color(0xFF21262F));
|
||||
expect(tokens.globalForeground, const Color(0xFFE2E8F5));
|
||||
expect(tokens.panelBackground, const Color(0xFF292E38));
|
||||
expect(tokens.sidebarItemSelected, const Color(0xFF00A3D2));
|
||||
expect(tokens.statusSuccess, const Color(0xFF00AB9A));
|
||||
expect(tokens.statusError, const Color(0xFFF06C6F));
|
||||
expect(tokens.globalTextMuted, const Color(0xFF6A7280));
|
||||
});
|
||||
|
||||
test('missing "muted" falls back to foreground', () {
|
||||
final tokens = resolver.resolve(
|
||||
palette: paletteOf(const {
|
||||
'primary': Color(0xFF111111),
|
||||
'accent': Color(0xFF222222),
|
||||
'background': Color(0xFF333333),
|
||||
'surface': Color(0xFF444444),
|
||||
'panel': Color(0xFF555555),
|
||||
'foreground': Color(0xFFEEEEEE),
|
||||
'success': Color(0xFF008800),
|
||||
'warning': Color(0xFFFF8800),
|
||||
'error': Color(0xFFFF0000),
|
||||
}),
|
||||
);
|
||||
expect(tokens.globalTextMuted, const Color(0xFFEEEEEE));
|
||||
});
|
||||
});
|
||||
|
||||
group('ThemeResolver overrides', () {
|
||||
test('surface override wins over default', () {
|
||||
final tokens = resolver.resolve(
|
||||
palette: paletteOf(const {
|
||||
'primary': Color(0xFF111111),
|
||||
'accent': Color(0xFF222222),
|
||||
'background': Color(0xFF333333),
|
||||
'surface': Color(0xFF444444),
|
||||
'panel': Color(0xFF555555),
|
||||
'foreground': Color(0xFFFFFFFF),
|
||||
'success': Color(0xFF008800),
|
||||
'warning': Color(0xFFFF8800),
|
||||
'error': Color(0xFFFF0000),
|
||||
}),
|
||||
surfaceOverride: const {
|
||||
'panel.background': '#00FF00',
|
||||
},
|
||||
);
|
||||
expect(tokens.panelBackground, const Color(0xFF00FF00));
|
||||
});
|
||||
|
||||
test('semantic override propagates to surface tokens using it', () {
|
||||
final tokens = resolver.resolve(
|
||||
palette: paletteOf(const {
|
||||
'primary': Color(0xFF111111),
|
||||
'accent': Color(0xFF222222),
|
||||
'background': Color(0xFF333333),
|
||||
'surface': Color(0xFF444444),
|
||||
'panel': Color(0xFF555555),
|
||||
'foreground': Color(0xFFFFFFFF),
|
||||
'success': Color(0xFF008800),
|
||||
'warning': Color(0xFFFF8800),
|
||||
'error': Color(0xFFFF0000),
|
||||
'teal': Color(0xFF007777),
|
||||
}),
|
||||
semanticOverride: const SemanticRoles({
|
||||
'focus': Color(0xFF007777),
|
||||
}),
|
||||
);
|
||||
// sidebarItemSelected defaults to semantic.focus — expect override.
|
||||
expect(tokens.sidebarItemSelected, const Color(0xFF007777));
|
||||
});
|
||||
|
||||
test('extensionOverride populates extensionTokens map', () {
|
||||
final tokens = resolver.resolve(
|
||||
palette: paletteOf(const {
|
||||
'primary': Color(0xFF111111),
|
||||
'background': Color(0xFF333333),
|
||||
'surface': Color(0xFF444444),
|
||||
'panel': Color(0xFF555555),
|
||||
'foreground': Color(0xFFFFFFFF),
|
||||
}),
|
||||
extensionOverride: const {
|
||||
'ext.sqlite.table.background': '#ABCDEF',
|
||||
},
|
||||
);
|
||||
expect(tokens.extensionTokens['ext.sqlite.table.background'],
|
||||
const Color(0xFFABCDEF));
|
||||
});
|
||||
});
|
||||
|
||||
group('Palette.parseHex', () {
|
||||
test('parses 6-digit hex as opaque', () {
|
||||
expect(Palette.parseHex('#112233'), const Color(0xFF112233));
|
||||
});
|
||||
test('parses 8-digit hex with alpha', () {
|
||||
expect(Palette.parseHex('#80112233'), const Color(0x80112233));
|
||||
});
|
||||
test('returns null on invalid', () {
|
||||
expect(Palette.parseHex('nope'), isNull);
|
||||
expect(Palette.parseHex('#12'), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user