UNPKG

testbook

Version:
486 lines 22.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = exports.runComponentTest = exports.findModuleTests = exports.serialiseError = void 0; const vm_1 = require("vm"); const mocks_1 = require("./mocks"); const dom_1 = require("./dom"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const express_1 = __importDefault(require("express")); const glob_1 = __importDefault(require("glob")); const uuid_1 = require("uuid"); const jsdom_1 = require("jsdom"); const webpack_1 = __importDefault(require("webpack")); const propTypes_1 = require("./propTypes"); const openBrowser_1 = __importDefault(require("react-dev-utils/openBrowser")); const promise_1 = require("./promise"); const logger_1 = require("./logger"); const hostNodeModulesPath = path_1.default.join(process.cwd(), "node_modules"); const { act } = require(path_1.default.join(hostNodeModulesPath, "react-dom/test-utils")); const serialiseError = (error) => ({ name: error.name, message: error.message, stack: error.stack, componentStack: error instanceof RenderError ? error.componentStack : undefined, }); exports.serialiseError = serialiseError; const loadModuleTests = (file) => getFile(file).then((f) => [file, f]); const findModuleTests = (searchPath) => new Promise((resolve, reject) => { glob_1.default(path_1.default.join(searchPath, "**/*.tests.json"), null, (err, files) => { console.log(`Test files found: ${files}`); if (err) { return reject(err); } Promise.all(files.map(loadModuleTests)) .then((files) => files.map(([fileName, file]) => ({ file: fileName .replace(/\.tests\.json$/, "") .replace(new RegExp(`^src${path_1.default.sep}`), ""), components: file.components.map((c) => (Object.assign(Object.assign({}, c), { exportName: c.name, name: getExportComponentName(c.name, fileName).replace(/\.(.+)\.tests$/, "") }))), }))) .then(resolve) .catch(reject); }); }); exports.findModuleTests = findModuleTests; const findModulesWithComponents = (searchPath) => new Promise((resolve, reject) => { glob_1.default(path_1.default.join(searchPath, "**/*.{js,jsx,ts,tsx}"), { ignore: "**/*.test.js", }, (err, files) => { console.log(`js/ts files found: ${files}`); if (err) { return reject(err); } promise_1.promiseBatch(files.map((file) => () => loadModuleWithComponents(file)), 3) .then((m) => m .filter((m) => !m.error && m.components.length) .map((m) => (Object.assign(Object.assign({}, m), { components: m.components.map((c) => (Object.assign(Object.assign({}, c), (c.error && { error: exports.serialiseError(c.error) })))), file: m.file.replace(new RegExp(`^src${path_1.default.sep}`), "") })))) .then(resolve); }); }); const loadModuleWithComponents = (modulePath) => compileModuleWithHostWebpack(modulePath) .then(([outputPath, filename]) => compileWrapperAndModuleWithWebpack(require.resolve("./findComponentsInModule"), outputPath, filename)) .then((moduleCode) => { console.log(`Loading ${modulePath}`); const script = new vm_1.Script(moduleCode); const context = createDOM().getInternalVMContext(); script.runInContext(context); context.close(); return { file: modulePath, components: context.result && context.result .filter(([, isComponent, error]) => isComponent || error) .map(([exportName, , error]) => (Object.assign({ exportName, name: getExportComponentName(exportName, modulePath) }, (error && { error })))) .sort((a, b) => (b.exportName === "default" ? 1 : -1)), }; }) .catch((error) => { console.log(`Load failed ${error}`); return { file: modulePath, components: [], error, }; }); const getExportComponentName = (exportName, filePath) => { if (exportName === "default") { const fileName = path_1.default.parse(filePath).name; if (fileName === "index") { return path_1.default.basename(path_1.default.dirname(filePath)); } return fileName; } return exportName; }; const compileModuleWithHostWebpack = (modulePath, externals = null) => { const craWebpackConfig = require(path_1.default.join(hostNodeModulesPath, "react-scripts/config/webpack.config"))("production"); const outputModulePath = modulePath.replace(/\.[^.]+$/, ".js"); return new Promise((resolve, reject) => webpack_1.default(Object.assign(Object.assign({}, craWebpackConfig), { entry: [path_1.default.resolve(modulePath)], output: { filename: outputModulePath, libraryTarget: "umd", }, optimization: Object.assign(Object.assign({}, craWebpackConfig.optimization), { splitChunks: false, runtimeChunk: false }), externals: Object.assign({ react: "react", "react-dom": "react-dom" }, externals) }), (err, stats) => { if (err) { return reject(err); } if (stats.hasErrors()) { return reject(stats.toJson().errors); } return resolve([stats.toJson().outputPath, outputModulePath]); })); }; const compileWithWebpack = (modulePath) => new Promise((resolve, reject) => webpack_1.default({ entry: [modulePath], output: { filename: path_1.default.basename(modulePath), }, module: { rules: [ { test: /\.tsx?$/, use: [ { loader: "ts-loader", options: { happyPackMode: true, }, }, ], exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, }, (err, stats) => { if (err) { return reject(err); } if (stats.hasErrors()) { return reject(stats.toJson().errors); } return resolve(path_1.default.resolve(path_1.default.join(stats.toJson().outputPath, path_1.default.basename(modulePath)))); })).then((wrapperModulePath) => new Promise((resolve, reject) => fs_1.default.readFile(wrapperModulePath, function (err, data) { if (err) { return reject(err); } resolve(data.toString()); }))); const compileWrapperAndModuleWithWebpack = (wrapperModulePath, modulePath, moduleFilename, aliases = null) => new Promise((resolve, reject) => webpack_1.default({ entry: [wrapperModulePath], output: { filename: path_1.default.join(path_1.default.basename(wrapperModulePath), moduleFilename), }, module: { rules: [ { test: /\.tsx?$/, use: [ { loader: "ts-loader", options: { happyPackMode: true, }, }, ], exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], alias: Object.assign({ module: modulePath && path_1.default.resolve(path_1.default.join(modulePath, moduleFilename)), react: path_1.default.join(hostNodeModulesPath, "react"), "react-dom": path_1.default.join(hostNodeModulesPath, "react-dom") }, aliases), }, }, (err, stats) => { if (err) { return reject(err); } if (stats.hasErrors()) { return reject(stats.toJson().errors); } return resolve(path_1.default.resolve(path_1.default.join(stats.toJson().outputPath, path_1.default.basename(wrapperModulePath), moduleFilename))); })).then((wrapperModulePath) => new Promise((resolve, reject) => fs_1.default.readFile(wrapperModulePath, function (err, data) { if (err) { return reject(err); } resolve(data.toString()); }))); const getComponent = (file, exportName) => getTestFile(file).then((f) => f.components.find((c) => c.name === exportName)); const getComponentTests = (file, exportName) => getComponent(file, exportName).then((c) => (c ? c.tests : []), () => []); const getComponentTestStatuses = (file, exportName) => getComponentTests(file, exportName) .then((tests) => Promise.all(tests.map((t) => exports.runComponentTest(file, exportName, t.id).then(([results]) => [ t.id, results, ])))) .then((listOfIdsAndResults) => listOfIdsAndResults.reduce((prev, [testId, results]) => (Object.assign(Object.assign({}, prev), { [testId]: results.every((r) => r.result === "success") ? "success" : "error" })), {})); const getComponentTest = (file, exportName, testId) => getComponentTests(file, exportName).then((t) => t.find((t) => t.id === testId)); const getOrCreateFileJson = (file) => getTestFile(file).catch(() => ({ components: [], })); const getFile = (file) => new Promise((resolve, reject) => fs_1.default.readFile(file, "utf8", (err, data) => err ? reject(err) : resolve(JSON.parse(data)))); const getTestFile = (file) => getFile(`${file}.tests.json`); const writeFile = (file, payload) => new Promise((resolve, reject) => fs_1.default.writeFile(`${file}.tests.json`, JSON.stringify(payload, null, 2), (err) => { err ? reject() : resolve(payload); })); const getOrCreateComponent = (fileJson, exportName) => { const component = fileJson.components.find((c) => c.name === exportName); return component ? fileJson : { components: [ ...fileJson.components, { name: exportName, tests: [], }, ], }; }; const createTest = (file, exportName) => { const test = { id: uuid_1.v1(), steps: [ { type: "render", definition: { props: {}, }, }, ], }; return getOrCreateFileJson(file) .then((fileJson) => getOrCreateComponent(fileJson, exportName)) .then((fileJson) => writeFile(file, fileJsonWithAddedTest(fileJson, exportName, test))) .then(() => test); }; const updatedTest = (test, updates) => (Object.assign(Object.assign({}, test), updates)); const componentWithAddedTest = (component, test) => (Object.assign(Object.assign({}, component), { tests: component.tests.concat([test]) })); const fileJsonWithAddedTest = (fileJson, exportName, test) => (Object.assign(Object.assign({}, fileJson), { components: fileJson.components.map((c) => c.name === exportName ? componentWithAddedTest(c, test) : c) })); const componentWithUpdatedTest = (component, testId, testUpdates) => (Object.assign(Object.assign({}, component), { tests: component.tests.map((t) => t.id === testId ? updatedTest(t, testUpdates) : t) })); const fileJsonWithUpdatedTest = (fileJson, exportName, testId, testUpdates) => (Object.assign(Object.assign({}, fileJson), { components: fileJson.components.map((c) => c.name === exportName ? componentWithUpdatedTest(c, testId, testUpdates) : c) })); const updateTestName = (file, exportName, testId, name) => getTestFile(file).then((fileJson) => writeFile(file, fileJsonWithUpdatedTest(fileJson, exportName, testId, { name }))); const updateTestSteps = (file, exportName, testId, steps) => getTestFile(file).then((fileJson) => writeFile(file, fileJsonWithUpdatedTest(fileJson, exportName, testId, { steps }))); class RenderError extends Error { constructor(name, message = undefined, stack = undefined, componentStack = undefined) { super(message); Object.setPrototypeOf(this, new.target.prototype); this.name = name; this.stack = stack; this.componentStack = componentStack; } } class EventError extends Error { constructor(message) { super(message); this.name = "EventError"; } } class AssertionError extends Error { constructor(message) { super(message); this.name = "AssertionError"; } } const createDOM = (logger) => new jsdom_1.JSDOM('<!doctype html><html lang="en-GB"><body /></html>', { pretendToBeVisual: true, runScripts: "dangerously", url: "http://localhost", virtualConsole: logger ? new jsdom_1.VirtualConsole().sendTo(logger) : new jsdom_1.VirtualConsole(), }); const render = (file, exportName, props, context, wrapper) => compileModuleWithHostWebpack(file, wrapper ? { [wrapper.file]: wrapper.file } : null) .then(([moduleOutputPath, moduleFilename]) => compileWrapperAndModuleWithWebpack(require.resolve("./render"), moduleOutputPath, moduleFilename, Object.assign({ wrapper: wrapper ? path_1.default.join(hostNodeModulesPath, wrapper.file) : require.resolve("./noop") }, (wrapper ? { [wrapper.file]: path_1.default.join(hostNodeModulesPath, wrapper.file) } : null)))) .then((moduleCode) => { const script = new vm_1.Script(moduleCode); context.exportName = exportName; context.props = props; context.wrapperExportName = wrapper && wrapper.exportName; context.wrapperProps = wrapper && wrapper.props; script.runInContext(context); return context.result; }) .catch(([errorException, componentError]) => { throw new RenderError(errorException.name, errorException.message, errorException.stack, componentError && componentError.componentStack); }); const setupVmContextWithContainerAndMocks = () => compileWithWebpack(require.resolve("./setupContainerAndMocks")).then((moduleCode) => { const script = new vm_1.Script(moduleCode); const logger = logger_1.setupConsoleLogger(); const context = createDOM(logger).getInternalVMContext(); script.runInContext(context); return [context, logger]; }); const renderComponentSideEffects = (file, exportName, testId, step) => exports.runComponentTest(file, exportName, testId, step).then(([, context, logger]) => { const { container, mocks } = context; if (!container) { return { regions: [], mocks: [], }; } const elements = dom_1.findTextNodes(container).map(([e, text]) => ({ text, type: ["BUTTON", "A"].includes(e.nodeName) ? "button" : "text", xpath: dom_1.getElementTreeXPath(e, context), })); return { regions: elements.map((e) => (Object.assign(Object.assign({}, e), { unique: !elements.find((f) => f.text === e.text && f.xpath !== e.xpath) }))), mocks: mocks.map(({ name, mock }) => ({ name, calls: mock.getCalls(), })), logs: logger.getLog(), }; }); const runRenderStep = (file, exportName, { definition: { props, wrapper } }, context) => render(file, exportName, props, context, wrapper) .then(() => ["success", context]) .catch((e) => [e, context]); const runEventStep = (file, exportName, { definition: { type, target } }, context) => { const { container } = context; const node = dom_1.findTextNodes(container).find(([, text]) => text === target); if (!node) { return Promise.resolve([ new EventError(`Text '${target}' could not be found`), context, ]); } const [targetNode] = node; act(() => { targetNode.dispatchEvent(new context.Event(type, { bubbles: true, cancelable: false, composed: false, })); }); return new Promise((resolve) => setTimeout(() => resolve(["success", context]), 10)); }; const runAssertionStep = (file, exportName, { definition: { type, target } }, context) => { const { container, mocks } = context; if (type === "text") { const matches = dom_1.findTextNodes(container).filter(([, text]) => text === target); return Promise.resolve([ matches.length ? "success" : new AssertionError(`Text '${target}' could not be found`), context, ]); } if (type === "mock") { const mock = mocks.find((m) => m.name === target.name); const calls = mock ? mock.mock.getCalls() : []; const callsWithMatchingArgs = calls.filter((args) => JSON.stringify(args) === JSON.stringify(target.args)); return Promise.resolve([ callsWithMatchingArgs.length ? "success" : new AssertionError(`Mock '${target.name}' was not called with args '${target.args}'`), context, ]); } return Promise.resolve([ new AssertionError(`Invalid assertion type '${type}'`), context, ]); }; const STEP_RUNNERS = { render: runRenderStep, event: runEventStep, assertion: runAssertionStep, mock: mocks_1.runMockStep, }; const runStep = (file, exportName, step, context) => { console.log(`Running step ${JSON.stringify(step)}`); return STEP_RUNNERS[step.type](file, exportName, step, context); }; const runComponentTest = (file, exportName, testId, step) => Promise.all([ setupVmContextWithContainerAndMocks(), getComponentTest(file, exportName, testId), ]) .then(([[context, logger], { steps }]) => steps.reduce((resultsAndContext, s, idx) => resultsAndContext.then(([results, context]) => idx <= (step === undefined ? steps.length - 1 : step) && !results.find((r) => r.result instanceof Error) ? runStep(file, exportName, s, context).then(([result, newContext,]) => [ [...results, { result }], newContext, logger, ]) : Promise.resolve([ results, context, logger, ])), Promise.resolve([[], context, logger]))) .then(([results, context, logger]) => [ results, context.close() || context, logger, ]); exports.runComponentTest = runComponentTest; const getComponentPropTypes = (modulePath, exportName) => { if (/tsx?$/.test(modulePath)) { return propTypes_1.getTsPropTypes(modulePath, exportName, require(path_1.default.join(hostNodeModulesPath, "react-scripts/config/paths")) .appTsConfig); } return compileModuleWithHostWebpack(modulePath) .then(([moduleOutputPath, moduleFilename]) => compileWrapperAndModuleWithWebpack(require.resolve("./getComponentPropTypes"), moduleOutputPath, moduleFilename)) .then((moduleCode) => { const context = createDOM().getInternalVMContext(); const script = new vm_1.Script(moduleCode); context.exportName = exportName; script.runInContext(context); context.close(); return context.result; }); }; const getWrapperOptions = () => Promise.resolve([ { file: "react-router-dom", exportName: "MemoryRouter", propTypes: {}, }, ]); const app = express_1.default(); const PORT = 9010; app.use(express_1.default.json()); const SEARCH_PATH = "./src"; app.get("/module-test", (req, res, next) => exports.findModuleTests(SEARCH_PATH) .then((moduleComponentTests) => res.send(moduleComponentTests)) .catch(next)); app.get("/module-component", (req, res, next) => findModulesWithComponents(SEARCH_PATH) .then((modulesWithComponents) => res.send(modulesWithComponents)) .catch(next)); app.get("/test", (req, res, next) => getComponentTests(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName) .then((tests) => res.send(tests)) .catch(next)); app.get("/test/status", (req, res, next) => getComponentTestStatuses(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName) .then((statuses) => res.send(statuses)) .catch(next)); app.get("/test/:testId", (req, res, next) => getComponentTest(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName, req.params.testId) .then((test) => res.send(test)) .catch(next)); app.post("/test", (req, res, next) => createTest(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName) .then((test) => res.send(test)) .catch(next)); app.put("/test/:testId/steps", (req, res, next) => updateTestSteps(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName, req.params.testId, req.body) .then((testSteps) => res.send(testSteps)) .catch(next)); app.put("/test/:testId/name", (req, res, next) => updateTestName(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName, req.params.testId, req.body.name) .then((test) => res.send(test)) .catch(next)); app.get("/test/:testId/render/side-effects", (req, res, next) => renderComponentSideEffects(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName, req.params.testId, parseInt(req.query.step, 10)) .then((testSideEffects) => res.send(testSideEffects)) .catch(next)); app.get("/test/:testId/run", (req, res, next) => exports.runComponentTest(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName, req.params.testId) .then(([results]) => results.map(({ result }) => result instanceof Error ? { result: "error", error: exports.serialiseError(result) } : { result })) .then((results) => res.send(results)) .catch(next)); app.get("/component/prop-types", (req, res, next) => getComponentPropTypes(path_1.default.join(SEARCH_PATH, req.query.file), req.query.exportName) .then((propTypes) => res.send(propTypes)) .catch(next)); app.get("/component/wrapper", (req, res, next) => getWrapperOptions() .then((wrappers) => res.send(wrappers)) .catch(next)); const startServer = (client_static_root) => { app.use("/", express_1.default.static(path_1.default.join(__dirname, client_static_root))); app.get("/*", (req, res) => res.sendFile(path_1.default.join(__dirname, client_static_root, "index.html"))); app.listen(PORT, () => { console.log(`Started Testbook http://localhost:${PORT}.`); openBrowser_1.default(`http://localhost:${PORT}`); }); }; exports.startServer = startServer; const isCLI = require.main === module; if (isCLI) { const [client_static_root] = process.argv.slice(2); exports.startServer(client_static_root); } //# sourceMappingURL=server.js.map