From de30d853041586461e3aa949081a411ea4b6aafa Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:39:12 -0700 Subject: [PATCH] prelude tests --- src/prelude/index.ts | 2 +- src/prelude/tests/prelude.test.ts | 256 ++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 src/prelude/tests/prelude.test.ts diff --git a/src/prelude/index.ts b/src/prelude/index.ts index 1423ef6..86beedd 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -44,7 +44,7 @@ export const globalFunctions = { }, // strings - join: (arr: any[], sep: string = ',') => arr.join(sep), + join: (arr: string[], sep: string = ',') => arr.join(sep), split: (str: string, sep: string = ',') => str.split(sep), 'to-upper': (str: string) => str.toUpperCase(), 'to-lower': (str: string) => str.toLowerCase(), diff --git a/src/prelude/tests/prelude.test.ts b/src/prelude/tests/prelude.test.ts new file mode 100644 index 0000000..7997150 --- /dev/null +++ b/src/prelude/tests/prelude.test.ts @@ -0,0 +1,256 @@ +import { expect, describe, test, mock } from 'bun:test' +import { globalFunctions, formatValue } from '#prelude' +import { toValue } from 'reefvm' + +describe('string operations', () => { + test('to-upper converts to uppercase', () => { + expect(globalFunctions['to-upper']('hello')).toBe('HELLO') + expect(globalFunctions['to-upper']('Hello World!')).toBe('HELLO WORLD!') + }) + + test('to-lower converts to lowercase', () => { + expect(globalFunctions['to-lower']('HELLO')).toBe('hello') + expect(globalFunctions['to-lower']('Hello World!')).toBe('hello world!') + }) + + test('trim removes whitespace', () => { + expect(globalFunctions.trim(' hello ')).toBe('hello') + expect(globalFunctions.trim('\n\thello\t\n')).toBe('hello') + }) + + test('split divides string by separator', () => { + expect(globalFunctions.split('a,b,c', ',')).toEqual(['a', 'b', 'c']) + expect(globalFunctions.split('hello', '')).toEqual(['h', 'e', 'l', 'l', 'o']) + }) + + test('split uses comma as default separator', () => { + expect(globalFunctions.split('a,b,c')).toEqual(['a', 'b', 'c']) + }) + + test('join combines array elements', () => { + expect(globalFunctions.join(['a', 'b', 'c'], '-')).toBe('a-b-c') + expect(globalFunctions.join(['hello', 'world'], ' ')).toBe('hello world') + }) + + test('join uses comma as default separator', () => { + expect(globalFunctions.join(['a', 'b', 'c'])).toBe('a,b,c') + }) +}) + +describe('introspection', () => { + test('type returns proper types', () => { + expect(globalFunctions.type(toValue('hello'))).toBe('string') + expect(globalFunctions.type('hello')).toBe('string') + + expect(globalFunctions.type(toValue(42))).toBe('number') + expect(globalFunctions.type(42)).toBe('number') + + expect(globalFunctions.type(toValue(true))).toBe('boolean') + expect(globalFunctions.type(false)).toBe('boolean') + + expect(globalFunctions.type(toValue(null))).toBe('null') + + expect(globalFunctions.type(toValue([1, 2, 3]))).toBe('array') + + const dict = new Map([['key', toValue('value')]]) + expect(globalFunctions.type({ type: 'dict', value: dict })).toBe('dict') + }) + + test('length', () => { + expect(globalFunctions.length(toValue('hello'))).toBe(5) + expect(globalFunctions.length('hello')).toBe(5) + + expect(globalFunctions.length(toValue([1, 2, 3]))).toBe(3) + expect(globalFunctions.length([1, 2, 3])).toBe(3) + + const dict = new Map([['a', toValue(1)], ['b', toValue(2)]]) + expect(globalFunctions.length({ type: 'dict', value: dict })).toBe(2) + + expect(globalFunctions.length(toValue(42))).toBe(0) + expect(globalFunctions.length(toValue(true))).toBe(0) + expect(globalFunctions.length(toValue(null))).toBe(0) + }) + + test('inspect formats values', () => { + const result = globalFunctions.inspect(toValue('hello')) + expect(result).toContain('hello') + }) +}) + +describe('collections', () => { + test('list creates array from arguments', () => { + expect(globalFunctions.list(1, 2, 3)).toEqual([1, 2, 3]) + expect(globalFunctions.list('a', 'b')).toEqual(['a', 'b']) + expect(globalFunctions.list()).toEqual([]) + }) + + test('dict creates object from named arguments', () => { + expect(globalFunctions.dict({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }) + expect(globalFunctions.dict()).toEqual({}) + }) + + test('at retrieves element at index', () => { + expect(globalFunctions.at([10, 20, 30], 0)).toBe(10) + expect(globalFunctions.at([10, 20, 30], 2)).toBe(30) + }) + + test('at retrieves property from object', () => { + expect(globalFunctions.at({ name: 'test' }, 'name')).toBe('test') + }) + + test('slice extracts array subset', () => { + expect(globalFunctions.slice([1, 2, 3, 4, 5], 1, 3)).toEqual([2, 3]) + expect(globalFunctions.slice([1, 2, 3, 4, 5], 2)).toEqual([3, 4, 5]) + }) + + test('range creates number sequence', () => { + expect(globalFunctions.range(0, 5)).toEqual([0, 1, 2, 3, 4, 5]) + expect(globalFunctions.range(3, 6)).toEqual([3, 4, 5, 6]) + }) + + test('range with single argument starts from 0', () => { + expect(globalFunctions.range(3, null)).toEqual([0, 1, 2, 3]) + expect(globalFunctions.range(0, null)).toEqual([0]) + }) +}) + +describe('enumerables', () => { + test('map transforms array elements', async () => { + const double = (x: number) => x * 2 + const result = await globalFunctions.map([1, 2, 3], double) + expect(result).toEqual([2, 4, 6]) + }) + + test('map works with async callbacks', async () => { + const asyncDouble = async (x: number) => { + await Promise.resolve() + return x * 2 + } + const result = await globalFunctions.map([1, 2, 3], asyncDouble) + expect(result).toEqual([2, 4, 6]) + }) + + test('map handles empty array', async () => { + const fn = (x: number) => x * 2 + const result = await globalFunctions.map([], fn) + expect(result).toEqual([]) + }) + + test('each iterates over array', async () => { + const results: number[] = [] + await globalFunctions.each([1, 2, 3], (x: number) => { + results.push(x * 2) + }) + expect(results).toEqual([2, 4, 6]) + }) + + test('each works with async callbacks', async () => { + const results: number[] = [] + await globalFunctions.each([1, 2, 3], async (x: number) => { + await Promise.resolve() + results.push(x * 2) + }) + expect(results).toEqual([2, 4, 6]) + }) + + test('each handles empty array', async () => { + let called = false + await globalFunctions.each([], () => { + called = true + }) + expect(called).toBe(false) + }) +}) + +describe('echo', () => { + test('echo logs arguments to console', () => { + const spy = mock(() => { }) + const originalLog = console.log + console.log = spy + + globalFunctions.echo('hello', 'world') + + expect(spy).toHaveBeenCalledWith('hello', 'world') + console.log = originalLog + }) + + test('echo returns null value', () => { + const originalLog = console.log + console.log = () => { } + + const result = globalFunctions.echo('test') + + expect(result).toEqual(toValue(null)) + console.log = originalLog + }) + + test('echo formats array values', () => { + const spy = mock(() => { }) + const originalLog = console.log + console.log = spy + + globalFunctions.echo(toValue([1, 2, 3])) + + // Should format the array, not just log the raw value + expect(spy).toHaveBeenCalled() + // @ts-ignore + const logged = spy.mock.calls[0][0] + // @ts-ignore + expect(logged).toContain('list') + + console.log = originalLog + }) +}) + +describe('formatValue', () => { + test('formats string with quotes', () => { + const result = formatValue(toValue('hello')) + expect(result).toContain('hello') + expect(result).toContain("'") + }) + + test('formats numbers', () => { + const result = formatValue(toValue(42)) + expect(result).toContain('42') + }) + + test('formats booleans', () => { + expect(formatValue(toValue(true))).toContain('true') + expect(formatValue(toValue(false))).toContain('false') + }) + + test('formats null', () => { + const result = formatValue(toValue(null)) + expect(result).toContain('null') + }) + + test('formats arrays', () => { + const result = formatValue(toValue([1, 2, 3])) + expect(result).toContain('list') + }) + + test('formats nested arrays with parentheses', () => { + const inner = toValue([1, 2]) + const outer = toValue([inner]) + const result = formatValue(outer) + expect(result).toContain('list') + expect(result).toContain('(') + expect(result).toContain(')') + }) + + test('formats dicts', () => { + const dict = new Map([ + ['name', toValue('test')], + ['age', toValue(42)] + ]) + const result = formatValue({ type: 'dict', value: dict }) + expect(result).toContain('dict') + expect(result).toContain('name=') + expect(result).toContain('age=') + }) + + test('escapes single quotes in strings', () => { + const result = formatValue(toValue("it's")) + expect(result).toContain("\\'") + }) +})