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
183 lines
6.7 KiB
Dart
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));
|
|
});
|
|
}
|