mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 07:03:38 +00:00
Reworked the test builder
This commit is contained in:
parent
c3a2c7024d
commit
579e4c32e9
@ -55,7 +55,9 @@ describe('Migration - draft and publish', () => {
|
||||
['with table modifications', { town: { type: 'string' } }, { color: { type: 'string' } }],
|
||||
])('%p', (testName, tableModification1, tableModification2) => {
|
||||
beforeAll(async () => {
|
||||
builder = await createTestBuilder()
|
||||
builder = createTestBuilder();
|
||||
|
||||
await builder
|
||||
.addContentType(dogModel)
|
||||
.addFixtures(dogModel.name, dogs)
|
||||
.build();
|
||||
|
||||
98
test/helpers/builder/action-registry.js
Normal file
98
test/helpers/builder/action-registry.js
Normal file
@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
|
||||
const { concat, merge, isFunction, map } = require('lodash/fp');
|
||||
const modelsUtils = require('../models');
|
||||
|
||||
const stringifyDates = object =>
|
||||
JSON.parse(
|
||||
JSON.stringify(object, (key, value) => {
|
||||
if (this[key] instanceof Date) {
|
||||
return this[key].toUTCString();
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
|
||||
const formatFixtures = map(stringifyDates);
|
||||
|
||||
module.exports = {
|
||||
ct: {
|
||||
create: contentType => {
|
||||
let createdModel;
|
||||
|
||||
return {
|
||||
async build(state) {
|
||||
createdModel = await modelsUtils.createContentType(contentType);
|
||||
return { ...state, models: [...state.models, createdModel] };
|
||||
},
|
||||
cleanup: () => modelsUtils.deleteContentType(createdModel.modelName),
|
||||
};
|
||||
},
|
||||
|
||||
createBatch: contentTypes => {
|
||||
let createdModels = [];
|
||||
|
||||
return {
|
||||
async build(state) {
|
||||
createdModels = await modelsUtils.createContentTypes(contentTypes);
|
||||
return { ...state, models: concat(state.models, createdModels) };
|
||||
},
|
||||
async cleanup() {
|
||||
for (const model of createdModels) {
|
||||
await modelsUtils.deleteContentType(model.modelName);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
createMany: contentTypes => {
|
||||
const createdModels = [];
|
||||
|
||||
return {
|
||||
async build(state) {
|
||||
for (const contentType of contentTypes) {
|
||||
createdModels.push(await modelsUtils.createContentType(contentType));
|
||||
}
|
||||
|
||||
return { ...state, models: concat(state.models, createdModels) };
|
||||
},
|
||||
async cleanup() {
|
||||
for (const model of createdModels) {
|
||||
await modelsUtils.deleteContentType(model.modelName);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
comp: {
|
||||
create: component => {
|
||||
let createdModel;
|
||||
|
||||
return {
|
||||
async build(state) {
|
||||
createdModel = await modelsUtils.createComponent(component);
|
||||
return { ...state, models: [...state.models, createdModel] };
|
||||
},
|
||||
cleanup: () => modelsUtils.deleteComponent(createdModel.uid),
|
||||
};
|
||||
},
|
||||
},
|
||||
fixtures: {
|
||||
create(modelName, entries, getFixtures) {
|
||||
let createdEntries = [];
|
||||
|
||||
return {
|
||||
async build(state) {
|
||||
createdEntries = formatFixtures(
|
||||
await modelsUtils.createFixturesFor(
|
||||
modelName,
|
||||
isFunction(entries) ? entries(getFixtures()) : entries
|
||||
)
|
||||
);
|
||||
return { ...state, fixtures: merge(state.fixtures, { [modelName]: createdEntries }) };
|
||||
},
|
||||
cleanup: () => modelsUtils.deleteFixturesFor(modelName, createdEntries),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,120 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { prop, isArray, curry, isNil, has, isFunction } = require('lodash/fp');
|
||||
const modelsUtils = require('../models');
|
||||
|
||||
const bindEvent = ({ event, fn, batch = false, metadata = {} } = {}) => {
|
||||
Object.assign(fn, {
|
||||
emitInfos: {
|
||||
event,
|
||||
batch,
|
||||
metadata,
|
||||
},
|
||||
});
|
||||
|
||||
return fn;
|
||||
};
|
||||
|
||||
const registry = {
|
||||
ct: {
|
||||
create: bindEvent({
|
||||
event: 'model.created',
|
||||
metadata: { type: 'ct' },
|
||||
fn: modelsUtils.createContentType,
|
||||
}),
|
||||
createBatch: bindEvent({
|
||||
event: 'model.created',
|
||||
metadata: { type: 'ct' },
|
||||
fn: modelsUtils.createContentTypes,
|
||||
batch: true,
|
||||
}),
|
||||
createMany: bindEvent({
|
||||
event: 'model.created',
|
||||
metadata: { type: 'ct' },
|
||||
fn: async cts => {
|
||||
const createdModels = [];
|
||||
|
||||
for (const ct of cts) {
|
||||
createdModels.push(await modelsUtils.createContentType(ct));
|
||||
}
|
||||
|
||||
return createdModels;
|
||||
},
|
||||
}),
|
||||
},
|
||||
comp: {
|
||||
create: bindEvent({
|
||||
event: 'model.created',
|
||||
metadata: { type: 'comp' },
|
||||
fn: modelsUtils.createComponent,
|
||||
}),
|
||||
},
|
||||
fixture: {
|
||||
create: bindEvent({
|
||||
event: 'fixture.created',
|
||||
batch: true,
|
||||
fn: async (model, entries, getFixtures) => {
|
||||
const entriesToCreate = isFunction(entries) ? entries(getFixtures()) : entries;
|
||||
const result = await modelsUtils.createFixturesFor(model, entriesToCreate);
|
||||
return { entries: result, model };
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const getActionByCode = code => prop(code, registry);
|
||||
|
||||
const createAction = (code, ...params) => {
|
||||
const _state = {
|
||||
emitter: null,
|
||||
params,
|
||||
code,
|
||||
fn: getActionByCode(code),
|
||||
};
|
||||
|
||||
return {
|
||||
get params() {
|
||||
return _state.params;
|
||||
},
|
||||
|
||||
get code() {
|
||||
return _state.code;
|
||||
},
|
||||
|
||||
get fn() {
|
||||
return _state.fn;
|
||||
},
|
||||
|
||||
setEmitter(emitter) {
|
||||
_state.emitter = emitter;
|
||||
return this;
|
||||
},
|
||||
|
||||
async execute() {
|
||||
const { fn, params, emitter } = _state;
|
||||
|
||||
const res = await fn(...params);
|
||||
|
||||
if (isNil(emitter) || !has('emitInfos', fn)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const { event, batch, metadata } = fn.emitInfos;
|
||||
|
||||
const emitEvent = curry(emitter.emit)(event);
|
||||
const bindMetadata = result => ({ result, metadata });
|
||||
|
||||
if (isArray(res) && !batch) {
|
||||
res.map(bindMetadata).forEach(emitEvent);
|
||||
} else {
|
||||
emitEvent(bindMetadata(res));
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAction,
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { contains, reduce, over } = require('lodash/fp');
|
||||
|
||||
const createEvent = (eventName, data) => ({ name: eventName, data, emittedAt: new Date() });
|
||||
|
||||
const createEventsManager = ({ allowedEvents = [] }) => {
|
||||
const toEventsMap = reduce((acc, eventName) => ({ ...acc, [eventName]: [] }), {});
|
||||
|
||||
const _state = {
|
||||
events: toEventsMap(allowedEvents),
|
||||
callbacks: toEventsMap(allowedEvents),
|
||||
allowedEvents,
|
||||
};
|
||||
|
||||
const pushEvent = event => _state.events[event.name].push(event.data);
|
||||
const notify = ({ name, data }) => over(_state.callbacks[name])(data);
|
||||
const validateEventName = eventName => {
|
||||
if (!contains(eventName, _state.allowedEvents)) {
|
||||
throw new Error(`"${eventName}" is not a valid event name.`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get eventsMap() {
|
||||
return _state.events;
|
||||
},
|
||||
|
||||
get allowedEvents() {
|
||||
return _state.allowedEvents;
|
||||
},
|
||||
|
||||
getEventsByName(name) {
|
||||
validateEventName(name);
|
||||
|
||||
return _state.events[name];
|
||||
},
|
||||
|
||||
register(eventName, callback) {
|
||||
validateEventName(eventName);
|
||||
|
||||
_state.callbacks[eventName].push(callback);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
unregister(eventName, callback) {
|
||||
validateEventName(eventName);
|
||||
|
||||
_state.callbacks[eventName] = _state.callbacks[eventName].filter(cb => cb !== callback);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
emit(eventName, data) {
|
||||
validateEventName(eventName);
|
||||
|
||||
const event = createEvent(eventName, data);
|
||||
const executeOperations = over([pushEvent, notify]);
|
||||
|
||||
executeOperations(event);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createEvent,
|
||||
createEventsManager,
|
||||
};
|
||||
@ -1,108 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { omit, get } = require('lodash/fp');
|
||||
const _ = require('lodash');
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { map, prop } = require('lodash/fp');
|
||||
const modelsUtils = require('../models');
|
||||
const { sanitizeEntity } = require('../../../packages/strapi-utils');
|
||||
const { createAction } = require('./action');
|
||||
const { createEventsManager } = require('./event');
|
||||
const actionRegistry = require('./action-registry');
|
||||
|
||||
const events = {
|
||||
MODEL_CREATED: 'model.created',
|
||||
MODEL_DELETED: 'model.deleted',
|
||||
FIXTURES_CREATED: 'fixture.created',
|
||||
};
|
||||
|
||||
const stringifyDates = object =>
|
||||
JSON.parse(
|
||||
JSON.stringify(object, (key, value) => {
|
||||
if (this[key] instanceof Date) {
|
||||
return this[key].toUTCString();
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
|
||||
const formatFixtures = map(stringifyDates);
|
||||
|
||||
/**
|
||||
* Create a test builder from the given args.
|
||||
*
|
||||
* A builder allows to create resources (content-types, components, fixtures) that can be cleaned-up easily.
|
||||
*
|
||||
* @param options The builder's constructor options
|
||||
* @param options.initialState {State} The builder's initial state
|
||||
*
|
||||
* @example ```
|
||||
* // Setup the builder
|
||||
* const builder = createTestBuilder()
|
||||
* .addContentType(articleModel)
|
||||
* .addComponent(myComponent);
|
||||
*
|
||||
* // Run the build operations
|
||||
* await builder.build();
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* // Cleanup created resources
|
||||
* await builder.cleanup();
|
||||
* ```
|
||||
*/
|
||||
const createTestBuilder = (options = {}) => {
|
||||
const { initialState } = options;
|
||||
const omitActions = omit('actions');
|
||||
const getDefaultState = () => ({ actions: [], models: [], fixtures: {}, ...initialState });
|
||||
|
||||
const state = getDefaultState();
|
||||
|
||||
const addAction = (code, ...params) => {
|
||||
const action = createAction(code, ...params).setEmitter(_eventsManager);
|
||||
_state.actions.push(action);
|
||||
return action;
|
||||
const action = get(code, actionRegistry);
|
||||
state.actions.push(action(...params));
|
||||
};
|
||||
|
||||
const getDefaultState = () => ({
|
||||
actions: [],
|
||||
deleted: [],
|
||||
created: [],
|
||||
fixtures: {},
|
||||
...initialState,
|
||||
});
|
||||
|
||||
const getModelsMap = (type = null) =>
|
||||
_.difference(_state.created, _state.deleted)
|
||||
// Keep the models with the wanted type (all, content-types, or components)
|
||||
.filter(event => _.isNull(type) || _.get(event, 'metadata.type') === type)
|
||||
// Flatten the data property to obtain an array of model
|
||||
.flatMap(_.property('result'))
|
||||
// Transform the array into a map where the key is the model's name
|
||||
.reduce((acc, model) => _.merge(acc, { [model.modelName]: model }), {});
|
||||
|
||||
const _state = getDefaultState();
|
||||
|
||||
const _eventsManager = createEventsManager({ allowedEvents: Object.values(events) });
|
||||
|
||||
_eventsManager
|
||||
.register(events.MODEL_CREATED, event => _state.created.push(event))
|
||||
.register(events.MODEL_DELETED, event => _state.deleted.push(event))
|
||||
.register(events.FIXTURES_CREATED, ({ result }) => {
|
||||
const existingFixtures = _.get(_state.fixtures, result.model, []);
|
||||
_state.fixtures[result.model] = existingFixtures.concat(formatFixtures(result.entries));
|
||||
});
|
||||
|
||||
return {
|
||||
get models() {
|
||||
return getModelsMap();
|
||||
},
|
||||
|
||||
get contentTypes() {
|
||||
return getModelsMap('ct');
|
||||
},
|
||||
|
||||
get components() {
|
||||
return getModelsMap('comp');
|
||||
return state.models;
|
||||
},
|
||||
|
||||
get fixtures() {
|
||||
return _state.fixtures;
|
||||
return state.fixtures;
|
||||
},
|
||||
|
||||
sanitizedFixtures(strapi) {
|
||||
@ -135,73 +57,32 @@ const createTestBuilder = (options = {}) => {
|
||||
return this;
|
||||
},
|
||||
|
||||
addFixtures(model, entries, { onCreated = null } = {}) {
|
||||
addAction('fixture.create', model, entries, () => this.fixtures);
|
||||
|
||||
if (_.isFunction(onCreated)) {
|
||||
_eventsManager.register(events.FIXTURES_CREATED, e => {
|
||||
const { result } = e;
|
||||
|
||||
// Filters fixture.created events triggered for other models
|
||||
if (result.model === model) {
|
||||
onCreated(formatFixtures(result.entries));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addFixtures(model, entries) {
|
||||
addAction('fixtures.create', model, entries, () => this.fixtures);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute every registered step
|
||||
*/
|
||||
async build() {
|
||||
for (const action of _state.actions) {
|
||||
await action.execute();
|
||||
for (const action of state.actions) {
|
||||
const newState = await action.build(omitActions(state));
|
||||
Object.assign(state, newState);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup and delete every model (content-types / components) created by the builder
|
||||
*/
|
||||
async cleanup() {
|
||||
// The first model to be created should be the last one deleted
|
||||
const createdEvents = _state.created.reverse();
|
||||
async cleanup(options = {}) {
|
||||
const { enableTestDataAutoCleanup = true } = options;
|
||||
|
||||
for (const event of createdEvents) {
|
||||
const {
|
||||
result,
|
||||
metadata: { type },
|
||||
} = event;
|
||||
|
||||
// Helpers for the given model
|
||||
const isBatchOperation = _.isArray(result);
|
||||
const modelType = type === 'ct' ? 'ContentType' : 'Component';
|
||||
const pluralSuffix = isBatchOperation ? 's' : '';
|
||||
|
||||
// Pluralized & Type related methods name
|
||||
const cleanupMethod = `cleanupModel${pluralSuffix}`;
|
||||
const deleteMethod = `delete${modelType}${pluralSuffix}`;
|
||||
|
||||
// Format params
|
||||
const uidAttribute = type === 'ct' ? 'modelName' : 'uid';
|
||||
const params = isBatchOperation
|
||||
? result.map(prop(uidAttribute))
|
||||
: prop(uidAttribute, result);
|
||||
|
||||
// Execute cleanup & delete operations
|
||||
await modelsUtils[cleanupMethod](params);
|
||||
await modelsUtils[deleteMethod](params);
|
||||
|
||||
// Notify the builder that the created model has been deleted
|
||||
_eventsManager.emit(events.MODEL_DELETED, event);
|
||||
if (enableTestDataAutoCleanup) {
|
||||
for (const model of state.models.reverse()) {
|
||||
await modelsUtils.cleanupModel(model.uid || model.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const action of state.actions.reverse()) {
|
||||
await action.cleanup();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createTestBuilder,
|
||||
};
|
||||
module.exports = { createTestBuilder };
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { isFunction, isNil } = require('lodash/fp');
|
||||
const { isFunction, isNil, prop } = require('lodash/fp');
|
||||
const { createStrapiInstance } = require('./strapi');
|
||||
|
||||
const createHelpers = async ({ strapi: strapiInstance = null, ...options } = {}) => {
|
||||
@ -172,6 +171,14 @@ async function createFixturesFor(model, entries, { strapi: strapiIst } = {}) {
|
||||
return results;
|
||||
}
|
||||
|
||||
async function deleteFixturesFor(model, entries, { strapi: strapiIst } = {}) {
|
||||
const { strapi, cleanup } = await createHelpers({ strapi: strapiIst });
|
||||
|
||||
await strapi.query(model).delete({ id_in: entries.map(prop('id')) });
|
||||
|
||||
await cleanup();
|
||||
}
|
||||
|
||||
async function modifyContentType(data, { strapi } = {}) {
|
||||
const { contentTypeService, cleanup } = await createHelpers({ strapi });
|
||||
|
||||
@ -223,6 +230,7 @@ module.exports = {
|
||||
// Fixtures
|
||||
createFixtures,
|
||||
createFixturesFor,
|
||||
deleteFixturesFor,
|
||||
// Update Content-Types
|
||||
modifyContentType,
|
||||
// Misc
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user