diff --git a/package.json b/package.json
index 60d427afbdf..84753d5560f 100644
--- a/package.json
+++ b/package.json
@@ -30,12 +30,14 @@
"babel-runtime": "^6.3.14",
"cjson": "^0.4.0",
"commander": "^2.9.0",
+ "enzyme": "^2.2.0",
"expect": "^1.6.0",
"express": "^4.13.3",
"json-stringify-safe": "^5.0.1",
"node-libs-browser": "^0.5.2",
"page-bus": "^3.0.1",
"query-string": "^3.0.3",
+ "react-addons-test-utils": "^15.0.1",
"redbox-react": "^1.2.2",
"shelljs": "^0.6.0",
"stack-source-map": "^1.0.4",
diff --git a/src/client/ui/__tests__/action_logger.js b/src/client/ui/__tests__/action_logger.js
new file mode 100644
index 00000000000..bfadde9f4e9
--- /dev/null
+++ b/src/client/ui/__tests__/action_logger.js
@@ -0,0 +1,33 @@
+const { describe, it } = global;
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import ActionLogger from '../action_logger';
+
+describe('', function () {
+ describe('render', function () {
+ it('should render logs - empty', function () {
+ const wrap = shallow();
+ const logs = wrap.find('pre').first();
+ expect(logs.text()).to.equal('');
+ });
+
+ it('should render logs', function () {
+ const data = ['a1', 'a2', 'a3'];
+ const wrap = shallow();
+ const logs = wrap.find('pre').first();
+ expect(logs.text()).to.equal('a1a2a3');
+ });
+ });
+
+ describe('functions', function () {
+ it('should call the onClear prop when the button is clicked', function () {
+ const onClear = sinon.spy();
+ const wrap = shallow();
+ const clear = wrap.find('button').first();
+ clear.simulate('click');
+ expect(onClear.calledOnce).to.equal(true);
+ });
+ });
+});
diff --git a/src/client/ui/__tests__/controls.js b/src/client/ui/__tests__/controls.js
new file mode 100644
index 00000000000..2886168dbd5
--- /dev/null
+++ b/src/client/ui/__tests__/controls.js
@@ -0,0 +1,71 @@
+const { describe, it } = global;
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import StorybookControls from '../controls';
+
+describe('', function () {
+ describe('render', function () {
+ it('should render stories - empty', function () {
+ const data = [];
+ const wrap = shallow();
+ const list = wrap.find('div').first().children('div').last();
+ expect(list.text()).to.equal('');
+ });
+
+ it('should render stories', function () {
+ const data = [
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ];
+ const wrap = shallow();
+ const list = wrap.find('div').first().children('div').last();
+ expect(list.text()).to.equal('ab');
+ });
+
+ it('should render stories with selected kind', function () {
+ const data = [
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ];
+ const wrap = shallow();
+ const list = wrap.find('div').first().children('div').last();
+ expect(list.text()).to.equal('aa1a2b');
+ });
+ });
+
+ describe('functions', function () {
+ it('should call the onKind prop when a kind is clicked', function () {
+ const data = [
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ];
+ const onKind = sinon.spy();
+ const wrap = shallow();
+ const kind = wrap.find('div')
+ .filterWhere(el => el.text() === 'a')
+ .last();
+ kind.simulate('click');
+ expect(onKind.calledOnce).to.equal(true);
+ expect(onKind.firstCall.args).to.deep.equal(['a']);
+ });
+
+ it('should call the onStory prop when a story is clicked', function () {
+ const data = [
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ];
+ const onStory = sinon.spy();
+ const wrap = shallow(
+
+ );
+ const story = wrap.find('div')
+ .filterWhere(el => el.text() === 'a1')
+ .last();
+ story.simulate('click');
+ expect(onStory.calledOnce).to.equal(true);
+ expect(onStory.firstCall.args).to.deep.equal(['a1']);
+ });
+ });
+});
diff --git a/src/client/ui/__tests__/text_filter.js b/src/client/ui/__tests__/text_filter.js
new file mode 100644
index 00000000000..128a12248d8
--- /dev/null
+++ b/src/client/ui/__tests__/text_filter.js
@@ -0,0 +1,48 @@
+const { describe, it } = global;
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import TextFilter from '../text_filter';
+
+describe('', function () {
+ describe('render', function () {
+ it('should render input without filterText', function () {
+ const wrap = shallow();
+ const input = wrap.find('input').first();
+ expect(input.props().placeholder).to.equal('Filter');
+ });
+
+ it('should render input with filterText', function () {
+ const wrap = shallow();
+ const input = wrap.find('input').first();
+ expect(input.props().value).to.equal('Filter Text');
+ });
+ });
+
+ describe('functions', function () {
+ it('should call the onChange prop when input changes', function () {
+ const onChange = sinon.spy();
+ const wrap = shallow();
+ const input = wrap.find('input').first();
+ input.value = 'new value';
+ input.simulate('change', { target: input });
+ expect(onChange.calledOnce).to.equal(true);
+ expect(onChange.firstCall.calledWith('new value'));
+ });
+
+ it('should call the onClear prop when the button is clicked', function () {
+ const onClear = sinon.spy();
+ const wrap = shallow();
+
+ // use the latest div to avoid parents
+ // example:
+ const clear = wrap.find('div')
+ .filterWhere(el => el.text() === 'x')
+ .last();
+
+ clear.simulate('click');
+ expect(onClear.calledOnce).to.equal(true);
+ });
+ });
+});