commit
						ab6295d0f1
					
				
					 16 changed files with 1514 additions and 5269 deletions
				
			
		| 
						 | 
				
			
			@ -75,10 +75,6 @@ jobs:
 | 
			
		|||
      - checkout
 | 
			
		||||
      - setup_npm
 | 
			
		||||
      - run: npm run build
 | 
			
		||||
      - run:
 | 
			
		||||
          name: Run geckodriver
 | 
			
		||||
          command: geckodriver
 | 
			
		||||
          background: true
 | 
			
		||||
      - run: npm run test:e2e
 | 
			
		||||
 | 
			
		||||
workflows:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@
 | 
			
		|||
    "default-case": "off",
 | 
			
		||||
    "dot-location": ["error", "property"],
 | 
			
		||||
    "function-paren-newline": "off",
 | 
			
		||||
    "function-call-argument-newline": ["error", "consistent"],
 | 
			
		||||
    "id-length": "off",
 | 
			
		||||
    "indent": ["error", 2],
 | 
			
		||||
    "init-declarations": "off",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										48
									
								
								QA.md
									
										
									
									
									
								
							
							
						
						
									
										48
									
								
								QA.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -25,46 +25,10 @@ The behaviors of the console are tested in [Console section](#consoles).
 | 
			
		|||
 | 
			
		||||
### Settings
 | 
			
		||||
 | 
			
		||||
#### JSON Settings
 | 
			
		||||
 | 
			
		||||
##### Validations
 | 
			
		||||
 | 
			
		||||
- [ ] show error on invalid json
 | 
			
		||||
- [ ] show error when top-level keys has keys other than `keymaps`, `search`, `blacklist`, and `properties`
 | 
			
		||||
 | 
			
		||||
###### `"keymaps"` section
 | 
			
		||||
 | 
			
		||||
- [ ] show error on unknown operation name in `"keymaps"`
 | 
			
		||||
 | 
			
		||||
###### `"search"` section
 | 
			
		||||
 | 
			
		||||
- validations in `"search"` section are not tested in this release
 | 
			
		||||
 | 
			
		||||
##### Updating
 | 
			
		||||
 | 
			
		||||
- [ ] changes are updated on textarea blure when no errors
 | 
			
		||||
- [ ] keymap settings are applied to open tabs without reload
 | 
			
		||||
- [ ] search settings are applied to open tabs without reload
 | 
			
		||||
 | 
			
		||||
##### Properties
 | 
			
		||||
 | 
			
		||||
- [ ] show errors when invalid property name
 | 
			
		||||
- [ ] show errors when invalid property type
 | 
			
		||||
 | 
			
		||||
#### Form Settings
 | 
			
		||||
 | 
			
		||||
<!-- validation on form settings does not implement in 0.7 -->
 | 
			
		||||
 | 
			
		||||
##### Search Engines
 | 
			
		||||
 | 
			
		||||
- [ ] able to change default
 | 
			
		||||
- [ ] able to remove item
 | 
			
		||||
- [ ] able to add item
 | 
			
		||||
 | 
			
		||||
##### `"blacklist"` section
 | 
			
		||||
 | 
			
		||||
- [ ] able to add item
 | 
			
		||||
- [ ] able to remove item
 | 
			
		||||
- [ ] `github.com/a` blocks `github.com/a`, and not blocks `github.com/aa`
 | 
			
		||||
 | 
			
		||||
##### Updating
 | 
			
		||||
| 
						 | 
				
			
			@ -72,12 +36,6 @@ The behaviors of the console are tested in [Console section](#consoles).
 | 
			
		|||
- [ ] keymap settings are applied to open tabs without reload
 | 
			
		||||
- [ ] search settings are applied to open tabs without reload
 | 
			
		||||
 | 
			
		||||
### Settings source
 | 
			
		||||
 | 
			
		||||
- [ ] show confirmation dialog on switched from json to form
 | 
			
		||||
- [ ] state is saved on source changed
 | 
			
		||||
- [ ] on switching form -> json -> form, first and last form setting is equivalent to first one
 | 
			
		||||
 | 
			
		||||
## Find mode
 | 
			
		||||
 | 
			
		||||
- [ ] open console with <kbd>/</kbd>
 | 
			
		||||
| 
						 | 
				
			
			@ -86,9 +44,3 @@ The behaviors of the console are tested in [Console section](#consoles).
 | 
			
		|||
- [ ] Wrap search by <kbd>n</kbd>/<kbd>N</kbd>
 | 
			
		||||
- [ ] Find with last keyword if keyword is empty
 | 
			
		||||
- [ ] Find keyword last used on new tab opened
 | 
			
		||||
 | 
			
		||||
## Misc
 | 
			
		||||
 | 
			
		||||
- [ ] Work on `about:blank`
 | 
			
		||||
- [ ] Able to map `<A-Z>` key.
 | 
			
		||||
- [ ] Open file menu by <kbd>Alt</kbd>+<kbd>F</kbd> (Other than Mac OS)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ describe("completion on open/tabopen/winopen commands", () => {
 | 
			
		|||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Add item into hitories
 | 
			
		||||
    await session.navigateTo(`https://i-beam.org`);
 | 
			
		||||
    await session.navigateTo(`https://i-beam.org/404`);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  after(async() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										99
									
								
								e2e/options.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								e2e/options.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
const express = require('express');
 | 
			
		||||
const lanthan = require('lanthan');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const assert = require('assert');
 | 
			
		||||
const eventually = require('./eventually');
 | 
			
		||||
 | 
			
		||||
const newApp = () => {
 | 
			
		||||
  let app = express();
 | 
			
		||||
  app.get('/', (req, res) => {
 | 
			
		||||
    res.send(`<!DOCTYPEhtml>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <body style="width:10000px; height:10000px"></body>
 | 
			
		||||
</html">`);
 | 
			
		||||
  });
 | 
			
		||||
  return app;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("options page", () => {
 | 
			
		||||
  const port = 12321;
 | 
			
		||||
  let http;
 | 
			
		||||
  let firefox;
 | 
			
		||||
  let session;
 | 
			
		||||
  let browser;
 | 
			
		||||
 | 
			
		||||
  before(async() => {
 | 
			
		||||
    http = newApp().listen(port);
 | 
			
		||||
 | 
			
		||||
    firefox = await lanthan.firefox({
 | 
			
		||||
      spy: path.join(__dirname, '..'),
 | 
			
		||||
      builderf: (builder) => {
 | 
			
		||||
        builder.addFile('build/settings.js');
 | 
			
		||||
        builder.addFile('build/settings.html');
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await firefox.session.installAddonFromPath(path.join(__dirname, '..'));
 | 
			
		||||
    session = firefox.session;
 | 
			
		||||
    browser = firefox.browser;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  after(async() => {
 | 
			
		||||
    if (firefox) {
 | 
			
		||||
      await firefox.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    http.close();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(async() => {
 | 
			
		||||
    let tabs = await browser.tabs.query({});
 | 
			
		||||
    for (let tab of tabs.slice(1)) {
 | 
			
		||||
      await browser.tabs.remove(tab.id);
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const updateTextarea = async(value) => {
 | 
			
		||||
    let textarea = await session.findElementByCSS('textarea');
 | 
			
		||||
    await session.executeScript(`document.querySelector('textarea').value = '${value}'`)
 | 
			
		||||
    await textarea.sendKeys(' ');
 | 
			
		||||
    await session.executeScript(() => document.querySelector('textarea').blur());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it('saves current config on blur', async () => {
 | 
			
		||||
    let url = await browser.runtime.getURL("build/settings.html")
 | 
			
		||||
    await session.navigateTo(url);
 | 
			
		||||
 | 
			
		||||
    await updateTextarea(`{ "blacklist": [ "https://example.com" ] }`);
 | 
			
		||||
 | 
			
		||||
    let { settings } = await browser.storage.local.get('settings');
 | 
			
		||||
    assert.equal(settings.source, 'json')
 | 
			
		||||
    assert.equal(settings.json, '{ "blacklist": [ "https://example.com" ] } ')
 | 
			
		||||
 | 
			
		||||
    await updateTextarea(`invalid json`);
 | 
			
		||||
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.equal(settings.source, 'json')
 | 
			
		||||
    assert.equal(settings.json, '{ "blacklist": [ "https://example.com" ] } ')
 | 
			
		||||
 | 
			
		||||
    let error = await session.findElementByCSS('.settings-ui-input-error');
 | 
			
		||||
    let text = await error.getText();
 | 
			
		||||
    assert.ok(text.startsWith('SyntaxError:'))
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('updates keymaps without reloading', async () => {
 | 
			
		||||
    await browser.tabs.create({ url: `http://127.0.0.1:${port}`, active: false });
 | 
			
		||||
    let url = await browser.runtime.getURL("build/settings.html")
 | 
			
		||||
    await session.navigateTo(url);
 | 
			
		||||
 | 
			
		||||
    let handles = await session.getWindowHandles();
 | 
			
		||||
    await updateTextarea(`{ "keymaps": { "zz": { "type": "scroll.vertically", "count": 10 } } }`);
 | 
			
		||||
 | 
			
		||||
    await session.switchToWindow(handles[1]);
 | 
			
		||||
 | 
			
		||||
    let body = await session.findElementByCSS('body');
 | 
			
		||||
    await body.sendKeys('zz')
 | 
			
		||||
 | 
			
		||||
    let y = await session.executeScript(() => window.pageYOffset);
 | 
			
		||||
    assert.equal(y, 640);
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										125
									
								
								e2e/options_form.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								e2e/options_form.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
const lanthan = require('lanthan');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const assert = require('assert');
 | 
			
		||||
 | 
			
		||||
describe("options form page", () => {
 | 
			
		||||
  let firefox;
 | 
			
		||||
  let session;
 | 
			
		||||
  let browser;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async() => {
 | 
			
		||||
    firefox = await lanthan.firefox({
 | 
			
		||||
      spy: path.join(__dirname, '..'),
 | 
			
		||||
      builderf: (builder) => {
 | 
			
		||||
        builder.addFile('build/settings.js');
 | 
			
		||||
        builder.addFile('build/settings.html');
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await firefox.session.installAddonFromPath(path.join(__dirname, '..'));
 | 
			
		||||
    session = firefox.session;
 | 
			
		||||
    browser = firefox.browser;
 | 
			
		||||
 | 
			
		||||
    let tabs = await browser.tabs.query({});
 | 
			
		||||
    for (let tab of tabs.slice(1)) {
 | 
			
		||||
      await browser.tabs.remove(tab.id);
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  afterEach(async() => {
 | 
			
		||||
    if (firefox) {
 | 
			
		||||
      await firefox.close();
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const setBlacklistValue = async(nth, value) => {
 | 
			
		||||
    let selector = '.form-blacklist-form .column-url';
 | 
			
		||||
    let input = (await session.findElementsByCSS(selector))[nth];
 | 
			
		||||
    await input.sendKeys(value);
 | 
			
		||||
    await session.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const setSearchEngineValue = async(nth, name, url) => {
 | 
			
		||||
    let selector = '.form-search-form input.column-name';
 | 
			
		||||
    let input = (await session.findElementsByCSS(selector))[nth];
 | 
			
		||||
    await input.sendKeys(name);
 | 
			
		||||
    await session.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`);
 | 
			
		||||
 | 
			
		||||
    selector = '.form-search-form input.column-url';
 | 
			
		||||
    input = (await session.findElementsByCSS(selector))[nth];
 | 
			
		||||
    await input.sendKeys(url);
 | 
			
		||||
    await session.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it('switch to form settings', async () => {
 | 
			
		||||
    let url = await browser.runtime.getURL("build/settings.html")
 | 
			
		||||
    await session.navigateTo(url);
 | 
			
		||||
 | 
			
		||||
    let useFormInput = await session.findElementByCSS('#setting-source-form');
 | 
			
		||||
    await useFormInput.click();
 | 
			
		||||
    await session.acceptAlert();
 | 
			
		||||
 | 
			
		||||
    let { settings } = await browser.storage.local.get('settings');
 | 
			
		||||
    assert.equal(settings.source, 'form')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('add blacklist', async () => {
 | 
			
		||||
    let url = await browser.runtime.getURL("build/settings.html")
 | 
			
		||||
    await session.navigateTo(url);
 | 
			
		||||
 | 
			
		||||
    let useFormInput = await session.findElementByCSS('#setting-source-form');
 | 
			
		||||
    await useFormInput.click();
 | 
			
		||||
    await session.acceptAlert();
 | 
			
		||||
    await session.executeScript(() => window.scrollBy(0, 1000));
 | 
			
		||||
 | 
			
		||||
    // assert default
 | 
			
		||||
    let settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.blacklist, [])
 | 
			
		||||
 | 
			
		||||
    // add blacklist items
 | 
			
		||||
    let addButton = await session.findElementByCSS('.form-blacklist-form .ui-add-button');
 | 
			
		||||
    await addButton.click();
 | 
			
		||||
    await setBlacklistValue(0, 'google.com')
 | 
			
		||||
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.blacklist, ['google.com'])
 | 
			
		||||
 | 
			
		||||
    await addButton.click();
 | 
			
		||||
    await setBlacklistValue(1, 'yahoo.com')
 | 
			
		||||
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.blacklist, ['google.com', 'yahoo.com'])
 | 
			
		||||
 | 
			
		||||
    // delete first item
 | 
			
		||||
    let deleteButton = (await session.findElementsByCSS('.form-blacklist-form .ui-delete-button'))[0];
 | 
			
		||||
    await deleteButton.click()
 | 
			
		||||
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.blacklist, ['yahoo.com'])
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('add search engines', async () => {
 | 
			
		||||
    let url = await browser.runtime.getURL("build/settings.html")
 | 
			
		||||
    await session.navigateTo(url);
 | 
			
		||||
 | 
			
		||||
    let useFormInput = await session.findElementByCSS('#setting-source-form');
 | 
			
		||||
    await useFormInput.click();
 | 
			
		||||
    await session.acceptAlert();
 | 
			
		||||
 | 
			
		||||
    // assert default
 | 
			
		||||
    let settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.search.default, 'google');
 | 
			
		||||
 | 
			
		||||
    // change default
 | 
			
		||||
    let radio = (await session.findElementsByCSS('.form-search-form input[type=radio]'))[2];
 | 
			
		||||
    await radio.click();
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.search.default, 'bing');
 | 
			
		||||
 | 
			
		||||
    let addButton = await session.findElementByCSS('.form-search-form .ui-add-button');
 | 
			
		||||
    await addButton.click();
 | 
			
		||||
    await setSearchEngineValue(6, 'yippy', 'https://www.yippy.com/search?query={}');
 | 
			
		||||
 | 
			
		||||
    settings = (await browser.storage.local.get('settings')).settings;
 | 
			
		||||
    assert.deepEqual(settings.form.search.engines[6], ['yippy', 'https://www.yippy.com/search?query={}']);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										6439
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										6439
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										63
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										63
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -3,12 +3,12 @@
 | 
			
		|||
  "description": "Vim vixen",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "webpack --mode development -w --debug --devtool inline-source-map",
 | 
			
		||||
    "build": "NODE_ENV=production webpack --mode production --progress --display-error-details",
 | 
			
		||||
    "build": "NODE_ENV=production webpack --mode production --progress --display-error-details --devtool inline-source-map",
 | 
			
		||||
    "package": "npm run build && script/package",
 | 
			
		||||
    "lint": "eslint --ext .js,.jsx,.ts,.tsx src",
 | 
			
		||||
    "type-checks": "tsc --noEmit",
 | 
			
		||||
    "test": "karma start",
 | 
			
		||||
    "test:e2e": "mocha --timeout 8000 --retries 5 e2e"
 | 
			
		||||
    "test:e2e": "mocha --timeout 10000 --retries 5 e2e"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
| 
						 | 
				
			
			@ -21,49 +21,50 @@
 | 
			
		|||
  },
 | 
			
		||||
  "homepage": "https://github.com/ueokande/vim-vixen",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/chai": "^4.1.7",
 | 
			
		||||
    "@types/mocha": "^5.2.6",
 | 
			
		||||
    "@types/chai": "^4.2.0",
 | 
			
		||||
    "@types/mocha": "^5.2.7",
 | 
			
		||||
    "@types/prop-types": "^15.7.1",
 | 
			
		||||
    "@types/react": "^16.8.18",
 | 
			
		||||
    "@types/react-dom": "^16.8.4",
 | 
			
		||||
    "@types/react-redux": "^7.0.9",
 | 
			
		||||
    "@types/react": "^16.9.2",
 | 
			
		||||
    "@types/react-dom": "^16.9.0",
 | 
			
		||||
    "@types/react-redux": "^7.1.2",
 | 
			
		||||
    "@types/redux-promise": "^0.5.28",
 | 
			
		||||
    "@types/sinon": "^7.0.11",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^1.9.0",
 | 
			
		||||
    "@types/sinon": "^7.0.13",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^2.0.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^2.0.0",
 | 
			
		||||
    "chai": "^4.2.0",
 | 
			
		||||
    "css-loader": "^2.1.1",
 | 
			
		||||
    "eslint": "^5.16.0",
 | 
			
		||||
    "eslint-plugin-react": "^7.13.0",
 | 
			
		||||
    "css-loader": "^3.2.0",
 | 
			
		||||
    "eslint": "^6.2.2",
 | 
			
		||||
    "eslint-plugin-react": "^7.14.3",
 | 
			
		||||
    "html-webpack-plugin": "^3.2.0",
 | 
			
		||||
    "jszip": "^3.2.1",
 | 
			
		||||
    "karma": "^4.1.0",
 | 
			
		||||
    "karma-firefox-launcher": "^1.1.0",
 | 
			
		||||
    "jszip": "^3.2.2",
 | 
			
		||||
    "karma": "^4.2.0",
 | 
			
		||||
    "karma-firefox-launcher": "^1.2.0",
 | 
			
		||||
    "karma-html2js-preprocessor": "^1.1.0",
 | 
			
		||||
    "karma-mocha": "^1.3.0",
 | 
			
		||||
    "karma-mocha-reporter": "^2.2.5",
 | 
			
		||||
    "karma-sinon": "^1.0.5",
 | 
			
		||||
    "karma-sourcemap-loader": "^0.3.7",
 | 
			
		||||
    "karma-webpack": "^3.0.5",
 | 
			
		||||
    "karma-webpack": "^4.0.2",
 | 
			
		||||
    "lanthan": "git+https://github.com/ueokande/lanthan.git#master",
 | 
			
		||||
    "mocha": "^6.1.4",
 | 
			
		||||
    "mocha": "^6.2.0",
 | 
			
		||||
    "node-sass": "^4.12.0",
 | 
			
		||||
    "react": "^16.8.6",
 | 
			
		||||
    "react-dom": "^16.8.6",
 | 
			
		||||
    "react-redux": "^7.0.3",
 | 
			
		||||
    "react-test-renderer": "^16.8.6",
 | 
			
		||||
    "redux": "^4.0.1",
 | 
			
		||||
    "react": "^16.9.0",
 | 
			
		||||
    "react-dom": "^16.9.0",
 | 
			
		||||
    "react-redux": "^7.1.1",
 | 
			
		||||
    "react-test-renderer": "^16.9.0",
 | 
			
		||||
    "redux": "^4.0.4",
 | 
			
		||||
    "redux-promise": "^0.6.0",
 | 
			
		||||
    "reflect-metadata": "^0.1.13",
 | 
			
		||||
    "sass-loader": "^7.1.0",
 | 
			
		||||
    "sinon": "^7.3.2",
 | 
			
		||||
    "sass-loader": "^7.3.1",
 | 
			
		||||
    "sinon": "^7.4.1",
 | 
			
		||||
    "sinon-chrome": "^3.0.1",
 | 
			
		||||
    "style-loader": "^0.23.1",
 | 
			
		||||
    "ts-loader": "^6.0.1",
 | 
			
		||||
    "tsyringe": "^3.2.0",
 | 
			
		||||
    "typescript": "^3.4.5",
 | 
			
		||||
    "web-ext-types": "^3.1.0",
 | 
			
		||||
    "style-loader": "^1.0.0",
 | 
			
		||||
    "ts-loader": "^6.0.4",
 | 
			
		||||
    "tsyringe": "^3.3.0",
 | 
			
		||||
    "typescript": "^3.6.2",
 | 
			
		||||
    "web-ext-types": "^3.2.1",
 | 
			
		||||
    "webextensions-api-fake": "^0.8.0",
 | 
			
		||||
    "webpack": "^4.32.2",
 | 
			
		||||
    "webpack-cli": "^3.3.2"
 | 
			
		||||
    "webpack": "^4.39.2",
 | 
			
		||||
    "webpack-cli": "^3.3.7"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,14 +22,14 @@ export default class ContentMessageClient {
 | 
			
		|||
    return enabled as any as boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleAddonEnabled(tabId: number): Promise<void> {
 | 
			
		||||
    return browser.tabs.sendMessage(tabId, {
 | 
			
		||||
  async toggleAddonEnabled(tabId: number): Promise<void> {
 | 
			
		||||
    await browser.tabs.sendMessage(tabId, {
 | 
			
		||||
      type: messages.ADDON_TOGGLE_ENABLED,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollTo(tabId: number, x: number, y: number): Promise<void> {
 | 
			
		||||
    return browser.tabs.sendMessage(tabId, {
 | 
			
		||||
  async scrollTo(tabId: number, x: number, y: number): Promise<void> {
 | 
			
		||||
    await browser.tabs.sendMessage(tabId, {
 | 
			
		||||
      type: messages.TAB_SCROLL_TO,
 | 
			
		||||
      x,
 | 
			
		||||
      y,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,11 +26,20 @@ export default class NotifyPresenter {
 | 
			
		|||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyInvalidSettings(): Promise<void> {
 | 
			
		||||
  async notifyInvalidSettings(onclick: () => void): Promise<void> {
 | 
			
		||||
    let title = `Loaded settings is invalid`;
 | 
			
		||||
    // eslint-disable-next-line max-len
 | 
			
		||||
    let message = 'The default settings is used due to the last saved settings is invalid.  Check your current settings from the add-on preference';
 | 
			
		||||
 | 
			
		||||
    const listener = (id: string) => {
 | 
			
		||||
      if (id !== NOTIFICATION_ID_INVALID_SETTINGS) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      onclick();
 | 
			
		||||
      browser.notifications.onClicked.removeListener(listener);
 | 
			
		||||
    };
 | 
			
		||||
    browser.notifications.onClicked.addListener(listener);
 | 
			
		||||
 | 
			
		||||
    await browser.notifications.create(NOTIFICATION_ID_INVALID_SETTINGS, {
 | 
			
		||||
      'type': 'basic',
 | 
			
		||||
      'iconUrl': browser.extension.getURL('resources/icon_48x48.png'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,12 @@ export default class SettingUseCase {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  async reload(): Promise<Settings> {
 | 
			
		||||
    let data = await this.persistentSettingRepository.load();
 | 
			
		||||
    let data;
 | 
			
		||||
    try {
 | 
			
		||||
      data = await this.persistentSettingRepository.load();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.showUnableToLoad(e);
 | 
			
		||||
    }
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      data = DefaultSettingData;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -30,10 +35,17 @@ export default class SettingUseCase {
 | 
			
		|||
    try {
 | 
			
		||||
      value = data.toSettings();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.notifyPresenter.notifyInvalidSettings();
 | 
			
		||||
      this.showUnableToLoad(e);
 | 
			
		||||
      value = DefaultSettingData.toSettings();
 | 
			
		||||
    }
 | 
			
		||||
    this.settingRepository.update(value!!);
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private showUnableToLoad(e: Error) {
 | 
			
		||||
    console.error('unable to load settings', e);
 | 
			
		||||
    this.notifyPresenter.notifyInvalidSettings(() => {
 | 
			
		||||
      browser.runtime.openOptionsPage();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,12 +25,19 @@ class PropertiesForm extends React.Component<Props> {
 | 
			
		|||
        Object.keys(types).map((name) => {
 | 
			
		||||
          let type = types[name];
 | 
			
		||||
          let inputType = '';
 | 
			
		||||
          let onChange = this.bindValue.bind(this);
 | 
			
		||||
          if (type === 'string') {
 | 
			
		||||
            inputType = 'text';
 | 
			
		||||
          } else if (type === 'number') {
 | 
			
		||||
            inputType = 'number';
 | 
			
		||||
          } else if (type === 'boolean') {
 | 
			
		||||
            inputType = 'checkbox';
 | 
			
		||||
 | 
			
		||||
            // Settings are saved onBlur, but checkbox does not fire it
 | 
			
		||||
            onChange = (e) => {
 | 
			
		||||
              this.bindValue(e);
 | 
			
		||||
              this.props.onBlur();
 | 
			
		||||
            };
 | 
			
		||||
          } else {
 | 
			
		||||
            return null;
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +47,7 @@ class PropertiesForm extends React.Component<Props> {
 | 
			
		|||
              <input type={inputType} name={name}
 | 
			
		||||
                className='column-input'
 | 
			
		||||
                value={value[name] ? value[name] : ''}
 | 
			
		||||
                onChange={this.bindValue.bind(this)}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                onBlur={this.props.onBlur}
 | 
			
		||||
                checked={value[name]}
 | 
			
		||||
              />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,12 @@ export const load = async(): Promise<SettingData> => {
 | 
			
		|||
  if (!settings) {
 | 
			
		||||
    return DefaultSettingData;
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    return SettingData.valueOf(settings as any);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error('unable to load settings', e);
 | 
			
		||||
    return DefaultSettingData;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const save = (data: SettingData) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,13 +21,6 @@ export default interface Settings {
 | 
			
		|||
  blacklist: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DefaultProperties: Properties = PropertyDefs.defs.reduce(
 | 
			
		||||
  (o: {[name: string]: PropertyDefs.Type}, def) => {
 | 
			
		||||
    o[def.name] = def.defaultValue;
 | 
			
		||||
    return o;
 | 
			
		||||
  }, {}) as Properties;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const keymapsValueOf = (o: any): Keymaps => {
 | 
			
		||||
  return Object.keys(o).reduce((keymaps: Keymaps, key: string): Keymaps => {
 | 
			
		||||
    let op = operations.valueOf(o[key]);
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +75,7 @@ export const propertiesValueOf = (o: any): Properties => {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    ...DefaultProperties,
 | 
			
		||||
    ...PropertyDefs.defaultValues,
 | 
			
		||||
    ...o,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -368,7 +368,9 @@ export type Operation =
 | 
			
		|||
const assertOptionalBoolean = (obj: any, name: string) => {
 | 
			
		||||
  if (Object.prototype.hasOwnProperty.call(obj, name) &&
 | 
			
		||||
      typeof obj[name] !== 'boolean') {
 | 
			
		||||
    throw new TypeError(`Not a boolean parameter: '${name}'`);
 | 
			
		||||
    throw new TypeError(
 | 
			
		||||
      `Not a boolean parameter: '${name} (${typeof obj[name]})'`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -376,7 +378,9 @@ const assertOptionalString = (obj: any, name: string, values?: string[]) => {
 | 
			
		|||
  if (Object.prototype.hasOwnProperty.call(obj, name)) {
 | 
			
		||||
    let value = obj[name];
 | 
			
		||||
    if (typeof value !== 'string') {
 | 
			
		||||
      throw new TypeError(`Not a string parameter: '${name}'`);
 | 
			
		||||
      throw new TypeError(
 | 
			
		||||
        `Not a string parameter: '${name}' (${typeof value})`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (values && values.length && values.indexOf(value) === -1) {
 | 
			
		||||
      // eslint-disable-next-line max-len
 | 
			
		||||
| 
						 | 
				
			
			@ -421,32 +425,32 @@ export const valueOf = (o: any): Operation => {
 | 
			
		|||
    assertOptionalBoolean(o, 'background');
 | 
			
		||||
    return {
 | 
			
		||||
      type: FOLLOW_START,
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
 | 
			
		||||
      background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === 'undefined' ? false : o.newTab),
 | 
			
		||||
      background: Boolean(typeof o.background === 'undefined' ? true : o.background), // eslint-disable-line max-len
 | 
			
		||||
    };
 | 
			
		||||
  case PAGE_HOME:
 | 
			
		||||
    assertOptionalBoolean(o, 'newTab');
 | 
			
		||||
    return {
 | 
			
		||||
      type: PAGE_HOME,
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === 'undefined' ? false : o.newTab),
 | 
			
		||||
    };
 | 
			
		||||
  case TAB_CLOSE:
 | 
			
		||||
    assertOptionalString(o, 'select', ['left', 'right']);
 | 
			
		||||
    return {
 | 
			
		||||
      type: TAB_CLOSE,
 | 
			
		||||
      select: (typeof o.select === undefined ? 'right' : o.select),
 | 
			
		||||
      select: (typeof o.select === 'undefined' ? 'right' : o.select),
 | 
			
		||||
    };
 | 
			
		||||
  case TAB_RELOAD:
 | 
			
		||||
    assertOptionalBoolean(o, 'cache');
 | 
			
		||||
    return {
 | 
			
		||||
      type: TAB_RELOAD,
 | 
			
		||||
      cache: Boolean(typeof o.cache === undefined ? false : o.cache),
 | 
			
		||||
      cache: Boolean(typeof o.cache === 'undefined' ? false : o.cache),
 | 
			
		||||
    };
 | 
			
		||||
  case URLS_PASTE:
 | 
			
		||||
    assertOptionalBoolean(o, 'newTab');
 | 
			
		||||
    return {
 | 
			
		||||
      type: URLS_PASTE,
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === 'undefined' ? false : o.newTab),
 | 
			
		||||
    };
 | 
			
		||||
  case INTERNAL_OPEN_URL:
 | 
			
		||||
    assertOptionalBoolean(o, 'newTab');
 | 
			
		||||
| 
						 | 
				
			
			@ -456,9 +460,9 @@ export const valueOf = (o: any): Operation => {
 | 
			
		|||
    return {
 | 
			
		||||
      type: INTERNAL_OPEN_URL,
 | 
			
		||||
      url: o.url,
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
 | 
			
		||||
      newWindow: Boolean(typeof o.newWindow === undefined ? false : o.newWindow), // eslint-disable-line max-len
 | 
			
		||||
      background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len
 | 
			
		||||
      newTab: Boolean(typeof o.newTab === 'undefined' ? false : o.newTab),
 | 
			
		||||
      newWindow: Boolean(typeof o.newWindow === 'undefined' ? false : o.newWindow), // eslint-disable-line max-len
 | 
			
		||||
      background: Boolean(typeof o.background === 'undefined' ? true : o.background), // eslint-disable-line max-len
 | 
			
		||||
    };
 | 
			
		||||
  case CANCEL:
 | 
			
		||||
  case ADDON_ENABLE:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,3 +48,9 @@ export const defs: Def[] = [
 | 
			
		|||
    'which are completed at the open page',
 | 
			
		||||
    'sbh'),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const defaultValues = {
 | 
			
		||||
  hintchars: 'abcdefghijklmnopqrstuvwxyz',
 | 
			
		||||
  smoothscroll: false,
 | 
			
		||||
  complete: 'sbh',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue