From e3532064b5ed7da6aac9b08133cd6d2d00f8208b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 30 Jun 2026 13:23:36 -0400 Subject: [PATCH] test: add suites for accentColor (color math) + matrix-uia (auth flows) (+15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils/accentColor (8): hexToRgb parsing, lighten/darken channel math, rgba clamping, WCAG relativeLuminance (black=0/white=1), contrastingText threshold, varNameFromToken, and derivePrimaryPalette's full 10-token output. - utils/matrix-uia (7): UIA flow helpers — getSupportedUIAFlows, completed/params/session/errcode/error accessors, getUIAFlowForStages (incl. the single-extra-dummy rule), has/requiredStageInFlows, and getLoginTermUrl language fallback. Full suite now 123 tests, all passing. Co-Authored-By: Claude Opus 4.8 --- src/app/utils/accentColor.test.ts | 68 +++++++++++++++++++++++++ src/app/utils/matrix-uia.test.ts | 84 +++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/app/utils/accentColor.test.ts create mode 100644 src/app/utils/matrix-uia.test.ts diff --git a/src/app/utils/accentColor.test.ts b/src/app/utils/accentColor.test.ts new file mode 100644 index 000000000..7303a0855 --- /dev/null +++ b/src/app/utils/accentColor.test.ts @@ -0,0 +1,68 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + hexToRgb, + lighten, + darken, + rgba, + relativeLuminance, + contrastingText, + varNameFromToken, + derivePrimaryPalette, +} from './accentColor'; + +test('hexToRgb parses 6-digit hex (with/without #, trimmed)', () => { + assert.deepEqual(hexToRgb('#ff8800'), { r: 255, g: 136, b: 0 }); + assert.deepEqual(hexToRgb('ff8800'), { r: 255, g: 136, b: 0 }); + assert.deepEqual(hexToRgb(' #FF8800 '), { r: 255, g: 136, b: 0 }); + assert.equal(hexToRgb('#fff'), undefined); // 3-digit not supported + assert.equal(hexToRgb('nope'), undefined); +}); + +test('lighten moves channels toward white', () => { + assert.deepEqual(lighten({ r: 255, g: 0, b: 0 }, 0.5), { r: 255, g: 127.5, b: 127.5 }); + assert.deepEqual(lighten({ r: 0, g: 0, b: 0 }, 1), { r: 255, g: 255, b: 255 }); + assert.deepEqual(lighten({ r: 10, g: 20, b: 30 }, 0), { r: 10, g: 20, b: 30 }); +}); + +test('darken moves channels toward black', () => { + assert.deepEqual(darken({ r: 200, g: 100, b: 50 }, 0.5), { r: 100, g: 50, b: 25 }); + assert.deepEqual(darken({ r: 255, g: 255, b: 255 }, 1), { r: 0, g: 0, b: 0 }); + assert.deepEqual(darken({ r: 10, g: 20, b: 30 }, 0), { r: 10, g: 20, b: 30 }); +}); + +test('rgba formats and clamps channels', () => { + assert.equal(rgba({ r: 255, g: 136, b: 0 }, 0.5), 'rgba(255, 136, 0, 0.5)'); + assert.equal(rgba({ r: 300, g: -5, b: 128 }, 1), 'rgba(255, 0, 128, 1)'); +}); + +test('relativeLuminance: black is 0, white is 1', () => { + assert.ok(Math.abs(relativeLuminance({ r: 0, g: 0, b: 0 })) < 1e-9); + assert.ok(Math.abs(relativeLuminance({ r: 255, g: 255, b: 255 }) - 1) < 1e-9); + // green contributes more than blue (per WCAG coefficients) + assert.ok(relativeLuminance({ r: 0, g: 255, b: 0 }) > relativeLuminance({ r: 0, g: 0, b: 255 })); +}); + +test('contrastingText picks black on light, white on dark', () => { + assert.equal(contrastingText({ r: 255, g: 255, b: 255 }), '#000'); + assert.equal(contrastingText({ r: 0, g: 0, b: 0 }), '#fff'); +}); + +test('varNameFromToken extracts the CSS var name', () => { + assert.equal(varNameFromToken('var(--oq6d07f)'), '--oq6d07f'); + assert.equal(varNameFromToken('--bare'), undefined); + assert.equal(varNameFromToken('not a token'), undefined); +}); + +test('derivePrimaryPalette produces the full Primary token set', () => { + const palette = derivePrimaryPalette({ r: 255, g: 136, b: 0 }); + assert.equal(Object.keys(palette).length, 10); + assert.equal(palette.Main, '#ff8800'); + assert.equal(palette.MainLine, '#ff8800'); + assert.equal(palette.Container, 'rgba(255, 136, 0, 0.12)'); + assert.equal(palette.ContainerLine, 'rgba(255, 136, 0, 0.4)'); + assert.equal(palette.OnMain, contrastingText({ r: 255, g: 136, b: 0 })); + // hover/active are valid 6-digit hex strings + assert.match(palette.MainHover, /^#[0-9a-f]{6}$/); + assert.match(palette.MainActive, /^#[0-9a-f]{6}$/); +}); diff --git a/src/app/utils/matrix-uia.test.ts b/src/app/utils/matrix-uia.test.ts new file mode 100644 index 000000000..c853b8f4a --- /dev/null +++ b/src/app/utils/matrix-uia.test.ts @@ -0,0 +1,84 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk'; +import { + getSupportedUIAFlows, + getUIACompleted, + getUIAParams, + getUIASession, + getUIAErrorCode, + getUIAError, + getUIAFlowForStages, + hasStageInFlows, + requiredStageInFlows, + getLoginTermUrl, +} from './matrix-uia'; + +const flows = (...stageLists: string[][]): UIAFlow[] => + stageLists.map((stages) => ({ stages })) as UIAFlow[]; +const auth = (data: Record): IAuthData => data as unknown as IAuthData; + +test('getSupportedUIAFlows keeps only fully-supported flows', () => { + const f = flows(['a', 'b'], ['a', 'c']); + assert.deepEqual(getSupportedUIAFlows(f, ['a', 'b']), [{ stages: ['a', 'b'] }]); + assert.deepEqual(getSupportedUIAFlows(f, ['a', 'b', 'c']), f); + assert.deepEqual(getSupportedUIAFlows(f, ['x']), []); +}); + +test('getUIACompleted / Params / Session default sensibly', () => { + assert.deepEqual(getUIACompleted(auth({ completed: ['m.login.password'] })), [ + 'm.login.password', + ]); + assert.deepEqual(getUIACompleted(auth({})), []); + assert.deepEqual(getUIAParams(auth({ params: { x: { y: 1 } } })), { x: { y: 1 } }); + assert.deepEqual(getUIAParams(auth({})), {}); + assert.equal(getUIASession(auth({ session: 'abc' })), 'abc'); + assert.equal(getUIASession(auth({})), undefined); +}); + +test('getUIAErrorCode / getUIAError read string fields only', () => { + assert.equal(getUIAErrorCode(auth({ errcode: 'M_FORBIDDEN' })), 'M_FORBIDDEN'); + assert.equal(getUIAErrorCode(auth({ errcode: 42 })), undefined); + assert.equal(getUIAErrorCode(auth({})), undefined); + assert.equal(getUIAError(auth({ error: 'Bad' })), 'Bad'); + assert.equal(getUIAError(auth({})), undefined); +}); + +test('getUIAFlowForStages: exact match and no match', () => { + const f = flows(['m.login.password'], ['m.login.recaptcha', 'm.login.password']); + assert.deepEqual(getUIAFlowForStages(f, ['m.login.password']), { stages: ['m.login.password'] }); + assert.equal(getUIAFlowForStages(f, ['m.login.sso']), undefined); +}); + +test('getUIAFlowForStages allows a single extra m.login.dummy stage', () => { + const f = flows(['m.login.recaptcha', AuthType.Dummy]); + assert.deepEqual(getUIAFlowForStages(f, ['m.login.recaptcha']), { + stages: ['m.login.recaptcha', AuthType.Dummy], + }); + // two extra stages (more than dummy) → no match + assert.equal(getUIAFlowForStages(flows(['a', 'b', AuthType.Dummy]), ['a']), undefined); +}); + +test('hasStageInFlows / requiredStageInFlows', () => { + const f = flows(['m.login.password'], ['m.login.recaptcha', 'm.login.password']); + assert.equal(hasStageInFlows(f, 'm.login.recaptcha'), true); + assert.equal(hasStageInFlows(f, 'm.login.sso'), false); + assert.equal(requiredStageInFlows(f, 'm.login.password'), true); + assert.equal(requiredStageInFlows(f, 'm.login.recaptcha'), false); +}); + +test('getLoginTermUrl prefers en, else the first language', () => { + const base = (policies: unknown) => ({ [AuthType.Terms]: { policies } }); + assert.equal( + getLoginTermUrl( + base({ privacy_policy: { en: { url: 'https://en' }, fr: { url: 'https://fr' } } }), + ), + 'https://en', + ); + assert.equal( + getLoginTermUrl(base({ privacy_policy: { fr: { url: 'https://fr' } } })), + 'https://fr', + ); + assert.equal(getLoginTermUrl({}), undefined); + assert.equal(getLoginTermUrl(base(null)), undefined); +});