import {render} from '@testing-library/react';
import {JSDOM} from 'jsdom';
import React, {MutableRefObject} from 'react';
import util from 'util';
import {z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {initFbt, toJSON} from './shared-runtime';
const {window: testWindow} = new JSDOM(undefined);
(globalThis as any).document = testWindow.document;
(globalThis as any).window = testWindow.window;
(globalThis as any).React = React;
(globalThis as any).render = render;
initFbt();
(globalThis as any).placeholderFn = function (..._args: Array<any>) {
throw new Error('Fixture not implemented!');
};
export type EvaluatorResult = {
kind: 'ok' | 'exception' | 'UnexpectedError';
value: string;
logs: Array<string>;
};
const EntrypointSchema = z.strictObject({
fn: z.union([z.function(), z.object({})]),
params: z.array(z.any()),
isComponent: z.optional(z.boolean()),
sequentialRenders: z.optional(z.nullable(z.array(z.any()))).default(null),
});
const ExportSchema = z.object({
FIXTURE_ENTRYPOINT: EntrypointSchema,
});
const NO_ERROR_SENTINEL = Symbol();
class WrapperTestComponentWithErrorBoundary extends React.Component<
{fn: any; params: Array<any>},
{errorFromLastRender: any}
> {
propsErrorMap: Map<any, any>;
lastProps: any | null;
constructor(props: any) {
super(props);
this.lastProps = null;
this.propsErrorMap = new Map<any, any>();
this.state = {
errorFromLastRender: NO_ERROR_SENTINEL,
};
}
static getDerivedStateFromError(error: any) {
return {errorFromLastRender: error};
}
override componentDidUpdate() {
if (this.state.errorFromLastRender !== NO_ERROR_SENTINEL) {
this.setState({errorFromLastRender: NO_ERROR_SENTINEL});
}
}
override render() {
if (
this.state.errorFromLastRender !== NO_ERROR_SENTINEL &&
this.props === this.lastProps
) {
const errorMsg = `[[ (exception in render) ${this.state.errorFromLastRender?.toString()} ]]`;
this.propsErrorMap.set(this.lastProps, errorMsg);
return errorMsg;
}
this.lastProps = this.props;
const cachedError = this.propsErrorMap.get(this.props);
if (cachedError != null) {
return cachedError;
}
return React.createElement(WrapperTestComponent, this.props);
}
}
function WrapperTestComponent(props: {fn: any; params: Array<any>}) {
const result = props.fn(...props.params);
if (typeof result === 'object' && result != null && '$$typeof' in result) {
return result;
} else {
return toJSON(result);
}
}
function renderComponentSequentiallyForEachProps(
fn: any,
sequentialRenders: Array<any>,
): string {
if (sequentialRenders.length === 0) {
throw new Error(
'Expected at least one set of props when using `sequentialRenders`',
);
}
const initialProps = sequentialRenders[0]!;
const results = [];
const {rerender, container} = render(
React.createElement(WrapperTestComponentWithErrorBoundary, {
fn,
params: [initialProps],
}),
);
results.push(container.innerHTML);
for (let i = 1; i < sequentialRenders.length; i++) {
rerender(
React.createElement(WrapperTestComponentWithErrorBoundary, {
fn,
params: [sequentialRenders[i]],
}),
);
results.push(container.innerHTML);
}
return results.join('\n');
}
type FixtureEvaluatorResult = Omit<EvaluatorResult, 'logs'>;
(globalThis as any).evaluateFixtureExport = function (
exports: unknown,
): FixtureEvaluatorResult {
const parsedExportResult = ExportSchema.safeParse(exports);
if (!parsedExportResult.success) {
const exportDetail =
typeof exports === 'object' && exports != null
? `object ${util.inspect(exports)}`
: `${exports}`;
return {
kind: 'UnexpectedError',
value: `${fromZodError(parsedExportResult.error)}\nFound ` + exportDetail,
};
}
const entrypoint = parsedExportResult.data.FIXTURE_ENTRYPOINT;
if (entrypoint.sequentialRenders !== null) {
const result = renderComponentSequentiallyForEachProps(
entrypoint.fn,
entrypoint.sequentialRenders,
);
return {
kind: 'ok',
value: result ?? 'null',
};
} else if (typeof entrypoint.fn === 'object') {
const result = render(
React.createElement(entrypoint.fn as any, entrypoint.params[0]),
).container.innerHTML;
return {
kind: 'ok',
value: result ?? 'null',
};
} else {
const result = render(React.createElement(WrapperTestComponent, entrypoint))
.container.innerHTML;
return {
kind: 'ok',
value: result ?? 'null',
};
}
};
export function doEval(source: string): EvaluatorResult {
'use strict';
const originalConsole = globalThis.console;
const logs: Array<string> = [];
const mockedLog = (...args: Array<any>) => {
logs.push(
`${args.map(arg => {
if (arg instanceof Error) {
return arg.toString();
} else {
return util.inspect(arg);
}
})}`,
);
};
(globalThis.console as any) = {
info: mockedLog,
log: mockedLog,
warn: mockedLog,
error: (...args: Array<any>) => {
if (
typeof args[0] === 'string' &&
args[0].includes('ReactDOMTestUtils.act` is deprecated')
) {
return;
}
const stack = new Error().stack?.split('\n', 5) ?? [];
for (const stackFrame of stack) {
if (
(stackFrame.includes('at logCaughtError') &&
stackFrame.includes('react-dom-client.development.js')) ||
(stackFrame.includes('at defaultOnRecoverableError') &&
stackFrame.includes('react-dom-client.development.js'))
) {
return;
}
}
mockedLog(...args);
},
table: mockedLog,
trace: () => {},
};
try {
const evalResult: any = eval(`
(() => {
// Exports should be overwritten by source
let exports = {
FIXTURE_ENTRYPOINT: {
fn: globalThis.placeholderFn,
params: [],
},
};
let reachedInvoke = false;
try {
// run in an iife to avoid naming collisions
(() => {${source}})();
reachedInvoke = true;
if (exports.FIXTURE_ENTRYPOINT?.fn === globalThis.placeholderFn) {
return {
kind: "exception",
value: "Fixture not implemented",
};
}
return evaluateFixtureExport(exports);
} catch (e) {
if (!reachedInvoke) {
return {
kind: "UnexpectedError",
value: e.message,
};
} else {
return {
kind: "exception",
value: e.message,
};
}
}
})()`);
const result = {
...evalResult,
logs,
};
return result;
} catch (e) {
return {
kind: 'UnexpectedError',
value:
'Unexpected error during eval, possible syntax error?\n' + e.message,
logs,
};
} finally {
globalThis.console = originalConsole;
}
}