Skip to content
Source
Sign inSign up

composerChipPresentation.test.ts

116 lines3.8 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import { contextChipAccent, contextChipPresentation } from "./composerChipPresentation";
4import {
5 flattenThemeColor,
6 getMobileThemeVariables,
7 MOBILE_THEME_IDS,
8 type MobileThemeVariables,
9} from "./mobileTheme";
10​
11const CHIP_ACCENTS = [
12 ...new Set(
13 [
14 ...[
15 "image",
16 "video",
17 "file",
18 "mention",
19 "terminal",
20 "element",
21 "review-comment",
22 "pull-request",
23 "skill",
24 "thread",
25 "environment",
26 ].map((kind) => contextChipPresentation(kind)),
27 ...[
28 { state: "open", isDraft: false },
29 { state: "open", isDraft: true },
30 { state: "merged", isDraft: false },
31 { state: "closed", isDraft: false },
32 ].map((pullRequest) => contextChipPresentation("pull-request", { pullRequest })),
33 ].flatMap(({ accent }) => (accent ? [accent] : [])),
34 ),
35];
36​
37function relativeLuminance(hex: string): number {
38 const [red = 0, green = 0, blue = 0] = [1, 3, 5].map((offset) => {
39 const value = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
40 return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
41 });
42 return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
44​
45function contrastRatio(first: string, second: string): number {
46 const [darker = 0, lighter = 0] = [relativeLuminance(first), relativeLuminance(second)].sort(
47 (a, b) => a - b,
48 );
49 return (lighter + 0.05) / (darker + 0.05);
51​
52function chipSurfaces(theme: MobileThemeVariables) {
53 const screen = theme["--color-screen"];
54 return (
55 [
56 ["--color-user-bubble", "--color-user-bubble-foreground"],
57 ["--color-thread-canvas", "--color-md-body"],
58 ["--color-composer-surface", "--color-foreground"],
59 ] as const
60 ).map(([surface, text]) => ({
61 surface: flattenThemeColor(theme[surface], screen).slice(0, 7),
62 text: theme[text],
63 }));
65​
66const BUILT_IN_THEMES = MOBILE_THEME_IDS.filter((id) => id !== "material-you").flatMap((id) =>
67 (["light", "dark"] as const).map((appearance) => ({
68 name: `${id} ${appearance}`,
69 theme: getMobileThemeVariables(id, appearance),
70 })),
71);
72​
73describe("contextChipAccent", () => {
74 it("draws one color per theme that reads at 5:1 in messages, replies and the composer", () => {
75 for (const { name, theme } of BUILT_IN_THEMES) {
76 for (const accent of CHIP_ACCENTS) {
77 const shown = contextChipAccent(accent, theme, theme["--color-md-body"])!;
78 for (const { surface, text } of chipSurfaces(theme)) {
79 expect(contextChipAccent(accent, theme, text)).toBe(shown);
80 expect(
81 contrastRatio(shown, surface),
82 `${name}: ${accent} → ${shown} on ${surface}`,
83 ).toBeGreaterThanOrEqual(5);
84 }
85 }
86 }
87 });
88​
89 it("reads on both a saturated Material You bubble and the light canvas beside it", () => {
90 const theme = {
91 ...getMobileThemeVariables("t3-code", "light"),
92 "--color-user-bubble": "#6750a4",
93 "--color-user-bubble-foreground": "#ffffff",
94 };
95 for (const { surface, text } of chipSurfaces(theme)) {
96 for (const accent of CHIP_ACCENTS) {
97 const shown = contextChipAccent(accent, theme, text)!;
98 expect(
99 contrastRatio(shown, surface),
100 `${accent} → ${shown} on ${surface}`,
101 ).toBeGreaterThanOrEqual(5);
102 }
103 }
104 });
105​
106 it("keeps an accent that already reads everywhere", () => {
107 const theme = getMobileThemeVariables("t3-code", "dark");
108 expect(contextChipAccent("#009f6e", theme, theme["--color-md-body"])).toBe("#009f6e");
109 });
110});
111​
112describe("contextChipPresentation", () => {
113 it("draws a mentioned machine as a machine, not the generic file chip", () => {
114 expect(contextChipPresentation("environment")).toEqual({ accent: "#3f9a4f", icon: "server" });
115 });
116});