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

Move to React
jh-changes
Shin'ya Ueoka 5 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. 1700
      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. 71
      test/settings/components/ui/input.test.jsx
  42. 2
      webpack.config.js

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

1700
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",
"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-loader": "^7.1.5",
"babel-preset-preact": "^1.1.0",
"babel-preset-stage-2": "^6.24.1",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
@ -42,8 +42,10 @@
"lanthan": "git+https://github.com/ueokande/lanthan.git#master",
"mocha": "^6.1.4",
"node-sass": "^4.12.0",
"preact": "^8.4.2",
"preact-redux": "^2.0.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"react-test-renderer": "^16.8.6",
"redux": "^4.0.1",
"redux-promise": "^0.6.0",
"sass-loader": "^7.1.0",

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

@ -1,17 +1,18 @@
import './console.scss';
import { connect } from 'preact-redux';
import { Component, h } from 'preact';
import Input from './console/input';
import Completion from './console/completion';
import Message from './console/message';
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import Input from './console/Input';
import Completion from './console/Completion';
import Message from './console/Message';
import * as consoleActions from '../../console/actions/console';
const COMPLETION_MAX_ITEMS = 33;
class ConsoleComponent extends Component {
class Console extends React.Component {
onBlur() {
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;
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') {
return this.context.store.dispatch(consoleActions.enterFind(value));
return this.props.dispatch(consoleActions.enterFind(value));
}
}
selectNext(e) {
this.context.store.dispatch(consoleActions.completionNext());
this.props.dispatch(consoleActions.completionNext());
e.stopPropagation();
e.preventDefault();
}
selectPrev(e) {
this.context.store.dispatch(consoleActions.completionPrev());
this.props.dispatch(consoleActions.completionPrev());
e.stopPropagation();
e.preventDefault();
}
onKeyDown(e) {
if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) {
this.context.store.dispatch(consoleActions.hideCommand());
this.props.dispatch(consoleActions.hideCommand());
}
switch (e.keyCode) {
case KeyboardEvent.DOM_VK_ESCAPE:
return this.context.store.dispatch(consoleActions.hideCommand());
return this.props.dispatch(consoleActions.hideCommand());
case KeyboardEvent.DOM_VK_RETURN:
return this.doEnter(e);
case KeyboardEvent.DOM_VK_TAB:
if (e.shiftKey) {
this.context.store.dispatch(consoleActions.completionPrev());
this.props.dispatch(consoleActions.completionPrev());
} else {
this.context.store.dispatch(consoleActions.completionNext());
this.props.dispatch(consoleActions.completionNext());
}
e.stopPropagation();
e.preventDefault();
break;
case KeyboardEvent.DOM_VK_OPEN_BRACKET:
if (e.ctrlKey) {
return this.context.store.dispatch(consoleActions.hideCommand());
return this.props.dispatch(consoleActions.hideCommand());
}
break;
case KeyboardEvent.DOM_VK_M:
@ -80,11 +81,11 @@ class ConsoleComponent extends Component {
}
}
onInput(e) {
onChange(e) {
let text = e.target.value;
this.context.store.dispatch(consoleActions.setConsoleText(text));
this.props.dispatch(consoleActions.setConsoleText(text));
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;
}
if (prevProps.mode !== 'command' && this.props.mode === 'command') {
this.context.store.dispatch(
this.props.dispatch(
consoleActions.getCompletions(this.props.consoleText));
this.focus();
} else if (prevProps.mode !== 'find' && this.props.mode === 'find') {
@ -117,7 +118,7 @@ class ConsoleComponent extends Component {
mode={this.props.mode}
onBlur={this.onBlur.bind(this)}
onKeyDown={this.onKeyDown.bind(this)}
onInput={this.onInput.bind(this)}
onChange={this.onChange.bind(this)}
value={this.props.consoleText}
/>
</div>;
@ -126,6 +127,8 @@ class ConsoleComponent extends Component {
return <Message mode={ this.props.mode } >
{ this.props.messageText }
</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;
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) => {
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 {
class Completion extends React.Component {
constructor() {
super();
this.state = { viewOffset: 0, select: -1 };
@ -63,9 +43,13 @@ class CompletionComponent extends Component {
let index = 0;
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) {
eles.push(<CompletionItem
key={`item-${index}`}
icon={item.icon}
caption={item.caption}
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() {
this.input.focus();
}
@ -23,10 +24,20 @@ export default class InputComponent extends Component {
ref={(c) => { this.input = c; }}
onBlur={this.props.onBlur}
onKeyDown={this.props.onKeyDown}
onInput={this.props.onInput}
onChange={this.props.onChange}
value={this.props.value}
/>
</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) {
case 'error':
return (
@ -15,4 +16,10 @@ export default function Message(props) {
</p>
);
}
}
};
Message.propTypes = {
children: PropTypes.string,
};
export default Message;

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

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

@ -1,8 +1,8 @@
import actions from 'settings/actions';
import * as validator from 'shared/settings/validator';
import KeymapsForm from '../components/form/keymaps-form';
import * as settingsValues from 'shared/settings/values';
import * as settingsStorage from 'shared/settings/storage';
import keymaps from '../keymaps';
const load = async() => {
let settings = await settingsStorage.loadRaw();
@ -29,8 +29,7 @@ const save = async(settings) => {
const switchToForm = (json) => {
try {
validator.validate(JSON.parse(json));
// AllowdOps filters operations, this is dirty dependency
let form = settingsValues.formFromJson(json, KeymapsForm.AllowdOps);
let form = settingsValues.formFromJson(json, keymaps.allowedOps);
return {
type: actions.SETTING_SWITCH_TO_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 AddButton from '../ui/add-button';
import DeleteButton from '../ui/delete-button';
import { h, Component } from 'preact';
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 Component {
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}
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>;
})
}
<AddButton name='add' style='float:right'
<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() : [];
@ -46,7 +42,22 @@ class BlacklistForm extends 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;

@ -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 { h, Component } from 'preact';
import './PropertiesForm.scss';
import React from 'react';
import PropTypes from 'prop-types';
class PropertiesForm extends Component {
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'>
{
@ -29,6 +27,7 @@ class PropertiesForm extends Component {
className='column-input'
value={value[name] ? value[name] : ''}
onChange={this.bindValue.bind(this)}
onBlur={this.props.onBlur}
checked={value[name]}
/>
</label>
@ -39,10 +38,6 @@ class PropertiesForm extends Component {
}
bindValue(e) {
if (!this.props.onChange) {
return;
}
let name = e.target.name;
let next = { ...this.props.value };
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;

@ -1,15 +1,13 @@
import './search-form.scss';
import { h, Component } from 'preact';
import AddButton from '../ui/add-button';
import DeleteButton from '../ui/delete-button';
import './SearchForm.scss';
import React from 'react';
import PropTypes from 'prop-types';
import AddButton from '../ui/AddButton';
import DeleteButton from '../ui/DeleteButton';
class SearchForm extends Component {
class SearchForm extends React.Component {
render() {
let value = this.props.value;
if (!value) {
value = { default: '', engines: []};
}
if (!value.engines) {
value.engines = [];
}
@ -25,11 +23,15 @@ class SearchForm extends 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]}
@ -40,16 +42,12 @@ class SearchForm extends Component {
</div>;
})
}
<AddButton name='add' style='float:right'
<AddButton name='add' style={{ float: 'right' }}
onClick={this.bindValue.bind(this)} />
</div>;
}
bindValue(e) {
if (!this.props.onChange) {
return;
}
let value = this.props.value;
let name = e.target.name;
let index = e.target.getAttribute('data-index');
@ -72,7 +70,23 @@ class SearchForm extends Component {
}
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;

@ -1,11 +1,11 @@
import './site.scss';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
import Input from './ui/input';
import SearchForm from './form/search-form';
import KeymapsForm from './form/keymaps-form';
import BlacklistForm from './form/blacklist-form';
import PropertiesForm from './form/properties-form';
import React from 'react';
import { connect } from 'react-redux';
import Input from './ui/Input';
import SearchForm from './form/SearchForm';
import KeymapsForm from './form/KeymapsForm';
import BlacklistForm from './form/BlacklistForm';
import PropertiesForm from './form/PropertiesForm';
import * as properties from 'shared/settings/properties';
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. ' +
'Do you want to continue?';
class SettingsComponent extends Component {
class SettingsComponent extends React.Component {
componentDidMount() {
this.props.dispatch(settingActions.load());
}
@ -25,6 +25,7 @@ class SettingsComponent extends 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 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 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 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 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 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 Component {
json: e.target.value,
form: this.props.form,
};
this.props.dispatch(settingActions.save(settings));
this.props.dispatch(settingActions.set(settings));
}
bindSource(e) {
@ -135,8 +140,10 @@ class SettingsComponent extends Component {
}
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));
}
}

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

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

@ -1,7 +1,8 @@
import { h, Component } from 'preact';
import './input.scss';
import React from 'react';
import PropTypes from 'prop-types';
import './Input.scss';
class Input extends Component {
class Input extends React.Component {
renderText(props) {
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;

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

@ -1,8 +1,4 @@
import './keymaps-form.scss';
import { h, Component } from 'preact';
import Input from '../ui/input';
const KeyMapFields = [
const fields = [
[
['scroll.vertically?{"count":1}', 'Scroll down'],
['scroll.vertically?{"count":-1}', 'Scroll up'],
@ -70,49 +66,9 @@ const KeyMapFields = [
]
];
const AllowdOps = [].concat(...KeyMapFields.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;
const allowedOps = [].concat(...fields.map(group => group.map(e => e[0])));
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 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);
});
});
});

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