test: cover runtime update gating and colour parsing

Adds unit tests for two pure modules which had no coverage:

- `runtime.utils.ts` gates every websocket broadcast and the
  `onUpdate` / `onClock` automation triggers. The tests pin the
  second-boundary rounding, the documented cases where a field is
  deliberately *not* broadcast (`elapsed`, `expectedFinish`), and the
  wrap-around behaviour of the load-next / load-previous / go-to-cue
  lookups.

- `colour.utils.ts` parses user supplied colour strings for both the
  Google Sheets export and the cuesheet rows. The tests cover the
  hex/CSS-name parsing, the null returns for invalid input, and the
  hexToColour <-> colourToHex round trip.

Both files are pure, so neither test uses a mock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAH21unBqSzEH9HMwqfa5Q
This commit is contained in:
Claude
2026-08-17 11:38:27 +00:00
parent c6eccec30e
commit 4c29756abe
2 changed files with 315 additions and 0 deletions
@@ -0,0 +1,89 @@
import { colourToHex, cssOrHexToColour, hexToColour, isLightColour, mixColours } from './colour.utils';
describe('hexToColour()', () => {
it('parses a full length hex', () => {
expect(hexToColour('#ff8800')).toStrictEqual({ red: 255, green: 136, blue: 0, alpha: 1 });
});
it('parses a compressed hex by duplicating each digit', () => {
expect(hexToColour('#f80')).toStrictEqual(hexToColour('#ff8800'));
});
it('parses the alpha channel of a full length hex', () => {
expect(hexToColour('#ff880000')).toStrictEqual({ red: 255, green: 136, blue: 0, alpha: 0 });
expect(hexToColour('#ff8800ff')).toStrictEqual({ red: 255, green: 136, blue: 0, alpha: 1 });
});
it('parses the alpha channel of a compressed hex', () => {
expect(hexToColour('#f800')).toStrictEqual(hexToColour('#ff880000'));
});
it('is case insensitive', () => {
expect(hexToColour('#FF8800')).toStrictEqual(hexToColour('#ff8800'));
});
it('returns null for values which are not a hex colour', () => {
// these are the values which reach us from user input
for (const invalid of ['', 'red', '#', '#ff', '#fffff', '#ffg', 'ff8800']) {
expect(hexToColour(invalid)).toBeNull();
}
});
});
describe('colourToHex()', () => {
it('pads single digit channels', () => {
expect(colourToHex({ red: 0, green: 1, blue: 2, alpha: 1 })).toBe('#000102ff');
});
it('round trips with hexToColour', () => {
for (const hex of ['#000000ff', '#ff8800ff', '#ffffffff', '#12345600']) {
expect(colourToHex(hexToColour(hex)!)).toBe(hex);
}
});
});
describe('cssOrHexToColour()', () => {
it('resolves named css colours', () => {
expect(cssOrHexToColour('red')).toStrictEqual({ red: 255, green: 0, blue: 0, alpha: 1 });
});
it('resolves named css colours regardless of casing', () => {
expect(cssOrHexToColour('CornflowerBlue')).toStrictEqual(cssOrHexToColour('cornflowerblue'));
});
it('delegates hex values to the hex parser', () => {
expect(cssOrHexToColour('#f80')).toStrictEqual(hexToColour('#f80'));
});
it('returns null for an unknown colour name', () => {
expect(cssOrHexToColour('not-a-colour')).toBeNull();
expect(cssOrHexToColour('')).toBeNull();
});
});
describe('mixColours()', () => {
const black = { red: 0, green: 0, blue: 0, alpha: 1 };
const white = { red: 255, green: 255, blue: 255, alpha: 1 };
it('defaults to an even mix', () => {
expect(mixColours(black, white)).toStrictEqual({ red: 128, green: 128, blue: 128, alpha: 1 });
});
it('weights the first colour by the given proportion', () => {
expect(mixColours(black, white, 1)).toStrictEqual({ ...black, alpha: 1 });
expect(mixColours(black, white, 0)).toStrictEqual({ ...white, alpha: 1 });
});
});
describe('isLightColour()', () => {
it('detects light and dark colours', () => {
expect(isLightColour({ red: 255, green: 255, blue: 255, alpha: 1 })).toBe(true);
expect(isLightColour({ red: 0, green: 0, blue: 0, alpha: 1 })).toBe(false);
});
it('weights green most heavily, as per the YIQ calculation', () => {
// pure green is considered light, pure blue is not
expect(isLightColour({ red: 0, green: 255, blue: 0, alpha: 1 })).toBe(true);
expect(isLightColour({ red: 0, green: 0, blue: 255, alpha: 1 })).toBe(false);
});
});