6db9178449
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Non-default campaigns gain an optional colour override (background / text / link) in two sets — one for every theme, one for the dark theme only — and an "urgent" flag. - Colours ride profile.get as six trailing FlatBuffers strings on BannerCampaign (backward-compatible). The client resolves the cascade (dark <- dark ?? all, light <- all) per rendered theme and derives the strip border from the background in JS (no CSS color-mix, for the old Android WebView floor); AdBanner applies them as inline vars scoped to the strip. - Urgent is resolved entirely server-side: while any enabled, in-window urgent campaign exists, computeActiveSet returns only the urgent campaigns and bannerFor skips the eligibility gate — so a system notice reaches every viewer (paid / hint-holding / no_banner included) and preempts the ordinary feed. No wire field; it appears on each viewer's next profile.get. - Admin console (/_gm/banners): native colour pickers + a live light/dark preview of the strip, and an urgent toggle. The default campaign stays plain, enforced by the service and a DB CHECK. Migration 00009 is additive (nullable colour columns + a bool default + all-or-nothing / hex / default-plain CHECKs) — expand-contract, rollback-safe. Docs: ARCHITECTURE §10, UI_DESIGN, FUNCTIONAL (+ru). Tests: ads unit (urgent preempt + colour validation), codec + resolver unit, gateway transcode, and integration (colour round-trip + urgent bypass against real Postgres).
49 lines
2.0 KiB
TypeScript
49 lines
2.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { derivedBorder, resolveBannerColors } from './bannerColors';
|
|
import type { BannerColors } from './model';
|
|
|
|
const red = { bg: '#aa0000', fg: '#ffffff', link: '#ffdd00' };
|
|
const darkRed = { bg: '#330000', fg: '#eeeeee', link: '#ffcc00' };
|
|
|
|
describe('resolveBannerColors', () => {
|
|
it('returns null when the campaign has no override (neutral tokens)', () => {
|
|
const none: BannerColors = { all: null, dark: null };
|
|
expect(resolveBannerColors(none, 'light')).toBeNull();
|
|
expect(resolveBannerColors(none, 'dark')).toBeNull();
|
|
});
|
|
|
|
it('applies the all-themes set on both themes when no dark override', () => {
|
|
const c: BannerColors = { all: red, dark: null };
|
|
expect(resolveBannerColors(c, 'light')).toMatchObject({ bg: red.bg, fg: red.fg, link: red.link });
|
|
expect(resolveBannerColors(c, 'dark')).toMatchObject({ bg: red.bg, fg: red.fg, link: red.link });
|
|
});
|
|
|
|
it('prefers the dark override on the dark theme, and the all set on light', () => {
|
|
const c: BannerColors = { all: red, dark: darkRed };
|
|
expect(resolveBannerColors(c, 'light')).toMatchObject({ bg: red.bg });
|
|
expect(resolveBannerColors(c, 'dark')).toMatchObject({ bg: darkRed.bg });
|
|
});
|
|
|
|
it('supports a dark-only override (light keeps the neutral tokens)', () => {
|
|
const c: BannerColors = { all: null, dark: darkRed };
|
|
expect(resolveBannerColors(c, 'light')).toBeNull();
|
|
expect(resolveBannerColors(c, 'dark')).toMatchObject({ bg: darkRed.bg });
|
|
});
|
|
|
|
it('derives the border from the resolved background', () => {
|
|
const c: BannerColors = { all: red, dark: null };
|
|
expect(resolveBannerColors(c, 'light')?.border).toBe(derivedBorder(red.bg));
|
|
});
|
|
});
|
|
|
|
describe('derivedBorder', () => {
|
|
it('darkens a light background and lightens a dark one', () => {
|
|
expect(derivedBorder('#ffffff')).toBe('#dbdbdb');
|
|
expect(derivedBorder('#000000')).toBe('#242424');
|
|
});
|
|
|
|
it('returns a malformed colour unchanged', () => {
|
|
expect(derivedBorder('nope')).toBe('nope');
|
|
});
|
|
});
|