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