diff options
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); }); }); }); |