From be900aa25ca205c467dcbbab9c284ef680441996 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Mon, 29 Apr 2019 15:27:23 +0900
Subject: Clean settings components

---
 src/settings/components/form/BlacklistForm.jsx | 52 ++++++++++++++++++++++++++
 1 file changed, 52 insertions(+)
 create mode 100644 src/settings/components/form/BlacklistForm.jsx

(limited to 'src/settings/components/form/BlacklistForm.jsx')

diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.jsx
new file mode 100644
index 0000000..13571f1
--- /dev/null
+++ b/src/settings/components/form/BlacklistForm.jsx
@@ -0,0 +1,52 @@
+import './BlacklistForm.scss';
+import AddButton from '../ui/AddButton';
+import DeleteButton from '../ui/DeleteButton';
+import React from 'react';
+
+class BlacklistForm extends React.Component {
+
+  render() {
+    let value = this.props.value;
+    if (!value) {
+      value = [];
+    }
+
+    return <div className='form-blacklist-form'>
+      {
+        value.map((url, index) => {
+          return <div key={index} className='form-blacklist-form-row'>
+            <input data-index={index} type='text' name='url'
+              className='column-url' value={url}
+              onChange={this.bindValue.bind(this)} />
+            <DeleteButton data-index={index} name='delete'
+              onClick={this.bindValue.bind(this)} />
+          </div>;
+        })
+      }
+      <AddButton name='add' style={{ float: 'right' }}
+        onClick={this.bindValue.bind(this)} />
+    </div>;
+  }
+
+  bindValue(e) {
+    if (!this.props.onChange) {
+      return;
+    }
+
+    let name = e.target.name;
+    let index = e.target.getAttribute('data-index');
+    let next = this.props.value ? this.props.value.slice() : [];
+
+    if (name === 'url') {
+      next[index] = e.target.value;
+    } else if (name === 'add') {
+      next.push('');
+    } else if (name === 'delete') {
+      next.splice(index, 1);
+    }
+
+    this.props.onChange(next);
+  }
+}
+
+export default BlacklistForm;
-- 
cgit v1.2.3


From ee078c677bbb7756636898830f766465d426f68b Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Mon, 29 Apr 2019 15:47:42 +0900
Subject: Declare types on settings

---
 src/settings/components/form/BlacklistForm.jsx  |  6 ++++++
 src/settings/components/form/KeymapsForm.jsx    |  6 ++++++
 src/settings/components/form/PropertiesForm.jsx |  6 ++++++
 src/settings/components/form/SearchForm.jsx     | 10 ++++++++++
 src/settings/components/ui/Input.jsx            |  8 ++++++++
 5 files changed, 36 insertions(+)

(limited to 'src/settings/components/form/BlacklistForm.jsx')

diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.jsx
index 13571f1..7dad9ee 100644
--- a/src/settings/components/form/BlacklistForm.jsx
+++ b/src/settings/components/form/BlacklistForm.jsx
@@ -2,6 +2,7 @@ import './BlacklistForm.scss';
 import AddButton from '../ui/AddButton';
 import DeleteButton from '../ui/DeleteButton';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 class BlacklistForm extends React.Component {
 
@@ -49,4 +50,9 @@ class BlacklistForm extends React.Component {
   }
 }
 
+BlacklistForm.propTypes = {
+  value: PropTypes.arrayOf(PropTypes.string),
+  onChange: PropTypes.func,
+};
+
 export default BlacklistForm;
diff --git a/src/settings/components/form/KeymapsForm.jsx b/src/settings/components/form/KeymapsForm.jsx
index 25a3710..f5444fc 100644
--- a/src/settings/components/form/KeymapsForm.jsx
+++ b/src/settings/components/form/KeymapsForm.jsx
@@ -1,5 +1,6 @@
 import './KeymapsForm.scss';
 import React from 'react';
+import PropTypes from 'prop-types';
 import Input from '../ui/Input';
 import keymaps from '../../keymaps';
 
@@ -44,4 +45,9 @@ class KeymapsForm extends React.Component {
   }
 }
 
+KeymapsForm.propTypes = {
+  value: PropTypes.objectOf(PropTypes.string),
+  onChange: PropTypes.func,
+};
+
 export default KeymapsForm;
diff --git a/src/settings/components/form/PropertiesForm.jsx b/src/settings/components/form/PropertiesForm.jsx
index 77991fc..9143a7a 100644
--- a/src/settings/components/form/PropertiesForm.jsx
+++ b/src/settings/components/form/PropertiesForm.jsx
@@ -1,5 +1,6 @@
 import './PropertiesForm.scss';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 class PropertiesForm extends React.Component {
 
@@ -57,4 +58,9 @@ class PropertiesForm extends React.Component {
   }
 }
 
+PropertiesForm.propTypes = {
+  value: PropTypes.objectOf(PropTypes.any),
+  onChange: PropTypes.func,
+};
+
 export default PropertiesForm;
diff --git a/src/settings/components/form/SearchForm.jsx b/src/settings/components/form/SearchForm.jsx
index f52fd5f..718cca1 100644
--- a/src/settings/components/form/SearchForm.jsx
+++ b/src/settings/components/form/SearchForm.jsx
@@ -1,5 +1,6 @@
 import './SearchForm.scss';
 import React from 'react';
+import PropTypes from 'prop-types';
 import AddButton from '../ui/AddButton';
 import DeleteButton from '../ui/DeleteButton';
 
@@ -75,4 +76,13 @@ class SearchForm extends React.Component {
   }
 }
 
+SearchForm.propTypes = {
+  value: PropTypes.shape({
+    default: PropTypes.string,
+    engines: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
+  }),
+  onChange: PropTypes.func,
+
+};
+
 export default SearchForm;
diff --git a/src/settings/components/ui/Input.jsx b/src/settings/components/ui/Input.jsx
index d090f5b..13a246b 100644
--- a/src/settings/components/ui/Input.jsx
+++ b/src/settings/components/ui/Input.jsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import './Input.scss';
 
 class Input extends React.Component {
@@ -49,4 +50,11 @@ class Input extends React.Component {
   }
 }
 
+Input.propTypes = {
+  type: PropTypes.string,
+  error: PropTypes.string,
+  label: PropTypes.string,
+  value: PropTypes.string,
+};
+
 export default Input;
-- 
cgit v1.2.3


From 808344eecfedd04149551867724e46a7988c45a0 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Tue, 30 Apr 2019 09:03:01 +0900
Subject: Fix React Component tests

---
 karma.conf.js                                      |   1 +
 src/settings/components/form/BlacklistForm.jsx     |  11 +-
 src/settings/components/form/KeymapsForm.jsx       |  10 +-
 src/settings/components/form/SearchForm.jsx        |   6 +-
 .../console/components/console/Completion.test.jsx | 168 +++++++++++++++++++++
 .../console/components/console/completion.test.jsx | 138 -----------------
 .../components/form/BlacklistForm.test.jsx         |  92 +++++++++++
 test/settings/components/form/KeymapsForm.test.jsx |  64 ++++++++
 .../components/form/PropertiesForm.test.jsx        | 104 +++++++++++++
 .../components/form/SearchEngineForm.test.jsx      | 128 ++++++++++++++++
 .../components/form/blacklist-form.test.jsx        |  81 ----------
 .../settings/components/form/keymaps-form.test.jsx |  52 -------
 .../components/form/properties-form.test.jsx       |  85 -----------
 .../components/form/search-engine-form.test.jsx    | 103 -------------
 test/settings/components/ui/input.test.jsx         |  71 ++++++---
 15 files changed, 620 insertions(+), 494 deletions(-)
 create mode 100644 test/console/components/console/Completion.test.jsx
 delete mode 100644 test/console/components/console/completion.test.jsx
 create mode 100644 test/settings/components/form/BlacklistForm.test.jsx
 create mode 100644 test/settings/components/form/KeymapsForm.test.jsx
 create mode 100644 test/settings/components/form/PropertiesForm.test.jsx
 create mode 100644 test/settings/components/form/SearchEngineForm.test.jsx
 delete mode 100644 test/settings/components/form/blacklist-form.test.jsx
 delete mode 100644 test/settings/components/form/keymaps-form.test.jsx
 delete mode 100644 test/settings/components/form/properties-form.test.jsx
 delete mode 100644 test/settings/components/form/search-engine-form.test.jsx

(limited to 'src/settings/components/form/BlacklistForm.jsx')

diff --git a/karma.conf.js b/karma.conf.js
index 5e69f9e..32da469 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -29,6 +29,7 @@ module.exports = function (config) {
     singleRun: true,
 
     webpack: {
+      mode: 'development',
       devtool: 'inline-source-map',
       resolve: webpackConfig.resolve,
       module: webpackConfig.module
diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.jsx
index 7dad9ee..cee04bd 100644
--- a/src/settings/components/form/BlacklistForm.jsx
+++ b/src/settings/components/form/BlacklistForm.jsx
@@ -7,14 +7,9 @@ import PropTypes from 'prop-types';
 class BlacklistForm extends React.Component {
 
   render() {
-    let value = this.props.value;
-    if (!value) {
-      value = [];
-    }
-
     return <div className='form-blacklist-form'>
       {
-        value.map((url, index) => {
+        this.props.value.map((url, index) => {
           return <div key={index} className='form-blacklist-form-row'>
             <input data-index={index} type='text' name='url'
               className='column-url' value={url}
@@ -55,4 +50,8 @@ BlacklistForm.propTypes = {
   onChange: PropTypes.func,
 };
 
+BlacklistForm.defaultProps = {
+  value: [],
+};
+
 export default BlacklistForm;
diff --git a/src/settings/components/form/KeymapsForm.jsx b/src/settings/components/form/KeymapsForm.jsx
index f5444fc..26a22d7 100644
--- a/src/settings/components/form/KeymapsForm.jsx
+++ b/src/settings/components/form/KeymapsForm.jsx
@@ -7,10 +7,6 @@ import keymaps from '../../keymaps';
 class KeymapsForm extends React.Component {
 
   render() {
-    let values = this.props.value;
-    if (!values) {
-      values = {};
-    }
     return <div className='form-keymaps-form'>
       {
         keymaps.fields.map((group, index) => {
@@ -19,7 +15,7 @@ class KeymapsForm extends React.Component {
               group.map((field) => {
                 let name = field[0];
                 let label = field[1];
-                let value = values[name];
+                let value = this.props.value[name] || '';
                 return <Input
                   type='text' id={name} name={name} key={name}
                   label={label} value={value}
@@ -50,4 +46,8 @@ KeymapsForm.propTypes = {
   onChange: PropTypes.func,
 };
 
+KeymapsForm.defaultProps = {
+  value: {},
+};
+
 export default KeymapsForm;
diff --git a/src/settings/components/form/SearchForm.jsx b/src/settings/components/form/SearchForm.jsx
index 718cca1..2e070fc 100644
--- a/src/settings/components/form/SearchForm.jsx
+++ b/src/settings/components/form/SearchForm.jsx
@@ -8,9 +8,6 @@ class SearchForm extends React.Component {
 
   render() {
     let value = this.props.value;
-    if (!value) {
-      value = { default: '', engines: []};
-    }
     if (!value.engines) {
       value.engines = [];
     }
@@ -82,7 +79,10 @@ SearchForm.propTypes = {
     engines: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
   }),
   onChange: PropTypes.func,
+};
 
+SearchForm.defaultProps = {
+  value: { default: '', engines: []},
 };
 
 export default SearchForm;
diff --git a/test/console/components/console/Completion.test.jsx b/test/console/components/console/Completion.test.jsx
new file mode 100644
index 0000000..16bf11a
--- /dev/null
+++ b/test/console/components/console/Completion.test.jsx
@@ -0,0 +1,168 @@
+import React from 'react';
+import Completion from 'console/components/console/Completion'
+import ReactTestRenderer from 'react-test-renderer';
+
+describe("console/components/console/completion", () => {
+  let completions = [{
+    name: "Fruit",
+    items: [{ caption: "apple" }, { caption: "banana" }, { caption: "cherry" }],
+  }, {
+    name: "Element",
+    items: [{ caption: "argon" }, { caption: "boron" }, { caption: "carbon" }],
+  }];
+
+  it('renders Completion component', () => {
+    let root = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={30}
+      />).root;
+
+    expect(root.children).to.have.lengthOf(1);
+
+    let children = root.children[0].children;
+    expect(children).to.have.lengthOf(8);
+    expect(children[0].props.title).to.equal('Fruit');
+    expect(children[1].props.caption).to.equal('apple');
+    expect(children[2].props.caption).to.equal('banana');
+    expect(children[3].props.caption).to.equal('cherry');
+    expect(children[4].props.title).to.equal('Element');
+    expect(children[5].props.caption).to.equal('argon');
+    expect(children[6].props.caption).to.equal('boron');
+    expect(children[7].props.caption).to.equal('carbon');
+  });
+
+  it('highlight current item', () => {
+    let root = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={30}
+      select={3}
+      />).root;
+
+    let children = root.children[0].children;
+    expect(children[5].props.highlight).to.be.true;
+  });
+
+  it('does not highlight any items', () => {
+    let root = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={30}
+      select={-1}
+      />).root;
+
+    let children = root.children[0].children;
+    for (let li of children[0].children) {
+      expect(li.props.highlight).not.to.be.ok;
+    }
+  });
+
+  it('limits completion items', () => {
+    let root = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={3}
+      select={-1}
+      />).root;
+
+    let children = root.children[0].children;
+    expect(children).to.have.lengthOf(3);
+
+    expect(children[0].props.title).to.equal('Fruit');
+    expect(children[1].props.caption).to.equal('apple');
+    expect(children[2].props.caption).to.equal('banana');
+
+    root = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={3} select={0}
+      />).root;
+
+    children = root.children[0].children;
+    expect(children[1].props.highlight).to.be.true;
+  })
+
+  it('scrolls up to down with select', () => {
+    let component = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={3}
+      select={1}
+      />);
+    let instance = component.getInstance();
+    let root = component.root;
+
+    let children = root.children[0].children;
+    expect(children).to.have.lengthOf(3);
+    expect(children[0].props.title).to.equal('Fruit');
+    expect(children[1].props.caption).to.equal('apple');
+    expect(children[2].props.caption).to.equal('banana');
+
+    component.update(<Completion
+      completions={completions}
+      size={3}
+      select={2}
+    />);
+
+    children = root.children[0].children;
+    expect(children).to.have.lengthOf(3);
+    expect(children[0].props.caption).to.equal('apple');
+    expect(children[1].props.caption).to.equal('banana');
+    expect(children[2].props.caption).to.equal('cherry');
+    expect(children[2].props.highlight).to.be.true;
+
+    component.update(<Completion
+      completions={completions}
+      size={3}
+      select={3}
+    />);
+
+    children = root.children[0].children;
+    expect(children).to.have.lengthOf(3);
+    expect(children[0].props.caption).to.equal('cherry');
+    expect(children[1].props.title).to.equal('Element');
+    expect(children[2].props.caption).to.equal('argon');
+    expect(children[2].props.highlight).to.be.true;
+  });
+
+  it('scrolls down to up with select', () => {
+    let component = ReactTestRenderer.create(<Completion
+      completions={completions}
+      size={3}
+      select={5}
+      />);
+    let root = component.root;
+    let instance = component.getInstance();
+
+    let children = root.children[0].children;
+    expect(children).to.have.lengthOf(3);
+    expect(children[0].props.caption).to.equal('argon');
+    expect(children[1].props.caption).to.equal('boron');
+    expect(children[2].props.caption).to.equal('carbon');
+
+    component.update(<Completion
+      completions={completions}
+      size={3}
+      select={4}
+    />);
+
+    children = root.children[0].children;
+    expect(children[1].props.highlight).to.be.true;
+
+    component.update(<Completion
+      completions={completions}
+      size={3}
+      select={3}
+    />);
+
+    children = root.children[0].children;
+    expect(children[0].props.highlight).to.be.true;
+
+    component.update(<Completion
+      completions={completions}
+      size={3}
+      select={2}
+    />);
+
+    children = root.children[0].children;
+    expect(children[0].props.caption).to.equal('cherry');
+    expect(children[1].props.title).to.equal('Element');
+    expect(children[2].props.caption).to.equal('argon');
+    expect(children[0].props.highlight).to.be.true;
+  });
+});
diff --git a/test/console/components/console/completion.test.jsx b/test/console/components/console/completion.test.jsx
deleted file mode 100644
index 8d81ce8..0000000
--- a/test/console/components/console/completion.test.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { render } from 'react';
-import Completion from 'console/components/console/completion'
-
-describe("console/components/console/completion", () => {
-  let completions = [{
-    name: "Fruit",
-    items: [{ caption: "apple" }, { caption: "banana" }, { caption: "cherry" }],
-  }, {
-    name: "Element",
-    items: [{ caption: "argon" }, { caption: "boron" }, { caption: "carbon" }],
-  }];
-
-  beforeEach(() => {
-    document.body.innerHTML = '';
-  });
-
-  it('renders Completion component', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={30}
-      />, document.body);
-
-    expect(ul.children).to.have.lengthOf(8);
-    expect(ul.children[0].textContent).to.equal('Fruit');
-    expect(ul.children[1].textContent).to.equal('apple');
-    expect(ul.children[2].textContent).to.equal('banana');
-    expect(ul.children[3].textContent).to.equal('cherry');
-    expect(ul.children[4].textContent).to.equal('Element');
-    expect(ul.children[5].textContent).to.equal('argon');
-    expect(ul.children[6].textContent).to.equal('boron');
-    expect(ul.children[7].textContent).to.equal('carbon');
-  });
-
-  it('highlight current item', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={30}
-      select={3}
-      />, document.body);
-    expect(ul.children[5].className.split(' ')).to.include('vimvixen-completion-selected');
-  });
-
-  it('does not highlight any items', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={30}
-      select={-1}
-      />, document.body);
-    for (let li of ul.children) {
-      expect(li.className.split(' ')).not.to.include('vimvixen-completion-selected');
-    }
-  });
-
-
-  it('limits completion items', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={-1}
-      />, document.body);
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
-
-    ul = render(<Completion
-      completions={completions}
-      size={3} select={0}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
-    expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected');
-  })
-
-  it('scrolls up to down with select', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={1}
-      />, document.body);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
-    expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
-
-    ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={2}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['apple', 'banana', 'cherry']);
-    expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
-
-    ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={3}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']);
-    expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
-  });
-
-  it('scrolls up to down with select', () => {
-    let ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={5}
-      />, document.body);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
-    expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
-
-    ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={4}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
-    expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected');
-
-    ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={3}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
-    expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected');
-
-    ul = render(<Completion
-      completions={completions}
-      size={3}
-      select={2}
-      />, document.body, ul);
-
-    expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']);
-    expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected');
-  });
-});
diff --git a/test/settings/components/form/BlacklistForm.test.jsx b/test/settings/components/form/BlacklistForm.test.jsx
new file mode 100644
index 0000000..2be5d96
--- /dev/null
+++ b/test/settings/components/form/BlacklistForm.test.jsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestRenderer from 'react-test-renderer';
+import ReactTestUtils from 'react-dom/test-utils';
+import BlacklistForm from 'settings/components/form/BlacklistForm'
+
+describe("settings/form/BlacklistForm", () => {
+  describe('render', () => {
+    it('renders BlacklistForm', () => {
+      let root = ReactTestRenderer.create(
+        <BlacklistForm value={['*.slack.com', 'www.google.com/maps']} />,
+      ).root;
+
+      let children = root.children[0].children;
+      expect(children).to.have.lengthOf(3);
+      expect(children[0].children[0].props.value).to.equal('*.slack.com');
+      expect(children[1].children[0].props.value).to.equal('www.google.com/maps');
+      expect(children[2].props.name).to.equal('add');
+    });
+
+    it('renders blank value', () => {
+      let root = ReactTestRenderer.create(<BlacklistForm />).root;
+
+      let children = root.children[0].children;
+      expect(children).to.have.lengthOf(1);
+      expect(children[0].props.name).to.equal('add');
+    });
+  });
+
+  describe('onChange', () => {
+    let container;
+
+    beforeEach(() => {
+      container = document.createElement('div');
+      document.body.appendChild(container);
+    });
+
+    afterEach(() => {
+      document.body.removeChild(container);
+      container = null;
+    });
+
+    it('invokes onChange event on edit', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<BlacklistForm
+          value={['*.slack.com', 'www.google.com/maps*']}
+          onChange={value => {
+            expect(value).to.have.lengthOf(2);
+            expect(value).to.have.members(['gitter.im', 'www.google.com/maps*']);
+            done();
+          }}
+        />, container)
+      });
+
+      let input = document.querySelectorAll('input[type=text]')[0];
+      input.value = 'gitter.im';
+      ReactTestUtils.Simulate.change(input);
+    });
+
+    it('invokes onChange event on delete', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<BlacklistForm
+          value={['*.slack.com', 'www.google.com/maps*']}
+          onChange={value => {
+            expect(value).to.have.lengthOf(1);
+            expect(value).to.have.members(['www.google.com/maps*']);
+            done();
+          }}
+        />, container)
+      });
+
+      let button = document.querySelectorAll('input[type=button]')[0];
+      ReactTestUtils.Simulate.click(button);
+    });
+
+    it('invokes onChange event on add', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<BlacklistForm
+          value={['*.slack.com']}
+          onChange={value => {
+            expect(value).to.have.lengthOf(2);
+            expect(value).to.have.members(['*.slack.com', '']);
+            done();
+          }}
+        />, container);
+      });
+
+      let button = document.querySelector('input[type=button].ui-add-button');
+      ReactTestUtils.Simulate.click(button);
+    });
+  });
+});
diff --git a/test/settings/components/form/KeymapsForm.test.jsx b/test/settings/components/form/KeymapsForm.test.jsx
new file mode 100644
index 0000000..6ac57c9
--- /dev/null
+++ b/test/settings/components/form/KeymapsForm.test.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestRenderer from 'react-test-renderer';
+import ReactTestUtils from 'react-dom/test-utils';
+import KeymapsForm from 'settings/components/form/KeymapsForm'
+
+describe("settings/form/KeymapsForm", () => {
+  describe('render', () => {
+    it('renders keymap fields', () => {
+      let root = ReactTestRenderer.create(<KeymapsForm value={{
+        'scroll.vertically?{"count":1}': 'j',
+        'scroll.vertically?{"count":-1}': 'k',
+      }} />).root
+
+      let inputj = root.findByProps({ id: 'scroll.vertically?{"count":1}' });
+      let inputk = root.findByProps({ id: 'scroll.vertically?{"count":-1}' });
+
+      expect(inputj.props.value).to.equal('j');
+      expect(inputk.props.value).to.equal('k');
+    });
+
+    it('renders blank value', () => {
+      let root = ReactTestRenderer.create(<KeymapsForm />).root;
+
+      let inputj = root.findByProps({ id: 'scroll.vertically?{"count":1}' });
+      let inputk = root.findByProps({ id: 'scroll.vertically?{"count":-1}' });
+
+      expect(inputj.props.value).to.be.empty;
+      expect(inputk.props.value).to.be.empty;
+    });
+  });
+
+  describe('onChange event', () => {
+    let container;
+
+    beforeEach(() => {
+      container = document.createElement('div');
+      document.body.appendChild(container);
+    });
+
+    afterEach(() => {
+      document.body.removeChild(container);
+      container = null;
+    });
+
+    it('invokes onChange event on edit', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<KeymapsForm
+          value={{
+            'scroll.vertically?{"count":1}': 'j',
+            'scroll.vertically?{"count":-1}': 'k',
+          }}
+          onChange={value => {
+            expect(value['scroll.vertically?{"count":1}']).to.equal('jjj');
+            done();
+          }} />, container);
+      });
+
+      let input = document.getElementById('scroll.vertically?{"count":1}');
+      input.value = 'jjj';
+      ReactTestUtils.Simulate.change(input);
+    });
+  });
+});
diff --git a/test/settings/components/form/PropertiesForm.test.jsx b/test/settings/components/form/PropertiesForm.test.jsx
new file mode 100644
index 0000000..80f60d2
--- /dev/null
+++ b/test/settings/components/form/PropertiesForm.test.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestRenderer from 'react-test-renderer';
+import ReactTestUtils from 'react-dom/test-utils';
+import PropertiesForm from 'settings/components/form/PropertiesForm'
+
+describe("settings/form/PropertiesForm", () => {
+  describe('render', () => {
+    it('renders PropertiesForm', () => {
+      let types = {
+        mystr: 'string',
+        mynum: 'number',
+        mybool: 'boolean',
+        empty: 'string',
+      }
+      let value = {
+        mystr: 'abc',
+        mynum: 123,
+        mybool: true,
+      };
+
+      let root = ReactTestRenderer.create(
+        <PropertiesForm types={types} value={value} />,
+      ).root
+
+      let input = root.findByProps({ name: 'mystr' });
+      expect(input.props.type).to.equals('text');
+      expect(input.props.value).to.equal('abc');
+
+      input = root.findByProps({ name: 'mynum' });
+      expect(input.props.type).to.equals('number');
+      expect(input.props.value).to.equal(123);
+
+      input = root.findByProps({ name: 'mybool' });
+      expect(input.props.type).to.equals('checkbox');
+      expect(input.props.value).to.equal(true);
+    });
+  });
+
+  describe('onChange', () => {
+    let container;
+
+    beforeEach(() => {
+      container = document.createElement('div');
+      document.body.appendChild(container);
+    });
+
+    afterEach(() => {
+      document.body.removeChild(container);
+      container = null;
+    });
+
+    it('invokes onChange event on text changed', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<PropertiesForm
+          types={{ 'myvalue': 'string' }}
+          value={{ 'myvalue': 'abc' }}
+          onChange={value => {
+            expect(value).to.have.property('myvalue', 'abcd');
+            done();
+          }}
+        />, container);
+      });
+
+      let input = document.querySelector('input[name=myvalue]');
+      input.value = 'abcd'
+      ReactTestUtils.Simulate.change(input);
+    });
+
+    it('invokes onChange event on number changeed', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<PropertiesForm
+          types={{ 'myvalue': 'number' }}
+          value={{ '': 123 }}
+          onChange={value => {
+            expect(value).to.have.property('myvalue', 1234);
+            done();
+          }}
+        />, container);
+      });
+
+      let input = document.querySelector('input[name=myvalue]');
+      input.value = '1234'
+      ReactTestUtils.Simulate.change(input);
+    });
+
+    it('invokes onChange event on checkbox changed', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<PropertiesForm
+          types={{ 'myvalue': 'boolean' }}
+          value={{ 'myvalue': false }}
+          onChange={value => {
+            expect(value).to.have.property('myvalue', true);
+            done();
+          }}
+        />, container);
+      });
+
+      let input = document.querySelector('input[name=myvalue]');
+      input.checked = true;
+      ReactTestUtils.Simulate.change(input);
+    });
+  });
+});
diff --git a/test/settings/components/form/SearchEngineForm.test.jsx b/test/settings/components/form/SearchEngineForm.test.jsx
new file mode 100644
index 0000000..06822f2
--- /dev/null
+++ b/test/settings/components/form/SearchEngineForm.test.jsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestRenderer from 'react-test-renderer';
+import ReactTestUtils from 'react-dom/test-utils';
+import SearchForm from 'settings/components/form/SearchForm'
+
+describe("settings/form/SearchForm", () => {
+  describe('render', () => {
+    it('renders SearchForm', () => {
+      let root = ReactTestRenderer.create(<SearchForm value={{
+        default: 'google',
+        engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']],
+      }} />).root;
+
+      let names = root.findAllByProps({ name: 'name' });
+      expect(names).to.have.lengthOf(2);
+      expect(names[0].props.value).to.equal('google');
+      expect(names[1].props.value).to.equal('yahoo');
+
+      let urls = root.findAllByProps({ name: 'url' });
+      expect(urls).to.have.lengthOf(2);
+      expect(urls[0].props.value).to.equal('google.com');
+      expect(urls[1].props.value).to.equal('yahoo.com');
+    });
+
+    it('renders blank value', () => {
+      let root = ReactTestRenderer.create(<SearchForm />).root;
+
+      let names = root.findAllByProps({ name: 'name' });
+      expect(names).to.be.empty;
+
+      let urls = root.findAllByProps({ name: 'url' });
+      expect(urls).to.be.empty;
+    });
+
+    it('renders blank engines', () => {
+      let root = ReactTestRenderer.create(
+        <SearchForm value={{ default: 'google' }} />,
+      ).root;
+
+      let names = root.findAllByProps({ name: 'name' });
+      expect(names).to.be.empty;
+
+      let urls = root.findAllByProps({ name: 'url' });
+      expect(urls).to.be.empty;
+    });
+  });
+
+  describe('onChange event', () => {
+    let container;
+
+    beforeEach(() => {
+      container = document.createElement('div');
+      document.body.appendChild(container);
+    });
+
+    afterEach(() => {
+      document.body.removeChild(container);
+      container = null;
+    });
+
+    it('invokes onChange event on edit', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<SearchForm
+          value={{
+            default: 'google',
+              engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']]
+          }}
+          onChange={value => {
+            expect(value.default).to.equal('louvre');
+            expect(value.engines).to.have.lengthOf(2)
+            expect(value.engines).to.have.deep.members(
+              [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
+            );
+            done();
+          }} />, container);
+      });
+
+      let radio = document.querySelectorAll('input[type=radio]');
+      radio.checked = true;
+
+      let name = document.querySelector('input[name=name]');
+      name.value = 'louvre';
+
+      ReactTestUtils.Simulate.change(name);
+    });
+
+    it('invokes onChange event on delete', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<SearchForm value={{
+            default: 'yahoo',
+            engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
+          }}
+          onChange={value => {
+            expect(value.default).to.equal('yahoo');
+            expect(value.engines).to.have.lengthOf(1)
+            expect(value.engines).to.have.deep.members(
+              [['yahoo', 'yahoo.com']]
+            );
+            done();
+          }} />, container);
+      });
+
+      let button = document.querySelector('input[type=button]');
+      ReactTestUtils.Simulate.click(button);
+    });
+
+    it('invokes onChange event on add', (done) => {
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<SearchForm value={{
+          default: 'yahoo',
+            engines: [['google', 'google.com']]
+          }}
+          onChange={value => {
+            expect(value.default).to.equal('yahoo');
+            expect(value.engines).to.have.lengthOf(2)
+            expect(value.engines).to.have.deep.members(
+              [['google', 'google.com'], ['', '']],
+            );
+            done();
+          }} />, container);
+      });
+
+      let button = document.querySelector('input[type=button].ui-add-button');
+      ReactTestUtils.Simulate.click(button);
+    });
+  });
+});
diff --git a/test/settings/components/form/blacklist-form.test.jsx b/test/settings/components/form/blacklist-form.test.jsx
deleted file mode 100644
index 40067d0..0000000
--- a/test/settings/components/form/blacklist-form.test.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { render } from 'react';
-import BlacklistForm from 'settings/components/form/blacklist-form'
-
-describe("settings/form/BlacklistForm", () => {
-  beforeEach(() => {
-    document.body.innerHTML = '';
-  });
-
-  describe('render', () => {
-    it('renders BlacklistForm', () => {
-      render(<BlacklistForm value={['*.slack.com', 'www.google.com/maps']} />, document.body);
-
-      let inputs = document.querySelectorAll('input[type=text]');
-      expect(inputs).to.have.lengthOf(2);
-      expect(inputs[0].value).to.equal('*.slack.com');
-      expect(inputs[1].value).to.equal('www.google.com/maps');
-    });
-
-    it('renders blank value', () => {
-      render(<BlacklistForm />, document.body);
-
-      let inputs = document.querySelectorAll('input[type=text]');
-      expect(inputs).to.be.empty;
-    });
-
-    it('renders blank value', () => {
-      render(<BlacklistForm />, document.body);
-
-      let inputs = document.querySelectorAll('input[type=text]');
-      expect(inputs).to.be.empty;
-    });
-  });
-
-  describe('onChange', () => {
-    it('invokes onChange event on edit', (done) => {
-      render(<BlacklistForm
-        value={['*.slack.com', 'www.google.com/maps*']}
-        onChange={value => {
-          expect(value).to.have.lengthOf(2)
-            .and.have.members(['gitter.im', 'www.google.com/maps*']);
-
-          done();
-        }}
-      />, document.body);
-
-      let input = document.querySelectorAll('input[type=text]')[0];
-      input.value = 'gitter.im';
-      input.dispatchEvent(new Event('change'))
-    });
-
-    it('invokes onChange event on delete', (done) => {
-      render(<BlacklistForm
-        value={['*.slack.com', 'www.google.com/maps*']}
-        onChange={value => {
-          expect(value).to.have.lengthOf(1)
-            .and.have.members(['www.google.com/maps*']);
-
-          done();
-        }}
-      />, document.body);
-
-      let button = document.querySelectorAll('input[type=button]')[0];
-      button.click();
-    });
-
-    it('invokes onChange event on add', (done) => {
-      render(<BlacklistForm
-        value={['*.slack.com']}
-        onChange={value => {
-          expect(value).to.have.lengthOf(2)
-            .and.have.members(['*.slack.com', '']);
-
-          done();
-        }}
-      />, document.body);
-
-      let button = document.querySelector('input[type=button].ui-add-button');
-      button.click();
-    });
-  });
-});
diff --git a/test/settings/components/form/keymaps-form.test.jsx b/test/settings/components/form/keymaps-form.test.jsx
deleted file mode 100644
index 79d8d20..0000000
--- a/test/settings/components/form/keymaps-form.test.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { render } from 'react';
-import KeymapsForm from 'settings/components/form/keymaps-form'
-
-describe("settings/form/KeymapsForm", () => {
-  beforeEach(() => {
-    document.body.innerHTML = '';
-  });
-
-  describe('render', () => {
-    it('renders KeymapsForm', () => {
-      render(<KeymapsForm value={{
-        'scroll.vertically?{"count":1}': 'j',
-        'scroll.vertically?{"count":-1}': 'k',
-      }} />, document.body);
-
-      let inputj = document.getElementById('scroll.vertically?{"count":1}');
-      let inputk = document.getElementById('scroll.vertically?{"count":-1}');
-
-      expect(inputj.value).to.equal('j');
-      expect(inputk.value).to.equal('k');
-    });
-
-    it('renders blank value', () => {
-      render(<KeymapsForm />, document.body);
-
-      let inputj = document.getElementById('scroll.vertically?{"count":1}');
-      let inputk = document.getElementById('scroll.vertically?{"count":-1}');
-
-      expect(inputj.value).to.be.empty;
-      expect(inputk.value).to.be.empty;
-    });
-  });
-
-  describe('onChange event', () => {
-    it('invokes onChange event on edit', (done) => {
-      render(<KeymapsForm
-        value={{
-          'scroll.vertically?{"count":1}': 'j',
-          'scroll.vertically?{"count":-1}': 'k',
-        }}
-        onChange={value => {
-          expect(value['scroll.vertically?{"count":1}']).to.equal('jjj');
-
-          done();
-        }} />, document.body);
-
-      let input = document.getElementById('scroll.vertically?{"count":1}');
-      input.value = 'jjj';
-      input.dispatchEvent(new Event('change'))
-    });
-  });
-});
diff --git a/test/settings/components/form/properties-form.test.jsx b/test/settings/components/form/properties-form.test.jsx
deleted file mode 100644
index 171dcb6..0000000
--- a/test/settings/components/form/properties-form.test.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { render } from 'react';
-import PropertiesForm from 'settings/components/form/properties-form'
-
-describe("settings/form/PropertiesForm", () => {
-  beforeEach(() => {
-    document.body.innerHTML = '';
-  });
-
-  describe('render', () => {
-    it('renders PropertiesForm', () => {
-      let types = {
-        mystr: 'string',
-        mynum: 'number',
-        mybool: 'boolean',
-        empty: 'string',
-      }
-      let value = {
-        mystr: 'abc',
-        mynum: 123,
-        mybool: true,
-      };
-      render(<PropertiesForm types={types} value={value} />, document.body);
-
-      let strInput = document.querySelector('input[name=mystr]');
-      let numInput = document.querySelector('input[name=mynum]');
-      let boolInput = document.querySelector('input[name=mybool]');
-      let emptyInput = document.querySelector('input[name=empty]');
-
-      expect(strInput.type).to.equals('text');
-      expect(strInput.value).to.equal('abc');
-      expect(numInput.type).to.equals('number');
-      expect(numInput.value).to.equal('123');
-      expect(boolInput.type).to.equals('checkbox');
-      expect(boolInput.checked).to.be.true;
-      expect(emptyInput.type).to.equals('text');
-      expect(emptyInput.value).to.be.empty;
-    });
-  });
-
-  describe('onChange', () => {
-    it('invokes onChange event on text changed', (done) => {
-      render(<PropertiesForm
-        types={{ 'myvalue': 'string' }}
-        value={{ 'myvalue': 'abc' }}
-        onChange={value => {
-          expect(value).to.have.property('myvalue', 'abcd');
-          done();
-        }}
-      />, document.body);
-
-      let input = document.querySelector('input[name=myvalue]');
-      input.value = 'abcd'
-      input.dispatchEvent(new Event('change'))
-    });
-
-    it('invokes onChange event on number changeed', (done) => {
-      render(<PropertiesForm
-        types={{ 'myvalue': 'number' }}
-        value={{ '': 123 }}
-        onChange={value => {
-          expect(value).to.have.property('myvalue', 1234);
-          done();
-        }}
-      />, document.body);
-
-      let input = document.querySelector('input[name=myvalue]');
-      input.value = '1234'
-      input.dispatchEvent(new Event('change'))
-    });
-
-    it('invokes onChange event on checkbox changed', (done) => {
-      render(<PropertiesForm
-        types={{ 'myvalue': 'boolean' }}
-        value={{ 'myvalue': false }}
-        onChange={value => {
-          expect(value).to.have.property('myvalue', true);
-          done();
-        }}
-      />, document.body);
-
-      let input = document.querySelector('input[name=myvalue]');
-      input.click();
-    });
-  });
-});
diff --git a/test/settings/components/form/search-engine-form.test.jsx b/test/settings/components/form/search-engine-form.test.jsx
deleted file mode 100644
index d1cbb30..0000000
--- a/test/settings/components/form/search-engine-form.test.jsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { render } from 'react';
-import SearchForm from 'settings/components/form/search-form'
-
-describe("settings/form/SearchForm", () => {
-  beforeEach(() => {
-    document.body.innerHTML = '';
-  });
-
-  describe('render', () => {
-    it('renders SearchForm', () => {
-      render(<SearchForm value={{
-        default: 'google',
-        engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']],
-      }} />, document.body);
-
-      let names = document.querySelectorAll('input[name=name]');
-      expect(names).to.have.lengthOf(2);
-      expect(names[0].value).to.equal('google');
-      expect(names[1].value).to.equal('yahoo');
-
-      let urls = document.querySelectorAll('input[name=url]');
-      expect(urls).to.have.lengthOf(2);
-      expect(urls[0].value).to.equal('google.com');
-      expect(urls[1].value).to.equal('yahoo.com');
-    });
-
-    it('renders blank value', () => {
-      render(<SearchForm />, document.body);
-
-      let names = document.querySelectorAll('input[name=name]');
-      let urls = document.querySelectorAll('input[name=url]');
-      expect(names).to.have.lengthOf(0);
-      expect(urls).to.have.lengthOf(0);
-    });
-
-    it('renders blank engines', () => {
-      render(<SearchForm value={{ default: 'google' }} />, document.body);
-
-      let names = document.querySelectorAll('input[name=name]');
-      let urls = document.querySelectorAll('input[name=url]');
-      expect(names).to.have.lengthOf(0);
-      expect(urls).to.have.lengthOf(0);
-    });
-  });
-
-  describe('onChange event', () => {
-    it('invokes onChange event on edit', (done) => {
-      render(<SearchForm
-        value={{
-          default: 'google',
-          engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']]
-        }}
-        onChange={value => {
-          expect(value.default).to.equal('louvre');
-          expect(value.engines).to.have.lengthOf(2)
-            .and.have.deep.members([['louvre', 'google.com'], ['yahoo', 'yahoo.com']])
-
-          done();
-        }} />, document.body);
-
-      let radio = document.querySelectorAll('input[type=radio]');
-      radio.checked = true;
-
-      let name = document.querySelector('input[name=name]');
-      name.value = 'louvre';
-      name.dispatchEvent(new Event('change'))
-    });
-
-    it('invokes onChange event on delete', (done) => {
-      render(<SearchForm value={{
-          default: 'yahoo',
-          engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
-        }}
-        onChange={value => {
-          expect(value.default).to.equal('yahoo');
-          expect(value.engines).to.have.lengthOf(1)
-            .and.have.deep.members([['yahoo', 'yahoo.com']])
-
-          done();
-        }} />, document.body);
-
-      let button = document.querySelector('input[type=button]');
-      button.click();
-    });
-
-    it('invokes onChange event on add', (done) => {
-      render(<SearchForm value={{
-          default: 'yahoo',
-          engines: [['google', 'google.com']]
-        }}
-        onChange={value => {
-          expect(value.default).to.equal('yahoo');
-          expect(value.engines).to.have.lengthOf(2)
-            .and.have.deep.members([['google', 'google.com'], ['', '']])
-
-          done();
-        }} />, document.body);
-
-      let button = document.querySelector('input[type=button].ui-add-button');
-      button.click();
-    });
-  });
-});
diff --git a/test/settings/components/ui/input.test.jsx b/test/settings/components/ui/input.test.jsx
index db12bf8..432efcb 100644
--- a/test/settings/components/ui/input.test.jsx
+++ b/test/settings/components/ui/input.test.jsx
@@ -1,14 +1,28 @@
-import { render } from 'react';
-import Input from 'settings/components/ui/input'
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestUtils from 'react-dom/test-utils';
+import Input from 'settings/components/ui/Input'
 
 describe("settings/ui/Input", () => {
+  let container;
+
   beforeEach(() => {
-    document.body.innerHTML = '';
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(container);
+    container = null;
   });
 
   context("type=text", () => {
     it('renders text input', () => {
-      render(<Input type='text' name='myname' label='myfield' value='myvalue'/>, document.body)
+      ReactTestUtils.act(() => {
+        ReactDOM.render(
+          <Input type='text' name='myname' label='myfield' value='myvalue'/>,
+          container);
+      });
 
       let label = document.querySelector('label');
       let input = document.querySelector('input');
@@ -19,20 +33,26 @@ describe("settings/ui/Input", () => {
     });
 
     it('invoke onChange', (done) => {
-      render(<Input type='text' name='myname' label='myfield' value='myvalue' onChange={(e) => {
-        expect(e.target.value).to.equal('newvalue');
-        done();
-      }}/>, document.body);
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<Input type='text' name='myname' label='myfield' value='myvalue' onChange={(e) => {
+          expect(e.target.value).to.equal('newvalue');
+          done();
+        }}/>, container);
+      });
 
       let input = document.querySelector('input');
       input.value = 'newvalue';
-      input.dispatchEvent(new Event('change'))
+      ReactTestUtils.Simulate.change(input);
     });
   });
 
   context("type=radio", () => {
     it('renders radio button', () => {
-      render(<Input type='radio' name='myname' label='myfield' value='myvalue'/>, document.body)
+      ReactTestUtils.act(() => {
+        ReactDOM.render(
+          <Input type='radio' name='myname' label='myfield' value='myvalue'/>,
+          container);
+      });
 
       let label = document.querySelector('label');
       let input = document.querySelector('input');
@@ -43,20 +63,27 @@ describe("settings/ui/Input", () => {
     });
 
     it('invoke onChange', (done) => {
-      render(<Input type='text' name='radio' label='myfield' value='myvalue' onChange={(e) => {
-        expect(e.target.checked).to.be.true;
-        done();
-      }}/>, document.body);
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<Input type='text' name='radio' label='myfield' value='myvalue' onChange={(e) => {
+          expect(e.target.checked).to.be.true;
+          done();
+        }}/>,
+        container);
+      });
 
       let input = document.querySelector('input');
       input.checked = true;
-      input.dispatchEvent(new Event('change'))
+      ReactTestUtils.Simulate.change(input);
     });
   });
 
   context("type=textarea", () => {
     it('renders textarea button', () => {
-      render(<Input type='textarea' name='myname' label='myfield' value='myvalue' error='myerror' />, document.body)
+      ReactTestUtils.act(() => {
+        ReactDOM.render(
+          <Input type='textarea' name='myname' label='myfield' value='myvalue' error='myerror' />,
+          container);
+      });
 
       let label = document.querySelector('label');
       let textarea = document.querySelector('textarea');
@@ -69,14 +96,16 @@ describe("settings/ui/Input", () => {
     });
 
     it('invoke onChange', (done) => {
-      render(<Input type='textarea' name='myname' label='myfield' value='myvalue' onChange={(e) => {
-        expect(e.target.value).to.equal('newvalue');
-        done();
-      }}/>, document.body);
+      ReactTestUtils.act(() => {
+        ReactDOM.render(<Input type='textarea' name='myname' label='myfield' value='myvalue' onChange={(e) => {
+          expect(e.target.value).to.equal('newvalue');
+          done();
+        }}/>, container);
+      });
 
       let input = document.querySelector('textarea');
       input.value = 'newvalue'
-      input.dispatchEvent(new Event('change'))
+      ReactTestUtils.Simulate.change(input);
     });
   });
 });
-- 
cgit v1.2.3


From fce2434dcdef126a3ab180447e7cb48bc24ec1f9 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Tue, 30 Apr 2019 09:15:58 +0900
Subject: Specify default props

---
 src/settings/components/form/BlacklistForm.jsx  |  5 +----
 src/settings/components/form/KeymapsForm.jsx    |  5 +----
 src/settings/components/form/PropertiesForm.jsx | 12 +++++-------
 src/settings/components/form/SearchForm.jsx     |  5 +----
 4 files changed, 8 insertions(+), 19 deletions(-)

(limited to 'src/settings/components/form/BlacklistForm.jsx')

diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.jsx
index cee04bd..cf7cffe 100644
--- a/src/settings/components/form/BlacklistForm.jsx
+++ b/src/settings/components/form/BlacklistForm.jsx
@@ -25,10 +25,6 @@ class BlacklistForm extends React.Component {
   }
 
   bindValue(e) {
-    if (!this.props.onChange) {
-      return;
-    }
-
     let name = e.target.name;
     let index = e.target.getAttribute('data-index');
     let next = this.props.value ? this.props.value.slice() : [];
@@ -52,6 +48,7 @@ BlacklistForm.propTypes = {
 
 BlacklistForm.defaultProps = {
   value: [],
+  onChange: () => {},
 };
 
 export default BlacklistForm;
diff --git a/src/settings/components/form/KeymapsForm.jsx b/src/settings/components/form/KeymapsForm.jsx
index 26a22d7..5bb65cb 100644
--- a/src/settings/components/form/KeymapsForm.jsx
+++ b/src/settings/components/form/KeymapsForm.jsx
@@ -30,10 +30,6 @@ class KeymapsForm extends React.Component {
   }
 
   bindValue(e) {
-    if (!this.props.onChange) {
-      return;
-    }
-
     let next = { ...this.props.value };
     next[e.target.name] = e.target.value;
 
@@ -48,6 +44,7 @@ KeymapsForm.propTypes = {
 
 KeymapsForm.defaultProps = {
   value: {},
+  onChange: () => {},
 };
 
 export default KeymapsForm;
diff --git a/src/settings/components/form/PropertiesForm.jsx b/src/settings/components/form/PropertiesForm.jsx
index 9143a7a..0014899 100644
--- a/src/settings/components/form/PropertiesForm.jsx
+++ b/src/settings/components/form/PropertiesForm.jsx
@@ -7,9 +7,6 @@ class PropertiesForm extends React.Component {
   render() {
     let types = this.props.types;
     let value = this.props.value;
-    if (!value) {
-      value = {};
-    }
 
     return <div className='form-properties-form'>
       {
@@ -40,10 +37,6 @@ class PropertiesForm extends React.Component {
   }
 
   bindValue(e) {
-    if (!this.props.onChange) {
-      return;
-    }
-
     let name = e.target.name;
     let next = { ...this.props.value };
     if (e.target.type.toLowerCase() === 'checkbox') {
@@ -63,4 +56,9 @@ PropertiesForm.propTypes = {
   onChange: PropTypes.func,
 };
 
+PropertiesForm.defaultProps = {
+  value: {},
+  onChange: () => {},
+};
+
 export default PropertiesForm;
diff --git a/src/settings/components/form/SearchForm.jsx b/src/settings/components/form/SearchForm.jsx
index 2e070fc..d2f2d48 100644
--- a/src/settings/components/form/SearchForm.jsx
+++ b/src/settings/components/form/SearchForm.jsx
@@ -44,10 +44,6 @@ class SearchForm extends React.Component {
   }
 
   bindValue(e) {
-    if (!this.props.onChange) {
-      return;
-    }
-
     let value = this.props.value;
     let name = e.target.name;
     let index = e.target.getAttribute('data-index');
@@ -83,6 +79,7 @@ SearchForm.propTypes = {
 
 SearchForm.defaultProps = {
   value: { default: '', engines: []},
+  onChange: () => {},
 };
 
 export default SearchForm;
-- 
cgit v1.2.3


From 20f79f1da54441fe22a172009d34734af1bd3807 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Tue, 30 Apr 2019 09:49:45 +0900
Subject: Save settings by onBlur callback

---
 src/settings/actions/setting.js                 |  2 +-
 src/settings/components/form/BlacklistForm.jsx  | 13 +++++++++++--
 src/settings/components/form/KeymapsForm.jsx    |  1 +
 src/settings/components/form/PropertiesForm.jsx |  1 +
 src/settings/components/form/SearchForm.jsx     | 11 +++++++++--
 src/settings/components/index.jsx               | 11 +++++++++--
 6 files changed, 32 insertions(+), 7 deletions(-)

(limited to 'src/settings/components/form/BlacklistForm.jsx')

diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js
index 8844252..db63a45 100644
--- a/src/settings/actions/setting.js
+++ b/src/settings/actions/setting.js
@@ -60,4 +60,4 @@ const set = (settings) => {
   };
 };
 
-export { load, save, switchToForm, switchToJson };
+export { load, save, set, switchToForm, switchToJson };
diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.jsx
index cf7cffe..c470758 100644
--- a/src/settings/components/form/BlacklistForm.jsx
+++ b/src/settings/components/form/BlacklistForm.jsx
@@ -13,9 +13,13 @@ class BlacklistForm extends React.Component {
           return <div key={index} className='form-blacklist-form-row'>
             <input data-index={index} type='text' name='url'
               className='column-url' value={url}
-              onChange={this.bindValue.bind(this)} />
+              onChange={this.bindValue.bind(this)}
+              onBlur={this.props.onBlur}
+            />
             <DeleteButton data-index={index} name='delete'
-              onClick={this.bindValue.bind(this)} />
+              onClick={this.bindValue.bind(this)}
+              onBlur={this.props.onBlur}
+            />
           </div>;
         })
       }
@@ -38,17 +42,22 @@ class BlacklistForm extends React.Component {
     }
 
     this.props.onChange(next);
+    if (name === 'delete') {
+      this.props.onBlur();
+    }
   }
 }
 
 BlacklistForm.propTypes = {
   value: PropTypes.arrayOf(PropTypes.string),
   onChange: PropTypes.func,
+  onBlur: PropTypes.func,
 };
 
 BlacklistForm.defaultProps = {
   value: [],
   onChange: () => {},
+  onBlur: () => {},
 };
 
 export default BlacklistForm;
diff --git a/src/settings/components/form/KeymapsForm.jsx b/src/settings/components/form/KeymapsForm.jsx
index 5bb65cb..01acf61 100644
--- a/src/settings/components/form/KeymapsForm.jsx
+++ b/src/settings/components/form/KeymapsForm.jsx
@@ -20,6 +20,7 @@ class KeymapsForm extends React.Component {
                   type='text' id={name} name={name} key={name}
                   label={label} value={value}
                   onChange={this.bindValue.bind(this)}
+                  onBlur={this.props.onBlur}
                 />;
               })
             }
diff --git a/src/settings/components/form/PropertiesForm.jsx b/src/settings/components/form/PropertiesForm.jsx
index 0014899..979fdd8 100644
--- a/src/settings/components/form/PropertiesForm.jsx
+++ b/src/settings/components/form/PropertiesForm.jsx
@@ -27,6 +27,7 @@ class PropertiesForm extends React.Component {
                 className='column-input'
                 value={value[name] ? value[name] : ''}
                 onChange={this.bindValue.bind(this)}
+                onBlur={this.props.onBlur}
                 checked={value[name]}
               />
             </label>
diff --git a/src/settings/components/form/SearchForm.jsx b/src/settings/components/form/SearchForm.jsx
index d2f2d48..6b0bd01 100644
--- a/src/settings/components/form/SearchForm.jsx
+++ b/src/settings/components/form/SearchForm.jsx
@@ -23,11 +23,15 @@ class SearchForm extends React.Component {
           return <div key={index} className='form-search-form-row'>
             <input data-index={index} type='text' name='name'
               className='column-name' value={engine[0]}
-              onChange={this.bindValue.bind(this)} />
+              onChange={this.bindValue.bind(this)}
+              onBlur={this.props.onBlur}
+            />
             <input data-index={index} type='text' name='url'
               placeholder='http://example.com/?q={}'
               className='column-url' value={engine[1]}
-              onChange={this.bindValue.bind(this)} />
+              onChange={this.bindValue.bind(this)}
+              onBlur={this.props.onBlur}
+            />
             <div className='column-option'>
               <input data-index={index} type='radio' name='default'
                 checked={value.default === engine[0]}
@@ -66,6 +70,9 @@ class SearchForm extends React.Component {
     }
 
     this.props.onChange(next);
+    if (name === 'delete' || name === 'default') {
+      this.props.onBlur();
+    }
   }
 }
 
diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx
index affa2e9..4ef59d7 100644
--- a/src/settings/components/index.jsx
+++ b/src/settings/components/index.jsx
@@ -25,6 +25,7 @@ class SettingsComponent extends React.Component {
         <KeymapsForm
           value={form.keymaps}
           onChange={value => this.bindForm('keymaps', value)}
+          onBlur={this.save.bind(this)}
         />
       </fieldset>
       <fieldset>
@@ -32,6 +33,7 @@ class SettingsComponent extends React.Component {
         <SearchForm
           value={form.search}
           onChange={value => this.bindForm('search', value)}
+          onBlur={this.save.bind(this)}
         />
       </fieldset>
       <fieldset>
@@ -39,6 +41,7 @@ class SettingsComponent extends React.Component {
         <BlacklistForm
           value={form.blacklist}
           onChange={value => this.bindForm('blacklist', value)}
+          onBlur={this.save.bind(this)}
         />
       </fieldset>
       <fieldset>
@@ -47,6 +50,7 @@ class SettingsComponent extends React.Component {
           types={properties.types}
           value={form.properties}
           onChange={value => this.bindForm('properties', value)}
+          onBlur={this.save.bind(this)}
         />
       </fieldset>
     </div>;
@@ -61,6 +65,7 @@ class SettingsComponent extends React.Component {
         spellCheck='false'
         error={error}
         onChange={this.bindJson.bind(this)}
+        onBlur={this.save.bind(this)}
         value={json}
       />
     </div>;
@@ -109,7 +114,7 @@ class SettingsComponent extends React.Component {
       form: { ...this.props.form },
     };
     settings.form[name] = value;
-    this.props.dispatch(settingActions.save(settings));
+    this.props.dispatch(settingActions.set(settings));
   }
 
   bindJson(e) {
@@ -118,7 +123,7 @@ class SettingsComponent extends React.Component {
       json: e.target.value,
       form: this.props.form,
     };
-    this.props.dispatch(settingActions.save(settings));
+    this.props.dispatch(settingActions.set(settings));
   }
 
   bindSource(e) {
@@ -135,7 +140,9 @@ class SettingsComponent extends React.Component {
       }
       this.props.dispatch(settingActions.switchToForm(this.props.json));
     }
+  }
 
+  save() {
     let settings = this.props.store.getState();
     this.props.dispatch(settingActions.save(settings));
   }
-- 
cgit v1.2.3

