a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 420 lines 14 kB view raw
1import { evaluate, evaluateStatements, EvaluationError } from "$core/evaluator"; 2import { signal } from "$core/signal"; 3import type { Scope, Signal } from "$types/volt"; 4import { beforeEach, describe, expect, it } from "vitest"; 5 6describe("Evaluator - Functional Tests", () => { 7 let scope: Scope; 8 9 beforeEach(() => { 10 scope = {}; 11 }); 12 13 describe("Literals", () => { 14 it("should evaluate number literals", () => { 15 expect(evaluate("42", scope)).toBe(42); 16 expect(evaluate("-10", scope)).toBe(-10); 17 expect(evaluate("3.14", scope)).toBe(3.14); 18 }); 19 20 it("should evaluate string literals", () => { 21 expect(evaluate("'hello'", scope)).toBe("hello"); 22 expect(evaluate("\"world\"", scope)).toBe("world"); 23 expect(evaluate("\"hello world\"", scope)).toBe("hello world"); 24 }); 25 26 it("should evaluate boolean literals", () => { 27 expect(evaluate("true", scope)).toBe(true); 28 expect(evaluate("false", scope)).toBe(false); 29 }); 30 31 it("should evaluate null and undefined", () => { 32 expect(evaluate("null", scope)).toBe(null); 33 expect(evaluate("undefined", scope)).toBe(undefined); 34 }); 35 }); 36 37 describe("Arithmetic Operators", () => { 38 it("should handle addition", () => { 39 expect(evaluate("1 + 2", scope)).toBe(3); 40 expect(evaluate("10 + 5", scope)).toBe(15); 41 }); 42 43 it("should handle subtraction", () => { 44 expect(evaluate("10 - 5", scope)).toBe(5); 45 expect(evaluate("5 - 10", scope)).toBe(-5); 46 }); 47 48 it("should handle multiplication", () => { 49 expect(evaluate("3 * 4", scope)).toBe(12); 50 expect(evaluate("10 * 0", scope)).toBe(0); 51 }); 52 53 it("should handle division", () => { 54 expect(evaluate("10 / 2", scope)).toBe(5); 55 expect(evaluate("7 / 2", scope)).toBe(3.5); 56 }); 57 58 it("should handle modulo", () => { 59 expect(evaluate("10 % 3", scope)).toBe(1); 60 expect(evaluate("7 % 2", scope)).toBe(1); 61 }); 62 63 it("should respect operator precedence", () => { 64 expect(evaluate("2 + 3 * 4", scope)).toBe(14); 65 expect(evaluate("(2 + 3) * 4", scope)).toBe(20); 66 }); 67 }); 68 69 describe("Comparison Operators", () => { 70 it("should handle equality", () => { 71 expect(evaluate("5 === 5", scope)).toBe(true); 72 expect(evaluate("5 === 6", scope)).toBe(false); 73 expect(evaluate("5 !== 6", scope)).toBe(true); 74 expect(evaluate("5 !== 5", scope)).toBe(false); 75 }); 76 77 it("should handle relational operators", () => { 78 expect(evaluate("5 < 10", scope)).toBe(true); 79 expect(evaluate("10 < 5", scope)).toBe(false); 80 expect(evaluate("5 > 3", scope)).toBe(true); 81 expect(evaluate("3 > 5", scope)).toBe(false); 82 expect(evaluate("5 <= 5", scope)).toBe(true); 83 expect(evaluate("5 >= 5", scope)).toBe(true); 84 }); 85 }); 86 87 describe("Logical Operators", () => { 88 it("should handle AND operator", () => { 89 expect(evaluate("true && true", scope)).toBe(true); 90 expect(evaluate("true && false", scope)).toBe(false); 91 expect(evaluate("false && true", scope)).toBe(false); 92 }); 93 94 it("should handle OR operator", () => { 95 expect(evaluate("true || false", scope)).toBe(true); 96 expect(evaluate("false || true", scope)).toBe(true); 97 expect(evaluate("false || false", scope)).toBe(false); 98 }); 99 100 it("should handle NOT operator", () => { 101 expect(evaluate("!true", scope)).toBe(false); 102 expect(evaluate("!false", scope)).toBe(true); 103 expect(evaluate("!!true", scope)).toBe(true); 104 }); 105 }); 106 107 describe("Ternary Operator", () => { 108 it("should evaluate ternary expressions", () => { 109 expect(evaluate("true ? 'yes' : 'no'", scope)).toBe("yes"); 110 expect(evaluate("false ? 'yes' : 'no'", scope)).toBe("no"); 111 expect(evaluate("5 > 3 ? 'greater' : 'lesser'", scope)).toBe("greater"); 112 }); 113 114 it("should handle nested ternaries", () => { 115 expect(evaluate("true ? (false ? 'a' : 'b') : 'c'", scope)).toBe("b"); 116 }); 117 }); 118 119 describe("Variable Access", () => { 120 it("should access scope variables", () => { 121 scope.name = "Alice"; 122 scope.age = 30; 123 expect(evaluate("name", scope)).toBe("Alice"); 124 expect(evaluate("age", scope)).toBe(30); 125 }); 126 127 it("should return undefined for missing variables", () => { 128 expect(evaluate("missing", scope)).toBe(undefined); 129 }); 130 131 it("should handle variables in expressions", () => { 132 scope.x = 10; 133 scope.y = 5; 134 expect(evaluate("x + y", scope)).toBe(15); 135 expect(evaluate("x * y", scope)).toBe(50); 136 }); 137 }); 138 139 describe("Property Access", () => { 140 it("should access object properties with dot notation", () => { 141 scope.user = { name: "Bob", age: 25 }; 142 expect(evaluate("user.name", scope)).toBe("Bob"); 143 expect(evaluate("user.age", scope)).toBe(25); 144 }); 145 146 it("should access object properties with bracket notation", () => { 147 scope.user = { name: "Charlie", age: 35 }; 148 expect(evaluate("user['name']", scope)).toBe("Charlie"); 149 expect(evaluate("user['age']", scope)).toBe(35); 150 }); 151 152 it("should access nested properties", () => { 153 scope.data = { user: { profile: { name: "Dave" } } }; 154 expect(evaluate("data.user.profile.name", scope)).toBe("Dave"); 155 }); 156 157 it("should access array elements", () => { 158 scope.items = [10, 20, 30]; 159 expect(evaluate("items[0]", scope)).toBe(10); 160 expect(evaluate("items[1]", scope)).toBe(20); 161 expect(evaluate("items[2]", scope)).toBe(30); 162 }); 163 }); 164 165 describe("Function Calls", () => { 166 it("should call scope functions", () => { 167 scope.double = (x: number) => x * 2; 168 expect(evaluate("double(5)", scope)).toBe(10); 169 }); 170 171 it("should call functions with multiple arguments", () => { 172 scope.add = (a: number, b: number) => a + b; 173 expect(evaluate("add(3, 7)", scope)).toBe(10); 174 }); 175 176 it("should call object methods", () => { 177 scope.calc = { multiply: (a: number, b: number) => a * b }; 178 expect(evaluate("calc.multiply(4, 5)", scope)).toBe(20); 179 }); 180 181 it("should call safe global functions", () => { 182 expect(evaluate("Math.max(10, 20)", scope)).toBe(20); 183 expect(evaluate("Math.min(10, 20)", scope)).toBe(10); 184 expect(evaluate("Math.abs(-5)", scope)).toBe(5); 185 }); 186 }); 187 188 describe("Array Literals", () => { 189 it("should create array literals", () => { 190 const result = evaluate("[1, 2, 3]", scope); 191 expect(result).toEqual([1, 2, 3]); 192 }); 193 194 it("should handle empty arrays", () => { 195 const result = evaluate("[]", scope); 196 expect(result).toEqual([]); 197 }); 198 199 it("should handle arrays with expressions", () => { 200 scope.x = 10; 201 const result = evaluate("[x, x + 1, x + 2]", scope); 202 expect(result).toEqual([10, 11, 12]); 203 }); 204 205 it("should handle spread in arrays", () => { 206 scope.arr = [2, 3, 4]; 207 const result = evaluate("[1, ...arr, 5]", scope); 208 expect(result).toEqual([1, 2, 3, 4, 5]); 209 }); 210 }); 211 212 describe("Object Literals", () => { 213 it("should create object literals", () => { 214 const result = evaluate("{ name: 'Alice', age: 30 }", scope); 215 expect(result).toEqual({ name: "Alice", age: 30 }); 216 }); 217 218 it("should handle empty objects", () => { 219 const result = evaluate("{}", scope); 220 expect(result).toEqual({}); 221 }); 222 223 it("should handle objects with computed values", () => { 224 scope.x = 10; 225 const result = evaluate("{ value: x, double: x * 2 }", scope); 226 expect(result).toEqual({ value: 10, double: 20 }); 227 }); 228 229 it("should handle spread in objects", () => { 230 scope.base = { a: 1, b: 2 }; 231 const result = evaluate("{ ...base, c: 3 }", scope); 232 expect(result).toEqual({ a: 1, b: 2, c: 3 }); 233 }); 234 }); 235 236 describe("Arrow Functions", () => { 237 it("should support arrow functions", () => { 238 const fn = evaluate("(x) => x * 2", scope) as (x: number) => number; 239 expect(fn(5)).toBe(10); 240 }); 241 242 it("should support arrow functions with no parameters", () => { 243 const fn = evaluate("() => 42", scope) as () => number; 244 expect(fn()).toBe(42); 245 }); 246 247 it("should support arrow functions with multiple parameters", () => { 248 const fn = evaluate("(a, b) => a + b", scope) as (a: number, b: number) => number; 249 expect(fn(3, 7)).toBe(10); 250 }); 251 252 it("should support arrow functions that capture scope", () => { 253 scope.multiplier = 3; 254 const fn = evaluate("(x) => x * multiplier", scope) as (x: number) => number; 255 expect(fn(5)).toBe(15); 256 }); 257 }); 258 259 describe("Signal Auto-Unwrapping", () => { 260 it("should auto-unwrap signals on read", () => { 261 scope.count = signal(10); 262 expect(evaluate("count", scope)).toBe(10); 263 }); 264 265 it("should auto-unwrap signals in expressions", () => { 266 scope.count = signal(5); 267 expect(evaluate("count + 10", scope)).toBe(15); 268 expect(evaluate("count * 2", scope)).toBe(10); 269 }); 270 271 it("should auto-unwrap nested signal properties", () => { 272 scope.user = signal({ name: "Alice", age: 30 }); 273 expect(evaluate("user.name", scope)).toBe("Alice"); 274 expect(evaluate("user.age", scope)).toBe(30); 275 }); 276 277 it("should allow signal.set() calls", () => { 278 scope.count = signal(10); 279 evaluateStatements("count.set(20)", scope); 280 expect((scope.count as Signal<number>).get()).toBe(20); 281 }); 282 283 it("should support strict equality comparisons with signals", () => { 284 scope.status = signal("active"); 285 scope.page = signal("home"); 286 expect(evaluate("status === 'active'", scope)).toBe(true); 287 expect(evaluate("status === 'inactive'", scope)).toBe(false); 288 expect(evaluate("page === 'home'", scope)).toBe(true); 289 expect(evaluate("page === 'about'", scope)).toBe(false); 290 }); 291 292 it("should support loose equality comparisons with signals", () => { 293 scope.status = signal("active"); 294 expect(evaluate("status == 'active'", scope)).toBe(true); 295 expect(evaluate("status == 'inactive'", scope)).toBe(false); 296 }); 297 298 it("should support spreading signals containing arrays", () => { 299 scope.items = signal([2, 3, 4]); 300 const result = evaluate("[1, ...items, 5]", scope); 301 expect(result).toEqual([1, 2, 3, 4, 5]); 302 }); 303 304 it("should support spreading signals in complex expressions", () => { 305 scope.todos = signal([{ id: 1, text: "Learn" }, { id: 2, text: "Build" }]); 306 scope.newTodo = { id: 3, text: "Ship" }; 307 const result = evaluate("[...todos, newTodo]", scope); 308 expect(result).toEqual([{ id: 1, text: "Learn" }, { id: 2, text: "Build" }, { id: 3, text: "Ship" }]); 309 }); 310 311 it("should support iterating over signals containing arrays", () => { 312 scope.items = signal([1, 2, 3]); 313 const result = evaluate("[...items].map(x => x * 2)", scope); 314 expect(result).toEqual([2, 4, 6]); 315 }); 316 317 it("should handle spreading non-iterable signals gracefully", () => { 318 scope.count = signal(42); 319 expect(() => evaluate("[...count]", scope)).toThrow(); 320 }); 321 322 it("should unwrap signals in object literals when unwrapSignals is false", () => { 323 scope.id = signal(42); 324 scope.name = signal("Alice"); 325 const result = evaluate("{id: id, name: name}", scope, { unwrapSignals: false }); 326 expect(result).toEqual({ id: 42, name: "Alice" }); 327 }); 328 329 it("should unwrap signals in complex object literals", () => { 330 scope.todoId = signal(3); 331 scope.todoText = signal("New task"); 332 scope.todoDone = signal(false); 333 const result = evaluate("{id: todoId, text: todoText, done: todoDone}", scope, { unwrapSignals: false }); 334 expect(result).toEqual({ id: 3, text: "New task", done: false }); 335 }); 336 337 it("should not unwrap method calls in object literals", () => { 338 scope.text = signal(" hello "); 339 const result = evaluate("{value: text.trim()}", scope, { unwrapSignals: false }); 340 expect(result).toEqual({ value: "hello" }); 341 }); 342 }); 343 344 describe("Expression Caching", () => { 345 it("should cache compiled expressions", () => { 346 const expr = "x + y"; 347 scope.x = 10; 348 scope.y = 5; 349 350 const result1 = evaluate(expr, scope); 351 const result2 = evaluate(expr, scope); 352 353 expect(result1).toBe(15); 354 expect(result2).toBe(15); 355 }); 356 357 it("should cache statement expressions separately", () => { 358 scope.x = 10; 359 360 evaluateStatements("x = 20", scope); 361 expect(scope.x).toBe(20); 362 363 const result = evaluate("x", scope); 364 expect(result).toBe(20); 365 }); 366 }); 367 368 describe("Statement Evaluation", () => { 369 it("should execute single statements", () => { 370 scope.x = 10; 371 evaluateStatements("x = 20", scope); 372 expect(scope.x).toBe(20); 373 }); 374 375 it("should execute multiple statements", () => { 376 scope.x = 1; 377 scope.y = 2; 378 evaluateStatements("x = 10; y = 20", scope); 379 expect(scope.x).toBe(10); 380 expect(scope.y).toBe(20); 381 }); 382 383 it("should allow function calls in statements", () => { 384 scope.log = []; 385 scope.add = (value: number) => { 386 (scope.log as number[]).push(value); 387 }; 388 evaluateStatements("add(1); add(2); add(3)", scope); 389 expect(scope.log).toEqual([1, 2, 3]); 390 }); 391 }); 392 393 describe("Error Handling", () => { 394 it("should throw EvaluationError for invalid syntax", () => { 395 expect(() => evaluate("1 +", scope)).toThrow(EvaluationError); 396 }); 397 398 it("should throw EvaluationError for runtime errors", () => { 399 expect(() => evaluate("undefined.property", scope)).toThrow(EvaluationError); 400 }); 401 402 it("should include expression in error message", () => { 403 try { 404 evaluate("1 +", scope); 405 } catch (error) { 406 expect(error).toBeInstanceOf(EvaluationError); 407 expect((error as EvaluationError).expr).toBe("1 +"); 408 } 409 }); 410 411 it("should preserve original error cause", () => { 412 try { 413 evaluate("undefined.property", scope); 414 } catch (error) { 415 expect(error).toBeInstanceOf(EvaluationError); 416 expect((error as EvaluationError).cause).toBeDefined(); 417 } 418 }); 419 }); 420});