//{{HEADER COMMENT START}} // This is the template file used to generate the Test client // module. Prior to being built, it's effectively just a copy // of the TestBase class, without any plugins applied. //{{HEADER COMMENT END}} import { parseTestArgs, PromiseWithSubtest, TapPlugin, TestArgs, TestBase, TestBaseOpts, } from '@tapjs/core' //{{PLUGIN IMPORT START}} //{{PLUGIN IMPORT END}} import type { ConfigSet, ConfigSetFromMetaSet, Jack, ValidValue, } from 'jackspeak' import { isConfigOption } from 'jackspeak' import { inspect } from 'node:util' /** * The set of file extensions that the tap runner will load * by default. Expaned into the `include` config values if they * contain the token `__EXTENSIONS__`. * * If plugins export a `testFileExtensions` string array, then the * entries will be added to this set. */ export const testFileExtensions = new Set(['js', 'cjs', 'mjs', 'tap']) //{{FILE TYPES START}} //{{FILE TYPES END}} const kInspect = Symbol.for('nodejs.util.inspect.custom') const copyInspect = (v: Function) => ({ [kInspect]: (...args: any[]) => inspect(v, ...args), }) const copyToString = (v: Function) => ({ toString: Object.assign(() => v.toString(), { toString: () => 'function toString() { [native code] }', }), }) const copyFunction = ( t: Test, plug: Plug, v: Function, ) => { const f: (this: Plug, ...args: any) => any = function ( ...args: any[] ) { // if you do `const { method } = t` then calling `method` will // call it on the plugin that provided it. // So no need to pre-bind anything, really. const thisArg = this === t || this === undefined ? plug : this const ret = v.apply(thisArg, args) // If a plugin method returns 'this', and it's the plugin, // then we return the extended Test instead. return ret === thisArg && thisArg === plug ? t : ret } const vv = Object.assign( Object.assign(f, v), copyToString(v), copyInspect(v), ) vv.prototype = v.prototype const nameProp = Reflect.getOwnPropertyDescriptor(v, 'name') if (nameProp) { Reflect.defineProperty(f, 'name', nameProp) } return vv } /** * Utility type to combine the array of plugins into a single combined * return type. */ export type PluginResult

any)[]> = P extends ( [ infer H extends (t: TestBase, opts: any) => any, ...infer T extends ((t: TestBase, opts: any) => any)[], ] ) ? ReturnType & PluginResult : {} /** * The union of return types of an array of functions */ type AnyReturnValue any)[]> = A extends ( [ infer H extends (...a: any[]) => any, ...infer T extends ((...a: any[]) => any)[], ] ) ? ReturnType | AnyReturnValue : never type Plug = | TestBase | { t: Test pluginLoaded( plugin: (t: any, opts?: any) => T, ): boolean plugins: TapPlugin[] } | AnyReturnValue type Plugged = TestBase & { t: Test } & BuiltPlugins type PlugKeys = keyof Plugged /** * Utility type to get the second parameter of a function, used to * get the types of all plugin options. */ export type SecondParam = T extends [any, infer S] ? S : unknown /** * The union of the second parameters of all loaded plugin methods */ export type PluginOpts

any)[]> = P extends ( [ infer H extends (t: TestBase, opts: any) => any, ...infer T extends ((t: TestBase, opts: any) => any)[], ] ) ? SecondParam> & PluginOpts : {} /** * Options that may be provided to `t.test()`. Extends * {@link @tapjs/core!index.Extra}, {@link @tapjs/core!base.BaseOpts}, * {@link @tapjs/core!test-base.TestBaseOpts}, and the second argument to all * plugin methods currently in use. */ export type TestOpts = TestBaseOpts & PluginOpts let plugins_: PluginSet /** * Type that is the array of all plugin functions loaded */ //{{PLUGINS CODE START}} export type PluginSet = (TapPlugin | TapPlugin)[] const plugins = () => { if (plugins_) return plugins_ return (plugins_ = []) } //{{PLUGINS CODE END}} /** * The combined configuration object generated by the `config` * objects exported by plugins. */ //{{PLUGINS CONFIG START}} // just referenced to keep prettier/tslint happy /* c8 ignore start */ isConfigOption const c = (j: Jack) => j const cs = c as unknown as ValidValue & ConfigSetFromMetaSet<'boolean', false, { x: {} }> c cs /* c8 ignore stop */ //{{PLUGINS CONFIG END}} //{{LOADERS START}} // these are always added with --loader export const loaders = [] // these are added with --import, if available export const importLoaders = [] // these are added with --loader, only if --import is unavailable export const loaderFallbacks = [] //{{LOADERS END}} /** * The string signature that lists all loaded plugins alphabetically, used * to determine whether a rebuild is necessary by comparing it to the `plugin` * config value. */ //{{PLUGIN SIGNATURE START}} export const signature = '' //{{PLUGIN SIGNATURE END}} /** * Union of {@link @tapjs/core!test-base.TestBase} plus all plugin * return values */ export type TTest

= TestBase & PluginResult

/** * Interface that is the assembled result of every loaded plugin. * * This is extended into an interface because otherwise the code * hinting is overwhelmingly extravagant. */ export interface BuiltPlugins extends PluginResult {} const applyPlugins = ( base: Test, plugs: (TapPlugin | TapPlugin)[] = plugins() as ( | TapPlugin | TapPlugin )[], ): Test & Ext => { const ext: Plug[] = plugs // typecast in case we have *only* option-less plugins. .map(p => (p as TapPlugin, TestBaseOpts>)(base, base.options), ) .concat(base) const getCache = new Map() // extend the proxy with Object.create, and then set the toStringTag // to 'Test', so we don't get stack frames like `Proxy.` const t = Object.create( new Proxy(base, { has(_, p) { for (const t of ext) { if (Reflect.has(t, p)) return true } return false }, ownKeys() { const k: PlugKeys[] = [] for (const t of ext) { const keys = Reflect.ownKeys(t) as PlugKeys[] k.push(...keys) } return [...new Set(k)] }, set(_, p, v) { // check to see if there's any setters, and if so, set it there // otherwise, just set on the base let didSet = false if (getCache.has(p)) getCache.delete(p) for (const t of ext) { let o: Object | null = t while (o) { // assign to the all plugs that can receive it const prop = Reflect.getOwnPropertyDescriptor(o, p) if (prop) { if (prop.set || prop.writable) { //@ts-ignore t[p] = v didSet = true } break } o = Reflect.getPrototypeOf(o) } } if (!didSet) { // if nothing has that field, assign to the base //@ts-ignore base[p] = v } return true }, get(_, p) { if (p === 'parent') { return base.parent?.t } // cache get results so t.blah === t.blah // we only cache functions, so that getters aren't memoized // Of course, a getter that returns a function will be broken, // at least when accessed from outside the plugin, but that's // a pretty narrow caveat, and easily documented. if (getCache.has(p)) return getCache.get(p) for (const plug of ext) { if (p in plug) { //@ts-ignore const v = plug[p] // Functions need special handling so that they report // the correct toString and are called on the correct object // Otherwise attempting to access #private props will fail. if (typeof v === 'function') { if (getCache.has(v)) return getCache.get(v) const vv: Function = copyFunction(t, plug, v) getCache.set(p, vv) // aliases remain aliases getCache.set(v, vv) return vv } else { return v } } } }, }), ) // assign a reference to the extended Test for use in plugin at run-time Object.assign(base, { t }) // put the .t self-ref and plugin inspection on top of the stack const top = { t, get pluginLoaded() { return (plugin: (t: any, opts?: any) => T) => { return plugs.includes(plugin) } }, get plugins() { return [...plugs] }, } ext.unshift(top) //@ts-ignore const tst: string = base[Symbol.toStringTag] Object.defineProperty(t, Symbol.toStringTag, { value: tst, configurable: true, }) Object.defineProperty(top, Symbol.toStringTag, { value: tst, configurable: true, }) return t } const kPluginSet = Symbol('@tapjs/test construction plugin set') const kClass = Symbol('@tapjs/test construction class') /** * Option object used when extending the `Test` class via * {@link @tapjs/test!index.Test.applyPlugin} * * @internal */ export type PluginExtensionOption< E extends BuiltPlugins = BuiltPlugins, O extends TestOpts = TestOpts, > = { [kPluginSet]: TapPlugin[] [kClass]?: typeof Test } /** * interface defining the fully extended {@link @tapjs/test!index.Test} class. */ export interface Test< Ext extends BuiltPlugins = BuiltPlugins, Opts extends TestOpts = TestOpts, > extends TTest { /** * Explicitly mark the test as completed, outputting the TAP plan line if * needed. * * This is not required to be called if the test function returns a promise, * or if a plan is explicitly declared and eventually fulfilled. * * @group Test Lifecycle Management */ end(): this /** * Specify the number of Test Points expected by this test. * Outputs a TAP plan line. * * @group Test Lifecycle Management */ plan(n: number, comment?: string): void } /** * This is the class that is extended for the root {@link @tapjs/core!tap.TAP} * test, and used to instantiate test objects in its child tests. It extends * {@link @tapjs/core!test-base.TestBase}, and implements the union of return * values of all loaded plugins via a Proxy. */ export class Test< Ext extends BuiltPlugins = BuiltPlugins, Opts extends TestOpts = TestOpts, > extends TestBase implements TTest { #Class: typeof Test #pluginSet: TapPlugin[] /** * @param opts Test options for this instance * * @param __INTERNAL Extension option used by the subclasses created in * {@link @tapjs/test!index.Test#applyPlugin}. * * @internal */ constructor( opts: Opts, __INTERNAL: PluginExtensionOption = { [kPluginSet]: plugins() as TapPlugin[], [kClass]: Test, }, ) { super(opts) this.#Class = __INTERNAL[kClass] as typeof Test const pluginSet = __INTERNAL[kPluginSet] this.#pluginSet = pluginSet type T = Test & Ext // need to ignore this because it's a ctor that returns a value. /* c8 ignore start */ return applyPlugins(this, pluginSet) as T } /* c8 ignore stop */ /** * The string signature of the plugins built into this Test class */ get pluginSignature() { return signature } /** * Add a plugin at run-time. * * Creates a subclass of {@link @tapjs/test!index.Test} which has the * specified plugin, and which applies the plugin to all child tests it * creates. * * Typically, it's best to load plugins using configuration, set via the * `tap plugin ` command. * * However, in some cases, for example while developing plugins or if a * certain plugin is only needed in a small number of tests, it can be * useful to apply it after the fact. * * This is best used sparingly, as it may result in poor typescript * compilation performance, which can manifest in slower test start-up times * and lag loading autocomplete in editors. If you find yourself calling * applyPlugin often, consider whether it'd be better to just add the plugin * to the entire test suite, so that it can be built up front. * * @group Plugin Management */ applyPlugin( plugin: TapPlugin, ): Test & Ext & B { if (this.printedOutput) { throw new Error('Plugins must be applied prior to any test output') } if (this.#pluginSet.includes(plugin as TapPlugin)) { throw new Error('Plugin already applied') } type ExtExt = Ext & B type ExtOpts = Opts & O const p = plugin as TapPlugin const pluginSetExtended: TapPlugin[] = ( this.#pluginSet as TapPlugin[] ).concat([p]) const extended = this as unknown as Test & ExtExt class TestExtended extends Test { constructor( opts: ExtOpts, __INTERNAL: PluginExtensionOption = { [kPluginSet]: pluginSetExtended, [kClass]: TestExtended, }, ) { super(opts, __INTERNAL) } } extended.#pluginSet = pluginSetExtended extended.#Class = TestExtended Object.defineProperty(TestExtended, 'name', { value: 'Test', configurable: true, }) return applyPlugins(extended, pluginSetExtended) } // NB: this isn't ever actually called, because we add a pluginLoaded // method in the applyPlugins proxy, but it's here to establish the // type interface. /** * Return true if the specified plugin is loaded. Asserts that the * test object in question implements the return value of the plugin. * * @group Plugin Management */ pluginLoaded( plugin: (t: any, opts?: any) => T, ): this is TestBase & T { plugin return false } /** * Return the set of plugins loaded by this Test * * @group Plugin Management */ get plugins(): TapPlugin[] { return [] } /** * Create a child Test object and parse its output as a subtest * * @group Subtest Methods */ test( name: string, extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> test( name: string, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> test( extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> test( cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> test( ...args: TestArgs & Ext, Opts> ): PromiseWithSubtest & Ext> { const extra = parseTestArgs & Ext, Opts>(...args) return this.sub(this.#Class, extra, this.test) as PromiseWithSubtest< Test & Ext > } /** * Create a subtest which is marked as `todo` * * @group Subtest Methods */ todo( name: string, extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> todo( name: string, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> todo( extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> todo( cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> todo( ...args: TestArgs & Ext, Opts> ): PromiseWithSubtest & Ext> { const extra = parseTestArgs & Ext, Opts>(...args) extra.todo = true return this.sub(this.#Class, extra, this.todo) as PromiseWithSubtest< Test & Ext > } /** * Create a subtest which is marked as `skip` * * @group Subtest Methods */ skip( name: string, extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> skip( name: string, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> skip( extra: Opts, cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> skip( cb: (t: Test & Ext) => any, ): PromiseWithSubtest & Ext> skip( ...args: TestArgs & Ext, Opts> ): PromiseWithSubtest & Ext> { const extra = parseTestArgs & Ext, Opts>(...args) extra.skip = true return this.sub(this.#Class, extra, this.skip) as PromiseWithSubtest< Test & Ext > } }