Skip to content

Commit

Permalink
Merge pull request #20 from ademuk/logger
Browse files Browse the repository at this point in the history
Capture and display async render errors
  • Loading branch information
ademuk committed Dec 22, 2020
2 parents 245156b + d1a100b commit 1479749
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 62 deletions.
20 changes: 20 additions & 0 deletions server/logger.ts
Original file line number Diff line number Diff line change
@@ -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));
},
});
50 changes: 28 additions & 22 deletions server/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
);
4 changes: 2 additions & 2 deletions server/runner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
findModuleTests,
LoadedModule,
ResultsAndContext,
ResultsContextAndLogger,
runComponentTest,
serialiseError,
} from "./server";
Expand All @@ -16,7 +16,7 @@ const runComponentTests = (
file: string,
component: any
): Promise<TestRunnerResult[]> =>
promiseSequence<ResultsAndContext>(
promiseSequence<ResultsContextAndLogger>(
component.tests.map((test) => () =>
runComponentTest(
path.join(SEARCH_PATH, file),
Expand Down
95 changes: 57 additions & 38 deletions server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -552,12 +553,14 @@ class AssertionError extends Error {
}
}

const createDOM = () =>
const createDOM = (logger?: Logger) =>
new JSDOM('<!doctype html><html lang="en-GB"><body /></html>', {
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 = (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = (
Expand All @@ -757,38 +766,47 @@ export const runComponentTest = (
exportName,
testId,
step?: number
): Promise<ResultsAndContext> =>
): Promise<ResultsContextAndLogger> =>
Promise.all([
setupVmContextWithContainerAndMocks(),
getComponentTest(file, exportName, testId),
])
.then(
([context, { steps }]): Promise<ResultsAndContext> =>
([[context, logger], { steps }]): Promise<ResultsContextAndLogger> =>
steps.reduce(
(
resultsAndContext: Promise<ResultsAndContext>,
resultsAndContext: Promise<ResultsContextAndLogger>,
s: StepDefinition,
idx: number
): Promise<ResultsAndContext> =>
): Promise<ResultsContextAndLogger> =>
resultsAndContext.then(
([results, context]): Promise<ResultsAndContext> =>
([results, context]): Promise<ResultsContextAndLogger> =>
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)) {
Expand All @@ -813,6 +831,7 @@ const getComponentPropTypes = (modulePath, exportName) => {
const script = new Script(moduleCode);
context.exportName = exportName;
script.runInContext(context);
context.close();
return context.result;
});
};
Expand Down
2 changes: 2 additions & 0 deletions server/setupContainerAndMocks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Mock, setupMocks } from "./mocks";
import { Logger } from "./logger";

declare global {
interface Window {
container: HTMLElement;
mocks: Mock[];
console: Logger;
}
}

Expand Down

0 comments on commit 1479749

Please sign in to comment.