GithubHelp home page GithubHelp logo

jquense / bill Goto Github PK

View Code? Open in Web Editor NEW
53.0 4.0 6.0 115 KB

css selector engine for React elements and components

JavaScript 100.00%
css-selectors react testing pseudo-selectors react-elements dom-node

bill's Introduction

bill

Sort of like Sizzle for React, bill is a set of tools for matching React Element and Component trees against CSS selectors. bill is meant to be a substrate library for building more interesting and user friendly testing utilities. It probably shouldn't be used as a standalone tool.

import { querySelectorAll } from 'bill';

let matches = querySelectorAll('div li.foo',
  <div>
    <List>
      <li className='foo'>John</li>
      <li>Betty</li>
    </List>
  </div>
)

matches.length     // 1
matches[0].element // ReactElement{ type: 'li', props: { className: 'foo' } }

For selecting non string values, like custom Component types, you can use a tagged template strings

import { querySelectorAll, selector as s } from 'bill';

let min = 5;

let matches = querySelectorAll(s`div > ${List}, li[min=${min}]`,
  <div>
    <List>
      <li min={min}>John</li>
      <li>Betty</li>
    </List>
  </div>
)

matches.length     // 2
matches[0].element // { type: List, props }

React Compatibility

WARNING: mising module 'react/lib/ReactDOMComponentTree'

bill supports the latest React and back to v0.13.0, because a library like this involves the use of private API's, maintaining support across major versions of React is harder than normal. In particular we need to do dynamic requires to internal apis, which makes bundlers like Webpack warning about missing modules, and bundling with a less smart bundler hard.

Don't worry though they are missing because the version of React you are using doesn't have them, and thats ok, bill knows how to do its work on each supported version.

Supported

  • id: #foo
  • classes: .foo
  • attributes: div[propName="hi"] or div[boolProp]
  • >: direct descendent 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

Not supported

  • other pseudo selectors
  • non string interpolations for anything other than "tag" or prop values

API

Node

Nodes are a light object abstraction over both instances and elements that allow for a common matching and traversal API between the distinct types of React objects. The interface is similar to a traditional DOM node.

Most bill methods that accept elements or instances will also accept a node, allowing you to use the return values of the methods directly with other methods.

Node : {
  nodeType: NODE_TYPE,
  element: ReactElement,
  instance: ReactComponent | HTMLElement,
  privateInstance: ReactPrivateInstance,
  nextSibling: Node,
  prevSibling: Node,
  parentNode: Node,
  children: Array<Node>,
  findAll: (test (node) => bool, includeSelf? : bool) => array<Node>
}

Their is a caveat to the publicInstance property, when it comes to stateless functional components. Instead of returning null as React would, bill returns the instance of the internal wrapper component. This is to allow, potential chaining and also retrieval of the underlying DOM node if necessary (as in the example above).

Note: Nodes only have instances when matching against a rendered component tree

querySelectorAll(selector, subject: Element|Instance|Node) -> Array<Node>

querySelectorAll() traverses a react element or instance tree searching for nodes that match the provided selector. As the name suggests it's analogous to document.querySelectorAll. The return value is an array of Nodes.

let matches;
let elements = (
  <div>
    <List>
      <li className='foo'>John</li>
      <li>Betty</li>
    </List>
  </div>
)

// find elements in the above element description
matches = bill.querySelectorAll('div li.foo', elements)

// "John"
let textContent = matches.reduce(
  (str, node) => str + node.element.props.children, '')

// or search a rendered hierarchy
matches = bill.querySelectorAll('div li.foo', ReactDOM.render(elements))

let domNodes = matches.map(
  node => ReactDOM.findDOMNode(node.instance))

matches(selector, subject: Element|Instance|Node) -> bool

Analogous to the DOM element.matches method, matches returns true if a give element, instance or node is matched by the provided selector.

let matches;
let elements = (
  <div>
    <List>
      <li className='foo'>John</li>
      <li>Betty</li>
    </List>
  </div>
)

let johnItem = bill
  .querySelectorAll('div li', elements)
  .filter(node => bill.matches('.foo', node))


// or search a rendered hierarchy
let bettyItem = bill
  .querySelectorAll('div li.foo', ReactDOM.render(elements))
  .filter(node => bill.matches(':not(.foo)', node))

selector() -> Selector

A function used for tagged template strings,

You really only need to use the selector function when you want to write a selector matching exact prop values or a composite type.

selector`div > ${List}[length=${5}]`

findAll(subject: Element|Instance|Node, test: (node: Node)=> bool, includeSelf? : bool) -> Array

A tree traversal utility function for finding nodes that return true from the testFunction. findAll is similar to ReactTestUtils.findAllInRenderedTree, but more robust and works on both elements and instance trees.

import { findAll, NODE_TYPES } from 'bill';

let found = findAll(elements, function (node) {
  return node.nodeType === NODE_TYPES.COMPOSITE
})

compile(selector) => (node: Node) => bool

Compiles a selector string into a function that matches nodes.

registerPseudo(pseudoSelector, handlePseudo: (selector) => (node: Node) => bool)

Registers a new pseudo selector with the compiler. The second parameter is a function that will be called with the pseudo selector's argument (if it exists). The handler function should return a function that matches a node.

// A simple `:text(foo)` pseudo selector
bill.registerPseudo('text', function(value) {
  return function (node) {
    return node.children
      .filter(n => n.nodeType === NODE_TYPES.TEXT)
      .every(node => node.element === value)
  }
})

let matches = bill.querySelectorAll('li:text(john)',
  <ul>
    <li>betsy</li>
    <li>john</li>
    <li>yolanda</li>
  </ul>
)

matches[0].instance // <li class='bar'>john</li>

For pseudoSelectors whose inner argument is a selector, you can compile it to a test function with bill.compile.

// We want to test if an element has a sibling that matches
// a selector e.g. :nextSibling(.foo)
bill.registerPseudo('nextSibling', function (selector) {
  let matcher = bill.compile(selector);
  return function (node) {
    node = node.nextSibling
    return !!node && matcher(node)
  }
})

let matches = bill.querySelectorAll('li:nextSibling(li.baz)',
  <ul>
    <li className='foo'>1</li>
    <li className='bar'>2</li>
    <li className='baz'>3</li>
  </ul>
)

matches[0].instance // <li class='bar'>2</li>

registerNesting(nestingCombinator, handleNesting: (matcher: function) => (node: Node) => bool)

Similar to registerPseudo you can also register new combinator selectors (*, >, ~, +) using the same pattern. The handler function is called with the compiled selector segment.

Note: remember that selectors are matched right-to-left so the logic is generally reversed from what you might expect.

// lets implement the same previous sibling selector as above
// but with a nesting selector.
bill.registerNesting('!', test => node => {
  node = node.nextSibling
  return !!(node && test(node))
})

let matches = bill.querySelectorAll('li.baz ! li',
  <ul>
    <li className='foo'>1</li>
    <li className='bar'>2</li>
    <li className='baz'>3</li>
  </ul>
)

matches[0].instance // <li class='bar'>2</li>

NODE_TYPES Object

Set of constants that correspond to Node.nodeType. Useful for filtering out types of nodes while traversing a tree.

  • NODE_TYPES.COMPOSITE
  • NODE_TYPES.DOM
  • NODE_TYPES.TEXT

isNode() -> boolean

Determine if an object is a Node object.

bill's People

Contributors

defektive avatar jquense avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

bill's Issues

Cannot resolve module 'react/lib/ReactDOMComponentTree'

I see the following error when running karma + teaspoon tests under phantomjs. I'm using react v0.14.8. Bill @ v3.2.2 & Teaspoon v6.3.0. The top message is just a warning and the bottom one is fatal but they both cropped up at the same time; I'm struggling to track down the source of the actual error but I suspect they may be related.

WARNING in ./~/bill/utils.js
Module not found: Error: Cannot resolve module 'react/lib/ReactDOMComponentTree' in /Users/thom/dev/voltserver/ops/client/node_modules/bill
 @ ./~/bill/utils.js 18:26-68
08 04 2016 21:00:33.476:WARN [karma]: No captured browser, open http://localhost:9876/
08 04 2016 21:00:33.482:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
08 04 2016 21:00:33.491:INFO [launcher]: Starting browser PhantomJS
08 04 2016 21:00:34.275:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket /#x5ikoTDwSQyhfF5iAAAA with id 88681715
PhantomJS 2.1.1 (Mac OS X 0.0.0) ERROR
  ReferenceError: Strict mode forbids implicit creation of global property 'isPrimitive'

React 16 support

I'm attempting to update react-transition-group to react 16 but there's several tests failing with errors from within this library with "TypeError: _utils.getInstanceFromNode is not a function"

 FAIL  test/CSSTransition-test.js
  ● CSSTransition › should flush new props to the DOM before initiating a transition

    TypeError: _utils.getInstanceFromNode is not a function

      at normalizeSubject (node_modules/bill/node.js:57:57)
      at createNode (node_modules/bill/node.js:118:13)
      at NodeCollection (node_modules/teaspoon/index.js:49:44)
      at Object.<anonymous> (test/CSSTransition-test.js:17:28)
          at Promise (<anonymous>)
      at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
          at <anonymous>

For reference my branch is here: https://github.com/Graham42/react-transition-group/tree/react-16

Getting TypeError when searching a "shallowRendered" component

I'm using your teaspoon package to shallowRender my components for testing, and for some reason, I'm getting this error (from bill) when trying to call find on it:

TypeError: Cannot read property 'displayName' of undefined

Strangely, it only happens with one of my components, and I'm not sure why.

I've created a gist with the code that's giving me issues.

Edit: Forgot to mention on node v5.0.0

Silently ignores attempts at using #id selector

I was playing with teaspoon and was generating some React code that had a few buttons, each with a different id. I thought that button#a would get the button with id="a" and button#b would get the button with id="b", but both of those selectors kept returning both buttons.

After RTFM, I realized that the #id selector wasn't yet implemented.

Obviously, you probably intend to implement this. But for principle of least surprise, I'd rather it kick out an error for unsupported and/or invalid selectors. Or at least the documentation contain bolded a note that #id selectors weren't yet implemented, because I'm somewhat assuming that real front-end devs (I'm not one :) ) use #id selectors a decent amount of the time.

does this work with react-test-renderer AND react-addons-test-utils ?

I.e. does it work with the component tree's meant for serialization/snapshots:

import renderer from 'react-test-renderer'
import { querySelectorAll } from 'bill'

const tree = renderer.create(<div />).toJSON()
const matches = querySelectorAll('div', tree)
import ReactTestUtils from 'react-addons-test-utils'
import { querySelectorAll } from 'bill'

const renderer = ReactTestUtils.createRenderer()
renderer.render(<div />)

const tree = renderer.getRenderOutput()
const matches = querySelectorAll('div', tree)

The reason I ask is because you may have already created a component instance with renderer.create() which continues to stay reactively "alive" responding to state changes, and then you want to traverse its component tree, not the original component tree you first passed to renderer.create().

I'd like to add *Bill` to this library of mine:

https://github.com/faceyspacey/jest-redux-snap

So, for example, will bill be able to work with renderer.create(<div />).toJSON()?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.