From d1a100b28f8b5a953b01411de04f613ccf90afa7 Mon Sep 17 00:00:00 2001 From: Adem Gaygusuz Date: Tue, 22 Dec 2020 20:30:01 +0000 Subject: [PATCH] feat(errors): Capture and display async render errors --- server/logger.ts | 20 +++++++ server/render.ts | 50 +++++++++-------- server/runner.ts | 4 +- server/server.ts | 95 +++++++++++++++++++------------- server/setupContainerAndMocks.ts | 2 + 5 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 server/logger.ts diff --git a/server/logger.ts b/server/logger.ts new file mode 100644 index 0000000..1809f51 --- /dev/null +++ b/server/logger.ts @@ -0,0 +1,20 @@ +export class Logger { + entries: [string, any][]; + + constructor() { + this.entries = []; + } + + addToLog = (type, args: any[]) => this.entries.push([type, args]); + + getLog = () => this.entries; +} + +export const setupConsoleLogger = () => + new Proxy(new Logger(), { + get: (target, name) => + target[name] || + function wrapper() { + target.addToLog(name, Array.prototype.slice.call(arguments)); + }, + }); diff --git a/server/render.ts b/server/render.ts index 7023156..2d6425e 100644 --- a/server/render.ts +++ b/server/render.ts @@ -52,26 +52,32 @@ const getWrapperComponent = () => { return Promise.resolve(); }; -window.result = getWrapperComponent().then((WrapperComponent) => { - act(() => { - ReactDOM.render( - React.createElement( - ErrorBoundary, - null, - WrapperComponent - ? React.createElement( - WrapperComponent, - window.wrapperProps, - React.createElement(Component, window.props) - ) - : React.createElement(Component, window.props) - ), - window.container, - () => { - if (window.error) { - throw window.error; - } - } - ); +const render = (WrapperComponent) => + new Promise((resolve) => { + act(() => { + ReactDOM.render( + React.createElement( + ErrorBoundary, + null, + WrapperComponent + ? React.createElement( + WrapperComponent, + window.wrapperProps, + React.createElement(Component, window.props) + ) + : React.createElement(Component, window.props) + ), + window.container, + resolve + ); + }); }); -}); + +window.result = getWrapperComponent().then((WrapperComponent) => + render(WrapperComponent).then( + () => + new Promise((resolve, reject) => + setTimeout(() => (window.error ? reject(window.error) : resolve()), 0) + ) + ) +); diff --git a/server/runner.ts b/server/runner.ts index ddadf9e..54f54de 100644 --- a/server/runner.ts +++ b/server/runner.ts @@ -1,7 +1,7 @@ import { findModuleTests, LoadedModule, - ResultsAndContext, + ResultsContextAndLogger, runComponentTest, serialiseError, } from "./server"; @@ -16,7 +16,7 @@ const runComponentTests = ( file: string, component: any ): Promise => - promiseSequence( + promiseSequence( component.tests.map((test) => () => runComponentTest( path.join(SEARCH_PATH, file), diff --git a/server/server.ts b/server/server.ts index 0e64257..b5bbc19 100644 --- a/server/server.ts +++ b/server/server.ts @@ -12,6 +12,7 @@ import webpack from "webpack"; import { getTsPropTypes } from "./propTypes"; import openBrowser from "react-dev-utils/openBrowser"; import { promiseBatch } from "./promise"; +import { Logger, setupConsoleLogger } from "./logger"; const hostNodeModulesPath = path.join(process.cwd(), "node_modules"); const { act } = require(path.join(hostNodeModulesPath, "react-dom/test-utils")); @@ -552,12 +553,14 @@ class AssertionError extends Error { } } -const createDOM = () => +const createDOM = (logger?: Logger) => new JSDOM('', { pretendToBeVisual: true, runScripts: "dangerously", url: "http://localhost", - virtualConsole: new VirtualConsole().sendTo(console), + virtualConsole: logger + ? new VirtualConsole().sendTo((logger as unknown) as Console) + : new VirtualConsole(), }); const render = ( @@ -604,43 +607,49 @@ const render = ( ); }); -const setupVmContextWithContainerAndMocks = (): Context => +const setupVmContextWithContainerAndMocks = (): Promise<[Context, Logger]> => compileWrapperWithWebpack(require.resolve("./setupContainerAndMocks")).then( (moduleCode) => { const script = new Script(moduleCode); - const context = createDOM().getInternalVMContext(); + const logger = setupConsoleLogger(); + const context = createDOM(logger).getInternalVMContext(); script.runInContext(context); - return context; + return [context, logger]; } ); const renderComponentSideEffects = (file, exportName, testId, step: number) => - runComponentTest(file, exportName, testId, step).then(([, context]) => { - const { container, mocks } = context; - if (!container) { + runComponentTest(file, exportName, testId, step).then( + ([, context, logger]) => { + const { container, mocks } = context; + if (!container) { + return { + regions: [], + mocks: [], + }; + } + + const elements = findTextNodes(container).map(([e, text]) => ({ + text, + type: ["BUTTON", "A"].includes(e.nodeName) ? "button" : "text", + xpath: getElementTreeXPath(e, context), + })); + return { - regions: [], - mocks: [], + regions: elements.map((e) => ({ + ...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 elements = findTextNodes(container).map(([e, text]) => ({ - text, - type: ["BUTTON", "A"].includes(e.nodeName) ? "button" : "text", - xpath: getElementTreeXPath(e, context), - })); - - return { - regions: elements.map((e) => ({ - ...e, - unique: !elements.find((f) => f.text === e.text && f.xpath !== e.xpath), - })), - mocks: mocks.map(({ name, mock }) => ({ - name, - calls: mock.getCalls(), - })), - }; - }); + ); const runRenderStep = ( file, @@ -738,7 +747,7 @@ type ResultObj = { result: Result; }; -export type ResultsAndContext = [ResultObj[], Context]; +export type ResultsContextAndLogger = [ResultObj[], Context, Logger]; type ResultAndContext = [Result, Context]; const runStep = ( @@ -757,38 +766,47 @@ export const runComponentTest = ( exportName, testId, step?: number -): Promise => +): Promise => Promise.all([ setupVmContextWithContainerAndMocks(), getComponentTest(file, exportName, testId), ]) .then( - ([context, { steps }]): Promise => + ([[context, logger], { steps }]): Promise => steps.reduce( ( - resultsAndContext: Promise, + resultsAndContext: Promise, s: StepDefinition, idx: number - ): Promise => + ): Promise => resultsAndContext.then( - ([results, context]): Promise => + ([results, context]): Promise => idx <= (step === undefined ? steps.length - 1 : step) && !results.find((r) => r.result instanceof Error) ? runStep(file, exportName, s, context).then( ([ result, newContext, - ]: ResultAndContext): ResultsAndContext => [ + ]: ResultAndContext): ResultsContextAndLogger => [ [...results, { result }], newContext, + logger, ] ) - : Promise.resolve([results, context] as ResultsAndContext) + : Promise.resolve([ + results, + context, + logger, + ] as ResultsContextAndLogger) ), - Promise.resolve([[], context] as ResultsAndContext) + Promise.resolve([[], context, logger] as ResultsContextAndLogger) ) ) - .then(([results, context]) => [results, context.close() || context]); + .then(([results, context, logger]) => [ + results, + context.close() || context, + logger, + ]); const getComponentPropTypes = (modulePath, exportName) => { if (/tsx?$/.test(modulePath)) { @@ -813,6 +831,7 @@ const getComponentPropTypes = (modulePath, exportName) => { const script = new Script(moduleCode); context.exportName = exportName; script.runInContext(context); + context.close(); return context.result; }); }; diff --git a/server/setupContainerAndMocks.ts b/server/setupContainerAndMocks.ts index 3028df8..de0aea0 100644 --- a/server/setupContainerAndMocks.ts +++ b/server/setupContainerAndMocks.ts @@ -1,9 +1,11 @@ import { Mock, setupMocks } from "./mocks"; +import { Logger } from "./logger"; declare global { interface Window { container: HTMLElement; mocks: Mock[]; + console: Logger; } }