Merge pull request #303 from ueokande/properties

Properties support
jh-changes
Shin'ya Ueoka 7 years ago committed by GitHub
commit f5dfdb0bd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 79
      src/background/actions/command.js
  2. 5
      src/background/actions/index.js
  3. 21
      src/background/actions/setting.js
  4. 16
      src/background/components/background.js
  5. 4
      src/background/index.js
  6. 2
      src/background/reducers/index.js
  7. 24
      src/background/reducers/setting.js
  8. 16
      src/settings/actions/setting.js
  9. 60
      src/settings/components/form/properties-form.jsx
  10. 12
      src/settings/components/form/properties-form.scss
  11. 12
      src/settings/components/index.jsx
  12. 169
      src/shared/commands.js
  13. 84
      src/shared/commands/complete.js
  14. 3
      src/shared/commands/index.js
  15. 59
      src/shared/commands/parsers.js
  16. 2
      src/shared/settings/default.js
  17. 15
      src/shared/settings/properties.js
  18. 31
      src/shared/settings/storage.js
  19. 17
      src/shared/settings/validator.js
  20. 20
      src/shared/settings/values.js
  21. 37
      test/background/reducers/setting.test.js
  22. 86
      test/settings/components/form/properties-form.test.jsx
  23. 85
      test/shared/commands/parsers.test.js
  24. 2
      test/shared/settings/validator.test.js
  25. 29
      test/shared/settings/values.test.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 };

@ -0,0 +1,5 @@
export default {
// Settings
SETTING_SET_SETTINGS: 'setting.set.settings',
SETTING_SET_PROPERTY: 'setting.set.property',
};

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

@ -1,6 +1,7 @@
import messages from 'shared/messages'; import messages from 'shared/messages';
import * as operationActions from 'background/actions/operation'; 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 tabActions from 'background/actions/tab';
import * as commands from 'shared/commands'; import * as commands from 'shared/commands';
@ -35,18 +36,17 @@ export default class BackgroundComponent {
return this.store.dispatch( return this.store.dispatch(
tabActions.openToTab(message.url, sender.tab), sender); tabActions.openToTab(message.url, sender.tab), sender);
case messages.CONSOLE_ENTER_COMMAND: case messages.CONSOLE_ENTER_COMMAND:
return commands.exec(message.text, settings.value).catch((e) => { this.store.dispatch(
return browser.tabs.sendMessage(sender.tab.id, { commandActions.exec(message.text, settings.value),
type: messages.CONSOLE_SHOW_ERROR, sender
text: e.message, );
}); return this.broadcastSettingsChanged();
});
case messages.SETTINGS_QUERY: case messages.SETTINGS_QUERY:
return Promise.resolve(this.store.getState().setting.value); return Promise.resolve(this.store.getState().setting.value);
case messages.CONSOLE_QUERY_COMPLETIONS: case messages.CONSOLE_QUERY_COMPLETIONS:
return commands.complete(message.text, settings.value); return commands.complete(message.text, settings.value);
case messages.SETTINGS_RELOAD: case messages.SETTINGS_RELOAD:
this.store.dispatch(settingsActions.load()); this.store.dispatch(settingActions.load());
return this.broadcastSettingsChanged(); return this.broadcastSettingsChanged();
} }
} }

@ -1,4 +1,4 @@
import * as settingsActions from 'settings/actions/setting'; import * as settingActions from 'background/actions/setting';
import messages from 'shared/messages'; import messages from 'shared/messages';
import BackgroundComponent from 'background/components/background'; import BackgroundComponent from 'background/components/background';
import reducers from 'background/reducers'; import reducers from 'background/reducers';
@ -16,4 +16,4 @@ const store = createStore(reducers, (e, sender) => {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const backgroundComponent = new BackgroundComponent(store); const backgroundComponent = new BackgroundComponent(store);
store.dispatch(settingsActions.load()); store.dispatch(settingActions.load());

@ -1,4 +1,4 @@
import settingReducer from 'settings/reducers/setting'; import settingReducer from './setting';
// Make setting reducer instead of re-use // Make setting reducer instead of re-use
const defaultState = { const defaultState = {

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

@ -1,27 +1,23 @@
import actions from 'settings/actions'; import actions from 'settings/actions';
import messages from 'shared/messages'; import messages from 'shared/messages';
import DefaultSettings from 'shared/settings/default'; import DefaultSettings from 'shared/settings/default';
import * as settingsStorage from 'shared/settings/storage';
import * as settingsValues from 'shared/settings/values'; import * as settingsValues from 'shared/settings/values';
const load = () => { const load = () => {
return browser.storage.local.get('settings').then(({ settings }) => { return settingsStorage.loadRaw().then((settings) => {
if (!settings) { return set(settings);
return set(DefaultSettings); });
}
return set(Object.assign({}, DefaultSettings, settings));
}, console.error);
}; };
const save = (settings) => { const save = (settings) => {
return browser.storage.local.set({ return settingsStorage.save(settings).then(() => {
settings,
}).then(() => {
return browser.runtime.sendMessage({ return browser.runtime.sendMessage({
type: messages.SETTINGS_RELOAD type: messages.SETTINGS_RELOAD
});
}).then(() => { }).then(() => {
return set(settings); return set(settings);
}); });
});
}; };
const set = (settings) => { const set = (settings) => {

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

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

@ -4,8 +4,10 @@ import Input from './ui/input';
import SearchForm from './form/search-form'; import SearchForm from './form/search-form';
import KeymapsForm from './form/keymaps-form'; import KeymapsForm from './form/keymaps-form';
import BlacklistForm from './form/blacklist-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 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'; import * as settingsValues from 'shared/settings/values';
const DO_YOU_WANT_TO_CONTINUE = const DO_YOU_WANT_TO_CONTINUE =
@ -65,6 +67,14 @@ class SettingsComponent extends Component {
onChange={value => this.bindForm('blacklist', value)} onChange={value => this.bindForm('blacklist', value)}
/> />
</fieldset> </fieldset>
<fieldset>
<legend>Properties</legend>
<PropertiesForm
types={properties.types}
value={this.state.settings.form.properties}
onChange={value => this.bindForm('properties', value)}
/>
</fieldset>
</div>; </div>;
} }

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

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

@ -0,0 +1,3 @@
import complete from './complete';
export { complete };

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

@ -58,6 +58,8 @@ export default {
"duckduckgo": "https://duckduckgo.com/?q={}", "duckduckgo": "https://duckduckgo.com/?q={}",
"twitter": "https://twitter.com/search?q={}", "twitter": "https://twitter.com/search?q={}",
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}" "wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
},
"properties": {
} }
} }
}`, }`,

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

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

@ -1,6 +1,7 @@
import operations from 'shared/operations'; 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) => { const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
return operations[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) => { const validate = (settings) => {
validateInvalidTopKeys(settings); validateInvalidTopKeys(settings);
if (settings.keymaps) { if (settings.keymaps) {
@ -56,6 +68,9 @@ const validate = (settings) => {
if (settings.search) { if (settings.search) {
validateSearch(settings.search); validateSearch(settings.search);
} }
if (settings.properties) {
validateProperties(settings.properties);
}
}; };
export { validate }; export { validate };

@ -1,3 +1,5 @@
import * as properties from './properties';
const operationFromFormName = (name) => { const operationFromFormName = (name) => {
let [type, argStr] = name.split('?'); let [type, argStr] = name.split('?');
let args = {}; let args = {};
@ -44,9 +46,12 @@ const valueFromForm = (form) => {
} }
} }
let blacklist = form.blacklist; return {
keymaps,
return { keymaps, search, blacklist }; search,
blacklist: form.blacklist,
properties: form.properties
};
}; };
const jsonFromValue = (value) => { 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) => { const jsonFromForm = (form) => {

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

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

@ -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', []]);
});
});
});

@ -1,5 +1,5 @@
import { expect } from "chai"; import { expect } from "chai";
import { validate } from 'shared/validators/setting'; import { validate } from 'shared/settings/validator';
describe("setting validator", () => { describe("setting validator", () => {
describe("unknown top keys", () => { describe("unknown top keys", () => {

@ -7,13 +7,21 @@ describe("settings values", () => {
let json = `{ let json = `{
"keymaps": { "0": {"type": "scroll.home"}}, "keymaps": { "0": {"type": "scroll.home"}},
"search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }}, "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); let value = values.valueFromJson(json);
expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}}); 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.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
expect(value.blacklist).to.deep.equal(["*.slack.com"]); 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={}']], engines: [['google', 'https://google.com/search?q={}']],
}, },
blacklist: ['*.slack.com'], blacklist: ['*.slack.com'],
"properties": {
"mystr": "value",
"mynum": 123,
"mybool": true,
}
}; };
let value = values.valueFromForm(form); 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(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.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
expect(value.blacklist).to.deep.equal(["*.slack.com"]); 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', () => { 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('keymaps');
expect(value).to.not.have.key('search'); expect(value).to.not.have.key('search');
expect(value).to.not.have.key('blacklist'); expect(value).to.not.have.key('blacklist');
expect(value).to.not.have.key('properties');
}); });
it('override keymaps', () => { it('override keymaps', () => {
@ -96,7 +113,12 @@ describe("settings values", () => {
0: { type: 'scroll.home' }, 0: { type: 'scroll.home' },
}, },
search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }}, 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 allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ];
let form = values.formFromValue(value, allowed); 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.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]);
expect(form.blacklist).to.have.lengthOf(1); expect(form.blacklist).to.have.lengthOf(1);
expect(form.blacklist).to.include('*.slack.com'); 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);
}); });
}); });
}); });