@describe-me/react 0.3.0 → 0.5.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,8 +20,11 @@ pnpm add -D @describe-me/react
20
20
 
21
21
  ## Usage
22
22
 
23
- Each entry point re-exports everything its test library offers, with `render`
24
- swapped for the recording one, so switching an existing test is one import.
23
+ Each entry point re-exports everything its test library offers, with the
24
+ recording functions swapped in, so switching an existing test is one import.
25
+ `@describe-me/react` swaps `render`. The Testing Library entry swaps `render`,
26
+ `fireEvent` and `cleanup`: every `fireEvent` call records a frame, and
27
+ `cleanup` takes the closing frame before it unmounts.
25
28
 
26
29
  ```tsx
27
30
  import { render, step } from '@describe-me/react'
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Drop-in for `cleanup` from @testing-library/react. Takes the closing frame
3
+ * before it unmounts, so `afterEach(cleanup)` in a test file records the last
4
+ * state instead of an empty page.
5
+ */
6
+ export declare function cleanup(): void;
@@ -0,0 +1,11 @@
1
+ import { cleanup as unmountAll } from '@testing-library/react';
2
+ import { recorder } from '@describe-me/core';
3
+ /**
4
+ * Drop-in for `cleanup` from @testing-library/react. Takes the closing frame
5
+ * before it unmounts, so `afterEach(cleanup)` in a test file records the last
6
+ * state instead of an empty page.
7
+ */
8
+ export function cleanup() {
9
+ void recorder.capture('end', 'end of test', undefined, { settle: false });
10
+ unmountAll();
11
+ }
@@ -1,4 +1,7 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import { type ComponentInfo } from '@describe-me/core';
3
- /** Pull the component name and JSON-safe props out of a React element. */
3
+ /**
4
+ * Pull the component name, its defining file when the plugin registered it,
5
+ * and JSON-safe props out of a React element.
6
+ */
4
7
  export declare function describeElement(ui: ReactElement): ComponentInfo;
@@ -1,12 +1,10 @@
1
- import { serializeValue } from '@describe-me/core';
2
- /** Pull the component name and JSON-safe props out of a React element. */
1
+ import { describeComponentType, serializeValue } from '@describe-me/core';
2
+ /**
3
+ * Pull the component name, its defining file when the plugin registered it,
4
+ * and JSON-safe props out of a React element.
5
+ */
3
6
  export function describeElement(ui) {
4
- const type = ui.type;
5
- const name = typeof type === 'string'
6
- ? type
7
- : (type?.displayName ??
8
- type?.name ??
9
- 'Anonymous');
7
+ const identity = describeComponentType(ui.type);
10
8
  const props = serializeValue(ui.props ?? {});
11
- return { name, props };
9
+ return { ...identity, props };
12
10
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The frame label of one `fireEvent` call, e.g. `click(button "Save")`,
3
+ * `change(text "Name", "hello")` or `scroll()` for a target that is not an
4
+ * element, such as `window`.
5
+ */
6
+ export declare function fireEventLabel(eventName: string, target: unknown, init?: unknown): string;
@@ -0,0 +1,22 @@
1
+ import { elementLabel } from '@describe-me/core';
2
+ /**
3
+ * The frame label of one `fireEvent` call, e.g. `click(button "Save")`,
4
+ * `change(text "Name", "hello")` or `scroll()` for a target that is not an
5
+ * element, such as `window`.
6
+ */
7
+ export function fireEventLabel(eventName, target, init) {
8
+ const parts = [];
9
+ const element = elementLabel(target);
10
+ if (element) {
11
+ parts.push(element);
12
+ }
13
+ const { target: initTarget, key } = (init ?? {});
14
+ const value = initTarget?.value;
15
+ if (typeof value === 'string') {
16
+ parts.push(JSON.stringify(value));
17
+ }
18
+ if (typeof key === 'string') {
19
+ parts.push(JSON.stringify(key));
20
+ }
21
+ return `${eventName}(${parts.join(', ')})`;
22
+ }
@@ -0,0 +1,10 @@
1
+ import { fireEvent as baseFireEvent } from '@testing-library/react';
2
+ type FireEvent = typeof baseFireEvent;
3
+ /**
4
+ * Drop-in for `fireEvent` from @testing-library/react: the function and every
5
+ * method record one frame per call, labelled like `click(button "Save")`.
6
+ * The label is read before the event fires, so it names the element the test
7
+ * targeted, not what the event turned it into.
8
+ */
9
+ export declare const fireEvent: FireEvent;
10
+ export {};
@@ -0,0 +1,35 @@
1
+ import { fireEvent as baseFireEvent } from '@testing-library/react';
2
+ import { recorder } from '@describe-me/core';
3
+ import { fireEventLabel } from './fire-event-label.js';
4
+ import { recordSyncAction } from './record-sync-action.js';
5
+ /** Fire through Testing Library, recording a frame only while a test is recorded. */
6
+ function fire(label, dispatch) {
7
+ if (!recorder.isActive) {
8
+ return dispatch();
9
+ }
10
+ return recordSyncAction(label(), dispatch);
11
+ }
12
+ /** `fireEvent(element, event)` is named after the event it dispatches. */
13
+ function eventName(event) {
14
+ const type = event?.type;
15
+ return typeof type === 'string' ? type : 'fireEvent';
16
+ }
17
+ /** Testing Library's `fireEvent` with the function and each of its methods wrapped. */
18
+ function recordingFireEvent() {
19
+ const methods = baseFireEvent;
20
+ const recording = (element, event) => fire(() => fireEventLabel(eventName(event), element), () => baseFireEvent(element, event));
21
+ const wrappers = recording;
22
+ // Composite events such as `mouseEnter` call Testing Library's own object,
23
+ // not these wrappers, so one call stays one frame.
24
+ for (const name of Object.keys(baseFireEvent)) {
25
+ wrappers[name] = (element, init) => fire(() => fireEventLabel(name, element, init), () => methods[name](element, init));
26
+ }
27
+ return recording;
28
+ }
29
+ /**
30
+ * Drop-in for `fireEvent` from @testing-library/react: the function and every
31
+ * method record one frame per call, labelled like `click(button "Save")`.
32
+ * The label is read before the event fires, so it names the element the test
33
+ * targeted, not what the event turned it into.
34
+ */
35
+ export const fireEvent = recordingFireEvent();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Run one synchronous interaction and record a frame for it, but only for the
3
+ * outermost call and only when it returns. Testing Library wraps every event
4
+ * in `act()`, so the DOM is committed by then and the frame is taken without
5
+ * settling.
6
+ */
7
+ export declare function recordSyncAction<T>(label: string, run: () => T): T;
@@ -0,0 +1,27 @@
1
+ import { recorder } from '@describe-me/core';
2
+ /**
3
+ * Nesting depth of synchronous interactions currently running. A handler
4
+ * that fires another event from inside the first would otherwise record two
5
+ * frames for one call.
6
+ */
7
+ let depth = 0;
8
+ /**
9
+ * Run one synchronous interaction and record a frame for it, but only for the
10
+ * outermost call and only when it returns. Testing Library wraps every event
11
+ * in `act()`, so the DOM is committed by then and the frame is taken without
12
+ * settling.
13
+ */
14
+ export function recordSyncAction(label, run) {
15
+ depth++;
16
+ let result;
17
+ try {
18
+ result = run();
19
+ }
20
+ finally {
21
+ depth--;
22
+ }
23
+ if (depth === 0) {
24
+ void recorder.capture('action', label, undefined, { settle: false });
25
+ }
26
+ return result;
27
+ }
@@ -4,16 +4,18 @@ type RenderOptions = Parameters<typeof baseRender>[1];
4
4
  type RenderResult = ReturnType<typeof baseRender>;
5
5
  /**
6
6
  * Drop-in for `render` from @testing-library/react (jsdom and other DOM
7
- * environments). Records a frame after mount and after every `rerender`.
7
+ * environments). Records a frame after mount and after every `rerender`
8
+ * while a test is being recorded.
8
9
  *
9
10
  * Synchronous like the original, so `const { asFragment } = render(...)`
10
11
  * keeps working. Testing Library wraps mounting in `act()`, which means the DOM
11
12
  * is committed when it returns, so frames are captured without settling: the
12
13
  * snapshot is taken right here, before the test's next line can change the DOM.
13
14
  *
14
- * Unmounting is handed to the recorder's teardown rather than to Testing
15
- * Library's own auto-cleanup, which would run before the closing frame is
16
- * taken. The plugin switches auto-cleanup off with `RTL_SKIP_AUTO_CLEANUP`.
15
+ * Importing this module hands unmounting to the recorder's teardown rather
16
+ * than to Testing Library's own auto-cleanup, which would run before the
17
+ * closing frame is taken. The plugin switches auto-cleanup off with
18
+ * `RTL_SKIP_AUTO_CLEANUP`.
17
19
  */
18
20
  export declare function render(ui: ReactElement, options?: RenderOptions): RenderResult;
19
21
  export {};
@@ -1,25 +1,29 @@
1
- import { cleanup, render as baseRender } from '@testing-library/react';
1
+ import { cleanup as unmountAll, render as baseRender } from '@testing-library/react';
2
2
  import { isValidElement } from 'react';
3
3
  import { recorder } from '@describe-me/core';
4
4
  import { describeElement } from './describe-element.js';
5
5
  import { labelFor } from './label-for.js';
6
+ // On import rather than in `render()`, so a file that only calls `renderHook`
7
+ // is unmounted after each test too.
8
+ recorder.onTeardown(unmountAll);
6
9
  /**
7
10
  * Drop-in for `render` from @testing-library/react (jsdom and other DOM
8
- * environments). Records a frame after mount and after every `rerender`.
11
+ * environments). Records a frame after mount and after every `rerender`
12
+ * while a test is being recorded.
9
13
  *
10
14
  * Synchronous like the original, so `const { asFragment } = render(...)`
11
15
  * keeps working. Testing Library wraps mounting in `act()`, which means the DOM
12
16
  * is committed when it returns, so frames are captured without settling: the
13
17
  * snapshot is taken right here, before the test's next line can change the DOM.
14
18
  *
15
- * Unmounting is handed to the recorder's teardown rather than to Testing
16
- * Library's own auto-cleanup, which would run before the closing frame is
17
- * taken. The plugin switches auto-cleanup off with `RTL_SKIP_AUTO_CLEANUP`.
19
+ * Importing this module hands unmounting to the recorder's teardown rather
20
+ * than to Testing Library's own auto-cleanup, which would run before the
21
+ * closing frame is taken. The plugin switches auto-cleanup off with
22
+ * `RTL_SKIP_AUTO_CLEANUP`.
18
23
  */
19
24
  export function render(ui, options) {
20
- recorder.onTeardown(cleanup);
21
25
  const result = baseRender(ui, options);
22
- if (isValidElement(ui)) {
26
+ if (recorder.isActive && isValidElement(ui)) {
23
27
  const info = describeElement(ui);
24
28
  recorder.setComponent(info);
25
29
  void recorder.capture('render', labelFor(info), { props: info.props }, { settle: false });
@@ -27,7 +31,7 @@ export function render(ui, options) {
27
31
  const originalRerender = result.rerender;
28
32
  result.rerender = (next) => {
29
33
  originalRerender(next);
30
- if (isValidElement(next)) {
34
+ if (recorder.isActive && isValidElement(next)) {
31
35
  const info = describeElement(next);
32
36
  void recorder.capture('render', `rerender ${labelFor(info)}`, { props: info.props }, { settle: false });
33
37
  }
@@ -1,3 +1,5 @@
1
1
  export * from '@testing-library/react';
2
2
  export { step } from '@describe-me/core';
3
+ export { cleanup } from './cleanup.js';
4
+ export { fireEvent } from './fire-event.js';
3
5
  export { render } from './render-testing-library.js';
@@ -1,4 +1,7 @@
1
- // Everything @testing-library/react offers, with `render` swapped for the recording one.
1
+ // Everything @testing-library/react offers, with `render`, `fireEvent` and
2
+ // `cleanup` swapped for recording ones.
2
3
  export * from '@testing-library/react';
3
4
  export { step } from '@describe-me/core';
5
+ export { cleanup } from './cleanup.js';
6
+ export { fireEvent } from './fire-event.js';
4
7
  export { render } from './render-testing-library.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@describe-me/react",
3
- "version": "0.3.0",
3
+ "version": "0.5.0-next.0",
4
4
  "description": "React adapter for describe-me: drop-in render functions that record frames",
5
5
  "keywords": [
6
6
  "vitest",
@@ -42,14 +42,16 @@
42
42
  "README.md",
43
43
  "LICENSE"
44
44
  ],
45
- "sideEffects": false,
45
+ "sideEffects": [
46
+ "./dist/render-testing-library.js"
47
+ ],
46
48
  "dependencies": {
47
- "@describe-me/core": "^0.3.0"
49
+ "@describe-me/core": "^0.5.0-next.0"
48
50
  },
49
51
  "peerDependencies": {
50
- "@testing-library/react": ">=16",
51
- "react": ">=18",
52
- "vitest-browser-react": ">=2"
52
+ "@testing-library/react": "^16.0.0",
53
+ "react": "^18.0.0 || ^19.0.0",
54
+ "vitest-browser-react": "^2.0.0"
53
55
  },
54
56
  "peerDependenciesMeta": {
55
57
  "@testing-library/react": {