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
text/typescript
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');
});
});
});
});