@testing-library/react-render-stream
Version:
## What is this library?
720 lines (708 loc) • 21.4 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/pure.ts
var pure_exports = {};
__export(pure_exports, {
WaitForRenderTimeoutError: () => WaitForRenderTimeoutError,
cleanup: () => cleanup,
createRenderStream: () => createRenderStream,
disableActEnvironment: () => disableActEnvironment,
renderHookToSnapshotStream: () => renderHookToSnapshotStream,
useTrackRenders: () => useTrackRenders
});
module.exports = __toCommonJS(pure_exports);
// src/renderStream/createRenderStream.tsx
var React3 = __toESM(require("rehackt"), 1);
// src/assertable.ts
var assertableSymbol = Symbol.for(
"@testing-library/react-render-stream:assertable"
);
function markAssertable(assertable, stream) {
return Object.assign(assertable, {
[assertableSymbol]: stream
});
}
// src/renderWithoutAct.tsx
var ReactDOMClient = __toESM(require("react-dom/client"), 1);
var ReactDOM = __toESM(require("react-dom"), 1);
var import_dom2 = require("@testing-library/dom");
var import_react = __toESM(require("react"), 1);
// src/disableActEnvironment.ts
var import_dom = require("@testing-library/dom");
var dispose = (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Symbol.dispose ?? Symbol.for("nodejs.dispose")
);
function disableActEnvironment({
preventModification = disableActEnvironment.defaultOptions.preventModification,
adjustTestingLibConfig = disableActEnvironment.defaultOptions.adjustTestingLibConfig
} = {}) {
const typedGlobal = globalThis;
const cleanupFns = [];
{
const previous = typedGlobal.IS_REACT_ACT_ENVIRONMENT;
cleanupFns.push(() => {
Object.defineProperty(typedGlobal, "IS_REACT_ACT_ENVIRONMENT", {
value: previous,
writable: true,
configurable: true
});
});
Object.defineProperty(
typedGlobal,
"IS_REACT_ACT_ENVIRONMENT",
getNewPropertyDescriptor(false, preventModification)
);
}
if (adjustTestingLibConfig) {
const config = (0, import_dom.getConfig)();
const { asyncWrapper, eventWrapper } = config;
cleanupFns.push(() => {
Object.defineProperty(config, "asyncWrapper", {
value: asyncWrapper,
writable: true,
configurable: true
});
Object.defineProperty(config, "eventWrapper", {
value: eventWrapper,
writable: true,
configurable: true
});
});
Object.defineProperty(
config,
"asyncWrapper",
getNewPropertyDescriptor(
(fn) => fn(),
preventModification
)
);
Object.defineProperty(
config,
"eventWrapper",
getNewPropertyDescriptor(
(fn) => fn(),
preventModification
)
);
}
function cleanup2() {
while (cleanupFns.length > 0) {
cleanupFns.pop()();
}
}
return {
cleanup: cleanup2,
[dispose]: cleanup2
};
}
disableActEnvironment.defaultOptions = {
preventModification: true,
adjustTestingLibConfig: true
};
function getNewPropertyDescriptor(value, preventModification) {
return preventModification ? {
configurable: true,
enumerable: true,
get() {
return value;
},
set() {
}
} : {
configurable: true,
enumerable: true,
writable: true,
value
};
}
// src/renderWithoutAct.tsx
var mountedContainers = /* @__PURE__ */ new Set();
var mountedRootEntries = [];
function renderRoot(ui, {
baseElement,
container,
queries: queries2,
wrapper: WrapperComponent,
root
}) {
root.render(
WrapperComponent ? import_react.default.createElement(WrapperComponent, null, ui) : ui
);
return {
container,
baseElement,
debug: (el = baseElement, maxLength, options) => Array.isArray(el) ? (
// eslint-disable-next-line no-console
el.forEach(
(e) => console.log((0, import_dom2.prettyDOM)(e, maxLength, options))
)
) : (
// eslint-disable-next-line no-console,
console.log((0, import_dom2.prettyDOM)(el, maxLength, options))
),
unmount: () => {
root.unmount();
},
rerender: async (rerenderUi) => {
renderRoot(rerenderUi, {
container,
baseElement,
root,
wrapper: WrapperComponent
});
},
asFragment: () => {
if (typeof document.createRange === "function") {
return document.createRange().createContextualFragment(container.innerHTML);
} else {
const template = document.createElement("template");
template.innerHTML = container.innerHTML;
return template.content;
}
},
...(0, import_dom2.getQueriesForElement)(baseElement, queries2)
};
}
var renderWithoutAct = _renderWithoutAct;
async function _renderWithoutAct(ui, {
container,
baseElement = container,
queries: queries2,
wrapper
} = {}) {
if (!baseElement) {
baseElement = document.body;
}
if (!container) {
container = baseElement.appendChild(document.createElement("div"));
}
let root;
if (!mountedContainers.has(container)) {
root = (ReactDOM.version.startsWith("16") || ReactDOM.version.startsWith("17") ? createLegacyRoot : createConcurrentRoot)(container);
mountedRootEntries.push({ container, root });
mountedContainers.add(container);
} else {
mountedRootEntries.forEach((rootEntry) => {
if (rootEntry.container === container) {
root = rootEntry.root;
}
});
}
return renderRoot(ui, {
baseElement,
container,
queries: queries2,
wrapper,
root
});
}
function createLegacyRoot(container) {
return {
render(element) {
ReactDOM.render(element, container);
},
unmount() {
ReactDOM.unmountComponentAtNode(container);
}
};
}
function createConcurrentRoot(container) {
const anyThis = globalThis;
if (anyThis.IS_REACT_ACT_ENVIRONMENT) {
throw new Error(`Tried to create a React root for a render stream inside a React act environment.
This is not supported. Please use \`disableActEnvironment\` to disable the act environment for this test.`);
}
const root = ReactDOMClient.createRoot(container);
return {
render(element) {
if (anyThis.IS_REACT_ACT_ENVIRONMENT) {
throw new Error(`Tried to render a render stream inside a React act environment.
This is not supported. Please use \`disableActEnvironment\` to disable the act environment for this test.`);
}
root.render(element);
},
unmount() {
root.unmount();
}
};
}
function cleanup() {
if (!mountedRootEntries.length) {
return;
}
const disabledAct = disableActEnvironment({
preventModification: false,
adjustTestingLibConfig: false
});
try {
for (const { root, container } of mountedRootEntries) {
root.unmount();
if (container.parentNode === document.body) {
document.body.removeChild(container);
}
}
mountedRootEntries.length = 0;
mountedContainers.clear();
} finally {
disabledAct.cleanup();
}
}
// src/renderStream/Render.tsx
var import_dom3 = require("@testing-library/dom");
var import_jsdom = require("jsdom");
var RenderInstance = class {
id;
phase;
actualDuration;
baseDuration;
startTime;
commitTime;
count;
snapshot;
stringifiedDOM;
renderedComponents;
queries;
constructor(baseRender, snapshot, stringifiedDOM, renderedComponents, queries2) {
this.snapshot = snapshot;
this.stringifiedDOM = stringifiedDOM;
this.renderedComponents = renderedComponents;
this.id = baseRender.id;
this.phase = baseRender.phase;
this.actualDuration = baseRender.actualDuration;
this.baseDuration = baseRender.baseDuration;
this.startTime = baseRender.startTime;
this.commitTime = baseRender.commitTime;
this.count = baseRender.count;
this.queries = queries2;
}
_domSnapshot;
get domSnapshot() {
if (this._domSnapshot) return this._domSnapshot;
if (!this.stringifiedDOM) {
throw new Error(
"DOM snapshot is not available - please set the `snapshotDOM` option"
);
}
const virtualConsole = new import_jsdom.VirtualConsole();
virtualConsole.on("jsdomError", (error) => {
throw error;
});
const snapDOM = new import_jsdom.JSDOM(this.stringifiedDOM, {
runScripts: "dangerously",
virtualConsole
});
const document2 = snapDOM.window.document;
const body = document2.body;
const script = document2.createElement("script");
script.type = "text/javascript";
script.text = `
${errorOnDomInteraction.toString()};
${errorOnDomInteraction.name}();
`;
body.appendChild(script);
body.removeChild(script);
return this._domSnapshot = body;
}
get withinDOM() {
const snapScreen = Object.assign(
(0, import_dom3.getQueriesForElement)(
this.domSnapshot,
this.queries
),
{
debug: (...[dom = this.domSnapshot, ...rest]) => import_dom3.screen.debug(dom, ...rest),
logTestingPlaygroundURL: (...[dom = this.domSnapshot, ...rest]) => import_dom3.screen.logTestingPlaygroundURL(dom, ...rest)
}
);
return () => snapScreen;
}
};
function errorOnDomInteraction() {
const events = [
"auxclick",
"blur",
"change",
"click",
"copy",
"cut",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragleave",
"dragover",
"dragstart",
"drop",
"focus",
"focusin",
"focusout",
"input",
"keydown",
"keypress",
"keyup",
"mousedown",
"mouseenter",
"mouseleave",
"mousemove",
"mouseout",
"mouseover",
"mouseup",
"paste",
"pointercancel",
"pointerdown",
"pointerenter",
"pointerleave",
"pointermove",
"pointerout",
"pointerover",
"pointerup",
"scroll",
"select",
"selectionchange",
"selectstart",
"submit",
"toggle",
"touchcancel",
"touchend",
"touchmove",
"touchstart",
"wheel"
];
function warnOnDomInteraction() {
throw new Error(`
DOM interaction with a snapshot detected in test.
Please don't interact with the DOM you get from \`withinDOM\`,
but still use \`screen\` to get elements for simulating user interaction.
`);
}
events.forEach((event) => {
document.addEventListener(event, warnOnDomInteraction);
});
}
// src/renderStream/context.tsx
var React2 = __toESM(require("rehackt"), 1);
var RenderStreamContext = React2.createContext(void 0);
function RenderStreamContextProvider({
children,
value
}) {
const parentContext = useRenderStreamContext();
if (parentContext) {
throw new Error("Render streams should not be nested in the same tree");
}
return /* @__PURE__ */ React2.createElement(RenderStreamContext.Provider, { value }, children);
}
function useRenderStreamContext() {
return React2.useContext(RenderStreamContext);
}
// src/renderStream/syncQueries.ts
var import_dom4 = require("@testing-library/dom");
var syncQueries = Object.fromEntries(
Object.entries(import_dom4.queries).filter(
([key]) => key.startsWith("get") || key.startsWith("query")
)
);
// src/renderStream/createRenderStream.tsx
var WaitForRenderTimeoutError = class extends Error {
constructor() {
super("Exceeded timeout waiting for next render.");
this.name = "WaitForRenderTimeoutError";
Object.setPrototypeOf(this, new.target.prototype);
}
};
function createRenderStream({
onRender,
snapshotDOM = false,
initialSnapshot,
skipNonTrackingRenders,
queries: queries2 = syncQueries
} = {}) {
const stream = {};
let nextRender, resolveNextRender, rejectNextRender;
function resetNextRender() {
nextRender = void 0;
resolveNextRender = void 0;
rejectNextRender = void 0;
}
const snapshotRef = { current: initialSnapshot };
const replaceSnapshot = (snap) => {
if (typeof snap === "function") {
if (!initialSnapshot) {
throw new Error(
"Cannot use a function to update the snapshot if no initial snapshot was provided."
);
}
snapshotRef.current = snap(
typeof snapshotRef.current === "object" ? (
// "cheap best effort" to prevent accidental mutation of the last snapshot
{ ...snapshotRef.current }
) : snapshotRef.current
);
} else {
snapshotRef.current = snap;
}
};
const mergeSnapshot = (partialSnapshot) => {
replaceSnapshot((snapshot) => ({
...snapshot,
...typeof partialSnapshot === "function" ? partialSnapshot(snapshot) : partialSnapshot
}));
};
const renderStreamContext = {
renderedComponents: []
};
const profilerOnRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
if (skipNonTrackingRenders && renderStreamContext.renderedComponents.length === 0) {
return;
}
const renderBase = {
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
count: stream.renders.length + 1
};
try {
onRender?.({
...renderBase,
replaceSnapshot,
mergeSnapshot,
snapshot: snapshotRef.current
});
const snapshot = snapshotRef.current;
const domSnapshot = snapshotDOM ? window.document.body.innerHTML : void 0;
const render3 = new RenderInstance(
renderBase,
snapshot,
domSnapshot,
renderStreamContext.renderedComponents,
queries2
);
renderStreamContext.renderedComponents = [];
stream.renders.push(render3);
resolveNextRender?.(render3);
} catch (error) {
stream.renders.push({
phase: "snapshotError",
count: stream.renders.length,
error
});
rejectNextRender?.(error);
} finally {
resetNextRender();
}
};
let iteratorPosition = 0;
function Wrapper({ children }) {
return /* @__PURE__ */ React3.createElement(RenderStreamContextProvider, { value: renderStreamContext }, /* @__PURE__ */ React3.createElement(React3.Profiler, { id: "test", onRender: profilerOnRender }, children));
}
const render2 = async (ui, options) => {
const ret = await renderWithoutAct(ui, {
...options,
wrapper: (props) => {
const ParentWrapper = options?.wrapper ?? React3.Fragment;
return /* @__PURE__ */ React3.createElement(ParentWrapper, null, /* @__PURE__ */ React3.createElement(Wrapper, null, props.children));
}
});
if (stream.renders.length === 0) {
await stream.waitForNextRender();
}
const origRerender = ret.rerender;
ret.rerender = async function rerender(rerenderUi) {
const previousRenderCount = stream.renders.length;
try {
return await origRerender(rerenderUi);
} finally {
if (previousRenderCount === stream.renders.length) {
await stream.waitForNextRender();
}
}
};
return ret;
};
Object.assign(stream, {
replaceSnapshot,
mergeSnapshot,
renders: new Array(),
totalRenderCount() {
return stream.renders.length;
},
async peekRender(options = {}) {
try {
if (iteratorPosition < stream.renders.length) {
const peekedRender = stream.renders[iteratorPosition];
if (peekedRender.phase === "snapshotError") {
throw peekedRender.error;
}
return peekedRender;
}
return await stream.waitForNextRender(options).catch(rethrowWithCapturedStackTrace(stream.peekRender));
} finally {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
}
},
takeRender: markAssertable(async function takeRender(options = {}) {
let error;
try {
return await stream.peekRender({
...options
});
} catch (e) {
if (e instanceof Object) {
Error.captureStackTrace(e, stream.takeRender);
}
error = e;
throw e;
} finally {
if (!(error && error instanceof WaitForRenderTimeoutError)) {
iteratorPosition++;
}
}
}, stream),
getCurrentRender() {
const currentPosition = iteratorPosition - 1;
if (currentPosition < 0) {
throw new Error(
"No current render available. You need to call `takeRender` before you can get the current render."
);
}
const currentRender = stream.renders[currentPosition];
if (currentRender.phase === "snapshotError") {
throw currentRender.error;
}
return currentRender;
},
waitForNextRender({ timeout = 1e3 } = {}) {
if (!nextRender) {
nextRender = Promise.race([
new Promise((resolve, reject) => {
resolveNextRender = resolve;
rejectNextRender = reject;
}),
new Promise(
(_, reject) => setTimeout(() => {
const error = new WaitForRenderTimeoutError();
Error.captureStackTrace(error, stream.waitForNextRender);
reject(error);
resetNextRender();
}, timeout)
)
]);
}
return nextRender;
},
render: render2
});
return stream;
}
function rethrowWithCapturedStackTrace(constructorOpt) {
return function catchFn(error) {
if (error instanceof Object) {
Error.captureStackTrace(error, constructorOpt);
}
throw error;
};
}
// src/renderStream/useTrackRenders.ts
var import_rehackt = __toESM(require("rehackt"), 1);
function resolveR18HookOwner() {
return import_rehackt.default.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner?.current?.elementType;
}
function resolveR19HookOwner() {
return import_rehackt.default.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.A?.getOwner().elementType;
}
function useTrackRenders({ name } = {}) {
const component = name ?? resolveR18HookOwner() ?? resolveR19HookOwner();
if (!component) {
throw new Error(
"useTrackRenders: Unable to determine component. Please ensure the hook is called inside a rendered component or provide a `name` option."
);
}
const ctx = useRenderStreamContext();
if (!ctx) {
throw new Error(
"useTrackRenders: A Render Stream must be created and rendered to track component renders"
);
}
import_rehackt.default.useLayoutEffect(() => {
ctx.renderedComponents.unshift(component);
});
}
// src/renderHookToSnapshotStream.tsx
var import_rehackt2 = __toESM(require("rehackt"), 1);
async function renderHookToSnapshotStream(renderCallback, { initialProps, ...renderOptions } = {}) {
const { render: render2, ...stream } = createRenderStream();
const HookComponent = (props) => {
stream.replaceSnapshot({ value: renderCallback(props.arg) });
return null;
};
const { rerender: baseRerender, unmount } = await render2(
/* @__PURE__ */ import_rehackt2.default.createElement(HookComponent, { arg: initialProps }),
renderOptions
);
function rerender(rerenderCallbackProps) {
return baseRerender(/* @__PURE__ */ import_rehackt2.default.createElement(HookComponent, { arg: rerenderCallbackProps }));
}
return {
[assertableSymbol]: stream,
renders: stream.renders,
totalSnapshotCount: stream.totalRenderCount,
async peekSnapshot(options) {
return (await stream.peekRender(options)).snapshot.value;
},
takeSnapshot: markAssertable(async function takeSnapshot(options) {
return (await stream.takeRender(options)).snapshot.value;
}, stream),
getCurrentSnapshot() {
return stream.getCurrentRender().snapshot.value;
},
async waitForNextSnapshot(options) {
return (await stream.waitForNextRender(options)).snapshot.value;
},
rerender,
unmount
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
WaitForRenderTimeoutError,
cleanup,
createRenderStream,
disableActEnvironment,
renderHookToSnapshotStream,
useTrackRenders
});
//# sourceMappingURL=pure.cjs.map
;