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

@plaited/storybook-rite

v6.0.0

Published

Instrumented version of @plaited/rite for Storybook Interactions

Downloads

11

Readme

@plaited/storybook-rite

Storybook test framework based on RITEway, instrumented for use with the Interactions addon.

To learn more about the RITEway testing pattern read 5 questions every unit test must answer. RITEWay forces us to answer them.

  1. What is the unit under test (module, function, class, whatever)?
  2. What should it do? (Prose description)
  3. What was the actual output?
  4. What was the expected output?
  5. How do we reproduce the failure?

Why?

Storybook has a great test tooling, @storybook/testing-library & @storybook/jest. However they don't support the Shadow DOM and web components. We wanted to be able to support these for our work. So we decided to instrument a testing pattern we've loved for years to work inside a tool we love and have used for even longer!

Requirements

Test runner

JavaScript runtime

Installing

npm install--save-dev @plaited/storybook-rite

Exports

import { assert, findByAttribute, findByText, fireEvent, match, throws, wait } from '@plaited/storybook-rite'

Example Usage

Assert

export interface Assertion {
  <T>(param: {
    given: string;
    should: string;
    actual: T;
    expected: T;
  }): void;
}

How it works

When it comes to testing we like to keep it simple with basic deep equality checking, and meaningful test messages that clearly state, given some condition we should expect some outcome.

import { StoryObj, Meta, createFragment } from '@plaited/storybook'
import { withActions } from '@storybook/addon-actions/decorator'
import { assert, throws, findByText, findByAttribute} from '@plaited/storybook-rite'
import { Header } from './header.js'

const meta: Meta<typeof Header> = {
  title: 'Example/Header',
  component: Header,
  parameters: {
    layout: 'fullscreen',
  },
}

export default meta

type Story = StoryObj<typeof Header>

export const LoggedIn: Story = {
  render({ user }) {
    const frag = createFragment(<Header.template user={user?.name} />)
    return frag
  },
  play: async ({ canvasElement }) => {
    const button = await findByText<HTMLButtonElement>("Log out", canvasElement);
    assert({
      given: "button rendered",
      should: "should be in shadow dom",
      actual: button?.tagName,
      expected: "BUTTON",
    });
    assert({
      given: "button rendered",
      should: "should have correct content",
      actual: button?.value,
      expected: "onLogout",
    });
  },
  args: {
    user: {
      name: 'Jane Doe',
    },
  },
}

Test helpers

We've also included some useful helpers for testing in the browser

findByAttribute

type FindByAttribute: <T extends HTMLElement | SVGElement = HTMLElement | SVGElement>(attributeName: string, attributeValue: string | RegExp, context?: HTMLElement | SVGElement) => Promise<T>

How it works

Wether an element is in the light DOM or deeply nested in another elements shadow DOM we can find it using the helper findByAttribute. This helper takes three arguments:

  • type attribute = string
  • type value = string
  • type context = HTML Optional defaults to the document

It will search the light dom of the context, then penetrate all nested shadow DOMs in the context until it finds the first element with the target attribute and value or return undefined.

Example Scenario

Let's say we've rendered our Header component in a logged out state to the canvas. We can test to make sure it rendered correctly like so:

export const LoggedOut: Story = {
  play: async ({ canvasElement }) => {
    const bar = await findByAttribute("bp-target", "button-bar", canvasElement);
    assert({
      given: "Logged out mode",
      should: "Button bar should have two children",
      actual: bar.childElementCount,
      expected: 2,
    });
  }
}

findByText

type FindByText: <T extends HTMLElement = HTMLElement>(searchText: string | RegExp, context?: HTMLElement) => Promise<T>

How it works

Wether an element is in the light DOM or deeply nested in another elements shadow DOM we can find it using the helper findByText. This helper takes two arguments:

  • type searchText = string | RegExp
  • type context = HTMLElement Optional defaults to the document

It will search the light dom of the context, then penetrate all nested shadow DOMs in the context until it finds the first element with the Node.textContent of our searchText or return undefined.

Example Scenario

Let's say we've rendered our Header component in a logged in state to the canvas. We can verify it by asserting on the presence of a log out button like so:

export const LoggedIn: Story = {
  play: async ({ canvasElement }) => {
    const button = await findByText<HTMLButtonElement>("Log out", canvasElement);
    assert({
      given: "button rendered",
      should: "should be in shadow dom",
      actual: button?.tagName,
      expected: "BUTTON",
    });
    assert({
      given: "button rendered",
      should: "should have correct content",
      actual: button?.value,
      expected: "onLogout",
    });
  }
}

fireEvent

type FireEvent: <T extends HTMLElement | SVGElement = HTMLElement | SVGElement>(element: T, eventName: string, options?: EventArguments) => Promise<void>

How it works

When fireEvent is passed an Element and an event type it will trigger that event type on the Element. We can then subsequently assert some change.

Further we can also pass it an optional third argument object with the following type signature

type EventArguments = {
  bubbles?: boolean; // default true
  composed?: boolean; // default true
  cancelable?: boolean; // default true
  detail?: Record<string, unknown>; // default undefined
};

Example Scenario

We've rendered our Page component in a logged out state to the canvas. We have reference to the login button. When we click the button, we expect our button bar to now contain a Log Out button.

export const LoggingIn: Story = {
  play: async ({ canvasElement }) => {
    const loginButton = await findByAttribute('value', 'onLogin', canvasElement)
    await fireEvent(loginButton, 'click')
    const logoutButton = await findByAttribute('value', 'onLogout', canvasElement)
    assert({
      given: 'the user is logged in',
      should: 'render the logout button',
      actual: logoutButton?.textContent,
      expected: 'Log out',
    })
  },
}

match

type Match: (str: string) => (pattern: string | RegExp) => string

How it works

When match is passed a string of text it returns a search callback function. We can then pass that callback a string of text to search for in the original string or a regex pattern. It will return the matched text, if found, or an empty string.

Example Scenario

We want to make sure our Buttons are rendering our label arg so write an assertion like so to verify.

export const Small: Story = {
  play: async ({ canvasElement }) => {
    const button = await findByAttribute<HTMLButtonElement>('type', 'button', canvasElement)
    const expected = 'Small Button'
    const contains = match(button?.innerHTML);
    assert({
      given: 'label arg passed to story',
      should: 'render with label content',
      actual: contains(expected),
      expected,
    })
  },
  args: {
    dataTarget: 'button',
    size: 'small',
    label: 'Small Button',
  },
}

throws

type Throws = <U extends unknown[], V>(fn: (...args: U) => V, ...args: U) => string | undefined | Promise<string | undefined>

How it works

throws takes a function which can be synchronous or asynchronous along with any arguments that are to be passed to the function. If an error is thrown when the function is called with those arguments throws returns error.toString(). If an error is not thrown throws returns undefined.

Example Scenario

Sometimes you want to test a utility function or just make sure your exports are working as expected. We're exporting a file that defines our custom elements. We want to make sure it's working as expected. We've already imported it in our storybook's preview-head.html. So we now need to try to re-define one our custom elements to ensure it throws.

export const RegistryIsDefiningElements: Story = {
  play: async () => {
    const msg = await throws(
      (tag, el) =>  customElements.define(el, tag),
      Header, 
      Header.tag
    );
    assert({
      given: "reverent receives irreverent attitude",
      should: "throw an error",
      actual: msg.includes(`Failed to execute 'define' on 'CustomElementRegistry'`),
      expected: true,
    });
  },
}

wait

type Wait: (ms: number) => Promise<unknown>

How it works

wait is an async function that will wait the given time passed to it in milliseconds and then continue execution of the play function.

Example Scenario

We're testing plaited's useMessenger utility which uses the CustomEvent constructor. So we know we need to wait a bit before asserting message receipt, let's wait 60ms. We're also going to use sinon to create a spy callback to assert on message values.

export const ConnectSendClose: Story = {
  play: async () => {
     const msg = messenger()
    const spy = sinon.spy()
    const close = msg.connect('actor1', spy)
    msg('actor1', { type: 'a', detail: { value: 4 } })
    await wait(60)
    assert({
      given: 'message send',
      should: 'connected spy should receive message',
      actual: spy.calledWith({ type: 'a', detail: { value: 4 } }),
      expected: true,
    })
    close()
    assert({
      given: 'close',
      should: 'has should return false',
      actual: msg.has('actor1'),
      expected: false,
    })
  },
}