@describe-me/core 0.5.0 → 0.5.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
@@ -14,6 +14,9 @@ Most people never install this package directly — `@describe-me/react` and
14
14
  pnpm add -D @describe-me/core
15
15
  ```
16
16
 
17
+ All describe-me packages must be on the same version
18
+ ([Add it to your project](https://github.com/grzehub/describe-me#add-it-to-your-project)).
19
+
17
20
  ## Usage
18
21
 
19
22
  `step()` names a phase of a test. The DOM is captured after the body resolves,
@@ -1,8 +1,9 @@
1
1
  import { type Manifest, type ManifestDiagnostics } from './types.js';
2
2
  /**
3
3
  * List what the manifest could not document: anonymous components, named ones
4
- * without props, assets that will not load, font families that nothing loads
5
- * and hosts that frames load stylesheets from. The reporter prints it after a
6
- * run and the viewer shows it in the header, so both agree on what is missing.
4
+ * without props, assets that will not load, font families that nothing loads,
5
+ * hosts that frames load stylesheets from and problems in the project's setup.
6
+ * The reporter prints it after a run and the viewer shows it in the header, so
7
+ * both agree on what is missing.
7
8
  */
8
9
  export declare function manifestDiagnostics(manifest: Manifest): ManifestDiagnostics;
@@ -5,9 +5,10 @@ function isHostElement(name) {
5
5
  }
6
6
  /**
7
7
  * List what the manifest could not document: anonymous components, named ones
8
- * without props, assets that will not load, font families that nothing loads
9
- * and hosts that frames load stylesheets from. The reporter prints it after a
10
- * run and the viewer shows it in the header, so both agree on what is missing.
8
+ * without props, assets that will not load, font families that nothing loads,
9
+ * hosts that frames load stylesheets from and problems in the project's setup.
10
+ * The reporter prints it after a run and the viewer shows it in the header, so
11
+ * both agree on what is missing.
11
12
  */
12
13
  export function manifestDiagnostics(manifest) {
13
14
  const anonymous = [];
@@ -33,5 +34,6 @@ export function manifestDiagnostics(manifest) {
33
34
  assetsMissing: manifest.assetsMissing ?? [],
34
35
  fontsMissing: manifest.fontsMissing ?? [],
35
36
  remoteStylesheets: manifest.remoteStylesheets ?? [],
37
+ setupWarnings: manifest.setupWarnings ?? [],
36
38
  };
37
39
  }
@@ -7,12 +7,15 @@ type Teardown = () => void;
7
7
  * `beforeInteraction()`, `setComponent()` and `onTeardown()`.
8
8
  */
9
9
  declare class Recorder {
10
+ /** Which contract this recorder follows, so another copy of core can refuse it. */
11
+ readonly protocol = 2;
10
12
  private slots;
11
13
  private currentGeneration;
12
14
  private inFlight;
13
15
  private deferred;
14
16
  private pending;
15
17
  private renderFrame;
18
+ private aroundWait;
16
19
  private component?;
17
20
  private startedAt;
18
21
  private active;
@@ -26,9 +29,10 @@ declare class Recorder {
26
29
  */
27
30
  get generation(): number;
28
31
  /**
29
- * Choose when the render frame is taken, from the next render capture on.
30
- * The setup files call it with the plugin's options. An invalid `pending`
31
- * selector throws.
32
+ * Choose when the render frame is taken, from the next render capture on,
33
+ * and what runs around each wait. The setup files call it with the plugin's
34
+ * options. A `pending` selector the DOM rejects falls back to `'lazy'` with
35
+ * a warning.
32
36
  */
33
37
  configure(options: RecorderOptions): void;
34
38
  /**
@@ -62,6 +66,7 @@ declare class Recorder {
62
66
  */
63
67
  private defer;
64
68
  private clearDeferred;
69
+ private waitToSettle;
65
70
  private settleThenFill;
66
71
  private fill;
67
72
  }
package/dist/recorder.js CHANGED
@@ -5,16 +5,18 @@ import { realTimers } from './real-timers.js';
5
5
  import { serializeDocument } from './serialize-document.js';
6
6
  import { settle } from './settle.js';
7
7
  import { ANONYMOUS_COMPONENT, } from './types.js';
8
- /** Fails in the setup file rather than in the first test that renders. */
9
- function assertValidSelector(selector) {
10
- if (typeof document === 'undefined') {
11
- return;
12
- }
8
+ /**
9
+ * Bump it whenever the contract between the recorder and the adapters or setup
10
+ * files changes: a method or property added, removed or changed in meaning.
11
+ */
12
+ const RECORDER_PROTOCOL = 2;
13
+ function isValidSelector(selector) {
13
14
  try {
14
15
  document.querySelector(selector);
16
+ return true;
15
17
  }
16
18
  catch {
17
- throw new Error(`describe-me: renderFrame.pending is not a valid CSS selector: "${selector}"`);
19
+ return false;
18
20
  }
19
21
  }
20
22
  /**
@@ -24,6 +26,8 @@ function assertValidSelector(selector) {
24
26
  * `beforeInteraction()`, `setComponent()` and `onTeardown()`.
25
27
  */
26
28
  class Recorder {
29
+ /** Which contract this recorder follows, so another copy of core can refuse it. */
30
+ protocol = RECORDER_PROTOCOL;
27
31
  // One per capture, in call order, so a frame sits where it was asked for.
28
32
  slots = [];
29
33
  currentGeneration = 0;
@@ -32,6 +36,7 @@ class Recorder {
32
36
  deferred = null;
33
37
  pending = null;
34
38
  renderFrame = 'eager';
39
+ aroundWait = (wait) => wait();
35
40
  component;
36
41
  startedAt = 0;
37
42
  active = false;
@@ -59,14 +64,22 @@ class Recorder {
59
64
  return this.currentGeneration;
60
65
  }
61
66
  /**
62
- * Choose when the render frame is taken, from the next render capture on.
63
- * The setup files call it with the plugin's options. An invalid `pending`
64
- * selector throws.
67
+ * Choose when the render frame is taken, from the next render capture on,
68
+ * and what runs around each wait. The setup files call it with the plugin's
69
+ * options. A `pending` selector the DOM rejects falls back to `'lazy'` with
70
+ * a warning.
65
71
  */
66
72
  configure(options) {
67
73
  const renderFrame = options.renderFrame ?? 'eager';
68
- if (typeof renderFrame === 'object') {
69
- assertValidSelector(renderFrame.pending);
74
+ if (options.aroundWait !== undefined) {
75
+ this.aroundWait = options.aroundWait;
76
+ }
77
+ if (typeof renderFrame === 'object' &&
78
+ typeof document !== 'undefined' &&
79
+ !isValidSelector(renderFrame.pending)) {
80
+ console.warn(`describe-me: renderFrame.pending is not a valid CSS selector: "${renderFrame.pending}". Render frames fall back to 'lazy'.`);
81
+ this.renderFrame = 'lazy';
82
+ return;
70
83
  }
71
84
  this.renderFrame = renderFrame;
72
85
  }
@@ -149,7 +162,7 @@ class Recorder {
149
162
  return false;
150
163
  }
151
164
  const generation = this.currentGeneration;
152
- await settle();
165
+ await this.waitToSettle();
153
166
  // The test ended, or an interaction took the frame, while this settled.
154
167
  if (generation !== this.currentGeneration || this.deferred !== slot) {
155
168
  return false;
@@ -207,8 +220,13 @@ class Recorder {
207
220
  this.pending?.dispose();
208
221
  this.pending = null;
209
222
  }
223
+ // Only the wait runs inside `aroundWait`, so whatever the wrapper changes is
224
+ // undone before the snapshot and the teardown.
225
+ waitToSettle() {
226
+ return this.aroundWait(settle);
227
+ }
210
228
  async settleThenFill(slot, generation) {
211
- await settle();
229
+ await this.waitToSettle();
212
230
  if (generation === this.currentGeneration) {
213
231
  this.fill(slot);
214
232
  }
@@ -227,6 +245,21 @@ class Recorder {
227
245
  }
228
246
  }
229
247
  const RECORDER_KEY = Symbol.for('describe-me.recorder');
248
+ function protocolOf(existing) {
249
+ if (typeof existing !== 'object' || existing === null) {
250
+ return undefined;
251
+ }
252
+ const candidate = existing;
253
+ if (typeof candidate.protocol === 'number') {
254
+ return candidate.protocol;
255
+ }
256
+ // 0.5.0 did not stamp its recorder. It is the first version with `flush()`,
257
+ // and its contract is protocol 1.
258
+ if (typeof candidate.flush === 'function') {
259
+ return 1;
260
+ }
261
+ return undefined;
262
+ }
230
263
  /**
231
264
  * The adapter and the setup file may receive two copies of this module: Vite
232
265
  * pre-bundles the adapter together with its own copy of core, while a linked
@@ -237,7 +270,16 @@ const RECORDER_KEY = Symbol.for('describe-me.recorder');
237
270
  */
238
271
  function sharedRecorder() {
239
272
  const scope = globalThis;
240
- scope[RECORDER_KEY] ??= new Recorder();
241
- return scope[RECORDER_KEY];
273
+ const existing = scope[RECORDER_KEY];
274
+ if (existing === undefined) {
275
+ const created = new Recorder();
276
+ scope[RECORDER_KEY] = created;
277
+ return created;
278
+ }
279
+ const found = protocolOf(existing);
280
+ if (found !== RECORDER_PROTOCOL) {
281
+ throw new Error(`describe-me: all describe-me packages must be on the same version. Another copy of @describe-me/core created the test recorder (protocol ${found ?? 'none'}, expected ${RECORDER_PROTOCOL}). Install @describe-me/core, @describe-me/react, @describe-me/vitest and describe-me at one version.`);
282
+ }
283
+ return existing;
242
284
  }
243
285
  export const recorder = sharedRecorder();
package/dist/types.d.ts CHANGED
@@ -49,10 +49,18 @@ export type RenderFrameMode = 'eager' | 'lazy' | {
49
49
  pending: string;
50
50
  timeout?: number;
51
51
  };
52
+ /** Runs one wait of the recorder. It must call `wait` once and resolve after it. */
53
+ export type AroundWait = (wait: () => Promise<void>) => Promise<void>;
52
54
  /** Options for `recorder.configure()`. */
53
55
  export interface RecorderOptions {
54
56
  /** When the render frame is taken. Default: `'eager'`. */
55
57
  renderFrame?: RenderFrameMode;
58
+ /**
59
+ * Runs each wait of the recorder. It is kept when omitted, so a later
60
+ * `configure({ renderFrame })` keeps the setup file's wrapper. By default
61
+ * it runs `wait` as is.
62
+ */
63
+ aroundWait?: AroundWait;
56
64
  }
57
65
  /** Options for `recorder.capture()`. */
58
66
  export interface CaptureOptions {
@@ -157,8 +165,17 @@ export interface ComponentDoc {
157
165
  file: string;
158
166
  props: PropDoc[];
159
167
  }
168
+ /** The package that wrote a manifest. */
169
+ export interface ManifestGenerator {
170
+ /** The package name, e.g. `@describe-me/vitest`. */
171
+ name: string;
172
+ /** Its version, e.g. `0.5.1`. */
173
+ version: string;
174
+ }
160
175
  export interface Manifest {
161
176
  version: 1;
177
+ /** The package and version that wrote the manifest. Absent in manifests written before 0.5.1. */
178
+ generator?: ManifestGenerator;
162
179
  generatedAt: string;
163
180
  root: string;
164
181
  modules: ManifestModule[];
@@ -186,6 +203,12 @@ export interface Manifest {
186
203
  * manifests written before 0.5, which counts as empty.
187
204
  */
188
205
  remoteStylesheets?: RemoteStylesheet[];
206
+ /**
207
+ * Problems the plugin found in the project's setup, one sentence each.
208
+ * Absent when there are none and in manifests written before 0.5.1, which
209
+ * counts as empty.
210
+ */
211
+ setupWarnings?: string[];
189
212
  }
190
213
  /** A font family that the captured CSS uses but no `@font-face`, font stylesheet or preview head loads. */
191
214
  export interface FontMissing {
@@ -223,4 +246,6 @@ export interface ManifestDiagnostics {
223
246
  fontsMissing: FontMissing[];
224
247
  /** Hosts that frames load stylesheets from. */
225
248
  remoteStylesheets: RemoteStylesheet[];
249
+ /** Problems in the project's setup that can make tests fail. */
250
+ setupWarnings: string[];
226
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@describe-me/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Recorder and manifest types for describe-me: living component docs from your tests",
5
5
  "keywords": [
6
6
  "vitest",