From db68db8e4a9e3324cc104d306c972281ef2cff26 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Sat, 15 Aug 2026 00:41:31 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Introduce=20a=20plugin=20API=20(#72?= =?UTF-8?q?16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes #6289 Related to #6190 ## Checklist — _Don't delete this checklist and make sure you do the following before opening the PR_ - [ ] I have a full understanding of every line in this PR — whether the code was hand-written, AI-generated, copied from external sources or produced by any other tool - [ ] I flagged the impact of my change (minor / patch / major) either by running `pnpm run bump` or by following the instructions from the changeset bot - [ ] I kept this PR focused on a single concern and did not bundle unrelated changes - [ ] I followed the [gitmoji](https://gitmoji.dev/) specification for the name of the PR, including the package scope (e.g. `🐛(vitest) Something...`) when the change targets a package other than `fast-check` - [ ] I added relevant tests and they would have failed without my PR (when applicable) --- .changeset/breezy-ears-carry.md | 5 + .changeset/tall-moons-repeat.md | 6 + .../fast-check/src/check/plugin/Plugin.ts | 59 ++++++ .../fast-check/src/check/runner/Runner.ts | 82 ++++++-- .../runner/configuration/GlobalParameters.ts | 5 +- .../check/runner/configuration/Parameters.ts | 10 + .../configuration/QualifiedParameters.ts | 4 + packages/fast-check/src/fast-check-default.ts | 3 + packages/fast-check/test/e2e/Plugins.spec.ts | 197 ++++++++++++++++++ .../NoRegressionStack.spec.ts.snap | 8 +- .../configuration/QualifiedParameters.spec.ts | 2 + packages/jest/src/internals/TestBuilder.ts | 2 + packages/vitest/src/internals/TestBuilder.ts | 2 + website/docs/advanced/plugins.md | 27 +++ 14 files changed, 391 insertions(+), 21 deletions(-) create mode 100644 .changeset/breezy-ears-carry.md create mode 100644 .changeset/tall-moons-repeat.md create mode 100644 packages/fast-check/src/check/plugin/Plugin.ts create mode 100644 packages/fast-check/test/e2e/Plugins.spec.ts create mode 100644 website/docs/advanced/plugins.md diff --git a/.changeset/breezy-ears-carry.md b/.changeset/breezy-ears-carry.md new file mode 100644 index 00000000..e4a38c61 --- /dev/null +++ b/.changeset/breezy-ears-carry.md @@ -0,0 +1,5 @@ +--- +"fast-check": minor +--- + +Plugin api coming diff --git a/.changeset/tall-moons-repeat.md b/.changeset/tall-moons-repeat.md new file mode 100644 index 00000000..5571cdd9 --- /dev/null +++ b/.changeset/tall-moons-repeat.md @@ -0,0 +1,6 @@ +--- +"@fast-check/jest": patch +"@fast-check/vitest": patch +--- + +🔧 Force `plugins` to `undefined` on record-based `.prop` diff --git a/packages/fast-check/src/check/plugin/Plugin.ts b/packages/fast-check/src/check/plugin/Plugin.ts new file mode 100644 index 00000000..08d1ea06 --- /dev/null +++ b/packages/fast-check/src/check/plugin/Plugin.ts @@ -0,0 +1,59 @@ +import type { IRawProperty } from '../property/IRawProperty.js'; +import type { RunDetails } from '../runner/reporter/RunDetails.js'; + +/** + * Runtime part of a plugin. + * + * The runtime part is made of the hooks called by the runner. + * Hooks will be called when relevant for the runner. + * + * All the hooks are optional. + * + * @remarks Since 4.10.0 + * @public + */ +export type PluginInstance = { + /** + * Whether or not the plugin can only be attached to asynchronous properties. + * + * @defaultValue false + * @remarks Since 4.10.0 + */ + asyncOnly?: IsAsync extends false ? false : boolean; + /** + * Enrich the execution of the predicate linked to the property with extra behaviors. + * Called once per execution of the predicate. + * + * WARNING: `nestedRun` never throws and neither should the function returned by `decorateRun`. + * WARNING: Except in `asyncOnly: true` mode, if run returns synchronously, the decorated function must too. + * + * @remarks Since 4.10.0 + */ + decorateRun?: (nestedRun: IRawProperty['run']) => IRawProperty['run']; + /** + * Called once at the end of the full property assessment. + * Gets called with the result of the execution. + * + * WARNING: `afterAll` must not throw. Throwing would shadow the output of the property execution. + * WARNING: Make sure to always return synchronously except if you set `asyncOnly: true`. + * + * @remarks Since 4.10.0 + */ + afterAll?: (runDetails: RunDetails) => IsAsync extends true ? Promise | void : void; +}; + +/** + * Builder instantiating a plugin. + * Each property will instantiate its own plugin when starting to be assessed via {@link check} or {@link assert}. + * + * NOTE: The function gets called with a parameter shared across all builders. + * The variable can be leveraged to exchange insights with other builders. As such it is writable and can be mutated via the builder. + * We recommend using symbol keys when adding entries to the variable to reduce the risk of collision with other unrelated plugins. + * + * @remarks Since 4.10.0 + * @public + */ +export type Plugin = (crossPluginContext: { [K in any]?: unknown }) => PluginInstance< + Ts, + IsAsync +>; diff --git a/packages/fast-check/src/check/runner/Runner.ts b/packages/fast-check/src/check/runner/Runner.ts index c8160439..15bf5764 100644 --- a/packages/fast-check/src/check/runner/Runner.ts +++ b/packages/fast-check/src/check/runner/Runner.ts @@ -17,10 +17,11 @@ import { asyncReportRunDetails, reportRunDetails } from './utils/RunDetailsForma import type { IAsyncProperty } from '../property/AsyncProperty.js'; import type { IProperty } from '../property/Property.js'; import type { Value } from '../arbitrary/definition/Value.js'; +import type { Plugin, PluginInstance } from '../plugin/Plugin.js'; /** @internal */ function runIt( - property: IRawProperty, + run: IRawProperty['run'], shrink: (value: Value) => IterableIterator>, sourceValues: SourceValuesIterator>, verbose: VerbosityLevel, @@ -28,17 +29,22 @@ function runIt( ): RunExecution { const runner = new RunnerIterator(sourceValues, shrink, verbose, interruptedAsFailure); for (const v of runner) { - (property.runBeforeEach as () => void)(); - const out = property.run(v) as PreconditionFailure | PropertyFailure | null; - (property.runAfterEach as () => void)(); + const out = run(v) as PreconditionFailure | PropertyFailure | null; runner.handleResult(out); } return runner.runExecution; } +function propertyExecution(property: IRawProperty, v: Ts) { + (property.runBeforeEach as () => void)(); + const out = property.run(v) as PreconditionFailure | PropertyFailure | null; + (property.runAfterEach as () => void)(); + return out; +} + /** @internal */ async function asyncRunIt( - property: IRawProperty, + run: IRawProperty['run'], shrink: (value: Value) => IterableIterator>, sourceValues: SourceValuesIterator>, verbose: VerbosityLevel, @@ -46,14 +52,19 @@ async function asyncRunIt( ): Promise> { const runner = new RunnerIterator(sourceValues, shrink, verbose, interruptedAsFailure); for (const v of runner) { - await property.runBeforeEach(); - const out = await property.run(v); - await property.runAfterEach(); + const out = await run(v); runner.handleResult(out); } return runner.runExecution; } +async function asyncPropertyExecution(property: IRawProperty, v: Ts) { + await property.runBeforeEach(); + const out = await property.run(v); + await property.runAfterEach(); + return out; +} + /** * Run the property, do not throw contrary to {@link assert} * @@ -114,6 +125,25 @@ function check(rawProperty: IRawProperty, params?: Parameters): unkn throw new Error('Invalid parameters encountered, only asyncProperty can be used when asyncReporter specified'); const property = decorateProperty(rawProperty, qParams); + const pluginSharedSessionContext: { [K in any]?: unknown } = {}; + const plugins: Plugin[] = qParams.plugins; + const pluginAfterAllCallbacks: Required>['afterAll'][] = []; + let run: typeof property.run = property.isAsync() + ? async (v) => asyncPropertyExecution(property, v) + : (v) => propertyExecution(property, v); + for (let index = 0; index !== plugins.length; ++index) { + const pluginInstance = plugins[index](pluginSharedSessionContext); + if (pluginInstance.asyncOnly && !property.isAsync()) { + throw new Error('Cannot execute an asynchronous plugin on a synchronous property'); + } + if (pluginInstance.decorateRun !== undefined) { + run = pluginInstance.decorateRun(run); + } + if (pluginInstance.afterAll !== undefined) { + pluginAfterAllCallbacks.push(pluginInstance.afterAll.bind(pluginInstance)); + } + } + const maxInitialIterations = qParams.path.length === 0 || qParams.path.indexOf(':') === -1 ? qParams.numRuns : -1; const maxSkips = qParams.numRuns * qParams.maxSkipsPerRun; const shrink: typeof property.shrink = (...args) => property.shrink(...args); @@ -123,16 +153,32 @@ function check(rawProperty: IRawProperty, params?: Parameters): unkn : pathWalk(qParams.path, stream(lazyToss(property, qParams.seed, qParams.randomType, qParams.examples)), shrink); const sourceValues = new SourceValuesIterator(initialValues, maxInitialIterations, maxSkips); const finalShrink = !qParams.endOnFailure ? shrink : Stream.nil; - return property.isAsync() - ? asyncRunIt(property, finalShrink, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).then((e) => - e.toRunDetails(qParams.seed, qParams.path, maxSkips, qParams), - ) - : runIt(property, finalShrink, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).toRunDetails( - qParams.seed, - qParams.path, - maxSkips, - qParams, - ); + if (property.isAsync()) { + const out = asyncRunIt(run, finalShrink, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).then((e) => + e.toRunDetails(qParams.seed, qParams.path, maxSkips, qParams), + ); + if (pluginAfterAllCallbacks.length === 0) { + return out; + } + return out.then((details) => { + let queued = pluginAfterAllCallbacks[0](details); + for (let index = 1; index < pluginAfterAllCallbacks.length; ++index) { + const afterAll = pluginAfterAllCallbacks[index]; + queued = queued === undefined ? afterAll(details) : queued.then(() => afterAll(details)); + } + return queued === undefined ? details : queued.then(() => details); + }); + } + const out = runIt(run, finalShrink, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).toRunDetails( + qParams.seed, + qParams.path, + maxSkips, + qParams, + ); + for (let index = 0; index !== pluginAfterAllCallbacks.length; ++index) { + (pluginAfterAllCallbacks[index] as () => void)(); + } + return out; } /** diff --git a/packages/fast-check/src/check/runner/configuration/GlobalParameters.ts b/packages/fast-check/src/check/runner/configuration/GlobalParameters.ts index d07c0a77..1858644a 100644 --- a/packages/fast-check/src/check/runner/configuration/GlobalParameters.ts +++ b/packages/fast-check/src/check/runner/configuration/GlobalParameters.ts @@ -22,7 +22,10 @@ export type GlobalAsyncPropertyHookFunction = (() => Promise) | (() => * @remarks Since 1.18.0 * @public */ -export type GlobalParameters = Pick, Exclude, 'path' | 'examples'>> & { +export type GlobalParameters = Pick< + Parameters, + Exclude, 'path' | 'examples' | 'plugins'> +> & { /** * Specify a function that will be called before each execution of a property. * It behaves as-if you manually called `beforeEach` method on all the properties you execute with fast-check. diff --git a/packages/fast-check/src/check/runner/configuration/Parameters.ts b/packages/fast-check/src/check/runner/configuration/Parameters.ts index d9a32c62..88e78e81 100644 --- a/packages/fast-check/src/check/runner/configuration/Parameters.ts +++ b/packages/fast-check/src/check/runner/configuration/Parameters.ts @@ -2,6 +2,7 @@ import type { RandomType } from './RandomType.js'; import type { VerbosityLevel } from './VerbosityLevel.js'; import type { RunDetails } from '../reporter/RunDetails.js'; import type { RandomGenerator } from '../../../random/generator/RandomGenerator.js'; +import type { Plugin } from '../../plugin/Plugin.js'; /** * Customization of the parameters used to run the properties @@ -203,4 +204,13 @@ export interface Parameters { * as part of the message and not as a cause. */ includeErrorInReport?: boolean; + /** + * Set of plugins extending the way the property gets executed by the runner + * + * Each plugin is instantiated once per run. + * They can be leveraged to control and enrich the execution flow of each predicate. + * + * @remarks Since 4.10.0 + */ + plugins?: Plugin[]; } diff --git a/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts b/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts index 532f51f6..54069cf9 100644 --- a/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts +++ b/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts @@ -8,6 +8,7 @@ import { xoroshiro128plus } from 'pure-rand/generator/xoroshiro128plus'; import { adaptRandomGenerator } from '../../../random/generator/RandomGenerator.js'; import type { RandomGenerator, RandomGeneratorInternal } from '../../../random/generator/RandomGenerator.js'; +import type { Plugin } from '../../plugin/Plugin.js'; const safeDateNow = Date.now; const safeMathMin = Math.min; @@ -43,6 +44,7 @@ export class QualifiedParameters { reporter: ((runDetails: RunDetails) => void) | undefined; asyncReporter: ((runDetails: RunDetails) => Promise) | undefined; includeErrorInReport: boolean; + plugins: Plugin[]; constructor(op?: Parameters) { const p = op || {}; @@ -71,6 +73,7 @@ export class QualifiedParameters { this.reporter = p.reporter; this.asyncReporter = p.asyncReporter; this.includeErrorInReport = p.includeErrorInReport === true; + this.plugins = p.plugins !== undefined ? p.plugins : []; } toParameters(): Parameters { @@ -94,6 +97,7 @@ export class QualifiedParameters { reporter: this.reporter, asyncReporter: this.asyncReporter, includeErrorInReport: this.includeErrorInReport, + plugins: this.plugins, }; return parameters; } diff --git a/packages/fast-check/src/fast-check-default.ts b/packages/fast-check/src/fast-check-default.ts index 4d7bbaf6..93a77d74 100644 --- a/packages/fast-check/src/fast-check-default.ts +++ b/packages/fast-check/src/fast-check-default.ts @@ -208,6 +208,7 @@ import { noShrink } from './arbitrary/noShrink.js'; import { noBias } from './arbitrary/noBias.js'; import { limitShrink } from './arbitrary/limitShrink.js'; import type { RandomGenerator } from './random/generator/RandomGenerator.js'; +import type { Plugin, PluginInstance } from './check/plugin/Plugin.js'; // Explicit cast into string to avoid to have __type: "process.env.__PACKAGE_TYPE__" /** @@ -237,6 +238,8 @@ const __commitHash = process.env.__COMMIT_HASH__ as string; // combination of others // complex combinations export type { + Plugin, + PluginInstance, IRawProperty, IProperty, IPropertyWithHooks, diff --git a/packages/fast-check/test/e2e/Plugins.spec.ts b/packages/fast-check/test/e2e/Plugins.spec.ts new file mode 100644 index 00000000..dc5ffa23 --- /dev/null +++ b/packages/fast-check/test/e2e/Plugins.spec.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; +import * as fc from '../../src/fast-check.js'; +import { seed } from './seed.js'; + +describe(`Plugins (seed: ${seed})`, () => { + it('should wait and queue afterAll', async () => { + // Arrange + const probes: string[] = []; + const buildPlugin = (pluginName: string): fc.Plugin<[number], true> => { + return () => { + probes.push(`${pluginName} instantiated`); + return { + afterAll: async () => { + probes.push(`${pluginName}::afterAll started`); + await Promise.resolve(`${pluginName}1`); + await Promise.resolve(`${pluginName}2`); + await Promise.resolve(`${pluginName}3`); + probes.push(`${pluginName}::afterAll done`); + }, + }; + }; + }; + + // Act + probes.push('assert started'); + await fc.assert( + fc.asyncProperty(fc.integer(), async (_x) => true), + { plugins: [buildPlugin('a'), buildPlugin('b')] }, + ); + probes.push('assert done'); + + // Assert + expect(probes).toEqual([ + 'assert started', + 'a instantiated', + 'b instantiated', + 'a::afterAll started', + 'a::afterAll done', + 'b::afterAll started', + 'b::afterAll done', + 'assert done', + ]); + }); + + it('should stack decorateRun with the first plugin being the closest to the predicate', () => { + // Arrange + const probes: string[] = []; + const buildPlugin = (pluginName: string): fc.Plugin<[number], false> => { + return () => { + probes.push(`${pluginName} instantiated`); + return { + decorateRun: (nestedRun) => (value) => { + probes.push(`${pluginName}::run started`); + try { + return nestedRun(value); + } finally { + probes.push(`${pluginName}::run done`); + } + }, + }; + }; + }; + + // Act + probes.push('assert started'); + fc.assert( + fc.property(fc.integer(), (_x) => { + probes.push('predicate called'); + return true; + }), + { plugins: [buildPlugin('a'), buildPlugin('b')], numRuns: 2 }, + ); + probes.push('assert done'); + + // Assert + expect(probes).toEqual([ + 'assert started', + 'a instantiated', + 'b instantiated', + 'b::run started', + 'a::run started', + 'predicate called', + 'a::run done', + 'b::run done', + 'b::run started', + 'a::run started', + 'predicate called', + 'a::run done', + 'b::run done', + 'assert done', + ]); + }); + + it('should await decorateRun of asynchronous plugins', async () => { + // Arrange + const probes: string[] = []; + const buildPlugin = (pluginName: string): fc.Plugin<[number], true> => { + return () => { + return { + asyncOnly: true, + decorateRun: (nestedRun) => async (value) => { + probes.push(`${pluginName}::run started`); + await Promise.resolve(); + const out = await nestedRun(value); + await Promise.resolve(); + probes.push(`${pluginName}::run done`); + return out; + }, + }; + }; + }; + + // Act + await fc.assert( + fc.asyncProperty(fc.integer(), async (_x) => { + probes.push('predicate called'); + return true; + }), + { plugins: [buildPlugin('a'), buildPlugin('b')], numRuns: 2 }, + ); + + // Assert + expect(probes).toEqual([ + 'b::run started', + 'a::run started', + 'predicate called', + 'a::run done', + 'b::run done', + 'b::run started', + 'a::run started', + 'predicate called', + 'a::run done', + 'b::run done', + ]); + }); + + it('should support mixes of sync and async afterAll', async () => { + // Arrange + const probes: string[] = []; + const buildPlugin = (pluginName: string, isAsync: boolean): fc.Plugin<[number], true> => { + return () => { + probes.push(`${pluginName} instantiated`); + return { + afterAll: isAsync + ? async () => { + probes.push(`${pluginName}::afterAll started`); + await Promise.resolve(`${pluginName}1`); + await Promise.resolve(`${pluginName}2`); + await Promise.resolve(`${pluginName}3`); + probes.push(`${pluginName}::afterAll done`); + } + : () => { + probes.push(`${pluginName}::afterAll started`); + probes.push(`${pluginName}::afterAll done`); + }, + }; + }; + }; + + // Act + probes.push('assert started'); + await fc.assert( + fc.asyncProperty(fc.integer(), async (_x) => true), + { + plugins: [ + buildPlugin('a', true), + buildPlugin('b', true), + buildPlugin('c', false), + buildPlugin('d', false), + buildPlugin('e', true), + ], + }, + ); + probes.push('assert done'); + + // Assert + expect(probes).toEqual([ + 'assert started', + 'a instantiated', + 'b instantiated', + 'c instantiated', + 'd instantiated', + 'e instantiated', + 'a::afterAll started', + 'a::afterAll done', + 'b::afterAll started', + 'b::afterAll done', + 'c::afterAll started', + 'c::afterAll done', + 'd::afterAll started', + 'd::afterAll done', + 'e::afterAll started', + 'e::afterAll done', + 'assert done', + ]); + }); +}); diff --git a/packages/fast-check/test/e2e/__snapshots__/NoRegressionStack.spec.ts.snap b/packages/fast-check/test/e2e/__snapshots__/NoRegressionStack.spec.ts.snap index f3e1c004..4328e5c3 100644 --- a/packages/fast-check/test/e2e/__snapshots__/NoRegressionStack.spec.ts.snap +++ b/packages/fast-check/test/e2e/__snapshots__/NoRegressionStack.spec.ts.snap @@ -18,6 +18,8 @@ Got TypeError: v is not a function at packages/fast-check/test/e2e/NoRegressionStack.spec.ts:?:? at Property.predicate (packages/fast-check/src/check/property/Property.ts:?:?) at Property.run (packages/fast-check/src/check/property/Property.generic.ts:?:?) + at propertyExecution (packages/fast-check/src/check/runner/Runner.ts:?:?) + at run (packages/fast-check/src/check/runner/Runner.ts:?:?) at runIt (packages/fast-check/src/check/runner/Runner.ts:?:?) at check (packages/fast-check/src/check/runner/Runner.ts:?:?) at Module.assert (packages/fast-check/src/check/runner/Runner.ts:?:?) @@ -42,12 +44,12 @@ Counterexample: [0,1] Shrunk 32 time(s) Got error: Property failed by returning false at Property.run (packages/fast-check/src/check/property/Property.generic.ts:?:?) + at propertyExecution (packages/fast-check/src/check/runner/Runner.ts:?:?) + at run (packages/fast-check/src/check/runner/Runner.ts:?:?) at runIt (packages/fast-check/src/check/runner/Runner.ts:?:?) at check (packages/fast-check/src/check/runner/Runner.ts:?:?) at Module.assert (packages/fast-check/src/check/runner/Runner.ts:?:?) at packages/fast-check/test/e2e/__test-helpers__/StackSanitizer.ts:?:? - at Proxy. (node_modules/.pnpm/@vitest+expect@/node_modules/@vitest/expect/dist/index.js:?:?) - at Proxy.methodWrapper (node_modules/.pnpm/chai@/node_modules/chai/index.js:?:?) Hint: Enable verbose mode in order to have the list of all failing values encountered during the run] `; @@ -70,6 +72,8 @@ Got error: a must be >= b at packages/fast-check/test/e2e/NoRegressionStack.spec.ts:?:? at Property.predicate (packages/fast-check/src/check/property/Property.ts:?:?) at Property.run (packages/fast-check/src/check/property/Property.generic.ts:?:?) + at propertyExecution (packages/fast-check/src/check/runner/Runner.ts:?:?) + at run (packages/fast-check/src/check/runner/Runner.ts:?:?) at runIt (packages/fast-check/src/check/runner/Runner.ts:?:?) at check (packages/fast-check/src/check/runner/Runner.ts:?:?) at Module.assert (packages/fast-check/src/check/runner/Runner.ts:?:?) diff --git a/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts b/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts index c91b568e..10caeed5 100644 --- a/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts +++ b/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts @@ -8,6 +8,7 @@ import { xorshift128plus } from 'pure-rand/generator/xorshift128plus'; import { read } from '../../../../../src/check/runner/configuration/QualifiedParameters.js'; import type { RandomType } from '../../../../../src/check/runner/configuration/RandomType.js'; import { VerbosityLevel } from '../../../../../src/check/runner/configuration/VerbosityLevel.js'; +import type { Plugin } from '../../../../../src/check/plugin/Plugin.js'; const prand = { mersenne, congruential32, xorshift128plus, xoroshiro128plus }; const parametersArbitrary = fc.record( @@ -31,6 +32,7 @@ const parametersArbitrary = fc.record( reporter: fc.func(fc.constant(undefined)), asyncReporter: fc.func(fc.constant(Promise.resolve(undefined))), includeErrorInReport: fc.boolean(), + plugins: fc.constant([] satisfies Plugin[]), }, { requiredKeys: [] }, ); diff --git a/packages/jest/src/internals/TestBuilder.ts b/packages/jest/src/internals/TestBuilder.ts index 7dc95a22..038e5434 100644 --- a/packages/jest/src/internals/TestBuilder.ts +++ b/packages/jest/src/internals/TestBuilder.ts @@ -29,6 +29,7 @@ function adaptParametersForRecord( examples: parameters.examples !== undefined ? parameters.examples.map((example) => example[0]) : undefined, reporter: originalParamaters.reporter, asyncReporter: originalParamaters.asyncReporter, + plugins: undefined, }; } @@ -98,6 +99,7 @@ function buildTestProp( ? // oxlint-disable-next-line typescript/no-non-null-assertion (runDetails) => params.asyncReporter!(adaptRunDetailsForRecord(runDetails, params)) : undefined, + plugins: undefined, } : undefined; buildTestWithPropRunner( diff --git a/packages/vitest/src/internals/TestBuilder.ts b/packages/vitest/src/internals/TestBuilder.ts index 03a4dbad..6021a9d8 100644 --- a/packages/vitest/src/internals/TestBuilder.ts +++ b/packages/vitest/src/internals/TestBuilder.ts @@ -31,6 +31,7 @@ function adaptParametersForRecord( examples: parameters.examples !== undefined ? parameters.examples.map((example) => example[0]) : undefined, reporter: originalParamaters.reporter, asyncReporter: originalParamaters.asyncReporter, + plugins: undefined, }; return enrichedParameters; } @@ -98,6 +99,7 @@ function buildTestProp( ? // oxlint-disable-next-line typescript/no-non-null-assertion (runDetails) => params.asyncReporter!(adaptRunDetailsForRecord(runDetails, params)) : undefined, + plugins: undefined, } : undefined; buildTestWithPropRunner( diff --git a/website/docs/advanced/plugins.md b/website/docs/advanced/plugins.md new file mode 100644 index 00000000..076d1728 --- /dev/null +++ b/website/docs/advanced/plugins.md @@ -0,0 +1,27 @@ +--- +slug: /advanced/plugins/ +--- + +# Plugins + +Extend execution flow to meet your needs + +## Overview + +Plugins provide a way to extend and refine the runtime and execution behavior of your properties. The same way building custom arbitraries gives you the flexibility to tweak the generation flows, the plugins will give you ways to customize the way your properties will run. + +Plugins have been designed in a way to be capable of supporting things such as: + +- Executing something before the predicate +- Cutting predicate running for too long +- Capturing key insights about the execution flows including timings for observability + +## Using plugins + +Plugins can just be passed as part of the customizations accepted by the `fc.assert` runner. + +```ts +await fc.assert(fc.asyncProperty(...arbs, predicate), { + plugins: [pluginA(...paramsForPluginA), pluginB(...paramsForPluginB)], +}); +``` -- 2.51.2