From 4a446793212b97dc16f87c99f99def6fb5fcd1d2 Mon Sep 17 00:00:00 2001 From: chocolateboy Date: Fri, 17 Nov 2017 19:02:13 +0000 Subject: improve #linkPrev and #linkNext: - add support for elements - match if there's more than one rel e.g. ... - more tests --- test/content/navigates.test.js | 156 +++++++++++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 37 deletions(-) (limited to 'test') diff --git a/test/content/navigates.test.js b/test/content/navigates.test.js index b5144e9..d8a3316 100644 --- a/test/content/navigates.test.js +++ b/test/content/navigates.test.js @@ -1,56 +1,138 @@ -import { expect } from "chai"; +import { expect } from 'chai'; import * as navigates from 'content/navigates'; +const testRel = (done, rel, html) => { + const method = rel === 'prev' ? 'linkPrev' : 'linkNext'; + document.body.innerHTML = html; + navigates[method](window); + setTimeout(() => { + expect(document.location.hash).to.equal(`#${rel}`); + done(); + }, 0); +}; + +const testPrev = html => done => testRel(done, 'prev', html); +const testNext = html => done => testRel(done, 'next', html); + describe('navigates module', () => { describe('#linkPrev', () => { - it('clicks prev link by text content', (done) => { - document.body.innerHTML = 'xprevx go to prev'; - navigates.linkPrev(window); - setTimeout(() => { - expect(document.location.hash).to.equal('#prev'); - done(); - }, 0); - }); + it('navigates to elements whose rel attribute is "prev"', testPrev( + '' + )); - it('clicks a[rel=prev] element preferentially', (done) => { - document.body.innerHTML = 'prev '; - navigates.linkPrev(window); - setTimeout(() => { - expect(document.location.hash).to.equal('#prev'); - done(); - }, 0); - }); - }); + it('navigates to elements whose rel attribute starts with "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute ends with "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute contains "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute is "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute starts with "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose rel attribute ends with "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose rel attribute contains "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose text matches "prev"', testPrev( + 'previewgo to prev' + )); + + it('navigates to elements whose text matches "previous"', testPrev( + 'previewgo to previous' + )); + + it('navigates to elements whose decoded text matches "<<"', testPrev( + 'click me<<' + )); + + it('navigates to matching elements by clicking', testPrev( + `` + )); + + it('prefers link[rel~=prev] to a[rel~=prev]', testPrev( + '' + )); + it('prefers a[rel~=prev] to a::text(pattern)', testPrev( + 'go to prev' + )); + }); describe('#linkNext', () => { - it('clicks next link by text content', (done) => { - document.body.innerHTML = 'xnextx go to next'; - navigates.linkNext(window); - setTimeout(() => { - expect(document.location.hash).to.equal('#next'); - done(); - }, 0); - }); + it('navigates to elements whose rel attribute is "next"', testNext( + '' + )); - it('clicks a[rel=next] element preferentially', (done) => { - document.body.innerHTML = 'next '; - navigates.linkNext(window); - setTimeout(() => { - expect(document.location.hash).to.equal('#next'); - done(); - }, 0); - }); + it('navigates to elements whose rel attribute starts with "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute ends with "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute contains "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute is "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute starts with "next"', testNext( + 'click me' + )); + + it('navigates to elements whose rel attribute ends with "next"', testNext( + 'click me' + )); + + it('navigates to elements whose rel attribute contains "next"', testNext( + 'click me' + )); + + it('navigates to elements whose text matches "next"', testNext( + 'inextricablego to next' + )); + + it('navigates to elements whose decoded text matches ">>"', testNext( + 'click me>>' + )); + + it('navigates to matching elements by clicking', testNext( + `` + )); + + it('prefers link[rel~=next] to a[rel~=next]', testNext( + 'go to next' + )); }); describe('#parent', () => { // NOTE: not able to test location it('removes hash', () => { - window.location.hash = "#section-1"; + window.location.hash = '#section-1'; navigates.parent(window); expect(document.location.hash).to.be.empty; }); }); }); - - -- cgit v1.2.3 From ee3cd3faabbe64265746ef39d419f63db24d11ba Mon Sep 17 00:00:00 2001 From: chocolateboy Date: Thu, 23 Nov 2017 14:10:36 +0000 Subject: pagination tweaks and fixes: - fallback links: - select the last matching link rather than the first - use `innerText` rather than `textContent` - use a single regex for each pattern rather than an array - fix markup typo in test --- src/content/navigates.js | 49 +++++++++++++++++++++++------------------- test/content/navigates.test.js | 6 +++--- 2 files changed, 30 insertions(+), 25 deletions(-) (limited to 'test') diff --git a/src/content/navigates.js b/src/content/navigates.js index 3e12a6f..c9baa30 100644 --- a/src/content/navigates.js +++ b/src/content/navigates.js @@ -1,18 +1,18 @@ -const PREV_LINK_PATTERNS = [ - /\bprev\b/i, /\bprevious\b/i, /\bback\b/i, - //, /\u203a/, /\u2192/, /\xbb/, /\u226b/, />>/ -]; - -const findLinkByPatterns = (win, patterns) => { - const links = win.document.getElementsByTagName('a'); - return Array.prototype.find.call(links, (link) => { - return patterns.some(ptn => ptn.test(link.textContent)); - }); +const REL_PATTERN = { + prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<>/i, +}; + +// Return the last element in the document matching the supplied selector +// and the optional filter, or null if there are no matches. +const selectLast = (win, selector, filter) => { + let nodes = win.document.querySelectorAll(selector); + + if (filter) { + nodes = Array.from(nodes).filter(filter); + } + + return nodes.length ? nodes[nodes.length - 1] : null; }; const historyPrev = (win) => { @@ -23,16 +23,21 @@ const historyNext = (win) => { win.history.forward(); }; -const linkCommon = (win, rel, patterns) => { - let link = win.document.querySelector(`link[rel~=${rel}][href]`); +// Code common to linkPrev and linkNext which navigates to the specified page. +const linkRel = (win, rel) => { + let link = selectLast(win, `link[rel~=${rel}][href]`); if (link) { - win.location = link.getAttribute('href'); + win.location = link.href; return; } - link = win.document.querySelector(`a[rel~=${rel}]`) || - findLinkByPatterns(win, patterns); + const pattern = REL_PATTERN[rel]; + + link = selectLast(win, `a[rel~=${rel}][href]`) || + // `innerText` is much slower than `textContent`, but produces much better + // (i.e. less unexpected) results + selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText)); if (link) { link.click(); @@ -40,11 +45,11 @@ const linkCommon = (win, rel, patterns) => { }; const linkPrev = (win) => { - linkCommon(win, 'prev', PREV_LINK_PATTERNS); + linkRel(win, 'prev'); }; const linkNext = (win) => { - linkCommon(win, 'next', NEXT_LINK_PATTERNS); + linkRel(win, 'next'); }; const parent = (win) => { diff --git a/test/content/navigates.test.js b/test/content/navigates.test.js index d8a3316..f1f0741 100644 --- a/test/content/navigates.test.js +++ b/test/content/navigates.test.js @@ -53,7 +53,7 @@ describe('navigates module', () => { )); it('navigates to elements whose text matches "previous"', testPrev( - 'previewgo to previous' + 'previouslyprevious page' )); it('navigates to elements whose decoded text matches "<<"', testPrev( @@ -119,11 +119,11 @@ describe('navigates module', () => { )); it('prefers link[rel~=next] to a[rel~=next]', testNext( - '' )); it('prefers a[rel~=next] to a::text(pattern)', testNext( - 'go to next' + 'next page' )); }); -- cgit v1.2.3 From d15de42a75ee0f722e8779af1ecff8b51e59c56d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 Nov 2017 22:14:28 +0900 Subject: settings helpers --- .eslintrc | 1 + src/settings/actions/setting.js | 4 +- src/shared/settings/values.js | 95 +++++++++++++++++++++++++----- test/shared/settings/values.test.js | 112 ++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 test/shared/settings/values.test.js (limited to 'test') diff --git a/.eslintrc b/.eslintrc index 6717889..ab61dc7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -44,6 +44,7 @@ "no-plusplus": "off", "no-ternary": "off", "no-undefined": "off", + "no-undef-init": "off", "no-unused-vars": ["error", { "varsIgnorePattern": "h" }], "no-use-before-define": "off", "no-warning-comments": "off", diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js index 1d4ef34..1d01fda 100644 --- a/src/settings/actions/setting.js +++ b/src/settings/actions/setting.js @@ -27,9 +27,9 @@ const save = (settings) => { const set = (settings) => { let value = JSON.parse(DefaultSettings.json); if (settings.source === 'json') { - value = settingsValues.fromJson(settings.json); + value = settingsValues.valueFromJson(settings.json); } else if (settings.source === 'form') { - value = settingsValues.fromForm(settings.form); + value = settingsValues.valueFromForm(settings.form); } return { diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js index d86cfdc..4482fbb 100644 --- a/src/shared/settings/values.js +++ b/src/shared/settings/values.js @@ -1,4 +1,6 @@ -const operationFromName = (name) => { +import DefaultSettings from './default'; + +const operationFromFormName = (name) => { let [type, argStr] = name.split('?'); let args = {}; if (argStr) { @@ -7,29 +9,92 @@ const operationFromName = (name) => { return Object.assign({ type }, args); }; -const fromJson = (json) => { +const operationToFormName = (op) => { + let type = op.type; + let args = Object.assign({}, op); + delete args.type; + + if (Object.keys(args).length === 0) { + return type; + } + return op.type + '?' + JSON.stringify(args); +}; + +const valueFromJson = (json) => { return JSON.parse(json); }; -const fromForm = (form) => { - let keymaps = {}; - for (let name of Object.keys(form.keymaps)) { - let keys = form.keymaps[name]; - keymaps[keys] = operationFromName(name); +const valueFromForm = (form) => { + let keymaps = undefined; + if (form.keymaps) { + keymaps = {}; + for (let name of Object.keys(form.keymaps)) { + let keys = form.keymaps[name]; + keymaps[keys] = operationFromFormName(name); + } } - let engines = {}; - for (let { name, url } of form.search.engines) { - engines[name] = url; + let search = undefined; + if (form.search) { + search = { default: form.search.default }; + + if (form.search.engines) { + search.engines = {}; + for (let [name, url] of form.search.engines) { + search.engines[name] = url; + } + } } - let search = { - default: form.search.default, - engines, - }; let blacklist = form.blacklist; return { keymaps, search, blacklist }; }; -export { fromJson, fromForm }; +const jsonFromValue = (value) => { + return JSON.stringify(value, undefined, 2); +}; + +const formFromValue = (value) => { + + let keymaps = undefined; + if (value.keymaps) { + let allowedOps = new Set(Object.keys(DefaultSettings.form.keymaps)); + + keymaps = {}; + for (let keys of Object.keys(value.keymaps)) { + let op = operationToFormName(value.keymaps[keys]); + if (allowedOps.has(op)) { + keymaps[op] = keys; + } + } + } + + let search = undefined; + if (value.search) { + search = { default: value.search.default }; + if (value.search.engines) { + search.engines = Object.keys(value.search.engines).map((name) => { + return [name, value.search.engines[name]]; + }); + } + } + + let blacklist = value.blacklist; + + return { keymaps, search, blacklist }; +}; + +const jsonFromForm = (form) => { + return jsonFromValue(valueFromForm(form)); +}; + +const formFromJson = (json) => { + let value = valueFromJson(json); + return formFromValue(value); +}; + +export { + valueFromJson, valueFromForm, jsonFromValue, formFromValue, + jsonFromForm, formFromJson +}; diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js new file mode 100644 index 0000000..2c222b6 --- /dev/null +++ b/test/shared/settings/values.test.js @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import * as values from 'shared/settings/values'; + +describe("settings values", () => { + describe('valueFromJson', () => { + it('return object from json string', () => { + let json = `{ + "keymaps": { "0": {"type": "scroll.home"}}, + "search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }}, + "blacklist": [ "*.slack.com"] + }`; + 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"]); + }); + }); + + describe('valueFromForm', () => { + it('returns value from form', () => { + let form = { + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }, + search: { + default: 'google', + engines: [['google', 'https://google.com/search?q={}']], + }, + blacklist: ['*.slack.com'], + }; + let value = values.valueFromForm(form); + + expect(value.keymaps).to.have.deep.property('j', { type: "scroll.vertically", count: 1 }); + expect(value.keymaps).to.have.deep.property('0', { type: "scroll.home" }); + 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"]); + }); + + it('convert from empty form', () => { + let form = {}; + let value = values.valueFromForm(form); + expect(value).to.not.have.key('keymaps'); + expect(value).to.not.have.key('search'); + expect(value).to.not.have.key('blacklist'); + }); + + it('override keymaps', () => { + let form = { + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.vertically?{"count":-1}': 'j', + } + }; + let value = values.valueFromForm(form); + + expect(value.keymaps).to.have.key('j'); + }); + + it('override search engine', () => { + let form = { + search: { + default: 'google', + engines: [ + ['google', 'https://google.com/search?q={}'], + ['google', 'https://google.co.jp/search?q={}'], + ] + } + }; + let value = values.valueFromForm(form); + + expect(value.search.engines).to.have.property('google', 'https://google.co.jp/search?q={}'); + }); + }); + + describe('jsonFromValue', () => { + }); + + describe('formFromValue', () => { + it('convert empty value to form', () => { + let value = {}; + let form = values.formFromValue(value); + + expect(value).to.not.have.key('keymaps'); + expect(value).to.not.have.key('search'); + expect(value).to.not.have.key('blacklist'); + }); + + it('convert value to form', () => { + let value = { + keymaps: { + j: { type: 'scroll.vertically', count: 1 }, + JJ: { type: 'scroll.vertically', count: 100 }, + 0: { type: 'scroll.home' }, + }, + search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }}, + blacklist: [ '*.slack.com'] + }; + let form = values.formFromValue(value); + + expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j'); + expect(form.keymaps).to.have.property('scroll.home', '0'); + expect(Object.keys(form.keymaps)).to.have.lengthOf(2); + expect(form.search).to.have.property('default', 'google'); + 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'); + }); + }); +}); -- cgit v1.2.3 From e90cf895add023240e4aed700db76cb4b38595a5 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 Nov 2017 09:43:43 +0900 Subject: add input test --- karma.conf.js | 2 + src/settings/components/ui/input.jsx | 5 +- test/settings/components/ui/input.test.jsx | 83 ++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 test/settings/components/ui/input.test.jsx (limited to 'test') diff --git a/karma.conf.js b/karma.conf.js index 859cee0..46a1774 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -7,11 +7,13 @@ module.exports = function (config) { frameworks: ['mocha'], files: [ 'test/**/*.test.js', + 'test/**/*.test.jsx', 'test/**/*.html' ], preprocessors: { 'test/**/*.test.js': [ 'webpack' ], + 'test/**/*.test.jsx': [ 'webpack' ], 'test/**/*.html': ['html2js'] }, diff --git a/src/settings/components/ui/input.jsx b/src/settings/components/ui/input.jsx index 5138411..e99dbc7 100644 --- a/src/settings/components/ui/input.jsx +++ b/src/settings/components/ui/input.jsx @@ -6,10 +6,7 @@ class Input extends Component { renderText(props) { let inputClassName = props.error ? 'input-error' : ''; return
- +
; } diff --git a/test/settings/components/ui/input.test.jsx b/test/settings/components/ui/input.test.jsx new file mode 100644 index 0000000..98f2cef --- /dev/null +++ b/test/settings/components/ui/input.test.jsx @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import { h, render } from 'preact'; +import Input from 'settings/components/ui/input' + +describe("settings/ui/Input", () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + context("type=text", () => { + it('renders text input', () => { + render(, document.body) + + let label = document.querySelector('label'); + let input = document.querySelector('input'); + expect(label.textContent).to.contain('myfield'); + expect(input.type).to.contain('text'); + expect(input.name).to.contain('myname'); + expect(input.value).to.contain('myvalue'); + }); + + it('invoke onChange', (done) => { + render( { + expect(e.target.value).to.equal('newvalue'); + done(); + }}/>, document.body); + + let input = document.querySelector('input'); + input.value = 'newvalue'; + input.dispatchEvent(new Event('change')) + }); + }); + + context("type=radio", () => { + it('renders radio button', () => { + render(, document.body) + + let label = document.querySelector('label'); + let input = document.querySelector('input'); + expect(label.textContent).to.contain('myfield'); + expect(input.type).to.contain('radio'); + expect(input.name).to.contain('myname'); + expect(input.value).to.contain('myvalue'); + }); + + it('invoke onChange', (done) => { + render( { + expect(e.target.checked).to.be.true; + done(); + }}/>, document.body); + + let input = document.querySelector('input'); + input.checked = true; + input.dispatchEvent(new Event('change')) + }); + }); + + context("type=textarea", () => { + it('renders textarea button', () => { + render(, document.body) + + let label = document.querySelector('label'); + let textarea = document.querySelector('textarea'); + let error = document.querySelector('.settings-ui-input-error'); + expect(label.textContent).to.contain('myfield'); + expect(textarea.nodeName).to.contain('TEXTAREA'); + expect(textarea.name).to.contain('myname'); + expect(textarea.value).to.contain('myvalue'); + expect(error.textContent).to.contain('myerror'); + }); + + it('invoke onChange', (done) => { + render( { + expect(e.target.value).to.equal('newvalue'); + done(); + }}/>, document.body); + + let input = document.querySelector('textarea'); + input.value = 'newvalue' + input.dispatchEvent(new Event('change')) + }); + }); +}); -- cgit v1.2.3 From 7e2c2476ef02c6bd040ba2cfdc4ec66efe560dc6 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 Nov 2017 11:24:48 +0900 Subject: add BlacklistForm test --- .../components/form/blacklist-form.test.jsx | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/settings/components/form/blacklist-form.test.jsx (limited to 'test') diff --git a/test/settings/components/form/blacklist-form.test.jsx b/test/settings/components/form/blacklist-form.test.jsx new file mode 100644 index 0000000..95f5cde --- /dev/null +++ b/test/settings/components/form/blacklist-form.test.jsx @@ -0,0 +1,82 @@ +import { expect } from 'chai'; +import { h, render } from 'preact'; +import BlacklistForm from 'settings/components/form/blacklist-form' + +describe("settings/form/BlacklistForm", () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + describe('render', () => { + it('renders BlacklistForm', () => { + render(, document.body); + + let inputs = document.querySelectorAll('input[type=text]'); + expect(inputs).to.have.lengthOf(2); + expect(inputs[0].value).to.equal('*.slack.com'); + expect(inputs[1].value).to.equal('www.google.com/maps'); + }); + + it('renders blank value', () => { + render(, document.body); + + let inputs = document.querySelectorAll('input[type=text]'); + expect(inputs).to.be.empty; + }); + + it('renders blank value', () => { + render(, document.body); + + let inputs = document.querySelectorAll('input[type=text]'); + expect(inputs).to.be.empty; + }); + }); + + describe('onChange', () => { + it('invokes onChange event on edit', (done) => { + render( { + expect(value).to.have.lengthOf(2) + .and.have.members(['gitter.im', 'www.google.com/maps*']); + + done(); + }} + />, document.body); + + let input = document.querySelectorAll('input[type=text]')[0]; + input.value = 'gitter.im'; + input.dispatchEvent(new Event('change')) + }); + + it('invokes onChange event on delete', (done) => { + render( { + expect(value).to.have.lengthOf(1) + .and.have.members(['www.google.com/maps*']); + + done(); + }} + />, document.body); + + let button = document.querySelectorAll('input[type=button]')[0]; + button.click(); + }); + + it('invokes onChange event on add', (done) => { + render( { + expect(value).to.have.lengthOf(2) + .and.have.members(['*.slack.com', '']); + + done(); + }} + />, document.body); + + let button = document.querySelector('input[type=button].ui-add-button'); + button.click(); + }); + }); +}); -- cgit v1.2.3 From 0967304ebe713bc24078b20ac3a710fd42f2f611 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 Nov 2017 12:18:12 +0900 Subject: add SearchForm test --- src/settings/components/form/search-form.jsx | 18 ++-- .../components/form/search-engine-form.test.jsx | 104 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 test/settings/components/form/search-engine-form.test.jsx (limited to 'test') diff --git a/src/settings/components/form/search-form.jsx b/src/settings/components/form/search-form.jsx index 9bafb8d..98b0655 100644 --- a/src/settings/components/form/search-form.jsx +++ b/src/settings/components/form/search-form.jsx @@ -10,10 +10,9 @@ class SearchForm extends Component { if (!value) { value = { default: '', engines: []}; } - let { - default: defaultEngine, - engines - } = value; + if (!value.engines) { + value.engines = []; + } return
@@ -22,7 +21,7 @@ class SearchForm extends Component {
Default
{ - engines.map((engine, index) => { + value.engines.map((engine, index) => { return
@@ -50,12 +49,17 @@ class SearchForm extends Component { return; } + let value = this.props.value; let name = e.target.name; let index = e.target.getAttribute('data-index'); - let next = Object.assign({}, this.props.value); + let next = Object.assign({}, { + default: value.default, + engines: value.engines ? value.engines.slice() : [], + }); if (name === 'name') { next.engines[index][0] = e.target.value; + next.default = this.props.value.engines[index][0]; } else if (name === 'url') { next.engines[index][1] = e.target.value; } else if (name === 'default') { diff --git a/test/settings/components/form/search-engine-form.test.jsx b/test/settings/components/form/search-engine-form.test.jsx new file mode 100644 index 0000000..9600cae --- /dev/null +++ b/test/settings/components/form/search-engine-form.test.jsx @@ -0,0 +1,104 @@ +import { expect } from 'chai'; +import { h, render } from 'preact'; +import SearchForm from 'settings/components/form/search-form' + +describe("settings/form/SearchForm", () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + describe('render', () => { + it('renders SearchForm', () => { + render(, document.body); + + let names = document.querySelectorAll('input[name=name]'); + expect(names).to.have.lengthOf(2); + expect(names[0].value).to.equal('google'); + expect(names[1].value).to.equal('yahoo'); + + let urls = document.querySelectorAll('input[name=url]'); + expect(urls).to.have.lengthOf(2); + expect(urls[0].value).to.equal('google.com'); + expect(urls[1].value).to.equal('yahoo.com'); + }); + + it('renders blank value', () => { + render(, document.body); + + let names = document.querySelectorAll('input[name=name]'); + let urls = document.querySelectorAll('input[name=url]'); + expect(names).to.have.lengthOf(0); + expect(urls).to.have.lengthOf(0); + }); + + it('renders blank engines', () => { + render(, document.body); + + let names = document.querySelectorAll('input[name=name]'); + let urls = document.querySelectorAll('input[name=url]'); + expect(names).to.have.lengthOf(0); + expect(urls).to.have.lengthOf(0); + }); + }); + + describe('onChange event', () => { + it('invokes onChange event on edit', (done) => { + render( { + expect(value.default).to.equal('louvre'); + expect(value.engines).to.have.lengthOf(2) + .and.have.deep.members([['louvre', 'google.com'], ['yahoo', 'yahoo.com']]) + + done(); + }} />, document.body); + + let radio = document.querySelectorAll('input[type=radio]'); + radio.checked = true; + + let name = document.querySelector('input[name=name]'); + name.value = 'louvre'; + name.dispatchEvent(new Event('change')) + }); + + it('invokes onChange event on delete', (done) => { + render( { + expect(value.default).to.equal('yahoo'); + expect(value.engines).to.have.lengthOf(1) + .and.have.deep.members([['yahoo', 'yahoo.com']]) + + done(); + }} />, document.body); + + let button = document.querySelector('input[type=button]'); + button.click(); + }); + + it('invokes onChange event on add', (done) => { + render( { + expect(value.default).to.equal('yahoo'); + expect(value.engines).to.have.lengthOf(2) + .and.have.deep.members([['google', 'google.com'], ['', '']]) + + done(); + }} />, document.body); + + let button = document.querySelector('input[type=button].ui-add-button'); + button.click(); + }); + }); +}); -- cgit v1.2.3 From 705c47bc6379a8d1fea4f775a6af98c97d4eab81 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 Nov 2017 12:34:49 +0900 Subject: add KeymapsForm test --- src/settings/components/form/keymaps-form.jsx | 6 ++- .../settings/components/form/keymaps-form.test.jsx | 53 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/settings/components/form/keymaps-form.test.jsx (limited to 'test') diff --git a/src/settings/components/form/keymaps-form.jsx b/src/settings/components/form/keymaps-form.jsx index 8e93c3f..f64320c 100644 --- a/src/settings/components/form/keymaps-form.jsx +++ b/src/settings/components/form/keymaps-form.jsx @@ -58,6 +58,10 @@ const KeyMapFields = [ class KeymapsForm extends Component { render() { + let values = this.props.value; + if (!values) { + values = {}; + } return
{ KeyMapFields.map((group, index) => { @@ -66,7 +70,7 @@ class KeymapsForm extends Component { group.map((field) => { let name = field[0]; let label = field[1]; - let value = this.props.value[name]; + let value = values[name]; return { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + describe('render', () => { + it('renders KeymapsForm', () => { + render(, document.body); + + let inputj = document.getElementById('scroll.vertically?{"count":1}'); + let inputk = document.getElementById('scroll.vertically?{"count":-1}'); + + expect(inputj.value).to.equal('j'); + expect(inputk.value).to.equal('k'); + }); + + it('renders blank value', () => { + render(, document.body); + + let inputj = document.getElementById('scroll.vertically?{"count":1}'); + let inputk = document.getElementById('scroll.vertically?{"count":-1}'); + + expect(inputj.value).to.be.empty; + expect(inputk.value).to.be.empty; + }); + }); + + describe('onChange event', () => { + it('invokes onChange event on edit', (done) => { + render( { + expect(value['scroll.vertically?{"count":1}']).to.equal('jjj'); + + done(); + }} />, document.body); + + let input = document.getElementById('scroll.vertically?{"count":1}'); + input.value = 'jjj'; + input.dispatchEvent(new Event('change')) + }); + }); +}); -- cgit v1.2.3 From e1060f9bb218202d13a4382584f220d47173194c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 28 Nov 2017 20:45:22 +0900 Subject: remove default form settings --- src/settings/components/form/keymaps-form.jsx | 4 ++ src/settings/components/index.jsx | 15 +++++- src/shared/settings/default.js | 68 --------------------------- src/shared/settings/values.js | 14 +++--- test/shared/settings/values.test.js | 4 +- 5 files changed, 27 insertions(+), 78 deletions(-) (limited to 'test') diff --git a/src/settings/components/form/keymaps-form.jsx b/src/settings/components/form/keymaps-form.jsx index f99318f..f3b6abe 100644 --- a/src/settings/components/form/keymaps-form.jsx +++ b/src/settings/components/form/keymaps-form.jsx @@ -58,6 +58,8 @@ const KeyMapFields = [ ] ]; +const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0]))); + class KeymapsForm extends Component { render() { @@ -99,4 +101,6 @@ class KeymapsForm extends Component { } } +KeymapsForm.AllowdOps = AllowdOps; + export default KeymapsForm; diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index 3961982..38f7db8 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -123,6 +123,18 @@ class SettingsComponent extends Component { } } + validateValue(e) { + let next = Object.assign({}, this.state); + + next.errors.json = ''; + try { + this.validate(e.target); + } catch (err) { + next.errors.json = err.message; + } + next.settings[e.target.name] = e.target.value; + } + bindForm(name, value) { let next = Object.assign({}, this.state, { settings: Object.assign({}, this.state.settings, { @@ -164,7 +176,8 @@ class SettingsComponent extends Component { return; } next.settings.form = - settingsValues.formFromJson(this.state.settings.json); + settingsValues.formFromJson( + this.state.settings.json, KeymapsForm.AllowdOps); } next.settings.source = to; diff --git a/src/shared/settings/default.js b/src/shared/settings/default.js index 14ed548..d187565 100644 --- a/src/shared/settings/default.js +++ b/src/shared/settings/default.js @@ -61,72 +61,4 @@ export default { } } }`, - - 'form': { - 'keymaps': { - 'scroll.vertically?{"count":1}': 'j', - 'scroll.vertically?{"count":-1}': 'k', - 'scroll.horizonally?{"count":-1}': 'h', - 'scroll.horizonally?{"count":1}': 'l', - 'scroll.home': '0', - 'scroll.end': '$', - 'scroll.top': 'gg', - 'scroll.bottom': 'G', - 'scroll.pages?{"count":-0.5}': '', - 'scroll.pages?{"count":0.5}': '', - 'scroll.pages?{"count":-1}': '', - 'scroll.pages?{"count":1}': '', - - 'tabs.close': 'd', - 'tabs.reopen': 'u', - 'tabs.next?{"count":1}': 'J', - 'tabs.prev?{"count":1}': 'K', - 'tabs.first': 'g0', - 'tabs.last': 'g$', - 'tabs.reload?{"cache":false}': 'r', - 'tabs.reload?{"cache":true}': 'R', - 'tabs.pin.toggle': 'zp', - 'tabs.duplicate': 'zd', - - 'follow.start?{"newTab":false}': 'f', - 'follow.start?{"newTab":true}': 'F', - 'navigate.history.prev': 'H', - 'navigate.history.next': 'L', - 'navigate.link.next': ']]', - 'navigate.link.prev': '[[', - 'navigate.parent': 'gu', - 'navigate.root': 'gU', - - 'find.start': '/', - 'find.next': 'n', - 'find.prev': 'N', - - 'command.show': ':', - 'command.show.open?{"alter":false}': 'o', - 'command.show.open?{"alter":true}': 'O', - 'command.show.tabopen?{"alter":false}': 't', - 'command.show.tabopen?{"alter":true}': 'T', - 'command.show.winopen?{"alter":false}': 'w', - 'command.show.winopen?{"alter":true}': 'W', - 'command.show.buffer': 'b', - - 'addon.toggle.enabled': '', - 'urls.yank': 'y', - 'zoom.in': 'zi', - 'zoom.out': 'zo', - 'zoom.neutral': 'zz', - }, - 'search': { - 'default': 'google', - 'engines': [ - ['google', 'https,//google.com/search?q={}'], - ['yahoo', 'https,//search.yahoo.com/search?p={}'], - ['bing', 'https,//www.bing.com/search?q={}'], - ['duckduckgo', 'https,//duckduckgo.com/?q={}'], - ['twitter', 'https,//twitter.com/search?q={}'], - ['wikipedia', 'https,//en.wikipedia.org/w/index.php?search={}'], - ] - }, - 'blacklist': [], - } }; diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js index 4482fbb..4e55fa0 100644 --- a/src/shared/settings/values.js +++ b/src/shared/settings/values.js @@ -1,5 +1,3 @@ -import DefaultSettings from './default'; - const operationFromFormName = (name) => { let [type, argStr] = name.split('?'); let args = {}; @@ -55,16 +53,16 @@ const jsonFromValue = (value) => { return JSON.stringify(value, undefined, 2); }; -const formFromValue = (value) => { - +const formFromValue = (value, allowedOps) => { let keymaps = undefined; + if (value.keymaps) { - let allowedOps = new Set(Object.keys(DefaultSettings.form.keymaps)); + let allowedSet = new Set(allowedOps); keymaps = {}; for (let keys of Object.keys(value.keymaps)) { let op = operationToFormName(value.keymaps[keys]); - if (allowedOps.has(op)) { + if (allowedSet.has(op)) { keymaps[op] = keys; } } @@ -89,9 +87,9 @@ const jsonFromForm = (form) => { return jsonFromValue(valueFromForm(form)); }; -const formFromJson = (json) => { +const formFromJson = (json, allowedOps) => { let value = valueFromJson(json); - return formFromValue(value); + return formFromValue(value, allowedOps); }; export { diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js index 2c222b6..2632cd7 100644 --- a/test/shared/settings/values.test.js +++ b/test/shared/settings/values.test.js @@ -98,9 +98,11 @@ describe("settings values", () => { search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }}, blacklist: [ '*.slack.com'] }; - let form = values.formFromValue(value); + let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ]; + let form = values.formFromValue(value, allowed); expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j'); + expect(form.keymaps).to.not.have.property('scroll.vertically?{"count":100}'); expect(form.keymaps).to.have.property('scroll.home', '0'); expect(Object.keys(form.keymaps)).to.have.lengthOf(2); expect(form.search).to.have.property('default', 'google'); -- cgit v1.2.3 From fbdec04786e28bad45021bef4a74e7077e34282f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 4 Jan 2018 18:55:24 +0900 Subject: move settings validator --- src/settings/components/index.jsx | 2 +- src/shared/settings/validator.js | 61 +++++++++++++++++++++++++ src/shared/validators/setting.js | 61 ------------------------- test/shared/settings/validator.test.js | 82 ++++++++++++++++++++++++++++++++++ test/shared/validators/setting.test.js | 82 ---------------------------------- 5 files changed, 144 insertions(+), 144 deletions(-) create mode 100644 src/shared/settings/validator.js delete mode 100644 src/shared/validators/setting.js create mode 100644 test/shared/settings/validator.test.js delete mode 100644 test/shared/validators/setting.test.js (limited to 'test') diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index 73520ca..074c4c7 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -5,7 +5,7 @@ import SearchForm from './form/search-form'; import KeymapsForm from './form/keymaps-form'; import BlacklistForm from './form/blacklist-form'; 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 = diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js new file mode 100644 index 0000000..949ab29 --- /dev/null +++ b/src/shared/settings/validator.js @@ -0,0 +1,61 @@ +import operations from 'shared/operations'; + +const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist']; +const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { + return operations[key]; +}); + +const validateInvalidTopKeys = (settings) => { + let invalidKey = Object.keys(settings).find((key) => { + return !VALID_TOP_KEYS.includes(key); + }); + if (invalidKey) { + throw Error(`Unknown key: "${invalidKey}"`); + } +}; + +const validateKeymaps = (keymaps) => { + for (let key of Object.keys(keymaps)) { + let value = keymaps[key]; + if (!VALID_OPERATION_VALUES.includes(value.type)) { + throw Error(`Unknown operation: "${value.type}"`); + } + } +}; + +const validateSearch = (search) => { + let engines = search.engines; + for (let key of Object.keys(engines)) { + if (/\s/.test(key)) { + throw new Error( + `While space in search engine name is not allowed: "${key}"` + ); + } + let url = engines[key]; + if (!url.match(/{}/)) { + throw new Error(`No {}-placeholders in URL of "${key}"`); + } + if (url.match(/{}/g).length > 1) { + throw new Error(`Multiple {}-placeholders in URL of "${key}"`); + } + } + + if (!search.default) { + throw new Error(`Default engine is not set`); + } + if (!Object.keys(engines).includes(search.default)) { + throw new Error(`Default engine "${search.default}" not found`); + } +}; + +const validate = (settings) => { + validateInvalidTopKeys(settings); + if (settings.keymaps) { + validateKeymaps(settings.keymaps); + } + if (settings.search) { + validateSearch(settings.search); + } +}; + +export { validate }; diff --git a/src/shared/validators/setting.js b/src/shared/validators/setting.js deleted file mode 100644 index 949ab29..0000000 --- a/src/shared/validators/setting.js +++ /dev/null @@ -1,61 +0,0 @@ -import operations from 'shared/operations'; - -const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist']; -const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { - return operations[key]; -}); - -const validateInvalidTopKeys = (settings) => { - let invalidKey = Object.keys(settings).find((key) => { - return !VALID_TOP_KEYS.includes(key); - }); - if (invalidKey) { - throw Error(`Unknown key: "${invalidKey}"`); - } -}; - -const validateKeymaps = (keymaps) => { - for (let key of Object.keys(keymaps)) { - let value = keymaps[key]; - if (!VALID_OPERATION_VALUES.includes(value.type)) { - throw Error(`Unknown operation: "${value.type}"`); - } - } -}; - -const validateSearch = (search) => { - let engines = search.engines; - for (let key of Object.keys(engines)) { - if (/\s/.test(key)) { - throw new Error( - `While space in search engine name is not allowed: "${key}"` - ); - } - let url = engines[key]; - if (!url.match(/{}/)) { - throw new Error(`No {}-placeholders in URL of "${key}"`); - } - if (url.match(/{}/g).length > 1) { - throw new Error(`Multiple {}-placeholders in URL of "${key}"`); - } - } - - if (!search.default) { - throw new Error(`Default engine is not set`); - } - if (!Object.keys(engines).includes(search.default)) { - throw new Error(`Default engine "${search.default}" not found`); - } -}; - -const validate = (settings) => { - validateInvalidTopKeys(settings); - if (settings.keymaps) { - validateKeymaps(settings.keymaps); - } - if (settings.search) { - validateSearch(settings.search); - } -}; - -export { validate }; diff --git a/test/shared/settings/validator.test.js b/test/shared/settings/validator.test.js new file mode 100644 index 0000000..61d976a --- /dev/null +++ b/test/shared/settings/validator.test.js @@ -0,0 +1,82 @@ +import { expect } from "chai"; +import { validate } from 'shared/settings/validator'; + +describe("setting validator", () => { + describe("unknown top keys", () => { + it('throws an error for unknown settings', () => { + let settings = { keymaps: {}, poison: 123 }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'poison'); + }) + }); + + describe("keymaps settings", () => { + it('throws an error for unknown operation', () => { + let settings = { + keymaps: { + a: { 'type': 'scroll.home' }, + b: { 'type': 'poison.dressing' }, + } + }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'poison.dressing'); + }); + }); + + describe("search settings", () => { + it('throws an error for invalid search engine name', () => { + let settings = { + search: { + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'cherry pie': 'https://cherypie.com/search?q={}', + } + } + }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'cherry pie'); + }); + + it('throws an error for no {}-placeholder', () => { + let settings = { + search: { + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search', + } + } + }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'yahoo'); + }); + + it('throws an error for no default engines', () => { + let settings = { + search: { + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?q={}', + } + } + }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'Default engine'); + }); + + it('throws an error for invalid default engine', () => { + let settings = { + search: { + default: 'twitter', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?q={}', + } + } + }; + let fn = validate.bind(undefined, settings) + expect(fn).to.throw(Error, 'twitter'); + }); + }); +}); diff --git a/test/shared/validators/setting.test.js b/test/shared/validators/setting.test.js deleted file mode 100644 index 15d6a10..0000000 --- a/test/shared/validators/setting.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import { expect } from "chai"; -import { validate } from 'shared/validators/setting'; - -describe("setting validator", () => { - describe("unknown top keys", () => { - it('throws an error for unknown settings', () => { - let settings = { keymaps: {}, poison: 123 }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'poison'); - }) - }); - - describe("keymaps settings", () => { - it('throws an error for unknown operation', () => { - let settings = { - keymaps: { - a: { 'type': 'scroll.home' }, - b: { 'type': 'poison.dressing' }, - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'poison.dressing'); - }); - }); - - describe("search settings", () => { - it('throws an error for invalid search engine name', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'cherry pie': 'https://cherypie.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'cherry pie'); - }); - - it('throws an error for no {}-placeholder', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'yahoo'); - }); - - it('throws an error for no default engines', () => { - let settings = { - search: { - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'Default engine'); - }); - - it('throws an error for invalid default engine', () => { - let settings = { - search: { - default: 'twitter', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'twitter'); - }); - }); -}); -- cgit v1.2.3 From e19f89f16248ed4bf28deaa9ab4b5204061df5eb Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 4 Jan 2018 20:34:33 +0900 Subject: add property to settings --- src/shared/settings/property-types.js | 6 ++++++ src/shared/settings/validator.js | 17 ++++++++++++++++- src/shared/settings/values.js | 18 ++++++++++++------ test/shared/settings/values.test.js | 29 +++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 src/shared/settings/property-types.js (limited to 'test') diff --git a/src/shared/settings/property-types.js b/src/shared/settings/property-types.js new file mode 100644 index 0000000..bcfa809 --- /dev/null +++ b/src/shared/settings/property-types.js @@ -0,0 +1,6 @@ +export default { + // TODO describe property types here + // mystr: 'string', + // mynum: 'number', + // mybool: 'boolean', +}; diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js index 949ab29..6fadac7 100644 --- a/src/shared/settings/validator.js +++ b/src/shared/settings/validator.js @@ -1,6 +1,7 @@ import operations from 'shared/operations'; +import propertyTypes from './property-types'; -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 = (properties) => { + for (let name of Object.keys(properties)) { + if (!propertyTypes[name]) { + throw new Error(`Unknown property name: "${name}"`); + } + if (typeof properties[name] !== propertyTypes[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..5027ba5 100644 --- a/src/shared/settings/values.js +++ b/src/shared/settings/values.js @@ -44,9 +44,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 +81,12 @@ const formFromValue = (value, allowedOps) => { } } - let blacklist = value.blacklist; - - return { keymaps, search, blacklist }; + return { + keymaps, + search, + blacklist: value.blacklist, + properties: value.properties, + }; }; const jsonFromForm = (form) => { 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); }); }); }); -- cgit v1.2.3 From 86df54067f8a105264a6816e115fd65efa75fb5b Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 5 Jan 2018 22:54:36 +0900 Subject: Add PropertiesForm --- src/settings/components/form/properties-form.jsx | 60 +++++++++++++++ src/settings/components/form/properties-form.scss | 12 +++ src/settings/components/index.jsx | 10 +++ .../components/form/properties-form.test.jsx | 86 ++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/settings/components/form/properties-form.jsx create mode 100644 src/settings/components/form/properties-form.scss create mode 100644 test/settings/components/form/properties-form.test.jsx (limited to 'test') 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
+ { + 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
+ +
; + }) + } +
; + } + + 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 074c4c7..c41aa6b 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -4,6 +4,8 @@ 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 PropertyTypes from 'shared/settings/property-types'; import * as settingActions from 'settings/actions/setting'; import * as validator from 'shared/settings/validator'; import * as settingsValues from 'shared/settings/values'; @@ -65,6 +67,14 @@ class SettingsComponent extends Component { onChange={value => this.bindForm('blacklist', value)} /> +
+ Properties + this.bindForm('properties', value)} + /> +
; } 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(, 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( { + 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( { + 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( { + expect(value).to.have.property('myvalue', true); + done(); + }} + />, document.body); + + let input = document.querySelector('input[name=myvalue]'); + input.click(); + }); + }); +}); -- cgit v1.2.3 From 3dbdcdba074f020f88bd4d1f5554d49549973ccd Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 18:37:29 +0900 Subject: property parser --- src/shared/commands/properties.js | 31 ++++++++++++++++++++++++++ test/shared/commands/property.test.js | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/shared/commands/properties.js create mode 100644 test/shared/commands/property.test.js (limited to 'test') diff --git a/src/shared/commands/properties.js b/src/shared/commands/properties.js new file mode 100644 index 0000000..8a3213d --- /dev/null +++ b/src/shared/commands/properties.js @@ -0,0 +1,31 @@ +const mustNumber = (v) => { + let num = Number(v); + if (isNaN(num)) { + throw new Error('Not number: ' + v); + } + return num; +}; + +const parseProperty = (word, types) => { + let [key, value] = word.split('='); + if (!value) { + 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]; + } +}; + +export { parseProperty }; diff --git a/test/shared/commands/property.test.js b/test/shared/commands/property.test.js new file mode 100644 index 0000000..d949482 --- /dev/null +++ b/test/shared/commands/property.test.js @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import { parseProperty } from 'shared/commands/properties'; + +describe("shared/commands/properties", () => { + describe("#parseProperty", () => { + it('parse set string', () => { + let [key, value] = parseProperty('encoding=utf-8', { encoding: 'string' }); + expect(key).to.equal('encoding'); + expect(value).to.equal('utf-8'); + }); + + it('parse set string', () => { + let [key, value] = parseProperty('history=50', { history: 'number' }); + expect(key).to.equal('history'); + expect(value).to.equal(50); + }); + + it('parse set boolean', () => { + let [key, value] = parseProperty('paste', { paste: 'boolean' }); + expect(key).to.equal('paste'); + expect(value).to.be.true; + + [key, value] = parseProperty('nopaste', { paste: 'boolean' }); + expect(key).to.equal('paste'); + expect(value).to.be.false; + }); + + it('throws error on unknown property', () => { + expect(() => parseProperty('charset=utf-8', {})).to.throw(Error, 'Unknown'); + expect(() => parseProperty('smoothscroll', {})).to.throw(Error, 'Unknown'); + expect(() => parseProperty('nosmoothscroll', {})).to.throw(Error, 'Unknown'); + }) + + it('throws error on invalid property', () => { + expect(() => parseProperty('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); + expect(() => parseProperty('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); + expect(() => parseProperty('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); + expect(() => parseProperty('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); + }) + }); +}); -- cgit v1.2.3 From 6083e70ea089fa2683741a1118be0e4e6b76f858 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 21:54:16 +0900 Subject: separate setting actions and reducers --- src/background/actions/index.js | 4 ++++ src/background/actions/setting.js | 13 +++++++++++++ src/background/components/background.js | 4 ++-- src/background/index.js | 4 ++-- src/background/reducers/index.js | 2 +- src/background/reducers/setting.js | 17 +++++++++++++++++ src/settings/actions/setting.js | 18 +++++++----------- src/shared/settings/storage.js | 31 +++++++++++++++++++++++++++++++ test/background/reducers/setting.test.js | 19 +++++++++++++++++++ 9 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/background/actions/index.js create mode 100644 src/background/actions/setting.js create mode 100644 src/background/reducers/setting.js create mode 100644 src/shared/settings/storage.js create mode 100644 test/background/reducers/setting.test.js (limited to 'test') diff --git a/src/background/actions/index.js b/src/background/actions/index.js new file mode 100644 index 0000000..8c212c2 --- /dev/null +++ b/src/background/actions/index.js @@ -0,0 +1,4 @@ +export default { + // Settings + SETTING_SET_SETTINGS: 'setting.set.settings', +}; diff --git a/src/background/actions/setting.js b/src/background/actions/setting.js new file mode 100644 index 0000000..0454a68 --- /dev/null +++ b/src/background/actions/setting.js @@ -0,0 +1,13 @@ +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, + }; + }); +}; + +export { load }; diff --git a/src/background/components/background.js b/src/background/components/background.js index 2d94310..22c6693 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -1,6 +1,6 @@ import messages from 'shared/messages'; import * as operationActions from 'background/actions/operation'; -import * as settingsActions from 'settings/actions/setting'; +import * as settingActions from 'background/actions/setting'; import * as tabActions from 'background/actions/tab'; import * as commands from 'shared/commands'; @@ -46,7 +46,7 @@ export default class BackgroundComponent { 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..70bf8ea --- /dev/null +++ b/src/background/reducers/setting.js @@ -0,0 +1,17 @@ +import actions from 'settings/actions'; + +const defaultState = { + value: {}, +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.SETTING_SET_SETTINGS: + return { + value: 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/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/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js new file mode 100644 index 0000000..a6a5573 --- /dev/null +++ b/test/background/reducers/setting.test.js @@ -0,0 +1,19 @@ +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 }); + }); +}); -- cgit v1.2.3 From 22c34a0a6f9721fb9d907ab10de91cbbc40d6bbe Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 10 Jan 2018 21:34:40 +0900 Subject: add set property action in background --- src/background/actions/index.js | 1 + src/background/actions/setting.js | 10 +++++++++- src/background/reducers/setting.js | 9 ++++++++- test/background/reducers/setting.test.js | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/src/background/actions/index.js b/src/background/actions/index.js index 8c212c2..efe4074 100644 --- a/src/background/actions/index.js +++ b/src/background/actions/index.js @@ -1,4 +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 index 0454a68..773142f 100644 --- a/src/background/actions/setting.js +++ b/src/background/actions/setting.js @@ -10,4 +10,12 @@ const load = () => { }); }; -export { load }; +const setProperty = (name, value) => { + return { + type: actions.SETTING_SET_PROPERTY, + name, + value, + }; +}; + +export { load, setProperty }; diff --git a/src/background/reducers/setting.js b/src/background/reducers/setting.js index 70bf8ea..045a654 100644 --- a/src/background/reducers/setting.js +++ b/src/background/reducers/setting.js @@ -1,4 +1,4 @@ -import actions from 'settings/actions'; +import actions from 'background/actions'; const defaultState = { value: {}, @@ -10,6 +10,13 @@ export default function reducer(state = defaultState, action = {}) { 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/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js index a6a5573..2ef98cb 100644 --- a/test/background/reducers/setting.test.js +++ b/test/background/reducers/setting.test.js @@ -16,4 +16,22 @@ describe("setting reducer", () => { 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'); + }); }); -- cgit v1.2.3 From dda4e7475cdd092d00441c7cd0ceb194ee5dee3d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 11 Jan 2018 20:07:25 +0900 Subject: move commands to background action --- src/background/actions/command.js | 62 ++++++++++++++++++++++++ src/background/components/background.js | 3 +- src/shared/commands/exec.js | 85 --------------------------------- src/shared/commands/index.js | 3 +- src/shared/commands/parsers.js | 59 +++++++++++++++++++++++ src/shared/commands/properties.js | 31 ------------ test/shared/commands/parsers.test.js | 78 ++++++++++++++++++++++++++++++ test/shared/commands/property.test.js | 41 ---------------- 8 files changed, 202 insertions(+), 160 deletions(-) create mode 100644 src/background/actions/command.js delete mode 100644 src/shared/commands/exec.js create mode 100644 src/shared/commands/parsers.js delete mode 100644 src/shared/commands/properties.js create mode 100644 test/shared/commands/parsers.test.js delete mode 100644 test/shared/commands/property.test.js (limited to 'test') diff --git a/src/background/actions/command.js b/src/background/actions/command.js new file mode 100644 index 0000000..f11c61b --- /dev/null +++ b/src/background/actions/command.js @@ -0,0 +1,62 @@ +import * as tabs from 'background/tabs'; +import * as parsers from 'shared/commands/parsers'; + +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 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 '': + return Promise.resolve(); + } + throw new Error(name + ' command is not defined'); +}; + +export { exec }; diff --git a/src/background/components/background.js b/src/background/components/background.js index 22c6693..19bf27f 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -1,5 +1,6 @@ import messages from 'shared/messages'; import * as operationActions from 'background/actions/operation'; +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,7 +36,7 @@ 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 commandActions.exec(message.text, settings.value).catch((e) => { return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, diff --git a/src/shared/commands/exec.js b/src/shared/commands/exec.js deleted file mode 100644 index 7248827..0000000 --- a/src/shared/commands/exec.js +++ /dev/null @@ -1,85 +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 exec = (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'); -}; - -export default exec; diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js index c2cea3e..78cb4df 100644 --- a/src/shared/commands/index.js +++ b/src/shared/commands/index.js @@ -1,4 +1,3 @@ -import exec from './exec'; import complete from './complete'; -export { exec, complete }; +export { complete }; diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js new file mode 100644 index 0000000..af51338 --- /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) { + 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/commands/properties.js b/src/shared/commands/properties.js deleted file mode 100644 index 8a3213d..0000000 --- a/src/shared/commands/properties.js +++ /dev/null @@ -1,31 +0,0 @@ -const mustNumber = (v) => { - let num = Number(v); - if (isNaN(num)) { - throw new Error('Not number: ' + v); - } - return num; -}; - -const parseProperty = (word, types) => { - let [key, value] = word.split('='); - if (!value) { - 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]; - } -}; - -export { parseProperty }; diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js new file mode 100644 index 0000000..200323c --- /dev/null +++ b/test/shared/commands/parsers.test.js @@ -0,0 +1,78 @@ +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 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('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/commands/property.test.js b/test/shared/commands/property.test.js deleted file mode 100644 index d949482..0000000 --- a/test/shared/commands/property.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from "chai"; -import { parseProperty } from 'shared/commands/properties'; - -describe("shared/commands/properties", () => { - describe("#parseProperty", () => { - it('parse set string', () => { - let [key, value] = parseProperty('encoding=utf-8', { encoding: 'string' }); - expect(key).to.equal('encoding'); - expect(value).to.equal('utf-8'); - }); - - it('parse set string', () => { - let [key, value] = parseProperty('history=50', { history: 'number' }); - expect(key).to.equal('history'); - expect(value).to.equal(50); - }); - - it('parse set boolean', () => { - let [key, value] = parseProperty('paste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.true; - - [key, value] = parseProperty('nopaste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.false; - }); - - it('throws error on unknown property', () => { - expect(() => parseProperty('charset=utf-8', {})).to.throw(Error, 'Unknown'); - expect(() => parseProperty('smoothscroll', {})).to.throw(Error, 'Unknown'); - expect(() => parseProperty('nosmoothscroll', {})).to.throw(Error, 'Unknown'); - }) - - it('throws error on invalid property', () => { - expect(() => parseProperty('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); - expect(() => parseProperty('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parseProperty('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); - expect(() => parseProperty('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); - }) - }); -}); -- cgit v1.2.3 From befcff973aa0fd9b0f3a73932a39d36e9eb85bf2 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 11 Jan 2018 20:14:56 +0900 Subject: fix property parser --- src/shared/commands/parsers.js | 2 +- test/shared/commands/parsers.test.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js index af51338..fb37d2a 100644 --- a/src/shared/commands/parsers.js +++ b/src/shared/commands/parsers.js @@ -30,7 +30,7 @@ const mustNumber = (v) => { const parseSetOption = (word, types) => { let [key, value] = word.split('='); - if (!value) { + if (value === undefined) { value = !key.startsWith('no'); key = value ? key : key.slice(2); } diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js index 200323c..0a1960c 100644 --- a/test/shared/commands/parsers.test.js +++ b/test/shared/commands/parsers.test.js @@ -9,6 +9,12 @@ describe("shared/commands/parsers", () => { 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'); @@ -34,6 +40,7 @@ describe("shared/commands/parsers", () => { 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'); }) -- cgit v1.2.3 From 37410b874f89de4907ba038244318129835e8157 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 4 Mar 2018 17:57:56 +0900 Subject: add hide action for console --- src/console/actions/console.js | 8 +++++++- src/console/actions/index.js | 1 + src/console/index.js | 2 ++ src/console/reducers/index.js | 4 ++++ test/console/actions/console.test.js | 6 ++++++ test/console/reducers/console.test.js | 6 ++++++ 6 files changed, 26 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/src/console/actions/console.js b/src/console/actions/console.js index 2cf8e8d..f80045f 100644 --- a/src/console/actions/console.js +++ b/src/console/actions/console.js @@ -1,5 +1,11 @@ import actions from 'console/actions'; +const hide = () => { + return { + type: actions.CONSOLE_HIDE, + }; +}; + const showCommand = (text) => { return { type: actions.CONSOLE_SHOW_COMMAND, @@ -61,6 +67,6 @@ const completionPrev = () => { }; export { - showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, + hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, setCompletions, completionNext, completionPrev }; diff --git a/src/console/actions/index.js b/src/console/actions/index.js index a85e329..b394179 100644 --- a/src/console/actions/index.js +++ b/src/console/actions/index.js @@ -1,5 +1,6 @@ export default { // console commands + CONSOLE_HIDE: 'console.hide', CONSOLE_SHOW_COMMAND: 'console.show.command', CONSOLE_SHOW_ERROR: 'console.show.error', CONSOLE_SHOW_INFO: 'console.show.info', diff --git a/src/console/index.js b/src/console/index.js index 86edd9a..156456c 100644 --- a/src/console/index.js +++ b/src/console/index.js @@ -24,6 +24,8 @@ const onMessage = (message) => { return store.dispatch(consoleActions.showError(message.text)); case messages.CONSOLE_SHOW_INFO: return store.dispatch(consoleActions.showInfo(message.text)); + case messages.CONSOLE_HIDE: + return store.dispatch(consoleActions.hide()); } }; diff --git a/src/console/reducers/index.js b/src/console/reducers/index.js index 60c0007..2aec55c 100644 --- a/src/console/reducers/index.js +++ b/src/console/reducers/index.js @@ -53,6 +53,10 @@ const nextConsoleText = (completions, group, item, defaults) => { export default function reducer(state = defaultState, action = {}) { switch (action.type) { + case actions.CONSOLE_HIDE: + return Object.assign({}, state, { + mode: '', + }); case actions.CONSOLE_SHOW_COMMAND: return Object.assign({}, state, { mode: 'command', diff --git a/test/console/actions/console.test.js b/test/console/actions/console.test.js index 9af13d4..1774431 100644 --- a/test/console/actions/console.test.js +++ b/test/console/actions/console.test.js @@ -3,6 +3,12 @@ import actions from 'console/actions'; import * as consoleActions from 'console/actions/console'; describe("console actions", () => { + describe('hide', () => { + it('create CONSOLE_HIDE action', () => { + let action = consoleActions.hide(); + expect(action.type).to.equal(actions.CONSOLE_HIDE); + }); + }); describe("showCommand", () => { it('create CONSOLE_SHOW_COMMAND action', () => { let action = consoleActions.showCommand('hello'); diff --git a/test/console/reducers/console.test.js b/test/console/reducers/console.test.js index 438d513..d196011 100644 --- a/test/console/reducers/console.test.js +++ b/test/console/reducers/console.test.js @@ -13,6 +13,12 @@ describe("console reducer", () => { expect(state).to.have.property('itemSelection', -1); }); + it('return next state for CONSOLE_HIDE', () => { + let action = { type: actions.CONSOLE_HIDE }; + let state = reducer({ mode: 'error' }, action); + expect(state).to.have.property('mode', ''); + }) + it('return next state for CONSOLE_SHOW_COMMAND', () => { let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' }; let state = reducer({}, action); -- cgit v1.2.3 From 90b83d7b7b99598834466c97e9c74c82cbd25cf3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 4 Mar 2018 18:32:24 +0900 Subject: hide console on and --- src/background/actions/operation.js | 4 ++++ src/content/actions/setting.js | 9 ++++++++- src/shared/messages.js | 1 + src/shared/operations.js | 3 +++ test/background/reducers/setting.test.js | 1 - test/content/actions/setting.test.js | 2 ++ 6 files changed, 18 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/src/background/actions/operation.js b/src/background/actions/operation.js index 1188ea2..56eb168 100644 --- a/src/background/actions/operation.js +++ b/src/background/actions/operation.js @@ -73,6 +73,10 @@ const exec = (operation, tab) => { return browser.tabs.sendMessage(tab.id, { type: messages.CONSOLE_SHOW_FIND }); + case operations.CANCEL: + return browser.tabs.sendMessage(tab.id, { + type: messages.CONSOLE_HIDE, + }); default: return Promise.resolve(); } diff --git a/src/content/actions/setting.js b/src/content/actions/setting.js index 0238c71..4c1e385 100644 --- a/src/content/actions/setting.js +++ b/src/content/actions/setting.js @@ -1,10 +1,17 @@ import actions from 'content/actions'; import * as keyUtils from 'shared/utils/keys'; +import operations from 'shared/operations'; + +const reservedKeymaps = { + '': { type: operations.CANCEL }, + '': { type: operations.CANCEL }, +}; const set = (value) => { let entries = []; if (value.keymaps) { - entries = Object.entries(value.keymaps).map((entry) => { + let keymaps = Object.assign({}, value.keymaps, reservedKeymaps); + entries = Object.entries(keymaps).map((entry) => { return [ keyUtils.fromMapKeys(entry[0]), entry[1], diff --git a/src/shared/messages.js b/src/shared/messages.js index de00a3f..b7a1a7e 100644 --- a/src/shared/messages.js +++ b/src/shared/messages.js @@ -32,6 +32,7 @@ export default { CONSOLE_SHOW_ERROR: 'console.show.error', CONSOLE_SHOW_INFO: 'console.show.info', CONSOLE_SHOW_FIND: 'console.show.find', + CONSOLE_HIDE: 'console.hide', FOLLOW_START: 'follow.start', FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets', diff --git a/src/shared/operations.js b/src/shared/operations.js index 008e9eb..a2f980f 100644 --- a/src/shared/operations.js +++ b/src/shared/operations.js @@ -1,4 +1,7 @@ export default { + // Hide console, or cancel some user actions + CANCEL: 'cancel', + // Addons ADDON_ENABLE: 'addon.enable', ADDON_DISABLE: 'addon.disable', diff --git a/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js index 2ef98cb..8df5abe 100644 --- a/test/background/reducers/setting.test.js +++ b/test/background/reducers/setting.test.js @@ -30,7 +30,6 @@ describe("setting reducer", () => { }; 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/content/actions/setting.test.js b/test/content/actions/setting.test.js index 1248edf..3112b2d 100644 --- a/test/content/actions/setting.test.js +++ b/test/content/actions/setting.test.js @@ -23,6 +23,8 @@ describe("setting actions", () => { let map = new Map(keymaps); expect(map).to.have.deep.all.keys( [ + [{ key: 'Esc', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], + [{ key: '[', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, -- cgit v1.2.3 From 24e72aa6e079009d4c24c7f6be6bfb82c21e6643 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 7 Mar 2018 20:54:28 +0900 Subject: add global find keyword --- src/background/actions/find.js | 10 ++++++++++ src/background/actions/index.js | 3 +++ src/background/components/background.js | 8 ++++++++ src/background/reducers/find.js | 16 ++++++++++++++++ src/background/reducers/index.js | 3 +++ src/shared/messages.js | 2 ++ test/background/actions/find.test.js | 13 +++++++++++++ test/background/reducers/find.test.js | 19 +++++++++++++++++++ 8 files changed, 74 insertions(+) create mode 100644 src/background/actions/find.js create mode 100644 src/background/reducers/find.js create mode 100644 test/background/actions/find.test.js create mode 100644 test/background/reducers/find.test.js (limited to 'test') diff --git a/src/background/actions/find.js b/src/background/actions/find.js new file mode 100644 index 0000000..8da5572 --- /dev/null +++ b/src/background/actions/find.js @@ -0,0 +1,10 @@ +import actions from './index'; + +const setKeyword = (keyword) => { + return { + type: actions.FIND_SET_KEYWORD, + keyword, + }; +}; + +export { setKeyword }; diff --git a/src/background/actions/index.js b/src/background/actions/index.js index efe4074..2bdaaf2 100644 --- a/src/background/actions/index.js +++ b/src/background/actions/index.js @@ -2,4 +2,7 @@ export default { // Settings SETTING_SET_SETTINGS: 'setting.set.settings', SETTING_SET_PROPERTY: 'setting.set.property', + + // Find + FIND_SET_KEYWORD: 'find.set.keyword', }; diff --git a/src/background/components/background.js b/src/background/components/background.js index c4f436a..fdec8ec 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -2,6 +2,7 @@ import messages from 'shared/messages'; import * as operationActions from 'background/actions/operation'; import * as commandActions from 'background/actions/command'; import * as settingActions from 'background/actions/setting'; +import * as findActions from 'background/actions/find'; import * as tabActions from 'background/actions/tab'; import * as commands from 'shared/commands'; @@ -23,6 +24,8 @@ export default class BackgroundComponent { onMessage(message, sender) { let settings = this.store.getState().setting; + let find = this.store.getState().find; + switch (message.type) { case messages.BACKGROUND_OPERATION: return this.store.dispatch( @@ -48,6 +51,11 @@ export default class BackgroundComponent { case messages.SETTINGS_RELOAD: this.store.dispatch(settingActions.load()); return this.broadcastSettingsChanged(); + case messages.FIND_GET_KEYWORD: + return Promise.resolve(find.keyword); + case messages.FIND_SET_KEYWORD: + this.store.dispatch(findActions.setKeyword(message.keyword)); + return Promise.resolve({}); } } diff --git a/src/background/reducers/find.js b/src/background/reducers/find.js new file mode 100644 index 0000000..4ded801 --- /dev/null +++ b/src/background/reducers/find.js @@ -0,0 +1,16 @@ +import actions from 'content/actions'; + +const defaultState = { + keyword: null, +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.FIND_SET_KEYWORD: + return Object.assign({}, state, { + keyword: action.keyword, + }); + default: + return state; + } +} diff --git a/src/background/reducers/index.js b/src/background/reducers/index.js index dab0c62..63ff0f8 100644 --- a/src/background/reducers/index.js +++ b/src/background/reducers/index.js @@ -1,12 +1,15 @@ import settingReducer from './setting'; +import findReducer from './find'; // Make setting reducer instead of re-use const defaultState = { setting: settingReducer(undefined, {}), + find: findReducer(undefined, {}), }; export default function reducer(state = defaultState, action = {}) { return Object.assign({}, state, { setting: settingReducer(state.setting, action), + find: findReducer(state.find, action), }); } diff --git a/src/shared/messages.js b/src/shared/messages.js index b7a1a7e..a404658 100644 --- a/src/shared/messages.js +++ b/src/shared/messages.js @@ -45,6 +45,8 @@ export default { FIND_NEXT: 'find.next', FIND_PREV: 'find.prev', + FIND_GET_KEYWORD: 'find.get.keyword', + FIND_SET_KEYWORD: 'find.set.keyword', OPEN_URL: 'open.url', diff --git a/test/background/actions/find.test.js b/test/background/actions/find.test.js new file mode 100644 index 0000000..467604f --- /dev/null +++ b/test/background/actions/find.test.js @@ -0,0 +1,13 @@ +import { expect } from "chai"; +import actions from 'background/actions'; +import * as findActions from 'background/actions/find'; + +describe("find actions", () => { + describe("setKeyword", () => { + it('create FIND_SET_KEYWORD action', () => { + let action = findActions.setKeyword('banana'); + expect(action.type).to.equal(actions.FIND_SET_KEYWORD); + expect(action.keyword).to.equal('banana'); + }); + }); +}); diff --git a/test/background/reducers/find.test.js b/test/background/reducers/find.test.js new file mode 100644 index 0000000..707d73a --- /dev/null +++ b/test/background/reducers/find.test.js @@ -0,0 +1,19 @@ +import { expect } from "chai"; +import actions from 'background/actions'; +import findReducer from 'background/reducers/find'; + +describe("find reducer", () => { + it('return the initial state', () => { + let state = findReducer(undefined, {}); + expect(state).to.have.deep.property('keyword', null); + }); + + it('return next state for FIND_SET_KEYWORD', () => { + let action = { + type: actions.FIND_SET_KEYWORD, + keyword: 'cherry', + }; + let state = findReducer(undefined, action); + expect(state).to.have.deep.property('keyword', 'cherry') + }); +}); -- cgit v1.2.3 From 92f8365be7127c3fa0276b1a6e890571f634622e Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 7 Mar 2018 21:05:52 +0900 Subject: set find keyword from background --- src/content/actions/find.js | 51 ++++++++++++++++++++++++-------------- src/content/reducers/find.js | 2 +- test/content/reducers/find.test.js | 2 +- 3 files changed, 35 insertions(+), 20 deletions(-) (limited to 'test') diff --git a/src/content/actions/find.js b/src/content/actions/find.js index b266216..c7345cc 100644 --- a/src/content/actions/find.js +++ b/src/content/actions/find.js @@ -5,6 +5,7 @@ // NOTE: window.find is not standard API // https://developer.mozilla.org/en-US/docs/Web/API/Window/find +import messages from 'shared/messages'; import actions from 'content/actions'; import * as consoleFrames from '../console-frames'; @@ -31,35 +32,49 @@ const find = (string, backwards) => { return window.find(string, caseSensitive, backwards, wrapScan); }; -const findNext = (keyword, reset, backwards) => { +const findNext = (currentKeyword, reset, backwards) => { if (reset) { window.getSelection().removeAllRanges(); } - let found = find(keyword, backwards); - if (!found) { - window.getSelection().removeAllRanges(); - found = find(keyword, backwards); - } - if (found) { - postPatternFound(keyword); + let promise = Promise.resolve(currentKeyword); + if (currentKeyword) { + browser.runtime.sendMessage({ + type: messages.FIND_SET_KEYWORD, + keyword: currentKeyword, + }); } else { - postPatternNotFound(keyword); + promise = browser.runtime.sendMessage({ + type: messages.FIND_GET_KEYWORD, + }); } - return { - type: actions.FIND_SET_KEYWORD, - keyword, - found, - }; + return promise.then((keyword) => { + let found = find(keyword, backwards); + if (!found) { + window.getSelection().removeAllRanges(); + found = find(keyword, backwards); + } + if (found) { + postPatternFound(keyword); + } else { + postPatternNotFound(keyword); + } + + return { + type: actions.FIND_SET_KEYWORD, + keyword, + found, + }; + }); }; -const next = (keyword, reset) => { - return findNext(keyword, reset, false); +const next = (currentKeyword, reset) => { + return findNext(currentKeyword, reset, false); }; -const prev = (keyword, reset) => { - return findNext(keyword, reset, true); +const prev = (currentKeyword, reset) => { + return findNext(currentKeyword, reset, true); }; export { next, prev }; diff --git a/src/content/reducers/find.js b/src/content/reducers/find.js index eb43c37..8d63ee5 100644 --- a/src/content/reducers/find.js +++ b/src/content/reducers/find.js @@ -1,7 +1,7 @@ import actions from 'content/actions'; const defaultState = { - keyword: '', + keyword: null, found: false, }; diff --git a/test/content/reducers/find.test.js b/test/content/reducers/find.test.js index 93625da..908b01b 100644 --- a/test/content/reducers/find.test.js +++ b/test/content/reducers/find.test.js @@ -5,7 +5,7 @@ import findReducer from 'content/reducers/find'; describe("find reducer", () => { it('return the initial state', () => { let state = findReducer(undefined, {}); - expect(state).to.have.property('keyword', ''); + expect(state).to.have.property('keyword', null); expect(state).to.have.property('found', false); }); -- cgit v1.2.3