npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@tapjs/create-plugin

v1.1.20

Published

Create a tap plugin with 'npm init @tapjs/plugin'

Downloads

1,015

Readme

@tapjs/create-plugin

Create a tap plugin with npm init @tapjs/plugin (or yarn create @tapjs/plugin).

This will create the basic scaffolding required to write a tap plugin.

Writing Plugins

Tap plugins are typically a function that takes a Test object and optionally an options object, and returns an object that is used as the extension.

This is a very simple "hello, world" plugin:

For example, a very simple "hello, world" plugin:

import { TapPlugin, TestBase } from '@tapjs/core'
export interface HelloSayer {
  hello: (who?: string) => string
}
export const plugin: TapPlugin<HelloSayer> = (t: TestBase) => {
  return {
    hello: (who: string = 'world') => {
      console.error(`${t.name} says "Hello, ${who}!"`)
    },
  }
}

As a more realistic example, to add a isString method to all tests, you could define a plugin like this:

import {
  TestBase,
  TapPlugin,
  Extra,
  MessageExtra,
  normalizeMessageExtra,
} from '@tapjs/core'

export class StringTester {
  #t: TestBase
  constructor (t: TestBase) {
    this.#t = t
  }
  isString (
    s: any,
    ...[msg, extra]: MessageExtra
  ) {
    const args = [msg, extra] as MessageExtra
    const me = normalizeMessageExtra('expect string', args)
    // note: 'this' here is the StringTester plugin object
    if (typeof s === 'string') {
      return this.#t.pass(msg, extra)
    } else {
      return this.#t.fail(msg, {
        ...extra,
        value: s,
        wanted: 'string',
        found: typeof s,
      })
    }
  }
}

export const plugin: TapPlugin<StringTester> =
  t => new StringTester(t)

The object returned by a plugin can be any sort of thing. If you want to use a class with private properties, that's totally fine as well. Whatever type is expected as the second argument will be combined with the built-in TestBaseOpts interface, and required when tests are instantiated.

import { TestBase, TapPlugin, AssertionOpts } from '@tapjs/core'
import { cleanup, render } from '@testing-library/react'
import { ReactElement } from 'react'
import { RenderResult } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
class ReactTest {
  #result?: RenderResult
  constructor(node: ReactElement) {
    if (node) {
      this.#result = render(node)
    }
  }
  findByText(text: string) {
    return this.#result?.findByText(text)
  }
  // add other helpful methods here...
}
export interface ReactTestOpts {
  node?: ReactElement
}
export const plugin: TapPlugin<ReactTest, ReactTestOpts> = (t, opts) =>
  new ReactTest(node)

When loaded, this plugin would make it so that every test may provide a { node: ReactElement } option, and would then have a t.findByText() method that returns the results.

Accessing the Constructed Plugged-In Test Object

If you need access to the constructed Test object, you can get that after the initial plugin load, via t.t. However, it will be undefined until all plugins are done loading, so do not rely on it being present in your plugin function itself or any constructors it calls synchronously.

// my-plugin.ts
export const plugin = (t: TestBase) => {
  // here, t.t === undefined
  return {
    someMethod() {
      // here, t.t is the object with all the plugins applied
    },
  }
}

Plugin Requirements

  • Plugins must be able to be loaded both with require() and import(). This maintains tap's ability to run in all JavaScript dialects.
  • Plugins must export at least one of loader, importLoader, plugin, or config.
  • Plugins should be written in TypeScript, or failing that, provide complete type information so that users' tests can infer the types of added properties and methods appropriately.

Cleaning Up

A common use-case for plugins is to manage some state that needs to be disposed at the end of the test.

In those cases, you can leverage the @tapjs/after plugin's functionality by testing to see whether it is loaded.

For example, if you wanted to ruin the process object for some reason:

import { TapPlugin, TestBase } from '@tapjs/core'
import { plugin as AfterPlugin } from '@tapjs/after'

export class BreakProcess {
  // the method that restores the world to how it was before
  #restore?: () => void
  #t: TestBase
  #didCleanup: boolean = false

  constructor (t: TestBase) {
    this.#t = t
  }

  restoreProcess() {
    if (this.#restore) {
      this.#restore()
    }
  }

  breakProcess() {
    // if we already broke it, nothing to do
    if (this.#restore) return
    const originalProcess = process
    global.process = { not: 'the actual process object' }
    this.#restore = () => {
      global.process = originalProcess
      this.#restore = undefined
    }
    if (this.#t.t.pluginLoaded(AfterPlugin) && !this.#didCleanup) {
      this.#t.t.after(() => this.restoreProcess())
    }
  }
}

(Don't actually write this plugin, though. Just use @tapjs/intercept for this, it's included with tap already, and does the sort of cleanup described here.)

Plugin Collisions

The first plugin in a list that provides a given method or property will be the one that "wins", as far as the object presented in test code is concerned.

However, within a given plugin, it only sees itself and the TestBase object it's been given. For example, if returning an object constructed from a class defined in the plugin, this will refer to that object, always.

// first-plugin
export const plugin = (t: TestBase) => {
  return {
    // this is the first plugin to register this value
    // so this is what shows up on the Test object
    myVal: 4,
    getFirstPluginVal() {
      return this.myVal // always returns 4
    },
    // this is the first plugin to register this method
    // so this is what shows up on the Test object
    getFour() {
      return 4
    },
  }
}
// second-plugin
export const plugin = (t: TestBase) => {
  return {
    // user will never see this, because first-plugin registered it
    myVal: 5,
    getSecondPluginValue() {
      return this.myVal // always returns 5
    },
    // overridden, this isn't the 'getFour' that the user will see
    getFour() {
      return 'four'
    },
  }
}

Then in the test:

import t from 'tap'
console.log(t.myVal) // 4, not 5
console.log(t.getFour()) // 4, not 'four'
console.log(t.getFirstPluginVal()) // 4
console.log(t.getSecondPluginVal()) // 5

While collisions like this are usually not a big deal, this behavior can get confusing. So, it's best to name properties and methods somewhat uniquely, so as to make collisions less likely.