• This repository has been archived on 20/Sep/2018
  • Stars
    star
    388
  • Rank 110,734 (Top 3 %)
  • Language
    JavaScript
  • Created over 9 years ago
  • Updated about 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A jQuery like API for testing React elements and rendered components.

teaspoon

Project deprecated!!

teaspoon will not be updated to support React v16+. As of the recent release of enzyme v3, the differences between it and teaspoon have shrunk to almost complete API parity (baring a few things), Given that enzyme is more widely used and maintained it makes sense to switch to that going forward rather than continute to maintain teaspoon and it's underlying pieces. Thanks for using teaspoon ya'll!

Just the right amount of abstraction for writing clear, and concise React component tests.

Table of Contents generated with DocToc

Getting Started

To get started install teaspoon via npm:

npm i --save-dev teaspoon

Teaspoon is test environment agnostic, so you can (and should) bring your own test runner and frameworks. If you plan on doing normal component rendering (not just shallow rendering) you will also need a DOM environment, whether that's a browser, headless browser, or jsdom.

Like jQuery teaspoon exports a function that creates a collection of nodes; except in this case you select React elements instead of DOM nodes.

Usage

import $ from 'teaspoon';

let $div = $(<div />);

$div.length // 1
$div[0]     // ReactElement{ type: 'div', props: {} ... }

Since there is no globally accessible "document" of React elements like there is of DOM nodes, you need to start by selecting a tree. Once you have a tree you can query it with css selectors and jQuery-like methods.

let elements = (
  <MyComponent>
    <MyInput/>
    <MyInput/>
    <div className='fun-div'>  
  </MyComponent>
);

let $elements = $(elements);

$elements.find('div.fun-div').length // 1
$elements.find(MyInput).length // 2

Along with plain ol' ReactElements you can also use teaspoon to traverse a rendered component tree. Teaspoon also does a bunch of work under the hood to normalize the traversal behavior of DOM components, Custom Components, and Stateless function Components.

let Greeting = props => <div>hello <strong>{props.name}</strong></div>;

let instance = ReactDOM.render(<Greeting name='John' />, mountNode)

let $instance = $(instance);

$instance.find('strong').text() // "John"

That's nice but a bit verbose, luckily teaspoon lets you switch between both collection types (element and instance) nice and succinctly.

let Greeting = props => <div>hello <strong>{props.name}</strong></div>;

// renders `<Greeting/>` into the DOM and returns an collection of instances
let $elements = $(<Greeting />).render();

$elements.find('strong').text() // "John"

$elements.unmount() // removes the mounted component and returns a collection of elements

//or with shallow rendering
$elements.shallowRender()
  .find('strong').text() // "John"

Using selectors

The supported selector syntax is subset of standard css selectors:

  • classes: .foo
  • attributes: div[propName="hi"] or div[boolProp]
  • >: direct descendant div > .foo
  • +: adjacent sibling selector
  • ~: general sibling selector
  • :has(): parent selector div:has(a.foo)
  • :not(): negation
  • :first-child
  • :last-child
  • :text matches "text" (renderable) nodes, which may be a non string value (like a number)
  • :dom matches only DOM components
  • :composite matches composite (user defined) components
  • :contains(some text) matches nodes that have a text node descendant containing the provided text
  • :textContent(some text) matches whose text content matches the provided text

Selector support is derived from the underlying selector engine: bill. New minor versions of bill are released independent of teaspoon, so you can always check there to see what is supported on the cutting edge.

Complex selectors

Unlike normal css selectors, React elements and components often have prop values, and component types that are not serializable to a string; components are often best selected by their actual class and not a name, and prop values can complex objects such as a date prop equaling new Date().

For components, we've already seen that you can use the function name or the displayName, but sometimes they aren't available. A less brittle approach is to select by the function itself. You can use a tagged template string. via the $.selector (also aliased as $.s) function for writing complex selectors like so:

$.s`div > ${Greeting}`

// select components with `start` props _strictly_ equal to `min`
let min = 10
$.s`[start=${min}]`

If you don't want to use the newer syntax you can also call the selector function directly like:

$.s('div > ', Greeting, '.foo') // equivalent to: $.s`div > ${Greeting}.foo`

Use can use these complex selectors in any place a selector is allowed:

let Name = props => <strong>{props.name}</strong>;
let Time = props => <em>{props.date.toLocaleString()}</em>
let Greeting = props => <div>hello <Name {...props} /> its: <Time {...props} /></div>;

let now = new Date();
let $inst = $(<Greeting name='John' date={now} />);

$inst
  .render()
  .find($.s`${Greeting} > strong`)
  .text()

$inst
  .shallowRender()
  .find($.s`${Time}[date=${now}]`)
  .only()  

Testing patterns

As far as testing libraries go teaspoon has fairly few opinions about how to do stuff, so you can adapt whatever testing practices and patterns you like. However there are some patterns and paths that fall out naturally from teaspoon's API.

Using tap()

tap() provides a way to quickly step in the middle of a chain of queries and collections to make a quick assertion. Below we quickly make a few changes to the component props and check that the rendered output is what we'd expect.

let Greeting = props => <div>hello <strong>{props.name}</strong></div>;

$(<Greeting name='rikki-tikki-tavi'/>)
  .render()
  .tap(collection => {
    collection
      .first('div > :text')
      .unwrap()
      .should.equal('hello rikki-tikki-tavi')
  })
  .props('name', 'Nagaina')
  .tap(collection => {
    collection
      .first('div > :text')
      .unwrap()
      .should.equal('hello Nagaina')
  })
  .unmount()

Test specific querying ("ref" style querying).

An age old struggle with testing HTML output is that tests are usually not very resilient to DOM structure changes. You may move a save button into (or out of) some div that your test used to find the button, breaking the test. A classic technique to avoid this is the just use css classes, however it can be hard to distinguish between styling classes, and testing hooks.

In a React environment we can do one better, adding test specific attribute. This is a pattern taken up by libraries like react-test-tree, and while teaspoon doesn't specifically "support" that style of selection, its selector engine is more than powerful enough to allow that pattern of querying.

You can choose any prop name you like, but we recommend picking one that isn't likely to collide with a component's "real" props. In this example let's use _testID.

let Greeting = props => <div>hello <strong _testID='name'>{props.name}</strong></div>;

$(<Greeting name='Betty' />)
  .render()
  .find('[_testID=name]')
  .text()
  .should.equal('Betty')

You can adapt and expand this pattern however your team likes, maybe just using the single testing prop or a few. You can also add some helper methods or pseudo selectors to help codify enforce your teams testing conventions.

Build warnings with webpack

Teaspoon has a few conditional requires in order to support versions of React across major versions. This tends to mean webpack warns about missing files, even when they aren't actually bugs. You can ignore the warnings or add an extra bit of config to silence them.

/* webpack.config.js */
// ...
externals: {
  // use for react 15.4.+
  'react/lib/ReactMount': true,

  // use for React any version below that
  'react-dom/lib/ReactMount': true,
}
// ...

Adding collection methods and pseudo selectors

Teaspoon also allows extending itself and adding new pseudo selectors using a fairly straight forward API.

To add a new method for all collection types add it to $.fn (or $.prototype if the jQuery convention bothers you).

// Returns all DOM node descendants and filters by a selector
$.fn.domNodes = function(selector) {
  return this
    .find(':dom')
    .filter(selector)
}

// also works with shallowRender()
$(<MyComponent />).render().domNodes('.foo')

If you want to make a method only available to either instance of element collections you can extend $.instance.fn or $.element.fn following the same pattern as above.

createPseudo(pseudo: string, handler: (innerValue: string) => (node: Node) => bool)

For new pseudo selectors you can use the createPseudo API which provides a hook into the css selector engine used by teaspoon: bill. Pseudo selectors do introduce a new object not extensively covered here, the Node. A Node is a light abstraction that encapsulates both component instances and React elements, in order to provide a common traversal API across tree types. You can read about them and their properties here.

// input:name(email)
$.createPseudo('name', function (name) {
  // return a function that matches against elements or instances
  return function (node) {
    return $(node).is(`[name=${name}]`)
  }
})

// We want to test if an element has a sibling that matches
// a selector e.g. :nextSibling(.foo)
$.createPseudo('nextSibling', function (selector) {
  // turning the selector into a matching function up front
  // is a bit more performant, alternatively we could just do $(node).is(selector);
  let matcher = $.compileSelector(selector)

  return function (node) {
    let sibling = node.nextSibling;
    return sibling != null && matcher(sibling)
  }
})

API

Teaspoon does what it can to abstract away the differences between element and instance collections into a common API, however everything doesn't coalesce nicely, so some methods are only relevant and available for collections of instances and some for collections of elements.

Methods that are common to both collections are listed as: $.fn.methodName

Whereas methods that are specific to a collection type are listed as: $.instance.fn.methodName and $.element.fn.methodName respectively

Rendering

$.fn.render([Bool renderIntoDocument, HTMLElement mountPoint, Object context ])

Renders the first element of the Collection into the DOM using ReactDom.render. By default the component won't be added to the page document, you can pass true as the first parameter to render into the document.body. Additionally you can provide your own DOM node to mount the component into and/or a context object.

render() returns a new InstanceCollection

let elements = (
  <MyComponent>
    <div className='fun-div'>  
  </MyComponent>
);

let $elements = $(elements).render();

// accessible by document.querySelectorAll
$elements = $(elements).render(true);

// mount the component to the <span/>
$elements = $(elements).render(document.createElement('span'));
$.fn.shallowRender([props, context]) -> ElementCollection

Use the React shallow renderer utilities to shallowly render the first element of the collection.

let MyComponent ()=> <div>Hi there!</div>

$(<MyComponent/>)
  .find('div')
  .length // 0

$(<MyComponent/>)
  .shallowRender()
  .find('div')
  .length // 1
$.element.fn.update()

Since shallow collections are not "live" in the same way a real rendered component tree is, you may need to manually update the root collection to flush changes (such as those triggered by a child component).

In general you may not have to ever use update() since teaspoon tries to take care of all that for you by spying on the componentDidUpdate life-cycle hook of root component instance.

$.instance.fn.unmount()

Unmount the current tree and remove it from the DOM. unmount() returns an ElementCollection of the root component element.

let $inst = $(<Greeting name='John' date={now} />);
let rendered = $inst.render();

//do some stuff...then:
rendered.unmount()

Utility methods and properties

The methods are shared by both Element and Instance Collections.

$.selector => selector (alias: $.s)

Selector creation function.

$.dom(instance) => HTMLElement

Returns the DOM nodes for a component instance, if it exists.

$.compileSelector(selector) => (node) => bool

Compiles a selector into a function that matches a node

$.defaultContext(context: ?object) => (node) => bool

You can globally set a context object to be used for each and all renders, shallow or otherwise. This is helpful for context that is available to all levels of the application, like the router, i18n context, or a Redux Store.

$.fn.length

The length of the collection.

$.fn.unwrap() => Element|Instance|HTMLElement

Unwraps a collection of a single item returning the item. Equivalent to $el[0]; throws when there is more than one item in the collection.

$(<div><strong>hi!</strong></div>)
  .find('strong')
  .unwrap() // -> <strong>hi!</strong>
$.fn.get() => Array (alias: toArray())

Returns a real JavaScript array of the collection items.

$.fn.tap() => function(Collection)

Run an arbitrary function against the collection, helpful for making assertions while chaining.

$(<MyComponent/>).render()
  .prop({ name: 'John '})
  .tap(collection =>
    expect(collection.children().length).to.equal(2))
  .find('.foo')
$.fn.end() => Collection

Exits a chain, by returning the previous collection

$(<MyComponent/>).render()
  .find('ul')
    .single()
  .end()
  .find('div')
$.fn.each(Function iteratorFn)

An analog to Array.prototype.forEach; iterates over the collection calling the iteratorFn with each item, index, and collection.

$(<MyComponent/>).render()
  .find('div')
  .each((node, index, collection)=>{
    //do something
  })
$.fn.map(Function iteratorFn)

An analog to Array.prototype..map; maps over the collection calling the iteratorFn with each item, index, and collection.

$(<MyComponent/>).render()
  .find('div')
  .map((node, index, collection) => {
    //do something
  })
$.fn.reduce(Function iteratorFn, [initialValue]) -> Collection

An analog to Array.prototype..reduce, returns a new reduced teaspoon Collection

$(<MyComponent/>).render()
  .find('div')
  .reduce((current, node, index, collection)=>{
    return current + ', ' + node.textContent
  }, '')
$.fn.reduceRight(Function iteratorFn) -> Collection

An analog to Array.prototype.reduceRight.

$.fn.some(Function iteratorFn) -> bool

An analog to Array.prototype.some.

$.fn.every(Function iteratorFn) -> bool

An analog to Array.prototype.every.

$.instance.fn.dom -> HTMLElement

Returns the DOM nodes for each item in the Collection, if the exist

Accessors

$.fn.props

Set or get props from a component or element.

Setting props can only be done on root collections given the reactive nature of data flow in react trees (or on any element of a tree that isn't rendered).

  • .props(): retrieve all props
  • .props(propName): retrieve a single prop
  • .props(propName, propValue): update a single prop value
  • .props(newProps): merge newProps into the current set of props.
$.fn.state

Set or get state from a component or element. In shallowly rendered trees only the root component can be stateful.

  • .state(): retrieve state
  • .state(stateName): retrieve a single state value
  • .state(stateName, stateValue, [callback]): update a single state value
  • .state(newState, [callback]): merge newState into the current state.
$.fn.context

Set or get state from a component or element. In shallowly rendered trees only the root component can have context.

  • .context(): retrieve context
  • .context(String contextName): retrieve a single context value
  • .context(String contextName, Any contextValue, [Function callback]): update a single context value
  • .context(Object newContext, [Function callback]): replace current context.

Traversal methods

$.fn.find(selector)

Search all descendants of the current collection, matching against the provided selector.

$(
<div>
  <ul>
    <li>item 1</li>
  </ul>
</div>
).find('ul > li')
$.fn.filter(selector)

Filter the current collection matching against the provided selector.

let $list = $([
  <li>1</li>,
  <li className='foo'>2</li>,
  <li>3</li>,
]);

$list.filter('.foo').length // 1
$.fn.is(selector) -> Bool

Test if each item in the collection matches the provided selector.

$.fn.children([selector])

Return the children of the current selection, optionally filtered by those matching a provided selector.

note: rendered "Composite" components will only ever have one child since Components can only return a single node.

let $list = $(
  <ul>
    <li>1</li>
    <li className='foo'>2</li>
    <li>3</li>
  </ul>
);

$list.children().length // 3

$list.children('.foo').length // 1
$.fn.parent([selector])

Get the parent of each node in the current collection, optionally filtered by a selector.

$.fn.parents([selector])

Get the ancestors of each node in the current collection, optionally filtered by a selector.

$.fn.closest([selector])

For each node in the set, get the first element that matches the selector by testing the element and traversing up through its ancestors.

$.fn.first([selector])

return the first item in a collection, alternatively search all collection descendants matching the provided selector and return the first match.

$.fn.last([selector])

return the last item in a collection, alternatively search all collection descendants matching the provided selector and return the last match.

$.fn.only()

Assert that the current collection as only one item.

let $list = $(
  <ul>
    <li>1</li>
    <li className='foo'>2</li>
    <li>3</li>
  </ul>
);

$list.find('li').only() // Error! Matched more than one <li/>

$list.find('li.foo').only().length // 1
$.fn.single(selector)

Find assert that only item matches the provided selector.

let $list = $(
  <ul>
    <li>1</li>
    <li className='foo'>2</li>
    <li>3</li>
  </ul>
);

$list.single('li') // Error! Matched more than one <li/>

$list.single('.foo').length // 1
$.fn.any([selector])

Assert that the collection contains one or more nodes. Optionally search by a provided selector.

let $list = $(
  <ul>
    <li>1</li>
    <li className='foo'>2</li>
    <li>3</li>
  </ul>
);

$list.any('p')  // Error!

$list.any('li').length // 3
$.fn.none([selector])

Assert that the collection contains no nodes. Optionally search by a provided selector.

let $list = $(
  <ul>
    <li>1</li>
    <li className='foo'>2</li>
    <li>3</li>
  </ul>
);

$list.none('li')  // Error!

$list.none('p').length // 0
$.fn.text()

Return the text content of the matched Collection.

let $els = $(<div>Hello <strong>John</strong></div)

$els.text() // "Hello John"

Events

Utilities for triggering and testing events on rendered and shallowly rendered components.

$.instance.fn.trigger(String eventName, [Object data])

Trigger a "synthetic" React event on the collection items. works just like ReactTestUtils.simulate

  $(<Component/>).render()
    .trigger('click', { target: { value: 'hello ' } }).
$.element.fn.trigger(String eventName, [Object data])

Simulates (poorly) event triggering for shallow collections. The method looks for a prop following the convention 'on[EventName]': trigger('click') calls props.onClick(), and re-renders the root collection

Events don't bubble and don't have a proper event object.

  $(<Component/>).shallowRender()
    .find('button')
    .trigger('click', { target: { value: 'hello ' } }).

More Repositories

1

yup

Dead simple Object schema validation
TypeScript
21,194
star
2

react-big-calendar

gcal/outlook like calendar component
JavaScript
7,846
star
3

react-widgets

Polished, feature rich, accessible form inputs built with React
TypeScript
2,318
star
4

react-formal

Sophisticated HTML form management for React
TypeScript
528
star
5

react-dom-lite

Tiny dom implementation using react-reconciler
JavaScript
246
star
6

uncontrollable

Wrap a controlled react component, to allow specific prop/handler pairs to be uncontrolled
TypeScript
194
star
7

react-bootstrap-modal

React port of jschr's better bootstrap modals
JavaScript
89
star
8

jarle

Just Another React Live Editor
JavaScript
65
star
9

topeka

Idiomatic, "two-way" bindings, in React.
TypeScript
56
star
10

date-math

simple date math module
JavaScript
53
star
11

bill

css selector engine for React elements and components
JavaScript
53
star
12

react-layer

Simple abstraction for creating and managing new render trees
JavaScript
44
star
13

react-input-message

A small, completely unopinionated way, to display messages next to inputs based on events. Helpful for displaying input validation messages
JavaScript
42
star
14

docusaurus-theme-tailwind

TailwindCSS based Docusaurus theme
TypeScript
29
star
15

webpack-atoms

Small atomic bits for crafting webpack configs
TypeScript
25
star
16

react-component-metadata

JavaScript
21
star
17

expr

tiny util for getting and setting deep object props safely
JavaScript
20
star
18

babel-plugin-jsx-fragment

jsx syntatic sugar for React's `createFragment()`
JavaScript
16
star
19

mp4-info-parser

JavaScript
12
star
20

sass-tailwind-functions

Sass plugin implementing TailwindCSS functions
JavaScript
11
star
21

react-dialog

programtic replacements for Alert and Confirm in React
JavaScript
9
star
22

babel-preset-jason

JavaScript
9
star
23

node-stream-tokenizr

fluent streaming binary parser for working buffer chunks, strings, and byte words
JavaScript
7
star
24

react-formal-inputs

a react-widgets adaptor for react-formal
JavaScript
7
star
25

webpack-config-utils

experiments in webpack config building
JavaScript
7
star
26

babel-preset-env-modules

JavaScript
6
star
27

markdown-jsx-loader

JavaScript
6
star
28

component-metadata-loader

webpack loader to parse React component metadata from source code
JavaScript
5
star
29

mpeg-frame-parser

JavaScript
5
star
30

mini-storybook

no frills story book for components
JavaScript
5
star
31

ogg-info-parser

small streaming .ogg parser for Node.js
JavaScript
5
star
32

tiny-case

JavaScript
4
star
33

vorbis-info-parser

Small parser for Vorbis Bit streams
JavaScript
4
star
34

docpocalypse

TypeScript
4
star
35

vscode-hrx

Syntax Grammar for HRX files
JavaScript
4
star
36

css-module-loader

JavaScript
4
star
37

ID3v2-info-parser

ID3 tag version 2 streaming parser
JavaScript
4
star
38

chain-function

chain a bunch of functions
JavaScript
4
star
39

react-clonewithprops

Stand-alone React cloneWithProps util that works with multiple versions of React
JavaScript
3
star
40

flac-info-parser

JavaScript
3
star
41

node-audio-info

JavaScript
2
star
42

browser-test-pages

2
star
43

react-component-managers

utilities for composing functionality into react-components, alt-mixin
JavaScript
2
star
44

mtag

mp3 Tagging and Batch Altering Scripts
Python
2
star
45

ID3v1-info-parser

JavaScript
2
star
46

scoped-warning

Creates a scoped warning() error for tracking who is throwing what
JavaScript
2
star
47

tiny-set-immediate

Modern polyfill for set immediate
TypeScript
2
star
48

match-resource-repro

JavaScript
2
star
49

react-tackle-box

JavaScript
2
star
50

node-ktc

Pre-compiler for Kendo UI Core templates
JavaScript
2
star
51

asf-info-parser

JavaScript
2
star
52

StringUtils

Common string utilities
C#
1
star
53

style-convert-macro

Convert CSS strings to CSS object styles
JavaScript
1
star
54

babel-plugin-external-helpers

insert require for babel helpers
JavaScript
1
star
55

astonish

JavaScript
1
star
56

sound-bytes

JavaScript
1
star
57

oauthenticity

JavaScript
1
star
58

test-publish-actions

JavaScript
1
star
59

cobble

tiny object js object model for doing compositional inheritance
JavaScript
1
star
60

node-simple-chainable

A small node module for queuing a series of functions that follow from each other
JavaScript
1
star
61

svelte-combobox

Created with CodeSandbox
Svelte
1
star
62

kwik-e-mart

pub/sub and datastore
JavaScript
1
star
63

babel-plugin-globalize

Extract formatters and messages from source code
JavaScript
1
star
64

webpack-pitch-repro

JavaScript
1
star