UNPKG

test-fixture-factory

Version:

A minimal library for creating and managing test fixtures using Vitest, enabling structured, repeatable, and efficient testing processes.

302 lines (295 loc) 7.94 kB
// src/yn.ts var yn = (input) => { if (input === void 0 || input === null) { return false; } const value = String(input).trim(); if (/^(?:y|yes|true|1|on)$/i.test(value)) { return true; } if (/^(?:n|no|false|0|off)$/i.test(value)) { return false; } return false; }; // src/env-var.ts var getSkipDestroyEnvVar = () => { if (typeof process !== "undefined" && "env" in process) { return yn(process.env.TFF_SKIP_DESTROY); } return false; }; var SKIP_DESTROY = getSkipDestroyEnvVar(); // src/field.ts var FieldBuilder = class _FieldBuilder { state; constructor(field) { this.state = field; } // Implementation from(keyOrList, fn) { const [key, fixtureList] = Array.isArray(keyOrList) ? [keyOrList[0], keyOrList] : [keyOrList, [keyOrList]]; const getValueFromContext = fn ?? ((ctx) => ctx[key]); return new _FieldBuilder({ ...this.state, isRequired: true, fixtureList, getValueFromContext }); } // Implementation maybeFrom(keyOrList, fn) { const [key, fixtureList] = Array.isArray(keyOrList) ? [keyOrList[0], keyOrList] : [keyOrList, [keyOrList]]; const getValueFromContext = fn ?? ((ctx) => ctx[key]); return new _FieldBuilder({ ...this.state, isRequired: true, fixtureList, getValueFromContext }); } optional() { return new _FieldBuilder({ ...this.state, isRequired: false }); } default(defaultValue) { return new _FieldBuilder({ ...this.state, isRequired: true, defaultValue }); } }; var createFieldBuilder = () => ({ type() { return new FieldBuilder({ fixtureList: [], isRequired: true, defaultValue: void 0, getValueFromContext: void 0 }); } }); // src/schema-utils.ts var createSchema = () => ({ empty: () => { return {}; }, with: (schemaFn) => { return schemaFn( createFieldBuilder() ); } }); var deleteUndefinedKeys = (value) => { for (const key in value) { if (value[key] === void 0) { delete value[key]; } } return value; }; var schemaEntries = (s) => { return Object.entries(s).map(([key, value]) => [ key, value.state ]); }; var schemaValues = (s) => { return Object.values(s).map( (value) => value.state ); }; var resolveDefaultValues = (schema) => { return deleteUndefinedKeys( Object.fromEntries( schemaEntries(schema).map(([key, value]) => { const defaultValue = typeof value.defaultValue === "function" ? value.defaultValue() : value.defaultValue; return [key, defaultValue]; }) ) ); }; var resolveValuesFromContext = (schema, context) => { return deleteUndefinedKeys( Object.fromEntries( schemaEntries(schema).map(([key, value]) => { const getValueFromContext = typeof value.getValueFromContext === "function" ? value.getValueFromContext : void 0; if (getValueFromContext) { const valueFromContext = getValueFromContext(context); return [key, valueFromContext]; } return [key, void 0]; }) ) ); }; var validateSchemaData = (schema, data) => { return schemaEntries(schema).filter(([key, field]) => { if (!field.isRequired) { return false; } if (typeof data[key] !== "undefined") { return false; } return true; }).map(([key, field]) => ({ key, fixtureList: field.fixtureList })); }; var resolveSchema = (schema, context, attrs) => { const result = { ...resolveDefaultValues(schema), ...resolveValuesFromContext(schema, context), ...deleteUndefinedKeys({ ...attrs }) }; return result; }; var getFixtureList = (schema) => { const set = /* @__PURE__ */ new Set([]); for (const field of schemaValues(schema)) { for (const fixtureList of field.fixtureList) { set.add(fixtureList); } } const list = Array.from(set); return list; }; // src/undefined-field-error.ts var formatMissingField = (field) => { const { key, fixtureList } = field; if (fixtureList.length === 0) { return `- ${key}: must be provided as an attribute`; } return `- ${key}: must be provided as an attribute or via the test context (${fixtureList.join(", ")})`; }; var UndefinedFieldError = class extends Error { constructor(name, missingFields) { const count = missingFields.length; super( `[${name}] ${count} required field(s) have undefined values: ${missingFields.map(formatMissingField).join("\n")}` ); } }; // src/wrap-fixture-fn.ts var wrapFixtureFn = (schema, fn) => { const wrapped = (...args) => { return fn(...args); }; const fixtureList = getFixtureList(schema); const fixtureListString = fixtureList.length === 0 ? "" : ` ${fixtureList.join(", ")} `; wrapped.toString = () => { return `({${fixtureListString}}) => true`; }; return wrapped; }; // src/create-factory.ts var defaultFactoryOptions = { shouldDestroy: !SKIP_DESTROY }; var FactoryBuilder = class _FactoryBuilder { state; constructor(state) { this.state = state; this.useCreateValue = this.useCreateValue.bind(this); this.useValue = this.useValue.bind(this); } withContext() { return new _FactoryBuilder(this.state); } withSchema(schemaFn) { return new _FactoryBuilder({ ...this.state, factoryFn: void 0, schema: createSchema().with(schemaFn) }); } withValue(factoryFn) { return new _FactoryBuilder({ ...this.state, factoryFn }); } async build(attrs, context) { const { name, schema, factoryFn } = this.state; if (!factoryFn) { throw new Error(".withValue() must be called before .build()"); } const data = resolveSchema(schema, context ?? {}, attrs); const errorList = validateSchemaData(schema, data); if (errorList.length > 0) { throw new UndefinedFieldError(name, errorList); } const { value, destroy } = await factoryFn(data); return { value, destroy: destroy ?? (() => Promise.resolve()) }; } useCreateValue(presetAttrs, { shouldDestroy } = defaultFactoryOptions) { const { name, schema, factoryFn } = this.state; if (!factoryFn) { throw new Error(".withValue() must be called before .useCreateValue()"); } return wrapFixtureFn(schema, async (context, use) => { const destroyList = []; await use((async (attrs) => { const data = resolveSchema(schema, context, { ...presetAttrs ?? {}, ...attrs }); const errorList = validateSchemaData(schema, data); if (errorList.length > 0) { throw new UndefinedFieldError(name, errorList); } const { value, destroy } = await factoryFn(data); if (destroy) { destroyList.push(destroy); } return value; })); if (shouldDestroy) { for (const destroy of destroyList) { await destroy(); } } }); } useValue(attrs, options = defaultFactoryOptions) { const { name, schema, factoryFn } = this.state; const { shouldDestroy } = options; if (!factoryFn) { throw new Error(".withValue() must be called before .useValue()"); } return wrapFixtureFn(schema, async (context, use) => { const data = resolveSchema(schema, context, attrs); const errorList = validateSchemaData(schema, data); if (errorList.length > 0) { throw new UndefinedFieldError(name, errorList); } const { value, destroy } = await factoryFn(data); await use(value); if (shouldDestroy) { await destroy?.(); } }); } }; var createFactory = (name) => { if (name.trim().length === 0) { throw new Error("createFactory: name should be a non-empty string"); } return new FactoryBuilder({ name, schema: {}, factoryFn: void 0 }); }; export { UndefinedFieldError, createFactory };