Repository: manusa/electronim
Pull request #21 (2020-07-05 17:00:30)

Tabs share **can** have isolated context(s)

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=SECURITY_HOTSPOT) [1 Security Hotspot](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=21&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='95.9%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=21&metric=new_coverage&view=list) [95.9% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=21&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=21&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=21&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/README.md b/README.md
index 1b1a29a..b2686af 100644
--- a/README.md
+++ b/README.md
@@ -65,3 +65,4 @@ spell checking for free.
 - [Font Awesome Free](https://github.com/FortAwesome/Font-Awesome)
 - [Nodehun](https://github.com/Wulf/nodehun/)
 - [Woorm's dictionary repo](https://github.com/wooorm/dictionaries)
+- [Preact](https://preactjs.com)
diff --git a/docs/Roadmap.md b/docs/Roadmap.md
new file mode 100644
index 0000000..60e18c6
--- /dev/null
+++ b/docs/Roadmap.md
@@ -0,0 +1,24 @@
+# ElectronIM Roadmap
+
+## v1.0.0
+
+### Internationalization
+
+- String literals retrieved using translation library
+
+### Notification handling
+
+- Notifications can be snoozed #3
+- Private notifications (hidden content) #4
+
+### Electron
+
+- Screen sharing #12
+
+### NFR
+
+- Better support for Snap (#20 ...)
+- Embed dictionaries in repository // Pre-download dictionaries from original sources (Chromium, LibreOffice, etc.) 
+- Replace current templates to use [preact](https://github.com/preactjs/preact) instead.
+- ~~Evaluate migration to use template engines such as [lit-html](https://github.com/Polymer/lit-html)
+  or [hyperHTML](https://github.com/WebReflection/hyperHTML). Or maybe use [preact](https://preactjs.com/) instead.~~
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 2b29048..3f6fbf1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -358,6 +358,25 @@
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/runtime": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz",
+      "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==",
+      "dev": true,
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
+    "@babel/runtime-corejs3": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz",
+      "integrity": "sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==",
+      "dev": true,
+      "requires": {
+        "core-js-pure": "^3.0.0",
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
     "@babel/template": {
       "version": "7.8.6",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
@@ -723,6 +742,67 @@
         "defer-to-connect": "^1.0.1"
       }
     },
+    "@testing-library/dom": {
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.20.0.tgz",
+      "integrity": "sha512-TywaC+qDGm/Ro34kRYkFQPdT+pxSF4UjZGLIqcGfFQH5IGR43Y7sGLPnkieIW/GNsu337oxNsLUAgpI0JWhXHw==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.10.3",
+        "@types/aria-query": "^4.2.0",
+        "aria-query": "^4.2.2",
+        "dom-accessibility-api": "^0.4.5",
+        "pretty-format": "^25.5.0"
+      },
+      "dependencies": {
+        "@jest/types": {
+          "version": "25.5.0",
+          "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
+          "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
+          "dev": true,
+          "requires": {
+            "@types/istanbul-lib-coverage": "^2.0.0",
+            "@types/istanbul-reports": "^1.1.1",
+            "@types/yargs": "^15.0.0",
+            "chalk": "^3.0.0"
+          }
+        },
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "pretty-format": {
+          "version": "25.5.0",
+          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
+          "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
+          "dev": true,
+          "requires": {
+            "@jest/types": "^25.5.0",
+            "ansi-regex": "^5.0.0",
+            "ansi-styles": "^4.0.0",
+            "react-is": "^16.12.0"
+          }
+        }
+      }
+    },
+    "@types/aria-query": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz",
+      "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==",
+      "dev": true
+    },
     "@types/babel__core": {
       "version": "7.1.7",
       "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz",
@@ -1116,6 +1196,16 @@
         }
       }
     },
+    "aria-query": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
+      "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.10.2",
+        "@babel/runtime-corejs3": "^7.10.2"
+      }
+    },
     "arr-diff": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -1886,6 +1976,12 @@
       "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
       "optional": true
     },
+    "core-js-pure": {
+      "version": "3.6.5",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz",
+      "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==",
+      "dev": true
+    },
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -2256,6 +2352,12 @@
         "esutils": "^2.0.2"
       }
     },
+    "dom-accessibility-api": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz",
+      "integrity": "sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg==",
+      "dev": true
+    },
     "domexception": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
@@ -3619,6 +3721,11 @@
         "lru-cache": "^5.1.1"
       }
     },
+    "htm": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz",
+      "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
+    },
     "html-encoding-sniffer": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -5493,6 +5600,11 @@
       "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
       "dev": true
     },
+    "preact": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.4.5.tgz",
+      "integrity": "sha512-/GyQOM44xNbM1nx1NWWdEO9RFjOVl3Ji6HTnEP+y9OkfyvJDHXnWJPQnuknrslzu5lZfAtFavS8gka91fbFAPg=="
+    },
     "prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5709,6 +5821,12 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "regenerator-runtime": {
+      "version": "0.13.5",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
+      "dev": true
+    },
     "regex-not": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
diff --git a/package.json b/package.json
index de26a6b..68b456c 100644
--- a/package.json
+++ b/package.json
@@ -135,6 +135,7 @@
     }
   },
   "devDependencies": {
+    "@testing-library/dom": "^7.20.0",
     "electron-builder": "^22.6.1",
     "electron-packager": "^14.2.1",
     "eslint": "^7.1.0",
@@ -167,6 +168,8 @@
     "dictionary-uk": "^2.1.1",
     "draggabilly": "^2.2.0",
     "electron": "^9.0.0",
-    "nodehun": "^3.0.1"
+    "htm": "^3.0.4",
+    "nodehun": "^3.0.1",
+    "preact": "^10.4.5"
   }
 }
diff --git a/src/main/index.js b/src/main/index.js
index d7ddfd2..81ba09e 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -96,8 +96,8 @@ const closeSettings = () => {
   settingsView.destroy();
 };
 
-const saveSettings = (event, {tabs, dictionaries}) => {
-  updateSettings({enabledDictionaries: [...dictionaries]});
+const saveSettings = (event, {tabs, enabledDictionaries}) => {
+  updateSettings({enabledDictionaries: [...enabledDictionaries]});
   updateTabUrls(tabs);
   loadDictionaries();
   const currentBrowserView = mainWindow.getBrowserView();
diff --git a/src/settings/browser-settings.css b/src/settings/browser-settings.css
index 9d929d3..84cd29c 100644
--- a/src/settings/browser-settings.css
+++ b/src/settings/browser-settings.css
@@ -45,3 +45,7 @@ body {
     flex-wrap: wrap;
     max-height: 100px;
 }
+
+.settings__dictionaries .control input[type="checkbox"] {
+    margin-right: 6px;
+}
diff --git a/src/settings/browser-settings.js b/src/settings/browser-settings.js
index cc190c8..a490c44 100644
--- a/src/settings/browser-settings.js
+++ b/src/settings/browser-settings.js
@@ -14,14 +14,10 @@
    limitations under the License.
  */
 /* eslint-disable no-undef */
+const {h, render} = window.preact;
+const {useReducer} = window.preactHooks;
+const html = window.htm.bind(h);
 const settings = () => document.querySelector('.settings');
-const settingsForm = () => settings().querySelector('.form');
-const newTabField = () => settings().querySelector('.settings__new-tab .input');
-const newTabAddButton = () => settings().querySelector('.settings__new-tab .button');
-const tabs = () => settings().querySelector('.settings__tabs');
-const dictionaries = () => settings().querySelector('.settings__dictionaries');
-const submitButton = () => settings().querySelector('.settings__submit');
-const cancelButton = () => settings().querySelector('.settings__cancel');
 
 const prependProtocol = url => {
   if (url && !url.match(/^https?:\/\/.+/)) {
@@ -43,116 +39,188 @@ const validateUrl = url => {
   return false;
 };
 
-const validateNewTab = () => {
-  const newTabInputElement = newTabField();
-  const newTabAddButtonElement = newTabAddButton();
-  newTabInputElement.classList.remove('is-success', 'is-danger');
-  newTabAddButtonElement.setAttribute('disabled', 'disabled');
-  if (newTabInputElement.value.length > 0 && validateUrl(newTabInputElement.value)) {
-    newTabInputElement.classList.add('is-success');
-    newTabAddButtonElement.removeAttribute('disabled');
-  } else if (newTabInputElement.value.length > 0) {
-    newTabInputElement.classList.add('is-danger');
+const newTabClass = state => {
+  if (state.newTabValue.length === 0) {
+    return '';
   }
+  return state.newTabValid ? 'is-success' : 'is-danger';
 };
 
-const updateSaveButton = () => {
-  let enabled = true;
-  if (newTabField().value.length > 0) {
-    enabled = false;
-  }
-  if (new FormData(settingsForm()).getAll('tabs').length === 0) {
-    enabled = false;
-  }
-  if (enabled) {
-    submitButton().removeAttribute('disabled');
-  } else {
-    submitButton().setAttribute('disabled', 'true');
-  }
+const newId = () => (new Date().getTime().toString(36) + Math.random().toString(36).slice(2));
+
+const initialState = {
+  dictionaries: window.dictionaries,
+  tabs: window.tabs,
+  newTabValid: false,
+  newTabValue: '',
+  canSave: window.tabs.length > 0
 };
 
-const initTabsListener = () => {
-  const tabsElement = tabs();
-  const content = tabsElement.innerHTML;
-  tabsElement.innerHTML = '';
-  tabsElement.innerHTML = content;
-  tabsElement.querySelectorAll('.icon')
-    .forEach(icon => icon.addEventListener('click', ({target}) => {
-      target.closest('.settings__tab').remove();
-      updateSaveButton();
-    }));
+const ACTIONS = {
+  ADD: 'ADD',
+  REMOVE: 'REMOVE',
+  TOGGLE_DICTIONARY: 'TOGGLE_DICTIONARY',
+  TOGGLE_TAB_SANDBOX: 'TOGGLE_TAB_SANDBOX',
+  UPDATE_NEW_TAB_VALUE: 'UPDATE_NEW_TAB_VALUE'
 };
 
-const tabTemplate = url => `
-    <div class='field settings__tab'>
-      <div class='control'>
-          <input type='text' readonly class='input' name='tabs' value='${url}' />
-      </div>
-      <span class='icon is-medium'>
-        <i class='fas fa-trash'></i>
-      </span>
-    </div>
-  `;
+const dictionariesEnabled = state => state.dictionaries.enabled;
+const dictionariesAvailable = state => state.dictionaries.available;
+
+const reducer = (state, action) => {
+  switch (action.type) {
+    case ACTIONS.ADD: {
+      if (!validateUrl(state.newTabValue)) {
+        return {...state};
+      }
+      return {...state,
+        newTabValid: false,
+        newTabValue: '',
+        tabs: [...state.tabs, {
+          id: newId(),
+          sandboxed: false,
+          url: prependProtocol(state.newTabValue)
+        }],
+        canSave: true
+      };
+    }
+    case ACTIONS.REMOVE: {
+      return {...state,
+        tabs: state.tabs.filter(tab => tab.id !== action.payload.id),
+        canSave: state.tabs.filter(tab => tab.id !== action.payload.id).length > 0
+      };
+    }
+    case ACTIONS.TOGGLE_DICTIONARY: {
+      const newState = {...state};
+      if (dictionariesEnabled(newState).includes(action.payload)) {
+        newState.dictionaries.enabled = [...dictionariesEnabled(newState)
+          .filter(key => key !== action.payload)];
+      } else {
+        newState.dictionaries.enabled = [...dictionariesEnabled(newState), action.payload];
+      }
+      return newState;
+    }
+    case ACTIONS.TOGGLE_TAB_SANDBOX: {
+      const newState = {...state, tabs: []};
+      state.tabs.forEach(tab => {
+        const newTab = {...tab};
+        if (newTab.id === action.payload.id) {
+          newTab.sandboxed = !newTab.sandboxed;
+        }
+        newState.tabs.push(newTab);
+      });
+      return newState;
+    }
+    case ACTIONS.UPDATE_NEW_TAB_VALUE: {
+      return {...state,
+        newTabValid: validateUrl(action.payload),
+        newTabValue: action.payload,
+        canSave: action.payload.length === 0 && state.tabs.length > 0
+      };
+    }
+    default: return {...state};
+  }
+};
 
-const initNewTab = () => {
-  newTabField().addEventListener('input', () => {
-    validateNewTab();
-    updateSaveButton();
+const TabEntry = ({dispatch, id, url, sandboxed}) => (html`
+  <div class='field settings__tab'>
+    <div class='control'>
+      <input type='text' readonly class='input' name='tabs' value='${url}' />
+    </div>
+    <span class='icon is-medium sandbox' title='Use isolated session when lock is on'
+      onclick=${() => dispatch({type: ACTIONS.TOGGLE_TAB_SANDBOX, payload: {id}})}>
+      <i class='fas ${sandboxed ? 'fa-lock' : 'fa-lock-open'}'></i>
+    </span>
+    <span class='icon is-medium' title='Delete tab'
+      onclick=${() => dispatch({type: ACTIONS.REMOVE, payload: {id}})}>
+      <i class='fas fa-trash'></i>
+    </span>
+  </div>
+`);
+
+const DictionaryEntry = ({dispatch, dictionaryKey, name, enabled = false}) => (html`
+  <div class='control'>
+    <label class='checkbox'>
+        <input type='checkbox' name='dictionaries'
+          value='${dictionaryKey}' checked=${enabled}
+          onclick=${() => dispatch({type: ACTIONS.TOGGLE_DICTIONARY, payload: dictionaryKey})}
+        />
+        ${name} (${dictionaryKey})
+    </label>
+  </div>
+`);
+
+const Settings = () => {
+  const [state, dispatch] = useReducer(reducer, initialState);
+  const enabledDictionaries = dictionariesEnabled(state);
+  const availableDictionaries = dictionariesAvailable(state);
+  const onNewTabInput = ({target: {value}}) => dispatch({
+    type: ACTIONS.UPDATE_NEW_TAB_VALUE,
+    payload: value
   });
-  const addTab = () => {
-    const newTabFieldElement = newTabField();
-    tabs().innerHTML += tabTemplate(prependProtocol(newTabFieldElement.value));
-    initTabsListener();
-    newTabFieldElement.value = '';
-    validateNewTab();
-    updateSaveButton();
-  };
-  newTabField().addEventListener('keypress', event => {
-    const {target} = event;
-    if (event.code === 'Enter' && validateUrl(target.value)) {
-      event.preventDefault();
+  const addTab = () => dispatch({type: ACTIONS.ADD});
+  const onNewKeyDown = ({code}) => {
+    if (code === 'Enter') {
       addTab();
     }
+  };
+  const save = () => ipcRenderer.send(APP_EVENTS.settingsSave, {
+    tabs: state.tabs,
+    enabledDictionaries
   });
-  newTabAddButton().addEventListener('click', addTab);
-};
-
-const initTabsSettings = () => {
-  tabs().innerHTML = window.tabs.map(({url}) => url).map(tabTemplate).join('');
-  initTabsListener();
-};
-
-const initSpellCheckerSettings = () => {
-  const {available, enabled} = window.dictionaries;
-  dictionaries().innerHTML = Object.entries(available)
+  const cancel = () => ipcRenderer.send(APP_EVENTS.settingsCancel);
+  return html`
+  <h1 class="title">Settings</h1>
+  <div class="container">
+    <div class="form">
+      <label class="label">Tabs</label>
+      <div class="settings__new-tab container field">
+        <div class="control">
+          <input type="text"
+            class="input ${newTabClass(state)}"
+            placeholder="https://web.whatsapp.com"
+            value=${state.newTabValue}
+            oninput=${onNewTabInput}
+            onkeydown=${onNewKeyDown}
+          />
+        </div>
+        <button class="button" disabled=${!state.newTabValid} onclick=${addTab}>
+          <span class='icon is-medium'>
+            <em class='fas fa-plus'></em>
+          </span>
+        </button>
+      </div>
+      <div class="settings__tabs container field">
+        ${state.tabs.map(tab => (html`
+          <${TabEntry} dispatch=${dispatch} ...${tab} />
+      `))}
+      </div>
+      <div class="field">
+        <label class="label">Spell checker languages</label>
+        <div class="settings__dictionaries container">${
+  Object.entries(availableDictionaries)
     .sort(([, {name: name1}], [, {name: name2}]) => name1.localeCompare(name2))
-    .map(([key, {name}]) => `
-      <div class='control'>
-        <label class='checkbox'>
-            <input type='checkbox' name='dictionaries' value='${key}' ${enabled.includes(key) ? 'checked' : ''}>
-            ${name}
-        </label>
+    .map(([key, {name}]) => (html`
+      <${DictionaryEntry} dispatch=${dispatch} dictionaryKey=${key} name=${name}
+        enabled=${enabledDictionaries.includes(key)}
+      />
+    `))} 
+        </div>
       </div>
-    `).join('');
-};
-
-const initButtons = () => {
-  submitButton().addEventListener('click', () => {
-    const formData = new FormData(settingsForm());
-    ipcRenderer.send(APP_EVENTS.settingsSave, {
-      tabs: formData.getAll('tabs'),
-      dictionaries: formData.getAll('dictionaries')
-    });
-  });
-  updateSaveButton();
-
-  cancelButton().addEventListener('click', () => ipcRenderer.send(APP_EVENTS.settingsCancel));
-};
-
-const init = () => {
-  settingsForm().addEventListener('keypress', event => (event.code === 'Enter' ? event.preventDefault() : true));
-  settingsForm().addEventListener('submit', event => event.preventDefault());
-  [initNewTab, initSpellCheckerSettings, initTabsSettings, initButtons].forEach(f => f.call(this));
+      <div class="field is-grouped">
+        <div class="control">
+          <button class="settings__submit button is-link"
+            disabled=${!state.canSave} onclick=${save}>Ok</button>
+      </div>
+        <div class="control">
+          <button class="settings__cancel button is-link is-light" onclick=${cancel}>
+            Cancel
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+`;
 };
 
-init();
+render(html`<${Settings} />`, settings());
diff --git a/src/settings/index.html b/src/settings/index.html
index 4fcd860..dd9b1a7 100644
--- a/src/settings/index.html
+++ b/src/settings/index.html
@@ -24,37 +24,7 @@ limitations under the License.
         <link rel="stylesheet" href="./browser-settings.css">
     </head>
     <body>
-        <div class="settings container is-fluid">
-            <h1 class="title">Settings</h1>
-            <div class="container">
-                <form class="form">
-                    <label class="label">Tabs</label>
-                    <div class="settings__new-tab container field">
-                        <div class="control">
-                            <input class="input" type="text" placeholder="https://web.whatsapp.com">
-                        </div>
-                        <button class="button" disabled>
-                            <span class='icon is-medium'>
-                                <em class='fas fa-plus'></em>
-                            </span>
-                        </button>
-                    </div>
-                    <div class="settings__tabs container field"></div>
-                    <div class="field">
-                        <label class="label">Spell checker languages</label>
-                        <div class="settings__dictionaries container"></div>
-                    </div>
-                    <div class="field is-grouped">
-                        <div class="control">
-                            <button class="settings__submit button is-link">Ok</button>
-                        </div>
-                        <div class="control">
-                            <button class="settings__cancel button is-link is-light">Cancel</button>
-                        </div>
-                    </div>
-                </form>
-            </div>
-        </div>
+        <div class="settings container is-fluid"></div>
         <script src="./browser-settings.js"></script>
     </body>
 </html>
diff --git a/src/settings/index.js b/src/settings/index.js
index efbbd75..f5b5379 100644
--- a/src/settings/index.js
+++ b/src/settings/index.js
@@ -47,12 +47,8 @@ const writeSettings = settings => {
 
 const updateSettings = settings => writeSettings({...loadSettings(), ...settings});
 
-const updateTabUrls = newTabUrls => {
-  const {activeTab, tabs} = loadSettings();
-  const currentTabUrls = tabs.map(({url}) => url);
-  const updatedTabs = [];
-  updatedTabs.push(...tabs.filter(tab => newTabUrls.includes(tab.url)));
-  newTabUrls.filter(url => !currentTabUrls.includes(url)).forEach(url => updatedTabs.push({id: url, url}));
+const updateTabUrls = updatedTabs => {
+  const {activeTab} = loadSettings();
   updateSettings({tabs: [...updatedTabs]});
   if (updatedTabs.length > 0 && !updatedTabs.map(({id}) => id).includes(activeTab)) {
     updateSettings({activeTab: updatedTabs[0].id});
diff --git a/src/settings/preload.js b/src/settings/preload.js
index 2c1ff83..8f6b1a3 100644
--- a/src/settings/preload.js
+++ b/src/settings/preload.js
@@ -13,12 +13,19 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
+const preact = require('preact');
+const preactHooks = require('preact/hooks');
+const htm = require('htm');
 require('../main/preload');
 const {AVAILABLE_DICTIONARIES, getEnabledDictionaries} = require('../spell-check');
 const {loadSettings} = require('./');
 
 const settings = loadSettings();
 
+window.preact = preact;
+window.preactHooks = preactHooks;
+window.htm = htm;
+
 window.dictionaries = {
   available: AVAILABLE_DICTIONARIES,
   enabled: getEnabledDictionaries()
diff --git a/src/tab-manager/index.js b/src/tab-manager/index.js
index a430626..f0d669b 100644
--- a/src/tab-manager/index.js
+++ b/src/tab-manager/index.js
@@ -13,7 +13,7 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-const {app, BrowserView, Menu, MenuItem} = require('electron');
+const {app, BrowserView, Menu, MenuItem, session} = require('electron');
 const {APP_EVENTS} = require('../constants');
 const settings = require('../settings');
 const {contextMenuHandler} = require('../spell-check');
@@ -74,8 +74,12 @@ const cleanUserAgent = (browserView, url) => {
 };
 
 const addTabs = ipcSender => tabsMetadata => {
-  tabsMetadata.forEach(({id, url}) => {
-    const tab = new BrowserView({webPreferences});
+  tabsMetadata.forEach(({id, url, sandboxed = false}) => {
+    const tabPreferences = {...webPreferences};
+    if (sandboxed) {
+      tabPreferences.session = session.fromPartition(`persist:${id}`, {cache: true});
+    }
+    const tab = new BrowserView({webPreferences: tabPreferences});
     tab.setAutoResize({width: true, height: true});
 
     cleanUserAgent(tab, url);


Test patch:
diff --git a/src/main/__tests__/index.test.js b/src/main/__tests__/index.test.js
index 944cb07..b62f86a 100644
--- a/src/main/__tests__/index.test.js
+++ b/src/main/__tests__/index.test.js
@@ -277,7 +277,7 @@ describe('Main module test suite', () => {
       mockTabContainer.destroy = jest.fn();
       main.init();
       // When
-      mockIpc.listeners.settingsSave({}, {tabs: [], dictionaries: []});
+      mockIpc.listeners.settingsSave({}, {tabs: [], enabledDictionaries: []});
       // Then
       expect(spellCheckModule.loadDictionaries).toHaveBeenCalledTimes(2);
       expect(settingsModule.updateSettings).toHaveBeenCalledTimes(1);
diff --git a/src/settings/__tests__/browser-settings.test.js b/src/settings/__tests__/browser-settings.test.js
index b84bb9d..4d40c8f 100644
--- a/src/settings/__tests__/browser-settings.test.js
+++ b/src/settings/__tests__/browser-settings.test.js
@@ -13,23 +13,12 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
+const {fireEvent, waitFor} = require('@testing-library/dom');
+
 const mockDOM = () => {
   document.body.innerHTML = '';
   const $root = document.createElement('div');
-  $root.innerHTML = `
-    <div class="settings">
-        <form class="form">
-            <div class="settings__new-tab">
-                <input class="input" />
-                <button class="button" disabled />
-            </div>
-            <div class="settings__tabs"></div>
-            <div class="settings__dictionaries"></div>
-            <button class="settings__submit" />
-            <button class="settings__cancel" />
-        </form>
-    </div>
-    `;
+  $root.innerHTML = '<div class="settings container is-fluid"></div>';
   document.body.append($root);
 };
 
@@ -41,13 +30,17 @@ describe('Settings in Browser test suite', () => {
     mockDictionaries = {available: {
       en: {name: 'English'},
       es: {name: 'Spanish'}
-    }, enabled: []};
+    }, enabled: ['en']};
     mockTabs = [
-      {url: 'https://initial-tab.com'}
+      {url: 'https://initial-tab.com', sandboxed: true},
+      {url: 'https://initial-tab-2.com'}
     ];
     mockIpcRenderer = {
       send: jest.fn()
     };
+    window.preact = require('preact');
+    window.preactHooks = require('preact/hooks');
+    window.htm = require('htm');
     window.APP_EVENTS = {};
     window.dictionaries = mockDictionaries;
     window.tabs = mockTabs;
@@ -57,151 +50,199 @@ describe('Settings in Browser test suite', () => {
       require('../browser-settings');
     });
   });
-  test('Form submit on enter is prevented', () => {
-    // Given
-    const event = new Event('keypress');
-    event.code = 'Enter';
-    event.preventDefault = jest.fn();
-    // When
-    document.querySelector('.settings .form').dispatchEvent(event);
-    // Then
-    expect(event.preventDefault).toHaveBeenCalledTimes(1);
-  });
-  describe('Button events', () => {
+  describe('Main Button events', () => {
     test('Submit should send form data', () => {
       // Given
       window.APP_EVENTS = {settingsSave: 'Save my settings'};
       // When
-      document.querySelector('.settings__submit').dispatchEvent(new Event('click'));
+      fireEvent.click(document.querySelector('.settings__submit'));
       // Then
       expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
       expect(mockIpcRenderer.send).toHaveBeenCalledWith('Save my settings',
-        {tabs: ['https://initial-tab.com'], dictionaries: []});
+        {tabs: [
+          {url: 'https://initial-tab.com', sandboxed: true},
+          {url: 'https://initial-tab-2.com'}
+        ], enabledDictionaries: ['en']});
     });
     test('Cancel should send cancel event', () => {
       // Given
       window.APP_EVENTS = {settingsCancel: 'Cancel my settings'};
       // When
-      document.querySelector('.settings__cancel').dispatchEvent(new Event('click'));
+      fireEvent.click(document.querySelector('.settings__cancel'));
       // Then
       expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
       expect(mockIpcRenderer.send).toHaveBeenCalledWith('Cancel my settings');
     });
   });
-  describe('New tab field', () => {
+  describe('New tab Input field', () => {
+    let $tabContainer;
     let $input;
     let $addTabButton;
+    let $submitButton;
     beforeEach(() => {
       $input = document.querySelector('.settings__new-tab .input');
       $addTabButton = document.querySelector('.settings__new-tab .button');
+      $tabContainer = document.querySelector('.settings__tabs');
+      $submitButton = document.querySelector('.settings__submit');
     });
-    test('Regular key press (No Enter), should only update input value', () => {
-      // Given
-      const event = new Event('keypress');
-      event.code = 'A';
-      event.preventDefault = jest.fn();
+    test('key* events, Regular key press (No Enter), should only update input value', async () => {
       // When
-      $input.dispatchEvent(event);
+      fireEvent.keyDown($input, {code: 'A'});
+      fireEvent.keyPress($input, {code: 'A'});
+      fireEvent.keyUp($input, {code: 'A'});
+      fireEvent.input($input, {target: {value: 'A'}});
       // Then
-      expect(event.preventDefault).not.toHaveBeenCalled();
+      await waitFor(() => expect($input.value).toBe('A'));
+      expect($tabContainer.childElementCount).toBe(2);
     });
-    describe('Enter key with URLS', () => {
-      let event;
-      beforeEach(() => {
-        event = new Event('keypress');
-        event.code = 'Enter';
-        event.preventDefault = jest.fn();
-      });
-      test('Empty URL, should do nothing', () => {
+    describe('keydown event, Enter key with URLS', () => {
+      test('Empty URL, should do nothing', async () => {
         // Given
-        $input.value = '';
+        fireEvent.input($input, {target: {value: ''}});
         // When
-        $input.dispatchEvent(event);
+        fireEvent.keyDown($input, {code: 'Enter'});
         // Then
-        expect(event.preventDefault).not.toHaveBeenCalled();
+        expect($tabContainer.childElementCount).toBe(2);
+        expect($addTabButton.hasAttribute('disabled')).toBe(true);
+        expect($submitButton.hasAttribute('disabled')).toBe(false);
       });
-      test('Invalid URL, should do nothing', () => {
+      test('Invalid URL, should set input invalid', async () => {
         // Given
-        $input.value = 'invalid:1337:url';
+        fireEvent.input($input, {target: {value: 'invalid:1337:url'}});
         // When
-        $input.dispatchEvent(event);
+        fireEvent.keyDown($input, {code: 'Enter'});
         // Then
-        expect(event.preventDefault).not.toHaveBeenCalled();
+        expect($tabContainer.childElementCount).toBe(2);
+        await waitFor(() =>
+          expect($input.classList.contains('is-danger')).toBe(true));
+        expect($tabContainer.querySelectorAll('.settings__tab input').length).toBe(2);
+        expect($input.value).toBe('invalid:1337:url');
+        expect($addTabButton.hasAttribute('disabled')).toBe(true);
+        expect($submitButton.hasAttribute('disabled')).toBe(true);
       });
-      test('Valid URL without protocol, should add new url', () => {
+      test('Valid URL without protocol, should add new url with default protocol', async () => {
         // Given
-        $input.value = 'info.cern.ch';
+        fireEvent.input($input, {target: {value: 'info.cern.ch'}});
         // When
-        $input.dispatchEvent(event);
+        fireEvent.keyDown($input, {code: 'Enter'});
         // Then
-        expect(event.preventDefault).toHaveBeenCalledTimes(1);
-        expect(document.querySelector('.settings__tabs').innerHTML).toContain('https://info.cern.ch');
+        await waitFor(() =>
+          expect($tabContainer.childElementCount).toBe(3));
+        expect($input.classList.contains('is-success')).toBe(false);
+        expect($tabContainer.querySelectorAll('.settings__tab input')[2].value)
+          .toBe('https://info.cern.ch');
         expect($input.value).toBe('');
-        expect($addTabButton.getAttribute('disabled')).toBe('disabled');
-        expect(document.querySelector('.settings__submit').getAttribute('disabled')).toBeNull();
+        expect($addTabButton.hasAttribute('disabled')).toBe(true);
+        expect($submitButton.hasAttribute('disabled')).toBe(false);
       });
-      test('Valid URL wth protocol, should add new url', () => {
+      test('Valid URL wth protocol, should add new url with specified protocol', async () => {
         // Given
-        $input.value = 'http://info.cern.ch';
+        fireEvent.input($input, {target: {value: 'http://info.cern.ch'}});
         // When
-        $input.dispatchEvent(event);
+        fireEvent.keyDown($input, {code: 'Enter'});
         // Then
-        expect(event.preventDefault).toHaveBeenCalledTimes(1);
-        expect(document.querySelector('.settings__tabs').innerHTML).toContain('http://info.cern.ch');
+        await waitFor(() =>
+          expect($tabContainer.childElementCount).toBe(3));
+        await waitFor(() =>
+          expect($input.classList.contains('is-success')).toBe(false));
+        expect($tabContainer.querySelectorAll('.settings__tab input')[2].value)
+          .toBe('http://info.cern.ch');
         expect($input.value).toBe('');
-        expect($addTabButton.getAttribute('disabled')).toBe('disabled');
-        expect(document.querySelector('.settings__submit').getAttribute('disabled')).toBeNull();
+        expect($addTabButton.hasAttribute('disabled')).toBe(true);
+        expect($submitButton.hasAttribute('disabled')).toBe(false);
       });
     });
     describe('input event', () => {
-      let event;
-      beforeEach(() => {
-        event = new Event('input');
-      });
-      test('Valid URL, should add success class', () => {
-        // Given
-        $input.value = 'http://info.cern.ch';
+      test('Valid URL, should add success class', async () => {
         // When
-        $input.dispatchEvent(event);
+        fireEvent.input($input, {target: {value: 'http://info.cern.ch'}});
         // Then
-        expect($input.classList.contains('is-success')).toBe(true);
-        expect($addTabButton.getAttribute('disabled')).toBeNull();
+        await waitFor(() =>
+          expect($input.classList.contains('is-success')).toBe(true));
         expect($input.classList.contains('is-danger')).toBe(false);
+        expect($addTabButton.hasAttribute('disabled')).toBe(false);
+        expect($submitButton.hasAttribute('disabled')).toBe(true);
       });
-      test('Invalid URL, should add danger class', () => {
-        // Given
-        $input.value = 'http://invalid:1337:url';
+      test('Invalid URL, should add danger class', async () => {
         // When
-        $input.dispatchEvent(event);
+        fireEvent.input($input, {target: {value: 'http://invalid:1337:url'}});
         // Then
-        expect($input.classList.contains('is-danger')).toBe(true);
-        expect($addTabButton.getAttribute('disabled')).toBe('disabled');
+        await waitFor(() =>
+          expect($input.classList.contains('is-danger')).toBe(true));
         expect($input.classList.contains('is-success')).toBe(false);
+        expect($addTabButton.hasAttribute('disabled')).toBe(true);
+        expect($submitButton.hasAttribute('disabled')).toBe(true);
       });
     });
-    test('addTabButton, click with valid URL, should add tab', () => {
+    test('addTabButton, click with valid URL, should add tab', async () => {
       // Given
-      const event = new Event('click');
-      $input.value = 'info.cern.ch';
+      fireEvent.input($input, {target: {value: 'info.cern.ch'}});
       // When
-      $addTabButton.dispatchEvent(event);
+      fireEvent.click($addTabButton);
       // Then
-      expect(document.querySelector('.settings__tabs').innerHTML).toContain('https://info.cern.ch');
+      await waitFor(() =>
+        expect($tabContainer.childElementCount).toBe(3));
+      expect($tabContainer.querySelectorAll('.settings__tab input')[2].value)
+        .toBe('https://info.cern.ch');
       expect($input.value).toBe('');
-      expect($addTabButton.getAttribute('disabled')).toBe('disabled');
-      expect(document.querySelector('.settings__submit').getAttribute('disabled')).toBeNull();
+      expect($addTabButton.hasAttribute('disabled')).toBe(true);
+      expect($submitButton.hasAttribute('disabled')).toBe(false);
     });
   });
   describe('Tab events', () => {
-    test('Icon click, should remove tab', () => {
+    test('Lock icon click, sandboxed session, should unlock', async () => {
+      // Given
+      const $lockIcon = document.querySelector('.settings__tabs .icon .fa-lock');
+      // When
+      fireEvent.click($lockIcon);
+      // Then
+      await waitFor(() =>
+        expect($lockIcon.classList.contains('fa-lock')).toBe(false));
+      expect($lockIcon.classList.contains('fa-lock-open')).toBe(true);
+    });
+    test('Lock-open icon click, sandboxed session, should lock', async () => {
+      // Given
+      const $lockOpenIcon = document.querySelector('.settings__tabs .icon .fa-lock-open');
+      // When
+      fireEvent.click($lockOpenIcon);
+      // Then
+      await waitFor(() =>
+        expect($lockOpenIcon.classList.contains('fa-lock-open')).toBe(false));
+      expect($lockOpenIcon.classList.contains('fa-lock')).toBe(true);
+    });
+    test('Trash icon click, should remove tab', async () => {
+      // Given
+      const $tabContainer = document.querySelector('.settings__tabs');
+      const $trashIcon = $tabContainer.querySelector('.icon .fa-trash');
+      // When
+      fireEvent.click($trashIcon);
+      // Then
+      await waitFor(() =>
+        expect($tabContainer.childElementCount).toBe(0));
+    });
+  });
+  describe('Dictionary events', () => {
+    let $dictionaries;
+    beforeEach(() => {
+      $dictionaries = document.querySelector('.settings__dictionaries');
+    });
+    test('toggle active dictionary, should uncheck dictionary', async () => {
+      // Given
+      const $enDict = $dictionaries.querySelector('input[value=en]');
+      expect($enDict.checked).toBe(true);
+      // When
+      fireEvent.click($enDict);
+      // Then
+      waitFor(() => expect($enDict.checked).toBe(false));
+    });
+    test('toggle inactive dictionary, should check dictionary', async () => {
       // Given
-      const $tabs = document.querySelector('.settings__tabs');
-      expect($tabs.childElementCount).toBe(1);
-      const $icon = $tabs.querySelector('.icon');
+      const $esDict = $dictionaries.querySelector('input[value=es]');
+      expect($esDict.checked).toBe(false);
       // When
-      $icon.dispatchEvent(new Event('click'));
+      fireEvent.click($esDict);
       // Then
-      expect($tabs.childElementCount).toBe(0);
+      waitFor(() => expect($esDict.checked).toBe(true));
     });
   });
 });
diff --git a/src/settings/__tests__/index.test.js b/src/settings/__tests__/index.test.js
index 86abe7c..73a7eb5 100644
--- a/src/settings/__tests__/index.test.js
+++ b/src/settings/__tests__/index.test.js
@@ -109,19 +109,22 @@ describe('Settings module test suite', () => {
       expect(fs.writeFileSync).toHaveBeenCalledWith(path.join('$HOME', '.electronim', 'settings.json'),
         '{\n  "tabs": [],\n  "enabledDictionaries": []\n}');
     });
-    test('New tab URLs provided and existing tabs, should save array with merged tabs', () => {
+    test('New tab URLs provided and existing tabs, should save new tabs', () => {
       // Given
       fs.existsSync.mockImplementation(() => true);
       fs.readFileSync.mockImplementation(
         () => '{"enabledDictionaries":[], "tabs": [{"id": "1", "url": "http://to-be-kept", "otherSetting": 1337}]}');
       // When
-      settings.updateTabUrls(['http://to-be-kept', 'http://new-tab']);
+      settings.updateTabUrls([
+        {id: '1', url: 'http://to-be-kept', sandboxed: true},
+        {id: '1337', url: 'http://new-tab'}
+      ]);
       // Then
       expect(fs.readFileSync).toHaveBeenCalledTimes(3);
       expect(fs.writeFileSync).toHaveBeenCalledWith(path.join('$HOME', '.electronim', 'settings.json'),
         '{\n  "tabs": [\n' +
-        '    {\n      "id": "1",\n      "url": "http://to-be-kept",\n      "otherSetting": 1337\n    },\n' +
-        '    {\n      "id": "http://new-tab",\n      "url": "http://new-tab"\n    }\n  ],\n' +
+        '    {\n      "id": "1",\n      "url": "http://to-be-kept",\n      "sandboxed": true\n    },\n' +
+        '    {\n      "id": "1337",\n      "url": "http://new-tab"\n    }\n  ],\n' +
         '  "enabledDictionaries": []\n}');
     });
   });
diff --git a/src/tab-manager/__tests__/index.test.js b/src/tab-manager/__tests__/index.test.js
index 797402e..cd82747 100644
--- a/src/tab-manager/__tests__/index.test.js
+++ b/src/tab-manager/__tests__/index.test.js
@@ -35,7 +35,8 @@ describe('Tab Manager module test suite', () => {
       app: {},
       BrowserView: jest.fn(() => mockBrowserView),
       Menu: jest.fn(() => mockMenu),
-      MenuItem: jest.fn(() => mockMenuItem)
+      MenuItem: jest.fn(() => mockMenuItem),
+      session: {fromPartition: jest.fn(() => ({}))}
     }));
     jest.mock('../../settings');
     jest.mock('../../spell-check');
@@ -69,6 +70,22 @@ describe('Tab Manager module test suite', () => {
     });
   });
   describe('addTabs', () => {
+    test('not sandboxed, should use shared session', () => {
+      // When
+      tabManager.addTabs({send: jest.fn()})([{id: 1337, url: 'https://localhost'}]);
+      // Then
+      expect(require('electron').session.fromPartition).not.toHaveBeenCalled();
+      expect(require('electron').BrowserView).toBeCalledWith({
+        webPreferences: expect.not.objectContaining({session: expect.anything()})});
+    });
+    test('sandboxed, should use isolated session', () => {
+      // When
+      tabManager.addTabs({send: jest.fn()})([{id: 1337, url: 'https://localhost', sandboxed: true}]);
+      // Then
+      expect(require('electron').session.fromPartition).toHaveBeenCalledTimes(1);
+      expect(require('electron').BrowserView).toBeCalledWith({
+        webPreferences: expect.objectContaining({session: expect.anything()})});
+    });
     test('Tab webContents should be configured and loaded', () => {
       // Given
       const mockIpcSender = {send: jest.fn()};
@@ -88,7 +105,7 @@ describe('Tab Manager module test suite', () => {
       expect(mockBrowserView.webContents.executeJavaScript).toHaveBeenCalledWith('window.tabId = \'1337\';');
     });
     describe('cleanUserAgent', () => {
-      test(' chromium version available, should remove non-standard tokens from user-agent header and set version', () => {
+      test('chromium version available, should remove non-standard tokens from user-agent header and set version', () => {
         // Given
         userAgent.BROWSER_VERSIONS.chromium = '79.0.1337.79';
         // When
@@ -98,7 +115,7 @@ describe('Tab Manager module test suite', () => {
         expect(result).toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) Chrome/79.0.1337.79 Safari/537.36');
         expect(require('electron').app.userAgentFallback).toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) Chrome/79.0.1337.79 Safari/537.36');
       });
-      test(' chromium not version available, should remove non-standard tokens from user-agent header', () => {
+      test('chromium not version available, should remove non-standard tokens from user-agent header', () => {
         // When
         tabManager.addTabs({send: jest.fn()})([{id: 1337, url: 'https://localhost'}]);
         // Then