/** * Tests for utils/value-helpers.js * * These helpers back the settings page's "did the user change this?" * detection and its "old → new" change notifications. Broken equality * rules cause settings to look unchanged (silent drops) or cause * spurious save notifications — both surfaced as real bugs before. */ import '@js/utils/value-helpers.js'; const { areValuesEqual, areObjectsEqual, formatPropertyName, formatValueForDisplay, } = window.LdrValueHelpers; describe('areValuesEqual', () => { it('treats null and undefined as interchangeable (symmetric)', () => { expect(areValuesEqual(null, null)).toBe(true); expect(areValuesEqual(undefined, undefined)).toBe(true); expect(areValuesEqual(null, undefined)).toBe(true); expect(areValuesEqual(undefined, null)).toBe(true); }); it('returns false when only one side is null/undefined', () => { expect(areValuesEqual(null, 0)).toBe(false); expect(areValuesEqual(0, null)).toBe(false); expect(areValuesEqual(undefined, '')).toBe(false); expect(areValuesEqual('', undefined)).toBe(false); expect(areValuesEqual(null, false)).toBe(false); }); it('handles primitive equality', () => { expect(areValuesEqual(1, 1)).toBe(true); expect(areValuesEqual('a', 'a')).toBe(true); expect(areValuesEqual(true, true)).toBe(true); expect(areValuesEqual(1, 2)).toBe(false); expect(areValuesEqual('a', 'b')).toBe(false); }); it('coerces numeric strings to numbers for comparison', () => { expect(areValuesEqual(1000, '1000')).toBe(true); expect(areValuesEqual('1000', 1000)).toBe(true); expect(areValuesEqual(1.5, '1.5')).toBe(true); expect(areValuesEqual(1000, '1001')).toBe(false); }); it('returns false for mismatched types other than number/string', () => { expect(areValuesEqual(true, 1)).toBe(false); expect(areValuesEqual(false, 0)).toBe(false); expect(areValuesEqual({}, 'object')).toBe(false); }); it('compares arrays by length then deep equality', () => { expect(areValuesEqual([1, 2, 3], [1, 2, 3])).toBe(true); expect(areValuesEqual([], [])).toBe(true); expect(areValuesEqual([1, 2], [1, 2, 3])).toBe(false); expect(areValuesEqual([1, 2, 3], [1, 3, 2])).toBe(false); // order matters expect(areValuesEqual([{a: 1}], [{a: 1}])).toBe(true); }); it('compares objects by JSON stringification', () => { expect(areValuesEqual({a: 1, b: 2}, {a: 1, b: 2})).toBe(true); expect(areValuesEqual({a: 1}, {a: 2})).toBe(false); expect(areValuesEqual({}, {})).toBe(true); }); it('does not confuse an array with an equivalent object', () => { // [] stringifies as "[]" and {} as "{}", so these differ expect(areValuesEqual([], {})).toBe(false); }); }); describe('areObjectsEqual', () => { it('treats empty objects as equal', () => { expect(areObjectsEqual({}, {})).toBe(true); }); it('returns false when key counts differ', () => { expect(areObjectsEqual({a: 1}, {a: 1, b: 2})).toBe(false); expect(areObjectsEqual({a: 1, b: 2}, {a: 1})).toBe(false); }); it('returns false when a key is missing on the other side', () => { expect(areObjectsEqual({a: 1}, {b: 1})).toBe(false); }); it('delegates value comparison to areValuesEqual', () => { // Numeric string coercion works via areValuesEqual expect(areObjectsEqual({port: 8080}, {port: '8080'})).toBe(true); }); it('recurses through nested objects via areValuesEqual', () => { expect(areObjectsEqual( {nested: {a: 1, b: [2, 3]}}, {nested: {a: 1, b: [2, 3]}} )).toBe(true); expect(areObjectsEqual( {nested: {a: 1}}, {nested: {a: 2}} )).toBe(false); }); }); describe('formatPropertyName', () => { it('converts snake_case to Title Case', () => { expect(formatPropertyName('llm_provider')).toBe('Llm Provider'); expect(formatPropertyName('max_output_tokens')).toBe('Max Output Tokens'); }); it('handles a single word', () => { expect(formatPropertyName('provider')).toBe('Provider'); }); it('handles an already-capitalized word', () => { expect(formatPropertyName('Provider')).toBe('Provider'); }); }); describe('formatValueForDisplay', () => { it('returns "empty" for null and undefined', () => { expect(formatValueForDisplay(null)).toBe('empty'); expect(formatValueForDisplay(undefined)).toBe('empty'); }); it('returns "enabled"/"disabled" for booleans', () => { expect(formatValueForDisplay(true)).toBe('enabled'); expect(formatValueForDisplay(false)).toBe('disabled'); }); it('wraps short strings in quotes', () => { expect(formatValueForDisplay('hello')).toBe('"hello"'); expect(formatValueForDisplay('')).toBe('""'); }); it('truncates strings longer than 20 chars', () => { const long = 'a'.repeat(25); const result = formatValueForDisplay(long); expect(result).toBe('"' + 'a'.repeat(18) + '..."'); expect(result.length).toBeLessThan(long.length + 2); }); it('keeps 20-char strings as-is (boundary)', () => { const twenty = 'a'.repeat(20); expect(formatValueForDisplay(twenty)).toBe(`"${twenty}"`); }); it('truncates 21-char strings (off-by-one boundary)', () => { const twentyOne = 'a'.repeat(21); expect(formatValueForDisplay(twentyOne)).toBe('"' + 'a'.repeat(18) + '..."'); }); it('shows objects as "{...}"', () => { expect(formatValueForDisplay({a: 1})).toBe('{...}'); }); it('coerces numbers to their string form', () => { expect(formatValueForDisplay(42)).toBe('42'); expect(formatValueForDisplay(0)).toBe('0'); expect(formatValueForDisplay(3.14)).toBe('3.14'); }); });