@describe-me/react 0.5.4 → 0.6.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 +14 -10
- package/dist/empty-live-result.d.ts +17 -0
- package/dist/empty-live-result.js +18 -0
- package/dist/is-empty-live-ui.d.ts +6 -0
- package/dist/is-empty-live-ui.js +18 -0
- package/dist/live-cleanup.d.ts +2 -0
- package/dist/live-cleanup.js +2 -0
- package/dist/live-render-hook-testing-library.d.ts +5 -0
- package/dist/live-render-hook-testing-library.js +8 -0
- package/dist/live-render-hook.d.ts +5 -0
- package/dist/live-render-hook.js +8 -0
- package/dist/live-render-testing-library.d.ts +8 -0
- package/dist/live-render-testing-library.js +18 -0
- package/dist/live-render.d.ts +8 -0
- package/dist/live-render.js +27 -0
- package/dist/live-testing-library.d.ts +4 -0
- package/dist/live-testing-library.js +6 -0
- package/dist/live.d.ts +3 -0
- package/dist/live.js +4 -0
- package/dist/mount-live.d.ts +17 -0
- package/dist/mount-live.js +24 -0
- package/dist/name-live-mocks.d.ts +6 -0
- package/dist/name-live-mocks.js +42 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ story, and each step is a frame you can scrub through. Stories cannot rot,
|
|
|
14
14
|
because they are tests. If one drifts from the component, CI goes red.
|
|
15
15
|
|
|
16
16
|
Status: prototype. React 18 and 19, Vitest 4 and 5 in browser mode or jsdom,
|
|
17
|
-
|
|
17
|
+
recorded frames and Live in the viewer.
|
|
18
18
|
|
|
19
19
|
## Which package do I need
|
|
20
20
|
|
|
@@ -52,15 +52,19 @@ Drop-in `render` functions that record a frame after mount and after every
|
|
|
52
52
|
`rerender`, and read the component's name and props for the docs. It is the
|
|
53
53
|
only React-specific code in describe-me.
|
|
54
54
|
|
|
55
|
-
| Entry point
|
|
56
|
-
|
|
|
57
|
-
| [`@describe-me/react`](https://grzehub.github.io/describe-me/api.html#entry-react)
|
|
58
|
-
| [`@describe-me/react/testing-library`](https://grzehub.github.io/describe-me/api.html#entry-react-testing-library)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
| Entry point | Wraps | For |
|
|
56
|
+
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------- |
|
|
57
|
+
| [`@describe-me/react`](https://grzehub.github.io/describe-me/api.html#entry-react) | `vitest-browser-react` | Vitest browser mode |
|
|
58
|
+
| [`@describe-me/react/testing-library`](https://grzehub.github.io/describe-me/api.html#entry-react-testing-library) | `@testing-library/react` | jsdom |
|
|
59
|
+
| [`@describe-me/react/live`](https://grzehub.github.io/describe-me/api.html#entry-react-live) | stands in for `vitest-browser-react` | the live preview |
|
|
60
|
+
| [`@describe-me/react/live-testing-library`](https://grzehub.github.io/describe-me/api.html#entry-react-live-testing-library) | `@testing-library/react` | the live preview |
|
|
61
|
+
|
|
62
|
+
The first two entry points re-export their test library with the recording
|
|
63
|
+
functions swapped in. With the `describeMe()` plugin you never import them
|
|
64
|
+
yourself: it redirects your `render` imports, those in your own `test-utils`
|
|
65
|
+
included. It redirects them to the live entry points only in the live preview,
|
|
66
|
+
so tests never import those. Install the test library your tests use. Both are
|
|
67
|
+
optional peers.
|
|
64
68
|
|
|
65
69
|
## License
|
|
66
70
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { LiveMountOptions } from './mount-live.js';
|
|
3
|
+
/** What the live render returns while it has nothing to mount. */
|
|
4
|
+
export interface EmptyLiveResult {
|
|
5
|
+
container: Element | DocumentFragment;
|
|
6
|
+
baseElement: Element;
|
|
7
|
+
rerender(ui: ReactNode): unknown;
|
|
8
|
+
unmount(): void;
|
|
9
|
+
asFragment(): DocumentFragment;
|
|
10
|
+
debug(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The result of a live render that had nothing to mount yet. Its `rerender`
|
|
14
|
+
* renders again into the same container, so the component mounts at the
|
|
15
|
+
* test's rerender.
|
|
16
|
+
*/
|
|
17
|
+
export declare function emptyLiveResult(options: LiveMountOptions, render: (ui: ReactNode, options: LiveMountOptions) => unknown): EmptyLiveResult;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of a live render that had nothing to mount yet. Its `rerender`
|
|
3
|
+
* renders again into the same container, so the component mounts at the
|
|
4
|
+
* test's rerender.
|
|
5
|
+
*/
|
|
6
|
+
export function emptyLiveResult(options, render) {
|
|
7
|
+
const baseElement = options.baseElement ?? document.body;
|
|
8
|
+
const container = options.container ?? baseElement.appendChild(document.createElement('div'));
|
|
9
|
+
const next = { ...options, baseElement, container };
|
|
10
|
+
return {
|
|
11
|
+
container,
|
|
12
|
+
baseElement,
|
|
13
|
+
rerender: (ui) => render(ui, next),
|
|
14
|
+
unmount: () => undefined,
|
|
15
|
+
asFragment: () => document.createDocumentFragment(),
|
|
16
|
+
debug: () => undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment, isValidElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a test rendered nothing yet: `null`, `undefined`, a boolean, `''`,
|
|
4
|
+
* or a fragment whose children are all empty. The live render then waits for
|
|
5
|
+
* the test's `rerender`.
|
|
6
|
+
*/
|
|
7
|
+
export function isEmptyLiveUi(ui) {
|
|
8
|
+
if (ui === null || ui === undefined || typeof ui === 'boolean' || ui === '') {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(ui)) {
|
|
12
|
+
return ui.every((child) => isEmptyLiveUi(child));
|
|
13
|
+
}
|
|
14
|
+
if (!isValidElement(ui) || ui.type !== Fragment) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return isEmptyLiveUi(ui.props.children);
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { liveStop } from '@describe-me/core/live';
|
|
2
|
+
/**
|
|
3
|
+
* Stands in for Testing Library's `renderHook` on the live page. A hook shows
|
|
4
|
+
* nothing, so it stops the test and the page reports `no-render`.
|
|
5
|
+
*/
|
|
6
|
+
export function liveRenderHookTestingLibrary() {
|
|
7
|
+
throw liveStop();
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { liveStop } from '@describe-me/core/live';
|
|
2
|
+
/**
|
|
3
|
+
* Stands in for `renderHook` from vitest-browser-react on the live page. A
|
|
4
|
+
* hook shows nothing, so it stops the test and the page reports `no-render`.
|
|
5
|
+
*/
|
|
6
|
+
export function liveRenderHook() {
|
|
7
|
+
return Promise.reject(liveStop());
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type LiveMountOptions } from './mount-live.js';
|
|
3
|
+
/**
|
|
4
|
+
* Stands in for Testing Library's `render` on the live page. It mounts the
|
|
5
|
+
* component and throws the stop signal, so the test ends at its first
|
|
6
|
+
* render. For an empty UI it waits for the test's `rerender`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function liveRenderTestingLibrary(ui: ReactNode, options?: LiveMountOptions): unknown;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { within } from '@testing-library/react';
|
|
2
|
+
import { liveStop } from '@describe-me/core/live';
|
|
3
|
+
import { emptyLiveResult } from './empty-live-result.js';
|
|
4
|
+
import { isEmptyLiveUi } from './is-empty-live-ui.js';
|
|
5
|
+
import { mountLive } from './mount-live.js';
|
|
6
|
+
/**
|
|
7
|
+
* Stands in for Testing Library's `render` on the live page. It mounts the
|
|
8
|
+
* component and throws the stop signal, so the test ends at its first
|
|
9
|
+
* render. For an empty UI it waits for the test's `rerender`.
|
|
10
|
+
*/
|
|
11
|
+
export function liveRenderTestingLibrary(ui, options = {}) {
|
|
12
|
+
if (isEmptyLiveUi(ui)) {
|
|
13
|
+
const result = emptyLiveResult(options, liveRenderTestingLibrary);
|
|
14
|
+
return { ...within(result.baseElement), ...result };
|
|
15
|
+
}
|
|
16
|
+
mountLive(ui, options);
|
|
17
|
+
throw liveStop();
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type LiveMountOptions } from './mount-live.js';
|
|
3
|
+
/**
|
|
4
|
+
* Stands in for `render` from vitest-browser-react on the live page. It
|
|
5
|
+
* mounts the component and rejects with the stop signal, so the test ends at
|
|
6
|
+
* its first render. For an empty UI it waits for the test's `rerender`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function liveRender(ui: ReactNode, options?: LiveMountOptions): Promise<unknown>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { liveInert, liveStop } from '@describe-me/core/live';
|
|
2
|
+
import { emptyLiveResult } from './empty-live-result.js';
|
|
3
|
+
import { isEmptyLiveUi } from './is-empty-live-ui.js';
|
|
4
|
+
import { mountLive } from './mount-live.js';
|
|
5
|
+
/** The rest of a vitest-browser-react screen, such as its locators, is inert. Its `then` too. */
|
|
6
|
+
function withInertRest(result) {
|
|
7
|
+
return new Proxy(result, {
|
|
8
|
+
get(target, key, receiver) {
|
|
9
|
+
if (key in target) {
|
|
10
|
+
return Reflect.get(target, key, receiver);
|
|
11
|
+
}
|
|
12
|
+
return Reflect.get(liveInert(), key);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Stands in for `render` from vitest-browser-react on the live page. It
|
|
18
|
+
* mounts the component and rejects with the stop signal, so the test ends at
|
|
19
|
+
* its first render. For an empty UI it waits for the test's `rerender`.
|
|
20
|
+
*/
|
|
21
|
+
export function liveRender(ui, options = {}) {
|
|
22
|
+
if (isEmptyLiveUi(ui)) {
|
|
23
|
+
return Promise.resolve(withInertRest(emptyLiveResult(options, liveRender)));
|
|
24
|
+
}
|
|
25
|
+
mountLive(ui, options);
|
|
26
|
+
return Promise.reject(liveStop());
|
|
27
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from '@testing-library/react';
|
|
2
|
+
export { liveCleanup as cleanup } from './live-cleanup.js';
|
|
3
|
+
export { liveRenderTestingLibrary as render } from './live-render-testing-library.js';
|
|
4
|
+
export { liveRenderHookTestingLibrary as renderHook } from './live-render-hook-testing-library.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Stands in for @testing-library/react on the live preview page, with `render`,
|
|
2
|
+
// `renderHook` and `cleanup` swapped. Not for import in tests.
|
|
3
|
+
export * from '@testing-library/react';
|
|
4
|
+
export { liveCleanup as cleanup } from './live-cleanup.js';
|
|
5
|
+
export { liveRenderTestingLibrary as render } from './live-render-testing-library.js';
|
|
6
|
+
export { liveRenderHookTestingLibrary as renderHook } from './live-render-hook-testing-library.js';
|
package/dist/live.d.ts
ADDED
package/dist/live.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Stands in for vitest-browser-react on the live preview page. Not for import in tests.
|
|
2
|
+
export { liveCleanup as cleanup } from './live-cleanup.js';
|
|
3
|
+
export { liveRender as render } from './live-render.js';
|
|
4
|
+
export { liveRenderHook as renderHook } from './live-render-hook.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { type RootOptions } from 'react-dom/client';
|
|
3
|
+
/** The render options, of either flavour, that the live mount reads. */
|
|
4
|
+
export interface LiveMountOptions {
|
|
5
|
+
container?: Element | DocumentFragment;
|
|
6
|
+
baseElement?: Element;
|
|
7
|
+
wrapper?: ComponentType<{
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}>;
|
|
10
|
+
createRootOptions?: RootOptions;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mounts what a test rendered, in its wrapper, with `createRoot` as Testing
|
|
14
|
+
* Library does, and reports the page as mounted. A page that has a status
|
|
15
|
+
* already mounts nothing. Either way the caller then stops the test.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mountLive(ui: ReactNode, options?: LiveMountOptions): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { liveStatus, reportLiveStatus } from '@describe-me/core/live';
|
|
4
|
+
import { nameLiveMocks } from './name-live-mocks.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mounts what a test rendered, in its wrapper, with `createRoot` as Testing
|
|
7
|
+
* Library does, and reports the page as mounted. A page that has a status
|
|
8
|
+
* already mounts nothing. Either way the caller then stops the test.
|
|
9
|
+
*/
|
|
10
|
+
export function mountLive(ui, options = {}) {
|
|
11
|
+
if (liveStatus() !== null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
nameLiveMocks(ui);
|
|
15
|
+
// Testing Library's own `beforeAll` turns it on, but the live component
|
|
16
|
+
// runs outside `act()`, where React would warn about every update.
|
|
17
|
+
const scope = globalThis;
|
|
18
|
+
scope.IS_REACT_ACT_ENVIRONMENT = false;
|
|
19
|
+
const baseElement = options.baseElement ?? document.body;
|
|
20
|
+
const container = options.container ?? baseElement.appendChild(document.createElement('div'));
|
|
21
|
+
const element = options.wrapper === undefined ? ui : createElement(options.wrapper, null, ui);
|
|
22
|
+
createRoot(container, options.createRootOptions).render(element);
|
|
23
|
+
reportLiveStatus('mounted');
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names every unnamed mock in what a test rendered after the prop it is
|
|
3
|
+
* passed as, so a click shows as `onSelect("Rename")`. Walks element props,
|
|
4
|
+
* children and arrays, a few levels deep.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nameLiveMocks(node: unknown, depth?: number): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
const MAX_DEPTH = 5;
|
|
3
|
+
/** The name Vitest gives a mock that was never named. */
|
|
4
|
+
const UNNAMED = 'vi.fn()';
|
|
5
|
+
/** Duck typed, so it needs nothing from Vitest. */
|
|
6
|
+
function isUnnamedMock(value) {
|
|
7
|
+
if (typeof value !== 'function') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const mock = value;
|
|
11
|
+
return (mock._isMockFunction === true &&
|
|
12
|
+
typeof mock.getMockName === 'function' &&
|
|
13
|
+
typeof mock.mockName === 'function' &&
|
|
14
|
+
mock.getMockName() === UNNAMED);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Names every unnamed mock in what a test rendered after the prop it is
|
|
18
|
+
* passed as, so a click shows as `onSelect("Rename")`. Walks element props,
|
|
19
|
+
* children and arrays, a few levels deep.
|
|
20
|
+
*/
|
|
21
|
+
export function nameLiveMocks(node, depth = 0) {
|
|
22
|
+
if (depth > MAX_DEPTH) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(node)) {
|
|
26
|
+
for (const item of node) {
|
|
27
|
+
nameLiveMocks(item, depth + 1);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!isValidElement(node)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const [prop, value] of Object.entries(node.props)) {
|
|
35
|
+
if (isUnnamedMock(value)) {
|
|
36
|
+
value.mockName(prop);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
nameLiveMocks(value, depth + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@describe-me/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Living component documentation from your Vitest tests: the React adapter of describe-me",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vitest",
|
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
"./testing-library": {
|
|
38
38
|
"types": "./dist/testing-library.d.ts",
|
|
39
39
|
"default": "./dist/testing-library.js"
|
|
40
|
+
},
|
|
41
|
+
"./live": {
|
|
42
|
+
"types": "./dist/live.d.ts",
|
|
43
|
+
"default": "./dist/live.js"
|
|
44
|
+
},
|
|
45
|
+
"./live-testing-library": {
|
|
46
|
+
"types": "./dist/live-testing-library.d.ts",
|
|
47
|
+
"default": "./dist/live-testing-library.js"
|
|
40
48
|
}
|
|
41
49
|
},
|
|
42
50
|
"files": [
|
|
@@ -48,13 +56,14 @@
|
|
|
48
56
|
"./dist/render-testing-library.js"
|
|
49
57
|
],
|
|
50
58
|
"dependencies": {
|
|
51
|
-
"@describe-me/core": "^0.
|
|
59
|
+
"@describe-me/core": "^0.6.0"
|
|
52
60
|
},
|
|
53
61
|
"peerDependencies": {
|
|
54
62
|
"@testing-library/react": "^16.0.0",
|
|
55
63
|
"react": "^18.0.0 || ^19.0.0",
|
|
64
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
56
65
|
"vitest-browser-react": "^2.0.0",
|
|
57
|
-
"@describe-me/vitest": "^0.
|
|
66
|
+
"@describe-me/vitest": "^0.6.0"
|
|
58
67
|
},
|
|
59
68
|
"peerDependenciesMeta": {
|
|
60
69
|
"@testing-library/react": {
|
|
@@ -73,7 +82,7 @@
|
|
|
73
82
|
"react-dom": "^19.3.0",
|
|
74
83
|
"vitest": "^5.0.1",
|
|
75
84
|
"vitest-browser-react": "^2.3.0",
|
|
76
|
-
"@describe-me/vitest": "^0.
|
|
85
|
+
"@describe-me/vitest": "^0.6.0"
|
|
77
86
|
},
|
|
78
87
|
"scripts": {
|
|
79
88
|
"build": "rimraf dist && tsc -p tsconfig.json"
|