waybetter/packages/runtime/tests/themeTokens.test.ts
CRTkafa 41267a22fd Extract the wayBetter framework core from wayBetter for X
Site-agnostic parts of the X app, behind a small site adapter:

- crates/waybetter-host: webview-label security policy and tab rect
  clamp, account profiles with native-owned accounts (AccountRules),
  plugin store with bundled-plugin upgrades (PluginHost), signed
  data-only hotfix channel, update scheduling and crash handshake, tab
  payloads/link routing/tab-chrome validation, and (feature "tauri")
  webview helpers and the Windows system menu.
- packages/runtime: DOM hub, request hub, plugin API core, OKLab theme
  color model, split-view rail hiding, link routing and account
  reporting, each configured by a SiteAdapter.
- packages/shell: address bar, pane, link-drop, profile, palette and
  update-chip models, and window controls with the Tauri API injected.

Tests moved/adapted from the app: 45 Rust (headless), 40 TypeScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 03:01:33 +07:00

52 lines
2.9 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { axisPosition, buildColorModel, deriveColors, oklabToRgb, projectColor, rgbToOklab, rolePositions, BASE_STEPS, type SourceAxis } from '../src/themeTokens.ts';
// A made-up dark site palette: every role lists its colors as `r,g,b`, the first is canonical.
const SOURCE: SourceAxis = {
bg: ['10,10,12'],
surface: ['20,21,24'],
elevated: ['32,33,38'],
border: ['50,52,58'],
muted: ['140,144,150'],
text: ['235,236,238'],
accent: ['83,252,24'],
};
const THEME = { bg: '#fdf6e3', surface: '#f5eedb', elevated: '#eee8d5', border: '#d9d2bf', muted: '#657b83', text: '#073642', accent: '#268bd2' };
const rgb = (key: string) => key.split(',').map(Number) as [number, number, number];
const hex = (value: number[]) => `#${value.map(v => v.toString(16).padStart(2, '0')).join('')}`;
test('OKLab round trip is exact to one step', () => {
for (const color of [[0, 0, 0], [255, 255, 255], [29, 155, 240], [22, 24, 28], [83, 100, 113]] as [number, number, number][]) {
const back = oklabToRgb(rgbToOklab(color));
color.forEach((v, i) => assert.ok(Math.abs(v - back[i]!) <= 1, `${color} -> ${back}`));
}
});
test('listed site colors become exactly the theme role, per usage', () => {
const background = buildColorModel(SOURCE, THEME, 'background')!;
assert.equal(hex(projectColor(background, rgb(SOURCE.bg[0]!))!), THEME.bg);
assert.equal(hex(projectColor(background, rgb(SOURCE.surface[0]!))!), THEME.surface);
const text = buildColorModel(SOURCE, THEME, 'text')!;
assert.equal(hex(projectColor(text, rgb(SOURCE.text[0]!))!), THEME.text);
assert.equal(hex(projectColor(text, rgb(SOURCE.accent[0]!))!), THEME.accent, 'accent text is exactly the theme accent');
});
test('unlisted neutrals land on the theme curve in order; chromatic non-accent colors stay', () => {
const model = buildColorModel(SOURCE, THEME, 'background')!;
const steps = [15, 60, 120, 200].map(v => projectColor(model, [v, v, v + 2])!);
const lightness = steps.map(step => rgbToOklab(step)[0]);
for (let i = 1; i < lightness.length; i++) assert.ok(lightness[i]! < lightness[i - 1]!, 'light theme: darker as the site gets lighter');
assert.equal(projectColor(model, [249, 24, 128]), null, 'a pink like-heart is left alone');
assert.equal(axisPosition(model, [249, 24, 128]), null);
assert.equal(buildColorModel(SOURCE, { accent: '#ffffff' }), null, 'a theme needs a background and text');
});
test('derived roles follow the base structure', () => {
const dark = deriveColors({ bg: '#15202b', text: '#e7e9ea', accent: '#1d9bf0' });
const positions = rolePositions(dark);
for (const role of ['surface', 'elevated', 'border', 'muted'] as const) {
assert.ok(Math.abs(positions[role] - BASE_STEPS.dark[role]) < 0.03, `${role}: ${positions[role]}`);
}
assert.equal(deriveColors({ bg: '#ffffff', text: '#000000', accent: '#000000', border: '#123456' }).border, '#123456');
});