Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
MicheleBertoli committed Nov 5, 2017
1 parent 9fefd3b commit d7ec44f
Show file tree
Hide file tree
Showing 14 changed files with 4,352 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .babelrc
@@ -0,0 +1,8 @@
{
"presets": ["env", "react"],

"plugins": [
"transform-class-properties",
"transform-object-rest-spread"
]
}
26 changes: 26 additions & 0 deletions .eslintrc
@@ -0,0 +1,26 @@
{
"extends": "airbnb",

"parser": "babel-eslint",

"plugins": ["prettier"],

"env": {
"browser": true,
"jest": true
},

"rules": {
"arrow-parens": ["error", "as-needed", { "requireForBlockBody": false }],
"no-use-before-define": "off",
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"prettier/prettier": ["error", {
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}],
"react/jsx-filename-extension": ["error", { "extensions": [".js"] }],
"react/prop-types": ["error", { skipUndeclared: true }],
"semi": ["error", "never"],
}
}
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules
4 changes: 4 additions & 0 deletions .travis.yml
@@ -0,0 +1,4 @@
language: node_js

node_js:
- "lts/*"
20 changes: 19 additions & 1 deletion README.md
@@ -1 +1,19 @@
# react-automata
[![Build Status](https://travis-ci.org/MicheleBertoli/react-automata.svg?branch=master)](https://travis-ci.org/MicheleBertoli/react-automata)
[![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)

> This is a work in progress
# React Automata

What if your components' state was predictable?

Goals:
- Declaratively define states
- Automagically generate tests

# Inspiration

[Infinitely Better UIs with Finite Automata](https://www.youtube.com/watch?v=VU1NKX6Qkxc) by [David](https://twitter.com/DavidKPiano)

[Rambling thoughts on React and Finite State Machines](https://www.youtube.com/watch?v=MkdV2-U16tc) by [Ryan](https://twitter.com/ryanflorence)
41 changes: 41 additions & 0 deletions package.json
@@ -0,0 +1,41 @@
{
"name": "react-automata",
"version": "0.1.0",
"main": "index.js",
"author": "Michele Bertoli",
"license": "MIT",
"scripts": {
"test": "jest",
"precommit": "lint-staged"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.1",
"babel-jest": "^21.2.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"eslint": "^4.9.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-prettier": "^2.3.1",
"eslint-plugin-react": "^7.4.0",
"husky": "^0.14.3",
"jest": "^21.2.1",
"lint-staged": "^4.3.0",
"prettier": "^1.7.4",
"prop-types": "^15.6.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-test-renderer": "^16.0.0",
"xstate": "^1.2.1"
},
"lint-staged": {
"*.{js}": [
"eslint --fix",
"git add"
]
}
}
25 changes: 25 additions & 0 deletions src/State.js
@@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'

class State extends React.Component {
render() {
return this.context.machineState === this.props.name
? this.props.children
: null
}
}

State.defaultProps = {
children: null,
}

State.propTypes = {
name: PropTypes.string.isRequired,
children: PropTypes.node,
}

State.contextTypes = {
machineState: PropTypes.string,
}

export default State
3 changes: 3 additions & 0 deletions src/index.js
@@ -0,0 +1,3 @@
export { default as State } from './State'
export { default as testStateMachine } from './testStateMachine'
export { default as withStateMachine } from './withStateMachine'
51 changes: 51 additions & 0 deletions src/testStateMachine.js
@@ -0,0 +1,51 @@
import React from 'react'
import PropTypes from 'prop-types'
import TestRenderer from 'react-test-renderer'

const createContext = (context, Component) => {
class Context extends React.Component {
getChildContext() {
return { ...context }
}

render() {
return <Component />
}
}

Context.childContextTypes = {
machineState: PropTypes.string,
}

return Context
}

const injectState = (renderer, Component, fixtures) => {
if (fixtures) {
const { instance } = renderer.root.findByType(Component)
instance.setState(fixtures)
}
}

const moveToNextState = (config, Component, machineState) => {
const { on: actions } = config.machine.states[machineState]
if (actions) {
Object.values(actions).forEach(state => {
toMatchSnapshot(config, Component, state)
})
}
}

const toMatchSnapshot = (config, Component, machineState) => {
const Context = createContext({ machineState }, Component)
const renderer = TestRenderer.create(<Context />)
injectState(renderer, Component, config.fixtures[machineState])
expect(renderer.toJSON()).toMatchSnapshot(machineState)
moveToNextState(config, Component, machineState)
}

const testStateMachine = (config, Component) => {
toMatchSnapshot(config, Component, config.machine.initial)
}

export default testStateMachine
36 changes: 36 additions & 0 deletions src/withStateMachine.js
@@ -0,0 +1,36 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Machine } from 'xstate'

const withStateMachine = config => Component => {
class StateMachine extends React.Component {
state = {
machineState: this.machine.getInitialState(),
}

getChildContext() {
return { ...this.state }
}

machine = Machine(config)

handleTransition = action => {
this.setState(prevState => ({
machineState: this.machine.transition(prevState.machineState, action)
.value,
}))
}

render() {
return <Component {...this.props} transition={this.handleTransition} />
}
}

StateMachine.childContextTypes = {
machineState: PropTypes.string,
}

return StateMachine
}

export default withStateMachine
48 changes: 48 additions & 0 deletions test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`error 1`] = `
<div>
<h1>
State Machine
</h1>
Oh, snap!
</div>
`;

exports[`fetching 1`] = `
<div>
<h1>
State Machine
</h1>
Loading...
</div>
`;

exports[`idle 1`] = `
<div>
<h1>
State Machine
</h1>
<button
onClick={[Function]}
>
Fetch
</button>
</div>
`;

exports[`success 1`] = `
<div>
<h1>
State Machine
</h1>
<ul>
<li>
GIST1
</li>
<li>
GIST2
</li>
</ul>
</div>
`;
40 changes: 40 additions & 0 deletions test/app.js
@@ -0,0 +1,40 @@
import React from 'react'
import { State } from '../src'

class App extends React.Component {
state = { gists: [] }

handleClick = () => {
this.props.transition('FETCH')

fetch('https://api.github.com/users/gaearon/gists')
.then(response => response.json())
.then(gists => {
this.setState({ gists })
this.props.transition('SUCCESS')
})
.catch(() => this.props.transition('ERROR'))
}

render() {
return (
<div>
<h1>State Machine</h1>
<State name="idle">
<button onClick={this.handleClick}>Fetch</button>
</State>
<State name="fetching">Loading...</State>
<State name="success">
<ul>
{this.state.gists.map(gist => (
<li key={gist.id}>{gist.description}</li>
))}
</ul>
</State>
<State name="error">Oh, snap!</State>
</div>
)
}
}

export default App
40 changes: 40 additions & 0 deletions test/index.spec.js
@@ -0,0 +1,40 @@
import { testStateMachine } from '../src'
import App from './app'

const machine = {
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'fetching',
},
},
fetching: {
on: {
SUCCESS: 'success',
ERROR: 'error',
},
},
success: {},
error: {},
},
}

const fixtures = {
success: {
gists: [
{
id: 'ID1',
description: 'GIST1',
},
{
id: 'ID2',
description: 'GIST2',
},
],
},
}

test('it works', () => {
testStateMachine({ machine, fixtures }, App)
})

0 comments on commit d7ec44f

Please sign in to comment.