From 6914e57ac13c9969038bfaeac35e472c3ff07087 Mon Sep 17 00:00:00 2001 From: Tom Ashworth Date: Wed, 4 Nov 2015 11:35:43 +0000 Subject: [PATCH] feat: convert to an npm module BREAKING CHANGE: now an npm module and not Bower compatible. - Switch to Angular commit style - Setup semantic release - Convert to ES2015 with babel - Remove dependency on Flight (now lodash.merge) --- .bowerrc | 3 - .eslintrc | 18 ++++ .gitignore | 2 +- .jshintrc | 36 ------- .travis.yml | 13 ++- Gruntfile.js | 24 ----- README.md | 20 +--- bower.json | 21 ---- config/constants.js | 9 ++ config/karma.config.js | 47 +++++++++ config/webpack.config.js | 46 ++++++++ config/webpack.config.publish.js | 11 ++ config/webpack.config.test.js | 5 + karma.conf.js | 57 ---------- lib/with-state.js | 153 --------------------------- package.json | 67 ++++++++---- src/index.js | 149 ++++++++++++++++++++++++++ src/specs.context.js | 14 +++ src/with-state.spec.js | 173 +++++++++++++++++++++++++++++++ 19 files changed, 534 insertions(+), 334 deletions(-) delete mode 100644 .bowerrc create mode 100644 .eslintrc delete mode 100644 .jshintrc delete mode 100644 Gruntfile.js delete mode 100644 bower.json create mode 100644 config/constants.js create mode 100644 config/karma.config.js create mode 100644 config/webpack.config.js create mode 100644 config/webpack.config.publish.js create mode 100644 config/webpack.config.test.js delete mode 100644 karma.conf.js delete mode 100644 lib/with-state.js create mode 100644 src/index.js create mode 100644 src/specs.context.js create mode 100644 src/with-state.spec.js diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 44491d3..0000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "bower_components" -} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..ad81109 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "parser": "babel-eslint", + "extends": [ + "standard" + ], + "env": { + "jasmine": true + }, + "rules": { + // overrides of the standard style + "curly": [2, "all"], + "indent": [2, 4], + "max-len": [2, 100, 4], + "semi": [2, "always"], + "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], + "wrap-iife": [2, "outside"] + } +} diff --git a/.gitignore b/.gitignore index 7bf6eb1..44d646d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -bower_components node_modules +dist/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index cc2daa4..0000000 --- a/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": false, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "smarttabs": true, - "strict": true, - "trailing": true, - "undef": true, - "validthis": true, - "predef": [ - "$", - "jQuery", - "before", - "beforeEach", - "define", - "describe", - "describeComponent", - "describeMixin", - "expect", - "it", - "requirejs", - "setupComponent", - "spyOnEvent" - ] -} diff --git a/.travis.yml b/.travis.yml index fde32f5..fe23380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,17 @@ +sudo: false language: node_js +cache: + directories: + - node_modules +notifications: + email: false node_js: - - "0.10" + - stable +before_install: + - npm i -g npm@^2.0.0 before_script: + - npm prune - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start +after_success: + - npm run semantic-release diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 0c48074..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = function (grunt) { - - require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); - - grunt.initConfig({ - bump: { - options: { - files: [ - 'package.json', - 'bower.json' - ], - commit: true, - commitMessage: 'v%VERSION%', - commitFiles: ['-a'], - createTag: true, - tagName: 'v%VERSION%', - tagMessage: 'v%VERSION%', - push: true, - pushTo: 'origin', - gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' - } - } - }); -}; diff --git a/README.md b/README.md index 6d60a12..6c98da2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A [Flight](https://github.com/flightjs/flight) mixin for storing and reacting to ## Installation ```bash -bower install --save flight-with-state +npm install --save flight-with-state ``` ## Example @@ -156,25 +156,13 @@ this.initialState({ ## Development -Development of this component requires [Bower](http://bower.io) to be globally -installed: +To develop this module, clone the repository and run: -```bash -npm install -g bower ``` - -Then install the Node.js and client-side dependencies by running the following -commands in the repo's root directory. - -```bash -npm install & bower install +$ npm install && npm test ``` -To continuously run the tests in Chrome during development, just run: - -```bash -npm run watch-test -``` +If the tests pass, you have a working environment. You shouldn't need any external dependencies. ## Contributing to this project diff --git a/bower.json b/bower.json deleted file mode 100644 index b122bbf..0000000 --- a/bower.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "flight-with-state", - "main": "lib/with-state.js", - "dependencies": { - "flight": "^1.2.0" - }, - "devDependencies": { - "jasmine-flight": "latest", - "jasmine-jquery": "~2.1.0" - }, - "ignore": [ - ".gitignore", - ".gitattributes", - ".travis.yml", - "CONTRIBUTING.md", - "CHANGELOG.md", - "test", - "package.json", - "karma.conf.js" - ] -} diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..f4125af --- /dev/null +++ b/config/constants.js @@ -0,0 +1,9 @@ +var path = require('path'); + +var ROOT_DIRECTORY = path.resolve(__dirname, '..'); +var BUILD_DIRECTORY = path.resolve(ROOT_DIRECTORY, 'dist'); + +module.exports = { + ROOT_DIRECTORY: ROOT_DIRECTORY, + BUILD_DIRECTORY: BUILD_DIRECTORY +}; diff --git a/config/karma.config.js b/config/karma.config.js new file mode 100644 index 0000000..cc76bc8 --- /dev/null +++ b/config/karma.config.js @@ -0,0 +1,47 @@ +'use strict'; + +var constants = require('./constants'); +var webpackConfig = require('./webpack.config.test'); +// entry is determined by karma config 'files' array +webpackConfig.entry = {}; + +module.exports = function (config) { + config.set({ + basePath: constants.ROOT_DIRECTORY, + browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ], + browserNoActivityTimeout: 60000, + client: { + captureConsole: true, + useIframe: true + }, + files: [ + 'node_modules/jquery/dist/jquery.min.js', + 'src/specs.context.js' + ], + frameworks: [ + 'jasmine' + ], + plugins: [ + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-jasmine', + 'karma-sourcemap-loader', + 'karma-webpack' + ], + preprocessors: { + 'src/specs.context.js': [ 'webpack', 'sourcemap' ] + }, + reporters: [ 'dots' ], + singleRun: true, + webpack: webpackConfig, + webpackMiddleware: { + stats: { + assetsSort: 'name', + colors: true, + children: false, + chunks: false, + modules: false + } + } + }); +}; diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 0000000..fd991bb --- /dev/null +++ b/config/webpack.config.js @@ -0,0 +1,46 @@ +var webpack = require('webpack'); + +var DedupePlugin = webpack.optimize.DedupePlugin; +var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin; +var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; + +var plugins = [ + new DedupePlugin(), + new OccurenceOrderPlugin() +]; + +if (process.env.NODE_ENV === 'production') { + plugins.push( + new UglifyJsPlugin({ + compress: { + dead_code: true, + drop_console: true, + screw_ie8: true, + warnings: true + } + }) + ); +} + +module.exports = { + entry: './src', + module: { + loaders: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader' + } + ] + }, + resolve: { + alias: { + flight: 'flightjs' + } + }, + output: { + path: './dist', + filename: 'flight-with-state.js' + }, + plugins: plugins +}; diff --git a/config/webpack.config.publish.js b/config/webpack.config.publish.js new file mode 100644 index 0000000..3aa352a --- /dev/null +++ b/config/webpack.config.publish.js @@ -0,0 +1,11 @@ +var constants = require('./constants'); +var baseConfig = require('./webpack.config'); + +module.exports = Object.assign(baseConfig, { + output: { + library: 'flight-with-state', + filename: 'flight-with-state.js', + libraryTarget: 'umd', + path: constants.BUILD_DIRECTORY + } +}); diff --git a/config/webpack.config.test.js b/config/webpack.config.test.js new file mode 100644 index 0000000..fd13dad --- /dev/null +++ b/config/webpack.config.test.js @@ -0,0 +1,5 @@ +var baseConfig = require('./webpack.config'); + +module.exports = Object.assign(baseConfig, { + devtool: 'inline-source-map' +}); diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index a14e38c..0000000 --- a/karma.conf.js +++ /dev/null @@ -1,57 +0,0 @@ -// Karma configuration file -// -// For all available config options and default values, see: -// https://github.com/karma-runner/karma/blob/stable/lib/config.js#L54 - -module.exports = function (config) { - 'use strict'; - - config.set({ - // base path, that will be used to resolve files and exclude - basePath: '', - - frameworks: [ - 'jasmine', - 'requirejs' - ], - - // list of files / patterns to load in the browser - files: [ - // loaded without require - 'bower_components/jquery/dist/jquery.min.js', - 'bower_components/jasmine-jquery/lib/jasmine-jquery.js', - 'bower_components/jasmine-flight/lib/jasmine-flight.js', - - // loaded with require - {pattern: 'bower_components/flight/**/*.js', included: false}, - {pattern: 'lib/**/*.js', included: false}, - {pattern: 'test/spec/**/*.spec.js', included: false}, - - 'test/test-main.js' - ], - - // enable / disable watching file and executing tests whenever any file changes - // CLI --auto-watch --no-auto-watch - autoWatch: true, - - // Start these browsers - // CLI --browsers Chrome, Firefox, Safari - browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], - - // If browser does not capture in given timeout [ms], kill it - // CLI --capture-timeout 5000 - captureTimeout: 20000, - - // list of files to exclude - exclude: [], - - // use dots reporter, as travis terminal does not support escaping sequences - // possible values: 'dots', 'progress' - // CLI --reporters progress - reporters: [process.env.TRAVIS ? 'dots' : 'progress'], - - // Auto run tests on start (when browsers are captured) and exit - // CLI --single-run --no-single-run - singleRun: false - }); -}; diff --git a/lib/with-state.js b/lib/with-state.js deleted file mode 100644 index 92331e5..0000000 --- a/lib/with-state.js +++ /dev/null @@ -1,153 +0,0 @@ -define(function (require) { - 'use strict'; - - var utils = require('flight/lib/utils'); - - /** - * Returns a function that returns a clone of the object passed to it initially - */ - function cloneStateDef(stateDef) { - stateDef = (stateDef || {}); - return function () { - var ctx = this; - return Object.keys(stateDef).reduce(function (state, k) { - var value = stateDef[k]; - state[k] = (typeof value === 'function' ? value.call(ctx) : value); - return state; - }, {}); - }; - } - - return withState; - function withState() { - - /** - * Define the component's initial state. Takes an object. Values - * can be of any type; functions will be called at initialize time - * to produce values that will be used as part of the component's - * initial this.state value. - * - * Examples: - * - * this.initialState({ - * active: false, - * counter: 0, - * id: function () { - * return this.node.getAttribute('data-id'); - * } - * }); - * - * Warning: reference data types (objects, arrays, functions) will - * be shared between instances of the component. Be careful. - * - * Can only be called once. - */ - this.initialState = function (txOrFn) { - if (this._stateDef) { - throw new Error("initialState can only be defined once") - } - this._stateDef = (typeof txOrFn === 'function' ? txOrFn : cloneStateDef(txOrFn)); - }; - - /** - * Change the component's state to a new value. - * - * Returns the new state. - */ - this.replaceState = function (state) { - if (!state || typeof state !== 'object') { - return; - } - this.state = state; - this.stateChanged(this.state); - return this.state; - }; - - /** - * Merge an object of new state data onto the existing state. Takes - * an object containing the changes. - * - * Merge is shallow (only merges based on top-level keys). - * - * Examples: - * - * // this.state === { counter: 0, active: false } - * - * this.mergeState({ - * counter: this.state.counter + 1 - * }); - * - * // this state === { counter: 1, active: false } - * - * Returns the new state. - */ - this.mergeState = function (tx) { - return this.replaceState(utils.merge(this.state, tx)); - }; - - /** - * Make a function that returns the piece of state specified by the - * `key` passed. - * - * Examples: - * - * var getActive = this.fromState('active'); - * ... - * getActive(); // returns this.state.active - * - * Returns a function. - */ - this.fromState = function (key) { - return function () { - return this.state[key]; - }; - }; - - /** - * Make a function that sets the state at `key` to the value it is - * called with. - * - * Example: - * - * var setActive = this.toState('active'); - * ... - * setActive(false); // sets this.state.active to false - * - * Returns a fuction. - */ - this.toState = function (key) { - return function (data) { - var tx = {}; - tx[key] = data; - this.mergeState(tx); - }; - }; - - /** - * Make a function that returns the attr at `key`. - * - * Examples: - * - * var getId = this.fromAttr('id'); - * ... - * getId(); // returns this.attr.id - * - * Returns a function. - */ - this.fromAttr = function (key) { - return function (data) { - return this.attr[key]; - }; - }; - - /** - * Noop for advice around state changes. - */ - this.stateChanged = function () {}; - - this.after('initialize', function () { - this._stateDef = (this._stateDef || function () { return {} }); - this.replaceState(this._stateDef.call(this)); - }); - } -}); diff --git a/package.json b/package.json index 6bdec9e..e212f38 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,48 @@ { "name": "flight-with-state", - "version": "2.1.1", - "devDependencies": { - "bower": "^1.4.1", - "grunt": "~0.4.5", - "grunt-bump": "^0.0.15", - "karma": "~0.12.6", - "karma-chrome-launcher": "*", - "karma-cli": "0.0.4", - "karma-firefox-launcher": "*", - "karma-jasmine": "~0.2.0", - "karma-requirejs": "~0.2.2", - "matchdep": "^0.3.0", - "requirejs": "^2.1.15" - }, + "description": "A Flight mixin for storing and reacting to change in a component's internal state.", + "main": "dist/flght-with-state.js", "scripts": { - "install": "bower install", - "test": "karma start --single-run", - "watch-test": "karma start" + "build": "rm -rf ./dist && NODE_ENV=publish webpack --config config/webpack.config.publish.js --sort-assets-by --progress", + "lint": "eslint config src", + "lint:fix": "eslint --fix config src", + "prepublish": "npm run build", + "specs": "NODE_ENV=test karma start config/karma.config.js", + "specs:watch": "npm run specs -- --no-single-run", + "test": "npm run specs && npm run lint", + "semantic-release": "semantic-release pre && npm publish && semantic-release post" }, - "description": "A Flight mixin for storing and reacting to change in a component's internal state.", - "main": "lib/with-state.js", - "directories": { - "test": "test" + "devDependencies": { + "babel-core": "^5.8.24", + "babel-eslint": "^4.1.1", + "babel-loader": "^5.3.2", + "babel-plugin-typecheck": "^1.2.0", + "babel-runtime": "^5.8.20", + "chai": "^3.2.0", + "eslint": "^1.3.1", + "eslint-config-standard": "^4.3.1", + "eslint-config-standard-react": "^1.0.4", + "eslint-plugin-react": "^3.3.1", + "eslint-plugin-standard": "^1.3.0", + "flightjs": "^1.5.1", + "immutable": "^3.7.5", + "jasmine-core": "^2.3.4", + "jquery": "^2.1.4", + "karma": "^0.13.9", + "karma-chrome-launcher": "^0.2.0", + "karma-cli": "^0.1.0", + "karma-firefox-launcher": "^0.1.6", + "karma-jasmine": "^0.3.6", + "karma-mocha": "^0.2.0", + "karma-sourcemap-loader": "^0.3.5", + "karma-webpack": "^1.7.0", + "mocha": "^2.3.2", + "object-assign": "^4.0.1", + "semantic-release": "^4.3.5", + "webpack": "^1.12.1" + }, + "peerDependencies": { + "flightjs": "^1.5.1" }, "repository": { "type": "git", @@ -42,5 +62,8 @@ "bugs": { "url": "https://github.com/flightjs/flight-with-state/issues" }, - "homepage": "https://github.com/flightjs/flight-with-state" + "homepage": "https://github.com/flightjs/flight-with-state", + "dependencies": { + "lodash.merge": "^3.3.2" + } } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..dd24e2b --- /dev/null +++ b/src/index.js @@ -0,0 +1,149 @@ +import merge from 'lodash.merge'; + +const noop = () => ({}); + +/** + * Returns a function that returns a clone of the object passed to it initially + */ +function cloneStateDef(stateDef) { + stateDef = (stateDef || {}); + return function () { + var ctx = this; + return Object.keys(stateDef).reduce((state, k) => { + var value = stateDef[k]; + state[k] = (typeof value === 'function' ? value.call(ctx) : value); + return state; + }, {}); + }; +} + +export default function withState() { + /** + * Define the component's initial state. Takes an object. Values + * can be of any type; functions will be called at initialize time + * to produce values that will be used as part of the component's + * initial this.state value. + * + * Examples: + * + * this.initialState({ + * active: false, + * counter: 0, + * id: function () { + * return this.node.getAttribute('data-id'); + * } + * }); + * + * Warning: reference data types (objects, arrays, functions) will + * be shared between instances of the component. Be careful. + * + * Can only be called once. + */ + this.initialState = function (txOrFn) { + if (this._stateDef) { + throw new Error('initialState can only be defined once'); + } + this._stateDef = (typeof txOrFn === 'function' ? txOrFn : cloneStateDef(txOrFn)); + }; + + /** + * Change the component's state to a new value. + * + * Returns the new state. + */ + this.replaceState = function (state) { + if (!state || typeof state !== 'object') { + return; + } + this.state = state; + this.stateChanged(this.state); + return this.state; + }; + + /** + * Merge an object of new state data onto the existing state. Takes + * an object containing the changes. + * + * Merge is shallow (only merges based on top-level keys). + * + * Examples: + * + * // this.state === { counter: 0, active: false } + * + * this.mergeState({ + * counter: this.state.counter + 1 + * }); + * + * // this state === { counter: 1, active: false } + * + * Returns the new state. + */ + this.mergeState = function (tx) { + return this.replaceState(merge(this.state, tx)); + }; + + /** + * Make a function that returns the piece of state specified by the + * `key` passed. + * + * Examples: + * + * var getActive = this.fromState('active'); + * ... + * getActive(); // returns this.state.active + * + * Returns a function. + */ + this.fromState = function (key) { + return function () { + return this.state[key]; + }; + }; + + /** + * Make a function that sets the state at `key` to the value it is + * called with. + * + * Example: + * + * var setActive = this.toState('active'); + * ... + * setActive(false); // sets this.state.active to false + * + * Returns a fuction. + */ + this.toState = function (key) { + return function (data) { + var tx = {}; + tx[key] = data; + this.mergeState(tx); + }; + }; + + /** + * Make a function that returns the attr at `key`. + * + * Examples: + * + * var getId = this.fromAttr('id'); + * ... + * getId(); // returns this.attr.id + * + * Returns a function. + */ + this.fromAttr = function (key) { + return function (data) { + return this.attr[key]; + }; + }; + + /** + * Noop for advice around state changes. + */ + this.stateChanged = function () {}; + + this.after('initialize', function () { + this._stateDef = (this._stateDef || noop); + this.replaceState(this._stateDef()); + }); +}; diff --git a/src/specs.context.js b/src/specs.context.js new file mode 100644 index 0000000..0c285e5 --- /dev/null +++ b/src/specs.context.js @@ -0,0 +1,14 @@ +/** + * Since we use webpack-specific features in our modules (e.g., loaders, + * plugins, adding CSS to the dependency graph), we must use webpack to build a + * test bundle. + * + * This module creates a context of all the unit test files (as per the unit + * test naming convention). It's used as the webpack entry file for unit tests. + * + * See: https://github.com/webpack/docs/wiki/context + */ + +const specsContext = require.context('.', true, /.+\.spec\.js$/); +specsContext.keys().forEach(specsContext); +module.exports = specsContext; diff --git a/src/with-state.spec.js b/src/with-state.spec.js new file mode 100644 index 0000000..0fbd757 --- /dev/null +++ b/src/with-state.spec.js @@ -0,0 +1,173 @@ +import { component } from 'flight'; +import withState from '.'; + +describe('withState', function () { + function makeComponent(Component) { + return component(withState, Component); + } + + function initializeComponent(Component, fixture, opts) { + return (new Component()).initialize(fixture || document.body, opts); + } + + var ComponentA; + var ComponentB; + var ComponentC; + var instanceAofA; + var instanceBofA; + var instanceAofB; + var instanceAofC; + + // Initialize a component and attach it to the DOM + beforeEach(function () { + ComponentA = makeComponent(function () { + this.attributes({ + initialNumber: 10 + }); + + this.initialState({ + alive: true, + count: 0, + fn: () => true, + currentNumber: function () { + return this.attr.initialNumber; + } + }); + }); + + ComponentB = makeComponent(function () { + this.initialState(function (existingState) { + return { + alive: true + }; + }); + }); + + ComponentC = makeComponent(function () {}); + + instanceAofA = initializeComponent(ComponentA); + instanceBofA = initializeComponent(ComponentA); + instanceAofB = initializeComponent(ComponentB); + instanceAofC = initializeComponent(ComponentC); + }); + + afterEach(function () { + ComponentA && ComponentA.teardownAll(); + ComponentB && ComponentB.teardownAll(); + ComponentC && ComponentC.teardownAll(); + }); + + describe('initialState', function () { + it('should add this.state', function () { + expect(instanceAofA).toBeDefined(); + expect(instanceAofA.state).toBeDefined(); + }); + + it('should add empty state if initialState is not called', function () { + expect(instanceAofC).toBeDefined(); + expect(instanceAofC.state).toEqual({}); + }); + + it('propagates initialState to this.state', function () { + expect(instanceAofA.state.alive).toBe(true); + }); + + it('calls functions defined on initialState to this.state', function () { + expect(instanceAofA.state.fn).toBe(true); + }); + + it('should be able to access attrs', function () { + expect(instanceAofA.state.currentNumber).toBe(10); + }); + + it('should take a function that returns a full initial state data', function () { + expect(instanceAofB.state.alive).toBe(true); + }); + + it('should throw on multiple calls', function () { + expect(function () { + makeComponent(function () { + this.initialState({ + alive: true + }); + this.initialState({ + dead: true + }); + }); + }).toThrow(); + }); + }); + + describe('this.state', function () { + it('should not be shared between instances', function () { + expect(instanceAofA.state).not.toBe(instanceBofA.state); + instanceAofA.state.alive = false; + expect(instanceBofA.state.alive).toBe(true); + }); + }); + + describe('this.replaceState', function () { + it('should replace this.state', function () { + instanceAofA.replaceState({ + count: 2 + }); + expect(instanceAofA.state.count).toBe(2); + expect(instanceAofA.state.alive).not.toBeDefined(); + }); + + it('handles no data', function () { + instanceAofA.replaceState(); + expect(instanceAofA.state.alive).toBe(true); + }); + }); + + describe('this.mergeState', function () { + it('should merge onto this.state', function () { + instanceAofA.mergeState({ + count: 2 + }); + expect(instanceAofA.state.count).toBe(2); + expect(instanceAofA.state.alive).toBe(true); + }); + + it('handles no data', function () { + instanceAofA.mergeState(); + expect(instanceAofA.state.alive).toBe(true); + }); + }); + + describe('this.toState', function () { + it('should make a function that mutates the specified key', function () { + var fn = instanceAofA.toState('count'); + fn.call(instanceAofA, 3); + expect(instanceAofA.state.count).toBe(3); + }); + }); + + describe('this.fromState', function () { + it('should make a function that returns data from the specified key', function () { + var fn = instanceAofA.fromState('alive'); + expect(fn.call(instanceAofA)).toBe(true); + }); + }); + + describe('this.fromAttr', function () { + it('should make a function that returns data from the specified attr key', function () { + var fn = instanceAofA.fromAttr('initialNumber'); + expect(fn.call(instanceAofA)).toBe(10); + }); + }); + + describe('this.stateChanged', function () { + it('should be advice-able to react to state changes', function () { + var data; + instanceAofA.after('stateChanged', function (state) { + data = state; + }); + var newState = {}; + instanceAofA.replaceState(newState); + expect(data).toBe(newState); + }); + }); +}); +