UNPKG

context

Version:

Lightweight context propagation for JavaScript and TypeScript. Create a scoped storage object, run code inside it, and read the active value anywhere down the call stack - without depending on React.

343 lines (294 loc) 9.3 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createCascade, CtxCascadeApi } from '../context'; describe('Cascading Context', () => { let ctx: CtxCascadeApi<any>; beforeEach(() => { ctx = createCascade(); }); describe('createCascade', () => { it('Should return a new context on each run', () => { expect(createCascade()).not.toBe(createCascade()); }); it('Should return all methods', () => { expect(createCascade()).toMatchSnapshot(); }); }); describe('context.run', () => { it('Should create a new context instance', () => { const top = ctx.use(); ctx.run({}, () => { expect(ctx.use()).not.toBe(top); }); }); it('Should pass no arguments to the callback', () => { ctx.run({}, (...args) => { expect(args).toHaveLength(0); }); }); it('Adds provided `ctxref` properties to current context level', () => { ctx.run( { id: 55, user: 'boomsa', }, () => { expect(ctx.use().id).toBe(55); expect(ctx.use().user).toBe('boomsa'); }, ); }); it('Returns undefined when property is not in context', () => { ctx.run( { id: 55, }, () => { expect(ctx.use().id).toBe(55); expect(ctx.use().user).toBeUndefined(); }, ); }); it('Should clear context after callback run', () => { expect(ctx.use()).toBeUndefined(); ctx.run({ a: 1 }, () => { expect(ctx.use()).toMatchSnapshot(); ctx.run({ b: 2 }, () => { expect(ctx.use()).toMatchSnapshot(); }); }); expect(ctx.use()).toBeUndefined(); }); describe('Context nesting', () => { it('Should refer to closest defined value', () => { ctx.run( { id: 99, name: 'watermelonbunny', }, () => { expect(ctx.use().id).toBe(99); expect(ctx.use().name).toBe('watermelonbunny'); ctx.run( { name: 'Emanuelle', color: 'blue', }, () => { expect(ctx.use().id).toBe(99); expect(ctx.use().name).toBe('Emanuelle'); expect(ctx.use().color).toBe('blue'); ctx.run({}, () => { expect(ctx.use().id).toBe(99); expect(ctx.use().name).toBe('Emanuelle'); expect(ctx.use().color).toBe('blue'); }); }, ); }, ); }); it('Should return previous context value after nested context run', () => { ctx.run( { id: 99, name: 'watermelonbunny', }, () => { ctx.run( { name: 'Emanuelle', color: 'blue', }, () => { ctx.run({}, () => null); expect(ctx.use().id).toBe(99); expect(ctx.use().name).toBe('Emanuelle'); expect(ctx.use().color).toBe('blue'); expect(ctx.use()).toMatchInlineSnapshot(` { "color": "blue", "id": 99, "name": "Emanuelle", } `); }, ); expect(ctx.use().id).toBe(99); expect(ctx.use().name).toBe('watermelonbunny'); expect(ctx.use()).toMatchInlineSnapshot(` { "id": 99, "name": "watermelonbunny", } `); }, ); }); }); }); describe('context.bind', () => { it('Returns a function', () => { expect(typeof ctx.bind({}, vi.fn())).toBe('function'); }); it('Wraps the function with context', () => { return new Promise<void>(done => { const fn = () => { expect(ctx.use()).toMatchInlineSnapshot(` { "value": 55, } `); done(); // this makes sure the function actually runs }; const bound = ctx.bind({ value: 55 }, fn); bound(); }); }); it('Passes runtime arguments to bound function', () => { const fn = vi.fn(); const args = Array.from({ length: 100 }, (_, i) => `${i}`); // 1-100 ctx.bind({}, fn)(...args); expect(fn).toHaveBeenCalledWith(...args); }); it('Maintains normal context behavior when runs within context.run', () => { return new Promise<void>(done => { const fn = () => { expect(ctx.use()).toMatchObject({ value: 200, value2: 300 }); expect(ctx.use()).toMatchInlineSnapshot(` { "value": 200, "value2": 300, } `); done(); }; const bound = ctx.bind({ value2: 300 }, fn); ctx.run({ value: 200, value2: 200 }, bound); }); }); }); describe('context.use', () => { describe('When in an active context', () => { it('Should return a cloned ctxRef object', () => { const ctxRef = { a: 1, b: 2 }; ctx.run(ctxRef, () => { expect(ctx.use()).toEqual(ctxRef); }); }); it('Should return a frozen context object', () => { const ctxRef = { a: 1, b: 2 }; ctx.run(ctxRef, () => { expect(Object.isFrozen(ctx.use())).toBe(true); }); }); describe('When before running the context', () => { it('Should return undefined', () => { expect(ctx.use()).toBeUndefined(); }); }); describe('When after closing the context', () => { it('Should return undefined', () => { ctx.run({}, () => {}); expect(ctx.use()).toBeUndefined(); }); }); }); }); describe('context.useX', () => { describe('When in an active context', () => { it('Should return a cloned ctxRef object', () => { const ctxRef = { a: 1, b: 2 }; ctx.run(ctxRef, () => { expect(ctx.useX()).toEqual(ctxRef); }); }); it('Should return a frozen context object', () => { const ctxRef = { a: 1, b: 2 }; ctx.run(ctxRef, () => { expect(Object.isFrozen(ctx.useX())).toBe(true); }); }); describe('When before running the context', () => { it('Should throw error', () => { expect(() => ctx.useX()).toThrow('Not inside of a running context.'); }); it('Should allow a custom context message', () => { expect(() => ctx.useX('Custom Failure Message')).toThrow( 'Custom Failure Message', ); }); }); describe('When after closing the context', () => { beforeEach(() => { ctx.run({}, () => {}); }); it('Should return undefined', () => { expect(() => ctx.useX()).toThrow('Not inside of a running context.'); }); it('Should allow a custom context message', () => { expect(() => ctx.useX('Custom Failure Message')).toThrow( 'Custom Failure Message', ); }); }); }); }); describe('init argument', () => { it('Should run init function on every context.run', () => { const init = vi.fn(); const ctx = createCascade(init); expect(init).not.toHaveBeenCalled(); ctx.run({}, () => { expect(init).toHaveBeenCalledTimes(1); ctx.run({}, () => { expect(init).toHaveBeenCalledTimes(2); ctx.run({}, () => { expect(init).toHaveBeenCalledTimes(3); }); }); }); expect(init).toHaveBeenCalledTimes(3); ctx.run({}, () => { expect(init).toHaveBeenCalledTimes(4); }); expect(init).toHaveBeenCalledTimes(4); }); it('Should accept ctxRef as first argument', () => { const init = vi.fn(); const ctx = createCascade(init); const ref1 = { a: 1, b: 2 }; const ref2 = { a: 2, b: 3 }; ctx.run(ref1, () => { ctx.run(ref2, () => null); }); expect(init.mock.calls[0][0]).toBe(ref1); expect(init.mock.calls[1][0]).toBe(ref2); }); it('Should accept parentContext as second argument', () => { const init = vi.fn(); const ctx = createCascade(init); let p1; ctx.run({}, () => { p1 = ctx.use(); ctx.run({}, () => null); }); expect(init.mock.calls[0][1]).toBeUndefined(); expect(init.mock.calls[1][1]).toBe(p1); }); it('When not nullish, should use init value as ctxRef', () => { const ctx = createCascade<{ override?: boolean; value?: string }>(() => ({ override: true, })); ctx.run({ value: 'x' }, () => { expect(ctx.useX().override).toBe(true); expect(ctx.useX().value).toBeUndefined(); }); }); it('When nullish, should default to ctxRef', () => { const ctx = createCascade(() => null); ctx.run({ value: 'x' }, () => { expect(ctx.useX().value).toBe('x'); }); }); }); });