diff --git a/packages/adapters/src/chalk/stress.test.ts b/packages/adapters/src/chalk/stress.test.ts new file mode 100644 index 000000000..bc56f0798 --- /dev/null +++ b/packages/adapters/src/chalk/stress.test.ts @@ -0,0 +1,136 @@ +// packages/adapters/src/chalk/stress.test.ts +// Stress tests for chalk adapter under high-volume output. + +import { describe, it, expect, afterEach } from 'vitest'; +import { chalkToTermUI } from './index.js'; + +function buildAnsiString(sequence: string, text: string): string { + return `${sequence}${text}\x1B[0m`; +} + +const MIXED_SEQUENCES = [ + '\x1B[31m', // red + '\x1B[32m', // green + '\x1B[33m', // yellow + '\x1B[34m', // blue + '\x1B[35m', // magenta + '\x1B[36m', // cyan + '\x1B[1m', // bold + '\x1B[4m', // underline + '\x1B[7m', // reverse + '\x1B[9m', // strikethrough +]; + +function mixedAnsi(text: string): string { + let result = text; + for (const seq of MIXED_SEQUENCES) { + result = buildAnsiString(seq, result); + } + return result; +} + +describe('chalk adapter stress tests', () => { + afterEach(() => { + delete process.env.NO_COLOR; + }); + + it('handles 1,000 rapid successive calls without error', () => { + const originalNoColor = process.env.NO_COLOR; + delete process.env.NO_COLOR; + + const texts = Array.from({ length: 1000 }, (_, i) => `line-${i}`); + const ansiTexts = texts.map((t, i) => { + const seq = MIXED_SEQUENCES[i % MIXED_SEQUENCES.length]; + return buildAnsiString(seq, t); + }); + + expect(() => { + for (const input of ansiTexts) { + chalkToTermUI(input); + } + }).not.toThrow(); + }); + + it('handles 5,000 rapid successive calls without error or slowdown', () => { + const originalNoColor = process.env.NO_COLOR; + delete process.env.NO_COLOR; + + const texts = Array.from({ length: 5000 }, (_, i) => `output-${i}`); + + const start = performance.now(); + for (const text of texts) { + chalkToTermUI(mixedAnsi(text)); + } + const elapsed = performance.now() - start; + + // 5,000 mixed-ANSI calls should complete in under 500ms + expect(elapsed).toBeLessThan(500); + }); + + it('produces consistent output at 1,000-call scale', () => { + const originalNoColor = process.env.NO_COLOR; + delete process.env.NO_COLOR; + + const results: string[] = []; + for (let i = 0; i < 1000; i++) { + results.push(chalkToTermUI(mixedAnsi(`item-${i}`))); + } + + // Each result should contain the original text + expect(results[0]).toContain('item-0'); + expect(results[999]).toContain('item-999'); + + // All results should have ANSI codes intact (no NO_COLOR) + for (const result of results) { + expect(result).toContain('\x1B['); + } + }); + + it('strips ANSI when NO_COLOR is set during high-volume calls', () => { + process.env.NO_COLOR = '1'; + + const texts = Array.from({ length: 1000 }, (_, i) => `line-${i}`); + const ansiTexts = texts.map((t, i) => { + const seq = MIXED_SEQUENCES[i % MIXED_SEQUENCES.length]; + return buildAnsiString(seq, t); + }); + + const results: string[] = []; + for (const input of ansiTexts) { + results.push(chalkToTermUI(input)); + } + + // All results should be stripped of ANSI codes + for (const result of results) { + expect(result).not.toContain('\x1B['); + expect(result).toMatch(/^line-\d+$/); + } + }); + + it('handles deeply nested ANSI sequences', () => { + const originalNoColor = process.env.NO_COLOR; + delete process.env.NO_COLOR; + + // Stack all sequences 10 times + let deeplyNested = 'text'; + for (let i = 0; i < 10; i++) { + deeplyNested = mixedAnsi(deeplyNested); + } + + // Should not throw and should preserve text content + const result = chalkToTermUI(deeplyNested); + expect(result).toBeDefined(); + expect(result.length).toBeGreaterThan(0); + }); + + it('handles empty strings at high volume', () => { + const originalNoColor = process.env.NO_COLOR; + delete process.env.NO_COLOR; + + expect(() => { + for (let i = 0; i < 1000; i++) { + chalkToTermUI(''); + } + }).not.toThrow(); + }); +}); diff --git a/packages/motion/src/easing-benchmark.test.ts b/packages/motion/src/easing-benchmark.test.ts new file mode 100644 index 000000000..f48bb36d1 --- /dev/null +++ b/packages/motion/src/easing-benchmark.test.ts @@ -0,0 +1,84 @@ +// packages/motion/src/easing-benchmark.test.ts +// Performance benchmarks for easing functions at simulated 60fps. + +import { describe, it, expect } from 'vitest'; +import { stepSpring, springPreset } from './spring.js'; +import { easings } from './transitions.js'; + +const FRAME_DT_MS = 1000 / 60; // ~16.67ms per frame at 60fps +const FRAME_DT_S = FRAME_DT_MS / 1000; +const ITERATIONS = 10_000; + +/** Average time in milliseconds for a single easing tick */ +function benchmarkEasing(_name: string, fn: () => void): number { + const start = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + fn(); + } + const elapsed = performance.now() - start; + return elapsed / ITERATIONS; +} + +describe('easing benchmark at 60fps', () => { + it('spring tick completes within 1ms per frame', () => { + const config = springPreset('default'); + let state = { value: 0, velocity: 0, target: 1, done: false }; + let totalMs = 0; + + for (let i = 0; i < ITERATIONS; i++) { + const frameStart = performance.now(); + state = stepSpring(state, config, FRAME_DT_S); + totalMs += performance.now() - frameStart; + } + + const avgMs = totalMs / ITERATIONS; + expect(avgMs).toBeLessThan(1); + }); + + it('linear easing tick completes within 0.1ms per frame', () => { + let avgMs = benchmarkEasing('linear', () => { + easings.linear(0.5); + }); + expect(avgMs).toBeLessThan(0.1); + }); + + it('easeInOut easing tick completes within 0.1ms per frame', () => { + let avgMs = benchmarkEasing('easeInOut', () => { + easings.easeInOut(0.5); + }); + expect(avgMs).toBeLessThan(0.1); + }); + + it('spring reaches target within 60 frames at 60fps', () => { + const config = springPreset('default'); + let state = { value: 0, velocity: 0, target: 1, done: false }; + let frames = 0; + const maxFrames = 60; // 1 second at 60fps + + while (!state.done && frames < maxFrames) { + state = stepSpring(state, config, FRAME_DT_S); + frames++; + } + + expect(state.done).toBe(true); + expect(frames).toBeLessThanOrEqual(maxFrames); + expect(state.value).toBeCloseTo(1, 2); + }); + + it('linear interpolation is deterministic across frames', () => { + const progress: number[] = []; + for (let f = 0; f <= 10; f++) { + progress.push(easings.linear(f / 10)); + } + expect(progress[5]).toBe(0.5); + expect(progress[10]).toBe(1); + }); + + it('easeInOut produces symmetric curve', () => { + const midpoint = easings.easeInOut(0.5); + expect(midpoint).toBeCloseTo(0.5); + // easeInOut(t) + easeInOut(1-t) should equal 1 + expect(easings.easeInOut(0.25) + easings.easeInOut(0.75)).toBeCloseTo(1); + expect(easings.easeInOut(0.1) + easings.easeInOut(0.9)).toBeCloseTo(1); + }); +});