UNPKG

libmodulor

Version:

A TypeScript library to create platform-agnostic applications

373 lines (372 loc) 14.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { inject, injectable } from 'inversify'; import { I18nEN } from '../i18n/locales/en.js'; import { I18nFR } from '../i18n/locales/fr.js'; import { FAKE_USER_ADMIN, UCBuilder, UCInputFieldChangeOperator, ucHTTPContract, } from '../uc/index.js'; // We inject directly the implementation because we'll generate all the reports and not only the one that is bound to the interface. // We can plan a setting à la Vitest where we specify the types of reports to generate though. import { SimpleHTMLAppTestReportEmitter } from './impl/SimpleHTMLAppTestReportEmitter.js'; import { optsAllSet } from './opts.js'; import { defaultUCAuthSetters } from './uc-auth.js'; import { allWithExamples, defaultUCInputFillers, } from './uc-input.js'; import { awaitForSrcImport } from './utils.js'; import { AppTesterCtxInitializer } from './workers/AppTesterCtxInitializer.js'; import { AppFolderChecker } from './workers/checkers/AppFolderChecker.js'; import { AppI18nChecker } from './workers/checkers/AppI18nChecker.js'; import { AppIndexChecker } from './workers/checkers/AppIndexChecker.js'; import { AppManifestChecker } from './workers/checkers/AppManifestChecker.js'; import { UCDefChecker } from './workers/checkers/UCDefChecker.js'; import { UCDefSourcesChecker, } from './workers/checkers/UCDefSourcesChecker.js'; import { UCExecutor, } from './workers/UCExecutor.js'; let AppTester = class AppTester { appDocsEmitter; appFolderChecker; appI18nChecker; appIndexChecker; appManifestChecker; appTesterCtxInitializer; serverManager; simpleHTMLAppTestReportEmitter; ucBuilder; ucDefChecker; ucDefSourcesChecker; ucExecutor; configurator; ctx; /** * We use a "safe" one to avoid any "infinite loop" while trying to import/resolve a file. * This can happen in case of circular dependencies for example. */ safeSrcImporter; // biome-ignore lint/suspicious/noExplicitAny: can be anything testResults; testSummary; ucDefSourcesCheckerOutput; constructor(appDocsEmitter, appFolderChecker, appI18nChecker, appIndexChecker, appManifestChecker, appTesterCtxInitializer, serverManager, simpleHTMLAppTestReportEmitter, ucBuilder, ucDefChecker, ucDefSourcesChecker, ucExecutor) { this.appDocsEmitter = appDocsEmitter; this.appFolderChecker = appFolderChecker; this.appI18nChecker = appI18nChecker; this.appIndexChecker = appIndexChecker; this.appManifestChecker = appManifestChecker; this.appTesterCtxInitializer = appTesterCtxInitializer; this.serverManager = serverManager; this.simpleHTMLAppTestReportEmitter = simpleHTMLAppTestReportEmitter; this.ucBuilder = ucBuilder; this.ucDefChecker = ucDefChecker; this.ucDefSourcesChecker = ucDefSourcesChecker; this.ucExecutor = ucExecutor; this.testResults = []; this.testSummary = { counts: { danger: 0, success: 0, warning: 0, }, }; } async checkAppFolder() { const { errors } = await this.appFolderChecker.exec({ appPath: this.ctx.appPath, }); if (errors.length > 0) { throw new Error(errors[0]); } } async checkAppI18n() { const { errors } = await this.appI18nChecker.exec({ appI18n: this.ctx.appI18n, appManifest: this.ctx.appManifest, }); if (errors.length > 0) { throw new Error(errors[0]); } } async checkAppIndex() { const { errors } = await this.appIndexChecker.exec({ appPath: this.ctx.appPath, }); if (errors.length > 0) { throw new Error(errors[0]); } } async checkAppManifest() { const { errors } = await this.appManifestChecker.exec({ appManifest: this.ctx.appManifest, ucdRefs: this.ctx.ucdRefs, }); if (errors.length > 0) { throw new Error(errors[0]); } } async checkUCDSources() { this.ucDefSourcesCheckerOutput = await this.ucDefSourcesChecker.exec({ ctx: this.ctx, }); } async checkUC(ucdRef) { const { errors } = await this.ucDefChecker.exec({ ucdRef }); if (errors.length > 0) { throw new Error(errors[0]); } const { source } = ucdRef; const ucd = source[`${Object.keys(source)[0]}`]; return ucd; } async execFlow(flow) { const output = []; const { auth, authName, setup, steps } = flow; await setup?.(this.ctx); for await (const [ucd, inputOverride] of steps) { const inputFiller = (uc) => { allWithExamples(uc); if (inputOverride) { const inputOverrides = inputOverride(output); for (const [k, v] of Object.entries(inputOverrides)) { uc.inputField(k).setValue(UCInputFieldChangeOperator.SET, v); } } }; const { out } = await this.execUC({ auth: auth ?? FAKE_USER_ADMIN, authName: authName ?? 'ADMIN', inputFiller, inputFillerName: 'FLOW', ucd, }, flow); output.push(out); } return output; } async execMonkeyTest(ucd, input) { const inputFiller = (uc) => { uc.fill(input); }; // biome-ignore lint/suspicious/noExplicitAny: can be anything const { out } = await this.execUC({ // TODO : Pass the appropriate one so we test the real flow and not the auth auth: FAKE_USER_ADMIN, authName: 'ADMIN', inputFiller, inputFillerName: 'ANY_FIELD_AS_ANYTHING', ucd, }); return out; } async execUC(input, flow) { const { out } = await this.ucExecutor.exec({ ...input, appManifest: this.ctx.appManifest, }); // TODO : Add errors mapping to configurator so the developer can override this default behavior let status = 'warning'; if (!out.err) { status = 'success'; } else { const { name } = out.err; switch (name) { case 'CustomError': break; case 'Error': case 'TypeError': status = 'danger'; break; default: break; } } let name = input.ucd.metadata.name; let sideEffects = await this.configurator.sideEffects(this.ctx); if (flow) { name = `${flow.name} > ${name}`; // For regular uc tests, side effects are cleared between each execution // But it's not the case for a flow, thus each flow uc test shares the same side effects ref // To have the side effects at each stage, we take a snapshot (to be improved as this is not performant at all) if (sideEffects) { const snapshot = JSON.parse(JSON.stringify(Array.from(sideEffects.entries()))); sideEffects = new Map(snapshot); } } const testResult = { name, out, sideEffects, status, }; this.testResults.push(testResult); this.testSummary.counts[status] += 1; return testResult; } async finalize() { await this.serverManager.stop(); const { appPath } = this.ctx; if (this.ucDefSourcesCheckerOutput) { await this.appDocsEmitter.exec({ appPath, ucDefSourcesCheckerOutput: this.ucDefSourcesCheckerOutput, }); } await this.simpleHTMLAppTestReportEmitter.exec({ appPath, testResults: this.testResults, testSummary: this.testSummary, }); } getCtx() { return this.ctx; } async init({ appPath, configurator, serverClientSettings, srcImporter, }) { this.configurator = configurator; this.safeSrcImporter = (path) => Promise.race([ // We can cast because it fails or looses the race. awaitForSrcImport(path), srcImporter(path), ]); const { ctx } = await this.appTesterCtxInitializer.exec({ appPath, srcImporter: this.safeSrcImporter, }); this.ctx = ctx; this.ctx.opts = await configurator.opts(); await this.configurator.bindImplementations(this.ctx); await this.configurator.seed(this.ctx); await this.bindI18n(); await this.bindServerClientSettings(serverClientSettings); await this.initI18n(); await this.initServer(); } async ucTestData(ucdRef) { // Auth setters const defaultASs = defaultUCAuthSetters(); let asEntries = Object.entries(defaultASs); const asConfig = await this.configurator.authSettersConfig(); if (asConfig) { const { add, exclude } = asConfig; if (exclude) { for (const asName of exclude) { asEntries = asEntries.filter(([name]) => name !== asName); } } if (add) { asEntries.push(...Object.entries(add)); } } // Input fillers const defaultIFs = defaultUCInputFillers(); const ifEntries = Object.entries(defaultIFs); const specificIFs = await this.configurator.inputFillers(); const specificIFsForUC = specificIFs?.get(ucdRef.name); if (specificIFsForUC) { ifEntries.push(...Object.entries(specificIFsForUC)); } const data = []; for (const [authName, auth] of asEntries) { for (const [inputFillerName, inputFiller] of ifEntries) { data.push({ auth, authName, inputFiller, inputFillerName, }); } } return data; } async bindI18n() { const { appI18n, container } = this.ctx; const appLangCodes = Object.keys(appI18n); const coreI18n = { en: I18nEN, fr: I18nFR, }; const productI18n = { en: { ...I18nEN, }, fr: { ...I18nFR, }, }; for (const l of appLangCodes) { if (!(l in coreI18n)) { continue; } productI18n[l] = { ...productI18n[l], ...appI18n[l], }; } (await container.rebind('I18n')).toConstantValue(productI18n); } async bindServerClientSettings(serverClientSettings) { const { container } = this.ctx; const opts = optsAllSet(this.ctx.opts); const existingSettings = container.get('Settings'); (await container.rebind('Settings')).toConstantValue({ ...existingSettings, ...serverClientSettings, logger_level: opts.logger_level, }); } async initI18n() { await this.ctx.container.get('I18nManager').init(); } async initServer() { const ucManager = this.ctx.container.get('UCManager'); this.serverManager.overrideUCManager(ucManager); this.ucExecutor.overrideUCManager(ucManager); await this.serverManager.init(); for await (const { source } of this.ctx.ucdRefs) { const keys = Object.keys(source); const [ucdKey] = keys; if (!ucdKey) { continue; } const ucd = source[ucdKey]; const uc = this.ucBuilder.exec({ appManifest: this.ctx.appManifest, auth: null, def: ucd, }); await this.serverManager.mount(uc.appManifest, ucd, ucHTTPContract(uc)); } await this.serverManager.warmUp(); await this.serverManager.start(); } }; AppTester = __decorate([ injectable(), __param(0, inject('AppDocsEmitter')), __param(1, inject(AppFolderChecker)), __param(2, inject(AppI18nChecker)), __param(3, inject(AppIndexChecker)), __param(4, inject(AppManifestChecker)), __param(5, inject(AppTesterCtxInitializer)), __param(6, inject('ServerManager')), __param(7, inject(SimpleHTMLAppTestReportEmitter)), __param(8, inject(UCBuilder)), __param(9, inject(UCDefChecker)), __param(10, inject(UCDefSourcesChecker)), __param(11, inject(UCExecutor)), __metadata("design:paramtypes", [Object, AppFolderChecker, AppI18nChecker, AppIndexChecker, AppManifestChecker, AppTesterCtxInitializer, Object, SimpleHTMLAppTestReportEmitter, UCBuilder, UCDefChecker, UCDefSourcesChecker, UCExecutor]) ], AppTester); export { AppTester };