aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json6
-rw-r--r--package.json3
-rwxr-xr-xpackage.sh20
-rw-r--r--script/package.js55
-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/content/actions/operation.js19
-rw-r--r--src/content/components/common/keymapper.js3
-rw-r--r--src/content/components/top-content/follow-controller.js12
-rw-r--r--src/content/scrolls.js163
-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.js16
-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--src/shared/utils/keys.js1
-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
34 files changed, 903 insertions, 272 deletions
diff --git a/package-lock.json b/package-lock.json
index 7e88990..0d5ffd8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -199,6 +199,12 @@
}
}
},
+ "adm-zip": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz",
+ "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=",
+ "dev": true
+ },
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
diff --git a/package.json b/package.json
index 47ec787..05e3ff5 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"scripts": {
"start": "webpack -w --debug --devtool inline-source-map",
"build": "NODE_ENV=production webpack --progress --display-error-details",
- "package": "npm run build && ./package.sh",
+ "package": "npm run build && node script/package.js",
"lint": "eslint --ext .jsx,.js src",
"test": "karma start"
},
@@ -19,6 +19,7 @@
},
"homepage": "https://github.com/ueokande/vim-vixen",
"devDependencies": {
+ "adm-zip": "^0.4.7",
"babel-cli": "^6.24.1",
"babel-eslint": "^8.0.2",
"babel-loader": "^7.1.1",
diff --git a/package.sh b/package.sh
deleted file mode 100755
index 34535ba..0000000
--- a/package.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/sh
-
-MANIFEST="manifest.json"
-
-version=$(jq -r '.version' $MANIFEST)
-
-icons=$(jq -r '.icons[]' $MANIFEST)
-content_scripts=$(jq -r '.content_scripts[].js[]' $MANIFEST)
-background_scripts=$(jq -r '.background.scripts[]' $MANIFEST)
-web_accessible_resources=$(jq -r '.web_accessible_resources[]' $MANIFEST)
-options_ui=$(jq -r '.options_ui.page' $MANIFEST)
-options_scripts=""
-for html in $options_ui; do
- scripts=$(grep -Po "(?<=src=['\"])[^'\"]*" "$html")
- for js in $scripts; do
- options_scripts="$options_scripts $(dirname $html)/$js"
- done
-done
-
-zip ${version}.zip $MANIFEST $icons $content_scripts $background_scripts $web_accessible_resources $options_ui $options_scripts
diff --git a/script/package.js b/script/package.js
new file mode 100644
index 0000000..0128f0f
--- /dev/null
+++ b/script/package.js
@@ -0,0 +1,55 @@
+let path = require('path');
+let fs = require('fs');
+let AdmZip = require('adm-zip');
+let manifest = require('../manifest');
+
+manifest.iconFiles = function() {
+ return Object.keys(this.icons).map(key => this.icons[key]);
+};
+
+manifest.contentScriptFiles = function() {
+ let files = this.content_scripts.map(entry => entry.js);
+ return [].concat.apply([], files);
+};
+
+manifest.backgroundScriptFiles = function() {
+ return this.background.scripts;
+
+};
+
+manifest.webAccessibleResourceFiles = function() {
+ return this.web_accessible_resources;
+};
+
+manifest.optionFiles = function() {
+ let uiFile = this.options_ui.page;
+ let dir = path.dirname(uiFile);
+ let html = fs.readFileSync(uiFile, 'utf-8');
+
+ let files = [uiFile];
+ let regex = /<\s*script\s+src\s*=\s*'(.*)'\s*>/g;
+ let match = regex.exec(html);
+ while (match) {
+ files.push(path.join(dir, match[1]));
+ match = regex.exec(html);
+ }
+ return files;
+};
+
+let files = []
+ .concat('manifest.json')
+ .concat(manifest.iconFiles())
+ .concat(manifest.contentScriptFiles())
+ .concat(manifest.backgroundScriptFiles())
+ .concat(manifest.webAccessibleResourceFiles())
+ .concat(manifest.optionFiles());
+let zip = new AdmZip();
+let output = `${manifest.version}.zip`;
+console.log(output);
+for (let f of files) {
+ let dir = path.dirname(f);
+ zip.addLocalFile(f, dir);
+ console.log('=>', path.join(dir, f));
+}
+
+zip.writeZip(output);
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/content/actions/operation.js b/src/content/actions/operation.js
index 0a5cd97..b4b2e38 100644
--- a/src/content/actions/operation.js
+++ b/src/content/actions/operation.js
@@ -5,9 +5,12 @@ import * as navigates from 'content/navigates';
import * as urls from 'content/urls';
import * as consoleFrames from 'content/console-frames';
import * as addonActions from './addon';
+import * as properties from 'shared/settings/properties';
// eslint-disable-next-line complexity
-const exec = (operation) => {
+const exec = (operation, repeat, settings) => {
+ let smoothscroll = settings.properties.smoothscroll ||
+ properties.defaults.smoothscroll;
switch (operation.type) {
case operations.ADDON_ENABLE:
return addonActions.enable();
@@ -24,19 +27,19 @@ const exec = (operation) => {
type: messages.FIND_PREV,
}), '*');
case operations.SCROLL_VERTICALLY:
- return scrolls.scrollVertically(window, operation.count);
+ return scrolls.scrollVertically(operation.count, smoothscroll, repeat);
case operations.SCROLL_HORIZONALLY:
- return scrolls.scrollHorizonally(window, operation.count);
+ return scrolls.scrollHorizonally(operation.count, smoothscroll, repeat);
case operations.SCROLL_PAGES:
- return scrolls.scrollPages(window, operation.count);
+ return scrolls.scrollPages(operation.count, smoothscroll, repeat);
case operations.SCROLL_TOP:
- return scrolls.scrollTop(window);
+ return scrolls.scrollTop(smoothscroll, repeat);
case operations.SCROLL_BOTTOM:
- return scrolls.scrollBottom(window);
+ return scrolls.scrollBottom(smoothscroll, repeat);
case operations.SCROLL_HOME:
- return scrolls.scrollHome(window);
+ return scrolls.scrollHome(smoothscroll, repeat);
case operations.SCROLL_END:
- return scrolls.scrollEnd(window);
+ return scrolls.scrollEnd(smoothscroll, repeat);
case operations.FOLLOW_START:
return window.top.postMessage(JSON.stringify({
type: messages.FOLLOW_START,
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.js
index fb8fabe..d9d1b2f 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.js
@@ -47,7 +47,8 @@ export default class KeymapperComponent {
return true;
}
let operation = keymaps.get(matched[0]);
- this.store.dispatch(operationActions.exec(operation));
+ this.store.dispatch(operationActions.exec(
+ operation, key.repeat, state.setting));
this.store.dispatch(inputActions.clearKeys());
return true;
}
diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js
index d373177..1e7f3cd 100644
--- a/src/content/components/top-content/follow-controller.js
+++ b/src/content/components/top-content/follow-controller.js
@@ -1,8 +1,7 @@
import * as followControllerActions from 'content/actions/follow-controller';
import messages from 'shared/messages';
import HintKeyProducer from 'content/hint-key-producer';
-
-const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
+import * as properties from 'shared/settings/properties';
const broadcastMessage = (win, message) => {
let json = JSON.stringify(message);
@@ -84,7 +83,7 @@ export default class FollowController {
this.store.dispatch(followControllerActions.backspace());
break;
default:
- if (DEFAULT_HINT_CHARSET.includes(key)) {
+ if (this.hintchars().includes(key)) {
this.store.dispatch(followControllerActions.keyPress(key));
}
break;
@@ -93,7 +92,7 @@ export default class FollowController {
}
count() {
- this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
+ this.producer = new HintKeyProducer(this.hintchars());
let doc = this.win.document;
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
@@ -135,4 +134,9 @@ export default class FollowController {
type: messages.FOLLOW_REMOVE_HINTS,
});
}
+
+ hintchars() {
+ return this.store.getState().setting.properties.hintchars ||
+ properties.defaults.hintchars;
+ }
}
diff --git a/src/content/scrolls.js b/src/content/scrolls.js
index ef38273..e8e9642 100644
--- a/src/content/scrolls.js
+++ b/src/content/scrolls.js
@@ -1,7 +1,11 @@
const SCROLL_DELTA_X = 48;
const SCROLL_DELTA_Y = 48;
+const SMOOTH_SCROLL_DURATION = 150;
-const isVisible = (win, element) => {
+// dirty way to store scrolling state on globally
+let scrolling = [false];
+
+const isVisible = (element) => {
let rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
@@ -9,19 +13,19 @@ const isVisible = (win, element) => {
if (rect.right < 0 && rect.bottom < 0) {
return false;
}
- if (win.innerWidth < rect.left && win.innerHeight < rect.top) {
+ if (window.innerWidth < rect.left && window.innerHeight < rect.top) {
return false;
}
- let { display, visibility } = win.getComputedStyle(element);
+ let { display, visibility } = window.getComputedStyle(element);
if (display === 'none' || visibility === 'hidden') {
return false;
}
return true;
};
-const isScrollableStyle = (win, element) => {
- let { overflowX, overflowY } = win.getComputedStyle(element);
+const isScrollableStyle = (element) => {
+ let { overflowX, overflowY } = window.getComputedStyle(element);
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
overflowY !== 'scroll' && overflowY !== 'auto');
};
@@ -35,15 +39,15 @@ const isOverflowed = (element) => {
// this method is called by each scrolling, and the returned value of this
// method is not cached. That does not cause performance issue because in the
// most pages, the window is root element i,e, documentElement.
-const findScrollable = (win, element) => {
- if (isScrollableStyle(win, element) && isOverflowed(element)) {
+const findScrollable = (element) => {
+ if (isScrollableStyle(element) && isOverflowed(element)) {
return element;
}
let children = Array.prototype
- .filter.call(element.children, e => isVisible(win, e));
+ .filter.call(element.children, e => isVisible(e));
for (let child of children) {
- let scrollable = findScrollable(win, child);
+ let scrollable = findScrollable(child);
if (scrollable) {
return scrollable;
}
@@ -51,68 +55,153 @@ const findScrollable = (win, element) => {
return null;
};
-const scrollTarget = (win) => {
- if (isOverflowed(win.document.documentElement)) {
- return win.document.documentElement;
+const scrollTarget = () => {
+ if (isOverflowed(window.document.documentElement)) {
+ return window.document.documentElement;
}
- if (isOverflowed(win.document.body)) {
- return win.document.body;
+ if (isOverflowed(window.document.body)) {
+ return window.document.body;
}
- let target = findScrollable(win, win.document.documentElement);
+ let target = findScrollable(window.document.documentElement);
if (target) {
return target;
}
- return win.document.documentElement;
+ return window.document.documentElement;
};
-const scrollVertically = (win, count) => {
- let target = scrollTarget(win);
+class SmoothScroller {
+ constructor(element, repeat) {
+ this.element = element;
+ this.repeat = repeat;
+ this.scrolling = scrolling;
+ if (repeat) {
+ this.easing = SmoothScroller.linearEasing;
+ } else {
+ this.easing = SmoothScroller.inOutQuadEasing;
+ }
+ }
+
+ scroll(x, y) {
+ if (this.scrolling[0]) {
+ return;
+ }
+ scrolling[0] = true;
+
+ this.startX = this.element.scrollLeft;
+ this.startY = this.element.scrollTop;
+
+ this.targetX = x;
+ this.targetY = y;
+ this.distanceX = x - this.startX;
+ this.distanceY = y - this.startY;
+ this.timeStart = 0;
+
+ window.requestAnimationFrame(this.loop.bind(this));
+ }
+
+ loop(time) {
+ if (!this.timeStart) {
+ this.timeStart = time;
+ }
+
+ let elapsed = time - this.timeStart;
+ let v = this.easing(elapsed / SMOOTH_SCROLL_DURATION);
+ let nextX = this.startX + this.distanceX * v;
+ let nextY = this.startY + this.distanceY * v;
+
+ window.scrollTo(nextX, nextY);
+
+ if (elapsed < SMOOTH_SCROLL_DURATION) {
+ window.requestAnimationFrame(this.loop.bind(this));
+ } else {
+ scrolling[0] = false;
+ this.element.scrollTo(this.targetX, this.targetY);
+ }
+ }
+
+ static inOutQuadEasing(t) {
+ if (t < 1) {
+ return t * t;
+ }
+ return -(t - 1) * (t - 1) + 1;
+ }
+
+ static linearEasing(t) {
+ return t;
+ }
+}
+
+class RoughtScroller {
+ constructor(element) {
+ this.element = element;
+ }
+
+ scroll(x, y) {
+ this.element.scrollTo(x, y);
+ }
+}
+
+const scroller = (element, smooth, repeat) => {
+ if (smooth) {
+ return new SmoothScroller(element, repeat);
+ }
+ return new RoughtScroller(element);
+};
+
+const scrollVertically = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollTop + SCROLL_DELTA_Y * count;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHorizonally = (win, count) => {
- let target = scrollTarget(win);
+const scrollHorizonally = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft + SCROLL_DELTA_X * count;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollPages = (win, count) => {
- let target = scrollTarget(win);
+const scrollPages = (count, smooth, repeat) => {
+ let target = scrollTarget();
let height = target.clientHeight;
let x = target.scrollLeft;
let y = target.scrollTop + height * count;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollTop = (win) => {
- let target = scrollTarget(win);
+const scrollTop = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = 0;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollBottom = (win) => {
- let target = scrollTarget(win);
+const scrollBottom = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollHeight;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHome = (win) => {
- let target = scrollTarget(win);
+const scrollHome = (smooth, repeat) => {
+ let target = scrollTarget();
let x = 0;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollEnd = (win) => {
- let target = scrollTarget(win);
+const scrollEnd = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollWidth;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
export {
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 6b71717..0c3f0d9 100644
--- a/src/shared/settings/default.js
+++ b/src/shared/settings/default.js
@@ -60,6 +60,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..37dc881
--- /dev/null
+++ b/src/shared/settings/properties.js
@@ -0,0 +1,16 @@
+// describe types of a propety as:
+// mystr: 'string',
+// mynum: 'number',
+// mybool: 'boolean',
+const types = {
+ hintchars: 'string',
+ smoothscroll: 'boolean',
+};
+
+// describe default values of a property
+const defaults = {
+ hintchars: 'abcdefghijklmnopqrstuvwxyz',
+ smoothscroll: false,
+};
+
+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/src/shared/utils/keys.js b/src/shared/utils/keys.js
index fba8ce3..8a86dfb 100644
--- a/src/shared/utils/keys.js
+++ b/src/shared/utils/keys.js
@@ -18,6 +18,7 @@ const fromKeyboardEvent = (e) => {
return {
key: modifierdKeyName(e.key),
+ repeat: e.repeat,
shiftKey: shift,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
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);
});
});
});