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

@msinnes/dom-testing-library

v0.0.21-alpha.0

Published

Testing library for @msinnes/dom applications.

Downloads

3

Readme

@msinnes/dom-testing-library

A jest testing library for @msinnes/dom applications. This testing library can be used to execute behavioral testing, end-to-end testing, and limited unit testing on rendered DOM components. The reason we call it 'limited unit testing' because we can't do direct unit testing on shallow rendered components. This library renders the components to a headless DOM (with JSDOM) and then provides an interface for querying that render.

The true power of this libary lies in the fact that screen renders are atomic. Multiple screens can be rendered in parallel. Assertions and actions can be executed independently from screen to screen. There is no deen for DOM cleanup. If a screen renders within the context of a single test, it will descope after the test run.

Usage

Install with your preferred package manager

With npm

npm install --save @msinnes/dom-testing-library

With yarn

yarn add @msinnes/dom-testing-library

You'll also need to install some dev dependencies to to transpile the code and execute the tests. Babel is the only supported Transpiler at this time, and Jest is required for the library. These dependencies could very well change with time, but they are neccessary at this time. As far as transpiling is concerned, using JSX will require @msinnes/babel-preset-dom-jsx or you'll need to pair @msinnes/babel-plugin-dom-jsx with @babel/plugin-syntax-jsx.

The dev install command would like like this with npm:

npm install --save-dev @msinnes/dom @msinnes/babel-preset-dom-jsx jest

or with yarn:

yarn add -D @msinnes/dom @msinnes/babel-preset-dom-jsx jest

With this we have the minimum required libraries to test with @msinnes/dom-testing-library.

At the top level of the application, we'll need a .babelrc file.

.babelrc
{
  "presets": ["@msinnes/babel-preset-dom-jsx"]
}

A jest configuration will match tests.

jest.config.js
module.exports = {
  testMatch: ['**/*.test.js'],
};

We can construct a simple application.

index.js
import { useState } from '@msinnes/dom';

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <button
      onclick={() => setCount(count + 1)}
    >
      Click (Click'd: {count})
    </button>
  );
};

export { App };

We can now add a test file and test our application.

index.test.js
import { render } from '@msinnes/dom-testing-library';

import { App } from './App';

describe('App', () => {
  it('should render to the DOM', () => {
    // render the screen
    const screen = render(<App />);
    // query for the button
    const button = screen.getByText('Click (Click\'d: 0)');
    // assert that the button is on screen
    expect(button).toBeDefined();
  });

  it('should increment the counter', () => {
    // render the screen
    const screen = render(<App />);
    // query for the button
    const button = screen.getByText('Click (Click\'d: 0)');
    // click the button
    button.click();
    // check for the next expected render
    const reRenderedButton = screen.getByText('Click (Click\'d: 1)');
    // assert that the button is on screen
    expect(reRenderedButton).toBeDefined();
  });
});

API

- render

The enrtypoint for the API. The render function takes a JSX render as an input and returns a screen. Screens are an interactive wrapper over a rendered DOM. The render function works as a Screen factory, creating a screen and initiating the render process. The screen provides access to the root node, the body of the wrapped dom, and some helpful query functions. This is all experimental at this time, and many of the queries are likely to change.

function render(jsxRender: JSXRender, config?: renderConfig);

- config

A configuration can be passed to the render function as a second argument. There is limited functionality, but it will expand as more features are added.

digestExpiredTimers -- Boolean

Tells the rendering engine whether to run any timers that have expired. If a setTimeout is called without a second parameter or the second parameter is 0, the timer will execute inline with the render. This simulates clearing the call-queue with a render. Even if timers are nested, the simulated queue will run recursively until the queue is empty.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const App = () => {
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    setTimeout(() => {
      setText('async text');
    });
  }, []);
  return text;
};

const screen1 = render(<App />); // Value defaults to true
const screen2 = render(<App />, { digestExpiredTimers: false });

console.log(screen1.container.innerHTML); // <-- 'async text'
console.log(screen2.container.innerHTML); // <-- 'default text'

In this case screen1 will render the text produced asynchronously, but screen2 will render the default text. This allows for more control over the timers throughout the rendering process.

digestFetch -- Boolean

Tells the rendering engine whether to fulfill any promises for fetch requests. This simulates a fetch request that is immediately resolved via the fetch configuration property.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const App = () => {
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    if (!text) fetch('url').then(data => data.text()).then(setText);
  }, []);
  return text;
};

const screen1 = render(<App /> { fetch: (req, res) => res.text('async text') }); // Value defaults to true
const screen2 = render(<App />, { digestFetch: false, fetch: (req, res) => res.text('async text') });

console.log(screen1.container.innerHTML); // <-- 'async text'
console.log(screen2.container.innerHTML); // <-- 'default text'

In this case screen1 will render the text produced asynchronously, but screen2 will render the default text. This allows for more control over the timers throughout the rendering process.

fetch -- Boolean

Provides a mechanism for handling fetch requests. Fetch requests will be passed to the provided fetch handler, and data passed to the response will be provided to the application.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const App = () => {
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    if (!text) fetch('url').then(data => data.text()).then(setText);
  }, []);
  return text;
};

const screen = render(<App /> { fetch: (req, res) => {
  res.text('async text');
  res.close();
}});

console.log(screen.container.innerHTML); // <-- 'async text'

In this case screen will render the text produced asynchronously. Calling res.close will resolve the fetch request and re-render the page.

url -- String

The location to pass to JSDOM during page load. If no url is passed, the location will be the default location. This feature is required in order to render routed application on a server, or in a testing environment. If no url is passed to the renderConfig, then the default location.href = 'about:blank will be used. If @msinnes/dom-router senses there is no route, it will return a default in-op message.

import * as DOM from '@msinnes/dom';
import { Router, Switch, Case } from '@msinnes/dom-router';
import { render } from '@msinnes/dom-testing-library';

const AppRender = (
  <Router>
    <Switch>
      <Case path="/" render="Home" />
    </Switch>
  </Router>
);

const screen1 = render(<AppRender />, { url: 'https://url.com/' });
const screen2 = render(<AppRender />);

console.log(screen1.container.innerHTML) // <-- Home
console.log(screen2.container.innerHTML) // <-- Routing inoperable without a valid URL.

- Screen

The Screen class represents a user interface for rendering @msinnes/dom apps in a NodeJS environment. Although I have not found a use case outside of testing, this screen object could be used in any type of application. It is not tied to any testing environment. That said, the screen object is a good mechanism for testing rendered applications.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const screen = render(<div>text</div>);

expect(screen.container.innerHTML).toEqual('<div>text</div>');

Instances of the Screen class render atomically. It is entirely possible to render multiple applications simultaneously in the same test suite. Queries can be made against any screen object without worrying about affecting a query or action in an application rendered in parallel.

Screen.container

The root visual element of rendered screen. Correspondes to the document.body element, not the ref associated with the element.

Screen.fetch

The time interface for executing asyncronous timers. Depending on configuration, timers can run automatically, can be batch processed, or they can be executed one by one.

Screen.fetch.next - () => void

Processes the next fetch handler. If no handlers are queued, then nothing will happen.

Screen.fetch.run - () => void

Will process all fetch handlers, running them first-in-first-out. If no handlers are queued, then nothing will happen.

Screen.time

The time interface for executing asyncronous timers. Depending on configuration, timers can run automatically, can be batch processed, or they can be executed one by one.

Screen.time.next - () => void

Processes the next expired timer associated with the screen instance. If no timers have expired, then nothing will happen.

Screen.time.play - (ticks: ?number) => void

Will tick all timers and digest the scope. If the screen is not configured to run expired timers (digestExpiredTimers = false) then none of the timers will be executed. This allows the user to advance the clock and run any timers that expired in that time.

Screen.time.run - () => void

Will run all timers that have expired, running them in expiration order. If no timers have expired, then no timers will execute.

Screen.time.tick - () => void

Will tick all timers by 1. If the screen is not configured to run expired timers (digestExpiredTimers = false) then none of the timers will be executed. This allows the user to tick the clock and run any timers that expired in that time.

Screen.(getBy|getAllBy|queryBy)* (Queries)

Screen instances support DOM queries. There are three variations on all queries. getBy will query the DOM, throwing an error if nothing is found, or throwing an error if more than one result is found. getAllBy will query the DOM, throwing an error if no results are found. queryBy will run a base query on the dom, returning an array with found results if any exist. queryBy will not throw an error if no results are found.

Screen.*ByLabelText

A query family for querying by a form element's label text.

Screen.*ByRole

A query family for querying by an element's role.

Screen.*ByText

A query family for querying an element based on its text content.

Asynchronous Testing

Currently Timeouts and Intervals are supported. requestAnimationFrame is supported as well, but only basic animation logic should be unit tested at this time. Default behavior will run timers when the view 'digests.' The digest cycle begins once the dom view has rendered. If any handles have been opened during the render cycle -- if some component or service triggers an asynchronous action -- the renderer will process those handles, rendering the applcation and reprocessing recursively until the call stack is exhausted.

There is currently a maximum recursive depth of 50, just like there is with dom effect processing (component handles that execute after the render cycle). It is important to point out now that these recursive counters are disjoint. At any time during the digest process, recursive effect process starts at 0. Quantitatively, this means that any button click could trigger up to 2500 recursive operations. Qualitatively, it means that this testing framework empowers the user to test things like nested intervals. In the right hands, that kind of concept can be very powerful. If done wrong, nesting timers mixed with poor application design have the potential of creating long running tests.

Timers

There are two main approaches when testing with timers in this package. One approach involves letting the library run the screen and process timers in order as they come. The other approach prevents automatic timer execution for granular testing.


- Automated approach Screen.time.play

Let's say I am writing a test, and it is going to write a counter to the screen. The counter will increment every second.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const App = () => {
  const [count, setCount] = DOM.useState(0);
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
      setText(`async text ${count}`);
    }, 1000);
  }, []);
  return text;
};

test('this could be a test written in any JavaScript testing library', () => {
  const screen = render(<App />);
  // At this point nothing asynchronous has processed.
  expect(screen.container.innerHTML).equals('default text');
  // We can run time for 1000 milliseconds.
  screen.time.play(1000);
  // The timer will execute once the app has 'ticked' 1000 times, digesting and rendering recursively.
  expect(screen.container.innerHTML).equals('async text 0');
  // Run time for 10 seconds.
  screen.time.play(10000);
  // The app will re-render 10 times.
  expect(screen.container.innerHTML).equals('async text 10');
});

We will say that this testing library exists and that the above tests are of how to implement it. We should note that although this library simulates browser behavior, it does so in a tightly controlled manner. The clock ticks in millisecond increments, and all code executes 'instantly' with respect to the ticking clock. Any handles left open during the render process will automatically, and the screen will do its best to behave just like a running browser.


- Manual Approach Screen.time.(next|run|tick)

Now we are going to test an application with several timers, and we are going to pass a configuration to the render function.

import * as DOM from '@msinnes/dom';
import { render } from '@msinnes/dom-testing-library';

const App = () => {
  const [count1, setCount1] = DOM.useState(0);
  const [text1, setText1] = DOM.useState('default text 1');
  const [count2, setCount2] = DOM.useState(0);
  const [text2, setText2] = DOM.useState('default text 2');
  const [text3, setText3] = DOM.useState('default text 3');
  DOM.useEffect(() => {
    setInterval(() => {
      setCount1(count1 + 1);
      setText1(`async text ${count1}}`);
    }, 500);
  }, []);
  DOM.useEffect(() => {
    setInterval(() => {
      setCount2(count2 + 1);
      setText2(`async text ${count2}`);
    }, 1000);
  }, []);
  DOM.useEffect(() => {
    setTimeout(() => {
      setText3(`async text`);
    }, 2000);
  }, []);
  return (
    <>
      <p>{text1}</p>
      <p>{text2}</p>
      <p>{text3}</p>
    </>
  );
};

test('this could be a test written in any JavaScript testing library', () => {
  const screen = render(<App />);
  // Nothing has happened.
  expect(screen.container.children[0].innerHTML).equals('default text 1');
  expect(screen.container.children[1].innerHTML).equals('default text 2');
  expect(screen.container.children[2].innerHTML).equals('default text 3');

  screen.time.tick(500);
  // We can execute the next timer, which will process the first interval
  screen.time.next();
  // The first node has updated.
  expect(screen.container.children[0].innerHTML).equals('async text 0');
  expect(screen.container.children[1].innerHTML).equals('default text 2');
  expect(screen.container.children[2].innerHTML).equals('default text 3');

  screen.time.tick(500);
  // We can execute the next timer, which will execute the first interval
  screen.time.next();
  // The first node has updated again.
  expect(screen.container.children[0].innerHTML).equals('async text 1');
  expect(screen.container.children[1].innerHTML).equals('default text 2');
  expect(screen.container.children[2].innerHTML).equals('default text 3');
  // We can execute the next timer, which will execute the second interval
  screen.time.next();
  // The second node has updated.
  expect(screen.container.children[0].innerHTML).equals('async text 1');
  expect(screen.container.children[1].innerHTML).equals('async text 0');
  expect(screen.container.children[2].innerHTML).equals('default text 3');

  // Advance the screen a full second.
  screen.time.tick(1000);
  // Execute expire timers
  screen.time.run();
  // All nodes have updated, but the first interval has only executed once when it should have run twice.
  expect(screen.container.children[0].innerHTML).equals('async text 2');
  expect(screen.container.children[1].innerHTML).equals('async text 1');
  expect(screen.container.children[2].innerHTML).equals('async text');
});

This example shows how to control timers manually. The last example shows that long running intervals do not accumulate. The library is designed to support this type of accumulation, but it will require a few changes to get that working correctly. The main issue comes because we have to limit timer execution to once per tick. Let's say we try and process an immediate interval, immediate code execution resolves to an exceeded call stack by definition. Because of this behavior an interval with 0 wait time will process the same as an interval with a wait time of 1.

Fetch

Like timers, fetch is best processed automatically (keeping digestFetch set to true). In this case, the response will resolve when close is called on the response passed to the fetch handler. Fetch handlers execute in 'Server Mode`, which means all of the server's rendering oeprations have been suspended. In 'Render Mode', async timer's are intercepted. During fetch processing, these timers will not get intercepted. Certain consideration must be taken when tests close a response asynchronousely.

import { render } from '@msinnes/dom-testing-library';
import * as DOM from '@msinnes/dom';

const App = () => {
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    if (!text) fetch('url').then(data => data.text()).then(setText);
  }, []);
  return text;
};

const screen = render(<App /> {
  fetch: (req, res) => {
    res.text('async text');
    setTimeout(() => {
      console.log(screen.container.innerHTML); // <-- 'default text'
      res.close();
      console.log(screen.container.innerHTML); // <-- 'async text'
    });
  },
});

console.log(screen.container.innerHTML); // <-- 'default text'

In this case, the close call comes inside of a setTimeout. Since the timer executes after the call stack is executed, the screen will not update until the response is closed. In most test cases, the response can be resolved synchronously. This behavior exists to support resolving fetch calls in-line during the live rendering process in @msinnes/dom-server.


- Manual Approach Screen.fetch.(next|run)

If there is any reason to manually control, the default operation can be overridden by setting config.digestFetch to false. This will prevent fetch operations from executing during the digest cycle. The fetch requests can be processed like so.

import { render } from '@msinnes/dom-testing-library';
import * as DOM from '@msinnes/dom';

const App = () => {
  const [text, setText] = DOM.useState('default text');
  DOM.useEffect(() => {
    if (!text) fetch('url').then(data => data.text()).then(setText);
  }, []);
  return text;
};

const screen = render(<App /> {
  digestFetch: false,
  fetch: (req, res) => {
    res.text('async text');
    res.close();
  },
});

console.log(screen.container.innerHTML); // <-- 'default text'
screen.fetch.next();
console.log(screen.container.innerHTML); // <-- 'async text'

In this case, executing screen.fetch.next will cause the screen to render the async text.