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