Merge pull request #576 from ueokande/move-to-react

Move to React
jh-changes
Shin'ya Ueoka 6 years ago committed by GitHub
commit 457d954e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      karma.conf.js
  2. 1592
      package-lock.json
  3. 14
      package.json
  4. 6
      src/background/domains/Completions.js
  5. 54
      src/console/components/Console.jsx
  6. 51
      src/console/components/console/Completion.jsx
  7. 28
      src/console/components/console/CompletionItem.jsx
  8. 14
      src/console/components/console/CompletionTitle.jsx
  9. 17
      src/console/components/console/Input.jsx
  10. 13
      src/console/components/console/Message.jsx
  11. 4
      src/console/index.html
  12. 14
      src/console/index.jsx
  13. 7
      src/settings/actions/setting.js
  14. 47
      src/settings/components/form/BlacklistForm.jsx
  15. 0
      src/settings/components/form/BlacklistForm.scss
  16. 51
      src/settings/components/form/KeymapsForm.jsx
  17. 0
      src/settings/components/form/KeymapsForm.scss
  18. 25
      src/settings/components/form/PropertiesForm.jsx
  19. 0
      src/settings/components/form/PropertiesForm.scss
  20. 44
      src/settings/components/form/SearchForm.jsx
  21. 0
      src/settings/components/form/SearchForm.scss
  22. 29
      src/settings/components/index.jsx
  23. 6
      src/settings/components/ui/AddButton.jsx
  24. 0
      src/settings/components/ui/AddButton.scss
  25. 6
      src/settings/components/ui/DeleteButton.jsx
  26. 0
      src/settings/components/ui/DeleteButton.scss
  27. 14
      src/settings/components/ui/Input.jsx
  28. 0
      src/settings/components/ui/Input.scss
  29. 9
      src/settings/index.jsx
  30. 56
      src/settings/keymaps.js
  31. 168
      test/console/components/console/Completion.test.jsx
  32. 138
      test/console/components/console/completion.test.jsx
  33. 92
      test/settings/components/form/BlacklistForm.test.jsx
  34. 64
      test/settings/components/form/KeymapsForm.test.jsx
  35. 104
      test/settings/components/form/PropertiesForm.test.jsx
  36. 128
      test/settings/components/form/SearchEngineForm.test.jsx
  37. 81
      test/settings/components/form/blacklist-form.test.jsx
  38. 52
      test/settings/components/form/keymaps-form.test.jsx
  39. 85
      test/settings/components/form/properties-form.test.jsx
  40. 103
      test/settings/components/form/search-engine-form.test.jsx
  41. 59
      test/settings/components/ui/input.test.jsx
  42. 2
      webpack.config.js

@ -29,6 +29,7 @@ module.exports = function (config) {
singleRun: true, singleRun: true,
webpack: { webpack: {
mode: 'development',
devtool: 'inline-source-map', devtool: 'inline-source-map',
resolve: webpackConfig.resolve, resolve: webpackConfig.resolve,
module: webpackConfig.module module: webpackConfig.module

1592
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -20,11 +20,11 @@
}, },
"homepage": "https://github.com/ueokande/vim-vixen", "homepage": "https://github.com/ueokande/vim-vixen",
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5", "babel-loader": "^8.0.5",
"babel-preset-preact": "^1.1.0",
"babel-preset-stage-2": "^6.24.1",
"chai": "^4.2.0", "chai": "^4.2.0",
"css-loader": "^2.1.1", "css-loader": "^2.1.1",
"eslint": "^5.16.0", "eslint": "^5.16.0",
@ -42,8 +42,10 @@
"lanthan": "git+https://github.com/ueokande/lanthan.git#master", "lanthan": "git+https://github.com/ueokande/lanthan.git#master",
"mocha": "^6.1.4", "mocha": "^6.1.4",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"preact": "^8.4.2", "react": "^16.8.6",
"preact-redux": "^2.0.3", "react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"react-test-renderer": "^16.8.6",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-promise": "^0.6.0", "redux-promise": "^0.6.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",

@ -19,9 +19,9 @@ export default class Completions {
})); }));
} }
static EMPTY_COMPLETIONS = new Completions([]);
static empty() { static empty() {
return Completions.EMPTY_COMPLETIONS; return EMPTY_COMPLETIONS;
} }
} }
let EMPTY_COMPLETIONS = new Completions([]);

@ -1,17 +1,18 @@
import './console.scss'; import './console.scss';
import { connect } from 'preact-redux'; import { connect } from 'react-redux';
import { Component, h } from 'preact'; import React from 'react';
import Input from './console/input'; import PropTypes from 'prop-types';
import Completion from './console/completion'; import Input from './console/Input';
import Message from './console/message'; import Completion from './console/Completion';
import Message from './console/Message';
import * as consoleActions from '../../console/actions/console'; import * as consoleActions from '../../console/actions/console';
const COMPLETION_MAX_ITEMS = 33; const COMPLETION_MAX_ITEMS = 33;
class ConsoleComponent extends Component { class Console extends React.Component {
onBlur() { onBlur() {
if (this.props.mode === 'command' || this.props.mode === 'find') { if (this.props.mode === 'command' || this.props.mode === 'find') {
return this.context.store.dispatch(consoleActions.hideCommand()); return this.props.dispatch(consoleActions.hideCommand());
} }
} }
@ -21,45 +22,45 @@ class ConsoleComponent extends Component {
let value = e.target.value; let value = e.target.value;
if (this.props.mode === 'command') { if (this.props.mode === 'command') {
return this.context.store.dispatch(consoleActions.enterCommand(value)); return this.props.dispatch(consoleActions.enterCommand(value));
} else if (this.props.mode === 'find') { } else if (this.props.mode === 'find') {
return this.context.store.dispatch(consoleActions.enterFind(value)); return this.props.dispatch(consoleActions.enterFind(value));
} }
} }
selectNext(e) { selectNext(e) {
this.context.store.dispatch(consoleActions.completionNext()); this.props.dispatch(consoleActions.completionNext());
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
selectPrev(e) { selectPrev(e) {
this.context.store.dispatch(consoleActions.completionPrev()); this.props.dispatch(consoleActions.completionPrev());
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
onKeyDown(e) { onKeyDown(e) {
if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) { if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) {
this.context.store.dispatch(consoleActions.hideCommand()); this.props.dispatch(consoleActions.hideCommand());
} }
switch (e.keyCode) { switch (e.keyCode) {
case KeyboardEvent.DOM_VK_ESCAPE: case KeyboardEvent.DOM_VK_ESCAPE:
return this.context.store.dispatch(consoleActions.hideCommand()); return this.props.dispatch(consoleActions.hideCommand());
case KeyboardEvent.DOM_VK_RETURN: case KeyboardEvent.DOM_VK_RETURN:
return this.doEnter(e); return this.doEnter(e);
case KeyboardEvent.DOM_VK_TAB: case KeyboardEvent.DOM_VK_TAB:
if (e.shiftKey) { if (e.shiftKey) {
this.context.store.dispatch(consoleActions.completionPrev()); this.props.dispatch(consoleActions.completionPrev());
} else { } else {
this.context.store.dispatch(consoleActions.completionNext()); this.props.dispatch(consoleActions.completionNext());
} }
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
break; break;
case KeyboardEvent.DOM_VK_OPEN_BRACKET: case KeyboardEvent.DOM_VK_OPEN_BRACKET:
if (e.ctrlKey) { if (e.ctrlKey) {
return this.context.store.dispatch(consoleActions.hideCommand()); return this.props.dispatch(consoleActions.hideCommand());
} }
break; break;
case KeyboardEvent.DOM_VK_M: case KeyboardEvent.DOM_VK_M:
@ -80,11 +81,11 @@ class ConsoleComponent extends Component {
} }
} }
onInput(e) { onChange(e) {
let text = e.target.value; let text = e.target.value;
this.context.store.dispatch(consoleActions.setConsoleText(text)); this.props.dispatch(consoleActions.setConsoleText(text));
if (this.props.mode === 'command') { if (this.props.mode === 'command') {
this.context.store.dispatch(consoleActions.getCompletions(text)); this.props.dispatch(consoleActions.getCompletions(text));
} }
} }
@ -94,7 +95,7 @@ class ConsoleComponent extends Component {
return; return;
} }
if (prevProps.mode !== 'command' && this.props.mode === 'command') { if (prevProps.mode !== 'command' && this.props.mode === 'command') {
this.context.store.dispatch( this.props.dispatch(
consoleActions.getCompletions(this.props.consoleText)); consoleActions.getCompletions(this.props.consoleText));
this.focus(); this.focus();
} else if (prevProps.mode !== 'find' && this.props.mode === 'find') { } else if (prevProps.mode !== 'find' && this.props.mode === 'find') {
@ -117,7 +118,7 @@ class ConsoleComponent extends Component {
mode={this.props.mode} mode={this.props.mode}
onBlur={this.onBlur.bind(this)} onBlur={this.onBlur.bind(this)}
onKeyDown={this.onKeyDown.bind(this)} onKeyDown={this.onKeyDown.bind(this)}
onInput={this.onInput.bind(this)} onChange={this.onChange.bind(this)}
value={this.props.consoleText} value={this.props.consoleText}
/> />
</div>; </div>;
@ -126,6 +127,8 @@ class ConsoleComponent extends Component {
return <Message mode={ this.props.mode } > return <Message mode={ this.props.mode } >
{ this.props.messageText } { this.props.messageText }
</Message>; </Message>;
default:
return null;
} }
} }
@ -135,5 +138,12 @@ class ConsoleComponent extends Component {
} }
} }
Console.propTypes = {
mode: PropTypes.string,
consoleText: PropTypes.string,
messageText: PropTypes.string,
children: PropTypes.string,
};
const mapStateToProps = state => state; const mapStateToProps = state => state;
export default connect(mapStateToProps)(ConsoleComponent); export default connect(mapStateToProps)(Console);

@ -1,29 +1,9 @@
import { Component, h } from 'preact'; import React from 'react';
import PropTypes from 'prop-types';
import CompletionItem from './CompletionItem';
import CompletionTitle from './CompletionTitle';
const CompletionTitle = (props) => { class Completion extends React.Component {
return <li className='vimvixen-console-completion-title' >{props.title}</li>;
};
const CompletionItem = (props) => {
let className = 'vimvixen-console-completion-item';
if (props.highlight) {
className += ' vimvixen-completion-selected';
}
return <li
className={className}
style={{ backgroundImage: 'url(' + props.icon + ')' }}
>
<span
className='vimvixen-console-completion-item-caption'
>{props.caption}</span>
<span
className='vimvixen-console-completion-item-url'
>{props.url}</span>
</li>;
};
class CompletionComponent extends Component {
constructor() { constructor() {
super(); super();
this.state = { viewOffset: 0, select: -1 }; this.state = { viewOffset: 0, select: -1 };
@ -63,9 +43,13 @@ class CompletionComponent extends Component {
let index = 0; let index = 0;
for (let group of this.props.completions) { for (let group of this.props.completions) {
eles.push(<CompletionTitle title={ group.name }/>); eles.push(<CompletionTitle
key={`group-${index}`}
title={ group.name }
/>);
for (let item of group.items) { for (let item of group.items) {
eles.push(<CompletionItem eles.push(<CompletionItem
key={`item-${index}`}
icon={item.icon} icon={item.icon}
caption={item.caption} caption={item.caption}
url={item.url} url={item.url}
@ -86,4 +70,17 @@ class CompletionComponent extends Component {
} }
} }
export default CompletionComponent; Completion.propTypes = {
select: PropTypes.number,
size: PropTypes.number,
completions: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string,
caption: PropTypes.string,
url: PropTypes.string,
})),
})),
};
export default Completion;

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
const CompletionItem = (props) => {
let className = 'vimvixen-console-completion-item';
if (props.highlight) {
className += ' vimvixen-completion-selected';
}
return <li
className={className}
style={{ backgroundImage: 'url(' + props.icon + ')' }}
>
<span
className='vimvixen-console-completion-item-caption'
>{props.caption}</span>
<span
className='vimvixen-console-completion-item-url'
>{props.url}</span>
</li>;
};
CompletionItem.propTypes = {
highlight: PropTypes.bool,
caption: PropTypes.string,
url: PropTypes.string,
};
export default CompletionItem;

@ -0,0 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
const CompletionTitle = (props) => {
return <li className='vimvixen-console-completion-title'>
{props.title}
</li>;
};
CompletionTitle.propTypes = {
title: PropTypes.string,
};
export default CompletionTitle;

@ -1,6 +1,7 @@
import { Component, h } from 'preact'; import React from 'react';
import PropTypes from 'prop-types';
export default class InputComponent extends Component { class Input extends React.Component {
focus() { focus() {
this.input.focus(); this.input.focus();
} }
@ -23,10 +24,20 @@ export default class InputComponent extends Component {
ref={(c) => { this.input = c; }} ref={(c) => { this.input = c; }}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
onKeyDown={this.props.onKeyDown} onKeyDown={this.props.onKeyDown}
onInput={this.props.onInput} onChange={this.props.onChange}
value={this.props.value} value={this.props.value}
/> />
</div> </div>
); );
} }
} }
Input.propTypes = {
mode: PropTypes.string,
value: PropTypes.string,
onBlur: PropTypes.func,
onKeyDown: PropTypes.func,
onChange: PropTypes.func,
};
export default Input;

@ -1,6 +1,7 @@
import { h } from 'preact'; import React from 'react';
import PropTypes from 'prop-types';
export default function Message(props) { const Message = (props) => {
switch (props.mode) { switch (props.mode) {
case 'error': case 'error':
return ( return (
@ -15,4 +16,10 @@ export default function Message(props) {
</p> </p>
); );
} }
} };
Message.propTypes = {
children: PropTypes.string,
};
export default Message;

@ -5,5 +5,7 @@
<title>VimVixen console</title> <title>VimVixen console</title>
<script src='console.js'></script> <script src='console.js'></script>
</head> </head>
<body class='vimvixen-console'></body> <body>
<div id='vimvixen-console' class='vimvixen-console'></div>
</body>
</html> </html>

@ -3,11 +3,10 @@ import reducers from 'console/reducers';
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware } from 'redux';
import promise from 'redux-promise'; import promise from 'redux-promise';
import * as consoleActions from 'console/actions/console'; import * as consoleActions from 'console/actions/console';
import { Provider } from 'react-redux';
import { Provider } from 'preact-redux'; import Console from './components/Console';
import Console from './components/console'; import React from 'react';
import ReactDOM from 'react-dom';
import { render, h } from 'preact';
const store = createStore( const store = createStore(
reducers, reducers,
@ -15,11 +14,12 @@ const store = createStore(
); );
window.addEventListener('load', () => { window.addEventListener('load', () => {
render( let wrapper = document.getElementById('vimvixen-console');
ReactDOM.render(
<Provider store={store} > <Provider store={store} >
<Console></Console> <Console></Console>
</Provider>, </Provider>,
document.body); wrapper);
}); });
const onMessage = (message) => { const onMessage = (message) => {

@ -1,8 +1,8 @@
import actions from 'settings/actions'; import actions from 'settings/actions';
import * as validator from 'shared/settings/validator'; import * as validator from 'shared/settings/validator';
import KeymapsForm from '../components/form/keymaps-form';
import * as settingsValues from 'shared/settings/values'; import * as settingsValues from 'shared/settings/values';
import * as settingsStorage from 'shared/settings/storage'; import * as settingsStorage from 'shared/settings/storage';
import keymaps from '../keymaps';
const load = async() => { const load = async() => {
let settings = await settingsStorage.loadRaw(); let settings = await settingsStorage.loadRaw();
@ -29,8 +29,7 @@ const save = async(settings) => {
const switchToForm = (json) => { const switchToForm = (json) => {
try { try {
validator.validate(JSON.parse(json)); validator.validate(JSON.parse(json));
// AllowdOps filters operations, this is dirty dependency let form = settingsValues.formFromJson(json, keymaps.allowedOps);
let form = settingsValues.formFromJson(json, KeymapsForm.AllowdOps);
return { return {
type: actions.SETTING_SWITCH_TO_FORM, type: actions.SETTING_SWITCH_TO_FORM,
form, form,
@ -61,4 +60,4 @@ const set = (settings) => {
}; };
}; };
export { load, save, switchToForm, switchToJson }; export { load, save, set, switchToForm, switchToJson };

@ -1,38 +1,34 @@
import './blacklist-form.scss'; import './BlacklistForm.scss';
import AddButton from '../ui/add-button'; import AddButton from '../ui/AddButton';
import DeleteButton from '../ui/delete-button'; import DeleteButton from '../ui/DeleteButton';
import { h, Component } from 'preact'; import React from 'react';
import PropTypes from 'prop-types';
class BlacklistForm extends Component { class BlacklistForm extends React.Component {
render() { render() {
let value = this.props.value;
if (!value) {
value = [];
}
return <div className='form-blacklist-form'> 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'> return <div key={index} className='form-blacklist-form-row'>
<input data-index={index} type='text' name='url' <input data-index={index} type='text' name='url'
className='column-url' value={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' <DeleteButton data-index={index} name='delete'
onClick={this.bindValue.bind(this)} /> onClick={this.bindValue.bind(this)}
onBlur={this.props.onBlur}
/>
</div>; </div>;
}) })
} }
<AddButton name='add' style='float:right' <AddButton name='add' style={{ float: 'right' }}
onClick={this.bindValue.bind(this)} /> onClick={this.bindValue.bind(this)} />
</div>; </div>;
} }
bindValue(e) { bindValue(e) {
if (!this.props.onChange) {
return;
}
let name = e.target.name; let name = e.target.name;
let index = e.target.getAttribute('data-index'); let index = e.target.getAttribute('data-index');
let next = this.props.value ? this.props.value.slice() : []; let next = this.props.value ? this.props.value.slice() : [];
@ -46,7 +42,22 @@ class BlacklistForm extends Component {
} }
this.props.onChange(next); 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; export default BlacklistForm;

@ -0,0 +1,51 @@
import './KeymapsForm.scss';
import React from 'react';
import PropTypes from 'prop-types';
import Input from '../ui/Input';
import keymaps from '../../keymaps';
class KeymapsForm extends React.Component {
render() {
return <div className='form-keymaps-form'>
{
keymaps.fields.map((group, index) => {
return <div key={index} className='form-keymaps-form-field-group'>
{
group.map((field) => {
let name = field[0];
let label = field[1];
let value = this.props.value[name] || '';
return <Input
type='text' id={name} name={name} key={name}
label={label} value={value}
onChange={this.bindValue.bind(this)}
onBlur={this.props.onBlur}
/>;
})
}
</div>;
})
}
</div>;
}
bindValue(e) {
let next = { ...this.props.value };
next[e.target.name] = e.target.value;
this.props.onChange(next);
}
}
KeymapsForm.propTypes = {
value: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func,
};
KeymapsForm.defaultProps = {
value: {},
onChange: () => {},
};
export default KeymapsForm;

@ -1,14 +1,12 @@
import './properties-form.scss'; import './PropertiesForm.scss';
import { h, Component } from 'preact'; import React from 'react';
import PropTypes from 'prop-types';
class PropertiesForm extends Component { class PropertiesForm extends React.Component {
render() { render() {
let types = this.props.types; let types = this.props.types;
let value = this.props.value; let value = this.props.value;
if (!value) {
value = {};
}
return <div className='form-properties-form'> return <div className='form-properties-form'>
{ {
@ -29,6 +27,7 @@ class PropertiesForm extends Component {
className='column-input' className='column-input'
value={value[name] ? value[name] : ''} value={value[name] ? value[name] : ''}
onChange={this.bindValue.bind(this)} onChange={this.bindValue.bind(this)}
onBlur={this.props.onBlur}
checked={value[name]} checked={value[name]}
/> />
</label> </label>
@ -39,10 +38,6 @@ class PropertiesForm extends Component {
} }
bindValue(e) { bindValue(e) {
if (!this.props.onChange) {
return;
}
let name = e.target.name; let name = e.target.name;
let next = { ...this.props.value }; let next = { ...this.props.value };
if (e.target.type.toLowerCase() === 'checkbox') { if (e.target.type.toLowerCase() === 'checkbox') {
@ -57,4 +52,14 @@ class PropertiesForm extends Component {
} }
} }
PropertiesForm.propTypes = {
value: PropTypes.objectOf(PropTypes.any),
onChange: PropTypes.func,
};
PropertiesForm.defaultProps = {
value: {},
onChange: () => {},
};
export default PropertiesForm; export default PropertiesForm;

@ -1,15 +1,13 @@
import './search-form.scss'; import './SearchForm.scss';
import { h, Component } from 'preact'; import React from 'react';
import AddButton from '../ui/add-button'; import PropTypes from 'prop-types';
import DeleteButton from '../ui/delete-button'; import AddButton from '../ui/AddButton';
import DeleteButton from '../ui/DeleteButton';
class SearchForm extends Component { class SearchForm extends React.Component {
render() { render() {
let value = this.props.value; let value = this.props.value;
if (!value) {
value = { default: '', engines: []};
}
if (!value.engines) { if (!value.engines) {
value.engines = []; value.engines = [];
} }
@ -25,11 +23,15 @@ class SearchForm extends Component {
return <div key={index} className='form-search-form-row'> return <div key={index} className='form-search-form-row'>
<input data-index={index} type='text' name='name' <input data-index={index} type='text' name='name'
className='column-name' value={engine[0]} 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' <input data-index={index} type='text' name='url'
placeholder='http://example.com/?q={}' placeholder='http://example.com/?q={}'
className='column-url' value={engine[1]} className='column-url' value={engine[1]}
onChange={this.bindValue.bind(this)} /> onChange={this.bindValue.bind(this)}
onBlur={this.props.onBlur}
/>
<div className='column-option'> <div className='column-option'>
<input data-index={index} type='radio' name='default' <input data-index={index} type='radio' name='default'
checked={value.default === engine[0]} checked={value.default === engine[0]}
@ -40,16 +42,12 @@ class SearchForm extends Component {
</div>; </div>;
}) })
} }
<AddButton name='add' style='float:right' <AddButton name='add' style={{ float: 'right' }}
onClick={this.bindValue.bind(this)} /> onClick={this.bindValue.bind(this)} />
</div>; </div>;
} }
bindValue(e) { bindValue(e) {
if (!this.props.onChange) {
return;
}
let value = this.props.value; let value = this.props.value;
let name = e.target.name; let name = e.target.name;
let index = e.target.getAttribute('data-index'); let index = e.target.getAttribute('data-index');
@ -72,7 +70,23 @@ class SearchForm extends Component {
} }
this.props.onChange(next); this.props.onChange(next);
if (name === 'delete' || name === 'default') {
this.props.onBlur();
} }
} }
}
SearchForm.propTypes = {
value: PropTypes.shape({
default: PropTypes.string,
engines: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
}),
onChange: PropTypes.func,
};
SearchForm.defaultProps = {
value: { default: '', engines: []},
onChange: () => {},
};
export default SearchForm; export default SearchForm;

@ -1,11 +1,11 @@
import './site.scss'; import './site.scss';
import { h, Component } from 'preact'; import React from 'react';
import { connect } from 'preact-redux'; import { connect } from 'react-redux';
import Input from './ui/input'; import Input from './ui/Input';
import SearchForm from './form/search-form'; import SearchForm from './form/SearchForm';
import KeymapsForm from './form/keymaps-form'; import KeymapsForm from './form/KeymapsForm';
import BlacklistForm from './form/blacklist-form'; import BlacklistForm from './form/BlacklistForm';
import PropertiesForm from './form/properties-form'; import PropertiesForm from './form/PropertiesForm';
import * as properties from 'shared/settings/properties'; import * as properties from 'shared/settings/properties';
import * as settingActions from 'settings/actions/setting'; import * as settingActions from 'settings/actions/setting';
@ -13,7 +13,7 @@ const DO_YOU_WANT_TO_CONTINUE =
'Some settings in JSON can be lost when migrating. ' + 'Some settings in JSON can be lost when migrating. ' +
'Do you want to continue?'; 'Do you want to continue?';
class SettingsComponent extends Component { class SettingsComponent extends React.Component {
componentDidMount() { componentDidMount() {
this.props.dispatch(settingActions.load()); this.props.dispatch(settingActions.load());
} }
@ -25,6 +25,7 @@ class SettingsComponent extends Component {
<KeymapsForm <KeymapsForm
value={form.keymaps} value={form.keymaps}
onChange={value => this.bindForm('keymaps', value)} onChange={value => this.bindForm('keymaps', value)}
onBlur={this.save.bind(this)}
/> />
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -32,6 +33,7 @@ class SettingsComponent extends Component {
<SearchForm <SearchForm
value={form.search} value={form.search}
onChange={value => this.bindForm('search', value)} onChange={value => this.bindForm('search', value)}
onBlur={this.save.bind(this)}
/> />
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -39,6 +41,7 @@ class SettingsComponent extends Component {
<BlacklistForm <BlacklistForm
value={form.blacklist} value={form.blacklist}
onChange={value => this.bindForm('blacklist', value)} onChange={value => this.bindForm('blacklist', value)}
onBlur={this.save.bind(this)}
/> />
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -47,6 +50,7 @@ class SettingsComponent extends Component {
types={properties.types} types={properties.types}
value={form.properties} value={form.properties}
onChange={value => this.bindForm('properties', value)} onChange={value => this.bindForm('properties', value)}
onBlur={this.save.bind(this)}
/> />
</fieldset> </fieldset>
</div>; </div>;
@ -61,6 +65,7 @@ class SettingsComponent extends Component {
spellCheck='false' spellCheck='false'
error={error} error={error}
onChange={this.bindJson.bind(this)} onChange={this.bindJson.bind(this)}
onBlur={this.save.bind(this)}
value={json} value={json}
/> />
</div>; </div>;
@ -109,7 +114,7 @@ class SettingsComponent extends Component {
form: { ...this.props.form }, form: { ...this.props.form },
}; };
settings.form[name] = value; settings.form[name] = value;
this.props.dispatch(settingActions.save(settings)); this.props.dispatch(settingActions.set(settings));
} }
bindJson(e) { bindJson(e) {
@ -118,7 +123,7 @@ class SettingsComponent extends Component {
json: e.target.value, json: e.target.value,
form: this.props.form, form: this.props.form,
}; };
this.props.dispatch(settingActions.save(settings)); this.props.dispatch(settingActions.set(settings));
} }
bindSource(e) { bindSource(e) {
@ -135,8 +140,10 @@ class SettingsComponent extends Component {
} }
this.props.dispatch(settingActions.switchToForm(this.props.json)); this.props.dispatch(settingActions.switchToForm(this.props.json));
} }
}
let settings = this.context.store.getState(); save() {
let settings = this.props.store.getState();
this.props.dispatch(settingActions.save(settings)); this.props.dispatch(settingActions.save(settings));
} }
} }

@ -1,7 +1,7 @@
import './add-button.scss'; import './AddButton.scss';
import { h, Component } from 'preact'; import React from 'react';
class AddButton extends Component { class AddButton extends React.Component {
render() { render() {
return <input return <input
className='ui-add-button' type='button' value='&#x271a;' className='ui-add-button' type='button' value='&#x271a;'

@ -1,7 +1,7 @@
import './delete-button.scss'; import './DeleteButton.scss';
import { h, Component } from 'preact'; import React from 'react';
class DeleteButton extends Component { class DeleteButton extends React.Component {
render() { render() {
return <input return <input
className='ui-delete-button' type='button' value='&#x2716;' className='ui-delete-button' type='button' value='&#x2716;'

@ -1,7 +1,8 @@
import { h, Component } from 'preact'; import React from 'react';
import './input.scss'; import PropTypes from 'prop-types';
import './Input.scss';
class Input extends Component { class Input extends React.Component {
renderText(props) { renderText(props) {
let inputClassName = props.error ? 'input-error' : ''; let inputClassName = props.error ? 'input-error' : '';
@ -49,4 +50,11 @@ class Input extends Component {
} }
} }
Input.propTypes = {
type: PropTypes.string,
error: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
};
export default Input; export default Input;

@ -1,7 +1,8 @@
import { h, render } from 'preact'; import React from 'react';
import ReactDOM from 'react-dom';
import SettingsComponent from './components'; import SettingsComponent from './components';
import reducer from './reducers/setting'; import reducer from './reducers/setting';
import { Provider } from 'preact-redux'; import { Provider } from 'react-redux';
import promise from 'redux-promise'; import promise from 'redux-promise';
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware } from 'redux';
@ -12,9 +13,9 @@ const store = createStore(
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
let wrapper = document.getElementById('vimvixen-settings'); let wrapper = document.getElementById('vimvixen-settings');
render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<SettingsComponent /> <SettingsComponent store={store} />
</Provider>, </Provider>,
wrapper wrapper
); );

@ -1,8 +1,4 @@
import './keymaps-form.scss'; const fields = [
import { h, Component } from 'preact';
import Input from '../ui/input';
const KeyMapFields = [
[ [
['scroll.vertically?{"count":1}', 'Scroll down'], ['scroll.vertically?{"count":1}', 'Scroll down'],
['scroll.vertically?{"count":-1}', 'Scroll up'], ['scroll.vertically?{"count":-1}', 'Scroll up'],
@ -70,49 +66,9 @@ const KeyMapFields = [
] ]
]; ];
const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0]))); const allowedOps = [].concat(...fields.map(group => group.map(e => e[0])));
class KeymapsForm extends Component {
render() {
let values = this.props.value;
if (!values) {
values = {};
}
return <div className='form-keymaps-form'>
{
KeyMapFields.map((group, index) => {
return <div key={index} className='form-keymaps-form-field-group'>
{
group.map((field) => {
let name = field[0];
let label = field[1];
let value = values[name];
return <Input
type='text' id={name} name={name} key={name}
label={label} value={value}
onChange={this.bindValue.bind(this)}
/>;
})
}
</div>;
})
}
</div>;
}
bindValue(e) {
if (!this.props.onChange) {
return;
}
let next = { ...this.props.value };
next[e.target.name] = e.target.value;
this.props.onChange(next);
}
}
KeymapsForm.AllowdOps = AllowdOps;
export default KeymapsForm; export default {
fields,
allowedOps,
};

@ -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;
});
});

@ -1,138 +0,0 @@
import { h, render } from 'preact';
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');
});
});

@ -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);
});
});
});

@ -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);
});
});
});

@ -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);
});
});
});

@ -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);
});
});
});

@ -1,81 +0,0 @@
import { h, render } from 'preact';
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();
});
});
});

@ -1,52 +0,0 @@
import { h, render } from 'preact';
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'))
});
});
});

@ -1,85 +0,0 @@
import { h, render } from 'preact';
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();
});
});
});

@ -1,103 +0,0 @@
import { h, render } from 'preact';
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();
});
});
});

@ -1,14 +1,28 @@
import { h, render } from 'preact'; import React from 'react';
import Input from 'settings/components/ui/input' import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import Input from 'settings/components/ui/Input'
describe("settings/ui/Input", () => { describe("settings/ui/Input", () => {
let container;
beforeEach(() => { beforeEach(() => {
document.body.innerHTML = ''; container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
}); });
context("type=text", () => { context("type=text", () => {
it('renders text input', () => { 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 label = document.querySelector('label');
let input = document.querySelector('input'); let input = document.querySelector('input');
@ -19,20 +33,26 @@ describe("settings/ui/Input", () => {
}); });
it('invoke onChange', (done) => { it('invoke onChange', (done) => {
render(<Input type='text' name='myname' label='myfield' value='myvalue' onChange={(e) => { ReactTestUtils.act(() => {
ReactDOM.render(<Input type='text' name='myname' label='myfield' value='myvalue' onChange={(e) => {
expect(e.target.value).to.equal('newvalue'); expect(e.target.value).to.equal('newvalue');
done(); done();
}}/>, document.body); }}/>, container);
});
let input = document.querySelector('input'); let input = document.querySelector('input');
input.value = 'newvalue'; input.value = 'newvalue';
input.dispatchEvent(new Event('change')) ReactTestUtils.Simulate.change(input);
}); });
}); });
context("type=radio", () => { context("type=radio", () => {
it('renders radio button', () => { 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 label = document.querySelector('label');
let input = document.querySelector('input'); let input = document.querySelector('input');
@ -43,20 +63,27 @@ describe("settings/ui/Input", () => {
}); });
it('invoke onChange', (done) => { it('invoke onChange', (done) => {
render(<Input type='text' name='radio' label='myfield' value='myvalue' onChange={(e) => { ReactTestUtils.act(() => {
ReactDOM.render(<Input type='text' name='radio' label='myfield' value='myvalue' onChange={(e) => {
expect(e.target.checked).to.be.true; expect(e.target.checked).to.be.true;
done(); done();
}}/>, document.body); }}/>,
container);
});
let input = document.querySelector('input'); let input = document.querySelector('input');
input.checked = true; input.checked = true;
input.dispatchEvent(new Event('change')) ReactTestUtils.Simulate.change(input);
}); });
}); });
context("type=textarea", () => { context("type=textarea", () => {
it('renders textarea button', () => { 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 label = document.querySelector('label');
let textarea = document.querySelector('textarea'); let textarea = document.querySelector('textarea');
@ -69,14 +96,16 @@ describe("settings/ui/Input", () => {
}); });
it('invoke onChange', (done) => { it('invoke onChange', (done) => {
render(<Input type='textarea' name='myname' label='myfield' value='myvalue' onChange={(e) => { ReactTestUtils.act(() => {
ReactDOM.render(<Input type='textarea' name='myname' label='myfield' value='myvalue' onChange={(e) => {
expect(e.target.value).to.equal('newvalue'); expect(e.target.value).to.equal('newvalue');
done(); done();
}}/>, document.body); }}/>, container);
});
let input = document.querySelector('textarea'); let input = document.querySelector('textarea');
input.value = 'newvalue' input.value = 'newvalue'
input.dispatchEvent(new Event('change')) ReactTestUtils.Simulate.change(input);
}); });
}); });
}); });

@ -24,7 +24,7 @@ config = {
exclude: /node_modules/, exclude: /node_modules/,
loader: 'babel-loader', loader: 'babel-loader',
query: { query: {
presets: ['preact', 'stage-2'] presets: ['@babel/react']
} }
}, },
{ {