UNPKG

@testing-library/react-render-stream

Version:
720 lines (708 loc) 21.4 kB
"use strict"; 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