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
JavaScript
// 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
};