aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/background/actions/command.js79
-rw-r--r--src/background/actions/index.js5
-rw-r--r--src/background/actions/setting.js21
-rw-r--r--src/background/components/background.js16
-rw-r--r--src/background/index.js4
-rw-r--r--src/background/reducers/index.js2
-rw-r--r--src/background/reducers/setting.js24
-rw-r--r--src/settings/actions/setting.js18
-rw-r--r--src/settings/components/form/properties-form.jsx60
-rw-r--r--src/settings/components/form/properties-form.scss12
-rw-r--r--src/settings/components/index.jsx12
-rw-r--r--src/shared/commands.js169
-rw-r--r--src/shared/commands/complete.js84
-rw-r--r--src/shared/commands/index.js3
-rw-r--r--src/shared/commands/parsers.js59
-rw-r--r--src/shared/settings/default.js2
-rw-r--r--src/shared/settings/properties.js15
-rw-r--r--src/shared/settings/storage.js31
-rw-r--r--src/shared/settings/validator.js (renamed from src/shared/validators/setting.js)17
-rw-r--r--src/shared/settings/values.js20
-rw-r--r--test/background/reducers/setting.test.js37
-rw-r--r--test/settings/components/form/properties-form.test.jsx86
-rw-r--r--test/shared/commands/parsers.test.js85
-rw-r--r--test/shared/settings/validator.test.js (renamed from test/shared/validators/setting.test.js)2
-rw-r--r--test/shared/settings/values.test.js29
25 files changed, 691 insertions, 201 deletions
diff --git a/src/background/actions/command.js b/src/background/actions/command.js
new file mode 100644
index 0000000..4c52bca
--- /dev/null
+++ b/src/background/actions/command.js
@@ -0,0 +1,79 @@
+import actions from '../actions';
+import * as tabs from 'background/tabs';
+import * as parsers from 'shared/commands/parsers';
+import * as properties from 'shared/settings/properties';
+
+const openCommand = (url) => {
+ return browser.tabs.query({
+ active: true, currentWindow: true
+ }).then((gotTabs) => {
+ if (gotTabs.length > 0) {
+ return browser.tabs.update(gotTabs[0].id, { url: url });
+ }
+ });
+};
+
+const tabopenCommand = (url) => {
+ return browser.tabs.create({ url: url });
+};
+
+const winopenCommand = (url) => {
+ return browser.windows.create({ url });
+};
+
+const bufferCommand = (keywords) => {
+ if (keywords.length === 0) {
+ return Promise.resolve([]);
+ }
+ let keywordsStr = keywords.join(' ');
+ return browser.tabs.query({
+ active: true, currentWindow: true
+ }).then((gotTabs) => {
+ if (gotTabs.length > 0) {
+ if (isNaN(keywordsStr)) {
+ return tabs.selectByKeyword(gotTabs[0], keywordsStr);
+ }
+ let index = parseInt(keywordsStr, 10) - 1;
+ return tabs.selectAt(index);
+ }
+ });
+};
+
+const setCommand = (args) => {
+ if (!args[0]) {
+ return Promise.resolve();
+ }
+
+ let [name, value] = parsers.parseSetOption(args[0], properties.types);
+ return {
+ type: actions.SETTING_SET_PROPERTY,
+ name,
+ value
+ };
+};
+
+const exec = (line, settings) => {
+ let [name, args] = parsers.parseCommandLine(line);
+
+ switch (name) {
+ case 'o':
+ case 'open':
+ return openCommand(parsers.normalizeUrl(args, settings.search));
+ case 't':
+ case 'tabopen':
+ return tabopenCommand(parsers.normalizeUrl(args, settings.search));
+ case 'w':
+ case 'winopen':
+ return winopenCommand(parsers.normalizeUrl(args, settings.search));
+ case 'b':
+ case 'buffer':
+ return bufferCommand(args);
+ case 'set':
+ return setCommand(args);
+ case '':
+ return Promise.resolve();
+ }
+ throw new Error(name + ' command is not defined');
+};
+
+export { exec };
diff --git a/src/background/actions/index.js b/src/background/actions/index.js
new file mode 100644
index 0000000..efe4074
--- /dev/null
+++ b/src/background/actions/index.js
@@ -0,0 +1,5 @@
+export default {
+ // Settings
+ SETTING_SET_SETTINGS: 'setting.set.settings',
+ SETTING_SET_PROPERTY: 'setting.set.property',
+};
diff --git a/src/background/actions/setting.js b/src/background/actions/setting.js
new file mode 100644
index 0000000..773142f
--- /dev/null
+++ b/src/background/actions/setting.js
@@ -0,0 +1,21 @@
+import actions from '../actions';
+import * as settingsStorage from 'shared/settings/storage';
+
+const load = () => {
+ return settingsStorage.loadValue().then((value) => {
+ return {
+ type: actions.SETTING_SET_SETTINGS,
+ value,
+ };
+ });
+};
+
+const setProperty = (name, value) => {
+ return {
+ type: actions.SETTING_SET_PROPERTY,
+ name,
+ value,
+ };
+};
+
+export { load, setProperty };
diff --git a/src/background/components/background.js b/src/background/components/background.js
index 2d94310..9578e78 100644
--- a/src/background/components/background.js
+++ b/src/background/components/background.js
@@ -1,6 +1,7 @@
import messages from 'shared/messages';
import * as operationActions from 'background/actions/operation';
-import * as settingsActions from 'settings/actions/setting';
+import * as commandActions from 'background/actions/command';
+import * as settingActions from 'background/actions/setting';
import * as tabActions from 'background/actions/tab';
import * as commands from 'shared/commands';
@@ -35,18 +36,17 @@ export default class BackgroundComponent {
return this.store.dispatch(
tabActions.openToTab(message.url, sender.tab), sender);
case messages.CONSOLE_ENTER_COMMAND:
- return commands.exec(message.text, settings.value).catch((e) => {
- return browser.tabs.sendMessage(sender.tab.id, {
- type: messages.CONSOLE_SHOW_ERROR,
- text: e.message,
- });
- });
+ this.store.dispatch(
+ commandActions.exec(message.text, settings.value),
+ sender
+ );
+ return this.broadcastSettingsChanged();
case messages.SETTINGS_QUERY:
return Promise.resolve(this.store.getState().setting.value);
case messages.CONSOLE_QUERY_COMPLETIONS:
return commands.complete(message.text, settings.value);
case messages.SETTINGS_RELOAD:
- this.store.dispatch(settingsActions.load());
+ this.store.dispatch(settingActions.load());
return this.broadcastSettingsChanged();
}
}
diff --git a/src/background/index.js b/src/background/index.js
index 8a68767..3ef712f 100644
--- a/src/background/index.js
+++ b/src/background/index.js
@@ -1,4 +1,4 @@
-import * as settingsActions from 'settings/actions/setting';
+import * as settingActions from 'background/actions/setting';
import messages from 'shared/messages';
import BackgroundComponent from 'background/components/background';
import reducers from 'background/reducers';
@@ -16,4 +16,4 @@ const store = createStore(reducers, (e, sender) => {
// eslint-disable-next-line no-unused-vars
const backgroundComponent = new BackgroundComponent(store);
-store.dispatch(settingsActions.load());
+store.dispatch(settingActions.load());
diff --git a/src/background/reducers/index.js b/src/background/reducers/index.js
index 4be8fac..dab0c62 100644
--- a/src/background/reducers/index.js
+++ b/src/background/reducers/index.js
@@ -1,4 +1,4 @@
-import settingReducer from 'settings/reducers/setting';
+import settingReducer from './setting';
// Make setting reducer instead of re-use
const defaultState = {
diff --git a/src/background/reducers/setting.js b/src/background/reducers/setting.js
new file mode 100644
index 0000000..045a654
--- /dev/null
+++ b/src/background/reducers/setting.js
@@ -0,0 +1,24 @@
+import actions from 'background/actions';
+
+const defaultState = {
+ value: {},
+};
+
+export default function reducer(state = defaultState, action = {}) {
+ switch (action.type) {
+ case actions.SETTING_SET_SETTINGS:
+ return {
+ value: action.value,
+ };
+ case actions.SETTING_SET_PROPERTY:
+ return {
+ value: Object.assign({}, state.value, {
+ properties: Object.assign({}, state.value.properties,
+ { [action.name]: action.value })
+ })
+ };
+ default:
+ return state;
+ }
+}
+
diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js
index 1d01fda..92c9f8a 100644
--- a/src/settings/actions/setting.js
+++ b/src/settings/actions/setting.js
@@ -1,26 +1,22 @@
import actions from 'settings/actions';
import messages from 'shared/messages';
import DefaultSettings from 'shared/settings/default';
+import * as settingsStorage from 'shared/settings/storage';
import * as settingsValues from 'shared/settings/values';
const load = () => {
- return browser.storage.local.get('settings').then(({ settings }) => {
- if (!settings) {
- return set(DefaultSettings);
- }
- return set(Object.assign({}, DefaultSettings, settings));
- }, console.error);
+ return settingsStorage.loadRaw().then((settings) => {
+ return set(settings);
+ });
};
const save = (settings) => {
- return browser.storage.local.set({
- settings,
- }).then(() => {
+ return settingsStorage.save(settings).then(() => {
return browser.runtime.sendMessage({
type: messages.SETTINGS_RELOAD
- }).then(() => {
- return set(settings);
});
+ }).then(() => {
+ return set(settings);
});
};
diff --git a/src/settings/components/form/properties-form.jsx b/src/settings/components/form/properties-form.jsx
new file mode 100644
index 0000000..55c8512
--- /dev/null
+++ b/src/settings/components/form/properties-form.jsx
@@ -0,0 +1,60 @@
+import './properties-form.scss';
+import { h, Component } from 'preact';
+
+class PropertiesForm extends Component {
+
+ render() {
+ let types = this.props.types;
+ let value = this.props.value;
+ if (!value) {
+ value = {};
+ }
+
+ return <div className='form-properties-form'>
+ {
+ Object.keys(types).map((name) => {
+ let type = types[name];
+ let inputType = null;
+ if (type === 'string') {
+ inputType = 'text';
+ } else if (type === 'number') {
+ inputType = 'number';
+ } else if (type === 'boolean') {
+ inputType = 'checkbox';
+ }
+ return <div key={name} className='form-properties-form-row'>
+ <label>
+ <span className='column-name'>{name}</span>
+ <input type={inputType} name={name}
+ className='column-input'
+ value={value[name] ? value[name] : ''}
+ onChange={this.bindValue.bind(this)}
+ checked={value[name]}
+ />
+ </label>
+ </div>;
+ })
+ }
+ </div>;
+ }
+
+ bindValue(e) {
+ if (!this.props.onChange) {
+ return;
+ }
+
+ let name = e.target.name;
+ let next = Object.assign({}, this.props.value);
+ if (e.target.type.toLowerCase() === 'checkbox') {
+ next[name] = e.target.checked;
+ } else if (e.target.type.toLowerCase() === 'number') {
+ next[name] = Number(e.target.value);
+ } else {
+ next[name] = e.target.value;
+ }
+
+ this.props.onChange(next);
+ }
+}
+
+export default PropertiesForm;
diff --git a/src/settings/components/form/properties-form.scss b/src/settings/components/form/properties-form.scss
new file mode 100644
index 0000000..7c9e167
--- /dev/null
+++ b/src/settings/components/form/properties-form.scss
@@ -0,0 +1,12 @@
+.form-properties-form {
+ &-row {
+ .column-name {
+ display: inline-block;
+ min-width: 5rem;
+ font-weight: bold;
+ }
+ .column-input {
+ line-height: 2.2rem;
+ }
+ }
+}
diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx
index 73520ca..d7696a1 100644
--- a/src/settings/components/index.jsx
+++ b/src/settings/components/index.jsx
@@ -4,8 +4,10 @@ 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 * as properties from 'shared/settings/properties';
import * as settingActions from 'settings/actions/setting';
-import * as validator from 'shared/validators/setting';
+import * as validator from 'shared/settings/validator';
import * as settingsValues from 'shared/settings/values';
const DO_YOU_WANT_TO_CONTINUE =
@@ -65,6 +67,14 @@ class SettingsComponent extends Component {
onChange={value => this.bindForm('blacklist', value)}
/>
</fieldset>
+ <fieldset>
+ <legend>Properties</legend>
+ <PropertiesForm
+ types={properties.types}
+ value={this.state.settings.form.properties}
+ onChange={value => this.bindForm('properties', value)}
+ />
+ </fieldset>
</div>;
}
diff --git a/src/shared/commands.js b/src/shared/commands.js
deleted file mode 100644
index ed64a63..0000000
--- a/src/shared/commands.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import * as tabs from 'background/tabs';
-import * as histories from 'background/histories';
-
-const normalizeUrl = (args, searchConfig) => {
- let concat = args.join(' ');
- try {
- return new URL(concat).href;
- } catch (e) {
- if (concat.includes('.') && !concat.includes(' ')) {
- return 'http://' + concat;
- }
- let query = concat;
- let template = searchConfig.engines[
- searchConfig.default
- ];
- for (let key in searchConfig.engines) {
- if (args[0] === key) {
- query = args.slice(1).join(' ');
- template = searchConfig.engines[key];
- }
- }
- return template.replace('{}', encodeURIComponent(query));
- }
-};
-
-const openCommand = (url) => {
- return browser.tabs.query({
- active: true, currentWindow: true
- }).then((gotTabs) => {
- if (gotTabs.length > 0) {
- return browser.tabs.update(gotTabs[0].id, { url: url });
- }
- });
-};
-
-const tabopenCommand = (url) => {
- return browser.tabs.create({ url: url });
-};
-
-const winopenCommand = (url) => {
- return browser.windows.create({ url });
-};
-
-const bufferCommand = (keywords) => {
- if (keywords.length === 0) {
- return Promise.resolve([]);
- }
- let keywordsStr = keywords.join(' ');
- return browser.tabs.query({
- active: true, currentWindow: true
- }).then((gotTabs) => {
- if (gotTabs.length > 0) {
- if (isNaN(keywordsStr)) {
- return tabs.selectByKeyword(gotTabs[0], keywordsStr);
- }
- let index = parseInt(keywordsStr, 10) - 1;
- return tabs.selectAt(index);
- }
- });
-};
-
-const getOpenCompletions = (command, keywords, searchConfig) => {
- return histories.getCompletions(keywords).then((pages) => {
- let historyItems = pages.map((page) => {
- return {
- caption: page.title,
- content: command + ' ' + page.url,
- url: page.url
- };
- });
- let engineNames = Object.keys(searchConfig.engines);
- let engineItems = engineNames.filter(name => name.startsWith(keywords))
- .map(name => ({
- caption: name,
- content: command + ' ' + name
- }));
-
- let completions = [];
- if (engineItems.length > 0) {
- completions.push({
- name: 'Search Engines',
- items: engineItems
- });
- }
- if (historyItems.length > 0) {
- completions.push({
- name: 'History',
- items: historyItems
- });
- }
- return completions;
- });
-};
-
-const doCommand = (line, settings) => {
- let words = line.trim().split(/ +/);
- let name = words.shift();
-
- switch (name) {
- case 'o':
- case 'open':
- return openCommand(normalizeUrl(words, settings.search));
- case 't':
- case 'tabopen':
- return tabopenCommand(normalizeUrl(words, settings.search));
- case 'w':
- case 'winopen':
- return winopenCommand(normalizeUrl(words, settings.search));
- case 'b':
- case 'buffer':
- return bufferCommand(words);
- case '':
- return Promise.resolve();
- }
- throw new Error(name + ' command is not defined');
-};
-
-const getCompletions = (line, settings) => {
- let typedWords = line.trim().split(/ +/);
- let typing = '';
- if (!line.endsWith(' ')) {
- typing = typedWords.pop();
- }
-
- if (typedWords.length === 0) {
- return Promise.resolve([]);
- }
- let name = typedWords.shift();
- let keywords = typedWords.concat(typing).join(' ');
-
- switch (name) {
- case 'o':
- case 'open':
- case 't':
- case 'tabopen':
- case 'w':
- case 'winopen':
- return getOpenCompletions(name, keywords, settings.search);
- case 'b':
- case 'buffer':
- return tabs.getCompletions(keywords).then((gotTabs) => {
- let items = gotTabs.map((tab) => {
- return {
- caption: tab.title,
- content: name + ' ' + tab.title,
- url: tab.url,
- icon: tab.favIconUrl
- };
- });
- return [
- {
- name: 'Buffers',
- items: items
- }
- ];
- });
- }
- return Promise.resolve([]);
-};
-
-const exec = (line, settings) => {
- return doCommand(line, settings);
-};
-
-const complete = (line, settings) => {
- return getCompletions(line, settings);
-};
-
-export { exec, complete };
diff --git a/src/shared/commands/complete.js b/src/shared/commands/complete.js
new file mode 100644
index 0000000..0bdbab8
--- /dev/null
+++ b/src/shared/commands/complete.js
@@ -0,0 +1,84 @@
+import * as tabs from 'background/tabs';
+import * as histories from 'background/histories';
+
+const getOpenCompletions = (command, keywords, searchConfig) => {
+ return histories.getCompletions(keywords).then((pages) => {
+ let historyItems = pages.map((page) => {
+ return {
+ caption: page.title,
+ content: command + ' ' + page.url,
+ url: page.url
+ };
+ });
+ let engineNames = Object.keys(searchConfig.engines);
+ let engineItems = engineNames.filter(name => name.startsWith(keywords))
+ .map(name => ({
+ caption: name,
+ content: command + ' ' + name
+ }));
+
+ let completions = [];
+ if (engineItems.length > 0) {
+ completions.push({
+ name: 'Search Engines',
+ items: engineItems
+ });
+ }
+ if (historyItems.length > 0) {
+ completions.push({
+ name: 'History',
+ items: historyItems
+ });
+ }
+ return completions;
+ });
+};
+
+const getCompletions = (line, settings) => {
+ let typedWords = line.trim().split(/ +/);
+ let typing = '';
+ if (!line.endsWith(' ')) {
+ typing = typedWords.pop();
+ }
+
+ if (typedWords.length === 0) {
+ return Promise.resolve([]);
+ }
+ let name = typedWords.shift();
+ let keywords = typedWords.concat(typing).join(' ');
+
+ switch (name) {
+ case 'o':
+ case 'open':
+ case 't':
+ case 'tabopen':
+ case 'w':
+ case 'winopen':
+ return getOpenCompletions(name, keywords, settings.search);
+ case 'b':
+ case 'buffer':
+ return tabs.getCompletions(keywords).then((gotTabs) => {
+ let items = gotTabs.map((tab) => {
+ return {
+ caption: tab.title,
+ content: name + ' ' + tab.title,
+ url: tab.url,
+ icon: tab.favIconUrl
+ };
+ });
+ return [
+ {
+ name: 'Buffers',
+ items: items
+ }
+ ];
+ });
+ }
+ return Promise.resolve([]);
+};
+
+const complete = (line, settings) => {
+ return getCompletions(line, settings);
+};
+
+export default complete;
diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js
new file mode 100644
index 0000000..78cb4df
--- /dev/null
+++ b/src/shared/commands/index.js
@@ -0,0 +1,3 @@
+import complete from './complete';
+
+export { complete };
diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js
new file mode 100644
index 0000000..fb37d2a
--- /dev/null
+++ b/src/shared/commands/parsers.js
@@ -0,0 +1,59 @@
+const normalizeUrl = (args, searchConfig) => {
+ let concat = args.join(' ');
+ try {
+ return new URL(concat).href;
+ } catch (e) {
+ if (concat.includes('.') && !concat.includes(' ')) {
+ return 'http://' + concat;
+ }
+ let query = concat;
+ let template = searchConfig.engines[
+ searchConfig.default
+ ];
+ for (let key in searchConfig.engines) {
+ if (args[0] === key) {
+ query = args.slice(1).join(' ');
+ template = searchConfig.engines[key];
+ }
+ }
+ return template.replace('{}', encodeURIComponent(query));
+ }
+};
+
+const mustNumber = (v) => {
+ let num = Number(v);
+ if (isNaN(num)) {
+ throw new Error('Not number: ' + v);
+ }
+ return num;
+};
+
+const parseSetOption = (word, types) => {
+ let [key, value] = word.split('=');
+ if (value === undefined) {
+ value = !key.startsWith('no');
+ key = value ? key : key.slice(2);
+ }
+ let type = types[key];
+ if (!type) {
+ throw new Error('Unknown property: ' + key);
+ }
+ if (type === 'boolean' && typeof value !== 'boolean' ||
+ type !== 'boolean' && typeof value === 'boolean') {
+ throw new Error('Invalid argument: ' + word);
+ }
+
+ switch (type) {
+ case 'string': return [key, value];
+ case 'number': return [key, mustNumber(value)];
+ case 'boolean': return [key, value];
+ }
+};
+
+const parseCommandLine = (line) => {
+ let words = line.trim().split(/ +/);
+ let name = words.shift();
+ return [name, words];
+};
+
+export { normalizeUrl, parseCommandLine, parseSetOption };
diff --git a/src/shared/settings/default.js b/src/shared/settings/default.js
index d187565..6425354 100644
--- a/src/shared/settings/default.js
+++ b/src/shared/settings/default.js
@@ -58,6 +58,8 @@ export default {
"duckduckgo": "https://duckduckgo.com/?q={}",
"twitter": "https://twitter.com/search?q={}",
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
+ },
+ "properties": {
}
}
}`,
diff --git a/src/shared/settings/properties.js b/src/shared/settings/properties.js
new file mode 100644
index 0000000..ff8039b
--- /dev/null
+++ b/src/shared/settings/properties.js
@@ -0,0 +1,15 @@
+const types = {
+ // TODO describe property types here
+ // mystr: 'string',
+ // mynum: 'number',
+ // mybool: 'boolean',
+};
+
+const defaults = {
+ // TODO describe property defaults values
+ // mystr: 'hello',
+ // mynum: 123,
+ // mybool: true,
+};
+
+export { types, defaults };
diff --git a/src/shared/settings/storage.js b/src/shared/settings/storage.js
new file mode 100644
index 0000000..1edb441
--- /dev/null
+++ b/src/shared/settings/storage.js
@@ -0,0 +1,31 @@
+import DefaultSettings from './default';
+import * as settingsValues from './values';
+
+const loadRaw = () => {
+ return browser.storage.local.get('settings').then(({ settings }) => {
+ if (!settings) {
+ return DefaultSettings;
+ }
+ return Object.assign({}, DefaultSettings, settings);
+ });
+};
+
+const loadValue = () => {
+ return loadRaw().then((settings) => {
+ let value = JSON.parse(DefaultSettings.json);
+ if (settings.source === 'json') {
+ value = settingsValues.valueFromJson(settings.json);
+ } else if (settings.source === 'form') {
+ value = settingsValues.valueFromForm(settings.form);
+ }
+ return value;
+ });
+};
+
+const save = (settings) => {
+ return browser.storage.local.set({
+ settings,
+ });
+};
+
+export { loadRaw, loadValue, save };
diff --git a/src/shared/validators/setting.js b/src/shared/settings/validator.js
index 949ab29..1589420 100644
--- a/src/shared/validators/setting.js
+++ b/src/shared/settings/validator.js
@@ -1,6 +1,7 @@
import operations from 'shared/operations';
+import * as properties from './properties';
-const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist'];
+const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
return operations[key];
});
@@ -48,6 +49,17 @@ const validateSearch = (search) => {
}
};
+const validateProperties = (props) => {
+ for (let name of Object.keys(props)) {
+ if (!properties.types[name]) {
+ throw new Error(`Unknown property name: "${name}"`);
+ }
+ if (typeof props[name] !== properties.types[name]) {
+ throw new Error(`Invalid type for property: "${name}"`);
+ }
+ }
+};
+
const validate = (settings) => {
validateInvalidTopKeys(settings);
if (settings.keymaps) {
@@ -56,6 +68,9 @@ const validate = (settings) => {
if (settings.search) {
validateSearch(settings.search);
}
+ if (settings.properties) {
+ validateProperties(settings.properties);
+ }
};
export { validate };
diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js
index 4e55fa0..bd03be2 100644
--- a/src/shared/settings/values.js
+++ b/src/shared/settings/values.js
@@ -1,3 +1,5 @@
+import * as properties from './properties';
+
const operationFromFormName = (name) => {
let [type, argStr] = name.split('?');
let args = {};
@@ -44,9 +46,12 @@ const valueFromForm = (form) => {
}
}
- let blacklist = form.blacklist;
-
- return { keymaps, search, blacklist };
+ return {
+ keymaps,
+ search,
+ blacklist: form.blacklist,
+ properties: form.properties
+ };
};
const jsonFromValue = (value) => {
@@ -78,9 +83,14 @@ const formFromValue = (value, allowedOps) => {
}
}
- let blacklist = value.blacklist;
+ let formProperties = Object.assign({}, properties.defaults, value.properties);
- return { keymaps, search, blacklist };
+ return {
+ keymaps,
+ search,
+ blacklist: value.blacklist,
+ properties: formProperties,
+ };
};
const jsonFromForm = (form) => {
diff --git a/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js
new file mode 100644
index 0000000..2ef98cb
--- /dev/null
+++ b/test/background/reducers/setting.test.js
@@ -0,0 +1,37 @@
+import { expect } from "chai";
+import actions from 'background/actions';
+import settingReducer from 'background/reducers/setting';
+
+describe("setting reducer", () => {
+ it('return the initial state', () => {
+ let state = settingReducer(undefined, {});
+ expect(state).to.have.deep.property('value', {});
+ });
+
+ it('return next state for SETTING_SET_SETTINGS', () => {
+ let action = {
+ type: actions.SETTING_SET_SETTINGS,
+ value: { key: 123 },
+ };
+ let state = settingReducer(undefined, action);
+ expect(state).to.have.deep.property('value', { key: 123 });
+ });
+
+ it('return next state for SETTING_SET_PROPERTY', () => {
+ let state = {
+ value: {
+ properties: { smoothscroll: true }
+ }
+ }
+ let action = {
+ type: actions.SETTING_SET_PROPERTY,
+ name: 'encoding',
+ value: 'utf-8',
+ };
+ state = settingReducer(state, action);
+
+ console.log(state);
+ expect(state.value.properties).to.have.property('smoothscroll', true);
+ expect(state.value.properties).to.have.property('encoding', 'utf-8');
+ });
+});
diff --git a/test/settings/components/form/properties-form.test.jsx b/test/settings/components/form/properties-form.test.jsx
new file mode 100644
index 0000000..4807361
--- /dev/null
+++ b/test/settings/components/form/properties-form.test.jsx
@@ -0,0 +1,86 @@
+import { expect } from 'chai';
+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();
+ });
+ });
+});
diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js
new file mode 100644
index 0000000..0a1960c
--- /dev/null
+++ b/test/shared/commands/parsers.test.js
@@ -0,0 +1,85 @@
+import { expect } from "chai";
+import * as parsers from 'shared/commands/parsers';
+
+describe("shared/commands/parsers", () => {
+ describe("#parsers.parseSetOption", () => {
+ it('parse set string', () => {
+ let [key, value] = parsers.parseSetOption('encoding=utf-8', { encoding: 'string' });
+ expect(key).to.equal('encoding');
+ expect(value).to.equal('utf-8');
+ });
+
+ it('parse set empty string', () => {
+ let [key, value] = parsers.parseSetOption('encoding=', { encoding: 'string' });
+ expect(key).to.equal('encoding');
+ expect(value).to.equal('');
+ });
+
+ it('parse set string', () => {
+ let [key, value] = parsers.parseSetOption('history=50', { history: 'number' });
+ expect(key).to.equal('history');
+ expect(value).to.equal(50);
+ });
+
+ it('parse set boolean', () => {
+ let [key, value] = parsers.parseSetOption('paste', { paste: 'boolean' });
+ expect(key).to.equal('paste');
+ expect(value).to.be.true;
+
+ [key, value] = parsers.parseSetOption('nopaste', { paste: 'boolean' });
+ expect(key).to.equal('paste');
+ expect(value).to.be.false;
+ });
+
+ it('throws error on unknown property', () => {
+ expect(() => parsers.parseSetOption('charset=utf-8', {})).to.throw(Error, 'Unknown');
+ expect(() => parsers.parseSetOption('smoothscroll', {})).to.throw(Error, 'Unknown');
+ expect(() => parsers.parseSetOption('nosmoothscroll', {})).to.throw(Error, 'Unknown');
+ })
+
+ it('throws error on invalid property', () => {
+ expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number');
+ expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid');
+ expect(() => parsers.parseSetOption('charset=', { charset: 'boolean' })).to.throw(Error, 'Invalid');
+ expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid');
+ expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid');
+ })
+ });
+
+ describe('#normalizeUrl', () => {
+ const config = {
+ default: 'google',
+ engines: {
+ google: 'https://google.com/search?q={}',
+ yahoo: 'https://yahoo.com/search?q={}',
+ }
+ };
+
+ it('convertes search url', () => {
+ expect(parsers.normalizeUrl(['google', 'apple'], config))
+ .to.equal('https://google.com/search?q=apple');
+ expect(parsers.normalizeUrl(['yahoo', 'apple'], config))
+ .to.equal('https://yahoo.com/search?q=apple');
+ expect(parsers.normalizeUrl(['google', 'apple', 'banana'], config))
+ .to.equal('https://google.com/search?q=apple%20banana');
+ expect(parsers.normalizeUrl(['yahoo', 'C++CLI'], config))
+ .to.equal('https://yahoo.com/search?q=C%2B%2BCLI');
+ });
+
+ it('user default search engine', () => {
+ expect(parsers.normalizeUrl(['apple', 'banana'], config))
+ .to.equal('https://google.com/search?q=apple%20banana');
+ });
+ });
+
+ describe('#parseCommandLine', () => {
+ it('parse command line as name and args', () => {
+ expect(parsers.parseCommandLine('open google apple')).to.deep.equal(['open', ['google', 'apple']]);
+ expect(parsers.parseCommandLine(' open google apple ')).to.deep.equal(['open', ['google', 'apple']]);
+ expect(parsers.parseCommandLine('')).to.deep.equal(['', []]);
+ expect(parsers.parseCommandLine(' ')).to.deep.equal(['', []]);
+ expect(parsers.parseCommandLine('exit')).to.deep.equal(['exit', []]);
+ expect(parsers.parseCommandLine(' exit ')).to.deep.equal(['exit', []]);
+ });
+ });
+});
diff --git a/test/shared/validators/setting.test.js b/test/shared/settings/validator.test.js
index 15d6a10..61d976a 100644
--- a/test/shared/validators/setting.test.js
+++ b/test/shared/settings/validator.test.js
@@ -1,5 +1,5 @@
import { expect } from "chai";
-import { validate } from 'shared/validators/setting';
+import { validate } from 'shared/settings/validator';
describe("setting validator", () => {
describe("unknown top keys", () => {
diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js
index 2632cd7..62cfb5f 100644
--- a/test/shared/settings/values.test.js
+++ b/test/shared/settings/values.test.js
@@ -7,13 +7,21 @@ describe("settings values", () => {
let json = `{
"keymaps": { "0": {"type": "scroll.home"}},
"search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }},
- "blacklist": [ "*.slack.com"]
+ "blacklist": [ "*.slack.com"],
+ "properties": {
+ "mystr": "value",
+ "mynum": 123,
+ "mybool": true
+ }
}`;
let value = values.valueFromJson(json);
expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}});
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
+ expect(value.properties).to.have.property('mystr', 'value');
+ expect(value.properties).to.have.property('mynum', 123);
+ expect(value.properties).to.have.property('mybool', true);
});
});
@@ -29,6 +37,11 @@ describe("settings values", () => {
engines: [['google', 'https://google.com/search?q={}']],
},
blacklist: ['*.slack.com'],
+ "properties": {
+ "mystr": "value",
+ "mynum": 123,
+ "mybool": true,
+ }
};
let value = values.valueFromForm(form);
@@ -37,6 +50,9 @@ describe("settings values", () => {
expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} }));
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
+ expect(value.properties).to.have.property('mystr', 'value');
+ expect(value.properties).to.have.property('mynum', 123);
+ expect(value.properties).to.have.property('mybool', true);
});
it('convert from empty form', () => {
@@ -45,6 +61,7 @@ describe("settings values", () => {
expect(value).to.not.have.key('keymaps');
expect(value).to.not.have.key('search');
expect(value).to.not.have.key('blacklist');
+ expect(value).to.not.have.key('properties');
});
it('override keymaps', () => {
@@ -96,7 +113,12 @@ describe("settings values", () => {
0: { type: 'scroll.home' },
},
search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }},
- blacklist: [ '*.slack.com']
+ blacklist: [ '*.slack.com'],
+ properties: {
+ "mystr": "value",
+ "mynum": 123,
+ "mybool": true,
+ }
};
let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ];
let form = values.formFromValue(value, allowed);
@@ -109,6 +131,9 @@ describe("settings values", () => {
expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]);
expect(form.blacklist).to.have.lengthOf(1);
expect(form.blacklist).to.include('*.slack.com');
+ expect(form.properties).to.have.property('mystr', 'value');
+ expect(form.properties).to.have.property('mynum', 123);
+ expect(form.properties).to.have.property('mybool', true);
});
});
});