Files
Garage-SDKs/garage_ui/test/scheme_derive_test.dart
T
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

183 lines
6.7 KiB
Dart

import "dart:math" as math;
import "dart:ui" show Color;
import "package:flutter/widgets.dart" show Brightness;
import "package:flutter_test/flutter_test.dart";
import "package:garage_ui/theme/colour_scheme.dart";
import "package:garage_ui/theme/schemes.dart";
// ColourScheme.derive exists so a new app doesn't pay a 37-colour entry fee.
//
// It used to be a table of HSL offsets and these tests could only say "near
// enough". It's four measured laws now, taken off the ten hand authored
// schemes, so most of these check the law rather than the number - and at
// zinc's ground the numbers come out exact, which is worth pinning.
double _channel(double v) =>
v <= 0.04045 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
double lstar(Color c) {
final y =
0.2126 * _channel(c.r) + 0.7152 * _channel(c.g) + 0.0722 * _channel(c.b);
return y > 0.008856 ? 116 * math.pow(y, 1 / 3).toDouble() - 16 : 903.3 * y;
}
void main() {
// the core of Arcs & Angles' zinc, which the laws were measured against
ColourScheme zincish() => ColourScheme.derive(
brightness: Brightness.dark,
background: const Color(0xff303030),
foreground: const Color(0xffe6e6e6),
primary: const Color(0xff4772b3),
);
// carbon's ground. 3.6 L*, so almost no room underneath - this is the one
// that used to come apart.
ColourScheme carbonish() => ColourScheme.derive(
brightness: Brightness.dark,
background: const Color(0xff0d0d0d),
foreground: const Color(0xfff2f2f2),
primary: const Color(0xffe8e8e8),
);
test("four colours produce a complete scheme", () {
final s = zincish();
expect(s.chrome, isNotNull);
expect(s.panelBorderHighlighted, isNotNull);
expect(s.rowHovered, isNotNull);
expect(s.controlBorder, isNotNull);
});
test("reproduces zinc, the scheme it was measured from", () {
final s = zincish();
// not "near" - these are the hand authored values exactly. The laws were
// read off this scheme, so anything else means a law drifted.
expect(s.chrome, const Color(0xff161616));
expect(s.card, const Color(0xff3d3d3d));
expect(s.popover, const Color(0xff181818));
expect(s.muted, const Color(0xff1d1d1d));
expect(s.surfaceSunken, const Color(0xff282828));
expect(s.mutedForeground, const Color(0xff999999));
});
// ── law 2: recessed rungs squeeze into whatever basement there is ───────
test("a ground with no basement squeezes rather than reflecting", () {
final s = carbonish();
final ground = lstar(s.background);
// the old up() reflected an offset that ran off the end, so a slot asked
// to go 12 points DOWN came out 12 points UP - lighter than the surface
// it belongs under. Every recessed slot must still be at or below ground.
for (final (name, colour) in [
("chrome", s.chrome),
("popover", s.popover),
("muted", s.muted),
("controlFill", s.controlFill),
("surfaceSunken", s.surfaceSunken),
]) {
expect(
lstar(colour),
lessThanOrEqualTo(ground + 0.01),
reason: "$name must not surface above the ground it recesses from",
);
}
});
test("squeezing keeps the order of the recessed rungs", () {
final s = carbonish();
// shallowest to deepest, which is the thing reflection destroyed
expect(lstar(s.surfaceSunken), greaterThan(lstar(s.controlFill)));
expect(lstar(s.controlFill), greaterThan(lstar(s.popover)));
expect(lstar(s.popover), greaterThanOrEqualTo(lstar(s.chrome)));
});
// ── law 3: a stroke is measured off the surface it outlines ─────────────
test("strokes sit off their own surface, not off the ground", () {
for (final s in [zincish(), carbonish(), GarageSchemes.light]) {
expect(
lstar(s.popoverBorder) - lstar(s.popover),
closeTo(9.9, 0.6),
reason: "popoverBorder is measured off popover",
);
expect(
lstar(s.controlBorder) - lstar(s.controlFill),
closeTo(11.8, 0.6),
reason: "controlBorder is measured off controlFill",
);
expect(
lstar(s.propertiesSectionBorder) - lstar(s.card),
closeTo(6.2, 0.6),
reason: "a section border outlines card, which is what fills one",
);
}
});
test("a border always reads against the surface it outlines", () {
// the bug this replaces: carbon's popover was pinned to black and its
// border derived DARKER still, so the popover had no visible edge.
for (final s in [zincish(), carbonish(), GarageSchemes.light]) {
expect((lstar(s.popoverBorder) - lstar(s.popover)).abs(), greaterThan(3));
expect(
(lstar(s.controlBorder) - lstar(s.controlFill)).abs(),
greaterThan(3),
);
}
});
// ── law 4: text is a fraction of the span, not an absolute shift ────────
test("muted text sits at the same fraction of the span on any ground", () {
for (final s in [zincish(), carbonish()]) {
final ground = lstar(s.background);
final span = lstar(s.foreground) - ground;
expect(
(lstar(s.mutedForeground) - ground) / span,
closeTo(0.607, 0.02),
reason: "zinc and carbon both measure 60.7%, on spans of 71 and 92",
);
expect((lstar(s.rowText) - ground) / span, closeTo(0.814, 0.02));
}
});
test("light schemes travel the other way", () {
final light = GarageSchemes.light;
expect(lstar(light.chrome), lessThan(lstar(light.background)));
expect(lstar(zincish().chrome), lessThan(lstar(zincish().background)));
// and a light ground has basement to spare, so nothing squeezes
expect(
lstar(light.background) - lstar(light.chrome),
closeTo(12.6, 0.6),
);
});
test("every derived slot has an override", () {
final s = ColourScheme.derive(
brightness: Brightness.dark,
background: const Color(0xff000000),
foreground: const Color(0xffffffff),
primary: const Color(0xff00ff00),
chrome: const Color(0xff123456),
card: const Color(0xff654321),
);
expect(s.chrome, const Color(0xff123456));
expect(s.card, const Color(0xff654321));
// and the override propagates to the slots measured from it
expect(
lstar(s.propertiesSectionBorder) - lstar(const Color(0xff654321)),
closeTo(6.2, 0.6),
);
});
test("GarageSchemes ship a usable pair", () {
for (final s in [GarageSchemes.dark, GarageSchemes.light]) {
expect(s.chrome, isNotNull);
expect(
(lstar(s.foreground) - lstar(s.background)).abs(),
greaterThan(50),
reason: "foreground must contrast with background",
);
}
expect(GarageSchemes.of(Brightness.dark), same(GarageSchemes.dark));
expect(GarageSchemes.of(Brightness.light), same(GarageSchemes.light));
});
}