@describe-me/vitest 0.5.0-next.1 → 0.5.0-next.2

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
@@ -61,6 +61,11 @@ font links that the app shell loads and tests never do. It never reaches the
61
61
  test page, and the local files it links are copied into the output directory
62
62
  ([Fonts](https://github.com/grzehub/describe-me#fonts) in the root README).
63
63
 
64
+ `describeMe({ renderFrame })` chooses when the render frame is taken:
65
+ `'eager'` (default) right after mount, `'lazy'` right before the next
66
+ interaction, or `{ pending: '<selector>' }` once a loader is gone. See
67
+ [Render frame timing](https://github.com/grzehub/describe-me#render-frame-timing).
68
+
64
69
  The pieces can also be wired by hand. Tests then import from the adapter
65
70
  themselves: `render` from `@describe-me/react` in browser mode, and `render`,
66
71
  `fireEvent` and `cleanup` from `@describe-me/react/testing-library` in jsdom:
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ViteUserConfig } from 'vitest/config';
2
+ import type { RenderFrameMode } from '@describe-me/core/types';
2
3
  /** Where the tests run: Vitest browser mode, or a simulated DOM such as jsdom. */
3
4
  export type DescribeMeEnvironment = 'browser' | 'dom';
4
5
  export interface DescribeMeOptions {
@@ -41,6 +42,12 @@ export interface DescribeMeOptions {
41
42
  * Project files it links are copied into the output directory. Default: none.
42
43
  */
43
44
  previewHead?: string;
45
+ /**
46
+ * When the render frame is taken: `'eager'` right after mount, `'lazy'`
47
+ * before the next interaction, `{ pending }` once nothing matches the
48
+ * selector. Default: `'eager'`.
49
+ */
50
+ renderFrame?: RenderFrameMode;
44
51
  }
45
52
  type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
46
53
  /**
package/dist/plugin.js CHANGED
@@ -5,6 +5,7 @@ import { registerExports } from './register-exports.js';
5
5
  import DescribeMeReporter from './reporter.js';
6
6
  import { RUNTIME_OPTIONS_KEY } from './runtime-options.js';
7
7
  import { styledComponentsBrowserBuild } from './styled-components-browser-build.js';
8
+ import { validateRenderFrame } from './validate-render-frame.js';
8
9
  /** For each framework and environment: the render module tests import, and our adapter that wraps it. */
9
10
  const RENDER_MODULES = {
10
11
  react: {
@@ -101,11 +102,12 @@ export function describeMe(options = {}) {
101
102
  }
102
103
  const environment = options.environment ?? detectEnvironment(userConfig);
103
104
  renderModule = RENDER_MODULES[framework][environment];
104
- // Compiled here, so invalid globs fail before any test runs. Vite
105
+ // Checked here, so invalid options fail before any test runs. Vite
105
106
  // deep-merges `provide`, so the user's own keys survive.
106
107
  const runtimeOptions = {
107
108
  include: compileGlobs(options.include),
108
109
  exclude: compileGlobs(options.exclude),
110
+ renderFrame: validateRenderFrame(options.renderFrame),
109
111
  };
110
112
  const provide = { [RUNTIME_OPTIONS_KEY]: runtimeOptions };
111
113
  const reporter = new DescribeMeReporter({
@@ -2,5 +2,9 @@ import { inject } from 'vitest';
2
2
  import { RUNTIME_OPTIONS_KEY } from './runtime-options.js';
3
3
  /** Without the plugin nothing is provided, so a hand-wired setup keeps recording every test. */
4
4
  export function readRuntimeOptions() {
5
- return inject(RUNTIME_OPTIONS_KEY) ?? { include: [], exclude: [] };
5
+ return (inject(RUNTIME_OPTIONS_KEY) ?? {
6
+ include: [],
7
+ exclude: [],
8
+ renderFrame: 'eager',
9
+ });
6
10
  }
@@ -1,22 +1,32 @@
1
1
  import { recorder } from '@describe-me/core';
2
2
  /**
3
- * Nesting depth of interactions currently in flight. `userEvent.click(el)`
4
- * delegates to `locator.click()`, and `copy()` delegates to `keyboard()`, so
5
- * without this guard one gesture would produce two frames.
3
+ * Nesting depth of the interactions in flight, and the test they started in.
4
+ * `userEvent.click(el)` delegates to `locator.click()`, and `copy()` delegates
5
+ * to `keyboard()`, so without this guard one gesture would produce two
6
+ * frames. Keyed to the recorder's generation, so an interaction that an ended
7
+ * test left running cannot swallow the next test's frame.
6
8
  */
7
- let depth = 0;
9
+ const nesting = { generation: -1, depth: 0 };
8
10
  /** Run one user interaction and record a frame for it, but only for the outermost call. */
9
11
  export async function recordAction(label, run) {
10
- depth++;
12
+ const generation = recorder.generation;
13
+ if (generation !== nesting.generation) {
14
+ nesting.generation = generation;
15
+ nesting.depth = 0;
16
+ }
17
+ recorder.beforeInteraction();
18
+ nesting.depth++;
11
19
  let result;
12
20
  try {
13
21
  result = await run();
14
22
  }
15
23
  finally {
16
- depth--;
24
+ if (generation === nesting.generation) {
25
+ nesting.depth--;
26
+ }
17
27
  }
18
- if (depth === 0) {
19
- await recorder.capture('action', label);
28
+ if (generation === nesting.generation && nesting.depth === 0) {
29
+ await recorder.capture('action', label, undefined, { generation });
20
30
  }
21
31
  return result;
22
32
  }
@@ -1,9 +1,10 @@
1
1
  import type { RuntimeOptions } from './runtime-options.js';
2
2
  /**
3
3
  * The per-test lifecycle shared by every environment: begin a recording, take
4
- * the closing frame, hand the frames to the reporter via task.meta, and only
5
- * then let adapters unmount. Teardown runs for unrecorded tests too, because
6
- * Testing Library's auto-cleanup is off in jsdom. Concurrent tests would share
7
- * the one recorder, so they are not recorded.
4
+ * a deferred render frame or else the closing frame, hand the frames to the
5
+ * reporter via task.meta, and only then let adapters unmount. Teardown runs
6
+ * for unrecorded tests too, because Testing Library's auto-cleanup is off in
7
+ * jsdom. Concurrent tests would share the one recorder, so they are not
8
+ * recorded.
8
9
  */
9
10
  export declare function registerRecordingHooks(options: RuntimeOptions): void;
@@ -3,14 +3,16 @@ import { META_KEY, recorder } from '@describe-me/core';
3
3
  import { fileFilter } from './file-filter.js';
4
4
  /**
5
5
  * The per-test lifecycle shared by every environment: begin a recording, take
6
- * the closing frame, hand the frames to the reporter via task.meta, and only
7
- * then let adapters unmount. Teardown runs for unrecorded tests too, because
8
- * Testing Library's auto-cleanup is off in jsdom. Concurrent tests would share
9
- * the one recorder, so they are not recorded.
6
+ * a deferred render frame or else the closing frame, hand the frames to the
7
+ * reporter via task.meta, and only then let adapters unmount. Teardown runs
8
+ * for unrecorded tests too, because Testing Library's auto-cleanup is off in
9
+ * jsdom. Concurrent tests would share the one recorder, so they are not
10
+ * recorded.
10
11
  */
11
12
  export function registerRecordingHooks(options) {
12
13
  const isRecordedFile = fileFilter(options);
13
14
  let warnedConcurrent = false;
15
+ recorder.configure({ renderFrame: options.renderFrame });
14
16
  beforeEach((context) => {
15
17
  if (context.task.concurrent) {
16
18
  if (!warnedConcurrent) {
@@ -26,7 +28,11 @@ export function registerRecordingHooks(options) {
26
28
  afterEach(async (context) => {
27
29
  try {
28
30
  if (recorder.isActive) {
29
- await recorder.capture('end', 'end of test');
31
+ const tookRender = await recorder.flush();
32
+ // A render frame taken this late already shows how the test ended.
33
+ if (!tookRender) {
34
+ await recorder.capture('end', 'end of test');
35
+ }
30
36
  const record = recorder.end();
31
37
  if (record.frames.length > 0) {
32
38
  ;
@@ -2,8 +2,9 @@
2
2
  * Travels from the plugin to the setup files through Vitest's `provide` /
3
3
  * `inject`. Keep it plain JSON: the browser receives it serialized, so a
4
4
  * RegExp would not survive. The test runtime imports this file, so it imports
5
- * nothing itself.
5
+ * only types.
6
6
  */
7
+ import type { RenderFrameMode } from '@describe-me/core/types';
7
8
  export interface CompiledGlob {
8
9
  source: string;
9
10
  flags: string;
@@ -13,6 +14,7 @@ export interface RuntimeOptions {
13
14
  include: CompiledGlob[];
14
15
  /** Wins over `include`. */
15
16
  exclude: CompiledGlob[];
17
+ renderFrame: RenderFrameMode;
16
18
  }
17
19
  export declare const RUNTIME_OPTIONS_KEY: "describe-me";
18
20
  declare module 'vitest' {
@@ -2,6 +2,6 @@
2
2
  * Travels from the plugin to the setup files through Vitest's `provide` /
3
3
  * `inject`. Keep it plain JSON: the browser receives it serialized, so a
4
4
  * RegExp would not survive. The test runtime imports this file, so it imports
5
- * nothing itself.
5
+ * only types.
6
6
  */
7
7
  export const RUNTIME_OPTIONS_KEY = 'describe-me';
package/dist/setup-dom.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * `setupFiles: ['@describe-me/vitest/setup-dom']`. Patches Testing Library's
4
4
  * userEvent, when it is installed, so every interaction records a frame, sets
5
5
  * React's act flag where Testing Library would, and registers the recording
6
- * hooks. Which test files are recorded (`include` / `exclude`) comes from the
7
- * plugin. Never imports `vitest/browser`, which only exists in browser mode.
6
+ * hooks. Which test files are recorded (`include` / `exclude`) and when the
7
+ * render frame is taken (`renderFrame`) come from the plugin. Never imports
8
+ * `vitest/browser`, which only exists in browser mode.
8
9
  */
9
10
  import { mirrorReactActEnvironment } from './mirror-react-act-environment.js';
10
11
  import { patchTestingLibraryUserEvent } from './patch-testing-library-user-event.js';
package/dist/setup.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * Vitest setup file for browser mode: `setupFiles: ['@describe-me/vitest/setup']`.
3
3
  * Patches Vitest's locators and userEvent so every interaction records a
4
4
  * frame, and registers the recording hooks. Which test files are recorded
5
- * (`include` / `exclude`) comes from the plugin. For jsdom use `./setup-dom`.
5
+ * (`include` / `exclude`) and when the render frame is taken (`renderFrame`)
6
+ * come from the plugin. For jsdom use `./setup-dom`.
6
7
  */
7
8
  import { patchLocators } from './patch-locators.js';
8
9
  import { patchUserEvent } from './patch-user-event.js';
@@ -0,0 +1,6 @@
1
+ import type { RenderFrameMode } from '@describe-me/core/types';
2
+ /**
3
+ * The plugin's `renderFrame`, checked before any test runs. Selector syntax is
4
+ * left to `recorder.configure()` in the test runtime, which has a DOM to parse it.
5
+ */
6
+ export declare function validateRenderFrame(value: unknown): RenderFrameMode;
@@ -0,0 +1,26 @@
1
+ import { inspect } from 'node:util';
2
+ const ACCEPTED = "'eager', 'lazy' or { pending: string, timeout?: number } with a non-empty selector and a timeout above 0 ms";
3
+ function isPendingMode(value) {
4
+ if (typeof value !== 'object' || value === null) {
5
+ return false;
6
+ }
7
+ const { pending, timeout, ...rest } = value;
8
+ if (Object.keys(rest).length > 0 || typeof pending !== 'string' || pending === '') {
9
+ return false;
10
+ }
11
+ return (timeout === undefined ||
12
+ (typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0));
13
+ }
14
+ /**
15
+ * The plugin's `renderFrame`, checked before any test runs. Selector syntax is
16
+ * left to `recorder.configure()` in the test runtime, which has a DOM to parse it.
17
+ */
18
+ export function validateRenderFrame(value) {
19
+ if (value === undefined) {
20
+ return 'eager';
21
+ }
22
+ if (value === 'eager' || value === 'lazy' || isPendingMode(value)) {
23
+ return value;
24
+ }
25
+ throw new Error(`describe-me: renderFrame expects ${ACCEPTED}, got ${inspect(value)}`);
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@describe-me/vitest",
3
- "version": "0.5.0-next.1",
3
+ "version": "0.5.0-next.2",
4
4
  "description": "Vitest integration for describe-me: plugin, setup file and reporter",
5
5
  "keywords": [
6
6
  "vitest",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "dependencies": {
61
61
  "picomatch": "^4.0.3",
62
- "@describe-me/core": "^0.5.0-next.1"
62
+ "@describe-me/core": "^0.5.0-next.2"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "vitest": "^4.0.0 || ^5.0.0",