Files
welshman/packages/lib/__tests__/Tools.test.ts
T
2025-04-09 11:17:12 -07:00

351 lines
10 KiB
TypeScript

import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import * as T from "../src/Tools"
describe("Tools", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
describe("Basic Utils", () => {
it("should handle ifLet", () => {
const fn = vi.fn()
T.ifLet(undefined, fn)
expect(fn).not.toHaveBeenCalled()
T.ifLet(5, fn)
expect(fn).toHaveBeenCalledWith(5)
})
it("should handle array operations", () => {
const arr = [1, 2, 3]
expect(T.first(arr)).toBe(1)
expect(T.last(arr)).toBe(3)
})
})
describe("Math Operations", () => {
it("should handle basic math operations", () => {
expect(T.add(2, 3)).toBe(5)
expect(T.sub(5, 3)).toBe(2)
expect(T.mul(2, 3)).toBe(6)
expect(T.div(6, 2)).toBe(3)
})
it("should handle nil values in math operations", () => {
expect(T.add(undefined, 3)).toBe(3)
expect(T.sub(5, undefined)).toBe(5)
expect(T.mul(undefined, undefined)).toBe(0)
})
it("should handle comparisons", () => {
expect(T.lt(2, 3)).toBe(true)
expect(T.gt(3, 2)).toBe(true)
expect(T.lte(2, 2)).toBe(true)
expect(T.gte(2, 2)).toBe(true)
})
})
describe("Array Operations", () => {
it("should handle array transformations", () => {
expect(T.take(2, [1, 2, 3, 4])).toEqual([1, 2])
expect(T.drop(2, [1, 2, 3, 4])).toEqual([3, 4])
expect(T.uniq([1, 1, 2, 2, 3])).toEqual([1, 2, 3])
})
it("should handle chunk operations", () => {
expect(T.chunk(2, [1, 2, 3, 4])).toEqual([
[1, 2],
[3, 4],
])
expect(T.chunks(2, [1, 2, 3, 4])).toEqual([
[1, 3],
[2, 4],
])
})
it("should handle array sorting", () => {
expect(T.sort([3, 1, 2])).toEqual([1, 2, 3])
expect(T.sortBy(x => x.value, [{value: 3}, {value: 1}, {value: 2}])).toEqual([
{value: 1},
{value: 2},
{value: 3},
])
})
})
describe("Object Operations", () => {
it("should handle object transformations", () => {
const obj = {a: 1, b: 2, c: 3}
expect(T.omit(["a"], obj)).toEqual({b: 2, c: 3})
expect(T.pick(["a"], obj)).toEqual({a: 1})
})
it("should handle deep merging", () => {
const a = {x: {y: 1}}
const b = {x: {z: 2}}
expect(T.deepMergeLeft(a, b)).toEqual({x: {y: 1, z: 2}})
})
})
describe("Batch Operations", () => {
it("should handle memoization", () => {
const fn = vi.fn(x => x * 2)
const memoized = T.memoize(fn)
expect(memoized(2)).toBe(4)
expect(memoized(2)).toBe(4)
expect(fn).toHaveBeenCalledTimes(1)
})
it("should handle throttling", async () => {
const fn = vi.fn()
const throttled = T.throttle(100, fn)
throttled()
throttled()
throttled()
expect(fn).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(200)
expect(fn).toHaveBeenCalledTimes(2)
})
describe("batch", () => {
it("should collect items and process them in batches", async () => {
const processBatch = vi.fn()
const batchFn = T.batch(100, processBatch)
// Add items
batchFn("a")
batchFn("b")
batchFn("c")
// Initially the batch shouldn't be processed
expect(processBatch).toHaveBeenCalledTimes(1)
// Advance timer to trigger batch processing
await vi.advanceTimersByTimeAsync(100)
expect(processBatch).toHaveBeenCalledTimes(2)
expect(processBatch).toHaveBeenCalledWith(["a"])
expect(processBatch).toHaveBeenCalledWith(["b", "c"])
})
it("should handle multiple batch windows", async () => {
const processBatch = vi.fn()
const batchFn = T.batch(100, processBatch)
// First batch
batchFn("a")
batchFn("b")
batchFn("c")
await vi.advanceTimersByTimeAsync(100)
// Second batch
batchFn("d")
batchFn("e")
batchFn("f")
await vi.advanceTimersByTimeAsync(100)
expect(processBatch).toHaveBeenCalledTimes(4)
expect(processBatch).toHaveBeenCalledWith(["a"])
expect(processBatch).toHaveBeenCalledWith(["b", "c"])
expect(processBatch).toHaveBeenCalledWith(["d"])
expect(processBatch).toHaveBeenCalledWith(["e", "f"])
})
})
describe("batcher", () => {
it("should batch requests and return results", async () => {
const executeFn = vi.fn(async (requests: number[]) => requests.map(x => x * 2))
const batchFn = T.batcher(100, executeFn)
// Create multiple concurrent requests
const promise1 = batchFn(1)
const promise2 = batchFn(2)
const promise3 = batchFn(3)
await vi.advanceTimersByTimeAsync(100)
const results = await Promise.all([promise1, promise2, promise3])
expect(executeFn).toHaveBeenCalledTimes(1)
expect(executeFn).toHaveBeenCalledWith([1, 2, 3])
expect(results).toEqual([2, 4, 6])
})
it("should handle multiple batch windows", async () => {
const executeFn = vi.fn(async (requests: number[]) => requests.map(x => x * 2))
const batchFn = T.batcher(100, executeFn)
// First batch
const batch1Promise = Promise.all([batchFn(1), batchFn(2)])
await vi.advanceTimersByTimeAsync(100)
const batch1Results = await batch1Promise
// Second batch
const batch2Promise = Promise.all([batchFn(3), batchFn(4)])
await vi.advanceTimersByTimeAsync(100)
const batch2Results = await batch2Promise
expect(executeFn).toHaveBeenCalledTimes(2)
expect(batch1Results).toEqual([2, 4])
expect(batch2Results).toEqual([6, 8])
})
it.skip("should throw error if execute returns wrong number of results", async () => {
const executeFn = vi.fn(
async (requests: number[]) => [requests[0] * 2], // Return fewer results than requests
)
const batchFn = T.batcher(100, executeFn)
const promise = Promise.all([batchFn(1), batchFn(2)])
await vi.advanceTimersByTimeAsync(100)
await expect(promise).rejects.toMatch("Execute must return a result for each request")
})
})
describe("throttleWithValue", () => {
it("should return cached value between updates", async () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(100, getValue)
// First call should execute immediately
expect(throttledGet()).toBe(1)
expect(getValue).toHaveBeenCalledTimes(1)
// Subsequent calls within throttle window should return cached value
expect(throttledGet()).toBe(1)
expect(throttledGet()).toBe(1)
expect(getValue).toHaveBeenCalledTimes(1)
// After throttle window, should update value
await vi.advanceTimersByTimeAsync(100)
// the previous 2 called will have been batched, and the next throttledGet increase the counter to 3
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
it("should update value at most once per throttle window", async () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(100, getValue)
// Initial value
expect(throttledGet()).toBe(1)
// Multiple calls within window
for (let i = 0; i < 4; i++) {
throttledGet()
await vi.advanceTimersByTimeAsync(20) // 20ms each, still within 100ms window
}
expect(getValue).toHaveBeenCalledTimes(1)
// After window
await vi.advanceTimersByTimeAsync(100)
// the previous called will have been batched, and the next throttledGet increase the counter to 3
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
it("should handle zero throttle time", () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(0, getValue)
// Should update on every call
expect(throttledGet()).toBe(1)
expect(throttledGet()).toBe(2)
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
})
})
describe("Time Utilities", () => {
it("should handle time constants", () => {
expect(T.MINUTE).toBe(60)
expect(T.HOUR).toBe(60 * 60)
expect(T.DAY).toBe(24 * 60 * 60)
})
it("should handle time calculations", () => {
const timestamp = T.now()
expect(typeof timestamp).toBe("number")
expect(T.ms(1)).toBe(1000)
})
})
describe("String Operations", () => {
it("should handle URL formatting", () => {
expect(T.stripProtocol("https://example.com")).toBe("example.com")
expect(T.displayUrl("https://www.example.com/")).toBe("example.com")
expect(T.displayDomain("example.com/path")).toBe("example.com")
// @todo returns https
// expect(T.displayDomain("https://example.com/path")).toBe("example.com")
})
it("should handle string truncation", () => {
expect(T.ellipsize("hello world", 5)).toBe("hello...")
expect(T.ellipsize("hi", 5)).toBe("hi")
})
})
describe("Collection Operations", () => {
it("should handle group operations", () => {
const items = [
{type: "a", val: 1},
{type: "a", val: 2},
{type: "b", val: 3},
]
const grouped = T.groupBy(x => x.type, items)
expect(grouped.get("a")?.length).toBe(2)
expect(grouped.get("b")?.length).toBe(1)
})
it("should handle indexing", () => {
const items = [
{id: 1, val: "a"},
{id: 2, val: "b"},
]
const indexed = T.indexBy(x => x.id, items)
expect(indexed.get(1)?.val).toBe("a")
})
})
describe("Type Checking", () => {
it("should identify plain objects", () => {
expect(T.isPojo({})).toBe(true)
expect(T.isPojo([])).toBe(false)
expect(T.isPojo(null)).toBe(false)
expect(T.isPojo(new Date())).toBe(false)
})
it("should handle deep equality", () => {
expect(T.equals({a: 1}, {a: 1})).toBe(true)
expect(T.equals({a: 1}, {a: 2})).toBe(false)
expect(T.equals([1, 2], [1, 2])).toBe(true)
expect(T.equals(new Set([1, 2]), new Set([1, 2]))).toBe(true)
})
})
})